fix(#3894): quick path honors workflow.research_before_questions; key resolves from global defaults (#4047)
* test(#3894): research_before_questions must resolve globally and order quick.md (failing first) * fix(#3894): quick path honors workflow.research_before_questions; key resolves from global defaults Two layers, one key. The quick workflow ran its discussion phase before its research phase unconditionally — neither quick.md nor its steps ever read workflow.research_before_questions, though the key is documented, schema-registered, /gsd-settings-writable, and honored by /gsd-discuss-phase and /gsd-new-project. A gray-area answer given without research is then written to <quick_id>-CONTEXT.md as a locked decision downstream agents are told not to revisit — an evidence-free choice made unfalsifiable (the reporter's #3714 misresolution). - quick.md Step 4 now carries the same research-before-questions check the two honoring paths make: when enabled, research-phase executes before discussion-phase; false/unset keeps the written order. Both sections stay section-manifest gated. - src/config-loader.cts forwarded workflow.post_planning_gaps from ~/.gsd/defaults.json but silently dropped this key — same file, same nesting, one resolved and one didn't. Now forwarded with the same flat + nested-alias fallback shape, added to the resolution-keys lockstep canary and the #3532 shadowed-warning set (nested alias reporting generalized over both keys). Emitted-Drift-Ack-Growth: quick.md — #3894: +Step 4 ordering rule (the research-before-questions check the discuss-phase and new-project paths already make); a real behavioral gate, not incidental bloat. * fix(#3894): review fold-ins — gate the CONTEXT.md reference, colon slash-forms - quick/steps/research-phase.md directed the researcher subagent to read <quick_id>-CONTEXT.md under DISCUSS_MODE with no existence hedge — but under the new ordering (research BEFORE discussion) the file cannot exist yet when the researcher is dispatched. The reference now says read-only-if-present with the #3894 reason; the alignment purpose still applies on the default ordering. - quick.md's new rule used the hyphen slash forms (/gsd-discuss-phase, /gsd-new-project); source artifacts under gsd-core/workflows must author the colon form the install-time converters key on — the same file already uses /gsd:new-project and /gsd:quick elsewhere. * docs(#3894): planning-config row names the flat CONFIG_DEFAULTS alias config-field-docs requires every CONFIG_DEFAULTS key to appear in the doc; the row documented the canonical namespaced form only. Adds the same alias sentence post_planning_gaps's row carries, plus the #3894 quick-path note. * chore(#3894): changeset fragment (pr number backfilled after PR creation) * chore(#3894): backfill changeset PR number (4047) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -1299,7 +1299,7 @@ describe('#2997: phase_id_convention is not silently dropped on a clean read', (
|
||||
// would be false for the channel users control via effort sync.
|
||||
const GLOBAL_KEYS_SHADOWED_UNDER_PROJECT = [
|
||||
'model_profile', 'commit_docs', 'research', 'plan_checker', 'verifier',
|
||||
'nyquist_validation', 'post_planning_gaps', 'parallelization', 'text_mode',
|
||||
'nyquist_validation', 'post_planning_gaps', 'research_before_questions', 'parallelization', 'text_mode',
|
||||
'resolve_model_ids', 'context_window', 'subagent_timeout', 'model_overrides',
|
||||
'models', 'granularity', 'granularities', 'planning', 'dynamic_routing',
|
||||
'fast_mode', 'agent_skills', 'response_language', 'runtime',
|
||||
@@ -1601,3 +1601,75 @@ describe('regressions — #3760 loader never expands or persists a non-object se
|
||||
assert.equal(config.branching_strategy, 'none', 'the hoisted value still resolves');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #3894: workflow.research_before_questions resolves from global defaults ──
|
||||
|
||||
describe('#3894 research_before_questions global-defaults forwarding', () => {
|
||||
test('nested workflow.research_before_questions in ~/.gsd/defaults.json resolves', () => {
|
||||
const homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3894-home-'));
|
||||
const origGsdHome = process.env['GSD_HOME'];
|
||||
try {
|
||||
const gsdDir = path.join(homeTmp, '.gsd');
|
||||
fs.mkdirSync(gsdDir, { recursive: true });
|
||||
// The reporter's exact shape: same nesting as the forwarded
|
||||
// workflow.post_planning_gaps, one resolves and one did not.
|
||||
fs.writeFileSync(path.join(gsdDir, 'defaults.json'), JSON.stringify({
|
||||
workflow: { post_planning_gaps: true, research_before_questions: true },
|
||||
}), 'utf-8');
|
||||
process.env['GSD_HOME'] = homeTmp;
|
||||
const noPlanning = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3894-noplanning-'));
|
||||
try {
|
||||
const result = loadConfigResolved(noPlanning);
|
||||
assert.equal(result.config.post_planning_gaps, true, 'control: the already-forwarded key resolves');
|
||||
assert.equal(
|
||||
result.config.research_before_questions, true,
|
||||
'#3894: same file, same nesting — the quick-path key must resolve too, not just post_planning_gaps'
|
||||
);
|
||||
} finally {
|
||||
cleanup(noPlanning);
|
||||
}
|
||||
} finally {
|
||||
if (origGsdHome === undefined) delete process.env['GSD_HOME'];
|
||||
else process.env['GSD_HOME'] = origGsdHome;
|
||||
cleanup(homeTmp);
|
||||
}
|
||||
});
|
||||
|
||||
test('flat top-level research_before_questions in ~/.gsd/defaults.json also resolves (Branch D alias parity)', () => {
|
||||
const homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3894b-home-'));
|
||||
const origGsdHome = process.env['GSD_HOME'];
|
||||
try {
|
||||
const gsdDir = path.join(homeTmp, '.gsd');
|
||||
fs.mkdirSync(gsdDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(gsdDir, 'defaults.json'), JSON.stringify({ research_before_questions: true }), 'utf-8');
|
||||
process.env['GSD_HOME'] = homeTmp;
|
||||
const noPlanning = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3894b-noplanning-'));
|
||||
try {
|
||||
const result = loadConfigResolved(noPlanning);
|
||||
assert.equal(result.config.research_before_questions, true);
|
||||
} finally {
|
||||
cleanup(noPlanning);
|
||||
}
|
||||
} finally {
|
||||
if (origGsdHome === undefined) delete process.env['GSD_HOME'];
|
||||
else process.env['GSD_HOME'] = origGsdHome;
|
||||
cleanup(homeTmp);
|
||||
}
|
||||
});
|
||||
|
||||
test('unset resolves to the documented default (false), never undefined', () => {
|
||||
const noPlanning = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3894c-'));
|
||||
const homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3894c-home-'));
|
||||
const origGsdHome = process.env['GSD_HOME'];
|
||||
try {
|
||||
process.env['GSD_HOME'] = homeTmp; // no .gsd/defaults.json — builtin defaults
|
||||
const result = loadConfigResolved(noPlanning);
|
||||
assert.equal(result.config.research_before_questions, false, 'CANONICAL_CONFIG_DEFAULTS.workflow.research_before_questions is false');
|
||||
} finally {
|
||||
if (origGsdHome === undefined) delete process.env['GSD_HOME'];
|
||||
else process.env['GSD_HOME'] = origGsdHome;
|
||||
cleanup(homeTmp);
|
||||
cleanup(noPlanning);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -324,3 +324,36 @@ describe('quick workflow: banner variants for flag combinations', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #3894: quick.md honors workflow.research_before_questions ───────────────
|
||||
|
||||
describe('#3894 quick.md research-before-questions ordering', () => {
|
||||
const QUICK = fs.readFileSync(path.join(WORKFLOWS_DIR, 'quick.md'), 'utf-8');
|
||||
|
||||
test('quick.md reads workflow.research_before_questions', () => {
|
||||
assert.ok(
|
||||
QUICK.includes('research_before_questions'),
|
||||
'#3894: the quick path must read the documented key — before this fix neither quick.md nor its steps referenced it'
|
||||
);
|
||||
});
|
||||
|
||||
test('when enabled, research-phase is ordered BEFORE discussion-phase', () => {
|
||||
const orderingIdx = QUICK.indexOf('research_before_questions');
|
||||
assert.ok(orderingIdx !== -1, 'key referenced');
|
||||
const ruleWindow = QUICK.slice(orderingIdx, orderingIdx + 900);
|
||||
assert.ok(
|
||||
/research[- ]phase[^]{0,200}(before|prior to|first)[^]{0,120}discussion/i.test(ruleWindow)
|
||||
|| /when[^]{0,60}(true|enabled)[^]{0,300}research/i.test(ruleWindow),
|
||||
'the ordering rule must state research runs before discussion when the key is true'
|
||||
);
|
||||
assert.ok(
|
||||
ruleWindow.includes('false') || ruleWindow.includes('unset') || ruleWindow.toLowerCase().includes('unchanged'),
|
||||
'and must keep the written order when false/unset'
|
||||
);
|
||||
});
|
||||
|
||||
test('both step sections remain section-manifest gated (no structural regression)', () => {
|
||||
assert.ok(/gsd:section id="discussion-phase"/.test(QUICK), 'discussion-phase section marker intact');
|
||||
assert.ok(/gsd:section id="research-phase"/.test(QUICK), 'research-phase section marker intact');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user