Working posture (ADR-0056).
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
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.
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
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.
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).
1a. Read the vision ## Use cases section as framing — before drafting
(spec 068-02 / ADR-0025).
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.
Reserve the next free number on origin/main:
python3 "${CLAUDE_PLUGIN_ROOT}/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).
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).
But it is not silent. The trigger is mechanical and deterministic — the
classify_spec predicate in
skills/_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 and
ADR-0049
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):
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 ungrounded
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
## Assumptions A1–A4 + ## Kill criteria, and the
spec 064-01 retro,
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:
python3 "${CLAUDE_PLUGIN_ROOT}/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
Read the automatic jig hint: project-orientation headline injected at
SessionStart, or refresh it manually before choosing work:
python3 "${CLAUDE_PLUGIN_ROOT}/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.
Check docs/specs/README.md for the next slice in READY_FOR_IMPLEMENTATION
(or DRAFT for a slice you intend to plan now).
Run:
python3 "${CLAUDE_PLUGIN_ROOT}/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).
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.
Fill in / refresh plan.md and tasks.md for the slice.
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:
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.
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.
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.
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
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
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 §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).
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:
- Address any reviewer findings, adding regression tests for any real
bugs found.
- 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.
- 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).
# Compliance pass (always)
PROMPT=$(python3 "${CLAUDE_PLUGIN_ROOT}/skills/independent-review/review.py" \
implementation "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" \
"<deliverable-path-1>" ...)
SUBAGENT=$(python3 "${CLAUDE_PLUGIN_ROOT}/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 "${CLAUDE_PLUGIN_ROOT}/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 "${CLAUDE_PLUGIN_ROOT}/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 "${CLAUDE_PLUGIN_ROOT}/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 "${CLAUDE_PLUGIN_ROOT}/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 "${CLAUDE_PLUGIN_ROOT}/skills/independent-review/review.py" \
candidates arch_review "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" \
--pass arch
PROMPT=$(python3 "${CLAUDE_PLUGIN_ROOT}/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 "${CLAUDE_PLUGIN_ROOT}/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 "${CLAUDE_PLUGIN_ROOT}/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
# Ru
…(truncated)
1---2name: spec-workflow3description: 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.4---56> **Working posture ([ADR-0056](../../docs/decisions/adr-0056-adversarial-register-quarantine.md)).**7> Adversarial review is a *named, bounded operation.* This workflow invokes8> review passes (frame-critique / craft), whose skeptical register belongs9> *inside* those isolated reviewer subagents. Outside a review, default to10> collaborative and solution-forward; don't carry the adversarial stance into11> ordinary conversation.1213> Spec 003 promoted this skill from stub to active. The deterministic state14> mutations live in `workflow.py`; this SKILL.md drives the judgment layer.1516## What this skill does1718- Guides SPIDR-splitting a new spec into vertical slices (Spike last, not first —19 try Rules / Data / Interface / Path first).20- Flags slices that look like horizontal phasing (no user-facing layer touched).21- Drives the spec lifecycle state transitions via `workflow.py`.22- Coordinates implementer + reviewer subagent invocations at the right points.23- Enforces the reconciliation checklist before a slice goes DONE.24- Consults `docs/memory/glossary.md` when drafting ACs to surface unknown domain terms.25- Surfaces skill-routing observability via `workflow.py routing-stats [--days N]` —26 a read-only histogram of which skills fired (jig baseline vs. richer/"other"27 skill per category) from `.claude/skill-usage.jsonl` (slice 041-02).28- Surfaces use-case coverage via `workflow.py coverage [--project-dir DIR]` — a29 read-only, **advisory** (non-blocking), project-wide **bidirectional** check30 (slice 068-03): a deterministic set-difference over the `use_cases:` trace31 links that reports use cases with no implementing spec (coverage gap) and32 specs citing no parent use case (scope creep). No-op when the project has no33 `## Use cases` section.34- Guards the status board via `workflow.py check-board <project-dir>` — a35 read-only audit that exits non-zero when `docs/specs/README.md` no longer36 matches the spec records, or when two spec directories claim one number.37 Writes nothing, so it is safe to run in CI against a checkout.38- Surfaces gate-bypass telemetry via `workflow.py gate-stats [--days N]` — a39 read-only per-gate histogram of how often each gate honored its env-var40 override (an override-frequency audit trail, not a gate-value verdict) from41 `.claude/skill-usage.jsonl` (slice 078-02).4243## SPIDR splitting4445All non-trivial specs are SPIDR-split into vertical slices before46implementation begins. **Spike is the last resort — try Path /47Interface / Data / Rules first.**4849- **S — Spike**: research/learning activity. Only when none of P/I/D/R50 apply. AI agents default to spiking too eagerly — resist.51- **P — Path**: split by alternative paths through the story (happy52 path first, edge paths later).53- **I — Interface**: split by UI / platform / channel (minimal first,54 polish later).55- **D — Data**: split by data subset or format (less data first).56- **R — Rules**: split by business rules (simple first, edge cases later).5758**Anti-horizontal-phasing rule:** every slice must touch the59user-facing layer and deliver end-to-end value. A slice that touches60only the DB or only the parser is horizontal phasing — re-split.6162See [`worked-example-spidr-split.md`](worked-example-spidr-split.md)63for one applied example per axis plus a jig-native dogfood case (spec64017's three-axis split). The canonical primer for all five axes lives65at [`docs/spec-workflow/spidr-primer.md`](../../docs/spec-workflow/spidr-primer.md).6667### Spike slices6869When SPIDR's S axis fires during decomposition (none of P / I / D / R70apply because the team doesn't yet know enough to pick), the71resulting slice is marked `kind: spike` in its frontmatter — the72typed enum that `spec_lint.py` validates.7374**When to introduce a spike during decomposition.** Reach for S only75after trying R / D / I / P. The bias to resist is "let me research76this first" as a prelude to "now let me build it as one big slab" —77that is horizontal phasing in a trench coat. If the spike would78conclude with "now ship the implementation," the implementation IS79the slice, and the research goes inside it.8081**Body shape (four labelled blocks).** A `kind: spike` slice carries82four blocks alongside the standard Goal / DoR / AC / DoD scaffolding.83**Each label must be written with the trailing colon (`**Question:**`,84etc.) — that is what `spec_lint.py` matches against.**8586- **Question:** — one sentence stating the open question. Set at DRAFT.87- **Time-box:** — explicit budget (e.g., "1 day", "4 hours"). Set at DRAFT.88- **Findings:** — bullet evidence collected during the spike. Filled89 during IN_PROGRESS.90- **Outcome:** — one of `ADR-NNNN created` / `spec NNN-NN unblocked` /91 `abandoned (reason)`. Multiple outcomes separated by `;`92 (e.g., `ADR-0007 created; spec 030-02 unblocked`). Set at DONE.9394`spec_lint.py` soft-warns when a `kind: spike` slice is missing any of95the four labels — mid-flight spikes legitimately have empty Findings /96Outcome, so this is a warning, not a hard error.9798**Always nested, never standalone.** Spike slices live inside a real99spec — never as a standalone `docs/spikes/` artifact. The1001-slice-spec case (no clear downstream spec yet, just an101investigation) collapses to "spawn a normal spec where the only slice102is `kind: spike`." This forces the investigator to articulate the103downstream change up front and keeps jig at two numbered families104(specs+slices, ADRs).105106**Abandoned-spike manual-reshape failure mode.** When a spike's107Outcome is `abandoned (reason)`, dependents are NOT automatically108cascade-flagged. The human (or the next session) audits each109dependent slice and decides whether the original design still holds.110Automation here over-fires: "approach A abandoned" often means111"approach B from the same findings still satisfies the dependents."112`workflow.py` deliberately stays out of the cascade business; the113SKILL.md hand-off is the documented gate.114115## How to use116117### Creating a new spec1181190. **Step 0 — confirm the project is scaffolded (spec 063 / ADR-0011).**120 BEFORE reserving a number or drafting ANY `docs/specs/` or slice121 structure, confirm this project is a scaffolded jig project. If it122 isn't, **route — do not hand-roll directories**:123 - **Greenfield** (no jig structure yet) → tell the user to run124 `/jig:scaffold-init`. It lays down conventions, templates, hooks, the125 status board, and a seed reference spec.126 - **Existing spec/`slices/` layout, but not jig-scaffolded** (no127 `scaffold.json`) → tell the user to run `/jig:migrate`. It adopts the128 existing layout into jig structure.129130 You don't have to decide the state yourself: `workflow.py new` (step 2)131 **classifies and routes** for you (spec 063-01) — a `scaffold.json`-bearing132 project proceeds; a greenfield project is refused naming133 `/jig:scaffold-init`; an adoptable spec-driven project is refused naming134 `/jig:migrate`. The deterministic gate and this human-readable135 precondition agree by construction, so **don't restate the detection136 heuristic here** — run the helper and let it route. (Bypass for a137 deliberate out-of-band flow: `JIG_SCAFFOLD_PRECONDITION=0`.)138139 **The anti-pattern this step exists to kill:** an auto-triggered140 `spec-workflow` run improvising a loose `slices/` folder (or any ad-hoc141 `docs/specs/` skeleton) because `/jig:scaffold-init` was skipped. That142 produces a non-jig layout that then needs migrating — exactly the143 reported failure. When in doubt, route to setup first; never invent the144 structure by hand.1451461. Confirm the work needs a spec. Trivial fixes don't.147 - **Reference moved? Reframe first.** If a *load-bearing reference* changed148 from outside the system (a design system, vendor / API contract, test infra,149 compliance regime, platform, or product-positioning / strategic-vision150 shift), reach for `/jig:reframe` **before** drafting — re-baseline the corpus151 onto the new reference so you draft against it, not the dead premise (spec152 067 / [ADR-0024](../../docs/decisions/adr-0024-reference-reframe.md)).1531a. **Read the vision `## Use cases` section as framing — before drafting154 (spec 068-02 / [ADR-0025](../../docs/decisions/adr-0025-use-cases-breadth-layer.md)).**155 If the project's `docs/product-vision.md` carries a `## Use cases` section156 (the breadth-layer behaviors captured at init), **read it first** as framing157 context for this spec — *which captured behavior does this work serve?* The158 section is the shared frame specs anchor against; reading it before you draft159 is what keeps behavior-dense projects from each spec inventing its own slice160 of the world. (If there is **no** `## Use cases` section, the layer isn't161 adopted for this project — skip this step; nothing here applies.) You record162 the answer as a trace link in the spec's `use_cases:` frontmatter (step 2a).1631b. **Cross-check tracked bugs before drafting ACs.** For feedback/triage164 specs, read `docs/bugs/README.md` and any overlapping `docs/bugs/NNN-*.md`165 record before turning reported symptoms into acceptance criteria. If an166 item is a tracked defect with a bug record, route it through `bug-fix`167 (or link to that bug as an explicit dependency) instead of copying the168 defect into the spec as a second owner. Keep polish/design/new-behaviour169 items in the spec.1702. **Reserve the next free number on origin/main:**171172 ```bash173 python3 "${CLAUDE_PLUGIN_ROOT}/skills/spec-workflow/workflow.py" new <slug>174 ```175176 The helper computes `max(NNN) + 1` across `docs/specs/`, writes a177 minimum stub `docs/specs/NNN-<slug>/spec.md` (frontmatter + Overview178 + SPIDR-analysis headers), commits it as179 `docs(specs): reserve NNN-<slug>`, and pushes to `origin/main`. If180 the push is refused by branch protection / permissions, the helper181 automatically falls back to a `reserve/NNN-<slug>` branch + `gh pr182 create`. This locks the number **team-wide** before any drafting183 begins, killing the parallel-worktree spec-number-collision failure184 mode logged across specs 014/015/016/017.185186 **Works from any branch or worktree** (ADR-0015 / spec 051). The187 helper routes on the current branch: on `main` it runs the proven188 in-place flow (clean tree required, since the commit lands on local189 `main`); off `main` — a feature branch or a linked `.claude/worktrees/*`190 worktree — it builds the reservation commit in an *ephemeral detached191 worktree* checked out at `origin/main` and pushes it by SHA, never192 touching your branch, cwd, or working tree. You no longer need to193 switch to `main` (and a linked worktree can't, anyway).194195 Flags: `--no-push` for solo machines without a remote, or for an196 off-main *provisional* reservation committed on the current branch197 (the number is local-view and may collide at merge — treat it as198 provisional); `--pr` to skip the direct-push attempt on199 protection-locked main.2002a. **Record the use-case trace link — and grow the vision on discovery201 (spec 068-02 / [ADR-0025](../../docs/decisions/adr-0025-use-cases-breadth-layer.md)).**202 The stub seeds an empty `use_cases:` frontmatter list. Fill it with the203 `UC-N` id(s) (from the vision `## Use cases` section, step 1a) this spec204 serves — the `dependencies:`-style flow-list shape, e.g.205 `use_cases: [UC-1, UC-3]`. This is the machine-resolvable trace link the206 reconcile-phase coverage check (slice 03) reads.207208 **The discipline is soft — an empty/absent `use_cases:` never blocks a209 transition (AC4 / [ADR-0011](../../docs/decisions/adr-0011-spec-gate-model.md)).210 But it is not silent.** The trigger is **mechanical and deterministic** — the211 `classify_spec` predicate in212 [`skills/_common/use_cases.py`](../_common/use_cases.py) computes one of213 `no_section` / `empty` / `resolved` / `unresolvable` for this spec — **not** a214 voluntary "is this new?" self-report. **Whenever `classify_spec` returns `empty`215 or `unresolvable`** (the spec cites nothing, or cites a `UC-N` with no match216 in the vision) at draft/framing, surface a **three-path prompt** — every path217 is **one step** and **none blocks drafting**:218219 - **(a) cite an existing use case** — this spec serves a behavior already in220 the vision: put its `UC-N` id(s) in `use_cases:`. Done.221 - **(b) grow the vision** — this spec serves a behavior **not yet captured**:222 **reuse `vision-elicitation`'s capture loop, seeded with the existing223 entries** (so the author sees the current set), → normalize → **confirm**,224 then **write additively** (append, never discard-and-replace) and **assign225 the next free `UC-N`** (`use_cases.next_use_case_id` allocates `max + 1`;226 retired numbers are never reused). The **confirm step guards grow quality**227 so a reachable trigger can't silently bloat the section: **(i)** enforce228 **goal-level grain** — reject spec-shaped / requirements-level phrasing,229 re-running slice 01's normalize (`"[actor] can [goal]"`); **(ii)** run a230 **near-duplicate check** against the seeded existing entries231 (`use_cases.is_near_duplicate`) — on an apparent match, **route back to232 path (a)-cite** rather than minting a duplicate. Then record the new id in233 `use_cases:`.234 - **(c) decline** — legitimately untraced (infra / refactor / no user-facing235 behavior) or defer: leave `use_cases: []`. The vision is unchanged, and any236 resulting gap is slice 03's advisory coverage backstop. No-op.237238 **CRITICAL — the no-section no-op.** When `classify_spec` returns `no_section`239 (the project has **no** `## Use cases` section — the breadth layer is **not240 adopted**, e.g. jig's own repo), **the prompt is suppressed entirely**:241 nothing prompts and nothing errors. A project with specs but no use-case242 layer is wholly unaffected. The trigger fires **only** on `empty` /243 `unresolvable`, which presuppose the section exists.2443. Create `docs/specs/NNN-<slug>/{spec.md,plan.md,tasks.md}` with the conventional245 structure: status frontmatter, overview, SPIDR analysis, ordered slices.2464. SPIDR-split: for each slice, the goal is **one vertical piece** that delivers247 end-to-end value. Spike is the last resort, not the first reach.2485. Each slice is a whole-file document with the canonical frontmatter shape249 (`status`, `dependencies`, `last_verified`) plus DoR / AC / DoD / Close-out250 sections. `workflow.py new` (step 2) already emitted a well-formed starter251 `slice-01-tbd.md` from the packaged slice template, so you never hand-resolve252 a template path; add any further slices in that same shape. For the253 **structural reference** — what a filled-in slice looks like — mirror the254 in-project worked example that scaffolding installs at project root:255 `docs/specs/001-adopt-jig/` (`spec.md` + `slice-01-bootstrap.md`), the first256 spec, which your project's `CLAUDE.md` names as the worked example to257 imitate. Set `status: DRAFT` in the frontmatter. Legacy slices that use prose258 `**STATUS: DRAFT**` markers still work (lazy migration); no need to rewrite259 them.2605a. **Design-fidelity authoring nudge (spec 104-02 / ADR-0049).** When a261 slice you just wrote in step 5 ships **visual design** (a mockup, a design262 system spec, a screen with colours/spacing/sizes/layout rules to hit),263 don't let "doesn't match the mockup" live only in a picture:264 - **(a) Extract the design values into checkable ACs.** Pull the concrete265 values — colours, spacing, sizes, layout rules — out of the mockup and266 write them as this slice's acceptance criteria, the same way any other267 observable behavior becomes an AC. This is what turns a fuzzy "looks268 right" into something a reviewer (or an eval) can actually check.269 - **(b) When fidelity must *gate*, wire the servo rail.** If a screen's270 visual fidelity needs to be a hard, enforced condition of `DONE` — not271 just an eyeballed check — set `design_review: true` in the slice272 frontmatter and wire a servo `design-eval` (screenshots the running app273 against the reference, scores it with a pinned vision judge) as the274 done-condition. `design_review: true` is attested, read-only, at275 `REVIEWED` by spec 071's design-review pass (the deriver276 `slice_needs_design_review` in `workflow.py` reads the flag) — jig never277 re-derives the eval score itself.278279 **Graduated, not mandatory — jig offers, never forces, servo.** Not280 every screen earns a frozen eval:281 - **Low-stakes visual polish** → design-values-in-ACs plus282 attest-by-eyeball at review time is enough; no servo `design-eval`283 required.284 - **A hard fidelity gate** (fidelity must not regress, or is a stated285 product requirement) → servo `design-eval` + `design_review: true`.286287 Pick the tier that matches the stakes; when in doubt, start with (a) and288 add (b) only when eyeballing genuinely isn't enough. See289 [spec 071](../../docs/specs/071-design-review-pass/spec.md) and290 [ADR-0049](../../docs/decisions/adr-0049-design-fidelity-routing-to-originating-spec.md)291 for the full routing rationale; this step adds no new mechanism — teeth292 stay anchored to the existing `design_review` flag.2936. **Ground your factual claims (spec 064-02 / ADR-0020 §1–§2).** Any294 load-bearing factual claim about a *runnable* surface — library/API295 capability, version/perf behavior, behavior of existing code — must be296 backed by an **executed probe** (run the command, read the source /297 `node_modules`) or a citation. **A universal or negative claim** ("the298 only", "never", "always", "one-way", "nothing reads", "otherwise clean")299 is established by an ***enumeration*** — a search you can show returns the300 *complete* set — **not a single positive citation** ([ADR-0052](../../docs/decisions/adr-0052-grounding-enumeration-for-universal-claims.md)):301 one true example proves nothing about the rest of the set, and these are the302 highest-value claims a future reader relies on. To claim enumeration, **state303 why the search is exhaustive** — what closes the set so nothing escapes.304 Some sets are closed by syntax and this is easy (imports in a package,305 call-sites in a repo); many only *look* `grep`-bounded — a "nothing reads306 this" search misses dynamic / reflective / ORM / string-built / config-wired /307 codegen'd / cross-repo access (illustrative, **not a checklist to clear**: the308 burden is to show the search captures every member, not to rule out named309 escapes). When you cannot show the search is exhaustive, an **empty result is310 absence of evidence, not an enumeration**: weaken the claim, tighten the311 boundary until the search genuinely closes the set, or move it to312 `## Assumptions`. Never dress an empty search as an enumeration — the313 frame-critique reviewer treats "I searched and it was empty" as *un*grounded314 until you have shown what closes the set. Everything you cannot verify goes in315 the spec stub's risk-gated `## Assumptions` section, marked explicitly — never316 asserted as fact. This **makes mandatory + derived** the existing informal317 "Current state (verified …)" discipline that the 064-01 retro found jig318 already half-practices by hand: it was grounding-by-probe all along, just319 reliant on author diligence. The `## Assumptions` you surface here has320 downstream value — slice 064-04 derives the `frame_review` trigger321 mechanically from it, so honest framing now is what decides later whether322 the adversarial frame-critique pass fires. The section is risk-gated: write323 "None" / omit when there are no unverified load-bearing assumptions; don't324 pad with boilerplate. (For a worked example of marked assumptions plus325 probe-grounded claims, see [ADR-0020](../../docs/decisions/adr-0020-spec-frame-hardening.md)326 `## Assumptions` A1–A4 + `## Kill criteria`, and the327 [spec 064-01 retro](../../docs/specs/064-spec-frame-hardening/retro.md),328 which probe-verified its three most load-bearing claims before recording329 them.)3307. **Let the assumptions decide `frame_review` (spec 064-04 / ADR-0020).**331 You are **not** asked "is frame-review needed?" — the `## Assumptions` you332 just surfaced decide it, mechanically. Set the slice's `frame_review` flag333 from `workflow.py frame-review-needed`:334335 ```bash336 python3 "${CLAUDE_PLUGIN_ROOT}/skills/spec-workflow/workflow.py" \337 frame-review-needed "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>"338 ```339340 The rule is a derivation, not a judgment call: `true` iff the slice's341 `## Assumptions` section carries ≥1 real (non-placeholder) assumption —342 so honest framing in step 6 is exactly what fires (or silences) the343 adversarial frame-critique pass. An inline-mirror / refactor slice with344 no unverified assumptions (`## Assumptions` absent or just "None") stays345 default-off. **ADRs are always-on** (OQ3): any ADR gets `frame_review:346 true` unconditionally — the deriver returns `true` for any `adr-*.md`347 path. When the value is `true`, set `frame_review: true` in the slice348 frontmatter so the gate + `session-plan` dispatch the pass.3498. Add rows to `docs/specs/README.md` (or regenerate via `workflow.py status-board`).350351### Picking up a slice3523531. Read the automatic `jig hint:` project-orientation headline injected at354 `SessionStart`, or refresh it manually before choosing work:355 ```bash356 python3 "${CLAUDE_PLUGIN_ROOT}/skills/spec-workflow/workflow.py" orient \357 --project-dir .358 ```359 The headline is computed from `scaffold.json` and lifecycle artifacts. Treat360 `docs/architecture.md`, the spec corpus, and the status board as authoritative;361 a shallow source-tree listing is not evidence that a scaffolded project is362 greenfield or that recorded stack decisions are absent.3632. Check `docs/specs/README.md` for the next slice in `READY_FOR_IMPLEMENTATION`364 (or `DRAFT` for a slice you intend to plan now).3653. Run:366 ```bash367 python3 "${CLAUDE_PLUGIN_ROOT}/skills/spec-workflow/workflow.py" transition \368 "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" IN_PROGRESS369 ```370 **Claim-on-working-state (spec 049-01, amended by371 [ADR-0045](../../docs/decisions/adr-0045-slice-claim-covers-active-lifecycle.md)).**372 On a frontmatter (file-per-slice) slice, a transition into a **working373 state** — `READY_FOR_REVIEW` / `IN_PROGRESS` / `REVIEWED` / `RECONCILED` —374 stamps `claimed_by:` (the current branch name, or `JIG_CLAIM_ID`), so375 spec-level work is marked too, not just implementation. Entering a **release376 point** clears it: the two pickup-queue states `DRAFT` /377 `READY_FOR_IMPLEMENTATION` (step 2 above tells you to choose work from378 exactly those, so a leftover owner there would mark a free slice as379 occupied), plus the terminal `DONE` / `DEFERRED` / `ABANDONED`.380381 It **refuses** only when the slice is already `IN_PROGRESS` under a382 *different* identifier and you are moving it to `IN_PROGRESS` (naming the383 holder, pointing at `--release`); any other foreign claim — on your copy or384 on `origin/main` — is a loud **non-blocking warning**, because two sessions385 working one spec can be legitimate. The claim is **local by default**; add386 `--push` (direct) or `--pr` (via PR) to reserve it on `origin/main` so other387 worktrees see it, at any working state, though only an `IN_PROGRESS`388 reservation also publishes `status:` there (race / protected-branch handling389 mirrors `workflow.py new`). At a working state that reservation is390 **best-effort** (for a target other than `IN_PROGRESS`): if the trunk copy is391 already `status: IN_PROGRESS` under someone else's claim or none, it warns and392 pushes nothing, because that state is what the start-of-build guard393 hard-blocks on — stamping a claim over it would move a live lock, or394 manufacture the enforced pair on an unclaimed copy. Your own trunk claim just395 reports a benign no-op. To force-release a stale claim: `transition396 <spec> <slice> <state> --release --reason "<why>"` (clears `claimed_by:`,397 logs to `## Release log`).398399 **Class-A cross-ref guard (spec 112 / ADR-0058).** A transition into a400 working state is *refused* when the slice is already `DONE` on `origin/main`401 — a stale branch re-advancing already-integrated work. The `→ IN_PROGRESS`402 case is caught by the start-collision guard (spec 051-04); the other working403 states (`READY_FOR_REVIEW` / `REVIEWED` / `RECONCILED`) by404 `_refuse_integrated_advance`, which reads the shared405 `cross_ref_state.identifier_state_on_ref` primitive. For a sanctioned re-open406 / supersession of integrated work, pass **`--reopen`** (a first-class,407 audited bypass distinct from the blanket `JIG_CROSSREF_GATE=0`); the408 `→ IN_PROGRESS` path's own escape remains `JIG_START_COLLISION_GATE=0`409 (convergence tracked in `docs/refinement-todo.md`). Unreachable `origin/main`410 → non-blocking warning, transition proceeds.411412 **Do not read a blank `claimed_by:` as "free".** It means *no claim is413 recorded*: claims are local unless pushed, so another worktree's unpushed414 claim is invisible, and a plain `Edit`-tool write to a slice takes no claim415 at all. A claim that IS present names the session that last *moved* the416 slice into a working state — a presence hint, not a live lock. When it417 matters, ask rather than assume — see418 [bug 014](../../docs/bugs/014-slice-claim-covers-only-in-progress.md).4194. Fill in / refresh `plan.md` and `tasks.md` for the slice.4205. Spawn the `implementer` subagent with the spec path. Prefix the Task prompt421 with `[jig:phase=implementation] [jig:spec=NNN] [jig:slice=NNN-NN]` so422 `jig-telemetry.sh` can attribute implementation-phase cost. Implementer423 writes the deliverable to disk (TDD — failing tests first).424425### After implementation426427Slices 031-01 + 031-02 + 060-05 wired a **multi-pass review flow** into the428post-implementation step. Every slice runs through two passes before the429`IN_PROGRESS → REVIEWED` transition; two further passes fire on demand —430the **arch** pass when the slice declares `arch_review: true`, and the431**code-health** pass when it declares `code_health_review: true`.432433The orchestrator runs the passes in this order:4344351. **Compliance pass — `jig:independent-review`** (always). Spawn the436 `reviewer` subagent against the deliverable using the prompt built by437 `review.py implementation`. Reviewer is read-only; it evaluates each438 acceptance criterion and returns439 `pass | fail | needs-changes`.4404412. **Craft pass — `pr-review`** (always). After the compliance pass442 returns, build the craft-pass prompt with `review.py pr-review` and443 spawn a second `reviewer`-shaped subagent. The reviewer is read-only444 (Read/Glob/Grep, **no `Skill` tool**), so it cannot route to a skill445 via Claude's skill router; instead `review.py` hands it a concrete446 richer-skill path to read-and-apply, falling back to jig's inlined447 baseline buckets (scope / blockers / nits / strengths). The pass448 returns the same `VERDICT / REASONING / SPECIFIC ISSUES /449 RECONCILIATION NOTES` envelope as the compliance pass, with450 SPECIFIC ISSUES entries tagged `[blocker]` / `[nit]` / `[strength]`.451452 **Select the richer skill first (spec 096-03 / ADR-0040 D3).**453 `--richer-skill` is a **required** argument on `pr-review` (and `arch-review`454 / `code-health`). Before building the craft prompt:455 - **Run the candidate step:** `review.py candidates pr_review456 "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" --pass craft`. It457 prints a **tiered** list — `[high-confidence]` (with descriptions) and458 `[speculative]` (names only) — and writes the shown set to a sidecar.459 - **Pick the single best HIGH-CONFIDENCE candidate** for the category and460 pass it as `--richer-skill <name>`. If several are genuine, **pick one —461 do not refuse**; the alternatives are recorded as the shown-and-declined462 set. If none fits, pass `--richer-skill none` for jig's baseline.463 - **The pick is a heuristic, not a guarantee** — an explicit464 `review.<category>_skill` in `scaffold.json` (096-01) **overrides** it, and465 `candidates` never picks for you (a `[speculative]` false positive like a466 briefing skill must not be selected on lexical grounds alone).467 - **CI / no orchestrator:** pass `--richer-skill none --non-interactive`;468 config remains the reproducible path. Omitting `--richer-skill` entirely,469 or invoking a pass with no sidecar + no config + no `--non-interactive`,470 **fails fast** (naming the missing `candidates` step) rather than silently471 using the baseline.4724733. **Arch pass — `arch-review`** (on-demand). Before running this pass,474 query the slice's `arch_review:` frontmatter flag via475 `workflow.py arch-review-needed`. When the helper prints `true`,476 build the arch-pass prompt with `review.py arch-review` and spawn a477 third `reviewer`-shaped subagent. The pass produces the four478 canonical arch buckets (summary / strengths / concerns / open479 questions) wrapped in the same verdict envelope, using the same480 candidate-channel resolution as the craft pass (096-03): run481 `review.py candidates arch_review …`, pick the best, pass `--richer-skill`;482 config (`review.arch_review_skill`) overrides, else jig's baseline buckets483 are inlined. When the helper prints `false`, skip this484 pass entirely. Slice authors flip the flag by uncommenting the485 `arch_review: true` line in the slice template's frontmatter — set486 it when the slice changes module boundaries, public contracts, or487 architecture-shaped concerns.4884894. **Code-health pass — `jig:code-health`** (on-demand, **gated**). Before490 running, query the slice's `code_health_review:` frontmatter flag via491 `workflow.py code-health-review-needed`. When it prints `true`, **run492 `health.py` yourself** (the orchestrator / CI), capture its tight493 summary, and feed THAT summary into `review.py code-health … --summary-file`494 (`--summary-file -` to pipe it in). Then spawn a `reviewer`-shaped subagent. **The reviewer495 is read-only (Read/Glob/Grep, no Bash) — it must NOT run `health.py`;496 it judges the summary you provide.** The reviewer renders the judgment a497 tool can't: is duplication within the [ADR-0002](../../docs/decisions/adr-0002-extract-helper-on-third-caller.md)498 inline-mirror budget? is a complex function inherent or fixable? are499 the lint findings worth blocking on? The pass returns the same verdict500 envelope, with SPECIFIC ISSUES tagged `[blocker]` / `[nit]` /501 `[strength]`. **Why gated, not always-on:** [ADR-0017](../../docs/decisions/adr-0017-scaffolded-code-health.md)502 flags the per-slice review cost (specs 055/057 context-cost discipline)503 and recommends gating it like arch-review — so it defaults off and slice504 authors opt in with `code_health_review: true`. The evidence file is505 `reviews/slice-NN-code-health.md`.506507When spawning any reviewer Task above, prefix the Task prompt with telemetry508tags before the `review.py` body: `[jig:phase=<phase>] [jig:spec=NNN]509[jig:slice=NNN-NN]`. Use `compliance` for `review.py implementation`,510`craft` for `pr-review`, `arch` for `arch-review`, `code-health` for511`code-health`, and `reconciliation` for the final reconciliation review.512513**Block rule for the REVIEWED transition.** All required passes514(compliance + craft, plus arch when `arch_review: true`, plus code-health515when `code_health_review: true`) must pass before516`transition <slice> REVIEWED`:517518- Any `fail` verdict from any pass blocks the transition.519- `needs-changes` from the compliance pass blocks (the implementer520 addresses findings and re-runs).521- `needs-changes` from the craft pass does NOT block — the522 `[nit]`-tagged entries become reconciliation-log items (the523 implementer captures them in the deviation log during reconciliation).524 Only `[blocker]`-tagged entries from the craft pass block the525 transition.526- The arch pass follows the same rule as the craft pass:527 `[blocker]`-tagged entries block; `[nit]`-tagged entries and528 `needs-changes` become reconciliation-log items.529- The code-health pass follows the same rule: `[blocker]`-tagged entries530 block the `REVIEWED` transition; `[nit]`-tagged entries become531 reconciliation-log items.532533**Measurement tag (non-gating).** The craft/arch/code-health passes also534self-classify each SPECIFIC ISSUES entry `[spec]` (an acceptance-criteria /535spec-frame issue a smarter clarify pass could have prevented) or `[impl]`536(implementation-discipline — untested edge, brittle fixture, drift). This537carries no gating consequence — it exists purely so a future rollup can538count `[spec]`-tagged themes across specs (docs/refinement-todo.md539"Instrument the review→learnings→clarify loop before building it").540541**The gate is mechanical, not advisory (slice 045-03 / [ADR-0014](../../docs/decisions/adr-0014-review-evidence-model.md) §5).**542`workflow.py transition` now *refuses* the `REVIEWED` / `RECONCILED` /543`DONE` moves unless the required review evidence — recorded with544`review.py record-review` as `docs/specs/NNN-<slug>/reviews/slice-NN-<pass>.md`545— exists and clears (`verdict: pass`). `REVIEWED` requires546`compliance` + `craft` (+ `arch` when the slice declares547`arch_review: true`, + `code-health` when it declares548`code_health_review: true`); `RECONCILED` requires the `reconciliation` verdict549**and** `### Deviation log` plus `### Reconciliation sweep` subsections;550`DONE` re-validates the post-implementation and reconciliation evidence set551(in addition to the existing `dependencies:` check). A refusal names552the missing/invalid artifact and the `record-review` command to produce553it. The gate enforces *evidence consistency*, not human sign-off (it554lives in the agent's trust boundary per [ADR-0011](../../docs/decisions/adr-0011-spec-gate-model.md)).555Bypass it for a deliberate out-of-band flow by setting556`JIG_REVIEW_EVIDENCE_GATE=0` (also `false`/`off`/`no`) — the status still557transitions and the `DONE` dependency check still runs; only the evidence558check is skipped.559560After all required passes pass:5615624. Address any reviewer findings, adding regression tests for any real563 bugs found.5645. **Record each pass's verdict** as durable evidence with565 `review.py record-review` (writes566 `docs/specs/NNN-<slug>/reviews/slice-NN-<pass>.md` — see the567 independent-review SKILL.md § "Recording and checking review568 evidence"). The `REVIEWED` transition is gated on this evidence, so it569 is not optional.5706. Transition: `transition <spec.md> <slice> REVIEWED`. The gate571 re-validates the recorded `compliance` + `craft` (+ `arch`,572 + `code-health`) verdicts before the status flips (and before the573 003-04 auto-tick).574575**Recovering from a failed review.** A `fail`/`needs-changes` verdict — or576a `[blocker]`-tagged craft/arch finding, which is recorded as a non-`pass`577verdict — blocks the `REVIEWED` transition. To recover: address the578findings, re-run the pass against the updated deliverable, `record-review`579the new verdict (it **overwrites in place** the earlier file for that580`(slice, pass)`; git history keeps the prior one), then re-run581`transition … REVIEWED`. With every required pass now `pass`, the gate582clears. A non-`pass` artifact never overwritten by a later `pass` keeps583blocking — the "superseded without a later pass" case (ADR-0014 §4).584585**When a review retracts a *claim*, sweep the corpus before re-recording.**586The recovery above is written for a code-shaped finding, which is local to one587file. A finding about **content** is not: a retracted assertion is usually588copied by design into `CHANGELOG.md`, the slice record, the inbox, and589cross-referenced docs. Fixing only the reviewed deliverable leaves the590retracted version authoritative in every **sibling** artifact — and the stale591copy is frequently the one the next session reads first (the project's own592rules make `CHANGELOG.md` a read-before-you-fix record), so the pass re-fails593round after round on a document you never touched. Before you `record-review`594the new verdict, **grep the retracted phrasing across the docs root and595`CHANGELOG.md`, and reconcile every hit** — the plain sweep is what reaches596the changelog, the inbox, and arbitrary cross-referenced files. Within the597spec itself, `/jig:analyze` is the structured complement: its **Duplication**598and **Terminology Drift** categories catch a retracted claim surviving across599the spec's own slice files and the docs it cross-references (`product-vision`,600accepted ADRs, the glossary, `architecture.md`) — but it audits one spec's601files plus that fixed set, not the whole corpus, so it sharpens the sweep602rather than replacing it. Distinguish **surviving** assertions (the claim603still stated as true — must fix) from **explicit** retractions (the claim604named as withdrawn in a changelog or history entry — correct, and worth605keeping).606607```bash608# Compliance pass (always)609PROMPT=$(python3 "${CLAUDE_PLUGIN_ROOT}/skills/independent-review/review.py" \610 implementation "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" \611 "<deliverable-path-1>" ...)612SUBAGENT=$(python3 "${CLAUDE_PLUGIN_ROOT}/skills/independent-review/review.py" \613 subagent-type implementation)614# … feed "[jig:phase=compliance] [jig:spec=NNN] [jig:slice=NNN-NN]\n\n$PROMPT"615# … to Task with subagent_type: $SUBAGENT, wait for pass …616617# Craft pass (always) — spec 096-03: select the richer skill first.618# 1. Show the tiered candidates + write the sidecar:619python3 "${CLAUDE_PLUGIN_ROOT}/skills/independent-review/review.py" \620 candidates pr_review "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" \621 --pass craft622# 2. Read the [high-confidence] tier, pick the single best (or `none`), then623# build the prompt with the REQUIRED --richer-skill (config overrides it):624PROMPT=$(python3 "${CLAUDE_PLUGIN_ROOT}/skills/independent-review/review.py" \625 pr-review "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" \626 "<deliverable-path-1>" ... --richer-skill "<name-or-none>")627SUBAGENT=$(python3 "${CLAUDE_PLUGIN_ROOT}/skills/independent-review/review.py" \628 subagent-type pr-review)629# … feed "[jig:phase=craft] [jig:spec=NNN] [jig:slice=NNN-NN]\n\n$PROMPT"630# … to Task with subagent_type: $SUBAGENT, wait for pass …631# (CI / no orchestrator: --richer-skill none --non-interactive.)632633# Arch pass (only when slice frontmatter has `arch_review: true`)634# IMPORTANT: capture the helper exit code — a non-zero exit means the635# slice lookup failed (missing spec / unknown fragment / ambiguous),636# not "no arch pass needed." Surface the error rather than silently637# skipping the pass.638if ! NEED_ARCH=$(python3 "${CLAUDE_PLUGIN_ROOT}/skills/spec-workflow/workflow.py" \639 arch-review-needed "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>"); then640 echo "arch-review-needed failed — aborting" >&2641 exit 2642fi643if [ "$NEED_ARCH" = "true" ]; then644 # 096-03: show candidates for arch_review, then pick (config overrides).645 python3 "${CLAUDE_PLUGIN_ROOT}/skills/independent-review/review.py" \646 candidates arch_review "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" \647 --pass arch648 PROMPT=$(python3 "${CLAUDE_PLUGIN_ROOT}/skills/independent-review/review.py" \649 arch-review "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>" \650 "<deliverable-path-1>" ... --richer-skill "<name-or-none>")651 SUBAGENT=$(python3 "${CLAUDE_PLUGIN_ROOT}/skills/independent-review/review.py" \652 subagent-type arch-review)653 # … feed "[jig:phase=arch] [jig:spec=NNN] [jig:slice=NNN-NN]\n\n$PROMPT"654 # … to Task with subagent_type: $SUBAGENT, wait for pass …655fi656657# Code-health pass (only when slice frontmatter has `code_health_review: true`)658# The orchestrator runs health.py and feeds its summary IN — the read-only659# reviewer never runs the tool (no Bash).660if ! NEED_CH=$(python3 "${CLAUDE_PLUGIN_ROOT}/skills/spec-workflow/workflow.py" \661 code-health-review-needed "docs/specs/NNN-<slug>/spec.md" "<slice-fragment>"); then662 echo "code-health-review-needed failed — aborting" >&2663 exit 2664fi665if [ "$NEED_CH" = "true" ]; then666 # Ru667668…(truncated)