---
name: spec-workflow
description: >
  Drive the spec-driven lifecycle for any non-trivial work item: SPIDR-split a
  new spec into vertical slices, transition state markers (DRAFT → READY_FOR_REVIEW
  → READY_FOR_IMPLEMENTATION → IN_PROGRESS → REVIEWED → RECONCILED → DONE; also
  DEFERRED for parked slices with a stated resolution trigger), enforce the
  reconciliation checklist before commit, and surface stale specs/ADRs whose
  `last_verified` date has aged past dependency changes. Use when starting
  non-trivial new work, creating a spec, transitioning a slice's state,
  parking a slice as DEFERRED, reconciling a reviewed slice, or auditing
  doc freshness. Do not use for quick one-off fixes that don't need a spec,
  or for bug-shaped work where `jig:bug-fix` is the better fit.
user-invocable: true
---

> **Working posture ([ADR-0056](../../docs/decisions/adr-0056-adversarial-register-quarantine.md)).**
> Adversarial review is a *named, bounded operation.* This workflow invokes
> review passes (frame-critique / craft), whose skeptical register belongs
> *inside* those isolated reviewer subagents. Outside a review, default to
> collaborative and solution-forward; don't carry the adversarial stance into
> ordinary conversation.

> Spec 003 promoted this skill from stub to active. The deterministic state
> mutations live in `workflow.py`; this SKILL.md drives the judgment layer.

## What this skill does

- Guides SPIDR-splitting a new spec into vertical slices (Spike last, not first —
  try Rules / Data / Interface / Path first).
- Flags slices that look like horizontal phasing (no user-facing layer touched).
- Drives the spec lifecycle state transitions via `workflow.py`.
- Coordinates implementer + reviewer subagent invocations at the right points.
- Enforces the reconciliation checklist before a slice goes DONE.
- Consults `docs/memory/glossary.md` when drafting ACs to surface unknown domain terms.
- Surfaces skill-routing observability via `workflow.py routing-stats [--days N]` —
  a read-only histogram of which skills fired (jig baseline vs. richer/"other"
  skill per category) from `.claude/skill-usage.jsonl` (slice 041-02).
- Surfaces use-case coverage via `workflow.py coverage [--project-dir DIR]` — a
  read-only, **advisory** (non-blocking), project-wide **bidirectional** check
  (slice 068-03): a deterministic set-difference over the `use_cases:` trace
  links that reports use cases with no implementing spec (coverage gap) and
  specs citing no parent use case (scope creep). No-op when the project has no
  `## Use cases` section.
- Guards the status board via `workflow.py check-board <project-dir>` — a
  read-only audit that exits non-zero when `docs/specs/README.md` no longer
  matches the spec records, or when two spec directories claim one number.
  Writes nothing, so it is safe to run in CI against a checkout.
- Surfaces gate-bypass telemetry via `workflow.py gate-stats [--days N]` — a
  read-only per-gate histogram of how often each gate honored its env-var
  override (an override-frequency audit trail, not a gate-value verdict) from
  `.claude/skill-usage.jsonl` (slice 078-02).

## SPIDR splitting

All non-trivial specs are SPIDR-split into vertical slices before
implementation begins. **Spike is the last resort — try Path /
Interface / Data / Rules first.**

- **S — Spike**: research/learning activity. Only when none of P/I/D/R
  apply. AI agents default to spiking too eagerly — resist.
- **P — Path**: split by alternative paths through the story (happy
  path first, edge paths later).
- **I — Interface**: split by UI / platform / channel (minimal first,
  polish later).
- **D — Data**: split by data subset or format (less data first).
- **R — Rules**: split by business rules (simple first, edge cases later).

**Anti-horizontal-phasing rule:** every slice must touch the
user-facing layer and deliver end-to-end value. A slice that touches
only the DB or only the parser is horizontal phasing — re-split.

