Files
gstack/scripts/resolvers/model-overlay.ts
T
Garry Tan 6c8cf6774f feat: model overlays with explicit --model flag (no auto-detect)
Adds a per-model behavioral patch layer orthogonal to the host axis.
Different LLMs have different tendencies (GPT won't stop, Gemini
over-explains, o-series wants structured output). Overlays nudge each
model toward better defaults for gstack workflows.

Codex review caught three landmines the prior reviews missed:
1. Host != model — Claude Code can run any Claude model, Codex runs
   GPT/o-series, Cursor fronts multiple providers. Auto-detecting from
   host would lie. Dropped auto-detect. --model is explicit (default
   claude). Missing overlay file → empty string (graceful).
2. Import cycle — putting Model in resolvers/types.ts would cycle
   through hosts/index. Created neutral scripts/models.ts instead.
3. "Final say" is dangerous — overlay at the end of preamble could
   override STOP points, AskUserQuestion gates, /ship review gates.
   Placed overlay after spawned-session-check but before voice + tier
   sections. Wrapper heading adds explicit subordination language on
   every overlay: "subordinate to skill workflow, STOP points,
   AskUserQuestion gates, plan-mode safety, and /ship review gates."

Changes:
- scripts/models.ts: new neutral module. ALL_MODEL_NAMES, Model type,
  resolveModel() for family heuristics (gpt-5.4-mini → gpt-5.4, o3 →
  o-series, claude-opus-4-7 → claude), validateModel() helper.
- scripts/resolvers/types.ts: import Model, add ctx.model field.
- scripts/resolvers/model-overlay.ts: new resolver. Reads
  model-overlays/{model}.md. Supports {{INHERIT:base}} directive at
  top of file for concat (gpt-5.4 inherits gpt). Cycle guard.
- scripts/resolvers/index.ts: register MODEL_OVERLAY resolver.
- scripts/resolvers/preamble.ts: wire generateModelOverlay into
  composition before voice. Print MODEL_OVERLAY: {model} in preamble
  bash so users can see which overlay is active. Filter empty sections.
- scripts/gen-skill-docs.ts: parse --model CLI flag. Default claude.
  Unknown model → throw with list of valid options.
- model-overlays/{claude,gpt,gpt-5.4,gemini,o-series}.md: behavioral
  patches per model family. gpt-5.4.md uses {{INHERIT:gpt}} to extend
  gpt.md without duplication.
- test/gen-skill-docs.test.ts: fix qa-only guardrail regex scope.
  Was matching Edit/Glob/Grep anywhere after `allowed-tools:` in the
  whole file. Now scoped to frontmatter only. Body prose (Claude
  overlay references Edit as a tool) correctly no longer breaks it.

Verification:
- bun run gen:skill-docs --host all --dry-run → all fresh
- bun run gen:skill-docs --model gpt-5.4 → concat works, gpt.md +
  gpt-5.4.md content appears in order
- bun run gen:skill-docs --model unknown → errors with valid list
- All generated skills contain MODEL_OVERLAY: claude in preamble
- Golden ship fixtures regenerated

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 06:01:27 +08:00

61 lines
2.2 KiB
TypeScript

/**
* Model overlay resolver — reads model-overlays/{model}.md and returns it
* wrapped in a subordinate behavioral-patch section.
*
* Precedence:
* 1. Exact match: ctx.model === 'gpt-5.4' → reads model-overlays/gpt-5.4.md
* 2. INHERIT directive: if the file's first non-whitespace line is
* `{{INHERIT:claude}}`, the resolver reads model-overlays/claude.md first
* and concatenates it ahead of the rest of this file's content.
* This lets `gpt-5.4.md` build on top of `gpt.md` without duplication.
* 3. Missing file: returns empty string (graceful degradation, no error).
* 4. No ctx.model set: returns empty string.
*
* The returned block is subordinate to skill workflow, safety gates, and
* AskUserQuestion instructions. The subordination language is part of the
* wrapper heading so it appears with every overlay regardless of file content.
*/
import * as fs from 'fs';
import * as path from 'path';
import type { TemplateContext } from './types';
const OVERLAY_DIR = path.resolve(import.meta.dir, '../../model-overlays');
const INHERIT_RE = /^\s*\{\{INHERIT:([a-z0-9-]+(?:\.[0-9]+)*)\}\}\s*\n/;
function readOverlay(model: string, seen: Set<string> = new Set()): string {
if (seen.has(model)) return ''; // cycle guard
seen.add(model);
const filePath = path.join(OVERLAY_DIR, `${model}.md`);
if (!fs.existsSync(filePath)) return '';
const raw = fs.readFileSync(filePath, 'utf-8');
const match = raw.match(INHERIT_RE);
if (!match) return raw.trim();
const baseModel = match[1];
const base = readOverlay(baseModel, seen);
const rest = raw.replace(INHERIT_RE, '').trim();
if (!base) return rest;
return `${base}\n\n${rest}`;
}
export function generateModelOverlay(ctx: TemplateContext): string {
if (!ctx.model) return '';
const content = readOverlay(ctx.model);
if (!content) return '';
return `## Model-Specific Behavioral Patch (${ctx.model})
The following nudges are tuned for the ${ctx.model} model family. They are
**subordinate** to skill workflow, STOP points, AskUserQuestion gates, plan-mode
safety, and /ship review gates. If a nudge below conflicts with skill instructions,
the skill wins. Treat these as preferences, not rules.
${content}`;
}