aidokitwiki

How to debug template emission

Audience-Contributor Status-Shipped v0.5 Spec-Adapter Contract

Purpose #

When the wrong file content lands in the emitted tree, find the cause: staging dir, interpolation, file modes, or template source.

Big Picture #

The emission pipeline is:

  1. Adapter emit methods return EmittedFile[] from in-memory templates loaded via files-resolver.
  2. @aidokit/cli collects the plan.
  3. @aidokit/core writeFilesStaged writes to <projectRoot>/.aido-staging/.
  4. Atomic move into the project root.

Any of these layers can be the bug source.

How It Works #

1. Confirm what was actually emitted #

aidokit init --dry-run --verbose --json prints the full plan without writing. Pipe to jq to inspect:

node packages/cli/dist/bin/aidokit.js init --dry-run --verbose --json \
  --adapter claude-code --stack node-ts --yes \
  | jq '.result.filePlan[] | {path, mode, contentPreview: (.content | tostring | .[0:80])}'
?
TODO

Confirm with maintainer — exact JSON shape of the file plan in --json --dry-run mode. If the schema differs, log the in-memory plan via a temporary console.log in packages/cli/src/init/compute-file-plan.ts.

2. Confirm which template source was read #

files-resolver locates dist/files/ (built) or falls back to src/files/ (source-tree). If dist/files/ is stale, you'll get old content.

pnpm --filter @aidokit/adapter-claude-code build    # regenerates dist/files/

If the bug persists after a clean build, the template under src/files/ is wrong.

3. Confirm interpolation #

interpolate(template, vars) in @aidokit/core is the only substitution mechanism. If a {{varName}} lands literally in the output, the variable is missing from the var bag passed by the caller.

If the template needs a conditional or loop, it should NOT use interpolation — it should be programmatically generated by the emit method (CLAUDE.md §8).

4. Confirm file modes #

Executable scripts must be mode 0o755. Check the emitted file:

ls -l /tmp/scratch/.claude/scripts/

If a script is missing the executable bit, the EmittedFile.mode field was omitted in the relevant emit method (typically emitWatchdog in the adapter). Fix by setting mode: 0o755 on the returned object.

Per .docs/docs/specs/adapter-contract.md §7.11, prefer mode on the EmittedFile over a chmod in postInstall.

5. Confirm the staging → atomic-move boundary #

If the project root is left in a half-written state, the atomic-move step failed. The staging dir is preserved on failure with a warning telling you where it is. Inspect:

ls -la /tmp/scratch/.aido-staging/

Compare to what made it into the project root. If staging has the right content but the move didn't happen, dig into packages/core/src/write-files-staged.ts.

6. Confirm capability declarations match #

If your new path isn't declared in manifest.capabilityDeclarations.writesPaths, AD-STD-CAP-01 will fail. Add the glob to the manifest.

7. Confirm determinism #

Run the emit method twice; compare:

node packages/cli/dist/bin/aidokit.js init --dry-run --json ... > /tmp/a.json
node packages/cli/dist/bin/aidokit.js init --dry-run --json ... > /tmp/b.json
diff /tmp/a.json /tmp/b.json

Any difference → non-deterministic field somewhere (timestamp, random id, set iteration order). Fix; otherwise dogfood compare will fail.

Example: missing {{projectName}} substitution #

Symptom: emitted CLAUDE.md contains the literal {{projectName}} instead of my-app.

Trace:

  1. packages/adapter-claude-code/src/emit/agent-rules.ts reads src/files/agent-rules.md.
  2. It calls interpolate(template, vars).
  3. vars is built from ctx.projectName — but if the calling site spelled it projectname (lowercase), the helper threw "Unknown variable: projectname" before substitution.
  4. Either the template or the var bag is wrong; fix the spelling.

Common pitfalls #