fix(#3936): dispatch quick research with researcher persona and model (#4158)

* 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:
Tom Boucher
2026-09-01 19:40:34 -04:00
committed by GitHub
parent eca9c2b590
commit b6dd4e2e74
6 changed files with 102 additions and 4 deletions

View File

@@ -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}`);

View File

@@ -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(