feat(prompts): fragment infrastructure + RFC #4782 stub manifests

Phase 1 — Fragment infrastructure:
- Add {{include:fragment-name}} support to prompt-loader.js
  - fragmentsDir registered alongside promptsDir/templatesDir
  - warmCache() now reads prompts/fragments/*.md with 'frg:' prefix
  - Pre-resolution pass in loadPrompt() resolves {{include:}} before
    the {{var}} validator (colon is outside validator regex [a-zA-Z0-9_],
    so unresolved includes are caught as parse errors)
  - Lazy-load fallback for fragments mirrors existing prompt lazy-load
- Create prompts/fragments/working-directory.md (Variant A: full
  contract including 'Do NOT cd to any other directory')
- Create prompts/fragments/working-directory-ops.md (Variant B:
  ops prompts, no cd restriction)
- Replace duplicated 3-line Working Directory boilerplate in 17 prompts
  with {{include:working-directory}} (12 files) or
  {{include:working-directory-ops}} (5 ops files)
- One fix to Working Directory wording now propagates to all 17 prompts

Phase 2 — RFC #4782 stub manifests:
- Add deploy, smoke-production, release, rollback, challenge to
  KNOWN_UNIT_TYPES and UNIT_MANIFESTS in unit-context-manifest.js
- All 5 builders already called composeInlinedContext() but returned ""
  because resolveManifest() found no entry; now they return live content
- All 26 unit types now have manifests (resolveManifest returns non-null
  for every type in KNOWN_UNIT_TYPES)

Tests:
- 5 new tests in prompt-loader-fragments.test.mjs (include resolution,
  lazy-load fallback, unknown fragment error, nested var inheritance,
  variant-B fragment)
- Full unit suite: 427 files passed, 4476 tests passed, 0 regressions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-13 00:30:19 +02:00
parent 55229f6604
commit ca5d869e34
22 changed files with 253 additions and 51 deletions

View file

@ -43,6 +43,7 @@ function resolveExtensionDir() {
}
const __extensionDir = resolveExtensionDir();
const promptsDir = join(__extensionDir, "prompts");
const fragmentsDir = join(__extensionDir, "prompts", "fragments");
const templatesDir = join(__extensionDir, "templates");
/**
* Return the resolved templates directory path for use in prompts.
@ -95,6 +96,26 @@ function warmCache() {
);
}
}
try {
for (const file of readdirSync(fragmentsDir)) {
if (!file.endsWith(".md")) continue;
const cacheKey = `frg:${file.slice(0, -3)}`;
if (!templateCache.has(cacheKey)) {
templateCache.set(
cacheKey,
readFileSync(join(fragmentsDir, file), "utf-8"),
);
}
}
} catch {
// fragments/ may not exist in test environments or older installs.
if (!process.env.VITEST && !process.env.NODE_TEST) {
logWarning(
"prompt",
`warmCache: fragments dir not found: ${fragmentsDir}`,
);
}
}
}
// Snapshot all templates at module load time
warmCache();
@ -116,6 +137,29 @@ export function loadPrompt(name, vars = {}) {
"If a `SF Skill Preferences` block is present in system context, use it and the `<available_skills>` catalog in your system prompt to decide which skills to load and follow for this unit, without relaxing required verification or artifact rules.",
...vars,
};
// Resolve {{include:fragment-name}} directives before variable validation.
// Fragment content can itself declare {{varName}} placeholders, which become
// visible to the validator below and are substituted in the normal pass.
// The include pattern uses ":" which is outside the validator regex [a-zA-Z0-9_],
// so unresolved includes never accidentally pass validation.
content = content.replace(/\{\{include:([a-zA-Z][a-zA-Z0-9_-]*)\}\}/g, (_, fragName) => {
let frag = templateCache.get(`frg:${fragName}`);
if (frag === undefined) {
// Lazy-load fallback (mirrors the prompt lazy-load path): allows fragments
// added after warmCache() ran (e.g. in test environments) to be resolved.
try {
frag = readFileSync(join(fragmentsDir, `${fragName}.md`), "utf-8");
templateCache.set(`frg:${fragName}`, frag);
} catch {
throw new SFError(
SF_PARSE_ERROR,
`loadPrompt("${name}"): unknown fragment "{{include:${fragName}}}". ` +
`Create prompts/fragments/${fragName}.md and rebuild.`,
);
}
}
return frag;
});
// Check BEFORE substitution: find all {{varName}} placeholders the template
// declares and verify every one has a value in vars. Checking after substitution
// would also flag {{...}} patterns injected by inlined content (e.g. template

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Challenge (Adversarial Review) — {{milestoneId}}
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory.
{{include:working-directory-ops}}
{{inlinedContext}}

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Complete Milestone {{milestoneId}} ("{{milestoneTitle}}")
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
{{include:working-directory}}
## Your Role in the Pipeline

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Complete Slice {{sliceId}} ("{{sliceTitle}}") — Milestone {{milestoneId}}
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
{{include:working-directory}}
## Your Role in the Pipeline

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Deploy — {{milestoneId}}
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory.
{{include:working-directory-ops}}
{{inlinedContext}}

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Execute Task {{taskId}} ("{{taskTitle}}") — Slice {{sliceId}} ("{{sliceTitle}}"), Milestone {{milestoneId}}
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
{{include:working-directory}}
A researcher explored the codebase and a planner decomposed the work — you are the executor. The task plan below is your authoritative contract for the slice goal and verification bar, but it is not a substitute for local reality. Verify the referenced files and surrounding code before changing them. Do not do broad re-research or spontaneous re-planning. Small factual corrections, file-path fixes, and local implementation adaptations are part of execution. Escalate to `blocker_discovered: true` only when the slice contract or downstream task graph no longer holds.
@ -38,6 +36,7 @@ Then:
0. Narrate step transitions, key implementation decisions, and verification outcomes as you work. Keep it terse — one line between tool-call clusters, not between every call — but write complete sentences in user-facing prose, not shorthand notes or scratchpad fragments.
0a. **Batch independent tool calls in parallel.** When the next step needs to read or grep multiple files/paths that don't depend on each other's results, issue them in a single tool-call message (multiple tool uses in one assistant turn) rather than one-at-a-time. Examples: reading the handler + the test file + the schema file to triangulate a bug; greping for two unrelated symbols. Sequential tool calls are only correct when each call's input genuinely depends on the previous call's output. Talking-then-doing is also dead weight — if the next action is unambiguous, just take it; describe what you found in the result, not what you plan to look at.
0b. **Swarm opportunity check.** Before implementation, decide whether this task can be split into a 2-3 worker same-model swarm. Swarm only if the shards have disjoint file/directory ownership, no shared-interface or lockfile edits, shard-local verification, and clear wall-clock savings. If it passes, dispatch `subagent({ tasks: [...] })` with explicit write scopes, expected output files, and verification per worker; then inspect `git status --short`, synthesize results, resolve conflicts, and run final task verification yourself. If it does not pass, continue single-agent execution without ceremony.
0c. **Rubber-duck check (non-trivial tasks only).** If the task touches more than two files, introduces a new abstraction, changes an API boundary, or has a non-obvious failure mode — dispatch a `rubber-duck` subagent with the task plan and any relevant existing code as context. Summarise its verdict in one line. If it returns a **Blocking** finding, address it before writing code. Skip this step for simple edits, test fixes, or renaming tasks.
1. {{skillActivation}} Follow any activated skills before writing code. If no skills match this task, skip this step.
2. **Verify file existence before editing.** The task plan references specific files. Before reading or editing any file mentioned in the plan, confirm it exists with `ls`, `find`, or `existsSync`. If a referenced file does NOT exist, stop immediately — do not attempt to create it based on the plan's description of what "should" be there. The file may have been deleted, renamed, or moved. Escalate as `blocker_discovered: true` with a clear description of which file is missing and what the plan expected to find. This prevents phantom work on stale file paths.
3. Execute the steps in the inlined task plan, adapting minor local mismatches when the surrounding code differs from the planner's snapshot

View file

@ -0,0 +1,3 @@
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory.

View file

@ -0,0 +1,3 @@
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Plan Milestone {{milestoneId}} ("{{milestoneTitle}}")
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
{{include:working-directory}}
All relevant context has been preloaded below - start working immediately without re-reading these files.

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Plan Slice {{sliceId}} ("{{sliceTitle}}") — Milestone {{milestoneId}}
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
{{include:working-directory}}
All relevant context has been preloaded below — start working immediately without re-reading these files.

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Reassess Roadmap — Milestone {{milestoneId}} after {{completedSliceId}}
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
{{include:working-directory}}
## Your Role in the Pipeline

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Refine Slice {{sliceId}} ("{{sliceTitle}}") — Milestone {{milestoneId}}
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
{{include:working-directory}}
This unit **expands an approved sketch into a full plan**. It is not a blank-sheet planning pass — the sketch's scope is the authoritative boundary, and the prior slice's real outcomes are the authoritative context. Your job is to produce a detailed plan that fits inside the sketch while reflecting what actually shipped in earlier slices.

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Release — {{milestoneId}}
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory.
{{include:working-directory-ops}}
{{inlinedContext}}

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Replan Slice {{sliceId}} ("{{sliceTitle}}") — Milestone {{milestoneId}}
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
{{include:working-directory}}
A completed task reported `blocker_discovered: true`, meaning the current slice plan cannot be executed as-is. Your job is to rewrite the remaining tasks in the slice plan to address the blocker while preserving all completed work.

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Research Milestone {{milestoneId}} ("{{milestoneTitle}}")
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
{{include:working-directory}}
All relevant context has been preloaded below — start working immediately without re-reading these files.

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Research Slice {{sliceId}} ("{{sliceTitle}}") — Milestone {{milestoneId}}
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
{{include:working-directory}}
All relevant context has been preloaded below — start working immediately without re-reading these files.

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Rollback — {{milestoneId}}
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory.
{{include:working-directory-ops}}
{{inlinedContext}}

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Run UAT — {{milestoneId}}/{{sliceId}}
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
{{include:working-directory}}
All relevant context has been preloaded below. Start working immediately without re-reading these files.

View file

@ -2,9 +2,7 @@ You are executing SF autonomous mode.
## UNIT: Smoke Test Production — {{milestoneId}}
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory.
{{include:working-directory-ops}}
{{inlinedContext}}

View file

@ -2,9 +2,7 @@
You are the validation orchestrator for **{{milestoneId}} — {{milestoneTitle}}**.
## Working Directory
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
{{include:working-directory}}
## Mission

View file

@ -0,0 +1,108 @@
/**
* prompt-loader-fragments.test.mjs fragment include resolution contracts.
*
* Purpose: verify that {{include:name}} directives in prompt templates are
* resolved before variable validation so fragment-provided placeholder text
* participates in normal {{var}} substitution.
*
* Consumer: prompt-loader.js warmCache + loadPrompt.
*/
import { rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
import { loadPrompt } from "../prompt-loader.js";
const promptsDir = join(import.meta.dirname, "..", "prompts");
const fragmentsDir = join(promptsDir, "fragments");
// Temp fixture files written during tests — cleaned up in afterEach.
const tempFiles = [];
function writeFixturePrompt(name, content) {
const path = join(promptsDir, `${name}.md`);
writeFileSync(path, content);
tempFiles.push(path);
return name;
}
function writeFixtureFragment(name, content) {
const path = join(fragmentsDir, `${name}.md`);
writeFileSync(path, content);
tempFiles.push(path);
return name;
}
afterEach(() => {
for (const p of tempFiles.splice(0)) rmSync(p, { force: true });
});
describe("fragment include resolution", () => {
test("fragment_resolved_and_var_substituted_in_one_pass", () => {
// Write a minimal fragment and a minimal prompt that includes it.
writeFixtureFragment("testfrag-wd-simple", "## WD\n\nPath: `{{workingDirectory}}`.");
writeFixturePrompt("testprompt-include-simple", "{{include:testfrag-wd-simple}}\n");
const result = loadPrompt("testprompt-include-simple", {
workingDirectory: "/my/project",
});
expect(result).toContain("## WD");
expect(result).toContain("`/my/project`");
});
test("working_directory_fragment_resolves_in_real_prompt", () => {
// Use a real migrated prompt that includes {{include:working-directory}}.
// Write a minimal wrapper prompt so we control the exact var set.
writeFixturePrompt(
"testprompt-wd-real",
"# Header\n\n{{include:working-directory}}\n",
);
const result = loadPrompt("testprompt-wd-real", {
workingDirectory: "/real/path",
});
expect(result).toContain("## Working Directory");
expect(result).toContain("`/real/path`");
expect(result).toContain("Do NOT `cd` to any other directory");
});
test("ops_fragment_omits_no_cd_restriction", () => {
writeFixturePrompt(
"testprompt-wd-ops",
"# Header\n\n{{include:working-directory-ops}}\n",
);
const result = loadPrompt("testprompt-wd-ops", {
workingDirectory: "/ops/path",
});
expect(result).toContain("## Working Directory");
expect(result).toContain("`/ops/path`");
expect(result).not.toContain("Do NOT `cd`");
});
test("unknown_fragment_throws_parse_error", () => {
writeFixturePrompt(
"testprompt-unknown-frag",
"{{include:this-fragment-does-not-exist-xyzzy}}\n",
);
expect(() => loadPrompt("testprompt-unknown-frag", {})).toThrow(
/unknown fragment.*this-fragment-does-not-exist-xyzzy/,
);
});
test("prompts_without_includes_unaffected", () => {
// A plain template with no {{include:}} must still load and substitute cleanly.
writeFixturePrompt(
"testprompt-no-include",
"# Plain\n\nValue: {{someVar}}.\n",
);
const result = loadPrompt("testprompt-no-include", { someVar: "hello" });
expect(result).toBe("# Plain\n\nValue: hello.");
expect(result).not.toContain("{{include:");
});
});

View file

@ -116,6 +116,12 @@ export const KNOWN_UNIT_TYPES = [
"discuss-requirements",
"research-project",
"workflow-preferences",
// ─── Ops / deployment pipeline ────────────────────────────────────────
"deploy",
"smoke-production",
"release",
"rollback",
"challenge",
];
export const UNIT_MANIFESTS = {
// ─── Milestone-scoped ────────────────────────────────────────────────
@ -508,6 +514,77 @@ export const UNIT_MANIFESTS = {
},
maxSystemPromptChars: COMMON_BUDGET_MEDIUM,
},
// ─── Ops / deployment pipeline ────────────────────────────────────────
"deploy": {
skills: { mode: "all" },
knowledge: "critical-only",
memory: "critical-only",
codebaseMap: false,
preferences: "active-only",
tools: TOOLS_ALL,
artifacts: {
inline: ["project"],
excerpt: [],
onDemand: [],
},
maxSystemPromptChars: COMMON_BUDGET_SMALL,
},
"smoke-production": {
skills: { mode: "all" },
knowledge: "critical-only",
memory: "critical-only",
codebaseMap: false,
preferences: "active-only",
tools: TOOLS_ALL,
artifacts: {
inline: ["project"],
excerpt: [],
onDemand: [],
},
maxSystemPromptChars: COMMON_BUDGET_SMALL,
},
"release": {
skills: { mode: "all" },
knowledge: "critical-only",
memory: "critical-only",
codebaseMap: false,
preferences: "active-only",
tools: TOOLS_ALL,
artifacts: {
inline: ["project"],
excerpt: [],
onDemand: [],
},
maxSystemPromptChars: COMMON_BUDGET_SMALL,
},
"rollback": {
skills: { mode: "all" },
knowledge: "critical-only",
memory: "critical-only",
codebaseMap: false,
preferences: "active-only",
tools: TOOLS_ALL,
artifacts: {
inline: ["project"],
excerpt: [],
onDemand: [],
},
maxSystemPromptChars: COMMON_BUDGET_SMALL,
},
"challenge": {
skills: { mode: "all" },
knowledge: "critical-only",
memory: "critical-only",
codebaseMap: false,
preferences: "active-only",
tools: TOOLS_ALL,
artifacts: {
inline: ["project"],
excerpt: [],
onDemand: [],
},
maxSystemPromptChars: COMMON_BUDGET_SMALL,
},
};
// ─── Lookup helper ────────────────────────────────────────────────────────
/**