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

  1. Open relTmplPath and find the placeholder named in the message
  2. Cross-check the resolver name against the RESOLVERS keys in scripts/resolvers/index
  3. Fix the typo, or remove the placeholder if it is unused
  4. 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

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


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