garrytan/gstack · error · Error

}} — no section " " in /sections/manifest.json

Error message

{{SECTION:${id}}} — no section "${id}" in ${skill}/sections/manifest.json

What it means

findSection() in scripts/resolvers/sections.ts:46 looks up the id in the skill's sections/manifest.json. If no entry matches it throws, naming the missing id and the manifest path. The SECTION resolver delegates to findSection to locate the entry's file and trigger.

Solutions

  1. Add the id to sections/manifest.json with a `file` and `trigger` field
  2. Fix the typo to match an existing manifest id
  3. Regenerate the manifest if it is built by a separate step

Example fix

<!-- before -->
{{SECTION:onbording}}
<!-- after -->
{{SECTION:onboarding}}
Defensive patterns

Strategy: type-guard

Validate before calling

import { readFileSync } from 'fs';
const ids = new Set(JSON.parse(readFileSync(`${skill}/sections/manifest.json`, 'utf-8')).sections.map((s: any) => s.id));
if (!ids.has(id)) {
  throw new Error(`section id ${id} not in ${skill}/sections/manifest.json`);
}

Type guard

const isSectionId = (skill: string, id: string): boolean =>
  loadManifest(skill).sections.some(s => s.id === id);

Prevention

When it happens

Trigger: Referencing {{SECTION:foo}} before adding it to manifest.json. Renaming a section id without updating references. Typo in the id (e.g. onbording vs onboarding).

Common situations: Splitting a monolithic skill into sections. Copying a section reference from another skill. Manifest out of sync with template references.

Related errors


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

Appendix: source

Thrown at scripts/resolvers/sections.ts:46

  file: string;
  title: string;
  trigger: string;
}
interface SectionManifest {
  skill: string;
  sections: SectionEntry[];
}

function loadManifest(skill: string): SectionManifest {
  const p = path.join(ROOT, skill, 'sections', 'manifest.json');
  const raw = fs.readFileSync(p, 'utf-8');
  return JSON.parse(raw) as SectionManifest;
}

function findSection(skill: string, id: string): SectionEntry {
  const entry = loadManifest(skill).sections.find(s => s.id === id);
  if (!entry) {
    throw new Error(`{{SECTION:${id}}} — no section "${id}" in ${skill}/sections/manifest.json`);
  }
  return entry;
}

/**
 * {{SECTION:id}} — pointer on Claude, inline on other hosts.
 * Claude path uses the stable gstack-root install (`{skillRoot}/{skill}/sections/`),
 * which always exists, instead of a naked relative path (Codex outside-voice #7).
 */
export const SECTION: ResolverFn = (ctx: TemplateContext, args?: string[]): string => {
  const id = args?.[0];
  if (!id) throw new Error('{{SECTION:id}} requires a section id');
  const entry = findSection(ctx.skillName, id);

  if (ctx.host === 'claude') {
    const sectionPath = `${ctx.paths.skillRoot}/${ctx.skillName}/sections/${entry.file}`;
    return [
      `> **STOP.** Before ${entry.trigger}, Read \`${sectionPath}\` and execute it`,

View on GitHub (pinned to 94993f7401)