mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-06 05:35:46 +02:00
6c8cf6774f
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>
61 lines
2.2 KiB
TypeScript
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}`;
|
|
}
|