fix(sdk): pass phaseDir to executor prompt so SUMMARY.md lands in .planning/

The SDK's buildExecutorPrompt told executors to "Create a SUMMARY.md file"
with no directory path, causing them to write it in cwd (project root)
instead of .planning/phases/{phase}/. Thread phaseDir from PhaseRunner
through PromptFactory and into the completion instructions so the executor
gets an explicit path like `.planning/phases/01-auth/01-01-SUMMARY.md`.

Backward compatible — buildExecutorPrompt still accepts a plain string
(agentDef) for existing callers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lex Christopherson
2026-04-25 14:43:00 -06:00
parent 470c1a0bff
commit 2fafbd2753
6 changed files with 48 additions and 8 deletions

View File

@@ -296,6 +296,7 @@ export type { GSDConfig } from './config.js';
export { GSDTools, GSDToolsError, resolveGsdToolsPath } from './gsd-tools.js';
export { runPlanSession, runPhaseStepSession } from './session-runner.js';
export { buildExecutorPrompt, parseAgentTools } from './prompt-builder.js';
export type { ExecutorPromptOptions } from './prompt-builder.js';
export * from './types.js';
// S02: Event stream, context, prompt, and logging modules

View File

@@ -110,11 +110,12 @@ export class PromptFactory {
phaseType: PhaseType,
plan: ParsedPlan | null,
contextFiles: ContextFiles,
phaseDir?: string,
): Promise<string> {
// Execute phase with a plan: delegate to existing buildExecutorPrompt
if (phaseType === PhaseType.Execute && plan) {
const agentDef = await this.loadAgentDef(phaseType);
return sanitizePrompt(buildExecutorPrompt(plan, agentDef), this.projectDir);
return sanitizePrompt(buildExecutorPrompt(plan, { agentDef, phaseDir }), this.projectDir);
}
// Prompt assembly order is cache-optimized (#1614):

View File

@@ -790,7 +790,7 @@ export class PhaseRunner {
const phaseType = PhaseType.Execute;
const contextFiles = await this.contextEngine.resolveContextFiles(phaseType);
const prompt = await this.promptFactory.buildPrompt(phaseType, parsedPlan, contextFiles);
const prompt = await this.promptFactory.buildPrompt(phaseType, parsedPlan, contextFiles, phaseOp.phase_dir);
return await runPhaseStepSession(
prompt,

View File

@@ -156,11 +156,23 @@ describe('buildExecutorPrompt', () => {
expect(prompt).toContain('Refresh tokens rotate correctly');
});
it('includes SUMMARY.md creation instruction', () => {
it('includes SUMMARY.md creation instruction with derived filename', () => {
const plan = makePlan();
const prompt = buildExecutorPrompt(plan);
expect(prompt).toContain('01-01-SUMMARY.md');
});
it('includes phaseDir in SUMMARY path when provided', () => {
const plan = makePlan();
const prompt = buildExecutorPrompt(plan, { phaseDir: '.planning/phases/01-auth' });
expect(prompt).toContain('.planning/phases/01-auth/01-01-SUMMARY.md');
});
it('uses bare SUMMARY.md when phase/plan numbers missing', () => {
const plan = makePlan({ frontmatter: { phase: '', plan: '', type: 'execute', wave: 1, depends_on: [], files_modified: [], autonomous: true, requirements: [], must_haves: { truths: [], artifacts: [], key_links: [] } } });
const prompt = buildExecutorPrompt(plan);
expect(prompt).toContain('SUMMARY.md');
expect(prompt).toContain('Create a SUMMARY.md file');
expect(prompt).not.toContain('/SUMMARY.md');
});
it('includes sequential execution instruction', () => {

View File

@@ -87,6 +87,16 @@ function formatTask(task: PlanTask, index: number): string {
return lines.join('\n');
}
/**
* Options for buildExecutorPrompt beyond the required plan.
*/
export interface ExecutorPromptOptions {
/** Raw content of gsd-executor.md agent definition. */
agentDef?: string;
/** Phase directory relative to project root (e.g. `.planning/phases/01-auth`). */
phaseDir?: string;
}
/**
* Build the executor prompt from a parsed plan and optional agent definition.
*
@@ -97,10 +107,14 @@ function formatTask(task: PlanTask, index: number): string {
* 4. Produce a SUMMARY.md file on completion
*
* @param plan - Parsed plan structure from plan-parser
* @param agentDef - Raw content of gsd-executor.md agent definition (optional)
* @param agentDefOrOpts - Raw agent definition string (legacy) or ExecutorPromptOptions
* @returns Assembled prompt string
*/
export function buildExecutorPrompt(plan: ParsedPlan, agentDef?: string): string {
export function buildExecutorPrompt(plan: ParsedPlan, agentDefOrOpts?: string | ExecutorPromptOptions): string {
const opts: ExecutorPromptOptions = typeof agentDefOrOpts === 'string'
? { agentDef: agentDefOrOpts }
: agentDefOrOpts ?? {};
const { agentDef, phaseDir } = opts;
const sections: string[] = [];
// ── Role instructions from agent definition ──
@@ -173,11 +187,22 @@ export function buildExecutorPrompt(plan: ParsedPlan, agentDef?: string): string
}
// ── Completion instructions ──
// Derive the SUMMARY filename from plan frontmatter (e.g. "01-01-SUMMARY.md")
// Phase may be "01-auth" or "01" — extract leading number, zero-pad to 2 digits.
const phaseNum = (plan.frontmatter.phase || '').match(/^(\d+)/)?.[1] || '';
const planNum = (plan.frontmatter.plan || '').match(/^(\d+)/)?.[1] || '';
const summaryName = phaseNum && planNum
? `${phaseNum.padStart(2, '0')}-${planNum.padStart(2, '0')}-SUMMARY.md`
: 'SUMMARY.md';
const summaryPath = phaseDir
? `${phaseDir}/${summaryName}`
: summaryName;
sections.push(
`## Completion\n\n` +
`After all tasks are complete:\n` +
`1. Run any overall verification or success criteria checks\n` +
`2. Create a SUMMARY.md file documenting:\n` +
`2. Create \`${summaryPath}\` documenting:\n` +
` - One-line summary of what was accomplished\n` +
` - Tasks completed with commit hashes\n` +
` - Any deviations from the plan\n` +

View File

@@ -59,9 +59,10 @@ export async function runPlanSession(
agentDef?: string,
eventStream?: GSDEventStream,
streamContext?: EventStreamContext,
phaseDir?: string,
): Promise<PlanResult> {
// Build the executor prompt
const executorPrompt = buildExecutorPrompt(plan, agentDef);
const executorPrompt = buildExecutorPrompt(plan, { agentDef, phaseDir });
// Resolve allowed tools — from agent definition or defaults
const allowedTools = options?.allowedTools ??