From d101daff30c31af2d94a5aa07d82fca11d33fa36 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 24 Jun 2026 14:46:44 -0400 Subject: [PATCH] fix(#1516): expose adaptive model_profile in /gsd-new-project AI Models prompt (#1654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#1516): expose adaptive model_profile in /gsd-new-project AI Models prompt Both onboarding paths (Step 2a auto-mode + Step 5 interactive) enumerated only 4 profiles (Balanced/Quality/Budget/Inherit), omitting 'adaptive' even though the model catalog (model-catalog.json profiles) and docs/CONFIGURATION.md register 5. Mirrors the proven /gsd:settings two-question split (#3784): Q1 routes between Adaptive/Standard-tier/Inherit; Q2 (conditional on Q1=Standard) picks Quality/Balanced/Budget — keeping every AskUserQuestion within the 4-option cap. Both config-new-project example payloads now list adaptive. Regression cases folded into the owning tests/new-project-mvp-prompt.test.cjs (per the lint-regression-test-names ban on new top-level bug-NNNN files): each AI Models prompt makes adaptive reachable, all 5 profiles reachable, 4-option cap honored, both example enums include adaptive, brace balance. Workflow size baseline bumped (new-project.md 62324 -> 66138 bytes; still well under the XL hard cap). * chore(#1516): backfill changeset pr ref to 1654 --- .changeset/gallant-geese-run.md | 5 ++ gsd-core/workflows/new-project.md | 90 ++++++++++++++++--- tests/new-project-mvp-prompt.test.cjs | 124 ++++++++++++++++++++++++++ tests/workflow-size-baseline.json | 2 +- 4 files changed, 208 insertions(+), 13 deletions(-) create mode 100644 .changeset/gallant-geese-run.md diff --git a/.changeset/gallant-geese-run.md b/.changeset/gallant-geese-run.md new file mode 100644 index 000000000..c4c370747 --- /dev/null +++ b/.changeset/gallant-geese-run.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1654 +--- +**`/gsd-new-project` AI Models prompt now exposes the `adaptive` model profile** — both onboarding paths (auto-mode and interactive) listed only Balanced/Quality/Budget/Inherit, so the `adaptive` profile (role-based cost optimization across Claude/Codex/Gemini/OpenRouter/local) was unreachable through `/gsd-new-project` despite being a first-class catalog entry and documented in CONFIGURATION.md. Both prompts now use the proven two-question split (Q1: Adaptive / Standard tier / Inherit; Q2: Quality / Balanced / Budget) already shipped for `/gsd:settings` (#3784), and both `config-new-project` example payloads list `adaptive`. (#1516) diff --git a/gsd-core/workflows/new-project.md b/gsd-core/workflows/new-project.md index b9042e331..d1cfd3608 100644 --- a/gsd-core/workflows/new-project.md +++ b/gsd-core/workflows/new-project.md @@ -230,19 +230,52 @@ AskUserQuestion([ { label: "Yes (Recommended)", description: "Resolve symbol references against live source during plan review — catches hallucinated names before execution" }, { label: "No", description: "Skip symbol grounding — plan review proceeds without source verification" } ] - }, + } +]) + +// Model profile uses a two-question split because AskUserQuestion enforces a hard +// 4-option cap and there are 5 valid profiles (quality, balanced, budget, adaptive, +// inherit). Q1 routes between adaptive/standard-tier/inherit; Q2 (shown only when +// Q1 = "Standard tier…") picks among the three standard profiles. Mirrors the +// /gsd:settings split (#3784, #1516). +AskUserQuestion([ { header: "AI Models", question: "Which AI models for planning agents?", multiSelect: false, options: [ - { label: "Balanced (Recommended)", description: "Sonnet for most agents — good quality/cost ratio" }, - { label: "Quality", description: "Opus for research/roadmap — higher cost, deeper analysis" }, - { label: "Budget", description: "Haiku where possible — fastest, lowest cost" }, - { label: "Inherit", description: "Use the current session model for all agents (OpenCode /model)" } + { label: "Adaptive (Recommended)", description: "Role-based cost optimization: heavy roles use the highest-tier model available on the active runtime, light roles use the cheapest. Best balance of quality and cost across all supported runtimes (Claude, Codex, Gemini, OpenRouter, local)." }, + { label: "Standard tier…", description: "Choose Quality, Balanced, or Budget — flat tier applied to all agents" }, + { label: "Inherit", description: "Use the current session model for all agents (required for non-Claude runtimes: Codex, Gemini CLI, OpenCode /model, OpenRouter, local models)" } ] } ]) + +**Conditional visibility — model_profile (Q2):** + Only ask this question when Q1's answer is "Standard tier…". + If Q1 = "Adaptive (Recommended)" → write model_profile=adaptive and SKIP Q2. + If Q1 = "Inherit" → write model_profile=inherit and SKIP Q2. + If user cancels Q2 after picking "Standard tier…" → leave existing model_profile value unchanged. + +AskUserQuestion([ + { + question: "Which standard profile? (Quality / Balanced / Budget)", + header: "Model Tier", + multiSelect: false, + options: [ + { label: "Quality", description: "Opus everywhere except verification (highest cost) — Claude only" }, + { label: "Balanced", description: "Opus for planning, Sonnet for research/execution/verification — Claude only" }, + { label: "Budget", description: "Sonnet for writing, Haiku for research/verification (lowest cost) — Claude only" } + ] + } +]) + +// Map UI choices → config values: +// Q1 "Adaptive (Recommended)" → model_profile = "adaptive" +// Q1 "Inherit" → model_profile = "inherit" +// Q1 "Standard tier…" + Q2 "Quality" → model_profile = "quality" +// Q1 "Standard tier…" + Q2 "Balanced" → model_profile = "balanced" +// Q1 "Standard tier…" + Q2 "Budget" → model_profile = "budget" ``` **Round 3 — PR body onboarding:** @@ -273,7 +306,7 @@ Create `.planning/config.json` with all settings (CLI fills in remaining default ```bash mkdir -p .planning -gsd_run query config-new-project '{"mode":"yolo","granularity":"[selected]","parallelization":true|false,"commit_docs":true|false,"model_profile":"quality|balanced|budget|inherit","workflow":{"research":true|false,"plan_check":true|false,"verifier":true|false,"nyquist_validation":true|false,"auto_advance":true},"plan_review":{"source_grounding":true|false},"ship":{"pr_body_sections":[{"heading":"User Stories & Acceptance Criteria","enabled":true|false,"source":"REQUIREMENTS.md ## User Stories || REQUIREMENTS.md ## Acceptance Criteria","fallback":"- Acceptance criteria are covered by the linked requirements and verification evidence."},{"heading":"Risks & Dependencies","enabled":true|false,"source":"PLAN.md ## Risks || PLAN.md ## Dependencies","fallback":"- No known high-risk rollout dependencies."},{"heading":"Success Metrics & Release Criteria","enabled":true|false,"source":"REQUIREMENTS.md ## Definition of Done || VERIFICATION.md ## Release Criteria","fallback":"- Release when automated verification and required manual checks pass."},{"heading":"Stakeholder Review & Approval","enabled":true|false,"template":"- Product owner approval pending for {phase_name}."}]}}' +gsd_run query config-new-project '{"mode":"yolo","granularity":"[selected]","parallelization":true|false,"commit_docs":true|false,"model_profile":"quality|balanced|budget|adaptive|inherit","workflow":{"research":true|false,"plan_check":true|false,"verifier":true|false,"nyquist_validation":true|false,"auto_advance":true},"plan_review":{"source_grounding":true|false},"ship":{"pr_body_sections":[{"heading":"User Stories & Acceptance Criteria","enabled":true|false,"source":"REQUIREMENTS.md ## User Stories || REQUIREMENTS.md ## Acceptance Criteria","fallback":"- Acceptance criteria are covered by the linked requirements and verification evidence."},{"heading":"Risks & Dependencies","enabled":true|false,"source":"PLAN.md ## Risks || PLAN.md ## Dependencies","fallback":"- No known high-risk rollout dependencies."},{"heading":"Success Metrics & Release Criteria","enabled":true|false,"source":"REQUIREMENTS.md ## Definition of Done || VERIFICATION.md ## Release Criteria","fallback":"- Release when automated verification and required manual checks pass."},{"heading":"Stakeholder Review & Approval","enabled":true|false,"template":"- Product owner approval pending for {phase_name}."}]}}' ``` **If commit_docs = No:** Add `.planning/` to `.gitignore`. @@ -745,19 +778,52 @@ questions: [ { label: "Yes (Recommended)", description: "Confirm deliverables match phase goals" }, { label: "No", description: "Trust execution, skip verification" } ] - }, + } +] + +// Model profile uses a two-question split because AskUserQuestion enforces a hard +// 4-option cap and there are 5 valid profiles (quality, balanced, budget, adaptive, +// inherit). Q1 routes between adaptive/standard-tier/inherit; Q2 (shown only when +// Q1 = "Standard tier…") picks among the three standard profiles. Mirrors the +// /gsd:settings split (#3784, #1516). +questions: [ { header: "AI Models", question: "Which AI models for planning agents?", multiSelect: false, options: [ - { label: "Balanced (Recommended)", description: "Sonnet for most agents — good quality/cost ratio" }, - { label: "Quality", description: "Opus for research/roadmap — higher cost, deeper analysis" }, - { label: "Budget", description: "Haiku where possible — fastest, lowest cost" }, - { label: "Inherit", description: "Use the current session model for all agents (OpenCode /model)" } + { label: "Adaptive (Recommended)", description: "Role-based cost optimization: heavy roles use the highest-tier model available on the active runtime, light roles use the cheapest. Best balance of quality and cost across all supported runtimes (Claude, Codex, Gemini, OpenRouter, local)." }, + { label: "Standard tier…", description: "Choose Quality, Balanced, or Budget — flat tier applied to all agents" }, + { label: "Inherit", description: "Use the current session model for all agents (required for non-Claude runtimes: Codex, Gemini CLI, OpenCode /model, OpenRouter, local models)" } ] } ] + +**Conditional visibility — model_profile (Q2):** + Only ask this question when Q1's answer is "Standard tier…". + If Q1 = "Adaptive (Recommended)" → write model_profile=adaptive and SKIP Q2. + If Q1 = "Inherit" → write model_profile=inherit and SKIP Q2. + If user cancels Q2 after picking "Standard tier…" → leave existing model_profile value unchanged. + +questions: [ + { + question: "Which standard profile? (Quality / Balanced / Budget)", + header: "Model Tier", + multiSelect: false, + options: [ + { label: "Quality", description: "Opus everywhere except verification (highest cost) — Claude only" }, + { label: "Balanced", description: "Opus for planning, Sonnet for research/execution/verification — Claude only" }, + { label: "Budget", description: "Sonnet for writing, Haiku for research/verification (lowest cost) — Claude only" } + ] + } +] + +// Map UI choices → config values: +// Q1 "Adaptive (Recommended)" → model_profile = "adaptive" +// Q1 "Inherit" → model_profile = "inherit" +// Q1 "Standard tier…" + Q2 "Quality" → model_profile = "quality" +// Q1 "Standard tier…" + Q2 "Balanced" → model_profile = "balanced" +// Q1 "Standard tier…" + Q2 "Budget" → model_profile = "budget" ``` **PR body onboarding:** Ask which optional PRD-style sections `/gsd:ship` should append to generated PR bodies. Use the same `ship.pr_body_sections` mapping as Step 2a: selected sections get `enabled: true`, seeded-but-unselected sections get `enabled: false`, and selecting none writes an empty list. Prefer lean/agile PRD sections that make user value, acceptance criteria, Definition of Done, and stakeholder traceability explicit. @@ -773,7 +839,7 @@ Create `.planning/config.json` with all settings (CLI fills in remaining default ```bash mkdir -p .planning -gsd_run query config-new-project '{"mode":"[yolo|interactive]","granularity":"[selected]","parallelization":true|false,"commit_docs":true|false,"model_profile":"quality|balanced|budget|inherit","workflow":{"research":true|false,"plan_check":true|false,"verifier":true|false,"nyquist_validation":[false if granularity=coarse, true otherwise]},"plan_review":{"source_grounding":true|false},"ship":{"pr_body_sections":[{"heading":"User Stories & Acceptance Criteria","enabled":true|false,"source":"REQUIREMENTS.md ## User Stories || REQUIREMENTS.md ## Acceptance Criteria","fallback":"- Acceptance criteria are covered by the linked requirements and verification evidence."},{"heading":"Risks & Dependencies","enabled":true|false,"source":"PLAN.md ## Risks || PLAN.md ## Dependencies","fallback":"- No known high-risk rollout dependencies."},{"heading":"Success Metrics & Release Criteria","enabled":true|false,"source":"REQUIREMENTS.md ## Definition of Done || VERIFICATION.md ## Release Criteria","fallback":"- Release when automated verification and required manual checks pass."},{"heading":"Stakeholder Review & Approval","enabled":true|false,"template":"- Product owner approval pending for {phase_name}."}]}}' +gsd_run query config-new-project '{"mode":"[yolo|interactive]","granularity":"[selected]","parallelization":true|false,"commit_docs":true|false,"model_profile":"quality|balanced|budget|adaptive|inherit","workflow":{"research":true|false,"plan_check":true|false,"verifier":true|false,"nyquist_validation":[false if granularity=coarse, true otherwise]},"plan_review":{"source_grounding":true|false},"ship":{"pr_body_sections":[{"heading":"User Stories & Acceptance Criteria","enabled":true|false,"source":"REQUIREMENTS.md ## User Stories || REQUIREMENTS.md ## Acceptance Criteria","fallback":"- Acceptance criteria are covered by the linked requirements and verification evidence."},{"heading":"Risks & Dependencies","enabled":true|false,"source":"PLAN.md ## Risks || PLAN.md ## Dependencies","fallback":"- No known high-risk rollout dependencies."},{"heading":"Success Metrics & Release Criteria","enabled":true|false,"source":"REQUIREMENTS.md ## Definition of Done || VERIFICATION.md ## Release Criteria","fallback":"- Release when automated verification and required manual checks pass."},{"heading":"Stakeholder Review & Approval","enabled":true|false,"template":"- Product owner approval pending for {phase_name}."}]}}' ``` **Note:** Run `/gsd:settings` anytime to update model profile, workflow agents, branching strategy, and other preferences. diff --git a/tests/new-project-mvp-prompt.test.cjs b/tests/new-project-mvp-prompt.test.cjs index cb0e8e94c..67d281402 100644 --- a/tests/new-project-mvp-prompt.test.cjs +++ b/tests/new-project-mvp-prompt.test.cjs @@ -44,3 +44,127 @@ describe('new-project — MVP mode prompt', () => { assert.ok(contract.hasHorizontalStandardFallback, 'must specify fallback to standard template'); }); }); + +// Bug #1516 — folded into the new-project owning module test (new top-level bug-NNNN +// files are banned by lint-regression-test-names). /gsd-new-project's two AI Models +// prompts (Step 2a auto-mode + Step 5 interactive) enumerated only 4 profiles +// (Balanced/Quality/Budget/Inherit), omitting `adaptive` even though the model catalog +// (model-catalog.json profiles) and docs/CONFIGURATION.md register 5. The fix mirrors +// the #3784 two-question split already shipped for /gsd:settings. new-project.md has no +// tags, so blocks are located by the `header: "AI Models"` marker. + +describe('bug #1516: new-project AI Models prompt exposes all 5 model profiles', () => { + const content = fs.readFileSync(WORKFLOW, 'utf-8'); + + // Locate every `header: "AI Models"` AskUserQuestion block and grab a window large + // enough to include its conditional Q2 successor (the standard-tier picker). + function extractAiModelsBlocks(text) { + const blocks = []; + const headerRe = /header:\s*"AI Models"/g; + let m; + while ((m = headerRe.exec(text)) !== null) { + // Window from the header to the next ``` fence (closes the AskUserQuestion code block) + // or 60 lines, whichever comes first — captures Q1 + Q2 of the split. + const from = m.index; + const fenceAfter = text.indexOf('```', from + 1); + const windowEnd = fenceAfter === -1 ? from + 60 * 80 : Math.min(fenceAfter + 3, from + 60 * 80); + blocks.push(text.slice(from, windowEnd)); + } + return blocks; + } + + function labelsIn(block) { + const out = []; + const re = /label:\s*"([^"]+)"/g; + let mm; + while ((mm = re.exec(block)) !== null) out.push(mm[1].toLowerCase()); + return out; + } + + const aiModelsBlocks = extractAiModelsBlocks(content); + + test('new-project has at least two AI Models prompts (Step 2a auto + Step 5 interactive)', () => { + assert.ok( + aiModelsBlocks.length >= 2, + `expected ≥2 AI Models prompts (auto-mode + interactive), found ${aiModelsBlocks.length}`, + ); + }); + + test('each AI Models prompt makes adaptive reachable (#1516 — was omitted entirely)', () => { + assert.ok(aiModelsBlocks.length > 0, 'must find at least one AI Models block to assert against'); + for (let i = 0; i < aiModelsBlocks.length; i++) { + const labels = labelsIn(aiModelsBlocks[i]); + assert.ok( + labels.some(l => l === 'adaptive' || l.startsWith('adaptive')), + `AI Models prompt #${i + 1} must include an "Adaptive" option (the #1516 regression — adaptive was missing). Got labels: [${labels.join(', ')}]`, + ); + } + }); + + test('all 5 model profiles are reachable across the new-project model-selection surface', () => { + const surface = aiModelsBlocks.join('\n'); + const labels = labelsIn(surface); + for (const profile of ['adaptive', 'quality', 'balanced', 'budget', 'inherit']) { + assert.ok( + labels.some(l => l === profile || l.startsWith(profile)), + `model profile "${profile}" must be reachable as a selectable option in the AI Models prompts. Got labels: [${labels.join(', ')}]`, + ); + } + }); + + test('no options array in new-project.md exceeds the 4-option AskUserQuestion runtime cap', () => { + // Guards against a naive single 5-option block (which the AskUserQuestion runtime rejects). + const CAP = 4; + const optionsKeyRe = /\boptions\s*:\s*\[/g; + let match; + let questionIndex = 0; + let offender = null; + while ((match = optionsKeyRe.exec(content)) !== null) { + questionIndex++; + let depth = 0; + const start = match.index + match[0].length - 1; + let end = start; + for (let k = start; k < content.length; k++) { + if (content[k] === '[') depth++; + else if (content[k] === ']') { depth--; if (depth === 0) { end = k; break; } } + } + const optionsBody = content.slice(start, end + 1); + const labelMatches = optionsBody.match(/label:\s*"[^"]+"/g) || []; + if (labelMatches.length > CAP) { offender = { questionIndex, count: labelMatches.length }; break; } + } + assert.ok( + !offender, + offender + ? `options array #${offender.questionIndex} has ${offender.count} options — exceeds the AskUserQuestion runtime cap of ${CAP}. Split into multiple questions (as #3784 did for model_profile).` + : true, + ); + assert.ok(questionIndex > 0, 'new-project.md must contain at least one AskUserQuestion options array'); + }); + + test('both config-new-project example payloads list adaptive in the model_profile enum', () => { + // The two example payloads (Step 2a + Step 5) hard-coded "quality|balanced|budget|inherit" + // and must now include adaptive. + const enumRe = /model_profile"\s*:\s*"([^"]*)"/g; + let match; + const enums = []; + while ((match = enumRe.exec(content)) !== null) { + enums.push(match[1]); + } + assert.ok(enums.length >= 2, `expected >=2 config-new-project example payloads, found ${enums.length}`); + for (let i = 0; i < enums.length; i++) { + assert.ok( + enums[i].includes('adaptive'), + `config-new-project example payload #${i + 1} model_profile enum must include "adaptive". Got: "${enums[i]}"`, + ); + } + }); + + test('new-project.md has balanced braces (regression guard, mirrors #3784 bd53925f)', () => { + let depth = 0; + for (const ch of content) { + if (ch === '{') depth++; + if (ch === '}') depth--; + } + assert.strictEqual(depth, 0, `new-project.md has unbalanced braces: net depth ${depth}`); + }); +}); diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index 0ce71782c..1ed65362b 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -45,7 +45,7 @@ "milestone-summary.md": 11774, "mvp-phase.md": 13582, "new-milestone.md": 32422, - "new-project.md": 62324, + "new-project.md": 66138, "new-workspace.md": 11254, "next.md": 20094, "node-repair.md": 4173,