See [`worked-example-spidr-split.md`](worked-example-spidr-split.md)
for one applied example per axis plus a jig-native dogfood case (spec
017's three-axis split). The canonical primer for all five axes lives
at [`docs/spec-workflow/spidr-primer.md`](../../docs/spec-workflow/spidr-primer.md).

### Spike slices

When SPIDR's S axis fires during decomposition (none of P / I / D / R
apply because the team doesn't yet know enough to pick), the
resulting slice is marked `kind: spike` in its frontmatter — the
typed enum that `spec_lint.py` validates.

**When to introduce a spike during decomposition.** Reach for S only
after trying R / D / I / P. The bias to resist is "let me research
this first" as a prelude to "now let me build it as one big slab" —
that is horizontal phasing in a trench coat. If the spike would
conclude with "now ship the implementation," the implementation IS
the slice, and the research goes inside it.

**Body shape (four labelled blocks).** A `kind: spike` slice carries
four blocks alongside the standard Goal / DoR / AC / DoD scaffolding.
**Each label must be written with the trailing colon (`**Question:**`,
etc.) — that is what `spec_lint.py` matches against.**

- **Question:** — one sentence stating the open question. Set at DRAFT.
- **Time-box:** — explicit budget (e.g., "1 day", "4 hours"). Set at DRAFT.
- **Findings:** — bullet evidence collected during the spike. Filled
  during IN_PROGRESS.
- **Outcome:** — one of `ADR-NNNN created` / `spec NNN-NN unblocked` /
  `abandoned (reason)`. Multiple outcomes separated by `;`
  (e.g., `ADR-0007 created; spec 030-02 unblocked`). Set at DONE.

`spec_lint.py` soft-warns when a `kind: spike` slice is missing any of
the four labels — mid-flight spikes legitimately have empty Findings /
Outcome, so this is a warning, not a hard error.

**Always nested, never standalone.** Spike slices live inside a real
spec — never as a standalone `docs/spikes/` artifact. The
1-slice-spec case (no clear downstream spec yet, just an
investigation) collapses to "spawn a normal spec where the only slice
is `kind: spike`." This forces the investigator to articulate the
downstream change up front and keeps jig at two numbered families
(specs+slices, ADRs).

**Abandoned-spike manual-reshape failure mode.** When a spike's
Outcome is `abandoned (reason)`, dependents are NOT automatically
cascade-flagged. The human (or the next session) audits each
dependent slice and decides whether the original design still holds.
Automation here over-fires: "approach A abandoned" often means
"approach B from the same findings still satisfies the dependents."
`workflow.py` deliberately stays out of the cascade business; the
SKILL.md hand-off is the documented gate.

## How to use

### Creating a new spec

0. **Step 0 — confirm the project is scaffolded (spec 063 / ADR-0011).**
   BEFORE reserving a number or drafting ANY `docs/specs/` or slice
   structure, confirm this project is a scaffolded jig project. If it
   isn't, **route — do not hand-roll directories**:
   - **Greenfield** (no jig structure yet) → tell the user to run
     `/jig:scaffold-init`. It lays down conventions, templates, hooks, the
     status board, and a seed reference spec.
   - **Existing spec/`slices/` layout, but not jig-scaffolded** (no
     `scaffold.json`) → tell the user to run `/jig:migrate`. It adopts the
     existing layout into jig structure.

   You don't have to decide the state yourself: `workflow.py new` (step 2)
   **classifies and routes** for you (spec 063-01) — a `scaffold.json`-bearing
   project proceeds; a greenfield project is refused naming
   `/jig:scaffold-init`; an adoptable spec-driven project is refused naming
   `/jig:migrate`. The deterministic gate and this human-readable
   precondition agree by construction, so **don't restate the detection
   heuristic here** — run the helper and let it route. (Bypass for a
   deliberate out-of-band flow: `JIG_SCAFFOLD_PRECONDITION=0`.)

   **The anti-pattern this step exists to kill:** an auto-triggered
   `spec-workflow` run improvising a loose `slices/` folder (or any ad-hoc
   `docs/specs/` skeleton) because `/jig:scaffold-init` was skipped. That
   produces a non-jig layout that then needs migrating — exactly the
   reported failure. When in doubt, route to setup first; never invent the
   structure by hand.

1. Confirm the work needs a spec. Trivial fixes don't.
   - **Reference moved? Reframe first.** If a *load-bearing reference* changed
     from outside the system (a design system, vendor / API contract, test infra,
     compliance regime, platform, or product-positioning / strategic-vision
     shift), reach for `/jig:reframe` **before** drafting — re-baseline the corpus
     onto the new reference so you draft against it, not the dead premise (spec
     067 / [ADR-0024](../../docs/decisions/adr-0024-reference-reframe.md)).
1a. **Read the vision `## Use cases` section as framing — before drafting
   (spec 068-02 / [ADR-0025](../../docs/decisions/adr-0025-use-cases-breadth-layer.md)).**
   If the project's `docs/product-vision.md` carries a `## Use cases` section
   (the breadth-layer behaviors captured at init), **read it first** as framing
   context for this spec — *which captured behavior does this work serve?* The
   section is the shared frame specs anchor against; reading it before you draft
   is what keeps behavior-dense projects from each spec inventing its own slice
   of the world. (If there is **no** `## Use cases` section, the layer isn't
   adopted for this project — skip this step; nothing here applies.) You record
   the answer as a trace link in the spec's `use_cases:` frontmatter (step 2a).
1b. **Cross-check tracked bugs before drafting ACs.** For feedback/triage
   specs, read `docs/bugs/README.md` and any overlapping `docs/bugs/NNN-*.md`
   record before turning reported symptoms into acceptance criteria. If an
   item is a tracked defect with a bug record, route it through `bug-fix`
   (or link to that bug as an explicit dependency) instead of copying the
   defect into the spec as a second owner. Keep polish/design/new-behaviour
   items in the spec.
2. **Reserve the next free number on origin/main:**

   ```bash
   python3 ".github/skills/spec-workflow/workflow.py" new <slug>
   ```

   The helper computes `max(NNN) + 1` across `docs/specs/`, writes a
   minimum stub `docs/specs/NNN-<slug>/spec.md` (frontmatter + Overview
   + SPIDR-analysis headers), commits it as
   `docs(specs): reserve NNN-<slug>`, and pushes to `origin/main`. If
   the push is refused by branch protection / permissions, the helper
   automatically falls back to a `reserve/NNN-<slug>` branch + `gh pr
   create`. This locks the number **team-wide** before any drafting
   begins, killing the parallel-worktree spec-number-collision failure
   mode logged across specs 014/015/016/017.

   **Works from any branch or worktree** (ADR-0015 / spec 051). The
   helper routes on the current branch: on `main` it runs the proven
   in-place flow (clean tree required, since the commit lands on local
   `main`); off `main` — a feature branch or a linked `.claude/worktrees/*`
   worktree — it builds the reservation commit in an *ephemeral detached
   worktree* checked out at `origin/main` and pushes it by SHA, never
   touching your branch, cwd, or working tree. You no longer need to
   switch to `main` (and a linked worktree can't, anyway).

   Flags: `--no-push` for solo machines without a remote, or for an
   off-main *provisional* reservation committed on the current branch
   (the number is local-view and may collide at merge — treat it as
   provisional); `--pr` to skip the direct-push attempt on
   protection-locked main.
2a. **Record the use-case trace link — and grow the vision on discovery
   (spec 068-02 / [ADR-0025](../../docs/decisions/adr-0025-use-cases-breadth-layer.md)).**
   The stub seeds an empty `use_cases:` frontmatter list. Fill it with the
   `UC-N` id(s) (from the vision `## Use cases` section, step 1a) this spec
   serves — the `dependencies:`-style flow-list shape, e.g.
   `use_cases: [UC-1, UC-3]`. This is the machine-resolvable trace link the
   reconcile-phase coverage check (slice 03) reads.

   **The discipline is soft — an empty/absent `use_cases:` never blocks a
   transition (AC4 / [ADR-0011](../../docs/decisions/adr-0011-spec-gate-model.md)).
   But it is not silent.** The trigger is **mechanical and deterministic** — the
   `classify_spec` predicate in
   [`skills/_common/use_cases.py`](../_common/use_cases.py) computes one of
   `no_section` / `empty` / `resolved` / `unresolvable` for this spec — **not** a
   voluntary "is this new?" self-report. **Whenever `classify_spec` returns `empty`
   or `unresolvable`** (the spec cites nothing, or cites a `UC-N` with no match
   in the vision) at draft/framing, surface a **three-path prompt** — every path
   is **one step** and **none blocks drafting**:

   - **(a) cite an existing use case** — this spec serves a behavior already in
     the vision: put its `UC-N` id(s) in `use_cases:`. Done.
   - **(b) grow the vision** — this spec serves a behavior **not yet captured**:
     **reuse `vision-elicitation`'s capture loop, seeded with the existing
     entries** (so the author sees the current set), → normalize → **confirm**,
     then **write additively** (append, never discard-and-replace) and **assign
     the next free `UC-N`** (`use_cases.next_use_case_id` allocates `max + 1`;
     retired numbers are never reused). The **confirm step guards grow quality**
     so a reachable trigger can't silently bloat the section: **(i)** enforce
     **goal-level grain** — reject spec-shaped / requirements-level phrasing,
     re-running slice 01's normalize (`"[actor] can [goal]"`); **(ii)** run a
     **near-duplicate check** against the seeded existing entries
     (`use_cases.is_near_duplicate`) — on an apparent match, **route back to
     path (a)-cite** rather than minting a duplicate. Then record the new id in
     `use_cases:`.
   - **(c) decline** — legitimately untraced (infra / refactor / no user-facing
     behavior) or defer: leave `use_cases: []`. The vision is unchanged, and any
     resulting gap is slice 03's advisory coverage backstop. No-op.

   **CRITICAL — the no-section no-op.** When `classify_spec` returns `no_section`
   (the project has **no** `## Use cases` section — the breadth layer is **not
   adopted**, e.g. jig's own repo), **the prompt is suppressed entirely**:
   nothing prompts and nothing errors. A project with specs but no use-case
   layer is wholly unaffected. The trigger fires **only** on `empty` /
   `unresolvable`, which presuppose the section exists.
3. Create `docs/specs/NNN-<slug>/{spec.md,plan.md,tasks.md}` with the conventional
   structure: status frontmatter, overview, SPIDR analysis, ordered slices.
4. SPIDR-split: for each slice, the goal is **one vertical piece** that delivers
   end-to-end value. Spike is the last resort, not the first reach.
5. Each slice is a whole-file document with the canonical frontmatter shape
   (`status`, `dependencies`, `last_verified`) plus DoR / AC / DoD / Close-out
   sections. `workflow.py new` (step 2) already emitted a well-formed starter
   `slice-01-tbd.md` from the packaged slice template, so you never hand-resolve
   a template path; add any further slices in that same shape. For the
   **structural reference** — what a filled-in slice looks like — mirror the
   in-project worked example that scaffolding installs at project root:
   `docs/specs/001-adopt-jig/` (`spec.md` + `slice-01-bootstrap.md`), the first
   spec, which your project's `CLAUDE.md` names as the worked example to
   imitate. Set `status: DRAFT` in the frontmatter. Legacy slices that use prose
   `**STATUS: DRAFT**` markers still work (lazy migration); no need to rewrite
   them.
5a. **Design-fidelity authoring nudge (spec 104-02 / ADR-0049).** When a
   slice you just wrote in step 5 ships **visual design** (a mockup, a design
   system spec, a screen with colours/spacing/sizes/layout rules to hit),
   don't let "doesn't match the mockup" live only in a picture:
   - **(a) Extract the design values into checkable ACs.** Pull the concrete
     values — colours, spacing, sizes, layout rules — out of the mockup and
     write them as this slice's acceptance criteria, the same way any other
     observable behavior becomes an AC. This is what turns a fuzzy "looks
     right" into something a reviewer (or an eval) can actually check.
   - **(b) When fidelity must *gate*, wire the servo rail.** If a screen's
     visual fidelity needs to be a hard, enforced condition of `DONE` — not
     just an eyeballed check — set `design_review: true` in the slice
     frontmatter and wire a servo `design-eval` (screenshots the running app
     against the reference, scores it with a pinned vision judge) as the
     done-condition. `design_review: true` is attested, read-only, at
     `REVIEWED` by spec 071's design-review pass (the deriver
     `slice_needs_design_review` in `workflow.py` reads the flag) — jig never
     re-derives the eval score itself.

   **Graduated, not mandatory — jig offers, never forces, servo.** Not
   every screen earns a frozen eval:
   - **Low-stakes visual polish** → design-values-in-ACs plus
     attest-by-eyeball at review time is enough; no servo `design-eval`
     required.
   - **A hard fidelity gate** (fidelity must not regress, or is a stated
     product requirement) → servo `design-eval` + `design_review: true`.

   Pick the tier that matches the stakes; when in doubt, start with (a) and
   add (b) only when eyeballing genuinely isn't enough. See
   [spec 071](../../docs/specs/071-design-review-pass/spec.md) and
   [ADR-0049](../../docs/decisions/adr-0049-design-fidelity-routing-to-originating-spec.md)
   for the full routing rationale; this step adds no new mechanism — teeth
   stay anchored to the existing `design_review` flag.
6. **Ground your factual claims (spec 064-02 / ADR-0020 §1–§2).** Any
   load-bearing factual claim about a *runnable* surface — library/API
   capability, version/perf behavior, behavior of existing code — must be
   backed by an **executed probe** (run the command, read the source /
   `node_modules`) or a citation. **A universal or negative claim** ("the
   only", "never", "always", "one-way", "nothing reads", "otherwise clean")
   is established by an ***enumeration*** — a search you can show returns the
   *complete* set — **not a single positive citation** ([ADR-0052](../../docs/decisions/adr-0052-grounding-enumeration-for-universal-claims.md)):
   one true example proves nothing about the rest of the set, and these are the
   highest-value claims a future reader relies on. To claim enumeration, **state
   why the search is exhaustive** — what closes the set so nothing escapes.
   Some sets are closed by syntax and this is easy (imports in a package,
   call-sites in a repo); many only *look* `grep`-bounded — a "nothing reads
   this" search misses dynamic / reflective / ORM / string-built / config-wired /
   codegen'd / cross-repo access (illustrative, **not a checklist to clear**: the
   burden is to show the search captures every member, not to rule out named
   escapes). When you cannot show the search is exhaustive, an **empty result is
   absence of evidence, not an enumeration**: weaken the claim, tighten the
   boundary until the search genuinely closes the set, or move it to
   `## Assumptions`. Never dress an empty search as an enumeration — the
   frame-critique reviewer treats "I searched and it was empty" as *un*grounded
   until you have shown what closes the set. Everything you cannot verify goes in
   the spec stub's risk-gated `## Assumptions` section, marked explicitly — never
   asserted as fact. This **makes mandatory + derived** the existing informal
   "Current state (verified …)" discipline that the 064-01 retro found jig
   already half-practices by hand: it was grounding-by-probe all along, just
   reliant on author diligence. The `## Assumptions` you surface here has
   downstream value — slice 064-04 derives the `frame_review` trigger
   mechanically from it, so honest framing now is what decides later whether
   the adversarial frame-critique pass fires. The section is risk-gated: write
   "None" / omit when there are no unverified load-bearing assumptions; don't
   pad with boilerplate. (For a worked example of marked assumptions plus
   probe-grounded claims, see [ADR-0020](../../docs/decisions/adr-0020-spec-frame-hardening.md)
   `## Assumptions` A1–A4 + `## Kill criteria`, and the
   [spec 064-01 retro](../../docs/specs/064-spec-frame-hardening/retro.md),
   which probe-verified its three most load-bearing claims before recording
   them.)
7. **Let the assumptions decide `frame_review` (spec 064-04 / ADR-0020).**
   You are **not** asked "is frame-review needed?" — the `## Assumptions` you
   just surfaced decide it, mechanically. Set the slice's `frame_review` flag
   from `workflow.py frame-review-needed`:

   ```bash
   python3 ".github/skills/spec-workflow/workflow.py" \
     frame-review-needed "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>"
   ```

   The rule is a derivation, not a judgment call: `true` iff the slice's
   `## Assumptions` section carries ≥1 real (non-placeholder) assumption —
   so honest framing in step 6 is exactly what fires (or silences) the
   adversarial frame-critique pass. An inline-mirror / refactor slice with
   no unverified assumptions (`## Assumptions` absent or just "None") stays
   default-off. **ADRs are always-on** (OQ3): any ADR gets `frame_review:
   true` unconditionally — the deriver returns `true` for any `adr-*.md`
   path. When the value is `true`, set `frame_review: true` in the slice
   frontmatter so the gate + `session-plan` dispatch the pass.
8. Add rows to `docs/specs/README.md` (or regenerate via `workflow.py status-board`).

### Picking up a slice

1. Read the automatic `jig hint:` project-orientation headline injected at
   `SessionStart`, or refresh it manually before choosing work:
   ```bash
   python3 ".github/skills/spec-workflow/workflow.py" orient \
     --project-dir .
   ```
   The headline is computed from `scaffold.json` and lifecycle artifacts. Treat
   `docs/architecture.md`, the spec corpus, and the status board as authoritative;
   a shallow source-tree listing is not evidence that a scaffolded project is
   greenfield or that recorded stack decisions are absent.
2. Check `docs/specs/README.md` for the next slice in `READY_FOR_IMPLEMENTATION`
   (or `DRAFT` for a slice you intend to plan now).
3. Run:
   ```bash
   python3 ".github/skills/spec-workflow/workflow.py" transition \
     "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" IN_PROGRESS
   ```
   **Claim-on-working-state (spec 049-01, amended by
   [ADR-0045](../../docs/decisions/adr-0045-slice-claim-covers-active-lifecycle.md)).**
   On a frontmatter (file-per-slice) slice, a transition into a **working
   state** — `READY_FOR_REVIEW` / `IN_PROGRESS` / `REVIEWED` / `RECONCILED` —
   stamps `claimed_by:` (the current branch name, or `JIG_CLAIM_ID`), so
   spec-level work is marked too, not just implementation. Entering a **release
   point** clears it: the two pickup-queue states `DRAFT` /
   `READY_FOR_IMPLEMENTATION` (step 2 above tells you to choose work from
   exactly those, so a leftover owner there would mark a free slice as
   occupied), plus the terminal `DONE` / `DEFERRED` / `ABANDONED`.

   It **refuses** only when the slice is already `IN_PROGRESS` under a
   *different* identifier and you are moving it to `IN_PROGRESS` (naming the
   holder, pointing at `--release`); any other foreign claim — on your copy or
   on `origin/main` — is a loud **non-blocking warning**, because two sessions
   working one spec can be legitimate. The claim is **local by default**; add
   `--push` (direct) or `--pr` (via PR) to reserve it on `origin/main` so other
   worktrees see it, at any working state, though only an `IN_PROGRESS`
   reservation also publishes `status:` there (race / protected-branch handling
   mirrors `workflow.py new`). At a working state that reservation is
   **best-effort** (for a target other than `IN_PROGRESS`): if the trunk copy is
   already `status: IN_PROGRESS` under someone else's claim or none, it warns and
   pushes nothing, because that state is what the start-of-build guard
   hard-blocks on — stamping a claim over it would move a live lock, or
   manufacture the enforced pair on an unclaimed copy. Your own trunk claim just
   reports a benign no-op. To force-release a stale claim: `transition
   <spec> <slice> <state> --release --reason "<why>"` (clears `claimed_by:`,
   logs to `## Release log`).

   **Class-A cross-ref guard (spec 112 / ADR-0058).** A transition into a
   working state is *refused* when the slice is already `DONE` on `origin/main`
   — a stale branch re-advancing already-integrated work. The `→ IN_PROGRESS`
   case is caught by the start-collision guard (spec 051-04); the other working
   states (`READY_FOR_REVIEW` / `REVIEWED` / `RECONCILED`) by
   `_refuse_integrated_advance`, which reads the shared
   `cross_ref_state.identifier_state_on_ref` primitive. For a sanctioned re-open
   / supersession of integrated work, pass **`--reopen`** (a first-class,
   audited bypass distinct from the blanket `JIG_CROSSREF_GATE=0`); the
   `→ IN_PROGRESS` path's own escape remains `JIG_START_COLLISION_GATE=0`
   (convergence tracked in `docs/refinement-todo.md`). Unreachable `origin/main`
   → non-blocking warning, transition proceeds.

   **Do not read a blank `claimed_by:` as "free".** It means *no claim is
   recorded*: claims are local unless pushed, so another worktree's unpushed
   claim is invisible, and a plain `Edit`-tool write to a slice takes no claim
   at all. A claim that IS present names the session that last *moved* the
   slice into a working state — a presence hint, not a live lock. When it
   matters, ask rather than assume — see
   [bug 014](../../docs/bugs/014-slice-claim-covers-only-in-progress.md).
4. Fill in / refresh `plan.md` and `tasks.md` for the slice.
5. Spawn the `implementer` subagent with the spec path. Prefix the Task prompt
   with `[jig:phase=implementation] [jig:spec=NNN] [jig:slice=NNN-NN]` so
   `jig-telemetry.sh` can attribute implementation-phase cost. Implementer
   writes the deliverable to disk (TDD — failing tests first).

### After implementation

Slices 031-01 + 031-02 + 060-05 wired a **multi-pass review flow** into the
post-implementation step. Every slice runs through two passes before the
`IN_PROGRESS → REVIEWED` transition; two further passes fire on demand —
the **arch** pass when the slice declares `arch_review: true`, and the
**code-health** pass when it declares `code_health_review: true`.

The orchestrator runs the passes in this order:

1. **Compliance pass — `jig:independent-review`** (always). Spawn the
   `reviewer` subagent against the deliverable using the prompt built by
   `review.py implementation`. Reviewer is read-only; it evaluates each
   acceptance criterion and returns
   `pass | fail | needs-changes`.

2. **Craft pass — `pr-review`** (always). After the compliance pass
   returns, build the craft-pass prompt with `review.py pr-review` and
   spawn a second `reviewer`-shaped subagent. The reviewer is read-only
   (Read/Glob/Grep, **no `Skill` tool**), so it cannot route to a skill
   via Claude's skill router; instead `review.py` hands it a concrete
   richer-skill path to read-and-apply, falling back to jig's inlined
   baseline buckets (scope / blockers / nits / strengths). The pass
   returns the same `VERDICT / REASONING / SPECIFIC ISSUES /
   RECONCILIATION NOTES` envelope as the compliance pass, with
   SPECIFIC ISSUES entries tagged `[blocker]` / `[nit]` / `[strength]`.

   **Select the richer skill first (spec 096-03 / ADR-0040 D3).**
   `--richer-skill` is a **required** argument on `pr-review` (and `arch-review`
   / `code-health`). Before building the craft prompt:
   - **Run the candidate step:** `review.py candidates pr_review
     "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" --pass craft`. It
     prints a **tiered** list — `[high-confidence]` (with descriptions) and
     `[speculative]` (names only) — and writes the shown set to a sidecar.
   - **Pick the single best HIGH-CONFIDENCE candidate** for the category and
     pass it as `--richer-skill <name>`. If several are genuine, **pick one —
     do not refuse**; the alternatives are recorded as the shown-and-declined
     set. If none fits, pass `--richer-skill none` for jig's baseline.
   - **The pick is a heuristic, not a guarantee** — an explicit
     `review.<category>_skill` in `scaffold.json` (096-01) **overrides** it, and
     `candidates` never picks for you (a `[speculative]` false positive like a
     briefing skill must not be selected on lexical grounds alone).
   - **CI / no orchestrator:** pass `--richer-skill none --non-interactive`;
     config remains the reproducible path. Omitting `--richer-skill` entirely,
     or invoking a pass with no sidecar + no config + no `--non-interactive`,
     **fails fast** (naming the missing `candidates` step) rather than silently
     using the baseline.

3. **Arch pass — `arch-review`** (on-demand). Before running this pass,
   query the slice's `arch_review:` frontmatter flag via
   `workflow.py arch-review-needed`. When the helper prints `true`,
   build the arch-pass prompt with `review.py arch-review` and spawn a
   third `reviewer`-shaped subagent. The pass produces the four
   canonical arch buckets (summary / strengths / concerns / open
   questions) wrapped in the same verdict envelope, using the same
   candidate-channel resolution as the craft pass (096-03): run
   `review.py candidates arch_review …`, pick the best, pass `--richer-skill`;
   config (`review.arch_review_skill`) overrides, else jig's baseline buckets
   are inlined. When the helper prints `false`, skip this
   pass entirely. Slice authors flip the flag by uncommenting the
   `arch_review: true` line in the slice template's frontmatter — set
   it when the slice changes module boundaries, public contracts, or
   architecture-shaped concerns.

4. **Code-health pass — `jig:code-health`** (on-demand, **gated**). Before
   running, query the slice's `code_health_review:` frontmatter flag via
   `workflow.py code-health-review-needed`. When it prints `true`, **run
   `health.py` yourself** (the orchestrator / CI), capture its tight
   summary, and feed THAT summary into `review.py code-health … --summary-file`
   (`--summary-file -` to pipe it in). Then spawn a `reviewer`-shaped subagent. **The reviewer
   is read-only (Read/Glob/Grep, no Bash) — it must NOT run `health.py`;
   it judges the summary you provide.** The reviewer renders the judgment a
   tool can't: is duplication within the [ADR-0002](../../docs/decisions/adr-0002-extract-helper-on-third-caller.md)
   inline-mirror budget? is a complex function inherent or fixable? are
   the lint findings worth blocking on? The pass returns the same verdict
   envelope, with SPECIFIC ISSUES tagged `[blocker]` / `[nit]` /
   `[strength]`. **Why gated, not always-on:** [ADR-0017](../../docs/decisions/adr-0017-scaffolded-code-health.md)
   flags the per-slice review cost (specs 055/057 context-cost discipline)
   and recommends gating it like arch-review — so it defaults off and slice
   authors opt in with `code_health_review: true`. The evidence file is
   `reviews/slice-NN-code-health.md`.

When spawning any reviewer Task above, prefix the Task prompt with telemetry
tags before the `review.py` body: `[jig:phase=<phase>] [jig:spec=NNN]
[jig:slice=NNN-NN]`. Use `compliance` for `review.py implementation`,
`craft` for `pr-review`, `arch` for `arch-review`, `code-health` for
`code-health`, and `reconciliation` for the final reconciliation review.

**Block rule for the REVIEWED transition.** All required passes
(compliance + craft, plus arch when `arch_review: true`, plus code-health
when `code_health_review: true`) must pass before
`transition <slice> REVIEWED`:

- Any `fail` verdict from any pass blocks the transition.
- `needs-changes` from the compliance pass blocks (the implementer
  addresses findings and re-runs).
- `needs-changes` from the craft pass does NOT block — the
  `[nit]`-tagged entries become reconciliation-log items (the
  implementer captures them in the deviation log during reconciliation).
  Only `[blocker]`-tagged entries from the craft pass block the
  transition.
- The arch pass follows the same rule as the craft pass:
  `[blocker]`-tagged entries block; `[nit]`-tagged entries and
  `needs-changes` become reconciliation-log items.
- The code-health pass follows the same rule: `[blocker]`-tagged entries
  block the `REVIEWED` transition; `[nit]`-tagged entries become
  reconciliation-log items.

**Measurement tag (non-gating).** The craft/arch/code-health passes also
self-classify each SPECIFIC ISSUES entry `[spec]` (an acceptance-criteria /
spec-frame issue a smarter clarify pass could have prevented) or `[impl]`
(implementation-discipline — untested edge, brittle fixture, drift). This
carries no gating consequence — it exists purely so a future rollup can
count `[spec]`-tagged themes across specs (docs/refinement-todo.md
"Instrument the review→learnings→clarify loop before building it").

**The gate is mechanical, not advisory (slice 045-03 / [ADR-0014](../../docs/decisions/adr-0014-review-evidence-model.md) §5).**
`workflow.py transition` now *refuses* the `REVIEWED` / `RECONCILED` /
`DONE` moves unless the required review evidence — recorded with
`review.py record-review` as `docs/specs/NNN-<slug>/reviews/slice-NN-<pass>.md`
— exists and clears (`verdict: pass`). `REVIEWED` requires
`compliance` + `craft` (+ `arch` when the slice declares
`arch_review: true`, + `code-health` when it declares
`code_health_review: true`); `RECONCILED` requires the `reconciliation` verdict
**and** `### Deviation log` plus `### Reconciliation sweep` subsections;
`DONE` re-validates the post-implementation and reconciliation evidence set
(in addition to the existing `dependencies:` check). A refusal names
the missing/invalid artifact and the `record-review` command to produce
it. The gate enforces *evidence consistency*, not human sign-off (it
lives in the agent's trust boundary per [ADR-0011](../../docs/decisions/adr-0011-spec-gate-model.md)).
Bypass it for a deliberate out-of-band flow by setting
`JIG_REVIEW_EVIDENCE_GATE=0` (also `false`/`off`/`no`) — the status still
transitions and the `DONE` dependency check still runs; only the evidence
check is skipped.

After all required passes pass:

4. Address any reviewer findings, adding regression tests for any real
   bugs found.
5. **Record each pass's verdict** as durable evidence with
   `review.py record-review` (writes
   `docs/specs/NNN-<slug>/reviews/slice-NN-<pass>.md` — see the
   independent-review SKILL.md § "Recording and checking review
   evidence"). The `REVIEWED` transition is gated on this evidence, so it
   is not optional.
6. Transition: `transition <spec.md> <slice> REVIEWED`. The gate
   re-validates the recorded `compliance` + `craft` (+ `arch`,
   + `code-health`) verdicts before the status flips (and before the
   003-04 auto-tick).

**Recovering from a failed review.** A `fail`/`needs-changes` verdict — or
a `[blocker]`-tagged craft/arch finding, which is recorded as a non-`pass`
verdict — blocks the `REVIEWED` transition. To recover: address the
findings, re-run the pass against the updated deliverable, `record-review`
the new verdict (it **overwrites in place** the earlier file for that
`(slice, pass)`; git history keeps the prior one), then re-run
`transition … REVIEWED`. With every required pass now `pass`, the gate
clears. A non-`pass` artifact never overwritten by a later `pass` keeps
blocking — the "superseded without a later pass" case (ADR-0014 §4).

**When a review retracts a *claim*, sweep the corpus before re-recording.**
The recovery above is written for a code-shaped finding, which is local to one
file. A finding about **content** is not: a retracted assertion is usually
copied by design into `CHANGELOG.md`, the slice record, the inbox, and
cross-referenced docs. Fixing only the reviewed deliverable leaves the
retracted version authoritative in every **sibling** artifact — and the stale
copy is frequently the one the next session reads first (the project's own
rules make `CHANGELOG.md` a read-before-you-fix record), so the pass re-fails
round after round on a document you never touched. Before you `record-review`
the new verdict, **grep the retracted phrasing across the docs root and
`CHANGELOG.md`, and reconcile every hit** — the plain sweep is what reaches
the changelog, the inbox, and arbitrary cross-referenced files. Within the
spec itself, `/jig:analyze` is the structured complement: its **Duplication**
and **Terminology Drift** categories catch a retracted claim surviving across
the spec's own slice files and the docs it cross-references (`product-vision`,
accepted ADRs, the glossary, `architecture.md`) — but it audits one spec's
files plus that fixed set, not the whole corpus, so it sharpens the sweep
rather than replacing it. Distinguish **surviving** assertions (the claim
still stated as true — must fix) from **explicit** retractions (the claim
named as withdrawn in a changelog or history entry — correct, and worth
keeping).

```bash
# Compliance pass (always)
PROMPT=$(python3 ".github/skills/independent-review/review.py" \
  implementation "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" \
  "<deliverable-path-1>" ...)
SUBAGENT=$(python3 ".github/skills/independent-review/review.py" \
  subagent-type implementation)
# … feed "[jig:phase=compliance] [jig:spec=NNN] [jig:slice=NNN-NN]\n\n$PROMPT"
# … to Task with subagent_type: $SUBAGENT, wait for pass …

# Craft pass (always) — spec 096-03: select the richer skill first.
# 1. Show the tiered candidates + write the sidecar:
python3 ".github/skills/independent-review/review.py" \
  candidates pr_review "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" \
  --pass craft
# 2. Read the [high-confidence] tier, pick the single best (or `none`), then
#    build the prompt with the REQUIRED --richer-skill (config overrides it):
PROMPT=$(python3 ".github/skills/independent-review/review.py" \
  pr-review "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" \
  "<deliverable-path-1>" ... --richer-skill "<name-or-none>")
SUBAGENT=$(python3 ".github/skills/independent-review/review.py" \
  subagent-type pr-review)
# … feed "[jig:phase=craft] [jig:spec=NNN] [jig:slice=NNN-NN]\n\n$PROMPT"
# … to Task with subagent_type: $SUBAGENT, wait for pass …
# (CI / no orchestrator: --richer-skill none --non-interactive.)

# Arch pass (only when slice frontmatter has `arch_review: true`)
# IMPORTANT: capture the helper exit code — a non-zero exit means the
# slice lookup failed (missing spec / unknown fragment / ambiguous),
# not "no arch pass needed." Surface the error rather than silently
# skipping the pass.
if ! NEED_ARCH=$(python3 ".github/skills/spec-workflow/workflow.py" \
    arch-review-needed "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>"); then
  echo "arch-review-needed failed — aborting" >&2
  exit 2
fi
if [ "$NEED_ARCH" = "true" ]; then
  # 096-03: show candidates for arch_review, then pick (config overrides).
  python3 ".github/skills/independent-review/review.py" \
    candidates arch_review "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" \
    --pass arch
  PROMPT=$(python3 ".github/skills/independent-review/review.py" \
    arch-review "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" \
    "<deliverable-path-1>" ... --richer-skill "<name-or-none>")
  SUBAGENT=$(python3 ".github/skills/independent-review/review.py" \
    subagent-type arch-review)
  # … feed "[jig:phase=arch] [jig:spec=NNN] [jig:slice=NNN-NN]\n\n$PROMPT"
  # … to Task with subagent_type: $SUBAGENT, wait for pass …
fi

# Code-health pass (only when slice frontmatter has `code_health_review: true`)
# The orchestrator runs health.py and feeds its summary IN — the read-only
# reviewer never runs the tool (no Bash).
if ! NEED_CH=$(python3 ".github/skills/spec-workflow/workflow.py" \
    code-health-review-needed "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>"); then
  echo "code-health-review-needed failed — aborting" >&2
  exit 2
fi
if [ "$NEED_CH" = "true" ]; then
  # Run the jig:code-health runner yourself (health.py check .) and capture
  # its tight summary to /tmp/health-summary.txt — the read-only reviewer
  # MUST NOT run it. (The runner ships with the Tier-1 jig:code-health skill;
  # if it isn't installed, note "summary unavailable" and judge on the
  # deliverables.)
  # 096-03: show candidates for code_health, then pick (config overrides).
  python3 ".github/skills/independent-review/review.py" \
    candidates code_health "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" \
    --pass code-health
  PROMPT=$(python3 ".github/skills/independent-review/review.py" \
    code-health "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" \
    "<deliverable-path-1>" ... --summary-file /tmp/health-summary.txt \
    --richer-skill "<name-or-none>")
  SUBAGENT=$(python3 ".github/skills/independent-review/review.py" \
    subagent-type code-health)
  # … feed "[jig:phase=code-health] [jig:spec=NNN] [jig:slice=NNN-NN]\n\n$PROMPT"
  # … to Task with subagent_type: $SUBAGENT, wait for pass …
fi
```

### Reconciliation (REVIEWED → RECONCILED)

Walk the **Reconciliation checklist** below. Every item is a gate.

### Closing the slice

1. After the reconciliation review passes, **record its verdict** with
   `review.py record-review … --pass reconciliation`, then
   `transition <spec.md> <slice> RECONCILED`. That move is gated on the
   recorded `reconciliation` verdict (`pass`) **and** `### Deviation log`
   plus `### Reconciliation sweep` subsections under the slice heading
   (ADR-0014 §5 + ADR-0029).
2. Commit the work.
3. After commit: `transition <spec.md> <slice> DONE`. `DONE` re-validates
   the whole evidence set — `compliance` + `craft` (+ `arch`,
   + `code-health`) + `reconciliation` — plus the deviation log and
   reconciliation sweep, on top of the existing
   `dependencies:` check.
4. Regenerate the board: `workflow.py status-board <project-dir>`.
5. Run `/jig:memory-sync` (or `memory.py`) to consolidate any new learnings.

**Before landing, audit the board.** `docs/specs/README.md` is derived — every
column is computed from the spec records, and the curated Notes column is
carried across regens — so it is regenerated, never hand-edited. A merge
conflict on it is resolved by re-running `status-board`, not by picking a side:

```bash
python3 ".github/skills/spec-workflow/workflow.py" \
  check-board <project-dir>
```

Read-only; exits non-zero on either problem it can find. **Stale board** — the
spec records changed and `status-board` wasn't re-run. **Duplicate spec
number** — two spec directories claim one number, which is what parallel
branches produce when the number was never reserved on the trunk. The renderer
emits both without complaint and a staleness check can't see it (both *are*
faithfully derived), so it needs its own detector; the message names both
directories so you know which to renumber.

Notes-column text is not drift — it is hand-written by design and survives
regen. What *is* drift is any other cell edited by hand: it will be overwritten
on the next regen, so change the slice record instead.

## Spec lifecycle states

```
DRAFT → READY_FOR_REVIEW → READY_FOR_IMPLEMENTATION → IN_PROGRESS
  → REVIEWED → RECONCILED → DONE

         DEFERRED ⇄ DRAFT   (parked slices with a stated resolution trigger)
         ABANDONED ⇄ DRAFT  (permanently dropped slices, pre-DONE only)
```

Status transitions are mutations on either `spec.md`'s frontmatter `status:`
field (new convention, slice 015-01) or the prose `**STATUS: ...**` line
(legacy — still supported via lazy migration), AND the matching row in
`docs/specs/README.md`. Use `workflow.py transition` for the spec mutation
and `workflow.py status-board` to re-sync the board.

**Spec-level `status:` is derived, not authored** (slice 030-01; widened by
slice 085-01). The frontmatter `status:` at the top of each `spec.md`
overview file is computed by `compute_spec_status(spec_path)` from its
slices: `DONE` when every slice, excluding `DEFERRED`/`ABANDONED` ones, is
DONE (a mix of `DONE` + `DEFERRED` and/or `ABANDONED` still rolls up to
`DONE`); `ABANDONED` when every slice is `ABANDONED` (the spec's entire
scope was dropped); `DRAFT` when no slices exist, every slice is
`DEFERRED`, every non-`DEFERRED` slice is `DRAFT`, or the only non-`DONE`
slices are a `DEFERRED`+`ABANDONED` mix with no live work; otherwise
`IN_PROGRESS`. The rollup write happens automatically inside `workflow.py
transition` (after the slice mutation) and inside `workflow.py
status-board` (during regen). Don't set `spec.md`'s `status:` by hand —
it'll be overwritten on the next transition or regen anyway.

### DEFERRED state

A slice is `DEFERRED` when scoped but parked — the work is identified but
not the current priority. Different from `DRAFT` which means "not yet
fleshed out." Transitions:

- Any state → `DEFERRED` is allowed.
- `DEFERRED` → `DRAFT` (re-open) is allowed.
- `DEFERRED` → any other state is **refused** — re-open via DRAFT first
  so review gates aren't silently skipped. This is the first
  FROM-state-restricted transition in jig's lifecycle.

When transitioning a slice to `DEFERRED`, add a `**Resolution trigger:**`
line in the slice body (same convention `docs/refinement-todo.md` uses).
The status-board renders deferred slices in a separate `## Deferred slices`
section with that trigger as the per-row context.

### ABANDONED state

A slice is `ABANDONED` when it's permanently dropped — scoped, sometimes
even fully specced, and deliberately decided against with no intent to
ever resume. Different from `DEFERRED`, which means "parked, with a stated
resolution trigger that will resurface it." Added in slice 085-01 (filed as
[GitHub issue #72](https://github.com/ramboz/jig/issues/72)). Transitions:

- Any **pre-`DONE`** state → `ABANDONED` is allowed.
- `DONE` → `ABANDONED` is **refused** — "never attempted" and "shipped,
  then deliberately removed" are different events with different audit
  value; overloading one bucket for both would erase that distinction
  where an auditor most needs it (see spec 085 Non-goals). Un-shipping
  already-`DONE` work is a different, unbuilt concept.
- `ABANDONED` → `DRAFT` (re-open) is allowed.
- `ABANDONED` → any other state is **refused** — re-open via DRAFT first,
  mirroring `DEFERRED`'s restriction.

When transitioning a slice to `ABANDONED`, add a `**Abandonment reason:**`
line in the slice body (same convention shape as `**Resolution
trigger:**`). The status-board renders abandoned slices in a separate
`## Abandoned slices` section with that reason as the per-row context.
The transition also prints a one-time, non-blocking warning naming any
other slice, anywhere in the project, whose `dependencies:` names the
now-abandoned slice and whose own status isn't already `DONE`/`ABANDONED`
— advisory only, it never blocks the transition, modifies the dependent,
or cascades (a human decides what a live dependent should do next).

### Slice frontmatter (slice 015-01 convention, file shape per 018-03)

New slices are whole-file templates — frontmatter at the top, `## Slice ...`
heading immediately following the closing frontmatter delimiter. `workflow.py
new` emits a starter `slice-01-tbd.md` alongside `spec.md` in this
shape (from the packaged slice template); for a filled-in reference, mirror the
scaffolded worked example `docs/specs/001-adopt-jig/slice-01-bootstrap.md`.
Legacy specs that embed `## Slice` sections inside `spec.md`
(heading-first, frontmatter-after) remain supported by every helper —
no forced migration.

```yaml
---
status: DRAFT
dependencies: [007-02, adr-0004]
last_verified:
---
```

- `status` — current lifecycle state. `workflow.py transition` updates
  this when present.
- `dependencies` — flow-style list of slice fragments (e.g. `007-02`)
  and ADR IDs (e.g. `adr-0004`). `transition <slice> DONE` refuses if
  any listed dependency is not DONE / accepted.
- `last_verified` — date the slice was last reconciled. `transition`
  stamps this automatically on `→ RECONCILED`. Used by `stale`.

Legacy slices using prose `**STATUS:**` markers still work — the
transition helper writes to whichever shape is present. No retroactive
mass migration; new slices use the template, old slices stay as-is.

## Reconciliation checklist

When a slice transitions `REVIEWED → RECONCILED`, walk this checklist before the
status flip is allowed. Each item is a gate.

- [ ] **Deviation log** — write what changed during implementation and why,
      under a "Deviation log (after reconciliation)" subsection of the slice
      in `spec.md`. Original ACs preserved above; deviations append, not overwrite.
- [ ] **Reconciliation sweep** — write which drift-prone surfaces were checked,
      using `updated` / `no-op` / `deferred` dispositions. The transition gate
      checks the subsection exists; the reconciliation reviewer judges coverage
      and rationale quality.
- [ ] **Leanness sweep** — did the implementation over-build? Confirm nothing
      was added beyond what the acceptance criteria required — no
      over-engineering, premature abstraction, or speculative generality
      (indirection, config knobs, or extension points with no current caller).
      Anchored to the spec's needs (added-beyond-need), never a push to strip
      behavior the spec required. Complements the arch-pass leanness lens (spec 109). Non-blocking
      nudge; not a gate.
- [ ] **Lightweight decisions** — did this session's review or implementation
      settle any non-spec decisions (UI strings, visual choices, translation
      corrections, scoped brand/icon calls)? If yes, record them in
      `docs/decisions/lightweight-decisions.md`. (Non-blocking nudge; not a gate.)
- [ ] **Architecture impact** — did module boundaries or public contracts change?
      If yes, update `docs/architecture.md` AND write an ADR. **Ground what you
      write (ADR-0020 §1, same rule as spec-authoring step 6).** Reconciliation
      rewrites long-lived front-door prose that everyone reads and nobody
      re-derives, so any load-bearing factual claim about a runnable surface —
      library/API capability, version/perf behavior, behavior of existing code —
      must be backed by an executed probe or a `file:line` citation. Anything you
      cannot verify is marked as an assumption, never asserted as fact. (A prose
      claim naming a code symbol but citing no line is a candidate warning.)
- [ ] **Load-bearing decision (ADR trigger, judgment)** — beyond a boundary
      change, was a load-bearing design choice with rejected alternatives made?
      Canonical wording — single-sourced from ADR-0031, drift-tested verbatim
      across all four surfaces:
      A load-bearing design choice with rejected alternatives — one a future agent would need to know about to avoid undoing it — warrants an ADR even when it changes no module boundary or public contract.
- [ ] **Revised a recorded decision?** (spec 100 / [ADR-0042](../../docs/decisions/adr-0042-decision-routing-gate.md))
      Routing is asked once at first write and never again, so a decision
      re-priced during this slice can stay misfiled. If a revised entry now
      clears the trigger above, promote it (`decisions.py promote --title
      "<title>" --no-push` — push mode reserves the ADR on `origin/main` from
      an ephemeral worktree, so off `main` it never reaches your working copy
      and `promote` refuses); if it is still settled, local and bounded, revise
      it (`decisions.py update`). Never hand-edit `lightweight-decisions.md`.
- [ ] **Conventions impact** — did this slice introduce or change a rule worth
      recording? If yes, edit `docs/conventions.md` (requires
      `JIG_CONVENTIONS_APPROVED=1`).
- [ ] **Inbox triage** — sweep `docs/inbox.md` for items resolved by this slice;
      move them to the relevant memory file or strike them through.
- [ ] **Primer hygiene** — if this slice closes the spec (all non-deferred
      slices DONE), apply the spec 025 compress-on-close-out rule per the slice
      template's `### Close-out (post-DONE)` section. Check every primer surface
      present in this project: `CLAUDE.md`, `AGENTS.md`, and scaffold templates.
      Active-spec sections should only carry in-flight work; load-bearing
      per-slice invariants migrate to the status board Notes column (which
      `workflow.py status-board` preserves across regen), memory, or the
      reconciled spec/slice record.
- [ ] **Memory-sync** — run `/jig:memory-sync` (or invoke `memory.py` directly)
      to persist any new domain terms, dead-end learnings, or tool decisions
      that emerged during implementation. **This is where slice 002-04's
      integration lives**: the reconciliation phase explicitly surfaces
      memory-worthy items for persistence. The reviewer subagent reads from
      memory but never writes to it (see `agents/reviewer.md`).
- [ ] **Closed-spec drift** — if reconciliation surfaces a prior
      closed-spec inaccuracy (a `DONE` / `SUPERSEDED` spec/slice, or
      load-bearing skill/router/workflow prose that no longer matches
      reality), follow the policy in [ADR-0010](../../docs/decisions/adr-0010-amendment-scope-records-vs-live-prose.md)
      (supersedes ADR-0008). **Records** (closed specs/slices): append a
      dated `## Amendments` entry preserving the original. **Live prose**
      (SKILL.md / workflow.md / README): fix it **inline** — git history
      is the audit trail. New ADR (or superseding spec) only for
      decision-content changes.
      **Authorisation to amend (issue #125).** Amending a closed **record**
      requires **explicit owner approval** — a separate grant from approval of a
      *behaviour*. When two canon artifacts disagree, **surface the conflict and
      stop**: propose the amendment as text in the conversation and write it only
      after the owner agrees. **Never write the resolution in the same turn as
      discovering the conflict** — *including when the owner has already approved
      the underlying behaviour* (approving what the app does is not authority to
      rewrite what the spec says). And before asserting that artifact X
      contradicts criterion Y, read **all** of Y's sibling criteria: another may
      already satisfy X — for a cross-cutting question the unit of reading is the
      whole criteria block, not the item that appears to speak to it. This
      authorisation rule governs **records** only; correcting live operational
      prose inline (per the split above) is git-history-audited and needs no
      sign-off.
- [ ] **Reconciliation review** — spawn a second reviewer subagent with a
      reconciliation-review prompt prefixed by
      `[jig:phase=reconciliation] [jig:spec=NNN] [jig:slice=NNN-NN]`: are the
      doc changes faithful? Is the deviation log honest? Is scope appropriate
      (no scope creep in docs)?
- [ ] **Use-case coverage (advisory)** — run `workflow.py coverage
      [--project-dir DIR]` and review any **coverage gap** (a use case with no
      implementing spec) or **scope creep** (a spec citing no resolvable use
      case). **Non-blocking** — unlike the gates above, a finding here does
      **not** block `RECONCILED` / `DONE` (ADR-0025 OQ3 / ADR-0011); it is the
      reconcile-time backstop to slice 02's framing-time grow prompt. No-op
      when the project has no `## Use cases` section.
- [ ] **Commit** — only after all gates pass.

### Auditing staleness (`workflow.py stale`)

Slice 015-03 added a read-only freshness audit:

```bash
python3 ".github/skills/spec-workflow/workflow.py" stale \
  [--project-dir DIR] [--days N]
```

Walks `docs/specs/*/spec.md` and `docs/decisions/adr-*.md`, extracts
`last_verified` + `dependencies` from frontmatter, and lists items
meeting the **conjunctive criterion**:

> An item is stale iff (a) `today - last_verified > --days` (default 90)
> AND (b) at least one file referenced by `dependencies` was modified
> since `last_verified`.

Pure age isn't enough — a verified-2-years-ago ADR for an unchanged
decision shouldn't fire. Pure recency-of-dep isn't either — a doc
verified yesterday with old deps is fine. Both conditions must hold.

The check uses `git log -1 --format=%cs <path>` for committed-state
authority and falls back to filesystem mtime when git is unavailable
or the file isn't tracked. Read-only: it lists, doesn't transition.
Bumping `last_verified` is a deliberate human/agent action — edit the
file, or re-run `transition <slice> RECONCILED` after re-verifying.

## Gotchas

- **Spike is the LAST SPIDR technique** to reach for, not the first. AI agents
  default to spiking too eagerly; try Rules / Data / Interface / Path first.
- **Every slice must be vertical** (crosses all layers, delivers end-to-end value).
  A slice that touches only the DB or only the parser is horizontal phasing — flag it.
- **The reviewer subagent must NOT be invoked with prior implementation context.**
  Write the deliverable to disk first; reviewer reads only the spec + deliverable
  + acceptance criteria.
- **The reviewer is read-only on `docs/memory/`** — memory-sync runs as a separate
  step during reconciliation, never as part of review.
- **`workflow.py transition` uses substring matching on slice names** — `001-01`
  matches `## Slice 001-01 — greenfield-scaffold`. If you have multiple slices
  whose names share a fragment, the helper refuses with an `ambiguous` error;
  use a more specific fragment.
- **`workflow.py status-board` preserves the preamble** before the `| Spec` table
  header. Custom intro text survives regen. Idempotent: no churn if the board is
  already current. **Notes column** also survives regen (the helper parses existing
  Notes and re-emits them). **Deferred slices** appear in a separate `## Deferred
  slices` table below the active table; only the active table preserves Notes.
- **`workflow.py status-board` refuses to overwrite on a mid-regen race** (slice
  028-03). The helper captures a SHA256 of `docs/specs/README.md` at the start of
  regen and re-checksums right before the write; if another writer mutated the file
  in the gap, it raises `StatusBoardRaceError` and exits **4** with the message
  `status board changed during regen — another writer may have run. Re-run
  workflow.py status-board to retry.`. Pass `--force` to bypass the guard and
  overwrite anyway (use only when you've manually reconciled the conflict).
  Identical-content rewrites do NOT trigger a refusal (checksum is content-based,
  not mtime-based).
- **`workflow.py` ignores `## Spike` headers.** Spikes are research artifacts, not
  lifecycle-managed work items. They don't have a STATUS marker the helper can
  transition. If you need a spike to be tracked in the status board, model it as a
  `## Slice Nnna — <name>` instead, or update the board's Notes column manually.
- **Avoid raw `|` characters in the Notes column** of `docs/specs/README.md`.
  Markdown tables use pipes as cell separators; raw pipes in a Note value would
  truncate the cell during regen's preservation step. Use HTML-entity `&#124;`
  or rephrase if you really need a pipe.
- **`DEFERRED → DONE` (or any non-DRAFT state) is refused.** Re-open the
  slice with `DEFERRED → DRAFT` first, then advance through the normal
  lifecycle. This prevents silently skipping review gates when a parked
  slice is picked back up.
- **`transition <slice> DONE` validates `dependencies:`.** If any
  listed dep slice isn't DONE or any listed ADR isn't Accepted, the
  helper refuses with a structured error naming each unsatisfied dep.
  Empty / missing `dependencies:` skips the check.
