/** * THE canonical prompt-segment discovery + override resolution (P2 * reconciliation, 2026-09-19). The agent layer owns it because composition * lives here: composeInstructions (teacher-agent.ts) composes through * effectivePromptSegments, and the discovery reads THIS layer's own tree * (src/agent/prompts — base, shared/, per-agent fragments) — the exact * rules the pre-P2 loader used, now in one place. * * The architecture law pins the layout: the agent layer never imports the * store adapters, so the override-file READ lives here too (config-anchored * static file, same discipline as reading the source fragments); the * override-file CRUD (write/delete/list — the mutating store behind the * core port) stays in src/store/prompt-overrides.ts. Both sides share the * pure path grammar (src/core/prompt-segments.ts). * * SEGMENTS: the SHARED base (src/agent/system-prompt.md), then every * numeric-prefix *.md fragment in the agent's own directory (sorted by * filename), then the shared/ fragments directory (composed by BOTH * agents as a trailing appendix — the 2026-09-27 reorder). The segment * list adapts to the code: an override keyed to a segment the source no * longer ships stops composing automatically. * * OVERRIDES: override files mirror the source layout under * config.PROMPTS_OVERRIDE_DIR — "system-prompt.md" at the root, * "shared/", "teacher/" / "prober/". File presence is NOT * the override: the settings SWITCH is (owner decision) — the user keeps * their file and flips back to the source default to compare. */ import fs from "node:fs"; import path from "node:path"; import { PATHS, PROMPTS_OVERRIDE_DIR } from "../../config.ts"; import { FRAGMENT_RE, isValidSegmentPath, type PromptAgent, type PromptSegmentInfo } from "../core/prompt-segments.ts"; const PROMPTS_DIR = path.join(PATHS.root, "src", "agent", "prompts"); /** Absolute path for an override file (unchecked — validate first). */ function overrideFileFor(segmentPath: string): string { return path.join(PROMPTS_OVERRIDE_DIR, ...segmentPath.split("/")); } /** Read one override file, or null when absent/invalid — the composition * side's reader (the store's readOverride is the same mirror layout, * kept separate because the layers may not import each other). */ export function readPromptOverride(segmentPath: string): string | null { if (!isValidSegmentPath(segmentPath)) return null; try { return fs.readFileSync(overrideFileFor(segmentPath), "utf-8"); } catch { return null; } } /** List the SOURCE segments for one agent, in composition order — the * shared base, then the agent's own fragments sorted by filename, then * the shared/ fragments (the 2026-09-27 reorder: the agent's job and * method lead; the shared tool/file/format mechanics are an appendix). * The exact order composeInstructions composes them in. */ export function listSourceSegments(agent: PromptAgent): Array<{ path: string; shared: boolean; file: string }> { const segments: Array<{ path: string; shared: boolean; file: string }> = [ { path: "system-prompt.md", shared: true, file: PATHS.teacherPrompt }, ]; const dir = path.join(PROMPTS_DIR, agent); if (fs.existsSync(dir)) { const fragments = fs.readdirSync(dir).filter((f) => FRAGMENT_RE.test(f)).sort(); for (const f of fragments) { segments.push({ path: `${agent}/${f}`, shared: false, file: path.join(dir, f) }); } } const sharedDir = path.join(PROMPTS_DIR, "shared"); if (fs.existsSync(sharedDir)) { const sharedFragments = fs.readdirSync(sharedDir).filter((f) => FRAGMENT_RE.test(f)).sort(); for (const f of sharedFragments) { segments.push({ path: `shared/${f}`, shared: true, file: path.join(sharedDir, f) }); } } return segments; } /** * The effective segment view for one agent — every segment with its source * text, override text, and effective switch state. `switches` is the * settings record's agent.prompts map (absent key = off); enabled = * switch on AND the override file exists. */ export function effectivePromptSegments( agent: PromptAgent, switches: Record, ): PromptSegmentInfo[] { return listSourceSegments(agent).map((segment) => { let sourceText: string; try { sourceText = fs.readFileSync(segment.file, "utf-8"); } catch { sourceText = ""; // source unreadable — the agent loader would crash anyway } const overrideText = readPromptOverride(segment.path); return { path: segment.path, sourceText, overrideText, enabled: switches[segment.path] === true && overrideText !== null, shared: segment.shared, }; }); } /** Every known segment path across both agents (route validation). */ export function allSegmentPaths(): string[] { const paths = new Set(); for (const agent of ["prober", "teacher"] as const) { for (const segment of listSourceSegments(agent)) paths.add(segment.path); } return [...paths].sort(); }