garrytan/gstack · error · Error
Unknown placeholder }} in
Error message
Unknown placeholder {{${resolverName}}} in ${relTmplPath} What it means
Thrown by onePass() during template resolution in scripts/gen-skill-docs.ts:690 when a {{TOKEN}} or {{TOKEN:args}} placeholder is encountered whose resolver name is not in RESOLVERS and is not in the suppressed set. The error includes relTmplPath so you can locate the offending template.
Solutions
- Open relTmplPath and find the placeholder named in the message
- Cross-check the resolver name against the RESOLVERS keys in scripts/resolvers/index
- Fix the typo, or remove the placeholder if it is unused
- If the placeholder is intentionally absent for this build, suppress it (e.g. via --respect-detection for gbrain-aware suppression)
Example fix
<!-- before (in some.tmpl) -->
Use {{INVKOE_SKILL:plan-ceo-review}}
<!-- after -->
Use {{INVOKE_SKILL:plan-ceo-review}} Defensive patterns
Strategy: validation
Validate before calling
import { RESOLVERS } from './resolvers';
const known = new Set(Object.keys(RESOLVERS));
for (const m of tmplContent.matchAll(/\{\{(\w+)/g)) {
if (!known.has(m[1]) && !suppressed.has(m[1])) {
console.error(`Unknown placeholder {{${m[1]}}} in ${relTmplPath}`);
}
} Type guard
import { RESOLVERS } from './resolvers';
const isKnownResolver = (name: string): boolean => name in RESOLVERS; Prevention
- Grep all .tmpl files when renaming a resolver
- Keep resolver registry docs in sync with the RESOLVERS map
- Run a dry gen:skill-docs build in CI to catch typos early
When it happens
Trigger: Typing `{{INVKOE_SKILL:foo}}` in a .tmpl. Referencing a resolver that was renamed. Adding a new resolver but forgetting to register it in the RESOLVERS map.
Common situations: Renaming a resolver without grepping all .tmpl files. Copying a token from documentation that is out of date or from a different skill family.
Related errors
- Unresolved placeholders in
- Codex description for
- description for " " is chars (max ). Compress the…
- Invalid preamble-tier
- } requires a skill name, e.g. }
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/4133e3c6af218a5b.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/gen-skill-docs.ts:690
*/
function resolvePlaceholders(
tmplContent: string,
ctx: TemplateContext,
hostConfig: HostConfig,
relTmplPath: string,
): string {
// effectiveSuppressedResolvers() honors --respect-detection: when gbrain is
// detected locally, GBRAIN_* resolvers un-suppress. Shared by SKILL.md and
// section generation so both paths get the same gbrain-aware behavior.
const suppressed = effectiveSuppressedResolvers(hostConfig);
const onePass = (input: string): string =>
input.replace(/\{\{(\w+(?::[^}]+)?)\}\}/g, (_match, fullKey) => {
const parts = fullKey.split(':');
const resolverName = parts[0];
const args = parts.slice(1);
if (suppressed.has(resolverName)) return '';
const entry = RESOLVERS[resolverName];
if (!entry) throw new Error(`Unknown placeholder {{${resolverName}}} in ${relTmplPath}`);
const { resolve, appliesTo } = unwrapResolver(entry);
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);View on GitHub (pinned to 94993f7401)