garrytan/gstack · error · Error

Invalid preamble-tier: ${tier} in ${ctx.tmplPath}. Must be 1

Error message

Invalid preamble-tier: ${tier} in ${ctx.tmplPath}. Must be 1-4.

What it means

generatePreamble() in scripts/resolvers/preamble.ts:82 reads ctx.preambleTier (default 4). The tier selects which preamble sections render (T1 minimal through T4 full). Values outside 1-4 throw, naming ctx.tmplPath so the offending template is identifiable.

Source

Thrown at scripts/resolvers/preamble.ts:82

// 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 + checkpoint + context-health
// T3: T2 + repo-mode + search
// T4: (same as T3 — TEST_FAILURE_TRIAGE is a separate {{}} placeholder, not preamble)
//
// Skills by tier:
//   T1: browse, setup-cookies, benchmark
//   T2: investigate, cso, retro, doc-release, setup-deploy, canary, context-save, context-restore, health
//   T3: autoplan, codex, design-consult, office-hours, ceo/design/eng-review
//   T4: ship, review, qa, qa-only, design-review, land-deploy
export function generatePreamble(ctx: TemplateContext): string {
  const tier = ctx.preambleTier ?? 4;
  if (tier < 1 || tier > 4) {
    throw new Error(`Invalid preamble-tier: ${tier} in ${ctx.tmplPath}. Must be 1-4.`);
  }
  const sections = [
    generatePreambleBash(ctx),
    ...(ctx.skillName === 'make-pdf' ? [generateMakePdfSetup(ctx)] : []),
    // Plan-mode-skill semantics stays near the top: after bash (so _SESSION_ID /
    // _BRANCH / _TEL env vars are live) and before all onboarding gates so
    // models read the authoritative "AskUserQuestion satisfies plan mode's
    // end-of-turn" rule before any other instruction. Renders for all skills
    // (not interactive-gated); the text applies universally.
    generatePlanModeInfo(ctx),
    generateUpgradeCheck(ctx),
    generateWritingStyleMigration(ctx),
    generateLakeIntro(),
    generateTelemetryPrompt(ctx),
    generateProactivePrompt(ctx),
    generateFirstRunGuidance(ctx),
    generateRoutingInjection(ctx),
    generateVendoringDeprecation(ctx),

View on GitHub (pinned to 94993f7401)

Solutions

  1. Set preambleTier to 1, 2, 3, or 4 based on the skill's complexity (see the tier table in the comments)
  2. Omit the field to default to 4
  3. If you genuinely need a new tier, extend the bounds and the section assembly in generatePreamble first

Example fix

// before
preambleTier: 5
// after
preambleTier: 4
Defensive patterns

Strategy: type-guard

Validate before calling

if (ctx.preambleTier != null && (ctx.preambleTier < 1 || ctx.preambleTier > 4)) {
  throw new Error(`preambleTier out of range: ${ctx.preambleTier}`);
}

Type guard

const isPreambleTier = (n: number): n is 1 | 2 | 3 | 4 =>
  Number.isInteger(n) && n >= 1 && n <= 4;

Prevention

When it happens

Trigger: Setting `preambleTier: 0` or `preambleTier: 5` in a .tmpl frontmatter. A context-builder bug feeding a non-tier number. Off-by-one when introducing a new tier.

Common situations: Misunderstanding the tier scale. Editing tier per the comments but using the wrong integer. Copying tier from a skill that uses a different default.

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/bf386bbd8ddfddd5. Report an issue: GitHub.