fix(phase-runner): add research gate to block planning on unresolved open questions (#1618)
* chore: add v1.31.0 npm known-issue notice to issue template config Adds a top-priority contact link to the issue template chooser so users are redirected to the Discussions announcement before opening a duplicate issue about v1.31.0 not being on npm. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(phase-runner): add research gate to block planning on unresolved open questions (#1602) Plan-phase could proceed to planning even when RESEARCH.md had unresolved open questions in its ## Open Questions section. This caused agents to plan and execute with fundamental design decisions still undecided. - Add `research-gate.ts` with pure `checkResearchGate()` function that parses RESEARCH.md for unresolved open questions - Integrate gate into PhaseRunner between research (step 2) and plan (step 3) using existing `invokeBlockerCallback` pattern - Add Dimension 11 (Research Resolution) to gsd-plan-checker.md agent - Gate passes when: no Open Questions section, section has (RESOLVED) suffix, all individual questions marked RESOLVED, or section is empty - Gate fires `onBlockerDecision` callback with PhaseStepType.Research and lists the unresolved questions in the error message - Auto-approves (skip) when no callback registered (headless mode) - 18 new tests: 13 unit tests for checkResearchGate, 5 integration tests for PhaseRunner research gate behavior Closes #1602 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,9 @@ import type { PromptFactory } from './phase-prompt.js';
|
||||
import type { ContextEngine } from './context-engine.js';
|
||||
import type { GSDLogger } from './logger.js';
|
||||
import { runPhaseStepSession, runPlanSession } from './session-runner.js';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { checkResearchGate } from './research-gate.js';
|
||||
|
||||
// ─── Error type ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -185,6 +188,21 @@ export class PhaseRunner {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 2.5: Research gate (#1602) ──
|
||||
// Check RESEARCH.md for unresolved open questions before planning
|
||||
if (!halted && phaseOp.has_research) {
|
||||
const gateResult = await this.checkResearchGate(phaseOp);
|
||||
if (!gateResult.pass) {
|
||||
const questionList = gateResult.unresolvedQuestions.join(', ');
|
||||
const error = `RESEARCH.md has unresolved open questions: ${questionList}`;
|
||||
this.logger?.warn(error, { phase: phaseNumber });
|
||||
const decision = await this.invokeBlockerCallback(callbacks, phaseNumber, PhaseStepType.Research, error);
|
||||
if (decision === 'stop') {
|
||||
halted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 3: Plan ──
|
||||
if (!halted) {
|
||||
const result = await this.retryOnce('plan', () => this.runStep(PhaseStepType.Plan, phaseNumber, sessionOpts));
|
||||
@@ -1076,6 +1094,22 @@ export class PhaseRunner {
|
||||
return 'gaps_found';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check RESEARCH.md for unresolved open questions (#1602).
|
||||
* Returns the gate result — pass means safe to proceed to planning.
|
||||
*/
|
||||
private async checkResearchGate(phaseOp: PhaseOpInfo): Promise<{ pass: boolean; unresolvedQuestions: string[] }> {
|
||||
try {
|
||||
const researchPath = phaseOp.research_path ||
|
||||
join(phaseOp.phase_dir, `${phaseOp.padded_phase}-RESEARCH.md`);
|
||||
const content = await readFile(researchPath, 'utf-8');
|
||||
return checkResearchGate(content);
|
||||
} catch {
|
||||
// File doesn't exist or can't be read — pass (nothing to gate on)
|
||||
return { pass: true, unresolvedQuestions: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the onBlockerDecision callback, falling back to auto-approve.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user