Files
msd-core/gsd-core/workflows/spec-phase.md
Rezolv 395fb519e7 feat(spec-phase): prohibition probe — surface "must-NOT" constraints (#644) (#1149)
Adds the spec-time prohibition probe (spec-phase Step 5.6) — the second adapter of the probe-core resolution model. Surfaces unwritten must-NOT constraints as negative SPEC acceptance criteria with test/judgment verification tiers; fail-closed at verify time. Per ADR-550. Closes #644.
2026-06-14 21:29:11 -04:00

27 KiB
Raw Blame History

Clarify WHAT a phase delivers through a Socratic interview loop with quantitative ambiguity scoring. Produces a SPEC.md with falsifiable requirements that discuss-phase treats as locked decisions.

This workflow handles "what" and "why" — discuss-phase handles "how".

<ambiguity_model> Score each dimension 0.0 (completely unclear) to 1.0 (crystal clear):

Dimension Weight Minimum What it measures
Goal Clarity 35% 0.75 Is the outcome specific and measurable?
Boundary Clarity 25% 0.70 What's in scope vs out of scope?
Constraint Clarity 20% 0.65 Performance, compatibility, data requirements?
Acceptance Criteria 20% 0.70 How do we know it's done?

Ambiguity score = 1.0 − (0.35×goal + 0.25×boundary + 0.20×constraint + 0.20×acceptance)

Gate: ambiguity ≤ 0.20 AND all dimensions ≥ their minimums → ready to write SPEC.md.

A score of 0.20 means 80% weighted clarity — enough precision that the planner won't silently make wrong assumptions. </ambiguity_model>

<interview_perspectives> Rotate through these perspectives — each naturally surfaces different blindspots:

Researcher (rounds 1–2): Ground the discussion in current reality.

  • "What exists in the codebase today related to this phase?"
  • "What's the delta between today and the target state?"
  • "What triggers this work — what's broken or missing?"

Simplifier (round 2): Surface minimum viable scope.

  • "What's the simplest version that solves the core problem?"
  • "If you had to cut 50%, what's the irreducible core?"
  • "What would make this phase a success even without the nice-to-haves?"

Boundary Keeper (round 3): Lock the perimeter.

  • "What explicitly will NOT be done in this phase?"
  • "What adjacent problems is it tempting to solve but shouldn't?"
  • "What does 'done' look like — what's the final deliverable?"

Failure Analyst (round 4): Find the edge cases that invalidate requirements.

  • "What's the worst thing that could go wrong if we get the requirements wrong?"
  • "What does a broken version of this look like?"
  • "What would cause a verifier to reject the output?"

Seed Closer (rounds 5–6): Lock remaining undecided territory.

  • "We have [dimension] at [score] — what would make it completely clear?"
  • "The remaining ambiguity is in [area] — can we make a decision now?"
  • "Is there anything you'd regret not specifying before planning starts?" </interview_perspectives>

Step 1: Initialize

_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
INIT=$(gsd_run init phase-op "${PHASE}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi

Parse JSON for: phase_found, phase_dir, phase_number, phase_name, phase_slug, padded_phase, state_path, requirements_path, roadmap_path, planning_path, response_language, commit_docs.

If response_language is set: All user-facing text in this workflow MUST be in {response_language}. Technical terms, code, and file paths stay in English.

If phase_found is false:

Phase [X] not found in roadmap.
Use /gsd:progress to see available phases.

Exit.

Check for existing SPEC.md:

ls ${phase_dir}/*-SPEC.md 2>/dev/null | grep -v AI-SPEC | head -1 || true

If SPEC.md already exists:

If --auto: Auto-select "Update it". Log: [auto] SPEC.md exists — updating.

Otherwise: Use AskUserQuestion:

  • header: "Spec"
  • question: "Phase [X] already has a SPEC.md. What do you want to do?"
  • options:
    • "Update it" — Revise and re-score
    • "View it" — Show current spec
    • "Skip" — Exit (use existing spec as-is)

If "View": Display SPEC.md, then offer Update/Skip. If "Skip": Exit with message: "Existing SPEC.md unchanged. Run /gsd:discuss-phase [X] to continue." If "Update": Load existing SPEC.md, continue to Step 3.

Step 2: Scout Codebase

Read these files before any questions:

  • {requirements_path} — Project requirements
  • {state_path} — Decisions already made, current phase, blockers
  • ROADMAP.md phase entry — Phase description, goals, canonical refs

Grep the codebase for code/files relevant to this phase goal. Look for:

  • Existing implementations of similar functionality
  • Integration points where new code will connect
  • Test coverage gaps relevant to the phase
  • Prior phase artifacts (SUMMARY.md, VERIFICATION.md) that inform current state

Synthesize current state — the grounded baseline for the interview:

  • What exists today related to this phase
  • The gap between current state and the phase goal
  • The primary deliverable: what file/behavior/capability does NOT exist yet?

Confirm your current state synthesis internally. Do not present it to the user yet — you'll use it to ask precise, grounded questions.

Step 3: First Ambiguity Assessment

Before questioning begins, score the phase's current ambiguity based only on what ROADMAP.md and REQUIREMENTS.md say:

Goal Clarity:       [score 0.0–1.0]
Boundary Clarity:   [score 0.0–1.0]
Constraint Clarity: [score 0.0–1.0]
Acceptance Criteria:[score 0.0–1.0]

Ambiguity: [score] ([calculate])

If --auto and initial ambiguity already ≤ 0.20 with all minimums met: Skip interview — derive SPEC.md directly from roadmap + requirements. Log: [auto] Phase requirements are already sufficiently clear — generating SPEC.md from existing context. Jump to Step 6.

Otherwise: Continue to Step 4.

Step 4: Socratic Interview Loop

Max 6 rounds. Each round: 2–3 questions max. End round after user responds.

Round selection by perspective:

  • Round 1: Researcher
  • Round 2: Researcher + Simplifier
  • Round 3: Boundary Keeper
  • Round 4: Failure Analyst
  • Rounds 5–6: Seed Closer (focus on lowest-scoring dimensions)

After each round:

  1. Update all 4 dimension scores from the user's answers
  2. Calculate new ambiguity score
  3. Display the updated scoring:
After round [N]:
  Goal Clarity:       [score] (min 0.75) [✓ or ↑ needed]
  Boundary Clarity:   [score] (min 0.70) [✓ or ↑ needed]
  Constraint Clarity: [score] (min 0.65) [✓ or ↑ needed]
  Acceptance Criteria:[score] (min 0.70) [✓ or ↑ needed]
  Ambiguity: [score] (gate: ≤ 0.20)

Gate check after each round:

If gate passes (ambiguity ≤ 0.20 AND all minimums met):

If --auto: Jump to Step 6.

Otherwise: AskUserQuestion:

  • header: "Spec Gate Passed"
  • question: "Ambiguity is [score] — requirements are clear enough to write SPEC.md. Proceed?"
  • options:
    • "Yes — write SPEC.md" → Jump to Step 6
    • "One more round" → Continue interview
    • "Done talking — write it" → Jump to Step 6

If max rounds reached (6) and gate not passed:

If --auto: Write SPEC.md anyway — flag unresolved dimensions. Log: [auto] Max rounds reached. Writing SPEC.md with [N] dimensions below minimum. Planner will need to treat these as assumptions.

Otherwise: AskUserQuestion:

  • header: "Max Rounds"
  • question: "After 6 rounds, ambiguity is [score]. [List dimensions still below minimum.] What would you like to do?"
  • options:
    • "Write SPEC.md anyway — flag gaps" → Write SPEC.md, mark unresolved dimensions in Ambiguity Report
    • "Keep talking" → Continue (no round limit from here)
    • "Abandon" → Exit without writing

If --auto mode throughout: Replace all AskUserQuestion calls above with Claude's recommended choice. Log decisions inline. Apply the same logic as --auto in discuss-phase.

Text mode (workflow.text_mode: true or --text flag): Use plain-text numbered lists instead of AskUserQuestion TUI menus.

Step 5: (covered inline — ambiguity scoring is per-round)

Step 5.5: Edge-Completeness Probe

Run AFTER the ambiguity gate passes (you probe edges of clear requirements, not vague ones). Reference: @~/.claude/gsd-core/references/edge-probe.md.

Runtime coverage compute — resolve and invoke edge-probe.cjs:

# Resolve the compiled edge-probe.cjs against the GSD install dir via RUNTIME_DIR (#448)
# — NOT the consuming project's git root — falling back to git toplevel / $HOME/.claude.
# Mirrors the ui-safety-gate.cjs resolution idiom at autonomous.md:290 / plan-phase.md:631.
_GSD_RT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
EDGE_PROBE_JS=$(for _c in \
  "$_GSD_RT/gsd-core/bin/lib/edge-probe.cjs" \
  "$_GSD_RT/bin/lib/edge-probe.cjs" \
  "$_GSD_RT/.claude/bin/lib/edge-probe.cjs" \
  "$HOME/.claude/gsd-core/bin/lib/edge-probe.cjs" \
  "$HOME/.claude/bin/lib/edge-probe.cjs"; do
  [ -f "$_c" ] && { echo "$_c"; break; }
done)

# Graceful degradation — never silent skip (RR-04). Build ONLY when $_GSD_RT is a verified
# GSD source checkout (has tsconfig.build.json + src/edge-probe.cts), and pin npm to it with
# --prefix so we never trigger the CONSUMING project's own build:lib (its cwd package scripts:
# codegen/migrations/writes) during a spec workflow. Real installs ship the compiled .cjs via
# prepublishOnly, so this build path only matters in a GSD dev checkout (review High).
if [ -z "$EDGE_PROBE_JS" ]; then
  if [ -f "$_GSD_RT/tsconfig.build.json" ] && [ -f "$_GSD_RT/src/edge-probe.cts" ]; then
    npm --prefix "$_GSD_RT" run build:lib 2>/dev/null || true
    EDGE_PROBE_JS=$(for _c in \
      "$_GSD_RT/gsd-core/bin/lib/edge-probe.cjs" \
      "$_GSD_RT/bin/lib/edge-probe.cjs" \
      "$_GSD_RT/.claude/bin/lib/edge-probe.cjs" \
      "$HOME/.claude/gsd-core/bin/lib/edge-probe.cjs" \
      "$HOME/.claude/bin/lib/edge-probe.cjs"; do
      [ -f "$_c" ] && { echo "$_c"; break; }
    done)
  fi
  if [ -z "$EDGE_PROBE_JS" ]; then
    echo "ERROR: edge-probe.cjs not found — reinstall GSD or run \`npm run build:lib\` in your GSD checkout." >&2
    exit 1
  fi
fi

# Write the Requirements gathered in THIS spec session to a temp JSON, then invoke the
# canonical coverage compute. Populate the heredoc from the SPEC's Requirements — one object
# per requirement: {"id","text","shapes"?}. This is the load-bearing step: an empty file makes
# the probe a no-op, so the guard below fails loud rather than silently skipping (RR-04).
REQS_JSON=$(mktemp "${TMPDIR:-/tmp}/edge-probe-reqs-XXXXXX.json")
cat > "$REQS_JSON" <<'JSON'
[
  { "id": "R1", "text": "<replace: requirement text from the SPEC>" }
]
JSON
# Guard — never invoke on an empty/invalid array, OR one still holding the heredoc
# `<replace: …>` placeholder (a forgotten substitution would otherwise yield a
# meaningful-looking but bogus coverage report). Fail loud, not silent no-op.
if ! node -e 'const a=require(process.argv[1]);if(!Array.isArray(a)||a.length===0)process.exit(1);if(a.some(r=>typeof r.text!=="string"||!r.text.trim()||r.text.includes("<replace:")))process.exit(1)' "$REQS_JSON" 2>/dev/null; then
  echo "ERROR: edge-probe requirements JSON is empty/invalid or still holds the <replace: …> placeholder — populate \$REQS_JSON from the SPEC Requirements before Step 5.5 runs." >&2
  exit 1
fi
# Invoke the compiled engine and CAPTURE its report — it computes which categories apply per
# requirement. The covered/backstop/dismissed/unresolved rows in $COVERAGE drive the
# resolution loop below (canonical taxonomy compute, NOT LLM re-derivation from prose).
# The engine FAILS CLOSED (exit 2) on an invalid authored shape or bad input — so the capture
# MUST be exit-checked. A bare `COVERAGE=$(node …)` swallows that exit code, leaves $COVERAGE
# empty, and lets the workflow fall through to prose re-derivation: fail-OPEN at the boundary
# the engine validation exists to protect. Make the run fatal, then validate the captured
# report is well-formed JSON before the resolution loop consumes it.
if ! COVERAGE=$(node "$EDGE_PROBE_JS" "$REQS_JSON"); then
  rm -f "$REQS_JSON"
  echo "ERROR: edge-probe engine failed (invalid shapes or bad input) — fix the requirement(s) and re-run; never proceed with empty coverage." >&2
  exit 1
fi
rm -f "$REQS_JSON"
# Exit-0-but-garbage guard: the report must parse as JSON with the expected { items[], coverage{} } shape.
if ! printf '%s' "$COVERAGE" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let r;try{r=JSON.parse(s)}catch{process.exit(1)}if(!r||!Array.isArray(r.items)||typeof r.coverage!=="object"||r.coverage===null)process.exit(1)})'; then
  echo "ERROR: edge-probe produced an unparseable or malformed coverage report — refusing to proceed with the resolution loop." >&2
  exit 1
fi
# Zero-applicable guard: a report where the engine proposed NO applicable edge across ANY
# requirement is far more likely a shape-classification miss (or malformed requirements) than
# a genuinely edge-free spec — the same fail-open shape as an invalid shape yielding
# applicable:0. Surface it loudly; the author must explicitly confirm "no applicable edges"
# below rather than silently emitting a green empty ## Edge Coverage section.
APPLICABLE=$(printf '%s' "$COVERAGE" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let n=0;try{n=JSON.parse(s).coverage.applicable}catch{n=0}process.stdout.write(String(n))})')
if [ "$APPLICABLE" = "0" ]; then
  echo "WARNING: edge-probe proposed ZERO applicable edges across all requirements — likely a classification miss or malformed requirements, not a genuinely edge-free spec. Do NOT silently write an empty Edge Coverage section." >&2
fi

If $APPLICABLE is 0, do NOT proceed silently: ask the author to confirm via AskUserQuestion ("The edge probe found no applicable edges for any requirement — is this genuinely an edge-free spec, or should we revisit the requirement wording / authored shapes?"). Only write an empty ## Edge Coverage section after explicit confirmation.

For each Requirement gathered so far:

  1. Classify its shape and raise only applicable edge categories (relevance filter — see the taxonomy in the reference). Reuse any edges the Round-4 Failure Analyst already surfaced as pre-covered.
  2. For each raised category, propose a CONCRETE candidate edge (not "consider boundaries" — e.g. "R2 merges intervals; what about [[1,2],[2,3]] that only touch?").
  3. Resolve each with the user (AskUserQuestion; text mode → numbered list):
    • Specify it → write a new pass/fail line into Acceptance Criteria AND mark the edge covered.
    • Dismiss (reason) → mark dismissed with a required non-empty reason.
    • Backstop with a test → mark backstop; note "held-out edge test" for plan-phase.
    • Defer → leave unresolved.

Soft gate (after resolving):

  • All applicable edges resolved → proceed to Step 6.
  • Any unresolved → AskUserQuestion:
    • header: "Edge Coverage"
    • question: "[N] edge(s) are unresolved: [list]. What do you want to do?"
    • options: "Resolve now" (loop back) / "Write SPEC.md anyway — flag unresolved" / "Keep probing"
    • On "anyway": write SPEC.md with those rows marked ⚠ Edge unresolved — planner must treat as assumption.

--auto mode: auto-covered where a defensible acceptance criterion can be written; otherwise auto-backstop (never auto-dismiss — a wrong dismissal is the exact silent failure being eliminated). Log: [auto] edge coverage: C covered, B backstop, U unresolved.

Populate the ## Edge Coverage section of SPEC.md from the resolved edges.

Step 5.6: Prohibition-Completeness Probe (must-NOT)

Run AFTER Step 5.5 (you probe the must-NOT axis of clear requirements, over the same requirement list). Reference: @~/.claude/gsd-core/references/prohibition-probe.md — the portable two-stage protocol, the canon-referral rule, and the status×verification schema live there (size-cap discipline; keep this step lean).

D1 — no compiled engine (ADR-550 D7b). Unlike Step 5.5, the prohibition probe has NO compiled recall engine and runs NO node invocation here. The recall stage is an LLM prose pass: the closed eight-category edge taxonomy a classifier can apply does not exist for the open values/safety/ethics must-NOT axis. Do NOT copy the Step 5.5 engine-resolution block. Only the schema/projection layer is real code; the recall is prose.

For each Requirement gathered so far, run the two-stage recall→precision pass:

  1. Stage 1 — Recall (adversarial prose probe). Ask the single adversarial question of the requirement: "What could this feature silently become that the author would NOT want, but the spec does not forbid?" Over-produce (~10 raw must-NOT candidates) — recall first.
  2. Stage 2 — Precision (one-pass classifier). Filter the raw list in a single pass: DROP routine-engineering items (normal correctness/hygiene — "must not mutate input", "must not throw on empty" — owned by the edge probe or code review); KEEP values / safety / ethics items (manipulative framing, protected-attribute proxies, raw PII in plaintext). This collapses ~10 → ~2–3 genuine prohibitions.
  3. Canon-referral (ADR-550 D6, PROB-13). A kept candidate that is canon security/compliance (OWASP / prototype-pollution / path-traversal / injection / GDPR / generic fairness) is NOT minted here — emit a one-line breadcrumb ("prototype-pollution is canon — owned by /gsd:secure-phase + eslint; not minted here") and DROP it. Minting canon items duplicates /gsd:secure-phase and drowns the bespoke signal.
  4. Resolve each surfaced (non-canon) prohibition (AskUserQuestion; text mode → numbered list):
    • Keep it → write a NEGATIVE acceptance criterion (a must-NOT line) into Acceptance Criteria AND mark the prohibition resolved with a verification tier: test (a mechanical negative test/lint/assertion exists) or judgment (real but not mechanically checkable — routes to judgment review).
    • Dismiss (reason) → mark dismissed with a REQUIRED non-empty reason (PROB-05). The reason string is the audit trail; silence is not a valid dismissal.
    • Defer → leave unresolved.

Soft gate (after resolving) — PROB-06:

  • All applicable prohibitions resolved → proceed to Step 6.
  • Any unresolved → AskUserQuestion:
    • header: "Prohibitions"
    • question: "[N] prohibition(s) are unresolved: [list]. What do you want to do?"
    • options: "Resolve now" (loop back) / "Write SPEC.md anyway — flag unresolved" / "Keep probing"
    • On "anyway": write SPEC.md with those rows marked ⚠ Prohibition unresolved — planner must treat as assumption. This is a soft gate (write-anyway-with-flags), never a silent skip — the soft gate IS the control.

--auto mode: auto-resolved where a defensible negative acceptance criterion can be written (test or judgment tier); otherwise leave unresolved. --auto NEVER auto-dismisses a prohibition — a wrong dismissal is the exact silent failure this probe eliminates (PROB-06, the load-bearing safety property). Log: [auto] prohibitions: R resolved, U unresolved.

Text mode (PROB-09): per Step 5's text-mode rule, replace the AskUserQuestion menus above with plain-text numbered lists — there is NO hard AskUserQuestion dependency, so the probe runs identically for non-Claude / text-mode hosts.

Populate the ## Prohibitions section of SPEC.md from the resolved prohibitions (each resolved/test row is a checkable negative acceptance criterion; resolved/judgment rows route to judgment review; ⚠ UNRESOLVED rows are flagged as assumptions).

Step 6: Generate SPEC.md

Use the SPEC.md template from @~/.claude/gsd-core/templates/spec.md.

  • Populate the Edge Coverage section from Step 5.5 (covered/dismissed/backstop/unresolved rows).
  • Populate the Prohibitions section from Step 5.6 (resolved/dismissed/unresolved rows with the test|judgment tier).

Requirements for every requirement entry:

  • One specific, testable statement
  • Current state (what exists now)
  • Target state (what it should become)
  • Acceptance criterion (how to verify it was met)

Vague requirements are rejected:

  • ✗ "The system should be fast"
  • ✗ "Improve user experience"
  • ✓ "API endpoint responds in < 200ms at p95 under 100 concurrent requests"
  • ✓ "CLI command exits with code 1 and prints to stderr on invalid input"

Count requirements. The display in discuss-phase reads: "Found SPEC.md — {N} requirements locked."

Boundaries must be explicit lists:

  • "In scope" — what this phase produces
  • "Out of scope" — what it explicitly does NOT do (with brief reasoning)

Acceptance criteria must be pass/fail checkboxes — no "should feel good" or "looks reasonable."

If any dimensions are below minimum, mark them in the Ambiguity Report with: ⚠ Below minimum — planner must treat as assumption.

Write to: {phase_dir}/{padded_phase}-SPEC.md

Step 7: Commit

git add "${phase_dir}/${padded_phase}-SPEC.md"
git commit -m "spec(phase-${phase_number}): add SPEC.md for ${phase_name} — ${requirement_count} requirements (#2213)"

If commit_docs is false: Skip commit. Note that SPEC.md was written but not committed.

Step 8: Wrap Up

Display:

SPEC.md written — {N} requirements locked.

  Phase {X}: {name}
  Ambiguity: {final_score} (gate: ≤ 0.20)

Next: /gsd:discuss-phase {X}
  discuss-phase will detect SPEC.md and focus on implementation decisions only.

<critical_rules>

  • Every requirement MUST have current state, target state, and acceptance criterion
  • Boundaries section is MANDATORY — cannot be empty
  • "In scope" and "Out of scope" must be explicit lists, not narrative prose
  • Acceptance criteria must be pass/fail — no subjective criteria
  • SPEC.md is NEVER written if the user selects "Abandon"
  • Do NOT ask about HOW to implement — that is discuss-phase territory
  • Scout the codebase BEFORE the first question — grounded questions only
  • Max 2–3 questions per round — do not frontload all questions at once
  • Step 5.5 edge probe runs after the ambiguity gate; dismissals require a reason; --auto never auto-dismisses
  • Step 5.6 prohibition probe runs after the edge probe; dismissals require a reason; --auto never auto-dismisses a prohibition </critical_rules>

<success_criteria>

  • Codebase scouted and current state understood before questioning
  • All 4 dimensions scored after every round
  • Gate passed OR user explicitly chose to write despite gaps
  • SPEC.md contains only falsifiable requirements
  • Boundaries are explicit (in scope / out of scope with reasoning)
  • Acceptance criteria are pass/fail checkboxes
  • SPEC.md committed atomically (when commit_docs is true)
  • User directed to /gsd:discuss-phase as next step
  • Edge-completeness probe run; Edge Coverage section populated; unresolved edges flagged as assumptions
  • Prohibition-completeness probe run; Prohibitions section populated; unresolved prohibitions flagged as assumptions </success_criteria>