--- name: tdd description: Use when starting any TDD cycle, before writing any test file, editing any production file, running tests, or advancing a phase — required before any of those actions. argument-hint: "" --- # TDD: Mandatory MCP Instrumentation Three MCP calls are required protocol. Skipping any one of them is a named violation that corrupts the session record and all downstream metrics. ## For the main agent: dispatch the tdd-task agent If you are the main agent, complete these steps before spawning: 1. Call `inventory (kind: session)({ agentKind: "main", limit: 1 })` — capture the `chat_id` field from the first row as `chatId`. The DB value comes directly from the SessionStart hook payload and is immune to in-memory contamination from prior subagent runs. 2. Generate a `runId`: `` `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}` ``. Do **not** reuse a `runId` across dispatches — a fresh id per dispatch is the invariant. 3. The Task tools are frequently not available in a given session — treat `TaskCreate` as best-effort. When it is available, call `TaskCreate({ subject: "TDD Session: ", description: "Behavior tasks will appear as the orchestrator decomposes the goal." })` and capture the returned task ID as `parentTaskId`. Otherwise, this is the expected path: skip the call and proceed with `parentTaskId` unset — the dispatch and the orchestrator do not depend on it. 4. Initialize: `goalById = new Map()` (keyed by goal ID, each entry `{ ordinal, taskId? }`), `behaviorById = new Map()` (keyed by behavior ID, each entry `{ goalOrdinal, behaviorOrdinal, taskId }`). 5. Spawn `vitest-agent:tdd-task` as a **plain background subagent** — `run_in_background: true`, `subagent_type: "vitest-agent:tdd-task"`, and **no `name`/team argument**. Pass `goal`, `chatId`, `runId`, and `parentTaskId` (if captured — it's optional) in the launch prompt. **Dispatch it as a subagent, never as a named teammate.** An unnamed background subagent fires SubagentStart, which registers the run under a `-subagent--` session key that shares this session's `conversation_id` — so the subagent's `run_tests`/edit artifacts funnel back to the session you passed as `chatId`, and the `tdd_task (action: start)` the orchestrator opens against that `chatId` finds them (evidence-based phase gates pass). A **named teammate** dispatch instead spawns an independent session with its own `conversation_id` and no parent link; the `chatId` you pass then opens the task under this session while the artifacts land under the detached one — a split that makes every `tdd_phase_transition_request` deny with `missing_artifact_evidence`. **If you must dispatch as a named teammate anyway** (issue #144): two mitigations exist, layered on top of the mismatch above rather than replacing the "prefer unnamed subagent" guidance. 1. **Conversation-tree fallback (automatic).** `record-tdd-artifact`'s task lookup now also walks `conversation_id`: when the parent-session walk finds no open task AND the detached session's row shares a non-null `conversation_id` with the session that opened the task, the artifact still resolves there (newest open task wins; a session with `agent_kind = 'main'` is preferred when more than one open task exists across the conversation). This closes the gap for a named-teammate dispatch automatically, with no extra configuration — but it depends on `sessions.conversation_id` actually being populated for both sessions, which is a property of how the session was registered, not something this fallback controls. 2. **Explicit task-id escape hatch (manual, last resort).** Set `VITEST_AGENT_TDD_TASK_ID=` in the subagent's environment. `plugin/hooks/post-tool-use/tdd-artifact.sh` forwards it as `--tdd-task-id` on every `record tdd-artifact` call, which bypasses `chat_id` → session → task resolution (and therefore the conversation-tree fallback too) and writes straight to that task's current open phase. Use this only when you already know the fallback above isn't resolving correctly for the session shape you're operating under — it is a deliberate override, not a default. If a `tdd_phase_transition_request` denial's `remediation.hint` mentions artifacts landing under "a different session of this conversation", that message is telling you to set this variable (or reconsider whether an unnamed subagent dispatch was possible after all). Do not attempt TDD yourself — the tdd-task agent carries the required MCP tools and skill context for evidence-based phase transitions. ### Progress observation (main agent) The orchestrator subagent calls `tdd_progress_push` at every milestone below (unchanged), and every event is persisted server-side regardless of what the main agent observes. The MCP server is Effect-native and cannot emit the custom `notifications/claude/channel` method Claude Code's channel feature listens for — no `` tag is ever delivered to the main agent. Do not wait for one and do not gate any behavior on receiving it. **Primary path: read the subagent's plain-text narration.** The Task-panel tools (`TaskCreate`/`TaskUpdate`) are frequently absent from a given session's tool set — treat text narration as the norm, not a fallback. As the subagent narrates each milestone, relay it to the user in a short sentence keyed to the event class: - **Goals discovered** (`goals_ready`, `goal_added`) — name the goals (or the new one) so the user knows the decomposition. - **A goal or behavior starts** (`goal_started`, `behavior_started`, `behavior_added`) — say which goal/behavior is now active, using the `[G.B] ` label so the user can track it across events. - **A phase transition** (`phase_transition`) — mention the behavior and its new phase (`red`, `green`, `refactor`, etc.). - **A behavior or goal finishes** (`behavior_completed`, `goal_completed`) — confirm completion; for `goal_completed` note the finished behaviors. - **A behavior or goal is abandoned** (`behavior_abandoned`, `goal_abandoned`) — surface the `reason`. - **Blocked** (`blocked`) — surface `reason` and `failureSignatureHash` prominently; this is the one event class worth calling out even mid-narration. - **Session complete** (`session_complete`) — summarize the outcome (`succeeded` / `blocked` / `abandoned`) and the goals covered. **Secondary path: poll `tdd_task (action: get)(id)`** whenever you need the current phase/goals state — for example when the subagent's narration is quiet for a stretch, or before reporting status to the user. It returns the full goal+behavior tree so you can rebuild the shape from a single read. (`tdd_task (action: resume)(id)` returns only a short status summary; reach for `tdd_task (action: get)` when you need the tree.) Between narration and polling, wait for the background completion notification to know the dispatch has finished. ### Optional: mirror into a task panel (only when the host session has the Task tools — often it doesn't) When `TaskCreate`/`TaskUpdate` are available, you may additionally render a task panel from the same channel events, in parallel with the narration above. **The orchestrator's three-tier hierarchy (objective → goals → behaviors) renders flat in the task list** — `TaskCreate` does not nest cleanly past one parent, so encode the goal index in the task label as `[G.B] ` rather than building a 3-level tree. Maintain three pieces of state per session: - `parentTaskId` — the parent `TDD Session: {objective}` task, if one was captured at dispatch (see step 3 above). Optional — leave unset otherwise. - `goalById: Map` — goal metadata. Goal-level events render as marker tasks (e.g. `--- Goal 1 done ---`) inserted between behavior groups; goal status is conveyed via the marker, not a real task. - `behaviorById: Map` — keyed lookup for behavior-level updates. Event → action mapping: | `type` | Action | | ------ | ------ | | `goals_ready` | Record each `{id, ordinal, goal}` in `goalById`. No tasks yet — wait for `behaviors_ready`. | | `goal_added` | Append the new goal to `goalById`. | | `goal_started` | No-op. | | `behaviors_ready` | Record each behavior's ordinals in `behaviorById`. No tasks yet — task creation is deferred to `behavior_started` so abandoned sessions don't leave orphaned pending tasks. | | `behavior_added` | Append `{ goalOrdinal, behaviorOrdinal }` to `behaviorById`. No task yet. | | `behavior_started` | `TaskCreate({ subject: "[G.B] ", description: "...", activeForm: "Running behavior" })` — store the returned task id in `behaviorById`, then `TaskUpdate({ id: , status: "in_progress" })`. | | `phase_transition` | `TaskUpdate({ id: , content: "[G.B] · " })`. | | `behavior_completed` | `TaskUpdate({ id: , status: "completed" })`. | | `behavior_abandoned` | `TaskUpdate({ id: , status: "cancelled" })`; surface the `reason`. | | `goal_completed` | Reconcile against `behaviorIds[]` (mark any still-pending child `completed` — catches dropped `behavior_completed` events). Then `TaskCreate({ content: "--- Goal done ---", status: "completed", parentTaskId })` as a marker. | | `goal_abandoned` | Same reconcile but mark unfinished children `cancelled`. Insert marker `--- Goal abandoned: ---`. | | `blocked` | `TaskUpdate({ id: , status: "blocked" })`; surface `reason` and `failureSignatureHash`. | | `session_complete` | Reconcile against `goalIds[]` (catch dropped `goal_completed` events). `TaskUpdate({ id: parentTaskId, status: "completed" })` (or `cancelled` if outcome is `abandoned`). | ### Session hygiene (main agent) When the run finishes — the background completion notification arrives, or `tdd_task (action: get)` shows the session ended — clean up the agent you dispatched. A plain background subagent completes on its own, but if you spawned a named or otherwise persistent agent, or you are ending the run early, stop it with `TaskStop` so it does not linger idle. Do not leave dispatched agents running once you have their result. If your session has terminal-automation tools available (for example the `it2` iTerm integration), you may use them to give the user a clean visual view of the run — dedicate a pane or window to the orchestrator and tidy it up when the run completes. This is optional and best-effort: reach for it only when those tools are present, and never let window management block or delay the TDD work itself. --- ## Hard Gate 1 — `tdd_task (action: start)` Skipping this gate is the **UNREGISTERED SESSION** violation. This is the first action. Before any file read or write toward the goal: ```text tdd_task (action: start)({ goal, chatId, runId }) ``` Without a session ID there is no TDD session. Every phase artifact is homeless. RED-phase test failures are misclassified as flaky (the DB sees repeated failures with no session context and computes a low pass rate). `acceptance_metrics` returns 0% because zero evidence is bound to any session. The `chatId` and `runId` are normally passed in your launch prompt. When they are, use both exactly as given — do not call `inventory (kind: session)` to derive `chatId` and do not generate a new `runId`; both come from the main agent and bind you to its session. **If your launch prompt has no `chatId`** (a raw or programmatic dispatch that skipped the bootstrap), you are running as a detached session with no parent link. In that case only: recover your *own* `chatId` via `inventory (kind: session)({ agentKind: "main", limit: 1 })` — the first row is your own just-registered session — and generate your own `runId`. Do **not** guess or borrow another session's `chatId`: opening the task against a session whose artifacts you do not produce splits attribution and makes every phase gate deny. Self-bootstrapping into your own session keeps the task and its artifacts co-located, so the gates work. ## Hard Gate 2 — `hypothesis (action: record)` before every production edit Skipping this gate is the **UNCITED FIX** violation. **REQUIRED SUB-SKILL:** `vitest-agent:record-hypothesis-before-fix` Before editing any non-test file, call: ```text hypothesis (action: record)({ content: "", citedTestErrorId: , citedStackFrameId: }) ``` Do **not** pass a `sessionId` here. The MCP server resolves the binding session automatically from the recovered host context (your running subagent session), so any `sessionId` you supply is ignored. Passing the `tddTaskId` under a `sessionId` key — an easy slip after the goal/behavior calls — would mis-attribute the hypothesis if it were honored; the server now prevents it. Both `citedTestErrorId` and `citedStackFrameId` are required — they prove the hypothesis addresses a specific observed failure. A hypothesis without cited evidence is a guess. - "Fix the validation" — not a hypothesis - "The bounds check at line 42 runs after the index access, causing TypeError on index N" — is a hypothesis After the fix: `hypothesis (action: validate)({ id, outcome: "confirmed" | "refuted" | "abandoned" })`. ## Hard Gate 3 — `tdd_phase_transition_request` Skipping this gate is the **UNRECORDED PHASE CHANGE** violation. At every RED→GREEN and GREEN→REFACTOR boundary: ```text tdd_phase_transition_request({ tddTaskId: , goalId: , requestedPhase: "green" | "refactor", citedArtifactId: }) ``` Phase boundaries without MCP confirmation do not exist in the database. The validator enforces evidence-binding rules (D2): the cited artifact must itself have been recorded in the current phase (not replayed from an earlier, closed phase) and its test must have been authored in this session. A test first written and run before you requested red is fine — the artifact you cite just has to come from a run inside red. If the validator denies, read the `remediation` field and act on it before retrying. Do not advance the phase unilaterally. Two cross-behavior moves are first-class — request each in one call with `citedArtifactId` omitted (auto-resolution finds the row): - **Triangulation.** When one implementation satisfies several behaviors, enter `red.triangulate` (not `red`) for each. Later members' tests pass immediately (no own failing run) — request `red.triangulate→green` with the member's `behaviorId`; the batch's real failing run is accepted (phase-window and behavior-match are waived for this transition). Do not skip green with a `red→refactor` jump — the validator denies `red→refactor`, `red.triangulate→refactor`, and `spike→refactor` outright with `denialReason: 'refactor_without_passing_run'`, even when a valid `test_passed_run` from another behavior is cited (issue #361). `refactor` is only reachable from `green` or `green.fake-it`. - **Next behavior.** Cross a behavior boundary with a single `refactor→red` carrying the **new** `behaviorId`; the prior behavior's `test_passed_run` is accepted because `refactor→red` does not enforce behavior-match. No `refactor→red`-then-`red→red` rebind dance. --- ## Observed Rationalizations (baseline session, 2026-05-04) These are the exact behaviors from the previous orchestrator session. All four are violations: | What the orchestrator did | Named violation | Consequence | | --- | --- | --- | | Fixed bugs without calling `tdd_task (action: start)` | UNREGISTERED SESSION | `acceptance_metrics` 0%; RED failures classified as flaky (67% pass rate) | | Edited production code without `hypothesis (action: record)` | UNCITED FIX | No causal evidence bound to the fix; hypothesis audit is empty | | Advanced phases without `tdd_phase_transition_request` | UNRECORDED PHASE CHANGE | DB has no phase record; evidence-based transitions cannot validate | | Used `pnpm vitest run` via Bash instead of `run_tests` MCP | SESSION BYPASS | Results bypass persistence; `test_history` and phase artifacts are not written | **Violating the letter of these rules IS violating the spirit of these rules.** --- ## Red Flags — STOP before continuing | If you are about to... | Required action | | --- | --- | | Write any file before `tdd_task (action: start)` returned a session ID | STOP — UNREGISTERED SESSION. Call `tdd_task (action: start)` first. | | Edit any production file without a recorded hypothesis | STOP — UNCITED FIX. Call `hypothesis (action: record)` first. | | Begin the next phase without `tdd_phase_transition_request` | STOP — UNRECORDED PHASE CHANGE. Request the transition first. | | Run `vitest`, `pnpm vitest`, `npx vitest`, or any Bash test runner | STOP — SESSION BYPASS. Use `run_tests` MCP instead. | | "Skip setup just this once, the goal is simple" | UNREGISTERED SESSION + UNCITED FIX combined. No exceptions. | | Call `inventory (kind: session)` to find your `chatId` when one was passed | STOP — the `chatId` is in your launch prompt. Use it directly. (Only self-recover via `inventory` when the prompt genuinely has no `chatId` — see Gate 1.) |