diff --git a/.changeset/clever-dogs-howl.md b/.changeset/clever-dogs-howl.md new file mode 100644 index 000000000..85bf186a5 --- /dev/null +++ b/.changeset/clever-dogs-howl.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 4832 +--- +**`--auto` no longer stops on an artifact that already exists** — `/gsd-ui-phase --auto` blocked on "UI-SPEC.md already exists for Phase {N}" whenever the file was on disk, which is most often after an earlier run left an unverified draft, so a headless or board run stalled on a question nobody could answer. It now reuses the existing UI-SPEC untouched and proceeds to the checker. `/gsd-spec-phase --auto` had the opposite failure — it auto-selected "Update it" and regenerated a spec nobody was watching, discarding answers already recorded in it — and now also reuses it as-is. The same gap existed in three sibling commands with no prior `--auto` handling at all: `/gsd-ai-integration-phase --auto` now auto-selects Skip for an existing AI-SPEC, and `/gsd-eval-review --auto` / `/gsd-ui-review --auto` now auto-select View for an existing EVAL-REVIEW/UI-REVIEW instead of re-auditing. The max-revision-iterations escalation (Force approve / Edit manually / Abandon) still stops for a person under `--auto`, by design. (#4776) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index a135a4665..98f75dea1 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -118,7 +118,7 @@ Clarify WHAT a phase delivers through Socratic questioning with quantitative amb | Flag | Description | |------|-------------| -| `--auto` | Skip interactive questions; Claude selects recommended defaults and writes SPEC.md | +| `--auto` | Skip interactive questions; Claude selects recommended defaults and writes SPEC.md. An existing SPEC.md is **reused as-is**, never regenerated | | `--text` | Use plain-text numbered lists instead of TUI menus (required for `/rc` remote sessions) | **Position in workflow:** `spec-phase → discuss-phase → plan-phase → execute-phase → verify` @@ -178,11 +178,17 @@ Generate UI design contract for frontend phases. |----------|----------|-------------| | `N` | No | Phase number (defaults to current phase) | +| Flag | Description | +|------|-------------| +| `--auto` | Skip interactive questions. An existing UI-SPEC.md is **reused as-is** and sent straight to the checker, never re-researched | +| `--text` | Use plain-text numbered lists instead of TUI menus | + **Prerequisites:** `.planning/ROADMAP.md` exists, phase has frontend/UI work **Produces:** `{phase}-UI-SPEC.md` ```bash /gsd-ui-phase 2 # Design contract for phase 2 +/gsd-ui-phase 2 --auto # Non-interactive; reuses an existing UI-SPEC ``` --- @@ -426,9 +432,14 @@ Retroactive 6-pillar visual audit of implemented frontend. For richer visual evidence, pair this with `gsd-browser` or another browser MCP server so the audit can capture screenshots, state, console/network context, and reproducible interaction steps. +| Flag | Description | +|------|-------------| +| `--auto` | Skip interactive questions. An existing UI-REVIEW.md is **reused as-is**, never re-audited | + ```bash /gsd-ui-review # Audit current phase /gsd-ui-review 3 # Audit phase 3 +/gsd-ui-review 3 --auto # Non-interactive; reuses an existing UI-REVIEW ``` --- @@ -1659,9 +1670,14 @@ Generate an AI-SPEC.md design contract for phases that involve building AI syste **Spawns:** 3 parallel specialist agents: domain-researcher, framework-selector, ai-researcher, and eval-planner +| Flag | Description | +|------|-------------| +| `--auto` | Skip interactive questions. An existing AI-SPEC.md is **reused as-is**, never regenerated | + ```bash /gsd-ai-integration-phase # Wizard for the current phase /gsd-ai-integration-phase 3 # Wizard for a specific phase +/gsd-ai-integration-phase 3 --auto # Non-interactive; reuses an existing AI-SPEC ``` --- @@ -1673,9 +1689,14 @@ Audit an executed AI phase's evaluation coverage and produce an EVAL-REVIEW.md r **Prerequisites:** Phase has been executed and has an `AI-SPEC.md` **Produces:** `{phase}-EVAL-REVIEW.md` with findings, gaps, and remediation guidance +| Flag | Description | +|------|-------------| +| `--auto` | Skip interactive questions. An existing EVAL-REVIEW.md is **reused as-is**, never re-audited | + ```bash /gsd-eval-review # Audit current phase /gsd-eval-review 3 # Audit a specific phase +/gsd-eval-review 3 --auto # Non-interactive; reuses an existing EVAL-REVIEW ``` --- diff --git a/gsd-core/workflows/ai-integration-phase.md b/gsd-core/workflows/ai-integration-phase.md index 404826609..39cb2ae84 100644 --- a/gsd-core/workflows/ai-integration-phase.md +++ b/gsd-core/workflows/ai-integration-phase.md @@ -79,7 +79,15 @@ AI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-AI-SPEC.md 2>/dev/null | head -1) ``` **Text mode (`workflow.text_mode: true` in config or `--text` flag):** Set `TEXT_MODE=true` if `--text` is present in `$ARGUMENTS` OR `text_mode` from init JSON is `true`. When TEXT_MODE is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes (OpenAI Codex, Antigravity, etc.) where `AskUserQuestion` is not available. -**If exists:** Use AskUserQuestion: +**If exists:** + +**If `--auto`:** Auto-select "Skip" — keep the existing AI-SPEC untouched and exit. Log: +`[auto] AI-SPEC exists — reusing as-is.` Skip is the auto choice because it is the only +non-destructive one: "Update" re-runs framework selection with the existing spec as baseline, +which can rewrite decisions a person already recorded in it, and nobody is present to notice +(#4776). + +**Otherwise:** Use AskUserQuestion: - header: "Existing AI-SPEC" - question: "AI-SPEC.md already exists for Phase {N}. What would you like to do?" - options: diff --git a/gsd-core/workflows/eval-review.md b/gsd-core/workflows/eval-review.md index 09523509e..9c906cad9 100644 --- a/gsd-core/workflows/eval-review.md +++ b/gsd-core/workflows/eval-review.md @@ -46,7 +46,13 @@ EVAL_REVIEW_FILE=$(ls "${PHASE_DIR}"/*-EVAL-REVIEW.md 2>/dev/null | head -1) **State C** — No SUMMARY.md: Exit — "Phase {N} not executed. Run /gsd:execute-phase {N} first." **Text mode (`workflow.text_mode: true` in config or `--text` flag):** Set `TEXT_MODE=true` if `--text` is present in `$ARGUMENTS` OR `text_mode` from init JSON is `true`. When TEXT_MODE is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes (OpenAI Codex, Antigravity, etc.) where `AskUserQuestion` is not available. -**If `EVAL_REVIEW_FILE` non-empty:** Use AskUserQuestion: +**If `EVAL_REVIEW_FILE` non-empty:** + +**If `--auto`:** Auto-select "View" — keep the existing EVAL-REVIEW.md untouched and exit without +re-auditing. Log: `[auto] EVAL-REVIEW.md exists — reusing as-is.` "Re-audit" is the regenerating +choice; an unattended run reuses an existing artifact instead (#4776). + +**Otherwise:** Use AskUserQuestion: - header: "Existing Eval Review" - question: "EVAL-REVIEW.md already exists for Phase {N}." - options: diff --git a/gsd-core/workflows/spec-phase.md b/gsd-core/workflows/spec-phase.md index 235512eb8..32df46c16 100644 --- a/gsd-core/workflows/spec-phase.md +++ b/gsd-core/workflows/spec-phase.md @@ -79,7 +79,11 @@ 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.` +**If `--auto`:** Auto-select "Skip" — leave the existing SPEC.md unchanged and exit with the same +message the interactive "Skip" prints. Log: `[auto] SPEC.md exists — reusing as-is.` An unattended +run reuses an existing artifact rather than regenerating it (#4776): "Update it" re-scores and +rewrites the spec, discarding answers a person already recorded in it, and nobody is present to +notice. **Otherwise:** Use AskUserQuestion: - header: "Spec" diff --git a/gsd-core/workflows/ui-phase.md b/gsd-core/workflows/ui-phase.md index 4bafe1be3..5866a1af9 100644 --- a/gsd-core/workflows/ui-phase.md +++ b/gsd-core/workflows/ui-phase.md @@ -99,7 +99,16 @@ UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1) ``` **Text mode (`workflow.text_mode: true` in config or `--text` flag):** Set `TEXT_MODE=true` if `--text` is present in `$ARGUMENTS` OR `text_mode` from init JSON is `true`. When TEXT_MODE is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes (OpenAI Codex, Antigravity, etc.) where `AskUserQuestion` is not available. -**If exists:** Use AskUserQuestion: +**If exists:** + +**If `--auto`:** Auto-select "Skip" — keep the existing UI-SPEC untouched and proceed to step 7 +(checker). Log: `[auto] UI-SPEC exists — reusing as-is, proceeding to verification.` Skip is the +auto choice because it is the only non-destructive one: "Update" re-runs the researcher, which +rewrites the whole contract and drops answers a person already recorded in it, and "View" exits +without verifying anything. A `--auto` run most often meets this file as an unverified draft from +a run that ended before its checker ran, which is exactly the state the checker should now see. + +**Otherwise:** Use AskUserQuestion: - header: "Existing UI-SPEC" - question: "UI-SPEC.md already exists for Phase {N}. What would you like to do?" - options: diff --git a/gsd-core/workflows/ui-review.md b/gsd-core/workflows/ui-review.md index b6cc26767..917c61ef0 100644 --- a/gsd-core/workflows/ui-review.md +++ b/gsd-core/workflows/ui-review.md @@ -51,7 +51,13 @@ UI_REVIEW_FILE=$(ls "${PHASE_DIR}"/*-UI-REVIEW.md 2>/dev/null | head -1) **If `SUMMARY_FILES` empty:** Exit — "Phase {N} not executed. Run /gsd:execute-phase {N} first." **Text mode (`workflow.text_mode: true` in config or `--text` flag):** Set `TEXT_MODE=true` if `--text` is present in `$ARGUMENTS` OR `text_mode` from init JSON is `true`. When TEXT_MODE is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes (OpenAI Codex, Antigravity, etc.) where `AskUserQuestion` is not available. -**If `UI_REVIEW_FILE` non-empty:** Use AskUserQuestion: +**If `UI_REVIEW_FILE` non-empty:** + +**If `--auto`:** Auto-select "View" — keep the existing UI-REVIEW.md untouched and exit without +re-auditing. Log: `[auto] UI-REVIEW.md exists — reusing as-is.` "Re-audit" is the regenerating +choice; an unattended run reuses an existing artifact instead (#4776). + +**Otherwise:** Use AskUserQuestion: - header: "Existing UI Review" - question: "UI-REVIEW.md already exists for Phase {N}." - options: diff --git a/tests/ask-user-questions-fallback.test.cjs b/tests/ask-user-questions-fallback.test.cjs index f115841f5..e75f87f8c 100644 --- a/tests/ask-user-questions-fallback.test.cjs +++ b/tests/ask-user-questions-fallback.test.cjs @@ -199,3 +199,91 @@ describe('bug #17: AskUserQuestion options arrays respect runtime cap', () => { }); }); } + +// ──────────────────────────────────────────────────────────────────────── +// #4776 — an artifact-exists question must not block an unattended run +// ──────────────────────────────────────────────────────────────────────── +{ + const { describe: __autoDescribe, test: __autoTest } = require('node:test'); + __autoDescribe('artifact-exists prompts resolve under --auto (#4776)', () => { + const WF = path.join(__dirname, '..', 'gsd-core', 'workflows'); + + /** + * The decision points this issue covers. Each names the heading (or, for + * files with no dedicated heading at this point, the arm's own guard + * clause) that opens the artifact-exists branch, the option the `--auto` + * arm must take, and the sibling option that regenerates the artifact + * (the one an `--auto` arm must NOT select). + * + * The maintainer decision recorded on #4776: an unattended run REUSES an + * existing artifact rather than regenerating it. "Update"/"Re-audit" + * re-run the generator, rewriting a contract or review that may already + * carry answers or findings a person recorded, with nobody present to + * notice — so an `--auto` arm that selects one of those is the defect, + * not the fix. + * + * Widened from the original ui-phase.md/spec-phase.md pair to all 5 + * files sharing the gap (#4776's 2026-09-16 triage comment: "give all 5 + * files a consistent --auto branch... recommended fix"). + * ai-integration-phase.md mirrors ui-phase.md's 3-way Update/View/Skip + * shape; eval-review.md and ui-review.md have only Re-audit/View, so + * their non-destructive `--auto` choice is "View", not "Skip". + */ + const DECISION_POINTS = [ + { file: 'ui-phase.md', anchor: '## 4. Check Existing UI-SPEC', reuseOption: 'Skip', regenOption: 'Update' }, + { file: 'spec-phase.md', anchor: '**Check for existing SPEC.md:**', reuseOption: 'Skip', regenOption: 'Update' }, + { file: 'ai-integration-phase.md', anchor: '## 4. Check Existing AI-SPEC', reuseOption: 'Skip', regenOption: 'Update' }, + { file: 'eval-review.md', anchor: '**If `EVAL_REVIEW_FILE` non-empty:**', reuseOption: 'View', regenOption: 'Re-audit' }, + { file: 'ui-review.md', anchor: '**If `UI_REVIEW_FILE` non-empty:**', reuseOption: 'View', regenOption: 'Re-audit' }, + ]; + + /** The text from `anchor` up to the next heading of the same or higher level. */ + function section(content, anchor) { + const start = content.indexOf(anchor); + assert.notEqual(start, -1, `anchor not found: ${anchor}`); + const rest = content.slice(start + anchor.length); + const end = rest.search(/\n##? /); + return rest.slice(0, end === -1 ? rest.length : end); + } + + for (const { file, anchor, reuseOption, regenOption } of DECISION_POINTS) { + __autoTest(`${file}: the --auto arm reuses the existing artifact, before any prompt`, () => { + const body = section(fs.readFileSync(path.join(WF, file), 'utf8'), anchor); + + const autoIdx = body.search(/\*\*If `--auto`:\*\*/); + assert.notEqual(autoIdx, -1, + `${file}: the artifact-exists branch has no --auto arm, so an unattended run stops here`); + + // `Use AskUserQuestion`, not a bare mention: ui-phase.md's TEXT_MODE + // paragraph names AskUserQuestion above this branch, and anchoring on + // that would compare the arm against prose it has nothing to do with. + const askIdx = body.indexOf('Use AskUserQuestion'); + assert.notEqual(askIdx, -1, `${file}: expected a \`Use AskUserQuestion\` prompt at this decision point`); + assert.ok(autoIdx < askIdx, + `${file}: the --auto arm must resolve BEFORE the prompt, or the prompt still runs`); + + // The arm's own sentence — up to the end of that line — must name the + // reuse option. Scanning the whole section would match the interactive + // option list below it and pass on a file that auto-selects the + // regenerating option instead. + const armLine = body.slice(autoIdx, body.indexOf('\n', autoIdx) === -1 ? undefined : body.indexOf('\n', autoIdx)); + assert.match(armLine, new RegExp(`"${reuseOption}"`), + `${file}: the --auto arm must select "${reuseOption}" (reuse as-is), not regenerate the artifact`); + assert.doesNotMatch(armLine, new RegExp(`"${regenOption}`), + `${file}: auto-selecting "${regenOption}" regenerates an artifact nobody is watching (#4776)`); + }); + } + + __autoTest('the max-revision-iterations escalation stays interactive under --auto', () => { + // #4776 explicitly does NOT ask for this one: force-approving blocking + // findings is a decision a person makes. A future "resolve every prompt + // under --auto" sweep would break that, so it is pinned here. + const body = fs.readFileSync(path.join(WF, 'ui-phase.md'), 'utf8'); + const idx = body.indexOf('Force approve'); + assert.notEqual(idx, -1, 'ui-phase.md: expected the max-iterations escalation to still exist'); + const around = body.slice(Math.max(0, idx - 600), idx + 600); + assert.doesNotMatch(around, /\*\*If `--auto`:\*\*/, + 'the force-approve escalation must keep blocking for a human under --auto'); + }); + }); +}