chit

Manifest schema

Everything a chit manifest can declare, and what the runtime does with it.

A chit is a JSON manifest. This page is the reference for what it can declare. The two manifests in examples/ are the canonical examples, and src/manifest/parse.ts is the source of truth for behavior. Sections marked Deferred describe decisions held back on purpose, with the rationale recorded inline.

Top-level fields

FieldRequiredNotes
schemayesInteger. Currently 1.
idyesSlug. Used as the install id and run label.
descriptionyesOne sentence. Surfaced in the skill/MCP/CLI install artifact.
inputsyesMap of input name to { type, optional? }. Types: string, file[].
requiresnoSurface capabilities the schema cannot infer. Defaults to {}.
participantsyesInline aliases over registry agents, or references to a config role, with instructions, session, and permissions.
stepsyesMap of step id to step body. Step types: call, format.
outputyesStep id whose output is the final return value.

A participant is either inline or a reference to a named role. An inline participant declares agent, instructions, and session directly. A role reference names a role from config and inherits those fields, restating only what it overrides.

Fields:

  • role: id of a role defined in the roles section of ~/.config/chit/config.json. Optional, and the alternative to declaring agent/instructions/session inline. When present, the participant inherits the role's agent, instructions, session, and permissions; any of those the participant also sets replaces the role's value (a shallow replace, not a merge).
  • agent: id from the agent registry. Required on an inline participant; an optional override on a role reference. A role with no default agent is model-agnostic, and a participant referencing it must supply one.
  • instructions: short text describing the participant's identity or responsibility (the persona). The runtime prepends it to every step prompt that targets this participant, using a deterministic envelope (see call below). Adapters that natively support a system or developer channel may map it to that transport later; manifest semantics stay "instructions are applied before task."
  • session: stateless | per_topology | per_scope. per_scope means the session persists across runs sharing a scope (e.g. the same Claude session plus worktree). When any participant uses per_scope, the runtime infers can_provide_stable_scope as a surface requirement. A per_scope session inherited from a role counts the same as one declared inline.
  • permissions.filesystem: read_only | write. Optional. Default is read_only. Only declare it when the participant needs write.

A role reference is the inline form with the shared fields lifted into config:

"participants": {
  "reviewer": { "role": "reviewer" },
  "implementer": { "role": "reviewer", "agent": "claude", "permissions": { "filesystem": "write" } }
}

Both participants inherit the reviewer role's instructions and session. implementer overrides the agent and grants write. Role references resolve against config when the chit runs, not when it parses: an unknown role, or a model-agnostic role used without a participant agent, is a resolution error reported at run time, not a parse error.

Inline participants still let the same registry agent participate twice with different instructions. Without an inline alias or a role reference, a participant would be pure renaming and should not exist.

Steps

call

{
  "call": "<participant-id>",
  "prompt": "string with {{ template }} references"
}

Invokes the participant. Output is bound to steps.<id>.output.

The runtime constructs the final agent input deterministically:

Role:
{participant.instructions}

Task:
{rendered step prompt}

