diff --git a/.changeset/silly-pandas-frolic.md b/.changeset/silly-pandas-frolic.md new file mode 100644 index 000000000..0c5d1fdea --- /dev/null +++ b/.changeset/silly-pandas-frolic.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 4158 +--- +**`/gsd-quick` research dispatch uses the researcher persona and model tier** — the quick flow's research step no longer injects the planner persona and planner model into `gsd-phase-researcher`; `init quick` now emits `researcher_model` and the workflow resolves `AGENT_SKILLS_RESEARCHER`, matching `/gsd-plan-phase`. (#3936) diff --git a/gsd-core/workflows/quick.md b/gsd-core/workflows/quick.md index 225b05383..b71d5ec75 100644 --- a/gsd-core/workflows/quick.md +++ b/gsd-core/workflows/quick.md @@ -127,9 +127,10 @@ AGENT_SKILLS_PLANNER=$(gsd_run query agent-skills gsd-planner) AGENT_SKILLS_EXECUTOR=$(gsd_run query agent-skills gsd-executor) AGENT_SKILLS_CHECKER=$(gsd_run query agent-skills gsd-plan-checker) AGENT_SKILLS_VERIFIER=$(gsd_run query agent-skills gsd-verifier) +AGENT_SKILLS_RESEARCHER=$(gsd_run query agent-skills gsd-phase-researcher) ``` -Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `reviewer_model`, `commit_docs`, `branch_name`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`, `response_language`. +Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `reviewer_model`, `researcher_model`, `commit_docs`, `branch_name`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`, `response_language`. `init.quick` does not emit dedicated `state_path`/`project_path` fields, so derive them from the already-absolute `quick_dir` (#2376 — files handed to a spawned subagent must resolve regardless of that subagent's own cwd): ```bash diff --git a/gsd-core/workflows/quick/steps/research-phase.md b/gsd-core/workflows/quick/steps/research-phase.md index 51948b224..a45eb3d26 100644 --- a/gsd-core/workflows/quick/steps/research-phase.md +++ b/gsd-core/workflows/quick/steps/research-phase.md @@ -13,7 +13,7 @@ Spawn a single focused researcher (not 4 parallel researchers like full phases -> **Model omission (#2517).** Omit the `model` parameter entirely when the value it would carry (`planner_model`, `checker_model`, `executor_model`, `reviewer_model`, `verifier_model`) is `"inherit"` or empty. An empty value 404s on runtimes without native tier aliases — the default on non-Claude runtimes. Omitting it inherits the orchestrator's model. See @gsd-core/references/model-profile-resolution.md. +> **Model omission (#2517).** Omit the `model` parameter entirely when the value it would carry (`planner_model`, `checker_model`, `executor_model`, `reviewer_model`, `verifier_model`, `researcher_model`) is `"inherit"` or empty. An empty value 404s on runtimes without native tier aliases — the default on non-Claude runtimes. Omitting it inherits the orchestrator's model. See @gsd-core/references/model-profile-resolution.md. @@ -35,7 +35,7 @@ Agent( ${DISCUSS_MODE ? '- ' + QUICK_DIR + '/' + quick_id + '-CONTEXT.md (User decisions — research should align with these. #3894: when workflow.research_before_questions is enabled research runs BEFORE discussion, so this file will not exist yet — read it only if present)' : ''} -${AGENT_SKILLS_PLANNER} +${AGENT_SKILLS_RESEARCHER} @@ -56,7 +56,7 @@ Return: ## RESEARCH COMPLETE with file path ", subagent_type="gsd-phase-researcher", - model="{planner_model}", + model="{researcher_model}", description="Research: ${DESCRIPTION}" ) ``` diff --git a/src/init.cts b/src/init.cts index ed9e75cd3..db9004c4a 100644 --- a/src/init.cts +++ b/src/init.cts @@ -1467,6 +1467,10 @@ function cmdInitQuick( executor_model: resolveModelInternal(cwd, 'gsd-executor'), checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'), verifier_model: resolveModelInternal(cwd, 'gsd-verifier'), + // #3936: Step 4.75 dispatches gsd-phase-researcher; resolve its own tier + // (parity with cmdInitPlanPhase) so the research spawn stops pinning + // planner_model. + researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'), // #2072: the quick review step spawns gsd-code-reviewer; resolve its own model // so model_overrides / models.verification apply (was reusing executor_model). reviewer_model: resolveModelInternal(cwd, 'gsd-code-reviewer'), diff --git a/tests/init.test.cjs b/tests/init.test.cjs index 90211000b..f9b36bc47 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -1951,6 +1951,32 @@ describe('cmdInitQuick', () => { cleanup(tmpDir); }); + test('init quick emits researcher_model', () => { + // #3936: the quick research step (Step 4.75) dispatches gsd-phase-researcher, + // so init quick must resolve the researcher's own model tier — parity with + // init plan-phase (which emits researcher_model for the same agent). + const result = runGsdTools('init quick "Fix login bug"', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok('researcher_model' in output, + 'init quick should emit researcher_model for the Step 4.75 research dispatch'); + }); + + test('init quick resolves researcher_model from model_overrides', () => { + fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({ + model_profile: 'balanced', + model_overrides: { 'gsd-phase-researcher': 'openai/o4-mini' }, + })); + + const result = runGsdTools('init quick "Fix login bug" --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.researcher_model, 'openai/o4-mini', + 'model_overrides["gsd-phase-researcher"] must reach init quick\'s researcher_model'); + }); + test('with description generates slug and task_dir with YYMMDD-xxx format', () => { const result = runGsdTools('init quick "Fix login bug"', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); diff --git a/tests/quick-research.test.cjs b/tests/quick-research.test.cjs index c6987cdd4..818f4744b 100644 --- a/tests/quick-research.test.cjs +++ b/tests/quick-research.test.cjs @@ -129,6 +129,68 @@ describe('quick workflow: research step', () => { ); }); + test('research step injects the researcher persona, not the planner persona', () => { + // #3936: the dispatch targets gsd-phase-researcher, so the persona riding the + // prompt and the model tier must be the researcher's own — not the planner's. + content = expandWorkflowSections(workflowPath); + const researchSection = content.substring( + content.indexOf('Step 4.75'), + content.indexOf('Step 5:') + ); + assert.ok( + researchSection.includes('${AGENT_SKILLS_RESEARCHER}'), + 'research step should inject the gsd-phase-researcher persona' + ); + assert.ok( + !researchSection.includes('${AGENT_SKILLS_PLANNER}'), + 'research step must not inject the planner persona into a researcher dispatch' + ); + assert.ok( + researchSection.includes('model="{researcher_model}"'), + 'research step should pin the researcher model tier, not planner_model' + ); + }); + + test('quick workflow resolves the researcher persona', () => { + // #3936: AGENT_SKILLS_RESEARCHER must be resolved in quick.md's Step 2 block, + // the way plan-phase.md does, or the Step 4.75 interpolation expands empty. + const quickMd = fs.readFileSync(path.join(WORKFLOWS_DIR, 'quick.md'), 'utf8'); + assert.ok( + quickMd.includes('AGENT_SKILLS_RESEARCHER=$(gsd_run query agent-skills gsd-phase-researcher)'), + 'quick.md should resolve AGENT_SKILLS_RESEARCHER from agent-skills' + ); + }); + + test('quick workflow parses researcher_model from init quick', () => { + const quickMd = fs.readFileSync(path.join(WORKFLOWS_DIR, 'quick.md'), 'utf8'); + const parseList = quickMd.substring( + quickMd.indexOf('Parse JSON for:'), + quickMd.indexOf('```', quickMd.indexOf('Parse JSON for:')) + ); + assert.ok( + parseList.includes('researcher_model'), + 'quick.md parse list should include researcher_model' + ); + }); + + test('planner and executor dispatches keep their own personas', () => { + // Negative space: #3936 touches only the research dispatch — the planner and + // executor spawns must keep their own personas and model tiers. + content = expandWorkflowSections(workflowPath); + const plannerSection = content.substring( + content.indexOf('Step 5: Spawn planner'), + content.indexOf('Step 5.5') + ); + assert.ok( + plannerSection.includes('${AGENT_SKILLS_PLANNER}'), + 'planner dispatch should still inject the planner persona' + ); + assert.ok( + content.includes('${AGENT_SKILLS_EXECUTOR}'), + 'executor dispatch should still inject the executor persona' + ); + }); + test('research step writes RESEARCH.md', () => { content = expandWorkflowSections(workflowPath); const researchSection = content.substring(