Imported from nesquikm/dev-process-toolkit (
plugins/dev-process-toolkit/skills/implement/SKILL.md). Install upstream withnpx skills add nesquikm/dev-process-toolkit --skill implement. Copyright stays with the author.
Implement
Implement the following end-to-end: $ARGUMENTS
Invocation forms
Two shapes diverge at Phase 5. /implement <FR-id> to ship one FR (the smoke driver's prescribed form, and the path for FRs with no milestone binding); /implement M<N> to close out a milestone (every active FR + Phase 5 close + archival). End states differ by design (STE-83): single-FR leaves FR + milestone status: active; milestone form archives both on green gate. If single-FR + plan fully checked, /gate-check probe #14 fires the STE-180 advisory (M<N> plan fully checked but not archived — run /spec-archive M<N> or /implement M<N> to close) — the gate tells you, not memory.
| Phase | /implement <FR-id> |
/implement M<N> |
|---|---|---|
| 0 (Pre-flight) | single FR's binding | every active FR in M |
| 1 (Tracker claim) | claims one ticket | claims every ticket |
| 2 (Plan analysis) | plans tasks for the FR | plans every FR's tasks, dep-ordered |
| 3 (TDD loop) | builds the FR | builds every FR |
| 4 (Commit + close) | one feature commit + tracker → Done | one commit per FR + each tracker → Done |
| 5 (Milestone close) | silent-skip (no prompt, no archival) | runs it — close prompt + archival sweep |
Pre-flight: Branch Isolation
Ask the user: "Work in a git worktree (isolated branch) or on the current branch?" — worktrees let failed runs be discarded cleanly; current branch starts immediately.
If worktree: derive a branch name from the task (e.g., feat/user-auth), run git worktree add ../<branch-name> -b <branch-name>, install dependencies for the detected stack (npm install, uv sync, cargo build, go mod download, etc.), and perform all Phase 1–4 work inside the new directory. On success, tell the user how to merge back; on failure, offer git worktree remove <path> --force.
Partial Failure Recovery
If a multi-milestone worktree run partially succeeds, list completed work (milestones + commit hashes) and the failing milestone, then offer three recovery options: cherry-pick completed commits onto main (git cherry-pick <hash>...), continue in the worktree after a fix (cd <path>, resume /implement), or discard the worktree (git worktree remove <path> --force).
Phase 1: Understand
Do not read
specs/frs/archive/orspecs/plan/archive/during implementation — archived FRs and milestones are historical context only.
-
Tracker-mode probes — Before any other action:
- 0.a Tracker availability pre-flight (STE-199 AC-STE-199.4 / AC-STE-199.5) — When
mode != none, before any other Phase 1 step, enumerate the available tool list and check formcp__<tracker>__*patterns. If zero matching tools are loadable in this session, branch on--code-only: with the flag, logtracker_skipped: pre-flight probe failed; --code-only flag honoredand proceed through the code-only path (skips 0.c/0.d/0.e and Phase 4d release transition); without the flag, refuse with NFR-10 canonical shape:"Tracker <tracker> configured in CLAUDE.md but mcp__<tracker>__* tools are not available in this session. Remedy: run \claude /mcp` to (re)authenticate, or re-invoke with --code-only for a degraded tracker-skipped run."Surface atracker_skippedcapability row in the closing summary for each step the--code-onlypath skips. Vacuous onmode: none. Behavior identical tomode: nonefor the duration of the run; CLAUDE.md is **not** edited by--code-only`. - 0.a′ Skip baseline capture — Before any branch decision, on every run, RUN the
captureSkipBaselineentry point below from the project root. The baseline is keyed to the TRUNK COMMIT (git merge-base HEAD <trunk>), and capture declines unless HEAD stands on that commit with a clean tree — so on a feature branch it will refuse, and the duty here is to attempt it and report the refusal by name, never to swallow it. The refusal states thegit checkoutthat would satisfy it. Write-once per trunk commit: an existing baseline is left untouched. Without a baseline every latergate:evidence row rendersbaseline unmeasured, which step 14 treats as a refusal ground, so a clean run becomes uncertifiable. Full decision logic indocs/implement-reference.md§ Skip baseline capture.bun run "${CLAUDE_PLUGIN_ROOT}/adapters/_shared/src/capture_skip_baseline.ts" "<projectRoot>" - 0.b Provider resolution — Resolve
Provideronce per invocation:LocalProviderifmode: none(or--code-onlyflag set),TrackerProviderwrapping the configured tracker adapter otherwise. No re-resolution mid-execution. ACs come fromspecs/frs/<ulid>.md; Phase 4 archives viagit mv;Provider.claimLock/releaseLockgates entry/exit per the Provider lifecycle contract. - 0.b′ Resolver entry — Call
buildResolverConfig(claudeMdPath, adaptersDir)fromadapters/_shared/src/resolver_config.tsonce at entry, then pass the result toresolveFRArgument($ARGUMENTS, config)fromadapters/_shared/src/resolve.ts. Never hand-assemble the config inline; malformed adapter metadata surfaces asMalformedAdapterMetadataError→ NFR-10 canonical refusal. Route bykind:ulid→ proceed to 0.c with that ULID;tracker-id/url→ branch on mode: tracker mode usesfindFRPathByTrackerRef(specsDir, trackerKey, trackerId),mode: noneusesfindFRByTrackerRef(specsDir, trackerKey, trackerId); on hit, proceed to 0.c; on miss, runimportFromTracker(...)then proceed.milestone(STE-202 AC-STE-202.3) → read the milestone plan file atspecs/plan/<milestone>.mdand run the milestone-scope flow per § Invocation forms;fallthrough→ continue to step 2 for free-form argument handling. Branch-name interop: if the branch name contains a ticket ID that disagrees with the argument's resolved ticket, the argument wins with an NFR-10-shape warning. Full decision table:docs/resolver-entry.md. - 0.b′¹
needs_technical_reviewrefusal (STE-227 AC-STE-227.6) — Immediately after 0.b′ resolves a single FR (kind: ulid/tracker-id/url) and before 0.b″ branch proposal and 0.c claim, read the resolved FR's frontmatter atfrFilePath. Ifneeds_technical_review: true, hard-refuse with NFR-10 canonical shape: Verdict"/implement refused — FR <id> flagged needs_technical_review; technical sections are placeholders."and Remedy"Run /spec-write <FR-id> to complete the technical design + testing sections, then re-invoke /implement."(substitute<FR-id>with the resolved ID). Emit theimplement_refused_needs_technical_reviewcapability row in the closing summary, then exit non-zero. No claim, no branch, zero side effects — refusal fires before 0.b″ and 0.c so nothing is mutated on disk or on the tracker. - 0.b′²
needs_technical_reviewmilestone-scope refusal (STE-227 AC-STE-227.7) — When 0.b′ resolveskind: milestonetoM<N>, before any claim cycle starts, enumerate every active FR in scope: globspecs/frs/*.md, filter by frontmattermilestone: M<N>ANDstatus: active, then collect every FR whose frontmatter hasneeds_technical_review: trueinto a flagged-list. If the flagged-list is non-empty, hard-refuse the whole milestone with NFR-10 canonical shape: Verdict"/implement refused — milestone M<N> contains <N> FR(s) flagged needs_technical_review."and Remedy"Run /spec-write <FR-id> (no flag) for each flagged FR, then re-invoke /implement M<N>:\n - <id-1>\n - <id-2>\n - …"— the remedy enumerates every flagged FR so the reviewer can address them in one batch (no whack-a-mole). Emit theimplement_refused_needs_technical_reviewcapability row in the closing summary, then exit non-zero. Refusal fires before any claim — no per-FR claim cycle starts, no branch is proposed, no tracker write happens; nothing is mutated on disk or on the tracker for any FR in the milestone. - 0.b″ Branch proposal — Between 0.b′ and 0.c, if Schema L carries
branch_template:, callisCurrentBranchAcceptable(currentBranch, scope)fromadapters/_shared/src/branch_proposal.ts— FR scopes carry optionalmilestoneNumber(the BRANCH token derived from the FR'smilestone:frontmatter — bare digits for a numeric milestone, or the fullM_<epic-key>token for an Epic-keyed one:M_GF_78, never its digits78), so an m-named branch and a legacy ticket-named branch both pass. Unacceptable ⇒ derivetypedeterministically viabranchTypeFor({ changelogCategory })fromadapters/_shared/src/branch_type_for.ts, wherechangelogCategoryis the resolved FR'schangelog_categoryfrontmatter value (absent ⇒ STE-73 defaultAdded); the helper'snoTechopt never applies here — the STE-227needs_technical_reviewrefusal at 0.b′¹ fires before 0.b″ ever runs, so this is documented, not plumbed. Run a single LLM pass for{slug}only. The effective template comes fromcanonicalBranchTemplate({ milestone })inadapters/_shared/src/branch_proposal.ts— FR-scoped runs resolve{N}from the resolved FR's BRANCH token derived from themilestone:frontmatter (bare digits for a numeric milestone, or the fullM_<epic-key>token for an Epic-keyed one —M_GF_78, never its digits78), so a milestone-bound FR proposes{type}/m{N}-{slug}and a milestone-less FR falls back to{type}/{ticket-id}-{slug}; milestone runs take{N}from the run arg, unchanged. An explicit Schema Lbranch_template:value (anything other than the canonical seeded default) wins over the derived form — operator override preserved. Render viabuildBranchProposal, prompt[Y] accept / [e] edit / [n] abort.Y→git checkout -b;n→ clean exit, zero side effects. Absentbranch_template:⇒ skip entirely. Trunk-OK allowlist (STE-228 supersedes STE-202 AC-STE-202.5): the trunk-OK list narrows toTRUNK_OK_TYPES = ["ci"]only —choreanddocsno longer ship directly to trunk. When the type isci, the proposal flow accepts the trunk branch as-is. For every other type (feat,fix,refactor,perf,chore,docs), the derived branch is enforced. The same constant is consumed byrequireCommittableBranchfromadapters/_shared/src/require_committable_branch.ts, the universal pre-commit gate every commit-producing skill calls before staging — see STE-228 § Branch-name canonical table for the full per-skill builder mapping. Full decision logic indocs/implement-reference.md§ Branch Proposal. - 0.c Claim — Entry gate. Tracker mode: run the per-FR claim sequence in
docs/implement-tracker-mode.md§ Claim runbook. Four-way routing (claimed/already-ours/taken-elsewhere/already-released) per the runbook's decision steps.mode: none: constructnew LocalProvider({ repoRoot })fromadapters/_shared/src/local_provider.tsand callclaimLock(<id>, <currentBranch>)on it, where<id>is the FR's full mintedid:frontmatter value and never its filename stem.LocalProvider.claimLockwrites.dpt/locks/<id>and commits it aschore(locks): claim lock for <id>, with the branch on abranch: <branch>body line — that commit is the durable witness 0.d reads back, and the only proof a claim ever fired. Three-way routing (claimed/already-ours/taken-elsewhere); there is noalready-releasedon the local claim side. Lock lifecycle:docs/implement-reference.md§ Phase 4 Close. - 0.d Claim verification (Phase 1-exit self-check) — Tracker mode: Before entering Phase 2, re-fetch the ticket via
mcp__<tracker>__get_issue(<id>)and assert (1)status == status_mapping[in_progress]AND (2)assignee == currentUser. Mismatch ⇒ NFR-10 canonical refusal naming the ticket + observed status/assignee; hard-refuse to enter Phase 2.mode: none: thelocal-no-trackersentinel is vacuous but the artifact is not — before entering Phase 2, assert (1).dpt/locks/<id>exists AND (2)git log --format=%s | grep -Fxq "chore(locks): claim lock for <id>"succeeds AND (3)grep -Fxq "branch: <currentBranch>" .dpt/locks/<id>succeeds. All three are required together, never alternatives: (2) proves a claim fired at all, (3) proves the claim is ours. Subject-only, fixed-string, whole-line and branch-free on purpose:--grepanchors^at every line of a message, so a body quoting the subject would certify a claim that never fired;--format=%sprints the subject alone, so whole-line matching cannot be satisfied by thebranch:body line; and the subject carries no branch, because a branch-bearing subject is uncommittable past the 72-character cap. Conjunct (3) is where ownership lives: dropping the branch from the subject dropped the ownership half this step used to get for free (the tracker arm'sassignee == currentUser), so ownership is re-homed, not deleted, onto the lock file's ownbranch:line — byte-identically the predicateLocalProvideralready uses to decidealready-ours, so producer and verifier stop being independently-worded restatements of one rule. What the pair no longer asserts, named rather than assumed away: the retired branch-bearing subject made the witness commit and the lock file agree on the branch by inspection; (2) and (3) read each artifact separately and never cross-check them. Agreement still holds by construction at write time — subject andbranch:line are written from one variable in oneclaimLockcall — and post-hoc editing of a committed lock is outside the documented threat model (tracker-less locking detects cross-tree collision, it does not prevent it), so the residual is narrow and named rather than closed here. Refuse per NFR-10 naming which conjunct failed, never one undifferentiated refusal — the three states have different causes and different remedies. Neither present ⇒ no claim ever fired: refuse naming the lock path and observed (no lock, no claim commit) against expected, remedygo back and run 0.c before entering Phase 2. Witness present but ownership false ⇒ the lock is held by the branch its ownbranch:line names, which istaken-elsewhereand not yours: refuse naming that recorded branch against<currentBranch>, and never overwrite the lock. Lock present but no matching commit ⇒ the claim wrote.dpt/locks/<id>recordingbranch: <branch-in-lock>, could not commit it, and its rollback did not complete: refuse naming all three observations — the lock path, the branch the lock records, and the missing witness — and do NOT prescribe re-running 0.c, which repeats the failure and is not the remedy here. In all three, hard-refuse to enter Phase 2. Complements/gate-check's active-side ticket-state drift probe at gate time. - 0.e Project-milestone attach (any adapter with
project_milestone: true— Linear + Jira, idempotent) — In tracker mode withproject_milestone: true, after 0.d succeeds, read the FR'smilestone:frontmatter and callplanFileHeadingToMilestoneName(specs/plan/<milestone>.md)fromadapters/_shared/src/attach_project_milestone.ts. Then callattachProjectMilestone(provider, project, canonicalName, ticketId)— the surface it binds is chosen byprovider.milestoneBinding(absent ⇒object):objectbinds Linear's native project milestone,epicbinds the parent Epic (whatadapters/jira.mddeclares), and it reaches the legacylabelread-merge-write by TWO routes: a grandfathered numericM<N>token always does (that is where its reader looks), and an Epic-keyed one degrades there where the Epic-availability probe says the project cannot hold an Epic. Idempotent on already-bound tickets. Vacuous on archived FRs,mode: none, and adapters withproject_milestone: false.MilestoneAttachmentErrorsurfaces per NFR-10; on a permanent attach failure (transient retries exhausted, or a non-transient binding mismatch) the closing summary MUST emitmilestone_attach_failedas a loud warning-severity capability row — never a plain informational line. All-paths guarantee: the attach + verify runs per FR on every/implementpath, before that FR's Phase 4 close — the single-FR path (/implement <FR-id>) runs it here; the milestone-scope path (/implement M<N>fan-out, one/tddorchestrator per FR) runs 0.e inside each FR's claim cycle, never once-per-milestone. - 0.f Tracker-mode probe — Run the Schema L probe (see
docs/patterns.md§ Tracker Mode Probe). IfCLAUDE.mdhas no## Task Trackingsection, mode isnoneand tracker hooks below skip. If a tracker mode is active:- Ticket-binding pre-flight — 2-tier ticket-binding resolver + confirmation prompt per
docs/ticket-binding.md. Branch-regex mismatch fails loudly; decline exits cleanly. - Record
updatedAt(post-claimLock) — After step 0.cclaimLockhas succeeded, call the adapter'spull_acs(ticket_id)and store the ticket'supdatedAtin-session for/gate-checkto compare later. Recording after claimLock is load-bearing:claimLockitself mutates the ticket, so recording before would cause/gate-checkto flag the skill's own write as drift. Same rule applies to any other tracker-writing pre-flight step. - Bidirectional AC diff/resolve — Run the bidirectional AC sync loop before proceeding past Phase 1.
See
docs/implement-tracker-mode.mdfor the full tracker-mode flow.
- Ticket-binding pre-flight — 2-tier ticket-binding resolver + confirmation prompt per
- 0.a Tracker availability pre-flight (STE-199 AC-STE-199.4 / AC-STE-199.5) — When
-
Check for specs — If
specs/exists, check whether spec files have real content. If specs exist but are mostly empty, warn the user: "Specs appear to be incomplete. SDD works best when specs are filled in first. Consider running/dev-process-toolkit:spec-writeor continue with what's available?" Let the user decide. -
Resolve the target — Determine what to implement based on
$ARGUMENTS:next⇒ first milestone inspecs/plan/with unchecked ACs (reportAll milestones complete.if none).all/remaining⇒ all milestones with unchecked ACs, run sequentially after presenting the list for approval.- Multiple milestones (e.g.,
M2 and M3) ⇒ run sequentially in listed order. - Single milestone name (
M1,M2) ⇒ read that milestone fromspecs/plan/<M#>.md. - Numeric ⇒
gh issue view $ARGUMENTS. - Filename in
.tasks/⇒ read the task file. - Otherwise ⇒ read relevant specs in
specs/(if present) or treat as task description.
Slim closure detection (STE-200 AC-STE-200.3, milestone-scope only). After the target resolves to
M<N>, countclaimable_fr_count(M<N>)— activespecs/frs/*.mdfiles withmilestone: M<N>frontmatter. When the count is zero ANDevaluatePlanOnlyEligibility(specsDir, "M<N>")returns eligible (kind: scaffolding OR all-checked), route to slim closure: skip claim/build phases, invoke/spec-archive M<N>(which auto-takes the plan-only branch per AC-STE-200.1) + surface aplan_only_archivalcapability row in the closing summary; jump straight to Phase 5 (which silent-skips per AC-STE-200.4 onkind: scaffoldingand zero-FR milestones — no/ship-milestonechain prompt fires). Documented as the/setup-bootstrap-milestone path. -
Read the gate commands — Read CLAUDE.md and find the gate check commands (look for "Key Commands" or "Gating rule" section). These are the commands you'll use throughout.
-
Verify baseline health — Run the gate commands now, before writing any code. If the project is already broken, fix it first (or tell the user). Do not build new features on a broken foundation.
-
Read relevant code — Find the files that need to change.
-
Build the AC checklist — Extract every acceptance criterion as a binary pass/fail checklist. If no explicit ACs exist, derive them from the description. This checklist is your definition of done.
-
Present the plan — Show the user: AC checklist, files to create/modify, test strategy. Warn about parallel-conflict risk on shared files (like index.ts barrel exports). Ask for approval before proceeding.
Phase 2: Build (TDD)
-
Execute in TDD order via the multi-agent orchestrator —
TDD Orchestrator Contract. Violation name: Inline TDD Antipattern (writing tests + code in the parent
/implementcontext instead of forking the orchestrator). Auditable evidence shape: NSkill(/dev-process-toolkit:tdd <FR-id>)tool_useentries where N = FR count in milestone scope — one orchestrator invocation per FR, no inlined RED→GREEN→REFACTOR in the parent transcript. Residual-risk note: the STE-220→STE-270 prose-falsification chain shows prose alone is falsifiable; the documented escalation path on repeat violation is an evidence-based gate (STE-262 / STE-270 pattern) or a hard mechanic (STE-225 pattern). Catalog:docs/honored-contracts.md.Rationalization Prevention. The following rationalizations are documented antipatterns — each is preempted here so they cannot be invoked as waivers:
Excuse Reality Milestone spans N FRs / many ACs — orchestrator cost is too high Cost is not a contract waiver; orchestrator-per-FR IS the milestone-scope pattern (STE-225). /implement M<N>milestone-scope has no clear "use the orchestrator N times" patternN-times IS the pattern: one Skill(/dev-process-toolkit:tdd <FR-id>)tool_useper FR in scope — N invocations where N = FR count.Prioritized shipping over process fidelity Process fidelity IS the ship gate, not its competitor; an FR shipped via inline TDD has not shipped through the contract. invoke
/dev-process-toolkit:tdd <FR-id>inline (no separate opt-in path). Per STE-225 + STE-296, the orchestrator runs RED → GREEN → REFACTOR → AUDIT via four forked subagents (test-writer once per FR with the full AC list batched; implementer once per AC; refactorer once at end after all GREEN; spec-reviewer once at end post-REFACTOR) withcontext: forkisolation, a stricttdd-resultfenced-block hand-off contract, and bounded retry (max 2 per role per AC for semantic failures A/B/C/E; single targeted retry for format violation D). Per-stage isolation enforces the test-writer-cannot-see-implementation guarantee deterministically. The orchestrator's halt path does pause for the operator — that's intentional, not a pacing violation: halt fires only after the bounded-retry cap is exhausted, so it surfaces a real failure. Routine cycles (no retries) run end-to-end without operator interaction. Follow project patterns from CLAUDE.md. -
Spec deviation check — If reality contradicts the spec, STOP coding forward and classify:
underspecified(backfill + test + continue),ambiguous(provisional decision + user confirm at Phase 4),contradicts(wait for user decision),infeasible(wait). Always backfill edge cases tospecs/requirements.md/specs/technical-spec.mdplus a test. Full playbook:docs/implement-reference.md§ Spec Deviation Check.
Spec Breakout
If the current milestone accumulates 3 or more contradicts / infeasible deviations (configurable; default 3), STOP, emit a Spec Breakout report, recommend a spec rewrite for the affected areas before resuming. Breakout is a valid output, not a failure. Full report shape: docs/implement-reference.md § Spec Breakout.
-
Checkpoint — After completing each logical unit of work (a TDD cycle for a meaningful chunk), create a git commit on the working branch. These intermediate commits are recovery points.
-
Gate check — Run the gate commands from step 3. This is the deterministic kill switch: if it fails, fix before proceeding. Use
/dev-process-toolkit:debugfor unclear failures.
Parallelization
For fan-out-friendly tasks (independent files, ≥3 workers worth of work), parallel dispatch via native subagents, agent-teams, or worktree-per-subagent isolation can keep each context clean. See docs/parallel-execution.md before dispatching.
Phase 3: Self-Review Loop (max 2 rounds)
The gate check is the hard stop. This review loop is the smart stop.
Phase 3 review runs against the code Phase 2 produced via the /dev-process-toolkit:tdd orchestrator. If the orchestrator halted (bounded-retry exhausted on mode A/B/C/D/E per STE-225), Phase 2 already escalated to the operator and Phase 3 does not run — the halt report is the surfaced failure. Otherwise Phase 3's gate check is the deterministic backstop that confirms the orchestrator's GREEN-at-exit claim against the project's full gate command (typecheck + lint + tests).
Spec-review audit capability propagation (STE-296). When the /tdd orchestrator's spec-review audit step fires, /implement propagates its outcome through the Phase 4 step 14 closing summary as one of three literal, byte-checkable capability tokens — sourced from the static map in skills/spec-write/SKILL.md § 7 (single source of truth for capability-gap rendering):
- audit clean on first pass ⇒ MUST emit
tdd_spec_audit_passed(literal token, backticked). - audit found missing AC(s) on first pass and a bounded retry round (test-writer + implementer scoped to missing ACs) recovered them ⇒ MUST emit
tdd_spec_audit_missing_recovered(literal token, backticked). - audit found missing AC(s) on first pass and the bounded retry round did not recover them ⇒ orchestrator halts with
mode: spec-gapand MUST emittdd_spec_audit_halted(literal token, backticked);/implementsurfaces the unresolved AC list to the operator and exits non-zero before Phase 4 step 15.
The byte-checkable tokens are the structural signals /gate-check's closing_summary_capability_keys probe greps for; narrative prose like "spec audit was clean" is insufficient. Plain-language rendered prose lives in the skills/spec-write/SKILL.md § 7 static map under the same keys; do not paraphrase at runtime.
Best-practices conformance lens (M124). Applies when specs/best-practices.yaml has at least one entry; absent or empty manifest ⇒ skip the lens. Selection is deterministic and module-run — never re-derive the scope-glob semantics in prose:
- Derive the FR's changed-file list (
git diff --name-only <base-ref>), then RUN the select leg from the repo root:bun run ${CLAUDE_PLUGIN_ROOT}/adapters/_shared/src/best_practices_manifest.ts select --specs specs --files "<changed files>". It prints the selected entries (name + doc path) in manifest order: entries whosescopeglobs match a changed file, plus entries withoutscope, which always apply. - Read each selected entry's curated doc directly with the Read tool — never a research fork (standing operator ruling).
- File each violation of a curated doc as a review concern riding this phase's existing bounded 2-round resolve-or-escalate review loop — never a new blocking gate, never an auto-fix outside the loop.
- Disposition (Phase 4 step 14 closing summary): exactly one of the pair emits per run — never both, never neither; a silent skip is forbidden (XOR). Lens ran (manifest had at least one entry) ⇒ MUST emit
best_practices_lens_applied(literal token, backticked); manifest absent or empty ⇒ MUST emitbest_practices_lens_skipped_no_manifest(literal token, backticked). Rendered prose lives in theskills/spec-write/SKILL.md§ 7 static map under the same keys;/gate-check'sclosing_summary_capability_keysprobe greps the literals — narrative paraphrase is insufficient.
Proportional review: Scale review depth to change size. Trivial changes (single function, <20 lines, no new modules) need only AC + gate check. Reserve deep review for changes touching multiple modules or new patterns.
Each round has three sequential stages. Complete each stage before starting the next. If a stage finds issues, fix them and re-run the gate before proceeding.
- Round N (N = 1, 2):
Stage A — Spec Compliance
a. AC check — Walk the checklist from Phase 1. For each AC:
- ✓ Pass — implemented and directly tested (not just indirectly covered)
- ✗ Fail — missing or wrong
- ⚠ Partial — implemented but incomplete or only indirectly tested
If an AC explicitly names a module or function, verify a test file directly tests that module. Indirect coverage does NOT satisfy an explicit AC.
b. Cross-module coverage check — For every module created or significantly modified, verify direct test coverage. If an AC references a specific module without a dedicated test file, flag it as a gap.
c. Assertion quality check — Scan test files for shallow assertions: expect(fn).not.toThrow() / assert not raises as the sole assertion, expect(result).toBeDefined() / assert result is not None without checking the value, type-only checks (isinstance(), typeof) without verifying content. Tests using only these patterns aren't validating behavior — strengthen them.
If Stage A finds issues: fix, re-run gate check, then proceed to Stage B.
Stage B — Two-Pass Review (delegated to code-reviewer)
Stage B runs two sequential code-reviewer invocations via the Agent tool: Pass 1 — Spec Compliance then Pass 2 — Code Quality. Both use agents/code-reviewer.md's canonical rubric; only the prompt differs. Delegation keeps each review in an isolated context.
If Pass 1 returns critical findings: Skip Pass 2. Fix findings, re-run gate check, then re-invoke Pass 1 on round 2 — if round 2 still fails, escalate.
Resolve <base-ref> once before either pass: feature branch's merge base (e.g., git merge-base HEAD main), HEAD~1 on a hotfix on main, or HEAD if Phase 2 left uncommitted changes.
Pass 1: Spec Compliance
Runs only if specs/requirements.md exists. Otherwise skip silently and run Pass 2 as the sole review (graceful degradation).
d. Invoke code-reviewer via the Agent tool with this prompt:
Pass 1 — Spec Compliance. Check whether every change in the diff traces to an acceptance criterion in specs/requirements.md, and flag any code with no corresponding AC (undocumented behavior).
Changed files (name + status):
<paste output of: git diff --name-status <base-ref>>
Acceptance criteria from Phase 1 (this IS your concern):
<paste AC checklist>
Read specs/requirements.md directly. Use your Read tool to open each changed file. Return findings in the Pass-Specific Return Contracts shape documented in agents/code-reviewer.md (one line per AC: OK or CONCERN, plus OVERALL).
e. Integrate Pass 1:
OVERALL: OK→ Pass 1 passes; run Pass 2.OVERALL: CONCERNS(critical: undocumented features or missing AC coverage) → fail-fast. Skip Pass 2. Report Pass 2 as the literal linePass 2: Skipped (Pass 1 critical findings)— never silently omitted. Fix findings, re-run gate check, then re-invoke Pass 1 on round 2 — if round 2 still fails, escalate.
Pass 2: Code Quality
Runs only if Pass 1 returned OVERALL: OK, or Pass 1 was skipped because specs/ does not exist.
f. Invoke code-reviewer via the Agent tool with this prompt:
Pass 2 — Code Quality. Review changes against the canonical rubric (quality, security, patterns, stack-specific). Do NOT check spec compliance — Pass 1 (or /spec-review) owns that.
Changed files (name + status):
<paste output of: git diff --name-status <base-ref>>
Acceptance criteria from Phase 1 (context only, not your concern):
<paste AC checklist>
Read the project's CLAUDE.md for stack-specific patterns. Use your Read tool to open each changed file you need to inspect — the caller has not inlined the diff bodies. Return findings in the exact shape documented at the bottom of agents/code-reviewer.md.
g. Integrate Pass 2 — one line per criterion (<criterion> — OK or <criterion> — CONCERN: file:line — <reason>), ending with OVERALL: OK or OVERALL: CONCERNS (N).
OVERALL: OK→ Stage B passes; proceed to Stage C.OVERALL: CONCERNS→ fix each concern, re-run gate check, then re-invoke Pass 2 if you're still on round 1. On round 2, escalate.- Either subagent errors or returns an unparseable shape → fall back to reading
agents/code-reviewer.mdand executing the corresponding pass's rubric inline. Never skip Stage B because delegation failed.
Stage B report aggregates under two subheadings: ### Pass 1: Spec Compliance and ### Pass 2: Code Quality. The Pass 2 block must exist even when skipped (use the literal skipped line above). Advisory-note capture: when the round-2 escalation routes one or more Pass 2 CONCERNS to advisory rather than gate-blocking, capture each in advisoryNote[] — record schema { pass: 2, concern, rationale, classification: 'advisory' } — before exiting Stage B. The array threads into Phase 4 step 14 and § Milestone Archival via a single shared formatter; without this capture, advisory concerns disappear from claude -p runs (caught by an earlier smoke-test). Full schema + rationale: docs/implement-reference.md § Advisory Notes.
Stage C — Hardening (first round only)
After Stage B passes on round 1, run a hardening pass. Skip on round 2 (diminishing returns). Cover negative/edge-case tests + an error-path audit. See docs/implement-reference.md § Phase 3 Stage C — Hardening Pass.
Decision (deterministic, not vibes)
h. Decision: GATE PASSED ⇒ exit loop. GATE PASSED WITH NOTES ⇒ carry notes into Phase 4 report, exit loop. Issues on round 1 ⇒ fix + re-run gate + go to round 2. Issues on round 2 ⇒ escalate to user. Full matrix: docs/implement-reference.md § Decision matrix.
i. After any fix — re-run the full gate fresh, cite actual numbers (e.g., "47 tests, 0 failures, 0 errors"). Never claim clean from memory.
Phase 4: Report & Handoff
Four labeled sub-steps in order: Phase 4a (gate-check passed — no new logic), Phase 4b (doc fragment hook; writes docs/.pending/<fr-id>.md when docs generation is enabled), Phase 4c (report at step 14 + human approval at step 15), Phase 4d (Close procedure: commit → Provider.releaseLock → Provider.getTicketStatus).
Phase 4b: Doc fragment
Non-blocking hook between Phase 4a (gate pass) and Phase 4c (report + approval). Call readDocsConfig(CLAUDE.md) from adapters/_shared/src/docs_config.ts; both userFacingMode and packagesMode false (or ## Docs absent) ⇒ silent no-op, zero output. Otherwise run /docs --quick with a 60s timeout (FR ID resolves via branch_template: mapping, diff scan, or _unbound-<ts> fallback — no new flag). Grade the run by its docs-run: outcome line (/docs § 0 (c)), the last line it prints — never by its exit code, which is 0 both for a fragment that was written and for an empty-set no-op that deliberately wrote nothing. docs-run: written ⇒ append | Doc fragment | added | docs/.pending/<fr-id>.md | — | to the Spec Deviation Summary table; docs-run: no-op ⇒ append | Doc fragment | none | — | no doc-relevant changes in this FR's diff | and NOT added — reporting a fragment that was deliberately never written is precisely the false row the outcome line exists to close; on non-zero exit / thrown error / 60s timeout (text: timeout after 60s) / a run that printed no docs-run: line at all append | Doc fragment | skipped (error) | — | /docs --quick failed: <first-line-of-error>. Run manually after commit to retry. | and continue to Phase 4c — the implementation commit never blocks on a failed fragment write. Full decision table (resolver fallback ordering, log shape): docs/implement-reference.md § Phase 4b Doc Fragment Hook.
Phase 4b' — cross-cutting spec propagation (STE-215). Between the doc-fragment hook and Phase 4c. Derive deletedFiles[] from git diff --name-status <baseline>..HEAD --diff-filter=D (never recall from session memory) and call scanCrossCuttingSpecRefs(removedPath, specsDir) from adapters/_shared/src/scan_cross_cutting_spec_refs.ts per deleted path — the helper is detection-only, returning per-file lists of {line, snippet, kind: 'treeLeaf' | 'proseMention'} hits. For every treeLeaf hit, delete that line in specs/technical-spec.md / specs/testing-spec.md via the Edit tool (the line is inside a ``` fence — drop it whole; never rewrite surrounding text). Prose-mention hits stay untouched. ≥1 hit across either spec ⇒ emit one follow-up commit between the implementation commit and any archival commit; zero hits ⇒ silent no-op. Render its message with buildPropagationCommitMessage(removedPaths, proseMentions) from adapters/_shared/src/propagation_commit_message.ts, never by hand: the subject is the fixed, path-independent chore(specs): propagate file removal to cross-cutting specs, which can therefore never breach the commit-msg hook's 72-character cap, while every removed path and every prose-mention file:line + snippet is recorded in the commit body, which the hook does not read, so the operator can amend a restructured sentence in a follow-up. The cross_cutting_spec_stale_file_refs /gate-check probe is the read-side safety net for paths that bypass this. Full edit policy: docs/implement-reference.md § Phase 4b' Cross-Cutting Spec Propagation.
Phase 4b″ — Project Verification
Between the Phase 4b′ hook and Phase 4c (step-14 report). Resolve the project's optional check ("verification") skill through a shared discovery-precedence resolver, in strict order:
- Declared — call
readVerificationConfig(CLAUDE.md)for the## Verificationblock'sverify_skill. If set, use it verbatim (no scan). - Discover (fallback) — else call
scanCandidateCheckSkills(projectRoot)(adapters/_shared/src/scan_candidate_check_skills.ts) to scan.claude/skills/*/SKILL.mdfor candidates whose slug matches*drive*/*check*/*verify*or whose frontmatter carriesverify: true. Exactly one candidate ⇒ offer to adopt it — on accept, writeverify_skillinto the## Verificationblock, then use it; never silently run an undeclared skill. - Ambiguous — multiple candidates ⇒ list them and ask which to adopt; never guess.
- None — zero candidates and no declared
verify_skill⇒ the "no check declared" path. Rather than silently skip, offer to scaffold a check skill via the samescaffoldCheckSkillgenerator, or — for a small web project — to adopt the generic/dev-process-toolkit:visual-checkas the check. On accept ⇒ write the skill (MUST emitverify_skill_scaffolded). On decline ⇒ proceed to the step-14 report with a "no verification configured" note (MUST emitverify_skill_scaffold_declined). It never writes a skill without the offer.
Mandatory drive — a declared run_cmd. When the ## Verification block declares a run_cmd whose value is neither empty nor the literal none, the project has stated how it is run, and driving the resolved check skill in this phase is mandatory: it is not offered, never opt-in, and neither the path-2 adopt-offer nor the path-4 scaffold-offer gates whether it runs — on this path a resolution that yields nothing runnable is a failure, not a silent decline. What it changes is which mode the project defaults to, never what an explicit one means: resolveVerifyMode still gives a written verify_mode precedence, so a project that has written manual keeps the no-auto-run reminder path below. On the ran-the-drive path the step-14 report MUST emit verify_drive_mandatory, naming the declared run_cmd. A run_cmd that is absent, empty, or the literal none triggers none of this — every rule below stays exactly as it is, with no mandatory drive and no new token.
Non-interactive default (autonomous safety). The path-2 adopt-offer and path-4 scaffold / visual-check offer both carry a safe decline-default — they are advisory offers, not requires-input gates. In a non-interactive / non-TTY autonomous run (e.g. a claude -p chain), both offers default to decline (no adopt-write, no scaffold), proceed to the step-14 report with the "no verification configured" note (MUST emit verify_skill_none_declared), and never block — an autonomous /implement must never stall waiting on a verification offer. This safe decline-default does not apply when the ## Verification block declares a run_cmd that is neither empty nor the literal none — on that declared-runnable path the drive is mandatory (above), so a non-interactive run that cannot drive fails instead of declining itself: it still does not stall (the no-stall rule holds unchanged — it fails fast rather than waiting on an offer it is not allowed to decline), it never emits verify_skill_none_declared, and it MUST emit verify_drive_unavailable in its place, naming the declared run_cmd it could not drive. Unless that same block declares verify_mode: manual — an explicitly written mode still wins over the run_cmd-keyed default, so nothing was ever going to be auto-driven and there is no drive to be unavailable: that non-interactive run takes the manual reminder path exactly as an interactive one does, MUST emit verify_skill_manual_reminder, and never verify_drive_unavailable. An absent, empty, or none run_cmd keeps the decline-default above exactly as written.
Run placement + advisory report. When a check skill is resolved and the mode is not manual, run it here — after Phase 4a (gate-check GREEN) and the 4b/4b′ hooks, before the Phase 4c step-14 report — then render its outcome (pass/fail plus a short captured-output summary) as a row in that step-14 report. In manual mode /implement does not auto-run the skill; it prints a one-line reminder naming the resolved skill and how to run it. Design references travel with the run. Take the in-scope FR's design references from designReferencesForSpec(projectRoot, frSpecFile) — scanDesignReferences narrowed to that FR's own spec file, never a second parser — and shape them with renderDesignReferenceBlock(rows), the one shared renderer — both live in adapters/_shared/src/design_reference_block.ts, and its command-line front door, bun run plugins/dev-process-toolkit/adapters/_shared/src/design_reference_block.ts <projectRoot> <frSpecFile>, prints the block and the token together, so RUN it rather than re-emitting the block's shape by hand (a hand-written block is a second definition of the shape, which is the one thing this renderer exists to prevent): its rendered block is threaded into the resolved check skill's invocation, so the skill is invoked holding the images the FR was designed against. Every row names the reference's repo-root-relative path exactly as the FR wrote it, plus the caption authored beside it, verbatim — the renderer rewrites neither. An FR citing zero design references renders the empty string: nothing is threaded in, and the check skill's invocation stays byte-identical to today's. A cited reference whose path does not resolve on disk is rendered as unresolved-and-skipped — the renderer names it, marks it skipped, and carries on — and this phase never fails on it: probe #61 (design_references_resolve) already fails the gate for exactly that condition at error severity back in Phase 4a, so failing it a second time here would block the commit on a condition the gate already owns. The manual reminder renders the same block. The reminder path is not the quiet half: it prints the renderDesignReferenceBlock(rows) output beneath its one-line reminder, from the same call over the same rows the auto-run path threads into the invocation, so a verify_mode: manual project reads exactly the block an auto-running one would have been handed — the capability is not restricted to projects whose check skill auto-runs, and only the delivery differs. One token, every run. The step-14 report names which of the two dispositions fired, and exactly one of them fires per run — never both, never neither: MUST emit design_references_passed when the in-scope FR cited at least one design reference (whatever the rendered/skipped split, and on the auto-run and manual paths alike), and MUST emit design_references_none_cited when it cited none, each as a literal backticked token. renderDesignReferenceBlock decides it, not the caller: the token it returns beside the block IS the one reported, so the two can never disagree. The cited-none half is not ceremony — without it a vacuous run and a hand-off that silently dropped the block read identically in a closing summary.
Failure classification + propose (never auto-invoke). On a failing check, classify the failure as spec-gap (built behavior diverges from the intended design/spec) or impl-bug (a code defect), and surface a recommendation naming the exact next command — /dev-process-toolkit:brainstorm (reopen the design when the spec itself is wrong), /dev-process-toolkit:spec-write <FR-id> (amend an under-specified FR), or an inline fix (a self-contained code defect) — as a line in the step-14 report. /implement never auto-invokes any of them; it only proposes, and the human runs the recommended command.
verify_mode gating of the step-15 commit. verify_mode: blocking gates the Phase 4c step-15 commit approval — a failing check blocks the commit, which is not offered until the check passes or the operator explicitly types an override. The effective mode is resolved, never read straight off the key — call resolveVerifyMode(CLAUDE.md) (adapters/_shared/src/verification_config.ts): a written verify_mode always wins, and when the key is absent the default is blocking for a run_cmd declared as neither empty nor none, advisory otherwise. A drive that fails under that resolved default blocks the Phase 4c step-15 commit approval exactly as a written blocking does — no commit is offered until the drive passes or the operator overrides. An explicitly-set verify_mode: advisory reports a failing check in the step-14 report but the step-15 approval still proceeds — the human decides. verify_mode: manual never blocks (no auto-run).
Capability tokens (step-14 closing summary). The step-14 report emits exactly one outcome token (literal, backticked) per verification outcome: pass ⇒ MUST emit verify_skill_passed; fail under advisory ⇒ MUST emit verify_skill_failed_advisory; fail under blocking ⇒ MUST emit verify_skill_failed_blocking; manual-mode reminder ⇒ MUST emit verify_skill_manual_reminder; no check declared/discovered ⇒ MUST emit verify_skill_none_declared — except on the declared-runnable path (a run_cmd that is neither empty nor the literal none), where the mandatory-drive rule above substitutes verify_drive_unavailable for it, so the report still carries exactly one outcome token, a different one. Two further tokens are not outcome tokens and never compete with that one: when discovery in step 2 above auto-adopts a single candidate and writes verify_skill into ## Verification, MUST emit verify_skill_adopted for that adoption event, and when a declared run_cmd made the drive mandatory and it ran, verify_drive_mandatory records that drive event alongside whichever outcome token applies. these literals are the byte-checkable signal a reader or harness scans for, and narrative paraphrase is insufficient. NOTE: closing_summary_capability_keys does NOT enforce this family — its canonical key set carries no verify_* key, and it globs spec-write/gate-check/smoke-test rather than this file, so these directives are unenforced by any probe today. End-to-end authoring hook. A SEPARATE Phase 4 hook, not part of the verify_* family above and not covered by the NOTE that closes it. Same placement, before the step-14 report: when the ## Verification block declares an e2e_cmd that is a real command, this change owes end-to-end coverage — add or edit an end-to-end test covering it, run the suite, and resolve the decision through resolveE2eAuthoring in adapters/_shared/src/e2e_authoring.ts, which renders the e2e: evidence rows through the one shared renderer and refuses a green declaration when nothing was authored and nothing recorded. A change with no end-to-end observable surface RECORDS that explicitly with a non-blank reason, and that record — not the silence — is what emits end_to_end_none_needed and reads its reason back out: silence is not a decision, and a silent skip and a considered one leave identical trees. An absent e2e_cmd is vacuous; the literal none is an ANSWER (there is no suite) that emits end_to_end_none_needed with no reason recorded — a project with no suite has nothing to explain away, so the sentinel path is the one none-needed emission that carries a null reason — and the module asks isRunCmdNone / isRunCmdAnswered so the three states never collapse into two. The hook emits exactly one literal backticked token for the authoring outcome: an added end-to-end test ⇒ MUST emit end_to_end_tests_authored; an edited existing one ⇒ MUST emit end_to_end_tests_edited; a recorded non-blank none-needed reason ⇒ MUST emit end_to_end_none_needed; and a red suite outranks all three ⇒ MUST emit end_to_end_suite_red in place of whichever authoring token would otherwise apply. These literals are the byte-checkable signal, and narrative paraphrase is insufficient. Unlike the verify_* family, all four ARE in closing_summary_capability_keys' canonical key set, and the probe enforces a matching MUST-emit directive for each in /spec-write § 7's static map — the four directives here are the emission-site copy of those, so a rename has to move on both surfaces together. The hook renders no evidence block of its own: its captured end-to-end run is fed into step 14's single renderImplementReportEvidence call below, so one block carries gate:, drive: and e2e: each from its real capture — read alone the hook's own rows say - (none found) for gate: and drive:, captures it was never handed, and a second block would re-split the one source of truth.
Authoring a check skill. The scaffold-offer (step 4 above) and hand-rolled checks both follow docs/verification-skills.md (authoring guide) — see also patterns.md § Pattern 30 (Project-Authored Verification Skills).
Commit message format
Phase 4 commits use Conventional Commits v1.0.0 — enforced by the commit-msg hook installed by /setup. The proposed <type>(<scope>): <title> (≤ 72 chars) plus body and Refs: STE-<N> footer is rendered to the user at the step 14 approval gate before step 15 commits, so the user can redirect type/scope before commit. Use ! for breaking changes (feat(api)!:, feat!:). Full type heuristic table, scope-selection rules, and canonical example: docs/implement-reference.md § Commit message format.
Spec Deviation Summary
Before updating specs, compile all deviations discovered during Phase 2 into a table with columns Deviation | Classification | Resolution | Needs Confirmation?. Classification ∈ underspecified / ambiguous / contradicts / infeasible. Any row with Classification = ambiguous must have Needs Confirmation? = **Yes**.
Milestone Archival
After the human approves the Phase 4 report (step 15), and only then, archive every FR belonging to the completed milestone plus the milestone's plan file. This keeps specs/frs/ and specs/plan/ size bounded.
- technical-spec.md is never auto-archived — ADRs use
Superseded-by:in place./implementarchival touches onlyspecs/frs/**andspecs/plan/<M#>.md. specs/design/is immutable across archival — design-reference images underspecs/design/are nevergit mv'd intoarchive/and never link-rewritten on archival; only the spec markdown moves, the referenced images stay put.- Run archival only after explicit human approval in step 15, never before. If the user asks for changes instead, abort archival entirely.
- Single-FR runs (
/implement <FR-id>) intentionally leavestatus: active; bulk archive a completed milestone via/spec-archive M<N>before running/ship-milestone. - Milestone-binding assertion (tracker mode,
project_milestone: true). Before the archival sweep below, for each FR being archived callassertMilestoneBindingAtArchive(provider, project, frFile, { projectRoot, mode })fromadapters/_shared/src/assert_milestone_binding_at_archive.ts: it fetches the FR's bound ticket and verifies the adapter-aware milestone binding — all three kinds the helper handles (object→ an identifier-keyedM_<key>milestone derivesprojectMilestone.idforward to the token, a grandfathered numericM<N>byte-equals the plan-heading canonical name;epic→ the ticket'sparentkey sanitizes back to the Epic-keyed milestone token, degrading to the label surface for grandfathered numeric milestones;label→labelscontainsmilestone-<M-token>) — attaching once on a miss. Vacuous undermode: noneor on capability-gap adapters. Arefusedoutcome (milestone_label_archive_refused) blocks that FR's archival — skip itsgit mv, frontmatter flip, andreleaseLock; surface the NFR-10 detail VERBATIM (ticket, expected binding, and the refusal's OWN per-cause remedy — the gate refuses on six distinct causes and each carries a remedy that fits it; never substitute a generic backfill-or-attach line, which on three of those causes invites a duplicate write); the other FRs in the sweep proceed. Scope is FR-backed only: the assertion iterates exactly the FRs being archived and theirtracker:bindings — it never enumerates the tracker board; tickets without an FR are out of scope by construction. - Capability tokens (closing summary). Per FR in the archival sweep, the step-14 closing summary MUST emit
milestone_label_asserted_at_archiveon a passing assertion (binding present, or missing-then-attached) and MUST emitmilestone_label_archive_refusedon a refusal — exactly one literal backticked token per archived FR; narrative paraphrase is insufficient (/gate-check'sclosing_summary_capability_keysprobe greps the literal). - Token Stats re-render. Per in-scope FR, after the
git mv/ frontmatter flip and before the archive commit, re-render the FR's## Token Statsblock at its new archive path from the token ledger (.dpt/ledger/token-ledger.jsonl): select rows viaclaimRowsForFR(projectRoot, { branch, sessionLineage, brainstormClaim: <tracker-id> }), then callupsertTokenStatsBlock(frBody, rows)(both fromadapters/_shared/src/token_stats_render.ts) so the block now carries the FR'simplement+tddrows, andgit addthe archive path so the refresh lands in the same archive commit — no separate commit, no post-commit dirty tree, never a standalone working-tree mutation. Vacuous when the ledger is absent or has no rows for the FR: skip the upsert, write nothing. Opt-in gate: whenreadTokenStatsConfig(projectRoot).enabled === false(token stats disabled), skip this re-render — and itsgit add— entirely, writing nothing. Second pass — re-render after all claims settle.claimRowsForFRdemotes a row shared by two FRs from one session to thedesign/explorationbucket, and that demotion is retroactive, so an FR rendered earlier in this sweep can go stale mid-run. Once the sweep's last claim settles, re-render every in-scope FR's block once more from the settled ledger andgit addit — still the same archive commit. This second pass is unconditional and overrides the vacuous rule above: an FR whose rows all demoted now selects zero rows, and it must still be re-rendered (to a zeroed block) rather than skipped, or it keeps the inflated block the demotion just invalidated.
Procedure summary. For each archived FR, first run git status --porcelain specs/frs/<name> and feed the output to isFRUntrackedInPorcelain(porcelain, frPath) (adapters/_shared/src/spec_archive/stage_untracked_fr.ts). When it returns true, run git add specs/frs/<name> before git mv specs/frs/<name> specs/frs/archive/<name> — otherwise git mv falls back to plain mv for untracked files and git log --follow loses rename history (smoke #6 F2). Then (STE-210 AC-STE-210.2 — staging-order reorder for tracked-modified FRs): (1) git mv specs/frs/<name> specs/frs/archive/<name> first (stem preserved per NFR-15; the rename is staged with the working-tree content at this moment), (2) edit the frontmatter at the new archive path (flip status: active → archived + set archived_at: <ISO now>), (3) git add specs/frs/archive/<name> to re-stage the post-edit content. Editing frontmatter before git mv would stage the rename with un-flipped index content (the F11 bug — archive commits landing with status: active). Optional convenience: archiveFRWithFlip(repoRoot, frPath, archivedAt) → archivePath from adapters/_shared/src/archive_fr.ts runs the post-git mv frontmatter flip and returns the absolute archive path so the caller threads it into git add. The three-step caller pattern is git mv → archiveFRWithFlip(...) → git add (the helper covers step 2 only; the git invocations stay in skill prose where the Bash tool runs them). archived_at precision: full ISO-8601 with date + time + Z (e.g., 2026-04-30T17:23:11Z); not date-only with zeroed time (2026-04-30T00:00:00Z is the regression shape). Render the wall-clock instant of the archive commit via date -u +%Y-%m-%dT%H:%M:%SZ, never the shorter date +%Y-%m-%d form — the latter rounds to midnight UTC. An earlier smoke caught the LLM-rendered midnight value; the prose is now pedantic so the prompt is unambiguous. Plan-status flip: git mv specs/plan/<M#>.md specs/plan/archive/<M#>.md and apply the same status: active → archived flip + archived_at to the plan frontmatter — same atomic commit, same full-ISO-8601 timestamp (date + time + Z). Frontmatter-prepend (legacy plans, STE-197 AC-STE-197.4): if the plan file lacks a --- YAML frontmatter block at the very top, prepend ---\nstatus: archived\narchived_at: <ISO timestamp>\n---\n\n before the # Implementation Plan H1, in the same atomic commit. Backwards-compat for /setup-generated plans written before STE-197 shipped (no frontmatter to flip — synthesize one with the archived state directly). Before staging, call rewriteArchiveLinks(repoRoot, frId) per FR (adapters/_shared/src/spec_archive/rewrite_links.ts) to rewrite traceability references in specs/requirements.md, active plans, archive plans, and CHANGELOG's unreleased prefix. Append shipped-AC traceability row. Per archived FR, call appendTraceabilityRow(repoRoot, frId, acNumbers, implFiles, testFiles) from adapters/_shared/src/spec_archive/append_traceability_row.ts — acNumbers is the list of AC indices that shipped (1, 2, 3, … from the FR's ## Acceptance Criteria block); implFiles and testFiles are derived from git diff --name-status <baseline>..HEAD --diff-filter=AM filtered for non-test (implFiles) and *.test.* (testFiles) paths. The helper appends one row of shape `| AC-... | | <test-fi
Truncated - read the full file at https://github.com/nesquikm/dev-process-toolkit/blob/85fd645c9d8aa8adedc2cbff768ffabe0aece963/plugins/dev-process-toolkit/skills/implement/SKILL.md.