Audit logs and adapter calls always see this shape (the Role: label is the fixed envelope; the text under it is the participant's instructions). Adapters that natively support a separate role/system channel may route that portion to that transport instead, but the envelope stays the same so manifests remain transport-agnostic.

format

{
  "format": "string with {{ template }} references"
}

A pure string template. No agent call. Output is bound to steps.<id>.output.

Execution policy

An optional top-level policy decides what a run does. Absent, it defaults to one-shot, so every existing manifest is unchanged.

{
  "policy": { "kind": "one-shot" }
}
  • one-shot (the default) runs the step DAG once.
  • loop is the converge pattern: an implementer step and a reviewer step run in turn, and chit reads the reviewer's proceed | revise | block verdict each round. implementStep and reviewStep name the call steps; maxIterations is the budget (default 3).
{
  "policy": {
    "kind": "loop",
    "implementStep": "implement",
    "reviewStep": "review",
    "maxIterations": 3,
    // chit runs these ITSELF after a proceed review (ground truth, not the reviewer's
    // word). All pass -> converged; any fails -> revise; one cannot run -> needs-decision.
    "requiredChecks": [
      { "command": "bun", "args": ["test"], "name": "tests", "timeoutMs": 120000 }
    ]
  }
}

requiredChecks is structured, not a shell string: { command, args?, name?, timeoutMs? }, spawned as argv with no shell (a metacharacter in an arg is a literal argument). command is required; args defaults to []; name is a display label; timeoutMs defaults to 120000. No env, cwd, or shell strings -- this is chit-executed verification, not a CI language. When declared, chit's result is authoritative over the reviewer's self-reported checks. A run can override these per invocation (chit_start's required_checks, or a batch's task/batch-level checks) without editing the manifest; see the MCP reference.

Templates

Mustache-style references only. No filters, conditionals, or loops in the template language.

Resolvable references:

  • {{ inputs.<name> }}: a manifest input value.
  • {{ steps.<id>.output }}: the output of a prior step.

If a template references a step that has not run yet, the runtime constructs a dependency: step B references step A, so B depends on A. Steps with no cross-references run in parallel. Unresolved references at execution time are a runtime error.

Rendering by input type

Value substitution is fixed and runtime-owned. Surfaces do not negotiate format.

  • string inputs: substituted as-is.
  • file[] inputs: rendered as newline-joined absolute paths. The runtime normalizes relative paths against the invocation cwd/worktree. Paths that do not exist are a runtime error unless the input is declared optional and absent. File contents are not inlined. Agents with filesystem access read the paths themselves; this keeps token usage predictable and avoids hidden prompt bloat.

Surface capabilities

Each surface (Claude skill, MCP tool, CLI command) declares what it offers. Examples:

  • can_show_markdown: the surface can render markdown to the user.
  • can_pass_files: the surface can pass file paths through inputs.
  • can_read_git_diff: the surface can resolve a "diff" input to actual diff content.
  • can_provide_stable_scope: the surface can supply a stable scope identifier across runs (required for session: per_scope).

A manifest's requires block lists positive requirements only: capabilities the manifest needs. Absence is the convention for "not needed". Install fails if the surface lacks any required capability.

Inferred requirements

Several capabilities are implied by the manifest's shape. The author does not declare them; the validator computes them at install time:

  • Any input with type: file[] adds can_pass_files.
  • Any participant with session: per_scope adds can_provide_stable_scope.

requires is reserved for capabilities the schema cannot infer (e.g. can_show_markdown depends on what the author intends the format step to produce). Inferred requirements are merged with declared ones before install validation. Declaring an inferred capability explicitly is a no-op, not an error.

Session identity

A per_scope session is keyed by (scope, manifestId, participantId, fingerprint). All four are necessary:

  • scope: user-supplied via --scope, isolates concurrent workspaces.
  • manifestId: keeps two manifests from sharing sessions even when both use the same agent.
  • participantId: two participants sharing an agent get independent sessions.
  • fingerprint: model/instructions/permissions changes invalidate prior sessions.

The fingerprint hashes enough of the (agent, participant) pair that a meaningful change starts a fresh session instead of resuming a mismatched one. Sensitive env values (API keys, tokens) are deliberately not in the fingerprint material.

Permission enforcement

permissions is a governance contract, not a hint. If a manifest declares permissions.filesystem: read_only and the chosen adapter cannot enforce it, install fails by default. An install flag (--allow-unenforced-permissions) can opt into installing anyway, but the surface artifact must surface the gap to the executor at run time.

Adapters declare per-permission enforceability:

  • codex-exec enforces filesystem with an OS sandbox: read_only runs --sandbox read-only, write runs --sandbox workspace-write.
  • claude-cli enforces filesystem: read_only via --permission-mode plan, a Claude permission boundary that blocks writes from inside Claude (not an OS sandbox).
  • A generic subprocess adapter cannot enforce; install fails when read_only is requested unless the flag is set.

In practice: both built-in adapters enforce read_only today, so manifests using them run cleanly. --allow-unenforced-permissions is only needed for an adapter that declares it cannot enforce a requested permission.

Deferred details

None of these block the parser. They are settled for now with the working answer noted inline, and kept here so a future recipe can reopen the right one.

  1. Template engine. Custom regex over {{ x.y.z }} references, error on unresolved. Mustache sections and partials are unused.
  2. Explicit output vs implicit "last step". Explicit. Verbose but unambiguous.
  3. Parallelism budget. Unbounded for now. Add an implicit max_parallel only when a recipe hits the wall.
  4. inputs types beyond string and file[]. Add boolean, number, enum only when a recipe needs them.
  5. Error semantics. Fail-fast: if any call step fails, no downstream step runs. The runtime returns a structured failure envelope (which step failed, the error, partial outputs). Per-step on_failure: continue can come later.
  6. YAML vs JSON. JSON for now: parses everywhere, no dependency. Revisit if hand-authoring multi-line prompts becomes the dominant path.
  7. Config location. Layered: ~/.config/chit/config.json (global), then chit.config.json at the repo root. Both hold agents (the registry, alongside the non-redefinable built-in codex and claude) and roles (reusable participant profiles a manifest references by id). A later layer replaces an entry by id, whole; the repo file cannot set env or strictMcp. There is no separate agents.json. See Config layering.

On this page