pair-workflow-implement-phase

Stage 3 (initial) of the delivery workflow — implementation of one refined story inside its persistent worktree: on a fresh card above the base, writing the story's tests and code together test-first

SKILL.md

GitHub

/pair-workflow-implement-phase — Build the Story Test-First, Publish Its One PR

Turn one refined story into verified commits, then project them onto exactly one pull request. Two entries, one stage:

  • Fresh card (no $snapshot, no $contract — ADR-024 amendment 2026-09-23, US-506): there is no up-front acceptance contract. You work above the base and write the story's tests and code together, test-first, as practice — one AC at a time, the test that proves it first, then the code that makes it pass. Independence moves to the final verifier, who names per AC the test that proves it.
  • Sealed a0 ($snapshot + $contract given — a run started before that amendment, or an a0-rev<m> revision on it): the contract is the specification. You may change implementation inside its scope; you may not change what it asserts.

The review and the fixes are other stages with other actors.

Arguments

Argument Required Description
$run, $story, $branch, $worktree, $base, $stacked, $entry, $policy, $inputs, $workflowVersion Yes The cycle arguments, as every stage receives them. Handoffs live under .pair/working/runs/$run/$story/ in the MAIN checkout.
$phase Yes a0, or a0-rev<m> when the final verifier found a genuine gap in a SEALED initial acceptance contract and the revised contract was sealed as a successor snapshot — implement again on the same branch, above that seal.
$attempt Yes 1 on the first implementation; 2 when the previous attempt published with a RED gate — same base (or same seal), fix the gate, never bypass it.
$head Sealed a0 only 40-hex head the contract was prepared on; the snapshot sits directly on it. Absent on a fresh card: inputHead is the worktree's HEAD after Step 1.
$snapshot Sealed a0 only 40-hex sha of the sealed RED snapshot commit — you re-discover it from Git, you never trust the prompt for its content. Absent ⇒ fresh card.
$contract Sealed a0 only Absolute path of the sealed acceptance contract (main checkout's run directory). Absent ⇒ fresh card.
$title Yes Story title.
$implementSkill Yes The project's implement process skill (default /pair-process-implement).
$verifyQuality Yes The tier-resolved quality-gate skill (default /pair-capability-verify-quality).
$recordDecision Yes The decision-recording skill (default /pair-capability-record-decision).
$checkpoint Yes The checkpoint skill (default /pair-capability-checkpoint).
$publishPr Yes The project's PR-publishing skill (default /pair-capability-publish-pr).
$notes No Scope directive from the card; it overrides the issue body where they conflict.

Algorithm

Headless execution (single owner: .pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/headless-stage-execution.md) — this stage runs headless: never leave a command in the background and never end your turn while a command runs (a push with a pre-push gate, a quality gate): use the maximum tool timeout, poll to completion in the same turn, and ALWAYS end with the stage's structured result — on partial progress { status: "failed", reason: "incomplete", detail, branch, outputHead }.

Step 0: Resolve the durable state (mandatory)

SKILL_DIR="$(dirname "<absolute path of this SKILL.md>")"
MAIN="$(pwd)"                                   # the main checkout — you have not cd'd yet
RUN_DIR="$MAIN/.pair/working/runs/$run/$story"
AC_HASH="$(node "$SKILL_DIR/scripts/cycle-state.mjs" ac-hash --story $story --dir "$RUN_DIR" | node -pe 'JSON.parse(require("fs").readFileSync(0,"utf8")).acHash')"   # canonical card hash — the SAME input every stage resolves with
node "$SKILL_DIR/scripts/cycle-state.mjs" resolve --dir "$RUN_DIR" --workflowVersion $workflowVersion \
  --policy '$policy' --entry $entry --story $story --inputs $inputs --acHash "$AC_HASH" --runsRoot "$MAIN/.pair/working/runs" 
  • status: other-run ⇒ return { status: "other-run", runId }. incompatible | invalid ⇒ return { status: "redirect", next: { step: "blocked", reason: "failed-resume", detail: <reason> } }.
  • next.step is not implement ⇒ return { status: "redirect", next } verbatim. Spend no judgment. When next names THIS dispatch (implement, $phase) you ARE the step: continue, never return a redirect to yourself — with one exception: next names this dispatch but carries a contract while the dispatch has no $snapshot (the coordinator's contract-less first guess on a run whose a0 is already sealed, US-506) ⇒ return { status: "redirect", next } verbatim, so the coordinator re-dispatches this step WITH the seal. Never take the fresh-card branch on a sealed run.
  • Otherwise continue; next.attempt is your attempt number.
  • A prior attempt may already have published a PR: next then says verify, and you return that — never a second PR.

Step 1: Isolation and the contract (mandatory)

Fresh card ($snapshot and $contract absent): next from Step 0 carries no contract either, and $RUN_DIR holds no sealed red-verify handoff — if it does, the dispatch is inconsistent: return status: failed, reason: seal-present-but-not-dispatched. Fresh-branch check (deterministic, before any work) — "fresh" means no STALE history, judged on the commits the branch carries above $base, never on whether the branch descends from the CURRENT $base (main simply moves on: a branch forked from an older main is not stale). First refresh remote state — git fetch --prune origin — and confirm what really exists on the remote with git ls-remote --heads origin <branch>: a remote-tracking ref (origin/<branch>) that was deleted on the host lingers in the checkout until pruned, and judging that stale remote-tracking ref blocks a fresh card on history that no longer exists. Judge the branch that will actually be pushed — the local story branch — plus the remote branch ONLY if ls-remote shows it exists; a branch absent from ls-remote is not judged, whatever a local ref still says. When such a branch exists list git log --format='%H%x09%s%x09%(trailers:key=Pair-RED-Snapshot,valueonly,separator=%x2C)' $base..<branch> (when $base is not an ancestor of the branch, use git merge-base of the two as the lower bound, so only the branch's OWN commits are listed). A commit is the card's OWN when its subject starts with [#<card>] or [US-<card>] (the commit template's story reference). The branch is stale ONLY when it carries a FOREIGN commit (another story's history, e.g. pre-rewrite history) or a commit with a Pair-RED-Snapshot trailer while $RUN_DIR holds no sealed red-verify. An EMPTY $base..<branch> (no own work) is fresh and there is nothing to lose: reset it to $base yourself inside $worktree (git -C $worktree reset --hard $base on the story branch only, never the main checkout). The card's own commits above an older $base are NOT stale: that is a RESUME of the fresh path — keep them, inspect what is already done (git log, the diff, the checkpoint), continue implementing, verifying and publishing from there (do not rebase here; publish-pr's base preparation handles a moved base), never redo or discard them. The same holds for UNCOMMITTED changes in the story worktree on this story branch: they are a previous attempt of THIS story interrupted mid-work (a Ctrl-C, a killed driver) — the card lock this run holds guarantees no other run owns them. Resume them: review the diff against the AC and the task list, keep what is right, finish, verify and commit it as the card's own work; never stash, reset or discard it, and never stop on it as foreign. Only a stale branch returns { status: "failed", reason: "branch-not-fresh", detail: <the foreign commit or snapshot trailer found, and the branch head>, next: { step: "blocked" } } and tells the human what to do — archive the branch (rename it) or reset it to $base themselves, then re-run; never reset, delete, rebase or reuse a stale one yourself. A branch that does not exist yet passes. Then create or reuse the story worktree (git worktree add $worktree -B $branch $base on first setup, git worktree add $worktree $branch if the branch has commits, plain cd if the path exists); inputHead = git -C $worktree rev-parse HEAD. Never modify the main checkout's tree or switch its branch. Items 2 and 3 below do not apply; continue at Step 2.

Sealed a0:

  1. Do ALL git and file work inside $worktree; never modify the main checkout's tree or switch its branch. git -C $worktree rev-parse HEAD must be $snapshot (or a descendant of it on this branch); otherwise return status: failed with reason: head-not-snapshot — never reset, stash or rebase.
  2. Discover the contract from Git: the commit $snapshot carries Pair-RED-Snapshot: pr=0; phase=$phase; base=$head; manifest=<path> (pr=0: the initial chain is sealed before the PR exists and keeps that identity for its revisions); read the manifest and the sealed test blobs with git show / git ls-tree. Accept no digest or test path from the prompt. Missing, ambiguous or contradictory discovery ⇒ status: failed, reason: snapshot-not-found.
  3. With $stacked=true: $base is another story's branch — its commits are already in your history and must NOT be reverted, duplicated or re-implemented.

Step 2: Implement, test-first

  1. Activate the card first (both entries, attempt 1 or resume — never skipped on the cycle path): node "$SKILL_DIR/scripts/cycle-state.mjs" activate-card --dir "$RUN_DIR" --story $story --worktree $worktree — writes the board state mapped to In Progress (## State Mapping) through the bound PM adapter and reads it back (a local tracker — filesystem — writes the card inside $worktree and commits it on the story branch); exit 1 (confirmed: false) warns with its error and continues. Assign when unassigned ($implementSkill Step 0.1b). 0b. Progress list (when the harness has a todo tool — Claude Code TodoWrite): one todo per card task (T-1 <title> …), then Running checks, then Opening the PR; exactly one in_progress at a time, updated as you move. pair run shows the in-progress item on the card's live line — a stale list misreports where the stage is.
  2. Follow $implementSkill as the process of record — its task cycle, its TDD discipline, the task/commit templates. $notes, when present, is a SCOPE DIRECTIVE that overrides the issue body where they conflict.
    • Fresh card: TDD as practice — tests and code evolve together, task by task, in the card's task order. For every acceptance criterion there is a test that would fail without the behavior it claims (run it before the code when you can; a bug-fix task gets its reproducing test first, always). No proof of test-first is recorded or required: what the verifier judges is the tests that exist at the head, per AC.
    • Sealed a0: the sealed acceptance tests are the story's RED: make every baseline: red witness pass and keep every baseline: pass control passing; item 2 below applies.
  3. (Sealed a0 only.) Do NOT modify, format, rename, regenerate, delete or weaken any artifact the snapshot records; do NOT amend, rebase, reset or rewrite the snapshot commit. A test the contract does not cover that you need for your own task cycle may be added (it is listed in your handoff); a sealed one is untouchable — a gap in the contract is reported as contractGaps for the final verifier, never patched around.
  4. Finite-state completeness (mandatory when the change parses, selects, snapshots or branches on a finite protocol/state domain): make the whole decision table pass — every supported state and its invalid/boundary pair, including the smallest interaction cross-product. Do not implement one newly discovered row at a time and wait for review to name the next ordinary variant.
  5. Empirical evidence: before asserting a measured or factual claim in source comments, test names, ADR/ADL, PR body or a diagnostic, record claim | authoritative oracle | exact command/fixture/revision | observed output; absent evidence, remove or qualify the claim. Authoritative boundary proof: an external command, format or runtime claim is proven at its real producer/consumer in an isolated probe. Lossless diagnostics: an error that reports user input keeps actual, expected and candidate values distinguishable.
  6. Commit only the files you changed — stage explicit paths, never git add -A. Commit after every task: an uncommitted worktree loses everything if the supervisor kills the agent. Never run a single command that can stay silent for more than ~2 minutes (a cold full-repo gate qualifies): scope it, narrate between steps. Never leave a long command in the background and never end your turn while one runs (a git push that triggers a pre-push gate, a full-repo gate): this agent runs headless — there is no further turn and no notification, so a turn that ends "waiting for completion" ends the stage with no PR and no handoff. Give such a command the maximum tool timeout, poll it to completion in the same turn (re-check its output file until it exits), and only then continue to the next step.
  7. Run the tests you select by the changed producers and their consumers during iterations; record for each run command, identity (node "$SKILL_DIR/scripts/cycle-state.mjs" test-identity --cwd $worktree --command "<cmd>") and exitCode — a result is reusable in a later step ONLY under the same identity.

Step 3: Self-review, verify and record

Changeset (both entries, before the gates): Changeset: .changeset/<story-id>.md mirrors the PR description and states the bump (below 1.0 a breaking change is minor); write it, or refresh it when the description changed, and commit it with the change. A PR with no release-relevant change takes the no-changeset label instead.

  1. Informal self-review — fix, never record (both entries, before the gates): re-read the card's acceptance criteria and the review rules your project's review would apply ($implementSkill's quality standards, the code-review template's severities), walk your own diff against them, and FIX what you find — a missing test for an AC, a weak assertion, an edge case the AC names, a gate that would go red. Deliver only an artifact that holds up with its gates green. Nothing of this self-review is recorded anywhere — not in the handoff (publish refuses any selfReview* field: self-review-not-recordable), not in the PR body, not in a commit message, not in the checkpoint — so the independent verifier judges the result without your conclusions in front of it.
  2. (Sealed a0 only.) Remove the transient seal manifest (.pair/red-snapshots/pr-*-a0*.json — the initial chain is sealed under pr=0, it predates the PR) in your first commit above the snapshot: it is the seal's record and lives in Git history; the custody check expects it gone, and a manifest left in the tree fails the repository's format gate (canary run 11).
  3. Fresh worktree first: a worktree this run created has no dependencies or build — run the adopted worktree-setup-command (way-of-working.md; absent ⇒ none) inside $worktree before any gate; a worktree-setup-command non-zero exit HALTs the stage — no gate runs on a broken setup (status: failed, reason: worktree-setup-failed); record setup: <cmd> exit N alongside the gates run; never symlink another checkout's node_modules/dist. Verify the gates with $verifyQuality: it resolves the story's risk:* tier and runs exactly the checks CI would run for that tier, plus the repository's own pre-push gate when one exists. Sealed a0: re-run every sealed witness command from the manifest — all green. A red gate is gatesPassed: false and status: ok only if the PR was still published; never bypass a hook (--no-verify on a push is forbidden — it hides exactly the gate this stage must prove) and never report green what you did not run.
  4. Record any architectural or project decision with $recordDecision, never only in a commit message; on the sealed a0 path the acceptance contract's fixScope.allowedPaths covers the story's implementation surface and .pair/adoption/decision-log/ for that reason — a decision that does not fit is reported under contractGaps, never smuggled into the PR body.

Step 4: Checkpoint, publish the PR, persist

  1. Write the story checkpoint via $checkpoint $mode=write, then push the branch.
  2. Publish the PR by invoking $publishPr. Do NOT hand-roll the PR: that skill owns the whole sequence — the tier-resolved gate, the PR body composed from pr-template.md with only the pertinent conditional sections, the story's classification tags, ready-for-review, the pr-state:* label, the PR-URL back-link on the story, the board state. If a PR already exists for the branch it is UPDATED, never duplicated. One expected signal: it emits Review: review-dispatch-required instead of nesting a review — correct; the coordinator dispatches the final verifier. Never dispatch or run a review yourself.
  3. External ACs: an AC that needs live external verification this stage cannot perform (a real release, a third-party service) is listed by id as external — pending live verification in the PR body AND in the result's summary — never claimed done, never silently dropped.
  4. Text shape of the PR body: schematic, one decision per line, no narration of the diff; everything a blind reviewer needs (rationale, decisions, ADR links) goes there — the reviewer cannot see the checkpoint.
  5. Read back prNumber, url and the pushed head (git rev-parse origin/$branch, 40-hex). Publish the handoff (skill: "implement-phase", inputHead ($head on the sealed path, the Step 1 HEAD on a fresh card), snapshot (sealed path only), status: ok, gatesPassed, prNumber, url, outputHead, checkpointPath, tasks, testRuns: [{ command, identity, exitCode }], addedTests, contractGaps (sealed path only), elapsedMs — and NO self-review field) with cycle-state.mjs publish … --workflowVersion $workflowVersion --attempt $attempt --pr <prNumber>, plus --predecessor a0-red-verify on the sealed path only (a fresh card has no predecessor: a0-implement-phase is its first handoff) (the PR just created or updated — the envelope carries it even when the dispatch had none), run resolve again and return its next verbatim — copy the whole object the script printed; never retype, summarize or drop a field it carries because another field in your answer already seems to say the same thing (e.g. contract, next to a contractHash you also state). The orchestrator checks the object's shape, not your restatement of it; a hand-reconstructed next missing one field is refused as unusable.

US-479 S12 — no production work without the complete seal (sealed a0 path). When the dispatch names a contract, start only from a sealed contract whose applicable matrix rows are present and validated: if the seal is missing, partial, or its rows were reduced after validation, stop and return the typed refusal instead of coding. You may never edit, weaken or drop a sealed row — including a regression guard — to make your change pass.

Output Format

You ALWAYS end with this structured result, even on partial progress: if you cannot finish (a gate or push that will not complete, a step that failed), return { status: "failed", reason: "incomplete", detail: <what is done — commits, pushed or not — and what remains>, branch, outputHead } instead of ending in prose; the commits already on the branch are resumed by the next attempt.

{ status: ok | failed, gatesPassed, branch, checkpointPath, prNumber, url, outputHead, summary, reason?, next }. gatesPassed: false with status: ok means published but red: the cycle state routes ONE retry on the same base or seal ($attempt=2), then failed-implement.

Notes

  • Do NOT review. Do NOT merge. Never open a second PR for the same story.
  • Read nothing under .pair/working/ except the checkpoint and $RUN_DIR.
  • Your handoff publish is observed by the host runtime (cycle-runtime.mjs, US-479 T-25) as a phase-level progress point, through cycle-state.mjs — never something you invoke yourself.