garrytan/gstack · error · Error
Unknown placeholder {{${resolverName}}} in ${relTmplPath}
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.
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)
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 ${relTmplPath}: ${remaining.join(
- ${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/4133e3c6af218a5b.
Report an issue: GitHub.