fix(codex): use native interactive question controls

This commit is contained in:
Isaiah Ratahi 2026-07-15 13:10:55 +12:00
parent a3259400a3
commit 3bb5a88639
8 changed files with 302 additions and 139 deletions

View File

@ -30,6 +30,13 @@ const codex: HostConfig = {
{ from: '.claude/skills/review', to: '.agents/skills/gstack/review' },
{ from: '.claude/skills', to: '.agents/skills' },
],
toolRewrites: {
'Do NOT use AskUserQuestion for interactive prompts. Auto-choose the recommended option.':
'If request_user_input is unavailable, report BLOCKED and stop; never auto-choose an answer.',
'AskUserQuestion with 4 options': 'request_user_input using the sequential 2-3-option split rule',
AskUserQuestions: 'request_user_input calls',
AskUserQuestion: 'request_user_input',
},
suppressedResolvers: [
'DESIGN_OUTSIDE_VOICES', // design.ts:485 — Codex can't invoke itself

View File

@ -1,6 +1,94 @@
import type { TemplateContext } from '../types';
export function generateAskUserFormat(_ctx: TemplateContext): string {
function generateCodexRequestUserInputFormat(): string {
return `## request_user_input Format
### Availability (read first)
\`request_user_input\` is available in Codex Plan mode by default. Before each
decision, confirm that it appears in the current tool list.
If the tool is unavailable including in Default mode do NOT guess, silently
choose, auto-select the recommendation, or continue the workflow. Tell the user:
\`BLOCKED — this workflow needs interactive decisions. Switch this task to Plan
mode and rerun it.\` Then STOP. Do not enable the under-development
\`default_mode_request_user_input\` feature as a workaround.
### Tool-call shape
Send one decision per call and one item in the \`questions\` array. Use:
- \`header\`: at most 12 characters.
- \`id\`: stable snake_case.
- \`question\`: the complete decision brief below, including context, stakes,
recommendation, completeness, pros/cons, and the closing Net line.
- \`options\`: 2-3 explicit choices. Keep each label to 1-5 words, suffix the
recommended label with \`(Recommended)\`, and give each option one concise
sentence describing its concrete impact or tradeoff.
The detailed brief belongs in \`question\`; do not try to pack it into option
labels or omit it because the tool has structured options.
### Decision-brief format
\`\`\`
D<N> <one-line question title>
Project/branch/task: <1 short grounding sentence using _BRANCH>
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
Recommendation: <choice> because <one-line reason>
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage no completeness score)
Pros / cons:
A) <option label> (recommended)
<pro concrete, observable, 40 chars>
<second pro concrete, observable, 40 chars>
<con honest, 40 chars>
B) <option label>
<pro>
<second pro>
<con>
Net: <one-line synthesis of what you're actually trading off>
\`\`\`
D-numbering starts at \`D1\` for each skill invocation and increments at the
model level; it is not a runtime counter. ELI10 and Recommendation are always
present. Use completeness scores only when options differ in coverage; when
they differ in kind, use the kind-note. Preserve the \`(recommended)\` marker
even for a neutral taste call. Label effort on both scales when relevant, for
example \`human: ~2 days / Codex: ~15 min\`.
### Handling 4+ choices split, never drop
\`request_user_input\` allows at most 3 explicit options per question. With 4+
real choices, NEVER drop, merge, or silently defer one to fit:
- For mutually exclusive choices, paginate them. While choices remain, show at
most 2 real choices plus a concise \`More choices\` option. The final page may
show up to 3 real choices. Preserve every original choice and repeat an earlier
page if the user asks to go back.
- For independent scope items, ask one sequential 3-way question per item:
\`Include\`, \`Exclude\`, or \`Discuss\`.
Call the tool once, STOP, and wait for the answer before asking the next page or
item. Never pre-queue a split chain. Check dependencies before each next call and
finish with a final confirmation of the assembled choice set.
### Self-check before emitting
Before calling \`request_user_input\`, verify:
- [ ] The tool is available in the current mode; otherwise emit the Plan-mode block and STOP
- [ ] One question only, with a 12-character header and snake_case id
- [ ] 2-3 concise options; one is marked \`(Recommended)\`
- [ ] D<N>, ELI10, stakes, Recommendation, completeness/kind-note, pros/cons, and Net are present in \`question\`
- [ ] Every real choice is preserved; 4+ choices are paginated or split sequentially
- [ ] You will STOP after this call and wait for the answer
- [ ] Non-ASCII characters are written directly, never \\u-escaped
`;
}
export function generateAskUserFormat(ctx: TemplateContext): string {
if (ctx.host === 'codex') return generateCodexRequestUserInputFormat();
return `## AskUserQuestion Format
### Tool resolution (read first)

View File

@ -19,7 +19,25 @@ import type { TemplateContext } from '../types';
* gates. See ceo-plan 2026-04-24 "remove vestigial plan-mode handshake" for
* the full rationale.
*/
export function generatePlanModeInfo(_ctx: TemplateContext): string {
export function generatePlanModeInfo(ctx: TemplateContext): string {
if (ctx.host === 'codex') {
return `## Plan Mode Safe Operations
In Plan mode, allowed because they inform the plan: \`$B\`, \`$D\`, writes to
\`~/.gstack/\`, writes to the plan file, and opening generated artifacts.
## Skill Invocation During Plan Mode
If the user invokes a skill in Plan mode, treat the skill file as executable
instructions and follow it from Step 0. A \`request_user_input\` call satisfies
the end-of-turn requirement: at every STOP point, call it once, stop immediately,
and wait for the answer. Do not continue the workflow or complete the plan in the
same turn. If the tool is unavailable, report the Plan-mode block from
\`request_user_input Format\` and STOP; never infer an answer. Commands marked
\"PLAN MODE EXCEPTION — ALWAYS RUN\" still execute. Complete or leave Plan mode
only after the skill workflow finishes, or when the user cancels it.`;
}
return `## Plan Mode Safe Operations
In plan mode, allowed because they inform the plan: \`$B\`, \`$D\`, \`codex exec\`/\`codex review\`, writes to \`~/.gstack/\`, writes to the plan file, and \`open\` for generated artifacts.

View File

@ -39,7 +39,7 @@ echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$($GSTACK_BIN/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
# Conductor host: AskUserQuestion is unreliable here (native disabled, MCP
# Conductor host: request_user_input is unreliable here (native disabled, MCP
# variant flaky), so skills render decisions as prose instead of calling the
# tool. Gated on !headless so an eval/CI run INSIDE Conductor (GSTACK_HEADLESS)
# still BLOCKs rather than rendering prose to nobody.
@ -132,22 +132,30 @@ echo "GSTACK_PLAN_MODE: $GSTACK_PLAN_MODE"
## Plan Mode Safe Operations
In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts.
In Plan mode, allowed because they inform the plan: `$B`, `$D`, writes to
`~/.gstack/`, writes to the plan file, and opening generated artifacts.
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in Plan mode, treat the skill file as executable
instructions and follow it from Step 0. A `request_user_input` call satisfies
the end-of-turn requirement: at every STOP point, call it once, stop immediately,
and wait for the answer. Do not continue the workflow or complete the plan in the
same turn. If the tool is unavailable, report the Plan-mode block from
`request_user_input Format` and STOP; never infer an answer. Commands marked
"PLAN MODE EXCEPTION — ALWAYS RUN" still execute. Complete or leave Plan mode
only after the skill workflow finishes, or when the user cancels it.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
If `SKILL_PREFIX` is `"true"`, suggest/invoke `/gstack-*` names. Disk paths stay `$GSTACK_ROOT/[skill-name]/SKILL.md`.
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `$GSTACK_ROOT/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined).
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `$GSTACK_ROOT/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise request_user_input using the sequential 2-3-option split rule, write snooze state if declined).
If output shows `JUST_UPGRADED <from> <to>`: print "Running gstack v{to} (just updated!)". If `SPAWNED_SESSION` is true, skip feature discovery.
Feature discovery, max one prompt per session:
- Missing `$GSTACK_ROOT/.feature-prompted-continuous-checkpoint`: AskUserQuestion for Continuous checkpoint auto-commits. If accepted, run `$GSTACK_BIN/gstack-config set checkpoint_mode continuous`. Always touch marker.
- Missing `$GSTACK_ROOT/.feature-prompted-continuous-checkpoint`: request_user_input for Continuous checkpoint auto-commits. If accepted, run `$GSTACK_BIN/gstack-config set checkpoint_mode continuous`. Always touch marker.
- Missing `$GSTACK_ROOT/.feature-prompted-model-overlay`: inform "Model overlays are active. MODEL_OVERLAY shows the patch." Always touch marker.
After upgrade prompts, continue workflow.
@ -180,7 +188,7 @@ touch ~/.gstack/.completeness-intro-seen
Only run `open` if yes. Always run `touch`.
If `TEL_PROMPTED` is `no` AND `LAKE_INTRO` is `yes`: ask telemetry once via AskUserQuestion:
If `TEL_PROMPTED` is `no` AND `LAKE_INTRO` is `yes`: ask telemetry once via request_user_input:
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code or file paths. Your repo name is recorded locally only and stripped before any upload.
@ -247,7 +255,7 @@ Skip this section if `ACTIVATED` and `FIRST_LOOP_SHOWN` are both `yes`.
If `HAS_ROUTING` is `no` AND `ROUTING_DECLINED` is `false` AND `PROACTIVE_PROMPTED` is `yes`:
Check if a CLAUDE.md file exists in the project root. If it does not exist, create it.
Use AskUserQuestion:
Use request_user_input:
> gstack works best when your project's CLAUDE.md includes skill routing rules.
@ -285,7 +293,7 @@ If B: run `$GSTACK_BIN/gstack-config set routing_declined true` and say they can
This only happens once per project. Skip if `HAS_ROUTING` is `yes` or `ROUTING_DECLINED` is `true`.
If `VENDORED_GSTACK` is `yes`, warn once via AskUserQuestion unless `~/.gstack/.vendoring-warned-$SLUG` exists:
If `VENDORED_GSTACK` is `yes`, warn once via request_user_input unless `~/.gstack/.vendoring-warned-$SLUG` exists:
> This project has gstack vendored in `.agents/skills/gstack/`. Vendoring is deprecated.
> Migrate to team mode?
@ -313,50 +321,40 @@ If marker exists, skip.
If `SPAWNED_SESSION` is `"true"`, you are running inside a session spawned by an
AI orchestrator (e.g., OpenClaw). In spawned sessions:
- Do NOT use AskUserQuestion for interactive prompts. Auto-choose the recommended option.
- If request_user_input is unavailable, report BLOCKED and stop; never auto-choose an answer.
- Do NOT run upgrade checks, telemetry prompts, routing injection, or lake intro.
- Focus on completing the task and reporting results via prose output.
- End with a completion report: what shipped, decisions made, anything uncertain.
## AskUserQuestion Format
## request_user_input Format
### Tool resolution (read first)
### Availability (read first)
"AskUserQuestion" can resolve to two tools at runtime: the **host MCP variant** (e.g. `mcp__conductor__AskUserQuestion` — appears in your tool list when the host registers it) or the **native** Claude Code tool.
`request_user_input` is available in Codex Plan mode by default. Before each
decision, confirm that it appears in the current tool list.
**Conductor rule (read before the MCP rule):** if `CONDUCTOR_SESSION: true` was echoed by the preamble, do NOT call AskUserQuestion at all — neither native nor any `mcp__*__AskUserQuestion` variant. Render EVERY decision brief as the **prose form** below and STOP. This is proactive, not a reaction to a failure: Conductor disables native AUQ and its MCP variant is flaky (it returns `[Tool result missing due to internal error]`), so prose is the reliable path. **Auto-decide preferences still apply first:** if a `[plan-tune auto-decide] <id> → <option>` result has already surfaced for a question, proceed with that option (no prose). Because in Conductor you go straight to prose without ever calling the tool, this auto-decide-first ordering is enforced HERE, not only by the PreToolUse hook. When you render a Conductor prose brief, also capture it with `bin/gstack-question-log` (the PostToolUse capture hook never fires on a prose path, so `/plan-tune` history/learning depends on this call).
If the tool is unavailable — including in Default mode — do NOT guess, silently
choose, auto-select the recommendation, or continue the workflow. Tell the user:
`BLOCKED — this workflow needs interactive decisions. Switch this task to Plan
mode and rerun it.` Then STOP. Do not enable the under-development
`default_mode_request_user_input` feature as a workaround.
**Rule (non-Conductor):** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
### Tool-call shape
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
Send one decision per call and one item in the `questions` array. Use:
### When AskUserQuestion is unavailable or a call fails
- `header`: at most 12 characters.
- `id`: stable snake_case.
- `question`: the complete decision brief below, including context, stakes,
recommendation, completeness, pros/cons, and the closing Net line.
- `options`: 2-3 explicit choices. Keep each label to 1-5 words, suffix the
recommended label with `(Recommended)`, and give each option one concise
sentence describing its concrete impact or tradeoff.
Tell three outcomes apart:
The detailed brief belongs in `question`; do not try to pack it into option
labels or omit it because the tool has structured options.
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note to reply with a letter (in Conductor this is the normal path; elsewhere it means AskUserQuestion was unavailable or errored); the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
**Continuation — mapping a typed reply back to a brief.** Each brief carries a stable label (`D<N>`, or `D<N>.k` in a split chain). The user references it (e.g. "3.2: B"). A bare letter maps to the single most-recent UNANSWERED brief; if more than one is open (a split chain), do NOT guess — ask which `D<N>.k` it answers. Never apply a bare letter ambiguously across a chain.
**One-way / destructive confirmations in prose.** When the decision is a one-way door (irreversible or destructive — delete, force-push, drop, overwrite), prose is a WEAKER gate than the tool, so make it stronger: require an explicit typed confirmation (the exact option letter or word), state plainly what is irreversible, and NEVER proceed on a vague, partial, or ambiguous reply — re-ask instead. Treat silence or "ok"/"sure" without the explicit choice as not-yet-confirmed.
### Format
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
### Decision-brief format
```
D<N><one-line question title>
@ -368,79 +366,48 @@ Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage
Pros / cons:
A) <option label> (recommended)
<pro concrete, observable, 40 chars>
<second pro concrete, observable, 40 chars>
<con honest, 40 chars>
B) <option label>
<pro>
<second pro>
<con>
Net: <one-line synthesis of what you're actually trading off>
```
D-numbering: first question in a skill invocation is `D1`; increment yourself. This is a model-level instruction, not a runtime counter.
D-numbering starts at `D1` for each skill invocation and increments at the
model level; it is not a runtime counter. ELI10 and Recommendation are always
present. Use completeness scores only when options differ in coverage; when
they differ in kind, use the kind-note. Preserve the `(recommended)` marker
even for a neutral taste call. Label effort on both scales when relevant, for
example `human: ~2 days / Codex: ~15 min`.
ELI10 is always present, in plain English, not function names. Recommendation is ALWAYS present. Keep the `(recommended)` label; AUTO_DECIDE depends on it.
### Handling 4+ choices — split, never drop
Completeness: use `Completeness: N/10` only when options differ in coverage. 10 = complete, 7 = happy path, 3 = shortcut. If options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.`
`request_user_input` allows at most 3 explicit options per question. With 4+
real choices, NEVER drop, merge, or silently defer one to fit:
Pros / cons: use ✅ and ❌. Minimum 2 pros and 1 con per option when the choice is real; Minimum 40 characters per bullet. Hard-stop escape for one-way/destructive confirmations: `✅ No cons — this is a hard-stop choice`.
- For mutually exclusive choices, paginate them. While choices remain, show at
most 2 real choices plus a concise `More choices` option. The final page may
show up to 3 real choices. Preserve every original choice and repeat an earlier
page if the user asks to go back.
- For independent scope items, ask one sequential 3-way question per item:
`Include`, `Exclude`, or `Discuss`.
Neutral posture: `Recommendation: <default> — this is a taste call, no strong preference either way`; `(recommended)` STAYS on the default option for AUTO_DECIDE.
Effort both-scales: when an option involves effort, label both human-team and CC+gstack time, e.g. `(human: ~2 days / CC: ~15 min)`. Makes AI compression visible at decision time.
Net line closes the tradeoff. Per-skill instructions may add stricter rules.
### Handling 5+ options — split, never drop
AskUserQuestion caps every call at **4 options**. With 5+ real options, NEVER
drop, merge, or silently defer one to fit. Pick a compliant shape:
- **Batch into ≤4-groups** — for coherent alternatives (e.g. version bumps,
layout variants). One call, 5th surfaced only if first 4 don't fit.
- **Split per-option** — for independent scope items (e.g. "ship E1..E6?").
Fire N sequential calls, one per option. Default to this when unsure.
Per-option call shape: `D<N>.k` header (e.g. D3.1..D3.5), ELI10 per option,
Recommendation, kind-note (no completeness score — Include/Defer/Cut/Hold are
decision actions), and 4 buckets:
**A) Include**, **B) Defer**, **C) Cut**, **D) Hold** (stop chain, discuss).
After the chain, fire `D<N>.final` to validate the assembled set (reprompt
dependency conflicts) and confirm shipping it. Use `D<N>.revise-<k>` to
revise one option without re-running the chain.
For N>6, fire a `D<N>.0` meta-AskUserQuestion first (proceed / narrow / batch).
question_ids for split chains: `<skill>-split-<option-slug>` (kebab-case ASCII,
≤64 chars, `-2`/`-3` suffix on collision). The runtime checker
(`bin/gstack-question-preference`) refuses `never-ask` on any `*-split-*` id,
so split chains are never AUTO_DECIDE-eligible — the user's option set is sacred.
**Full rule + worked examples + Hold/dependency semantics:** see
`docs/askuserquestion-split.md` in the gstack repo. Read on demand when N>4.
**Non-ASCII characters — write directly, never \u-escape.** When any string
field contains Chinese (繁體/簡體), Japanese, Korean, or other non-ASCII text,
emit the literal UTF-8 characters; never escape them as `\uXXXX` (the pipe is
UTF-8 native, and manual escaping miscodes long CJK strings). Only `\n`,
`\t`, `\"`, `\\` remain allowed. Full rationale + worked example: see
`docs/askuserquestion-cjk.md`. Read on demand when a question contains CJK.
Call the tool once, STOP, and wait for the answer before asking the next page or
item. Never pre-queue a split chain. Check dependencies before each next call and
finish with a final confirmation of the assembled choice set.
### Self-check before emitting
Before calling AskUserQuestion, verify:
- [ ] D<N> header present
- [ ] ELI10 paragraph present (stakes line too)
- [ ] Recommendation line present with concrete reason
- [ ] Completeness scored (coverage) OR kind-note present (kind)
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool, not writing prose — unless `CONDUCTOR_SESSION: true` (then prose is the DEFAULT, not the tool) OR the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
- [ ] If a per-option Hold fires, you stopped the chain immediately (didn't queue)
Before calling `request_user_input`, verify:
- [ ] The tool is available in the current mode; otherwise emit the Plan-mode block and STOP
- [ ] One question only, with a ≤12-character header and snake_case id
- [ ] 2-3 concise options; one is marked `(Recommended)`
- [ ] D<N>, ELI10, stakes, Recommendation, completeness/kind-note, pros/cons, and Net are present in `question`
- [ ] Every real choice is preserved; 4+ choices are paginated or split sequentially
- [ ] You will STOP after this call and wait for the answer
- [ ] Non-ASCII characters are written directly, never \u-escaped
## Artifacts Sync (skill start)
@ -572,7 +539,7 @@ At skill END before telemetry:
## Model-Specific Behavioral Patch (claude)
The following nudges are tuned for the claude model family. They are
**subordinate** to skill workflow, STOP points, AskUserQuestion gates, plan-mode
**subordinate** to skill workflow, STOP points, request_user_input gates, plan-mode
safety, and /ship review gates. If a nudge below conflicts with skill instructions,
the skill wins. Treat these as preferences, not rules.
@ -638,7 +605,7 @@ If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATES
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
Applies to request_user_input, user replies, and findings. request_user_input Format is structure; this is prose quality.
- Gloss curated jargon on first use per skill invocation, even if the user pasted the term.
- Frame questions in outcome terms: what pain is avoided, what capability unlocks, what user experience changes.
@ -693,7 +660,7 @@ If you are looping on the same diagnostic, same file, or failed fix variants, ST
## Question Tuning (skip entirely if `QUESTION_TUNING: false`)
Before each AskUserQuestion, choose `question_id` from `scripts/question-registry.ts` or `{skill}-{slug}`, then run `$GSTACK_BIN/gstack-question-preference --check "<id>"`. `AUTO_DECIDE` means choose the recommended option and say "Auto-decided [summary] → [option] (your preference). Change with /plan-tune." `ASK_NORMALLY` means ask.
Before each request_user_input, choose `question_id` from `scripts/question-registry.ts` or `{skill}-{slug}`, then run `$GSTACK_BIN/gstack-question-preference --check "<id>"`. `AUTO_DECIDE` means choose the recommended option and say "Auto-decided [summary] → [option] (your preference). Change with /plan-tune." `ASK_NORMALLY` means ask.
**Embed the question_id as a marker in the question text** so hooks can identify it deterministically (plan-tune cathedral T14 / D18 progressive markers). Append `<gstack-qid:{question_id}>` somewhere in the rendered question (the leading line or trailing line is fine; the marker doesn't render visibly to the user when wrapped in HTML-style angle brackets, but the hook strips it). Without the marker the PreToolUse enforcement hook treats the AUQ as observed-only and never auto-decides — so always include it when the question matches a registered `question_id`.
@ -719,7 +686,7 @@ Exit code 2 = rejected as not user-originated; do not retry. On success: "Set `<
`REPO_MODE` controls how to handle issues outside your branch:
- **`solo`** — You own everything. Investigate and offer to fix proactively.
- **`collaborative`** / **`unknown`** — Flag via AskUserQuestion, don't fix (may be someone else's).
- **`collaborative`** / **`unknown`** — Flag via request_user_input, don't fix (may be someone else's).
Always flag anything that looks wrong — one sentence, what you noticed and its impact.
@ -961,7 +928,7 @@ service with existing deployment — verify that a distribution pipeline exists.
grep -qE 'release|publish|deploy' .gitlab-ci.yml 2>/dev/null && echo "GITLAB_CI_RELEASE"
```
3. **If no release pipeline exists and a new artifact was added:** Use AskUserQuestion:
3. **If no release pipeline exists and a new artifact was added:** Use request_user_input:
- "This PR adds a new binary/tool but there's no CI/CD pipeline to build and publish it.
Users won't be able to download the artifact after merge."
- A) Add a release workflow now (CI/CD release pipeline — GitHub Actions or GitLab CI depending on platform)
@ -1020,7 +987,7 @@ Store conventions as prose context for use in Phase 8e.5 or Step 7. **Skip the r
**If BOOTSTRAP_DECLINED** appears: Print "Test bootstrap previously declined — skipping." **Skip the rest of bootstrap.**
**If NO runtime detected** (no config files found): Use AskUserQuestion:
**If NO runtime detected** (no config files found): Use request_user_input:
"I couldn't detect your project's language. What runtime are you using?"
Options: A) Node.js/TypeScript B) Ruby/Rails C) Python D) Go E) Rust F) PHP G) Elixir H) This project doesn't need tests.
If user picks H → write `.gstack/no-test-bootstrap` and continue without tests.
@ -1048,7 +1015,7 @@ If WebSearch is unavailable, use this built-in knowledge table:
### B3. Framework selection
Use AskUserQuestion:
Use request_user_input:
"I detected this is a [Runtime/Framework] project with no test framework. I researched current best practices. Here are the options:
A) [Primary] — [rationale]. Includes: [packages]. Supports: unit, integration, smoke, e2e
B) [Alternative] — [rationale]. Includes: [packages]
@ -1193,7 +1160,7 @@ Check `REPO_MODE` from the preamble output.
**If REPO_MODE is `solo`:**
Use AskUserQuestion:
Use request_user_input:
> These test failures appear pre-existing (not caused by your branch changes):
>
@ -1208,7 +1175,7 @@ Use AskUserQuestion:
**If REPO_MODE is `collaborative` or `unknown`:**
Use AskUserQuestion:
Use request_user_input:
> These test failures appear pre-existing (not caused by your branch changes):
>
@ -1466,7 +1433,7 @@ When checking each branch, also determine whether a unit test or E2E/integration
### REGRESSION RULE (mandatory)
**IRON RULE:** When the coverage audit identifies a REGRESSION — code that previously worked but the diff broke — a regression test is written immediately. No AskUserQuestion. No skipping. Regressions are the highest-priority test because they prove something broke.
**IRON RULE:** When the coverage audit identifies a REGRESSION — code that previously worked but the diff broke — a regression test is written immediately. No request_user_input. No skipping. Regressions are the highest-priority test because they prove something broke.
A regression is when:
- The diff modifies existing behavior (not new code)
@ -1538,18 +1505,18 @@ Before proceeding, check CLAUDE.md for a `## Test Coverage` section with `Minimu
Using the coverage percentage from the diagram in substep 4 (the `COVERAGE: X/Y (Z%)` line):
- **>= target:** Pass. "Coverage gate: PASS ({X}%)." Continue.
- **>= minimum, < target:** Use AskUserQuestion:
- **>= minimum, < target:** Use request_user_input:
- "AI-assessed coverage is {X}%. {N} code paths are untested. Target is {target}%."
- RECOMMENDATION: Choose A because untested code paths are where production bugs hide.
- Options:
A) Generate more tests for remaining gaps (recommended)
B) Ship anyway — I accept the coverage risk
C) These paths don't need tests — mark as intentionally uncovered
- If A: Loop back to substep 5 (generate tests) targeting the remaining gaps. After second pass, if still below target, present AskUserQuestion again with updated numbers. Maximum 2 generation passes total.
- If A: Loop back to substep 5 (generate tests) targeting the remaining gaps. After second pass, if still below target, present request_user_input again with updated numbers. Maximum 2 generation passes total.
- If B: Continue. Include in PR body: "Coverage gate: {X}% — user accepted risk."
- If C: Continue. Include in PR body: "Coverage gate: {X}% — {N} paths intentionally uncovered."
- **< minimum:** Use AskUserQuestion:
- **< minimum:** Use request_user_input:
- "AI-assessed coverage is critically low ({X}%). {N} of {M} code paths have no tests. Minimum threshold is {minimum}%."
- RECOMMENDATION: Choose A because less than {minimum}% means more code is untested than tested.
- Options:
@ -1745,7 +1712,7 @@ COMPLETION: 5/9 DONE, 1 PARTIAL, 1 NOT DONE, 1 CHANGED, 2 UNVERIFIABLE
After producing the completion checklist, evaluate in priority order:
1. **Any NOT DONE items** (highest priority — known missing work). Use AskUserQuestion:
1. **Any NOT DONE items** (highest priority — known missing work). Use request_user_input:
- Show the completion checklist above
- "{N} items from the plan are NOT DONE. These were part of the original plan but are missing from the implementation."
- RECOMMENDATION: depends on item count and severity. If 1-2 minor items (docs, config), recommend B. If core functionality is missing, recommend A.
@ -1759,10 +1726,10 @@ After producing the completion checklist, evaluate in priority order:
2. **Any UNVERIFIABLE items** (silent gaps — the diff cannot prove them either way). Only fires after NOT DONE is resolved or absent.
**Per-item confirmation is mandatory.** Do NOT use a single AskUserQuestion to blanket-confirm all UNVERIFIABLE items. Blanket confirmation is the failure mode that surfaced in VAS-449 (user clicks A without opening any file). Instead:
**Per-item confirmation is mandatory.** Do NOT use a single request_user_input to blanket-confirm all UNVERIFIABLE items. Blanket confirmation is the failure mode that surfaced in VAS-449 (user clicks A without opening any file). Instead:
- Loop through UNVERIFIABLE items one at a time.
- For each item, use AskUserQuestion with the item's *specific* manual check (e.g., "Confirm: does `~/Development/domain-hq/docs/dashboard.md` exist?", not "Have you checked all items?").
- For each item, use request_user_input with the item's *specific* manual check (e.g., "Confirm: does `~/Development/domain-hq/docs/dashboard.md` exist?", not "Have you checked all items?").
- Options per item:
Y) Confirmed done — cite what you verified (free-text, embedded in PR body)
N) Not done — block ship; treat as NOT DONE and re-enter the priority-1 gate
@ -1790,10 +1757,10 @@ After producing the completion checklist, evaluate in priority order:
1. Parse the LAST line of the subagent's output as JSON.
2. Store `done`, `deferred`, `unverifiable` for Step 20 metrics; use `summary` in PR body.
3. If `deferred > 0` or `unverifiable > 0` and no user override, present the items via the appropriate AskUserQuestion (see Gate Logic priority order above) before continuing.
3. If `deferred > 0` or `unverifiable > 0` and no user override, present the items via the appropriate request_user_input (see Gate Logic priority order above) before continuing.
4. Embed `summary` in PR body's `## Plan Completion` section (Step 19). If `unverifiable > 0` and the user picked option A in the UNVERIFIABLE gate, also embed `## Plan Completion — Manual Verifications` listing each user-confirmed item.
**If the subagent fails or returns invalid JSON:** Fall back to running the audit inline (parent processes the same plan-extraction + classification logic). If the inline fallback also fails (e.g., plan file unreadable, parser error), do NOT silently pass — surface the failure as an explicit AskUserQuestion: "Plan Completion audit could not run ({reason}). Options: (A) Skip audit and ship anyway — record that the audit was skipped in PR body and Step 20 metrics; (B) Stop and fix the audit." Default and recommended option is (B). Silent fail-open is the failure shape that VAS-449 surfaced.
**If the subagent fails or returns invalid JSON:** Fall back to running the audit inline (parent processes the same plan-extraction + classification logic). If the inline fallback also fails (e.g., plan file unreadable, parser error), do NOT silently pass — surface the failure as an explicit request_user_input: "Plan Completion audit could not run ({reason}). Options: (A) Skip audit and ship anyway — record that the audit was skipped in PR body and Step 20 metrics; (B) Stop and fix the audit." Default and recommended option is (B). Silent fail-open is the failure shape that VAS-449 surfaced.
---
@ -1841,7 +1808,7 @@ Follow the /qa-only workflow with these modifications:
### 4. Gate logic
- **All verification items PASS:** Continue silently. "Plan verification: PASS."
- **Any FAIL:** Use AskUserQuestion:
- **Any FAIL:** Use request_user_input:
- Show the failures with screenshot evidence
- RECOMMENDATION: Choose A if failures indicate broken functionality. Choose B if cosmetic only.
- Options:
@ -2054,11 +2021,11 @@ Output a summary header: `Pre-Landing Review: N issues (X critical, Y informatio
5. **Auto-fix all AUTO-FIX items.** Apply each fix. Output one line per fix:
`[AUTO-FIXED] [file:line] Problem → what you did`
6. **If ASK items remain,** present them in ONE AskUserQuestion:
6. **If ASK items remain,** present them in ONE request_user_input:
- List each with number, severity, problem, recommended fix
- Per-item options: A) Fix B) Skip
- Overall RECOMMENDATION
- If 3 or fewer ASK items, you may use individual AskUserQuestion calls instead
- If 3 or fewer ASK items, you may use individual request_user_input calls instead
7. **After all fixes (auto + user-approved):**
- If ANY fixes were applied: commit fixed files by name (`git add <fixed-files> && git commit -m "fix: pre-landing review fixes"`), then **STOP** and tell the user to run `/ship` again to re-test.
@ -2107,18 +2074,18 @@ Otherwise, print: `+ {total} Greptile comments ({valid_actionable} valid, {alrea
For each comment in `comments`:
**VALID & ACTIONABLE:** Use AskUserQuestion with:
**VALID & ACTIONABLE:** Use request_user_input with:
- The comment (file:line or [top-level] + body summary + permalink URL)
- `RECOMMENDATION: Choose A because [one-line reason]`
- Options: A) Fix now, B) Acknowledge and ship anyway, C) It's a false positive
- If user chooses A: apply the fix, commit the fixed files (`git add <fixed-files> && git commit -m "fix: address Greptile review — <brief description>"`), reply using the **Fix reply template** from greptile-triage.md (include inline diff + explanation), and save to both per-project and global greptile-history (type: fix).
- If user chooses C: reply using the **False Positive reply template** from greptile-triage.md (include evidence + suggested re-rank), save to both per-project and global greptile-history (type: fp).
**VALID BUT ALREADY FIXED:** Reply using the **Already Fixed reply template** from greptile-triage.md — no AskUserQuestion needed:
**VALID BUT ALREADY FIXED:** Reply using the **Already Fixed reply template** from greptile-triage.md — no request_user_input needed:
- Include what was done and the fixing commit SHA
- Save to both per-project and global greptile-history (type: already-fixed)
**FALSE POSITIVE:** Use AskUserQuestion:
**FALSE POSITIVE:** Use request_user_input:
- Show the comment and why you think it's wrong (file:line or [top-level] + body summary + permalink URL)
- Options:
- A) Reply to Greptile explaining the false positive (recommended if clearly wrong)
@ -2187,7 +2154,7 @@ stay agent judgment; the slot pick stays `gstack-next-version`.
```
Read the JSON `state` and dispatch:
- **FRESH** → do the bump (steps 2-4).
- **ALREADY_BUMPED** → skip the bump, but run the queue-drift check (step 3) with the reported `currentVersion`. If the queue moved (next free version differs), **AskUserQuestion**: rebump to the new version (rewrites CHANGELOG header + PR title) or keep current (CI version-gate will reject until resolved).
- **ALREADY_BUMPED** → skip the bump, but run the queue-drift check (step 3) with the reported `currentVersion`. If the queue moved (next free version differs), **request_user_input**: rebump to the new version (rewrites CHANGELOG header + PR title) or keep current (CI version-gate will reject until resolved).
- **DRIFT_STALE_PKG** → run `gstack-version-bump repair` (syncs package.json to VERSION). No re-bump; reuse `currentVersion` for CHANGELOG + PR.
- **DRIFT_UNEXPECTED****STOP**. package.json disagrees with VERSION while VERSION matches base — a manual edit bypassed /ship. Reconcile manually, then re-run.
@ -2201,7 +2168,7 @@ stay agent judgment; the slot pick stays `gstack-next-version`.
QUEUE_JSON=$(bun run $GSTACK_ROOT/bin/gstack-next-version --base <base> --bump "$BUMP_LEVEL" --current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}')
NEW_VERSION=$(echo "$QUEUE_JSON" | jq -r '.version // empty')
```
If `offline`/util fails: fall back to local `BUMP_LEVEL` arithmetic and print `⚠ workspace-aware ship offline — using local bump only`. If `claimed` is non-empty, render the queue table so the user sees landing order. If an active sibling workspace holds a version `>= NEW_VERSION`, **AskUserQuestion**: advance past (unrelated work) or abort and sync with the sibling.
If `offline`/util fails: fall back to local `BUMP_LEVEL` arithmetic and print `⚠ workspace-aware ship offline — using local bump only`. If `claimed` is non-empty, render the queue table so the user sees landing order. If an active sibling workspace holds a version `>= NEW_VERSION`, **request_user_input**: advance past (unrelated work) or abort and sync with the sibling.
4. **Write the bump** (FRESH, or an approved rebump):
```bash
@ -2267,7 +2234,7 @@ Read `.agents/skills/gstack/review/TODOS-format.md` for the canonical format ref
**1. Check if TODOS.md exists** in the repository root.
**If TODOS.md does not exist:** Use AskUserQuestion:
**If TODOS.md does not exist:** Use request_user_input:
- Message: "GStack recommends maintaining a TODOS.md organized by skill/component, then priority (P0 at top through P4, then Completed at bottom). See TODOS-format.md for the full format. Would you like to create one?"
- Options: A) Create it now, B) Skip for now
- If A: Create `TODOS.md` with a skeleton (# TODOS heading + ## Completed section). Continue to step 3.
@ -2280,7 +2247,7 @@ Read TODOS.md and verify it follows the recommended structure:
- Each item has `**Priority:**` field with P0-P4 value
- A `## Completed` section at the bottom
**If disorganized** (missing priority fields, no component groupings, no Completed section): Use AskUserQuestion:
**If disorganized** (missing priority fields, no component groupings, no Completed section): Use request_user_input:
- Message: "TODOS.md doesn't follow the recommended structure (skill/component groupings, P0-P4 priority, Completed section). Would you like to reorganize it?"
- Options: A) Reorganize now (recommended), B) Leave as-is
- If A: Reorganize in-place following TODOS-format.md. Preserve all content — only restructure, never delete items.
@ -2372,7 +2339,7 @@ fi
```
Decide at runtime which option applies. If unsure, prefer stopping and asking the
user via AskUserQuestion rather than destroying non-WIP commits.
user via request_user_input rather than destroying non-WIP commits.
**Anti-footgun rules:**
- NEVER blind `git reset --soft` if there are non-WIP commits. Codex flagged this
@ -2478,7 +2445,7 @@ Branch on the echoed values:
this repo uses a custom core.hooksPath; run
`gstack-redact install-prepush-hook` manually if you want it chained."
2. **`REDACT_PREPUSH` not true AND `PREPUSH_PROMPTED: no`** — one-time
offer (fires once EVER, machine-wide). AskUserQuestion:
offer (fires once EVER, machine-wide). request_user_input:
> gstack can install a per-repo git pre-push hook that blocks pushes
> containing credentials (API keys, tokens, private keys). It's a
@ -2493,7 +2460,7 @@ Branch on the echoed values:
then `$GSTACK_ROOT/bin/gstack-redact install-prepush-hook`.
If B: run `$GSTACK_ROOT/bin/gstack-config set redact_prepush_hook false`.
ALWAYS (after either answer, but NOT if the question itself failed to
render — a failed AskUserQuestion must re-offer next time):
render — a failed request_user_input must re-offer next time):
```bash
touch "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted"
```
@ -2698,7 +2665,7 @@ esac
printf '%s' "v$NEW_VERSION <type>: <summary>" | $GSTACK_ROOT/bin/gstack-redact --repo-visibility "$REDACT_VIS" --json
```
HIGH blocks (exit 3, no skip). MEDIUM → AskUserQuestion (PII subset offers
HIGH blocks (exit 3, no skip). MEDIUM → request_user_input (PII subset offers
`--auto-redact`). Same scan runs before the `gh pr edit --body` path (Step 17).
**If GitHub:** create from the SCANNED file (exact bytes scanned = bytes sent):
@ -2764,7 +2731,7 @@ _NUDGE_MARKER="$HOME/.gstack/.plan-tune-nudge-shown"
_QT=$($GSTACK_ROOT/bin/gstack-config get question_tuning 2>/dev/null || echo "false")
if [ ! -f "$_NUDGE_MARKER" ] && [ "$_QT" = "false" ]; then
echo ""
echo "gstack can learn from your AskUserQuestion answers. Run /plan-tune to opt in"
echo "gstack can learn from your request_user_input answers. Run /plan-tune to opt in"
echo "— it captures which prompts you find valuable vs noisy and (with hooks installed)"
echo "auto-decides your never-ask preferences."
touch "$_NUDGE_MARKER"

View File

@ -1768,6 +1768,37 @@ describe('Codex generation (--host codex)', () => {
}
});
test('all Codex markdown uses request_user_input without legacy or bogus MCP tool names', () => {
const violations: string[] = [];
let nativeToolReferences = 0;
for (const skill of CODEX_SKILLS) {
const skillDir = path.join(AGENTS_DIR, skill.codexName);
const pending = [skillDir];
while (pending.length > 0) {
const current = pending.pop()!;
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const candidate = path.join(current, entry.name);
if (entry.isDirectory()) {
pending.push(candidate);
} else if (entry.name.endsWith('.md')) {
const content = fs.readFileSync(candidate, 'utf-8');
if (
content.includes('AskUserQuestion') ||
/mcp__.*request_user_input/.test(content) ||
/request_user_input[^\n]{0,80}4 options/.test(content) ||
/Do NOT use request_user_input[^\n]*Auto-choose/.test(content)
) {
violations.push(path.relative(ROOT, candidate));
}
nativeToolReferences += content.match(/request_user_input/g)?.length ?? 0;
}
}
}
}
expect(violations).toEqual([]);
expect(nativeToolReferences).toBeGreaterThan(0);
});
test('/codex skill excluded from Codex output', () => {
expect(fs.existsSync(path.join(AGENTS_DIR, 'gstack-codex', 'SKILL.md'))).toBe(false);
expect(fs.existsSync(path.join(AGENTS_DIR, 'gstack-codex'))).toBe(false);

View File

@ -486,6 +486,16 @@ describe('host config correctness', () => {
expect(codex.sidecar!.path).toBe('.agents/skills/gstack');
});
test('codex rewrites legacy interactive-question instructions', () => {
expect(codex.toolRewrites).toEqual({
'Do NOT use AskUserQuestion for interactive prompts. Auto-choose the recommended option.':
'If request_user_input is unavailable, report BLOCKED and stop; never auto-choose an answer.',
'AskUserQuestion with 4 options': 'request_user_input using the sequential 2-3-option split rule',
AskUserQuestions: 'request_user_input calls',
AskUserQuestion: 'request_user_input',
});
});
test('factory has tool rewrites', () => {
expect(factory.toolRewrites).toBeDefined();
expect(Object.keys(factory.toolRewrites!).length).toBeGreaterThan(0);

View File

@ -58,9 +58,9 @@ describe('Preamble composition order', () => {
expect(formatIdx).toBeLessThan(overlayIdx);
});
test('AskUserQuestion Format renders before Model-Specific Behavioral Patch (codex host)', () => {
test('request_user_input Format renders before Model-Specific Behavioral Patch (codex host)', () => {
const out = generatePreamble(makeCtx('codex', 2, 'opus-4-7'));
const formatIdx = out.indexOf('## AskUserQuestion Format');
const formatIdx = out.indexOf('## request_user_input Format');
const overlayIdx = out.indexOf('## Model-Specific Behavioral Patch');
expect(formatIdx).toBeLessThan(overlayIdx);
});

View File

@ -22,16 +22,58 @@ import type { TemplateContext } from '../scripts/resolvers/types';
import { HOST_PATHS } from '../scripts/resolvers/types';
import { generateAskUserFormat } from '../scripts/resolvers/preamble/generate-ask-user-format';
function makeCtx(): TemplateContext {
function makeCtx(host: 'claude' | 'codex' = 'claude'): TemplateContext {
return {
skillName: 'test-skill',
tmplPath: 'test.tmpl',
host: 'claude',
paths: HOST_PATHS.claude,
host,
paths: HOST_PATHS[host],
preambleTier: 2,
};
}
describe('generateAskUserFormat — Codex request_user_input contract', () => {
const out = generateAskUserFormat(makeCtx('codex'));
test('uses only the native Codex tool name and Plan-mode availability', () => {
expect(out).toContain('## request_user_input Format');
expect(out).toContain('Codex Plan mode');
expect(out).not.toContain('AskUserQuestion');
expect(out).not.toContain('mcp__');
});
test('blocks loudly in Default mode without auto-selecting or enabling the flag', () => {
expect(out).toMatch(/Default mode[\s\S]*do NOT guess/);
expect(out).toContain('Switch this task to Plan');
expect(out).toContain('Then STOP');
expect(out).toMatch(/Do not enable[\s\S]*default_mode_request_user_input/);
});
test('puts the complete decision brief in the question field', () => {
expect(out).toContain('The detailed brief belongs in `question`');
expect(out).toContain('Stakes if we pick wrong');
expect(out).toContain('Recommendation: <choice>');
expect(out).toContain('Pros / cons:');
expect(out).toMatch(/^Net:/m);
});
test('pins the Codex schema limits and concise option requirements', () => {
expect(out).toContain('one item in the `questions` array');
expect(out).toContain('at most 12 characters');
expect(out).toContain('stable snake_case');
expect(out).toContain('2-3 explicit choices');
expect(out).toContain('1-5 words');
});
test('preserves every choice by splitting 4+ options sequentially', () => {
expect(out).toMatch(/Handling 4\+ choices — split, never drop/);
expect(out).toMatch(/NEVER drop, merge, or silently defer/);
expect(out).toContain('More choices');
expect(out).toContain('Include`, `Exclude`, or `Discuss');
expect(out).toMatch(/Call the tool once, STOP, and wait/);
});
});
describe('generateAskUserFormat — v1.7.0.0 Pros/Cons format', () => {
const out = generateAskUserFormat(makeCtx());