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
| Field | Required | Notes |
|---|---|---|
schema | yes | Integer. Currently 1. |
id | yes | Slug. Used as the install id and run label. |
description | yes | One sentence. Surfaced in the skill/MCP/CLI install artifact. |
inputs | yes | Map of input name to { type, optional? }. Types: string, file[]. |
requires | no | Surface capabilities the schema cannot infer. Defaults to {}. |
participants | yes | Inline aliases over registry agents, or references to a config role, with instructions, session, and permissions. |
steps | yes | Map of step id to step body. Step types: call, format. |
output | yes | Step 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 therolessection of~/.config/chit/config.json. Optional, and the alternative to declaringagent/instructions/sessioninline. When present, the participant inherits the role'sagent,instructions,session, andpermissions; 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 (seecallbelow). 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_scopemeans the session persists across runs sharing a scope (e.g. the same Claude session plus worktree). When any participant usesper_scope, the runtime inferscan_provide_stable_scopeas a surface requirement. Aper_scopesession inherited from a role counts the same as one declared inline.permissions.filesystem:read_only|write. Optional. Default isread_only. Only declare it when the participant needswrite.
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 | blockverdict each round.implementStepandreviewStepname the call steps;maxIterationsis 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.
stringinputs: 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 declaredoptionaland 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 throughinputs.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 forsession: 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[]addscan_pass_files. - Any participant with
session: per_scopeaddscan_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-execenforcesfilesystemwith an OS sandbox:read_onlyruns--sandbox read-only,writeruns--sandbox workspace-write.claude-clienforcesfilesystem: read_onlyvia--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_onlyis 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.
- Template engine. Custom regex over
{{ x.y.z }}references, error on unresolved. Mustache sections and partials are unused. - Explicit
outputvs implicit "last step". Explicit. Verbose but unambiguous. - Parallelism budget. Unbounded for now. Add an implicit
max_parallelonly when a recipe hits the wall. inputstypes beyondstringandfile[]. Addboolean,number,enumonly when a recipe needs them.- Error semantics. Fail-fast: if any
callstep fails, no downstream step runs. The runtime returns a structured failure envelope (which step failed, the error, partial outputs). Per-stepon_failure: continuecan come later. - YAML vs JSON. JSON for now: parses everywhere, no dependency. Revisit if hand-authoring multi-line prompts becomes the dominant path.
- Config location. Layered:
~/.config/chit/config.json(global), thenchit.config.jsonat the repo root. Both holdagents(the registry, alongside the non-redefinable built-incodexandclaude) androles(reusable participant profiles a manifest references by id). A later layer replaces an entry by id, whole; the repo file cannot setenvorstrictMcp. There is no separateagents.json. See Config layering.