model-overlay.ts
71 lines · scripts/resolvers/model-overlay.ts at 3f79d81
1/**
2 * Model overlay resolver — reads model-overlays/{model}.md and returns it
3 * wrapped in a subordinate behavioral-patch section.
4 *
5 * Precedence:
6 * 1. Exact match: ctx.model === 'gpt-5.4' → reads model-overlays/gpt-5.4.md
7 * 2. INHERIT directive: if the file's first non-whitespace line is
8 * `{{INHERIT:claude}}`, the resolver reads model-overlays/claude.md first
9 * and concatenates it ahead of the rest of this file's content.
10 * This lets `gpt-5.4.md` build on top of `gpt.md` without duplication.
11 * 3. Missing file: returns empty string (graceful degradation, no error).
12 * 4. No ctx.model set: returns empty string.
13 *
14 * The returned block is subordinate to skill workflow, safety gates, and
15 * AskUserQuestion instructions. The subordination language is part of the
16 * wrapper heading so it appears with every overlay regardless of file content.
17 */
19import * as fs from 'fs';
20import * as path from 'path';
21import type { TemplateContext } from './types';
23const OVERLAY_DIR = path.resolve(import.meta.dir, '../../model-overlays');
25const INHERIT_RE = /^\s*\{\{INHERIT:([a-z0-9-]+(?:\.[0-9]+)*)\}\}\s*\n/;
27export function readOverlay(model: string, seen: Set<string> = new Set(), excluded: ReadonlySet<string> = new Set()): string {
28 if (seen.has(model)) return ''; // cycle guard
29 seen.add(model);
30 if (excluded.has(`overlay:${model}`)) return '';
32 const filePath = path.join(OVERLAY_DIR, `${model}.md`);
33 if (!fs.existsSync(filePath)) return '';
35 const raw = fs.readFileSync(filePath, 'utf-8');
36 const match = raw.match(INHERIT_RE);
37 if (!match) return raw.trim();
39 const baseModel = match[1];
40 const base = readOverlay(baseModel, seen, excluded);
41 const rest = raw.replace(INHERIT_RE, '').trim();
43 if (!base) return rest;
44 return `${base}\n\n${rest}`;
45}
47export function generateModelOverlay(ctx: TemplateContext): string {
48 if (!ctx.model) return '';
50 const content = readOverlay(ctx.model, new Set(), ctx.modules?.excluded);
51 if (!content) return '';
53 const precedence = ctx.model === 'gpt-5.6-sol'
54 ? `The following instructions disambiguate scope for the ${ctx.model} model.
55They govern ambiguous completeness words such as \`complete\`, \`full\`, \`every\`,
56\`exhaustive\`, \`100%\`, and \`Boil the Ocean\`, and when to stop iterating on
57work the user did not ask for. Concrete skill workflow steps, STOP points,
58AskUserQuestion gates, plan-mode safety, required tests, skill-mandated
59re-verification and re-review loops, and /ship review gates still win.
60Never use this patch to skip a concrete requirement.`
61 : `The following nudges are tuned for the ${ctx.model} model family. They are
62**subordinate** to skill workflow, STOP points, AskUserQuestion gates, plan-mode
63safety, and /ship review gates. If a nudge below conflicts with skill instructions,
64the skill wins. Treat these as preferences, not rules.`;
66 return `## Model-Specific Behavioral Patch (${ctx.model})
68${precedence}
70${content}`;
71}