Write your own chain
This guide builds a chain of your own from the shipped default chain. It adds
a lint node after verification. The node runs your linter as a command. When
the linter fails, an agent fixes what it reported and the linter runs again. A
judge decides when more attempts are not worth it. The node has a time cap, and
nobody can skip it.
For every key used here, see Chain nodes and Library and chains.
Before you start
- A running Kraft server and a connected repo.
- A lint command that exits 0 when the code is clean. This guide uses
npm run lint && npx tsc --noEmit. Use your own. - Your config lives in
$KRAFT_HOME/templates/(~/.kraft/templates/by default). You editlibrary.yamland add a file underchains/.
1. Add the tasks to the library
Add two tasks to library.yaml, under tasks:. One runs the linter. The other
is the agent that repairs what it reports.
tasks:
lint:
kind: subprocess
command: sh -c 'npm run lint && npx tsc --noEmit'
repair_lint:
kind: agent
harness: claude
profile: strong
prompt: Fix the lint and type errors the lint step reported. Change no behavior.
Kraft does not run command through a shell, so && and pipes need the
sh -c '...' wrapper above. Inside a fix loop, Kraft replaces the repair
task's prompt with its own instruction: which tasks failed and what they
reported. See Subprocess tasks and
Fix loop and judge.
2. Add the node to the library
Add a lint node to library.yaml, under nodes::
nodes:
lint:
kind: exec
skippable: false
policy:
time_cap_minutes: 30
tasks:
- id: lint
extends: lint
fix_loop:
tasks:
- id: repair
extends: repair_lint
judge:
id: judge
extends: strict_judge
max_attempts: 3
skippable: falserefuseskraft item skipon this node, a chain revision that proposes skipping it, andkraft item create --skip-nodes lint.policy.time_cap_minutes: 30caps the node's running time, fix loop included. Hitting it stops the item for you. It is never treated as a lint failure, so no repair attempt is spent on it. See Time caps.fix_loop.tasksruns after a failing lint, then the lint runs again. The loop repeats until lint passes ormax_attemptsruns out. Then the node is stuck: an automatic escalation turn tries first, then the item stops for you. The loop's wall clock comes frompolicy.yaml(default, orloops.lint.fix_loop). See Policy.judgeis optional.strict_judgeis the shipped judge task, the oneverificationuses. It runs before each repair cycle after the first, and can stop the loop. Itsstop_downgradeverdict never lets a failing lint command through. See The judge.
3. Make the chain
Copy the default chain and give the copy its own id:
cd ~/.kraft/templates
cp chains/default.yaml chains/with-lint.yaml
In chains/with-lint.yaml, change id: default to id: with-lint. Then add
the node right after verification:
- id: verification
extends: verification
- id: lint
extends: lint
A chain file cannot extend another chain, so the copy is a full copy. It no
longer follows changes to the shipped default chain. See
Upgrading your templates.
4. Check it and load it
kraft admin templates lint
kraft admin templates show with-lint --resolved
kraft admin reload
lint must print no errors before you reload. show --resolved prints the
chain with every library component expanded. Look for your lint node with
its fix_loop, its policy and skippable: false. reload makes the running
server read the files again, with no restart.
5. Use it
Name the chain when you file work:
kraft item create "Add the export button" --chain with-lint
kraft item set-chain ID --template with-lint switches an item that has not
started yet.
To make it the repo's default instead, set default_chain_template: with-lint
on the repo's entry in repos.yaml. Every item filed on that repo without a
chain then gets with-lint: kraft item create, the MCP tool, the board's
New work item dialog, POST /api/work-items, POST /api/triggers and
auto-intake. Naming a chain still wins.
How the lint node runs
The lint command runs in the item's worktree, with the worker's environment
and KRAFT_RESULT_PATH. Exit 0 is done, anything else failed, and a
failure becomes a critical finding holding the end of its output for the
repair agent. Subprocess tasks has
the full contract.
Or use a test scope instead
If you only need lint to run when certain files change, you may not need a
node at all. Add the linter to the repo's test_scopes in repos.yaml:
test_scopes:
- paths: ["src/**"]
command: npm test
- paths: ["src/**/*.ts"]
command: sh -c 'npm run lint && npx tsc --noEmit'
The shipped verification node runs every scope whose paths the change
touches, and its existing fix loop repairs the failures. A scope's command is
split the same way, with no shell. See Repos.
Verify
File a small item on the new chain. Once it passes verification, watch the
lint node run:
kraft view events ID --type findings_measured
A failing lint shows a findings_measured event for node lint with one
critical finding, then a fix_cycle_started event. A clean lint shows a
findings_measured event with no findings, then node_completed for lint.
kraft item skip ID on the lint node answers
path: 'lint' does not allow skipping.
Reviewing a change
Comment on a Kraft work item at any point in its life, not only at a gate, and see your feedback reach the next agent that touches the code.
Switch a harness
Run some or all of a chain's agent tasks on Codex, Cursor, OpenCode, Gemini, Antigravity or Amp instead of Claude Code, and what changes when you do.