Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
},
"metadata": {
"description": "Pure-Rust Clean Architecture workflow. Six commands (start, fix, plan, ship, review, install-ci) for axum + sqlx + Dioxus 0.7+ + tokio. Always-latest deps, CI audit gate, anti-slop enforced.",
"version": "4.2.3"
"version": "4.3.1"
},
"plugins": [
{
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

All notable changes to the code-et plugin will be documented in this file.

## [4.3.1] - 2026-05-17

### Fixed — `/code:plan` → `/code:ship` handoff: schema trap, silent rejection, stale-task pollution

A live `/code:plan` session emitted `metadata.user_story = "US-1 | AC-1.1, AC-1.2"`, the PreToolUse hook (`scripts/task-created-tag-check.sh`) correctly rejected it, but the orchestrator silently continued as if the task had been created. A subsequent `/clear` + `/code:ship` then read an empty `TaskList` (only stale items #33–#37 from a prior branch's PRD remained), reported "no queue," and exited. Three compounding bugs, fixed in priority order.

**Bug 1 — schema-doc trap (`commands/plan.md`).** The TaskCreate metadata example showed `"user_story": "US-N | AC-N.M | chore:<reason>"` and `"layer": "domain | application | infrastructure | interface | chore"`, with pipe-alternation in value position. The `|` was meant as a "one-of" reading aid but the planner read it as value composition — concatenating a US tag with the ACs it covers — which the regex rejects. Fix: replace the example values with concrete singular tags (`"US-1"`, `"interface"`) and lift the allowed-forms enumeration into adjacent prose, so the schema can't be copied verbatim into a rejected call.

**Bug 2 — no recovery from hook rejection (`commands/plan.md` §"On TaskCreate rejection").** Phase 3 had no instruction for handling exit-2 from the hook. The orchestrator narrated forward (`"Now wiring task dependencies…"`) on phantom task ids and `TaskUpdate(addBlockedBy)` did nothing visible. Fix: explicit guidance — read `${TMPDIR}/code-et-task-hook/last-rejected.json`, identify the bad field, re-issue the same TaskCreate with corrected metadata, do not call `TaskUpdate` on phantom ids. Three retries on the same field escalates to the user (structural PRD misread, not a typo).

**Bug 3 — silent empty queue in `/code:ship` + cross-branch stale-task pollution (`commands/ship.md` §"Pre-dispatch").** `TaskList` is project-global, not branch-scoped — pending tasks from a merged PRD persist as `pending` on every subsequent branch (the live session showed #33–#37 from `visma-agentic-platform-shell` still pending after that PR's merge in `4f98ac3`). `/code:ship` was dispatching whatever TaskList returned, with no awareness that those tasks belonged to a different PRD; conversely, when *no* tasks tied to the current PRD existed, it exited with "no queue" instead of diagnosing the gap. Fix: resolve the active PRD, parse its `## Story Checklist` for US tags, and scope the dispatch queue to tasks whose `metadata.user_story` matches one of those US/AC tags **or** starts with `chore:` (chores during a feature are this-branch work via `/code:fix`, never stale). The empty-queue branch now distinguishes three cases (PRD exists / no tasks tied; PRD exists / tasks belong to a different PRD; no PRD / no tasks) and emits a specific next-step message for each. Stale tasks from prior branches are left untouched — they belong to their owning branch, not this one.

**Known follow-up.** This release prevents *dispatch* of stale tasks but does not *clear* them — #33–#37 will keep appearing on every `/code:ship` until manually `TaskUpdate`'d to completed. A v4.3.2 task-completion-on-PR-merge hook is the proper fix; until then, run `TaskUpdate(id, status: completed)` on any task whose owning PR has merged.

Files touched: `code-et-implementer/commands/plan.md`, `code-et-implementer/commands/ship.md`, `code-et-implementer/.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json` (also catching up the 4.3.0 marketplace bump that was missed in #64).

## [4.3.0] - 2026-05-13

### Changed — `/code:plan` emits structured `files[]` entries; `/code:ship` consumes them
Expand Down
2 changes: 1 addition & 1 deletion code-et-implementer/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "code",
"version": "4.3.0",
"version": "4.3.1",
"description": "Pure-Rust Clean Architecture workflow. Six commands: start, fix, plan, ship, review, install-ci. Always-latest deps, CI audit gate, anti-slop enforced.",
"author": {
"name": "Kennet Kusk"
Expand Down
25 changes: 23 additions & 2 deletions code-et-implementer/commands/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,21 @@ The list lives in `code-et-implementer/docs/anti-slop.md`; the inline summary ab
],
"expected_outcome": "<observable end-to-end behaviour>",
"rationale": "<1-2 sentences: why this slice exists, the constraint driving it.>",
"user_story": "US-N | AC-N.M | chore:<reason>",
"layer": "domain | application | infrastructure | interface | chore"
"user_story": "US-1",
"layer": "interface"
}
```

**`user_story` — pick exactly one tag, no concatenation.** Allowed forms:

- `US-<N>` — the primary user story this slice delivers (e.g. `"US-1"`).
- `AC-<N>.<M>` — a single acceptance criterion when the slice is narrower than a full story (e.g. `"AC-1.2"`).
- `chore:<reason>` — non-PRD work (e.g. `"chore:bump-deps"`).

Do **not** emit values like `"US-1 | AC-1.1, AC-1.2"` or `"US-1, US-2"`. The pipes/commas in this doc are reading aids, not value separators — the `PreToolUse(TaskCreate)` hook regex matches a single tag and rejects anything else (see `scripts/task-created-tag-check.sh`). If one slice satisfies multiple ACs of the same story, tag it with the story (`"US-<N>"`); the ACs it covers belong in `expected_outcome` / tests, not the tag.

**`layer` — pick exactly one.** Allowed values: `"domain"`, `"application"`, `"infrastructure"`, `"interface"`, `"chore"`. A vertical slice may touch multiple layers via `files[]`, but the task's *primary* layer (the one this tag names) is the innermost layer the slice modifies.

**`files[]` entry shape:**

| Field | Required | Notes |
Expand All @@ -183,6 +193,17 @@ Set dependencies with `TaskUpdate(addBlockedBy)`. Independent slices stay parall

Save manifest to `.claude/${CLAUDE_CODE_TASK_LIST_ID}.json`.

### On `TaskCreate` rejection

The `PreToolUse(TaskCreate)` hook (`scripts/task-created-tag-check.sh`) validates `metadata.user_story` and, on Rust projects, `metadata.layer` before the task is created. **A rejection means the task does NOT exist.** Treat this as a hard stop on that call, never narrate forward:

1. Read the dump at `${TMPDIR:-/tmp}/code-et-task-hook/last-rejected.json` — the `extracted` field shows what the hook actually parsed.
2. Most-common cause: `user_story` contains alternation copied from this doc (`"US-1 | AC-1.1, AC-1.2"`) instead of one tag. Pick a single allowed form (above).
3. Re-issue the same `TaskCreate` with corrected metadata. Do not call `TaskUpdate` on a phantom id — the previous call returned no task.
4. Only proceed to the next slice once the prior `TaskCreate` returned a real task id.

If three retries in a row reject for the same field, stop and surface the rejection payload to the user — the planner has misread something structural in the PRD, not a typo.

### Output

```
Expand Down
27 changes: 27 additions & 0 deletions code-et-implementer/commands/ship.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,33 @@ Loads pending tasks from `TaskList` (or `.claude/${CLAUDE_CODE_TASK_LIST_ID}.jso

If the current branch is `main` or `master`, create `feature/<slug-from-prd-or-tasks>` first.

## Pre-dispatch: scope the queue to this branch's PRD

`TaskList` is global across the project, not branch-scoped — pending tasks from prior PRDs leak into a fresh branch and would otherwise re-execute against stale spec. Before dispatching, scope the queue:

1. Resolve the active PRD:
```
Bash('"${CLAUDE_PLUGIN_ROOT}/scripts/resolve-prd.sh"')
```
Exit 1 = no PRD for this branch → bug lane: ship whatever pending tasks exist (their tags should be `chore:*` from `/code:fix`). Otherwise capture the path.
2. Parse the PRD's `## Story Checklist` to enumerate the US tags that belong to it (`US-1`, `US-2`, …). A pending task belongs to this branch iff one of:
- `metadata.user_story` matches a `US-<N>` in the set, or
- `metadata.user_story` is `AC-<N>.<M>` whose `<N>` is in the set, or
- `metadata.user_story` starts with `chore:` (chores during a feature are this-branch work — `/code:fix` runs against the current tree, not the previous PRD).

All other pending tasks are stale-from-another-branch and **must be skipped** — do not dispatch them, do not mark them completed; leave them for their owning branch.
3. Build the dispatch queue from the scoped subset only.

**Empty-queue diagnostics — never exit silently.** If after scoping the queue is empty, the situation is one of three, each with a distinct message:

| Condition | Surface |
|---|---|
| PRD resolved, no pending tasks match its US tags | `Active PRD: <path>. 0 tasks tied to its user stories. Run /code:plan to decompose the PRD before /code:ship.` |
| PRD resolved, pending tasks exist but all match a *different* PRD | `Active PRD: <path>. Pending tasks (<count>) belong to a different PRD (<their tags>). Either switch branch or run /code:plan on this branch.` |
| No PRD, no pending tasks | `No PRD for this branch and no pending tasks. Nothing to ship — run /code:fix or /code:plan first.` |

In every case, **stop**. Do not invent tasks, do not dispatch the prior branch's queue.

## Dispatch

Every task runs as a forked subagent in its own worktree. Use `Agent` with `isolation: "worktree"`, `subagent_type: "general-purpose"`, and `model: "sonnet"` (Sonnet 4.6 — routine coding tier). **Do not** shell out to `git worktree add` — `isolation: "worktree"` handles it (requires `CLAUDE_CODE_FORK_SUBAGENT=1` on external builds; default-on inside this harness).
Expand Down
Loading