diff --git a/src/headless-reflect.ts b/src/headless-reflect.ts index 98f12a3bf..fac9bca46 100644 --- a/src/headless-reflect.ts +++ b/src/headless-reflect.ts @@ -91,6 +91,10 @@ export async function handleReflect( let mod: { assembleReflectionCorpus: (basePath: string) => unknown; renderReflectionCorpusBrief: (corpus: unknown) => string; + renderReflectionPrompt: ( + corpus: unknown, + options?: { workingDirectory?: string }, + ) => Promise; writeReflectionReport: (basePath: string, content: string) => string | null; runGeminiReflection: ( prompt: string, @@ -138,32 +142,21 @@ export async function handleReflect( return { exitCode: 0 }; } - let promptTemplate: string; + // renderReflectionPrompt routes through reflection.js → loadPrompt so the + // gap-audit detector sees the call site in EXTENSION_SRC and won't flag + // the prompt as orphan (sf-mp4warqc-y1u0b3). It also picks up fragment + // composition + variable validation for free. + let rendered: string; try { - const fs = await import("node:fs/promises"); - const templatePath = useAgentDir - ? join(agentExtensionsDir, "prompts", "reflection-pass.md") - : resolveBundledSourceResource( - import.meta.url, - "extensions", - "sf", - "prompts", - "reflection-pass.md", - ); - promptTemplate = await fs.readFile(templatePath, "utf-8"); + rendered = await mod.renderReflectionPrompt(corpus, { + workingDirectory: cwd, + }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); - process.stderr.write(`[reflect] prompt template load failed: ${msg}\n`); + process.stderr.write(`[reflect] prompt render failed: ${msg}\n`); return { exitCode: 1 }; } - const brief = mod.renderReflectionCorpusBrief(corpus); - // Inline-replace {{corpus}} in the template. We do NOT run the full - // loadPrompt fragment-resolver here — the operator just needs a clean - // rendered prompt to pipe into a model. The full template path runs - // inside SF when reflection-pass becomes a real unit type. - const rendered = promptTemplate.replace("{{corpus}}", brief); - if (!options.run) { process.stdout.write(`${rendered}\n`); return { exitCode: 0 }; diff --git a/src/resources/extensions/sf/reflection.js b/src/resources/extensions/sf/reflection.js index 7c203e0d3..2cbdb8d3d 100644 --- a/src/resources/extensions/sf/reflection.js +++ b/src/resources/extensions/sf/reflection.js @@ -347,6 +347,28 @@ export function writeReflectionReport(basePath, content) { const REFLECTION_TERMINATOR = "REFLECTION_COMPLETE"; +/** + * Render the reflection-pass prompt template with a corpus brief. + * + * Routes through the canonical loadPrompt path so: + * - fragment composition ({{include:working-directory}}) resolves + * - variable validation catches missing values at render time + * - the gap-audit detector sees a real loadPrompt("reflection-pass") + * call site in extension source (sf-mp4warqc-y1u0b3 — orphan flag + * would otherwise fire because grep only scans EXTENSION_SRC). + * + * Consumer: headless-reflect operator surface; future autonomous-loop + * reflection unit handler. + */ +export async function renderReflectionPrompt(corpus, options = {}) { + const brief = renderReflectionCorpusBrief(corpus); + const { loadPrompt } = await import("./prompt-loader.js"); + return loadPrompt("reflection-pass", { + corpus: brief, + workingDirectory: options.workingDirectory ?? process.cwd(), + }); +} + /** * Default provider/model used when --model is not supplied. This is the * single point of model defaulting — the rest of runReflection is