Skill Tracer
Skills › gstack › gstack › preamble.ts

preamble.ts

139 lines · scripts/resolvers/preamble.ts at 3f79d81

1/**
2 * Preamble composition root.
3 *
4 * Each generator lives in its own file under ./preamble/*.ts. This file only
5 * wires them together: preambleSections() is the composition, and
6 * generatePreamble() joins it into the full inline render that tests use as
7 * the pre-hoist oracle. {{PREAMBLE}} resolves through ./preamble-hoist.ts,
8 * which renders tier 1 inline and splits tier 2+ into a per-skill pointer plus
9 * the shared sections/preamble.md the skill Reads on load. Keep composition
10 * declarative — no inline logic beyond tier gating.
11 *
12 * Each skill runs independently via `claude -p` (or the host's equivalent).
13 * The preamble provides: update checks, session
14 * tracking, user preferences, repo mode detection, model overlays, and
15 * telemetry.
16 *
17 * Telemetry data flow:
18 * 1. Always: local JSONL append to ~/.gstack/analytics/ (inline, inspectable)
19 * 2. If _TEL != "off" AND binary exists: gstack-telemetry-log for remote reporting
20 */
23import type { TemplateContext } from './types';
24import { generateModelOverlay } from './model-overlay';
25import { generateQuestionTuning } from './question-tuning';
27// Core bootstrap
28import { generatePreambleBash } from './preamble/generate-preamble-bash';
29import { generateUpgradeCheck } from './preamble/generate-upgrade-check';
30import {
31 generateCompletionStatus,
32 generatePlanModeInfo,
33} from './preamble/generate-completion-status';
35// Host-specific instructions
36import { generateBrainHealthInstruction } from './preamble/generate-brain-health-instruction';
38// GBrain cross-machine sync (runs at skill start; end-side handled in completion-status)
39import { generateBrainSyncBlock } from './preamble/generate-brain-sync-block';
41// Behavioral / voice
42import { generateVoiceDirective } from './preamble/generate-voice-directive';
44// Tier 2+ context and interaction framework
45import { generateContextRecovery } from './preamble/generate-context-recovery';
46import { generateAskUserFormat } from './preamble/generate-ask-user-format';
47import { generateWritingStyle } from './preamble/generate-writing-style';
48import { generateCompletenessSection } from './preamble/generate-completeness-section';
49import { generateConfusionProtocol } from './preamble/generate-confusion-protocol';
50import { generateEvidenceDirective } from './preamble/generate-evidence-directive';
51import { generateContextHealth } from './preamble/generate-context-health';
53// Tier 3+ repo mode + search
54import { generateRepoModeSection } from './preamble/generate-repo-mode-section';
55import { generateSearchBeforeBuildingSection } from './preamble/generate-search-before-building';
56import { generateMakePdfSetup } from './make-pdf';
58// Standalone export used directly by the resolver registry
59export { generateTestFailureTriage } from './preamble/generate-test-failure-triage';
61// Preamble Composition (tier → sections)
62// ─────────────────────────────────────────────
63// T1: core + upgrade + lake + telemetry + voice(trimmed) + completion
64// T2: T1 + voice(full) + ask + completeness + context-recovery + confusion + context-health
65// T3: T2 + repo-mode + search
66// T4: (same as T3 — TEST_FAILURE_TRIAGE is a separate {{}} placeholder, not preamble)
67//
68// Which skill gets which tier lives in each template's frontmatter
69// (`preamble-tier: N`). Every template that resolves {{PREAMBLE}} must
70// declare it — there is no default.
71export function generatePreamble(ctx: TemplateContext): string {
72 return preambleSections(ctx).join('\n\n');
75interface PreamblePiece {
76 /** Module id suffix (`preamble:<id>`); null for the model overlay, whose module id is `overlay:<model>`. */
77 id: string | null;
78 minTier: number;
79 when?: (ctx: TemplateContext) => boolean;
80 render: (ctx: TemplateContext, tier: number) => string;
83/**
84 * The preamble's pieces in render order. AskUserQuestion Format renders BEFORE
85 * the model overlay so the pacing rule is the ambient default; the overlay's
86 * nudges land as subordinate patches (Opus 4.7 absorbs the first pacing
87 * directive it hits; reversing this regresses plan-review cadence, v1.6.4.0).
88 * Plan-mode semantics stays right after bash (so _SESSION_ID / _BRANCH / _TEL
89 * are live) and before every onboarding gate. The 8 one-time onboarding
90 * generators live in bin/gstack-skill-start's instruction layer (plan Q2/OV6/F5).
91 */
92export const PREAMBLE_PIECES: readonly PreamblePiece[] = [
93 { id: 'bash', minTier: 1, render: generatePreambleBash },
94 { id: 'make-pdf-setup', minTier: 1, when: ctx => ctx.skillName === 'make-pdf', render: generateMakePdfSetup },
95 { id: 'plan-mode-info', minTier: 1, render: generatePlanModeInfo },
96 { id: 'upgrade-check', minTier: 1, render: generateUpgradeCheck },
97 { id: 'brain-health', minTier: 1, render: generateBrainHealthInstruction },
98 { id: 'ask-user-format', minTier: 2, render: generateAskUserFormat },
99 { id: 'brain-sync', minTier: 1, render: generateBrainSyncBlock },
100 { id: null, minTier: 1, render: generateModelOverlay },
101 { id: 'voice', minTier: 1, render: generateVoiceDirective },
102 { id: 'context-recovery', minTier: 2, render: generateContextRecovery },
103 { id: 'writing-style', minTier: 2, render: generateWritingStyle },
104 { id: 'completeness', minTier: 2, render: generateCompletenessSection },
105 { id: 'confusion', minTier: 2, render: generateConfusionProtocol },
106 { id: 'evidence', minTier: 2, render: generateEvidenceDirective },
107 { id: 'context-health', minTier: 2, render: generateContextHealth },
108 { id: 'question-tuning', minTier: 2, render: generateQuestionTuning },
109 { id: 'repo-mode', minTier: 3, render: () => generateRepoModeSection() },
110 { id: 'search-before-building', minTier: 3, render: generateSearchBeforeBuildingSection },
111 { id: 'completion-status', minTier: 1, render: generateCompletionStatus },
112];
114export const PREAMBLE_MODULE_IDS: readonly string[] = PREAMBLE_PIECES.flatMap(p => (p.id ? [`preamble:${p.id}`] : []));
116/** The preamble's generator outputs in render order, empty ones dropped. */
117export function preambleSections(ctx: TemplateContext): string[] {
118 const tier = ctx.preambleTier;
119 if (tier === undefined) {
120 throw new Error(
121 `Missing preamble-tier frontmatter in ${ctx.tmplPath}: every template that ` +
122 `resolves {{PREAMBLE}} must declare 'preamble-tier: N' (1-4).`
123 );
124 }
125 if (tier < 1 || tier > 4) {
126 throw new Error(`Invalid preamble-tier: ${tier} in ${ctx.tmplPath}. Must be 1-4.`);
127 }
128 const filter = ctx.modules;
129 const pieceTier = (piece: PreamblePiece): number => {
130 if (!filter || piece.id === null || filter.pinned.has(`preamble:${piece.id}`)) return tier;
131 return Math.min(tier, filter.preambleTierMax ?? tier);
132 };
133 return PREAMBLE_PIECES
134 .filter(piece => (piece.when?.(ctx) ?? true)
135 && !(piece.id && filter?.excluded.has(`preamble:${piece.id}`))
136 && pieceTier(piece) >= piece.minTier)
137 .map(piece => piece.render(ctx, pieceTier(piece)))
138 .filter(s => s && s.trim().length > 0);