refactor(reflect): route reflection-pass through loadPrompt in extension

Move the loadPrompt("reflection-pass") call site from headless-reflect.ts
into a new renderReflectionPrompt helper in reflection.js. gap-audit
greps EXTENSION_SRC for loadPrompt call sites; without a hit there it
flagged the prompt as orphan even though the headless surface was using
it (sf-mp4warqc-y1u0b3).

Side benefits: fragment composition + variable validation now run via
the canonical path instead of the prior raw fs.readFile + string
substitution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-14 06:20:38 +02:00
parent 639dcde717
commit 48e793c003
2 changed files with 35 additions and 20 deletions

View file

@ -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<string>;
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 };

View file

@ -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