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
- Inspect each token in `remaining` against the RESOLVERS map
- For nested sections, ensure the inner template resolves against the same context (see buildContext / template-context-parity.test.ts)
- Fix the resolver so its output is placeholder-free
- 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
- Audit new section templates for tokens not provided by the parent context
- Test that resolver output is placeholder-free before nesting
- Keep section nesting shallow; the bound is 6 passes by design
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
- Unknown placeholder {{${resolverName}}} in ${relTmplPath}
- ${hostConfig.displayName} description for "${name}" is ${des
- Codex description for "${name}" is ${description.length} cha
- {{INVOKE_SKILL}} requires a skill name, e.g. {{INVOKE_SKILL:
- {{LEARNINGS_SEARCH:query=...}} value must match ${QUERY_SAFE
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/5dba4e2b1700e4d2.
Report an issue: GitHub.