Help us improve
Share bugs, ideas, or general feedback.
From spectre
Unified inception — distill a vague vision into a First-Principles spec with action/verification steps, then lock it as the active spec.
npx claudepluginhub joncik91/spectre --plugin spectreHow this skill is triggered — by the user, by Claude, or both
Slash command
/spectre:visionThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Triggered when the user types `/vision` followed by free-form text describing what they want built. Follow this protocol **exactly** — it is the only legitimate path from human intent to locked spec.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Breaks plans, specs, or PRDs into thin vertical-slice issues on the project issue tracker using tracer bullets. Useful for converting high-level work into grabbable implementation tickets.
Share bugs, ideas, or general feedback.
Triggered when the user types /vision followed by free-form text describing what they want built. Follow this protocol exactly — it is the only legitimate path from human intent to locked spec.
.active live under the user's current working directory at session start (e.g. /home/foo/myproject/specs/...), NEVER under the plugin's install path (~/.claude/plugins/... or ${CLAUDE_PLUGIN_ROOT}). If you find yourself about to write into a plugin cache directory, STOP — that is a bug. Resolve cwd via pwd once at the start of the Lock phase and use absolute paths from there..active files.step to 1; nothing else here writes to it..active.At skill entry, before any CLI invocation, Claude reads the most recent hydrate.signal
status emitted by the SessionStart hook. If is_first_run=true, render this block to the
user verbatim:
Welcome — Spectre turns vague ideas into machine-verified specs.
/vision <your idea> interview you to lock a spec
/implement execute the locked spec step-by-step
spectre glossary browse plain-English explanations of every term
Then surface the following numbered choice prompt:
1) Start fresh — describe what you want to build
2) Use the built-in template as a starter
3) Skip the welcome and just start (won't re-prompt next session)
Operator picks 1, 2, or 3:
spectre templates import-builtin --name template --slug <user-supplied-slug>
On OK templates.import_builtin, the imported draft becomes the seed for the walk's
draft materialization in the Draft phase. Skip the Fingerprint phase and proceed
directly to the Wizard phase.
If the import-builtin subcommand is unavailable (non-zero exit), fall back to
surfacing the manual copy command:
cp ${CLAUDE_PLUGIN_ROOT}/specs/template.spec.md specs/<slug>.spec.md.draft
Then re-invoke /vision.mkdir -p state && touch state/.spectre-welcomed
If is_first_run=false, skip this section entirely and proceed to the Fingerprint phase.
Before treating the user's text as a Spark, run the fingerprinter to surface prior art in the user's codebase:
python3 bin/fingerprint.py 2>&1 | tail -5
This writes state/local-symbols.json. Read the first 50 entries:
python3 -c "import json; d=json.load(open('state/local-symbols.json')); print(json.dumps(d[:50], indent=2))"
When the walker surfaces concerns in the Walker loop phase, scan the symbol map for any function/class/module conceptually related to the user's intent (e.g. intent says "fetch BTC price" → search for fetch, price, http, bitcoin, btc, requests). If you find a candidate, surface it via a branch-resolution concern: "We will NOT reinvent <symbol_name> at <file>:<line> — reuse it as the basis for step N." The "never reinvent the wheel" rule is enforced by construction here. Skipping this phase violates the rule silently.
Template-import surfacing. Also list any reusable spec/skill templates the user has previously exported:
spectre templates list --limit 10
Stdout: RESULT templates.list count=N items=spec:name,.... Pass --json for the full descriptor list when you need the absolute path.
If templates exist, surface them as candidate starting points during the Walker loop's interrogation walk: "Your library has <template_name> (a <kind>). Import as a base for this spec? (import / no)". On import, call templates.import_template(source_name=<name>, target_name=<slug>); the imported draft becomes the seed for the walk's draft materialization in the Draft phase.
Before §1 distillation, fire the substrate wizard to populate §8.2 (cognitive-substrate contract). Answers are cached at ~/.spectre/substrate-cache/<author-spec-hash>.json; re-running /vision on an unchanged spec body skips re-prompting (cache hit exits 0 immediately, flags are ignored unless --force is passed).
Step 1 — compute the author-spec hash (over the draft body MINUS any existing §8.2 block):
AUTHOR_SPEC_HASH="$(printf %s "$DRAFT_BODY_NO_82" | sha256sum | awk '{print $1}')"
Step 2 — ask the user the 4 questions (via the conversation, not via a TTY):
Ask the user these four questions and capture their answers as shell variables:
Receiver fingerprint — which execution context will implement this spec?
Accepted values: claude-code+human | claude-code-autonomous | non-claude-ai | human-only
Store as $RECEIVER.
Trust profile — which risk categories apply? Comma-separated subset of:
untrusted-input | handles-secrets | touches-network | executes-generated-code
Or the literal none for no risk flags.
Store as $TRUST_PROFILE.
Contextual binding — one-line description of what this spec is FOR (the evaluator refuses replay as something else). Must be non-empty.
Store as $BINDING.
Provenance — is this a fresh spec or derived from an existing locked spec?
Accepted values: none (fresh spec) | derived-from <slug> <parent-envelope-sha256> (fork).
Store as $PROVENANCE.
Step 3 — invoke the wizard CLI with flags:
spectre substrate_wizard run \
--author-spec-hash "$AUTHOR_SPEC_HASH" \
--receiver "$RECEIVER" \
--trust-profile "$TRUST_PROFILE" \
--binding "$BINDING" \
--provenance "$PROVENANCE"
Capture stdout — that's the §8.2 markdown block. Inject it after §8.1 in the spec template.
Failure modes:
error wizard.substrate reason=missing_flags missing=<list> — Claude did not populate all four flags. Fix: ensure all four variables are set before invoking.error wizard.substrate reason=invalid_<which> ... — a flag value failed validation. Fix: correct the value per the accepted-values list above and re-invoke.$BINDING, $RECEIVER, etc. from this run are ignored (cached values win). Pass --force to override.If trust-profile includes untrusted-input or handles-secrets, every step that produces an artifact MUST declare untrusted-input: yes/no and (when relevant) sanitizes: covering its sanitized OUTPUT. The evaluator's Tier 1 will block on missing annotations.
Step 4 — fire per-view substrate: §§8.3-8.7 carry one substrate block per non-agent view (product-input, product-output, human-user, integrator, operator). Each view is either in-scope (the spec author answered the scope-check concern with a value other than not-applicable) or marked N/A. For each in-scope view, ask the four substrate questions tailored to that view (the receiver-fingerprint vocabulary is per-view; see bin/substrate_wizard._VIEW_FINGERPRINTS for the allowed values). Invoke the per-view CLI:
spectre substrate_wizard run-per-view \
--view "$VIEW" \
--receiver "$RECEIVER" \
--trust-profile "$TRUST_PROFILE" \
--binding "$BINDING"
Capture stdout — that's the §8.x markdown block. Inject it after §8.2 in spec-document order.
For views the walker has answered not-applicable, invoke instead:
spectre substrate_wizard run-per-view \
--view "$VIEW" \
--receiver "not-applicable" \
--not-applicable-reason "$REASON"
The resulting §8.x block degenerates to a single not-applicable: <reason> field; the corresponding §9-13 view section body must also be replaced with the same not-applicable: <reason> marker.
Failure modes:
error wizard.substrate reason=invalid_view ... — view name not in {product-input, product-output, human-user, integrator, operator}.error wizard.substrate reason=invalid_receiver-fingerprint ... — receiver value not in the view's vocabulary.error wizard.substrate reason=missing_not_applicable_reason — --receiver not-applicable passed without --not-applicable-reason.Trust-profile vocabularies are per-view and NOT interchangeable. untrusted-input is valid only in §8.2 (implementing-agent); accessibility-required is valid only in §8.5 (human-user); paging-required is valid only in §8.7 (operator). Cross-pollination is rejected by the validator.
The user has typed /vision <free-form text>. Treat the text as intent, not as a spec. Anchor cwd via pwd to capture $PROJECT. If $PROJECT looks like a plugin cache (/root/.claude/plugins/...), HALT.
Open-question markers. The intent text may declare known unknowns in two formats:
YAML frontmatter (preferred):
---
open_questions:
- daemon lifecycle (start/stop/restart/owner)
- prompt failure-mode handling on malformed JSON
---
<rest of intent prose>
Inline markers in prose:
We need a code legibility daemon. open: should it run as systemd or pm2?
The auth strategy is unresolved: OAuth or API key?
The walker parses these markers automatically when you pass the intent to init-or-resume. Pass the raw intent text verbatim — do NOT paraphrase first. The walker extracts markers and tracks them as open_questions in state. Each detected open question gets a stable oq-N id. The walker refuses author-arbitrated stop until every open question is resolved or deferred.
Refuse physically impossible requests; if multi-subsystem, ask which sub-problem to lock first. Do NOT draft or interrogate further until this is settled.
Resume an existing walk if one is live for this spec, otherwise initialize a new one. Derive the canonical draft path from the slug via the spec-evaluator CLI (never substitute <slug> into a Path("specs/...") literal inline):
SPEC_PATH="$(spectre spec_evaluator slug-to-path --slug "<slug>")"
DRAFT_PATH="${SPEC_PATH}.draft"
spectre walker init-or-resume \
--intent "<verbatim user intent>" \
--draft "$DRAFT_PATH" \
--state-path state/.walk.json
Stdout: OK walker.init rounds=N pending=M stop=<reason|none>. The --intent value is ignored on resume (existing walk's intent wins); on first run it is persisted into state/.walk.json along with five seed concerns covering §8.1 mutates, never-touches, decision-budget, and reboot-survival fields.
Repeat until the walker reports stop:
Read next concern. Fetch the next pending concern body via the CLI:
spectre walker peek-pending \
--state-path state/.walk.json \
--json
Stdout: a JSON object with id, kind, receiver, summary, and round fields, or null when the walk is exhausted. If null, jump to the Draft phase.
Render the concern to the operator. peek-pending emits a PROMPT walker.concern line with id=, round=, prompt= (the concern summary), and optionally options= (comma-separated prefab choices). Read the RESULT walker.peek line for kind and receiver. Then render:
ROUND <N> · id: <concern_id> · receiver: <receivers> · kind: <kind>
<prompt= text from PROMPT walker.concern line>
If options= is present, render numbered choices below the question text:
1) <option_1>
2) <option_2>
...
The operator may answer with the digit (e.g. 1) or the option token (e.g. yes), or type a free-form answer. If options= is absent, accept free-form text only.
v1.0 — exemplar-selection concerns. When a concern ID matches *-style-* (e.g. help-text-style-hu, error-text-style-hu, log-format-style-op, api-exemplar-int, observability-style-op), the operator is being asked to bind the spec to a catalog exemplar. Before rendering the question, fetch the catalog entries for the relevant view-type:
spectre exemplars list --view-type <type> --json
Then render each option with its axis values so the operator sees what they're choosing between, not just from:
1) <slug-1> — axes: <axis1=value, axis2=value, ...>
2) <slug-2> — axes: <axis1=value, axis2=value, ...>
...
N) none — skip exemplar binding for this view
The operator's answer becomes the spec's exemplar binding (e.g. help-text-style: exemplar:help-text:curl). Tier-2 cross_view_gate validates the binding against the catalog at lock time; Tier-3 llm_judge injects the chosen exemplar's conventions into its contradiction prompt during implementation review.
v1.1 — fingerprint-filtered exemplar options. The walker automatically filters catalog options by the view's receiver-fingerprint (via calibrated_for metadata). If zero compatible exemplars exist, prefab_options switches to:
[fingerprint-mismatch]post-ship-iteration for catalog-gap deferralRender annotated options without the [fingerprint-mismatch] suffix in the display (it's metadata for the operator's awareness). If the operator picks post-ship-iteration, write the spec field as <aspect>-style: post-ship-iteration (not an exemplar: binding). At lock time, cross_view_gate emits post-ship-iteration-deferral (info, tier 2) for that view.
When to pick post-ship-iteration: the catalog has a structural gap — no exemplar exists for this view's fingerprint type. Do NOT pick it to avoid choosing; pick it only when the catalog genuinely lacks a compatible entry.
If more than one view is deferred via post-ship-iteration, lock emits excessive-post-ship-iteration (warn, tier 2) — a signal that the catalog needs new exemplars, not that individual mismatches should be tolerated.
v1.0 — view-scope concerns. Concern IDs scope-product-input, scope-product-output, scope-human-user, scope-integrator, scope-operator ask the operator to declare whether a view applies. When the operator answers not-applicable, the corresponding §8.x substrate block degenerates to not-applicable: <reason> and the §9-13 section body is omitted. The walker short-circuits follow-up concerns for that view.
Append on the same turn:
(answer, or `revise <concern_id>` to amend an earlier answer, or `stop` to lock the walk)
On revise without an id, fetch full state to list asked concerns:
spectre walker get-state \
--state-path state/.walk.json \
--json
Parse answered from the JSON and display each as <id> | <one-line summary> | <answer-truncated> so the user can pick which to amend.
Capture user input. Three branches:
stop — record stop via:
spectre walker stop \
--reason author-arbitrated \
--state-path state/.walk.json
Jump to the Draft phase. If the walker emits WARN walker.open-questions-unresolved count=K ids=..., the gate fired — K open questions must be answered or deferred before you can stop. To defer an open question to a later ADR:
spectre walker defer-open-question \
--id oq-2 \
--adr adr-0007 \
--state-path state/.walk.json
Stdout: OK walker.open-question-deferred id=oq-2 adr=adr-0007.revise <concern_id> <new_answer> — record the revised answer and ask the user about stale concerns:
spectre walker answer-concern \
--id <concern_id> \
--answer "<new_answer>" \
--state-path state/.walk.json
Re-run get-state to surface any now-stale sibling concerns. Ask: "These concerns are now stale: . Re-walk them or accept-stale?" On re-walk, the walker's peek-pending will surface them in the next round naturally.spectre walker answer-concern \
--id <concern_id> \
--answer "<user text>" \
--state-path state/.walk.json
Run the Tier 3 yield-delta check (only if ~/.spectre/reviewer.toml has [tier3] enabled = true AND we have an in-progress draft):
spectre walker yield-check \
--draft "$DRAFT_PATH" \
--state-path state/.walk.json
Stdout: OK walker.yield new_t3=N history=[...] on a real evaluation, or OK walker.yield_skipped reason=<reason> when preconditions fail (no walk state, draft missing, round_count=0). The CLI uses ~/.spectre/reviewer.toml and state/ as the bundle dir by default; override with --config and --bundle-dir when needed.
Recommend-stop transition. If answer-concern emits RESULT walker.recommend-stop reason=coverage-complete, surface this to the operator inline:
Walker recommends stopping: coverage is complete. Reply
stopto lock, or continue answering to refine further.
This emission is idempotent — it fires exactly once when coverage transitions from incomplete to complete. It does NOT fire again if the user continues and coverage stays complete.
Concern rendering with prefab options. When a concern has non-empty prefab_options, display them as numbered choices before the open-answer prompt. Rules:
kind is NOT receiver-clarification and "defer to later layer" is not already in the list, append it as the last choice.state.answered entry. Contradiction rule: the option shares ≥ 2 content tokens with an answered value that contains a negation word (not, no, never, without, excluding, vendor-agnostic). Example: if a prior answer says "vendor-agnostic, no DeepSeek-only options", drop a prefab containing "deepseek". Apply this in prose — the walker library exposes no helper for this in v0.8.2.state.answered shape (Fix N): state.answered is a dict[str, str] mapping concern_id → answer_text, not a list. Iterate over answered values with state.answered.values() (e.g. for answer in state.answered.values(): ...). The prefab-contradiction logic above uses this pattern to scan all prior answers.
Check stop conditions. Re-run peek-pending — if it returns null, the walk is exhausted; proceed to the Draft phase. Alternatively, check the stop field in the full state dump:
spectre walker get-state \
--state-path state/.walk.json \
--json
If stop is non-null in the returned JSON, proceed to the Draft phase. Otherwise, loop back.
The walk has stopped. Render the draft from accumulated state.answered (a dict[str, str] mapping concern_id → answer_text) plus state.spec_intent. To iterate over all answers, use state.answered.values() or state.answered.items() for the id–text pair:
Slugify the title (lowercase, [^a-z0-9]+ → -, trim).
Write the draft file atomically at $PROJECT/specs/<slug>.spec.md.draft using the answers as Steps + First Principles + §§8-13 content. The exact mapping from concerns to spec sections lives in your judgment of what the answers tell you; the skill does not impose a 1:1 mapping.
v1.0 structure (required): frontmatter must include **Spec-version:** 1.0. The draft must carry §§1-7 (implementing-agent view), §§8.1-8.7 (receiver-calibration family, one block per view), and §§9-13 (one view section per non-agent receiver) per specs/template.spec.md. Views the operator marked not-applicable via scope-* concerns get a single not-applicable: <reason> field in their §8.x substrate AND a single not-applicable: <reason> marker in place of their §9-13 contracts. Views the operator put in-scope get fully-populated §8.x and §9-13 contents.
§§9-13 contracts MUST declare at least one of: ### Mechanical contracts, ### Coverage contracts, ### Exemplar bindings (Tier-1 malformed-view-contract blocks lock otherwise). Exemplar bindings use the form <aspect>-style: exemplar:<view-type>:<slug> referencing a catalog entry surfaced by the walker (verify with spectre exemplars list). Pin the taxonomy version: taxonomy-version: <view-type>:<int>, ....
Cross-view string references (e.g. <halt-hint from §8.2 ux-contract>) are resolved by Tier-2 cross_view_gate at lock time — a reference to a non-existent §8.x field is a block finding. Prefer references over duplicate strings when the same message appears across views.
Emit the lock-confirm prompt and render it to the operator. After printing the draft summary line, emit:
spectre _status emit prompt vision.lock_confirm \
--field draft="specs/<slug>.spec.md.draft" \
--field summary="<N steps; walked R rounds; stop=<stop_reason>>" \
--field options="yes,refine,cancel"
Then render:
DRAFT: specs/<slug>.spec.md.draft (<summary>)
1) yes — lock and proceed to /implement
2) refine "<change>" — apply a refinement and re-evaluate
3) cancel — discard the draft
Wait for the user.
yes (or 1) → continue to the Wizard phase (setup wizard) → Evaluator gate.refine "<change>" (or 2) — if the change implies revising a prior concern, call answer-concern with the updated answer first (same CLI as Walker loop phase), then re-render the draft. Otherwise, edit the draft directly and re-run the evaluator.cancel (or 3) → delete the draft AND state/.walk.json AND state/.eval-bundle.json. Halt.The walker's invalidation set + dependency graph are never re-rendered for the user post-confirmation; they exist only to drive interrogation cycles. After the user says yes and the evaluator passes, state/.walk.json may be retained as audit trail or cleared.
Draft-to-disk. The user said yes. Do NOT print the full spec body again — write it directly to disk so the user can review in their editor.
pwd to capture the absolute path (e.g. /home/foo/myproject). Call this $PROJECT. All file paths in the rest of this phase are $PROJECT/specs/..., $PROJECT/state/..., and $PROJECT/decisions/.... If $PROJECT looks like a plugin cache (/root/.claude/plugins/, ${CLAUDE_PLUGIN_ROOT}, or contains plugins/cache/), HALT and tell the user to restart Claude Code from their project directory.mkdir -p "$PROJECT/specs" "$PROJECT/state" "$PROJECT/decisions" to ensure the dirs exist.-, collapse repeats, strip leading/trailing -.Generated: to today's ISO date and Slug: to the computed slug.$PROJECT/specs/<slug>.spec.md.draft. Use atomic write: write to <draft>.tmp then mv to <draft>.spectre _status emit prompt vision.lock_confirm \
--field draft="specs/<slug>.spec.md.draft" \
--field summary="<N steps>" \
--field options="yes,refine,cancel"
Then render to the operator:
Lock specs/<slug>.spec.md.draft (N steps).
1) yes — lock and proceed to /implement
2) refine "<change>" — apply a refinement and re-evaluate
3) cancel — discard the draft
yes (or 1) → continue to the Evaluator gate.refine "<change>" (or 2) → reopen the draft file, apply the requested change, atomically rewrite the .draft file, re-run the evaluator, re-emit the lock-confirm prompt. Repeat until yes.cancel → delete the .draft file AND clear state/.eval-bundle.json, halt, write nothing else.Before running the evaluator, ensure ~/.spectre/reviewer.toml exists. If it doesn't, the wizard auto-creates it — detecting any DeepSeek API key in the live environment, then optionally in a .env-style secrets file pointed to by the SPECTRE_SECRETS_FILE env var. The TOML is always written (with enabled=false if no key is found) so subsequent runs don't re-prompt.
spectre setup_wizard provision
Stdout: OK wizard.setup result=<result> target=<path>. Outcomes: result=exists (no-op), result=enabled (DeepSeek API key found), result=setup-skipped (no key found — placeholder enabled=false written). After the wizard runs, ~/.spectre/reviewer.toml is guaranteed to exist; the evaluator always passes that path.
After the user replies yes (or refine), run the spec evaluator over a review bundle (preview ADRs + preview Resource nodes + preview tier classifications materialized but not committed). Tiers 1+2 always run (deterministic, local). Tier 3 (DeepSeek deepseek-v4-flash adversarial reviewer) runs only when ~/.spectre/reviewer.toml has [tier3] enabled = true AND the configured API key is present in the environment. When Tier 3 is unavailable for any reason, the evaluator emits an info-severity tier3-unavailable finding so the skip is visible, never silent.
First derive the canonical draft path from the slug — never substitute the slug into a Path("specs/<slug>...") literal inline:
SPEC_PATH="$(spectre spec_evaluator slug-to-path --slug "<slug>")"
DRAFT_PATH="${SPEC_PATH}.draft"
Then run the evaluator and capture the full result (findings + sidecar_payload) to state/.eval-result.json so the Lock phase can re-use the same policy_hash/tiers_run without re-running the evaluator:
spectre spec_evaluator evaluate \
--spec "$DRAFT_PATH" \
--config "$HOME/.spectre/reviewer.toml" \
--bundle-dir state \
--output state/.eval-result.json
The output JSON has top-level keys findings (list), max_severity (string), and sidecar_payload (dict). The bundle is also persisted at state/.eval-bundle.json. Read state/.eval-result.json to surface findings; the tiers_run value is at sidecar_payload.tiers_run and max_severity is the top-level field.
After running, surface a one-line tier status block before the findings:
tier 1: PASS (n findings)
tier 2: PASS (n findings)
tier 3: PASS (n findings) # if tiers_run includes 3
tier 3: SKIPPED (<reason>) # if a tier3-unavailable finding is present
Reasons for SKIPPED come from the tier3-unavailable finding's message field: config-missing, disabled-in-config, no-api-key, auth failure (HTTP 401/403 …), provider error (HTTP 5xx), bad request, or socket-timeout. The user should always see one line per tier — the goal is to make Tier 3 status as visible as Tiers 1 and 2.
Auth-failure prominence. When the tier3-unavailable message contains the substring auth failure, prepend a separate banner ABOVE the tier status block so the user can act on it without scanning findings:
⚠ Tier 3 unavailable due to auth — fix ~/.spectre/secrets.env or DEEPSEEK_API_KEY then re-run /vision.
The banner must precede the tier 1/2/3 lines and be visible regardless of how many other findings the evaluator emitted.
Interpret the result (read max_severity from the JSON):
max_severity == "block" — list every block-severity finding to the user. The spec CANNOT lock until refine resolves them. Halt with:
RESULT eval.summary block=N warn=M info=K
[1] tier <X> · <kind> · step <S> · <message>
[2] tier <X> · <kind> · step <S> · <message>
...
Reply: refine "<change>" / cancel
Do NOT proceed to Lock. The bundle stays persisted at state/.eval-bundle.json keyed by the current draft hash; on refine, the rewritten draft will produce a new bundle automatically.
max_severity == "warn" — surface warnings to the user, then emit the warn-proceed prompt and render it:
spectre _status emit prompt vision.warn_proceed \
--field warn_count=N \
--field options="yes,refine,cancel"
Render to the operator:
Spec has N warn-severity finding(s).
1) yes — proceed to lock with warnings
2) refine "<change>" — address the warnings first
3) cancel — discard the draft
On yes (or 1), continue to Lock.
max_severity == "info" or "none" — surface findings briefly (≤3 lines) and proceed to Lock silently.
Tier 3 false-positive dismissal: if a Tier 3 finding has dismissable: true and the user wants to accept the risk, they can append a block at the bottom of the spec body:
# tier3-dismissed: <fingerprint> "one-line reason"
The fingerprint is the SHA-256 hex from bin.findings.fingerprint(f) — printable from the JSON output above. Re-running the evaluator will skip the dismissed finding; the dismissal is recorded in the <slug>.spec.md.eval.json sidecar after lock so audits can see what was suppressed and why.
dismissable: false findings (Tier 1+2 block-severity) CANNOT be dismissed via this mechanism — they must be fixed via refine.
Scan the locked spec's ## 2. First Principles bullets and step why: lines for explicit decision markers. A decision marker is any line that:
decision: (case-insensitive), ORwe choose <X> because, OR<X> over <Y> with a comparative justification.For each decision found:
spectre adr write \
--dir decisions \
--title "<extracted decision title>" \
--body "<one paragraph: the decision + the why from the spec>"
Stdout: OK adr.write path=<path>. Pass --supersedes "NNNN" only when this decision contradicts an existing ADR. The --date flag defaults to today's ISO date when omitted.
Supersedes detection. Before writing each ADR, list decisions/*.md and look for an existing ADR whose title or body contradicts the new decision. If found, pass supersedes="<old_id>". If unsure, do NOT supersede.
Graph wiring. If specs/.graph.md exists AND both new and old ADRs are represented as nodes, run:
spectre adr update-graph \
--graph specs/.graph.md \
--new adr-NNNN \
--old adr-MMMM
The CLI is a no-op when nodes are absent or the manifest is missing, so it is safe to always invoke after a supersede write.
Before invoking the evaluator (§6.4 prep), check coverage:
spectre walker coverage \
--state-path state/.walk.json \
--draft "$DRAFT_PATH"
Stdout: RESULT walker.coverage answered=N pending=M deferred=K undefined-invariants=L recommended-stop=yes|no rounds=R. Surface this line to the operator. If recommended-stop=no, emit the coverage-continue prompt then render it:
spectre _status emit prompt vision.coverage_continue \
--field missing="<pending+deferred count>" \
--field options="yes,refine"
Then render to the operator:
Coverage incomplete (pending/deferred questions remain).
1) yes — proceed to lock anyway
2) refine — continue the interview
On yes (or 1), proceed. On refine (or 2), re-enter the Walker loop.
Now that the user confirmed and ADRs are written:
Atomic rename: mv "$PROJECT/specs/<slug>.spec.md.draft" "$PROJECT/specs/<slug>.spec.md".
Atomically flip .active:
printf 'specs/<slug>.spec.md\n' > "$PROJECT/specs/.active.tmp" && mv "$PROJECT/specs/.active.tmp" "$PROJECT/specs/.active"
Reset $PROJECT/state/scratchpad.json for the active spec:
spectre _scratchpad ensure-v2 \
--scratchpad state/scratchpad.json
spectre _scratchpad reset \
--active-spec specs/<slug>.spec.md \
--scratchpad state/scratchpad.json
ensure-v2 promotes a v1 scratchpad to v2 (no-op if already v2). reset atomically writes the track's initial state (step=1, all counters zeroed) under the default track (or the named track if the user invoked /vision <track>), preserving any other tracks already present. Both commands emit a confirmation line on stdout.
Write the <slug>.spec.md.eval.json sidecar. The sidecar filename is always the spec filename with .eval.json appended. The evaluator's result.sidecar_payload carries the policy hash, tiers run, dismissals, findings summary, and DeepSeek model version. Persist next to the locked spec:
SPEC_PATH="$(spectre spec_evaluator slug-to-path --slug "<slug>")"
python3 -c 'import json,sys; d=json.load(open("state/.eval-result.json"))["sidecar_payload"]; d["config_path"]=sys.argv[1]; json.dump(d,sys.stdout)' \
"$HOME/.spectre/reviewer.toml" \
| spectre eval_metadata write-sidecar --spec "$SPEC_PATH"
policy_hash, tiers_run, dismissals, findings_summary, evaluator_version, config_hash, and deepseek_model_version are all carried verbatim from the evaluator's sidecar_payload — there is no re-computation.
Clear the persisted bundle — lock is complete, the bundle is no longer needed:
spectre spec_evaluator clear-bundle --bundle state/.eval-bundle.json
Write the handoff envelope. The envelope is the Vision→Implement handoff artifact: it carries a SHA-256 integrity hash over the locked spec, sidecar, contract resolution, and indexed ADR paths. /implement verifies this hash at Tier 0 on startup — a mismatch halts execution before any spec content is read. Re-running /vision is the only legitimate way to regenerate the envelope.
spectre eval_metadata write-envelope \
--spec "$SPEC_PATH" \
--walk state/.walk.json \
--decisions-dir decisions
Stdout: OK eval.envelope_written path=<path> on success. If the envelope write fails (CLI exits non-zero), treat as a lock failure and halt.
Append CDLC ledger transition. Record a generate transition (lock event):
spectre cdlc_ledger append --kind generate \
--payload-kv "spec_slug=<slug>" \
--payload-kv "round_count=<walker round_count from state/.walk.json>" \
--payload-kv "tiers_run=<tiers_run from <slug>.spec.md.eval.json sidecar>"
The CLI writes to state/cdlc-ledger.json under the project cwd (override with --project). For typed/nested payloads, use --payload <json> instead of --payload-kv.
Print exactly:
VISION LOCKED: specs/<slug>.spec.md (step 1)
Architecture locked. Ready for /implement?
Do not auto-invoke /implement. The user types /implement when ready — this preserves the human's veto right between spec-lock and execution.