spec-ask · diff
git:20260904.30e2ded to git:20260904.f4c79c8
79 added, 61 removed. Audit A to A.
---
name: spec-ask
- description: Interrogates preference-driven decision points. Works through the [TBD]s in research.md via Codex's request_user_input tool when the session has it, else as batched plain-text questions (lettered options, one-line combined reply) after a one-line enablement tip; answered items are moved to ## Decided. Inside $spec-workflow it switches to auto triage (decide + mark, no questions). Can be triggered multiple times; new [TBD]s may surface during the process.
+ description: Interrogates preference-driven decision points. Derives questions from the candidate sets research minted into each [TBD] (options are citations, not inventions), asks via Codex's request_user_input tool when the session has it (plain-text batch fallback otherwise), and moves answered items to ## Decided. Inside $spec-workflow it switches to auto triage (decide + mark, no questions). Can be triggered multiple times; new [TBD]s may surface during the process.
---
<!-- GENERATED from core/commands/ask.md — edit the core file and run node tools/generate.mjs; hand edits will be overwritten -->
# $spec-ask
## Process
1. Read `spec/changes/<name>/research.md` and list every entry in `## Open [TBD]`
- 2. For each [TBD], determine its nature:
- - **Factual** (determinable by reading code / docs) → Claude decides, marks "decided from status quo: X", moves to Decided
- - **Preference-driven** (multiple valid options, depends on user trade-offs) → ask per the host protocol below (`request_user_input` tool when available, else plain text)
- - **Uncertain → treat as preference-driven** (never skip)
- 3. **How to ask preference-driven questions** (inherits global *Asking Style* + SKILL "Self-Contained Prompts"):
- - **Tool always, when available**: Codex gates its structured question tool behind an experimental feature flag — with `default_mode_request_user_input` enabled, sessions carry a `request_user_input` tool. When that tool is available in this session, EVERY preference-driven question goes through it — plain text is the no-tool fallback ONLY, never an escape from the tool's shape limits (single-select, max 3 options per call). Fit questions into the shape instead:
- - **Single-select with >3 candidates**: the 3 slots are the (Recommended) option + the strongest genuine alternative + the mandatory skip/minimal candidate (never trimmed). Remaining candidates are named in the question text, one line each, closed with "prefer one of these → reply with its name in text". A genuinely wide option space (>4 real candidates) → progressive narrowing across two tool calls (category first, then specifics within it).
- - **Multi-select**: decompose into per-candidate single-select calls through the tool — each candidate becomes its own self-contained include/skip question (`A. Include (Recommended when it is) / B. Skip`, each side stating its consequence). Echo the combined outcome once at the end, same contract as the plain-text echo rule.
- - When the tool is NOT available, say ONCE before the first question, then proceed in plain text:
- `Tip: 'codex features enable default_mode_request_user_input' turns on structured questions (experimental; takes effect on a new task). Asking in plain text this run.`
- - **Plain-text form** (used ONLY when the tool is unavailable): **batch all mutually independent questions into ONE message** (≤4, per the shared rules below); only a mutually dependent chain goes one question at a time. Fixed format, no deviation: questions are numbered `Q1 / Q2 / …`; **options are ALWAYS lettered `A. / B. / C.` — never numbered** (numbered options visually continue the question sequence, and the user reads five questions where there is one); recommended option first, marked "(Recommended)". The message closes with ONE reply-protocol line (verbatim):
- `Reply in one line — e.g. "1A 3C". Multi-select: "2: A C". None fits: "3: <your answer>". "all recommended" takes every recommendation; questions you omit resolve to their (Recommended) option.`
- - **Echo the resolution before writing back**: after the reply, list every question's outcome (`Q1 → A · Q2 → B (default) · …`) with defaulted ones explicitly marked — a mistaken default must be overturnable with one line, never silently absorbed into Decided.
- - **Every question must be self-contained (top priority)**: ① one-sentence decision statement + ② why it must be settled now (what it affects / what breaks if left open) + ③ for each option, "choosing this leads to what — specific scenario / consequence". **The user must be able to answer without asking a follow-up**.
- - **Classify the select type first**: pick-ONE (mutually exclusive paths — architecture A/B/C) → single-select; pick-SEVERAL (independent inclusions — which edge cases are in scope, which pages to touch, which concerns to adopt) → **multi-select, never forced into single-select** (forcing drops legitimate picks; on this host it runs as per-candidate include/skip tool calls, or the plain-text combined reply). 2–4 options; recommended option goes first, labeled "(Recommended)" with a one-line reason
- - **Options must differ in consequence, not wording** (anti-padding test): for every pair of options you must be able to state different outcomes — can't → they are the same option, merge them. Option count follows the real decision space, never padded toward 4: a dominated option nobody would rationally pick, or one restating the question stem ("yes, do it properly"), is noise. Two honest options beat four manufactured ones.
- - **Always include a "skip / minimal" option** (SKILL Claim Self-Review fourth question applied at the ask stage): preference-driven questions MUST include a "don't do it yet (cost = X)" or "minimal viable (cost = Y)" candidate, to force the question "is this even necessary?" — unless there is genuinely no "skip" path for this decision (the cache example below is the canonical model). Not every option should be a variant of "which way to do it".
- - More than 4 options → use progressive narrowing: ask the broad category first, then narrow
- - Mutually dependent decision points: one question at a time, expand the next question based on the answer (don't pre-enumerate); mutually independent points: batch up to 4 questions at once
- - **Round caps are not run caps**: the per-round limits (max 4 questions; host option caps) never justify dropping a decision point — run as many rounds as it takes, until `## Open` is empty or the user says stop. Ending the run with items still Open is legal ONLY with an explicit declaration (`N decisions remain open — $spec-propose stays blocked until they're resolved`); a silently unasked item is exactly the miss the check-tbd gate bounces later
-
- **Anti-example → Correct example** (same [TBD]: cache library selection):
- - ❌ Empty question (will get follow-ups like "what does it affect / why recommend"): `Which cache? Redis(recommended) / Caffeine / Neither`
- - ✅ Self-contained:
- > Cache library selection — affects whether multiple instances read stale data:
- >
- > A. Redis (Recommended): shared across instances, strongly consistent across machines; cost = extra dependency + network round-trip per read
- > B. Caffeine: fastest in-process, zero dependencies; cost = each instance holds its own copy, **data inconsistent across machines**
- > C. Skip for now: simplest; cost = high-frequency reads hit the DB directly, revisit when load demands it
-
- (the reply-protocol line closes the whole batched message once — never repeated per question)
+ 2. **Classify — shared step 0 for BOTH modes** (interactive and workflow auto-triage run the same classification; only the delivery differs):
+ - **Factual** (determinable by reading code / docs / spec/knowledge.md) → decide from evidence, mark "decided from status quo: X", move to Decided
+ - **Preference-driven** → tag it **reversible** or **irreversible** (irreversible = any of: data migration / schema change / public API surface / new dependency / destructive operation / user-visible product semantics; **when unsure, it is irreversible**)
+ - Unsure whether factual or preference → **treat as preference-driven** (never skip)
+ - After classification the modes fork at delivery only: interactive asks (§ How to ask); workflow auto-triage marks (§ Auto triage)
+ 3. Work the preference-driven set through the four-stage pipeline below
4. Before the first question, one-line declaration:
```
- Found N open decisions — working through them one by one. This may not be exhaustive; flag anything I miss.
+ Found N open decisions — working through them. This may not be exhaustive; flag anything I miss.
```
- 5. **After the user answers → write back to research.md**:
- - Remove the entry from `## Open [TBD]`
- - Append to `## Decided`:
- ```
- [DEC-N] <decision> | source [TBD-N] | rationale: <distilled from user's answer>
- ```
- 6. **New [TBD]s surface during the process** → proactively append them to `## Open`, announce "found M new TBDs", continue asking
+ Legacy TBDs minted without candidate sets exist → add: `M legacy TBDs without candidate sets — asking those in legacy mode`
+ 5. **New [TBD]s surfacing during the process** → append them to `## Open` (with candidate sets), announce "found M new TBDs", continue
- ## Auto triage (workflow-invoked mode)
+ ## How to ask (four-stage pipeline)
- When `$spec-workflow` orchestrates this stage, the flow is two-touchpoint by design (HARD GATE + acceptance): do **NOT** ask the user question by question. Triage every `[TBD]` instead — **all of them end up in `## Decided`** (the pre-propose hook still requires `## Open` to be empty; leaving escalated items Open would deadlock the flow):
+ ### 1 · Derive the question set (from research, never from imagination)
- 1. **Factual** (determinable from code / docs / spec/knowledge.md) → decide from evidence, exactly as in the interactive flow: `[DEC-N] <decision> | source [TBD-N] | decided from status quo: <evidence>`
- 2. **Preference-driven, reversible and cheap** → decide the recommended option yourself: `[DEC-N] <decision> | source [TBD-N] | auto | <one-line rationale> | reversibility: <how to undo>`
- 3. **Preference-driven, irreversible or product-semantics** — any of: data migration / schema change / public API surface / new dependency / destructive operation / user-visible product semantics; **when unsure, it is in this class** → still decide the recommended option, but mark it: `[DEC-N] <decision> | source [TBD-N] | escalated | <rationale> | if wrong: <blast radius>`
+ - **Options are citations, not inventions**: each question's options come from the TBD's minted candidate set, each candidate anchored to a research.md line (Practice / Constraint / status-quo evidence). A candidate you want to offer but can't point back to a research line → **add it to research first**, then ask. The skip/minimal candidate is the one exemption (its evidence is the status quo itself). Invented-on-the-spot options are natural wording-variants of each other — the mechanism behind duplicated, nutrition-free choices.
+ - **Legacy fallback**: a TBD minted without a candidate set (older change, other tiers) → craft options the best you can and say so via the declaration line in Process step 4 — never silently mix modes.
+ - **Select type** comes from the TBD's mark (or classify now): pick-ONE (mutually exclusive paths) vs pick-SEVERAL (independent inclusions; "which of these…" phrasing is the signal).
+ - **Sibling aggregation**: N sibling TBDs that are each an include/skip decision over the same theme are ONE pick-several question (each sibling one option) — never a serial run of single-selects. Decomposition back into per-candidate calls happens ONLY in the Codex delivery stage; it never leaks back into how the question is constructed.
+ - **Question-level culling**: after the consequence-merge test (stage 2), a question left with fewer than 2 genuinely different options is not a question — decide it on the spot, record it in Decided, and surface it on the gate's `Decided without asking` line (one line to overturn). Evidence has made it factual.
+ - **Ordering & batching**: irreversible-tagged questions enter the queue first (the round budget must not be spent on trivia while irreversible calls wait). Mutually independent questions batch (≤4 per round); a mutually dependent chain goes one at a time, next question shaped by the answer.
- Every `auto` / `escalated` decision runs the **four-question filter** first (SKILL "Claim Self-Review"): why needed / when favorable / cost / can it be cut — and **"don't do it / minimal" is ALWAYS among the candidates**. A measure that survives only because "it might help" does not survive.
+ ### 2 · Craft each question
- **Surfacing contract** (what makes self-deciding safe — the decisions are never silent):
- - `auto` decisions → listed at the HARD GATE under `Decided without asking` (one line each + reversibility)
- - `escalated` decisions → the HARD GATE's `Escalated decisions` section, **pinned at the top of the gate block**; they stand by default — silence + `$spec-apply` = consent; the user overturns any with one line of evaluation (applied via `$spec-revise`), and `$spec-apply` echoes them once more at its first line
- - Standalone user-invoked `$spec-ask` keeps the interactive flow above — this section changes nothing about it
+ - **Self-contained (top priority)**: ① the decision in one sentence + ② why it must be settled now (what it affects / what breaks if left open) + ③ per option, what choosing it leads to — concrete scenario / consequence. **Test: the user can answer without asking a follow-up.**
+ - **Options differ in consequence, not wording**: for every pair of options you must be able to state different outcomes — can't → same option, merge. Option count follows the real decision space (2–4), never padded: a dominated option nobody would pick, or a restatement of the question stem, is noise. Two honest options beat four manufactured ones.
+ - **The skip/minimal candidate is mandatory** (Claim Self-Review question ④ applied at the ask stage): "don't do it yet (cost = X)" or "minimal viable (cost = Y)" — unless the decision genuinely has no skip path. It is never trimmed to fit any host limit.
+ - **Recommended option first**, labeled "(recommended)" with a one-line reason. More than 4 genuine candidates → progressive narrowing (category first, then specifics).
+ **Anti-example → Correct example** (same [TBD]: cache library selection):
+ - ❌ Empty question (invites follow-ups): `Which cache? Redis(recommended) / Caffeine / Neither`
+ - ✅ Self-contained, candidates cited from research:
+ > Cache library selection — affects whether multiple instances read stale data:
+ > • Redis (recommended, Practices L12): shared across instances, strongly consistent; cost = extra dependency + network round-trip per read
+ > • Caffeine (Practices L14): fastest in-process, zero dependencies; cost = per-instance copies, **data inconsistent across machines**
+ > • Skip for now: simplest; cost = high-frequency reads hit the DB directly, revisit when load demands it
+
+ ### 3 · Deliver per host
+
+ - **Tool always, when available**: with the experimental `default_mode_request_user_input` flag enabled, sessions carry a `request_user_input` tool — EVERY question goes through it; plain text is the no-tool fallback ONLY, never an escape from the tool's shape limits (single-select, max 3 options per call). Fit questions into the shape:
+ - **Single-select with >3 candidates**: the 3 slots are the (recommended) option + the strongest genuine alternative + the skip/minimal candidate (never trimmed). Remaining candidates are named in the question text, one line each, closed with "prefer one of these → reply with its name in text". More than 4 genuine candidates → progressive narrowing across two tool calls.
+ - **Pick-several**: decompose into per-candidate include/skip single-select calls — **a delivery-layer adaptation only** (the question was constructed as ONE pick-several in stage 1); each call self-contained with both sides' consequences; echo the combined outcome once at the end.
+ - **Tool unavailable** → say ONCE before the first question, then use the plain-text form:
+ `Tip: 'codex features enable default_mode_request_user_input' turns on structured questions (experimental; takes effect on a new task). Asking in plain text this run.`
+ - **Plain-text form**: batch all independent questions into ONE message; questions numbered `Q1 / Q2 / …`; **options ALWAYS lettered `A. / B. / C.` — never numbered** (numbered options visually continue the question sequence). The message closes with ONE reply-protocol line (verbatim):
+ `Reply in one line — e.g. "1A 3C". Multi-select: "2: A C". None fits: "3: <your answer>". "all recommended" takes every recommendation; questions you omit resolve to their (recommended) option.`
+ - **Round caps are not run caps** (both hosts): rounds repeat until `## Open` is empty or the user says stop — the per-round limits never justify dropping a decision point.
+
+ ### 4 · Book-keep
+
+ - **Echo before write-back whenever any outcome was not an explicit pick** (omitted questions resolved to recommended, culled questions self-decided): list `Q1 → A · Q2 → B (default) · Q3 → self-decided (culled)` — a wrong default must be one line to overturn, never silently absorbed.
+ - **Write back to research.md**: remove the entry from `## Open`, append to `## Decided`:
+ ```
+ [DEC-N] <decision> | source [TBD-N] | rationale: <distilled from user's answer>
+ ```
+ - **Every round ends with the tail line**: `Open: N asked / M answered / K pending` — the pending set stays visible on the books, never in memory.
+ - **Ending the run with pending items** is legal ONLY with the explicit declaration: `K decisions remain open — $spec-propose stays blocked until they're resolved.`
+
+ ## Auto triage (workflow-invoked delivery fork)
+
+ When `$spec-workflow` orchestrates this stage, the flow is two-touchpoint by design (HARD GATE + acceptance): classification (step 0) has already run — instead of asking, **every preference-driven TBD is decided and marked** (the pre-propose hook still requires `## Open` to be empty):
+
+ 1. **Reversible** → decide the recommended option yourself: `[DEC-N] <decision> | source [TBD-N] | auto | <one-line rationale> | reversibility: <how to undo>`
+ 2. **Irreversible** → still decide the recommended option, but mark it: `[DEC-N] <decision> | source [TBD-N] | escalated | <rationale> | if wrong: <blast radius>`
+
+ Every `auto` / `escalated` decision runs the four-question filter first (SKILL "Claim Self-Review") — and "don't do it / minimal" is ALWAYS among the candidates; a measure surviving only because "it might help" does not survive.
+
+ **Surfacing contract** (what makes self-deciding safe — decisions are never silent):
+ - `auto` decisions → the HARD GATE's `Decided without asking` line (one line each + reversibility)
+ - `escalated` decisions → the HARD GATE's `Escalated decisions` section, **pinned on top**; they stand by default — silence + `$spec-apply` = consent; overturn with one line (applied via `$spec-revise`); `$spec-apply` echoes them once more at its first line
+ - Standalone user-invoked `$spec-ask` keeps the interactive pipeline above — this section changes nothing about it
+
## Stopping conditions
| Situation | Action |
|---|---|
| Open [TBD] cleared | Stop; prompt "ready for $spec-propose" |
- | User says "stop asking" / "that's enough" | Stop; leave remaining items Open (the pre-$spec-propose hook will block execution) |
- | Interrogation diverges and can't be resolved | Stop; report "collected N decisions, M items remain — suggest revisiting later" |
+ | User says "stop asking" / "that's enough" | Stop; leave remaining items Open with the pending declaration (the pre-$spec-propose hook will block execution) |
+ | Interrogation diverges and can't be resolved | Stop; report "collected N decisions, K items remain — suggest revisiting later" |
- ## Anti-patterns
+ ## Anti-patterns (each violates a named pipeline rule)
- - ❌ Silently deciding a [TBD] based on assumed knowledge (interactive mode: MUST ask the user; auto-triage mode: deciding is legal but MUST carry the `auto` / `escalated` mark — "silent" means unmarked, and an unmarked self-decision is a violation in both modes)
- - ❌ Treating a preference-driven point as factual and skipping it
- - ❌ Throwing 5+ questions at the user at once (violates the 2–4 options + max-4-questions-per-round rule)
- - ❌ Producing a "decision tree" artifact (creates a false sense of completeness)
- - ❌ Empty questions: listing "A / B / C" without saying why it matters and what each option leads to (forcing the user to ask back = the number-one failure mode)
- - ❌ Duplicate / filler options: two options whose stated consequences read the same, or an option added only to fill the 2–4 range (dominated, or a restatement of the question stem)
- - ❌ Forcing a pick-several decision into single-select ("which edge cases / which pages / which suggestions" is multi-select by nature — single-select there drops legitimate picks)
- - ❌ Every option being a variant of "which way to do it" — never offering a "skip / remove" escape hatch (strips away the fourth self-check question; user cannot challenge "is this even necessary?")
+ - ❌ Unmarked self-decision (interactive: must ask; triage: legal only WITH the `auto`/`escalated` mark — "silent" means unmarked)
+ - ❌ Preference-driven point skipped as "factual" (Classify)
+ - ❌ Options invented on the spot with no research anchor (Derive: options are citations)
+ - ❌ A run of include/skip siblings asked as serial single-selects (Derive: sibling aggregation)
+ - ❌ Duplicate / filler / stem-restating options (Craft: consequence-differ, no padding)
+ - ❌ Empty questions — bare "A / B / C" without stakes and consequences (Craft: self-contained)
+ - ❌ No skip/minimal candidate, or trimming it to fit a host limit (Craft)
+ - ❌ >4 questions in one round (Derive: batching)
+ - ❌ Ending the run with silently unasked items (Book-keep: pending declaration)
+ - ❌ Producing a "decision tree" artifact (false sense of completeness)
## What this command does NOT do
- Does not write proposal.md (that is `$spec-propose`'s job)
- Does not touch drafts under `research/`; only works research.md, moving Open items into Decided