diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 226a607..f5c5eed 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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": [ { diff --git a/CHANGELOG.md b/CHANGELOG.md index f98af04..d312137 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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:"` 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 diff --git a/code-et-implementer/.claude-plugin/plugin.json b/code-et-implementer/.claude-plugin/plugin.json index eaaeee8..1ad5b7a 100644 --- a/code-et-implementer/.claude-plugin/plugin.json +++ b/code-et-implementer/.claude-plugin/plugin.json @@ -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" diff --git a/code-et-implementer/commands/plan.md b/code-et-implementer/commands/plan.md index 339cf71..9a420a1 100644 --- a/code-et-implementer/commands/plan.md +++ b/code-et-implementer/commands/plan.md @@ -163,11 +163,21 @@ The list lives in `code-et-implementer/docs/anti-slop.md`; the inline summary ab ], "expected_outcome": "", "rationale": "<1-2 sentences: why this slice exists, the constraint driving it.>", - "user_story": "US-N | AC-N.M | chore:", - "layer": "domain | application | infrastructure | interface | chore" + "user_story": "US-1", + "layer": "interface" } ``` +**`user_story` — pick exactly one tag, no concatenation.** Allowed forms: + +- `US-` — the primary user story this slice delivers (e.g. `"US-1"`). +- `AC-.` — a single acceptance criterion when the slice is narrower than a full story (e.g. `"AC-1.2"`). +- `chore:` — 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-"`); 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 | @@ -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 ``` diff --git a/code-et-implementer/commands/ship.md b/code-et-implementer/commands/ship.md index 7dfaba7..b1bf555 100644 --- a/code-et-implementer/commands/ship.md +++ b/code-et-implementer/commands/ship.md @@ -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/` 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-` in the set, or + - `metadata.user_story` is `AC-.` whose `` 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: . 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: . Pending tasks () belong to a different PRD (). 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).