garrytan/gstack · error · Error

Unresolved placeholders in ${relTmplPath}: ${remaining.join(

Error message

Unresolved placeholders in ${relTmplPath}: ${remaining.join(', ')}

What it means

After the bounded 6-pass resolution loop in scripts/gen-skill-docs.ts:710, the generator scans for surviving {{...}} tokens. If any remain it throws listing them. This catches nested-section placeholders whose inner resolvers are missing, cycles, or a resolver that emits its own placeholder.

Source

Thrown at scripts/gen-skill-docs.ts:710

      if (appliesTo && !appliesTo(ctx)) return '';
      return args.length > 0 ? resolve(ctx, args) : resolve(ctx);
    });

  // Multi-pass: a resolver may emit content that itself contains {{TOKENS}} — the
  // {{SECTION:id}} resolver inlines a section template (with its own resolvers)
  // for non-Claude hosts. .replace() doesn't re-scan inserted text, so loop until
  // the output stabilizes. Bounded to avoid an infinite loop if a resolver ever
  // emits its own placeholder; 6 passes is far more nesting than any skill needs.
  let content = tmplContent;
  for (let pass = 0; pass < 6; pass++) {
    const next = onePass(content);
    if (next === content) break;
    content = next;
  }

  const remaining = content.match(/\{\{(\w+(?::[^}]+)?)\}\}/g);
  if (remaining) {
    throw new Error(`Unresolved placeholders in ${relTmplPath}: ${remaining.join(', ')}`);
  }
  return content;
}

/**
 * Build the TemplateContext from a template's frontmatter. Shared by SKILL.md
 * and section generation so sections inherit the SAME context the parent skill
 * resolves with (skillName, tier, benefitsFrom, interactive) — enforced by
 * test/template-context-parity.test.ts. skillNameOverride lets section
 * generation pin the parent skill's name instead of deriving "sections".
 */
function buildContext(
  tmplContent: string,
  tmplPath: string,
  host: Host,
  skillNameOverride?: string,
): TemplateContext {
  const { name: extractedName } = extractNameAndDescription(tmplContent);

View on GitHub (pinned to 94993f7401)

Solutions

  1. Inspect each token in `remaining` against the RESOLVERS map
  2. For nested sections, ensure the inner template resolves against the same context (see buildContext / template-context-parity.test.ts)
  3. Fix the resolver so its output is placeholder-free
  4. Only as a last resort, raise the 6-pass loop bound if a legitimate >6-deep nesting exists

Example fix

<!-- before: section.tmpl references a token no resolver provides -->
{{LEGACY_TOKEN}}
<!-- after: replace with a literal or a known resolver -->
{{INVOKE_SKILL:plan-ceo-review}}
Defensive patterns

Strategy: validation

Validate before calling

const after = resolvePasses(tmplContent, 6);
const leftovers = after.match(/\{\{\w+(?::[^}]+)?\}\}/g);
if (leftovers) {
  console.error(`Unresolved placeholders: ${leftovers.join(', ')}`);
  process.exit(2);
}

Prevention

When it happens

Trigger: A section template references {{UNKNOWN}} that the parent context cannot resolve. A resolver emits text containing a {{SELF}} placeholder. Deeply nested {{SECTION:...}} chains exceeding 6 levels.

Common situations: Adding a new section that uses a placeholder not present in the parent resolver context. Resolver bug that re-emits placeholder syntax. Section content referencing legacy tokens.

Related errors


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