* test(#3936): quick research dispatch must use researcher persona/model Regression tests for the planner-persona leak in quick's Step 4.75 research dispatch: init quick emits researcher_model, the workflow resolves AGENT_SKILLS_RESEARCHER and parses researcher_model, and the researcher dispatch uses the researcher persona/tier while planner and executor dispatches keep their own. * fix(#3936): dispatch quick research with researcher persona and model cmdInitQuick now emits researcher_model (parity with the plan-phase init), quick.md resolves AGENT_SKILLS_RESEARCHER and parses the new field, and Step 4.75's dispatch swaps ${AGENT_SKILLS_PLANNER}/ {planner_model} for the researcher's own persona and tier. The #2517 model-omission list gains researcher_model. Emitted-Drift-Ack-Growth: quick.md — researcher persona resolution line + researcher_model parse-list entry (#3936) * chore(#3936): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/silly-pandas-frolic.md
Normal file
5
.changeset/silly-pandas-frolic.md
Normal file
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,7 @@ Spawn a single focused researcher (not 4 parallel researchers like full phases
|
||||
|
||||
<!-- #2517 model-omit-on-inherit -->
|
||||
|
||||
> **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.
|
||||
|
||||
<!-- #2508 runtime-aware-dispatch -->
|
||||
|
||||
@@ -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)' : ''}
|
||||
</required_reading>
|
||||
|
||||
${AGENT_SKILLS_PLANNER}
|
||||
${AGENT_SKILLS_RESEARCHER}
|
||||
|
||||
</research_context>
|
||||
|
||||
@@ -56,7 +56,7 @@ Return: ## RESEARCH COMPLETE with file path
|
||||
</output>
|
||||
",
|
||||
subagent_type="gsd-phase-researcher",
|
||||
model="{planner_model}",
|
||||
model="{researcher_model}",
|
||||
description="Research: ${DESCRIPTION}"
|
||||
)
|
||||
```
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user