mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
* feat(modules): module catalog, per-model loadouts manifest and gstack-modules render (GSTA-140 PR 1a) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(modules): cover bin/gstack-modules and the loadouts schema; allow modules/ in pregate strays Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(modules): gen:skill-docs refuses an exclusion render without --out-dir; render tests set up their own arm Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(modules): skill exclusions honor pinned sections and section references; own-property loadout lookups; llms.txt follows the selection; ownership rechecked before swap Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: bump version and changelog (v1.91.296.0) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: loadout render cost and the modules/ directory in PROJECT_STRUCTURE Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: unstamp #3554 (slot calls stamp); CHANGELOG entry back under [Unreleased] Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
140 lines
6.9 KiB
TypeScript
140 lines
6.9 KiB
TypeScript
/**
|
|
* Preamble composition root.
|
|
*
|
|
* Each generator lives in its own file under ./preamble/*.ts. This file only
|
|
* wires them together: preambleSections() is the composition, and
|
|
* generatePreamble() joins it into the full inline render that tests use as
|
|
* the pre-hoist oracle. {{PREAMBLE}} resolves through ./preamble-hoist.ts,
|
|
* which renders tier 1 inline and splits tier 2+ into a per-skill pointer plus
|
|
* the shared sections/preamble.md the skill Reads on load. Keep composition
|
|
* declarative — no inline logic beyond tier gating.
|
|
*
|
|
* Each skill runs independently via `claude -p` (or the host's equivalent).
|
|
* The preamble provides: update checks, session
|
|
* tracking, user preferences, repo mode detection, model overlays, and
|
|
* telemetry.
|
|
*
|
|
* Telemetry data flow:
|
|
* 1. Always: local JSONL append to ~/.gstack/analytics/ (inline, inspectable)
|
|
* 2. If _TEL != "off" AND binary exists: gstack-telemetry-log for remote reporting
|
|
*/
|
|
|
|
|
|
import type { TemplateContext } from './types';
|
|
import { generateModelOverlay } from './model-overlay';
|
|
import { generateQuestionTuning } from './question-tuning';
|
|
|
|
// Core bootstrap
|
|
import { generatePreambleBash } from './preamble/generate-preamble-bash';
|
|
import { generateUpgradeCheck } from './preamble/generate-upgrade-check';
|
|
import {
|
|
generateCompletionStatus,
|
|
generatePlanModeInfo,
|
|
} from './preamble/generate-completion-status';
|
|
|
|
// Host-specific instructions
|
|
import { generateBrainHealthInstruction } from './preamble/generate-brain-health-instruction';
|
|
|
|
// GBrain cross-machine sync (runs at skill start; end-side handled in completion-status)
|
|
import { generateBrainSyncBlock } from './preamble/generate-brain-sync-block';
|
|
|
|
// Behavioral / voice
|
|
import { generateVoiceDirective } from './preamble/generate-voice-directive';
|
|
|
|
// Tier 2+ context and interaction framework
|
|
import { generateContextRecovery } from './preamble/generate-context-recovery';
|
|
import { generateAskUserFormat } from './preamble/generate-ask-user-format';
|
|
import { generateWritingStyle } from './preamble/generate-writing-style';
|
|
import { generateCompletenessSection } from './preamble/generate-completeness-section';
|
|
import { generateConfusionProtocol } from './preamble/generate-confusion-protocol';
|
|
import { generateEvidenceDirective } from './preamble/generate-evidence-directive';
|
|
import { generateContextHealth } from './preamble/generate-context-health';
|
|
|
|
// Tier 3+ repo mode + search
|
|
import { generateRepoModeSection } from './preamble/generate-repo-mode-section';
|
|
import { generateSearchBeforeBuildingSection } from './preamble/generate-search-before-building';
|
|
import { generateMakePdfSetup } from './make-pdf';
|
|
|
|
// Standalone export used directly by the resolver registry
|
|
export { generateTestFailureTriage } from './preamble/generate-test-failure-triage';
|
|
|
|
// Preamble Composition (tier → sections)
|
|
// ─────────────────────────────────────────────
|
|
// T1: core + upgrade + lake + telemetry + voice(trimmed) + completion
|
|
// T2: T1 + voice(full) + ask + completeness + context-recovery + confusion + context-health
|
|
// T3: T2 + repo-mode + search
|
|
// T4: (same as T3 — TEST_FAILURE_TRIAGE is a separate {{}} placeholder, not preamble)
|
|
//
|
|
// Which skill gets which tier lives in each template's frontmatter
|
|
// (`preamble-tier: N`). Every template that resolves {{PREAMBLE}} must
|
|
// declare it — there is no default.
|
|
export function generatePreamble(ctx: TemplateContext): string {
|
|
return preambleSections(ctx).join('\n\n');
|
|
}
|
|
|
|
interface PreamblePiece {
|
|
/** Module id suffix (`preamble:<id>`); null for the model overlay, whose module id is `overlay:<model>`. */
|
|
id: string | null;
|
|
minTier: number;
|
|
when?: (ctx: TemplateContext) => boolean;
|
|
render: (ctx: TemplateContext, tier: number) => string;
|
|
}
|
|
|
|
/**
|
|
* The preamble's pieces in render order. AskUserQuestion Format renders BEFORE
|
|
* the model overlay so the pacing rule is the ambient default; the overlay's
|
|
* nudges land as subordinate patches (Opus 4.7 absorbs the first pacing
|
|
* directive it hits; reversing this regresses plan-review cadence, v1.6.4.0).
|
|
* Plan-mode semantics stays right after bash (so _SESSION_ID / _BRANCH / _TEL
|
|
* are live) and before every onboarding gate. The 8 one-time onboarding
|
|
* generators live in bin/gstack-skill-start's instruction layer (plan Q2/OV6/F5).
|
|
*/
|
|
export const PREAMBLE_PIECES: readonly PreamblePiece[] = [
|
|
{ id: 'bash', minTier: 1, render: generatePreambleBash },
|
|
{ id: 'make-pdf-setup', minTier: 1, when: ctx => ctx.skillName === 'make-pdf', render: generateMakePdfSetup },
|
|
{ id: 'plan-mode-info', minTier: 1, render: generatePlanModeInfo },
|
|
{ id: 'upgrade-check', minTier: 1, render: generateUpgradeCheck },
|
|
{ id: 'brain-health', minTier: 1, render: generateBrainHealthInstruction },
|
|
{ id: 'ask-user-format', minTier: 2, render: generateAskUserFormat },
|
|
{ id: 'brain-sync', minTier: 1, render: generateBrainSyncBlock },
|
|
{ id: null, minTier: 1, render: generateModelOverlay },
|
|
{ id: 'voice', minTier: 1, render: generateVoiceDirective },
|
|
{ id: 'context-recovery', minTier: 2, render: generateContextRecovery },
|
|
{ id: 'writing-style', minTier: 2, render: generateWritingStyle },
|
|
{ id: 'completeness', minTier: 2, render: generateCompletenessSection },
|
|
{ id: 'confusion', minTier: 2, render: generateConfusionProtocol },
|
|
{ id: 'evidence', minTier: 2, render: generateEvidenceDirective },
|
|
{ id: 'context-health', minTier: 2, render: generateContextHealth },
|
|
{ id: 'question-tuning', minTier: 2, render: generateQuestionTuning },
|
|
{ id: 'repo-mode', minTier: 3, render: () => generateRepoModeSection() },
|
|
{ id: 'search-before-building', minTier: 3, render: generateSearchBeforeBuildingSection },
|
|
{ id: 'completion-status', minTier: 1, render: generateCompletionStatus },
|
|
];
|
|
|
|
export const PREAMBLE_MODULE_IDS: readonly string[] = PREAMBLE_PIECES.flatMap(p => (p.id ? [`preamble:${p.id}`] : []));
|
|
|
|
/** The preamble's generator outputs in render order, empty ones dropped. */
|
|
export function preambleSections(ctx: TemplateContext): string[] {
|
|
const tier = ctx.preambleTier;
|
|
if (tier === undefined) {
|
|
throw new Error(
|
|
`Missing preamble-tier frontmatter in ${ctx.tmplPath}: every template that ` +
|
|
`resolves {{PREAMBLE}} must declare 'preamble-tier: N' (1-4).`
|
|
);
|
|
}
|
|
if (tier < 1 || tier > 4) {
|
|
throw new Error(`Invalid preamble-tier: ${tier} in ${ctx.tmplPath}. Must be 1-4.`);
|
|
}
|
|
const filter = ctx.modules;
|
|
const pieceTier = (piece: PreamblePiece): number => {
|
|
if (!filter || piece.id === null || filter.pinned.has(`preamble:${piece.id}`)) return tier;
|
|
return Math.min(tier, filter.preambleTierMax ?? tier);
|
|
};
|
|
return PREAMBLE_PIECES
|
|
.filter(piece => (piece.when?.(ctx) ?? true)
|
|
&& !(piece.id && filter?.excluded.has(`preamble:${piece.id}`))
|
|
&& pieceTier(piece) >= piece.minTier)
|
|
.map(piece => piece.render(ctx, pieceTier(piece)))
|
|
.filter(s => s && s.trim().length > 0);
|
|
}
|