From 2fafbd2753d0911fa2e5c681240b45ae2a750195 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Sat, 25 Apr 2026 14:43:00 -0600 Subject: [PATCH] fix(sdk): pass phaseDir to executor prompt so SUMMARY.md lands in .planning/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- sdk/src/index.ts | 1 + sdk/src/phase-prompt.ts | 3 ++- sdk/src/phase-runner.ts | 2 +- sdk/src/prompt-builder.test.ts | 16 ++++++++++++++-- sdk/src/prompt-builder.ts | 31 ++++++++++++++++++++++++++++--- sdk/src/session-runner.ts | 3 ++- 6 files changed, 48 insertions(+), 8 deletions(-) diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 9a9332a4d..898bbc31a 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -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 diff --git a/sdk/src/phase-prompt.ts b/sdk/src/phase-prompt.ts index 2d4d0fdd0..cb6faa254 100644 --- a/sdk/src/phase-prompt.ts +++ b/sdk/src/phase-prompt.ts @@ -110,11 +110,12 @@ export class PromptFactory { phaseType: PhaseType, plan: ParsedPlan | null, contextFiles: ContextFiles, + phaseDir?: string, ): Promise { // 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): diff --git a/sdk/src/phase-runner.ts b/sdk/src/phase-runner.ts index 1ce27ee0f..7a9847107 100644 --- a/sdk/src/phase-runner.ts +++ b/sdk/src/phase-runner.ts @@ -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, diff --git a/sdk/src/prompt-builder.test.ts b/sdk/src/prompt-builder.test.ts index a0ca7a238..26ff4fdd7 100644 --- a/sdk/src/prompt-builder.test.ts +++ b/sdk/src/prompt-builder.test.ts @@ -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', () => { diff --git a/sdk/src/prompt-builder.ts b/sdk/src/prompt-builder.ts index f5c8c30d1..75683c22b 100644 --- a/sdk/src/prompt-builder.ts +++ b/sdk/src/prompt-builder.ts @@ -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` + diff --git a/sdk/src/session-runner.ts b/sdk/src/session-runner.ts index 693a5927b..ed7aac3af 100644 --- a/sdk/src/session-runner.ts +++ b/sdk/src/session-runner.ts @@ -59,9 +59,10 @@ export async function runPlanSession( agentDef?: string, eventStream?: GSDEventStream, streamContext?: EventStreamContext, + phaseDir?: string, ): Promise { // 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 ??