nextv2.0.0
Customize and automate

Write your own chain

Add a lint node to a copy of the default chain, with an agent that fixes what lint reports, a time cap, and no way to skip it.

You will have a chain of your own: a copy of the shipped default chain with a lint node after verification. The node runs your linter. When the linter fails, an agent fixes what it reported and the linter runs again, with a time cap, and nobody can skip it.

If you only need lint to run when certain files change, you do not need a node. Add the linter to the repo's test_scopes in repos.yaml. The shipped verification node runs every scope whose paths the change touches, and its fix loop repairs the failures:
config/repos.yaml
test_scopes:
  - paths: ["src/**"]
    command: npm test
  - paths: ["src/**/*.ts"]
    command: sh -c 'npm run lint && npx tsc --noEmit'
A scope's command runs with no shell too. See test_scopes.

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 directory: ~/.kraft/config/, or $KRAFT_HOME/config/ if you set KRAFT_HOME. You edit library.yaml in it and add a file under chains/.

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 the linter reports.

config/library.yaml
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. A failing lint becomes a critical finding that holds the end of the command's output, and the repair agent reads it.

Inside a fix loop, Kraft replaces the repair task's prompt with its own instruction. See Subprocess tasks for how the command runs and how its exit code becomes a status, and Fix loop and judge for what the repair agent is told.

2. Add the node to the library

Add a lint node to library.yaml, under nodes::

config/library.yaml
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

What each key does:

  • skippable: false: refuses kraft item skip on this node, a chain revision that proposes skipping it, and kraft item create --skip-nodes lint.
  • policy.time_cap_minutes: 30: caps the node's running time, fix loop included. Hitting it stops the item and waits for you. Kraft does not count it as a lint failure, so no repair attempt is spent. See Time caps.
  • fix_loop.tasks: runs after a failing lint, then the lint runs again. The loop repeats until lint passes or max_attempts runs out. Then the node is stuck: an escalation turn tries first, then the item stops and waits for you. The loop's wall clock comes from policy.yaml (default, or loops.lint.fix_loop). See Fix loop and judge and policy.yaml.
  • judge: optional. strict_judge is the judge task the shipped library provides, the one verification uses. See The judge.

3. Make the chain

Copy the default chain and give the copy its own id. cp -n never overwrites a file, so run it once: if chains/with-lint.yaml exists, skip the copy and keep editing it.

cd "${KRAFT_HOME:-$HOME/.kraft}/config"
cp -n chains/default.yaml chains/with-lint.yaml

In chains/with-lint.yaml, change id: default to id: with-lint. Then add the lint node right after verification. The finished file, with the unchanged nodes left out:

config/chains/with-lint.yaml
id: with-lint

nodes:
  # ... spec through implementation, unchanged

  - id: verification
    extends: verification

  - id: lint
    extends: lint

  - id: work_brief
    kind: exec
    tasks:
      - id: author
        extends: write_work_brief

  # ... the rest, unchanged

A chain file cannot extend another chain, so the copy is a full copy. It does not follow changes to the shipped default chain. See Upgrade your configuration.

4. Check it and load it

kraft admin templates lint
kraft admin reload
kraft admin templates show with-lint --resolved

A pass prints these two lines (the counts are your own):

$ kraft admin templates lint
3 chain(s), no errors
$ kraft admin reload
reloaded 3 template(s) and policy.yaml and intake.yaml
  1. lint reads the files from disk. It prints N chain(s), no errors before you reload. On an error it prints one file:line:column: message line for each issue and exits 1.
  2. reload makes the running server read the files again, with no restart. It prints invalid: lines and exits 1 for a file that does not load.
  3. show --resolved prints the chain with every library component expanded. Look for your lint node with its fix_loop, its policy and skippable: false. show asks the running server, so before the reload it answers unknown chain.

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 --chain with-lint switches an item that has not started yet.

To make it the repo's default instead, set default_chain: with-lint on the repo's entry in repos.yaml. Every item filed on that repo without a chain then gets with-lint, however it is filed: kraft item create, the MCP tool, the board's New work item composer, POST /api/work-items, POST /api/triggers, and auto-intake. Naming a chain still wins.

Check it worked

Steps 3 and 4 already show the chain loads. These checks prove it on a real item and cost a full run, so they are optional:

  1. kraft item skip ID on the lint node answers path: 'lint' does not allow skipping.
  2. File a small item on the new chain. Once it passes verification, list the lint node's findings:
    kraft view events ID --type findings_measured
    

    The output is a table with the columns SEQ, WHEN, TYPE and DETAIL. A failing lint shows a findings_measured row for node lint with one critical finding. A clean lint shows a findings_measured row with no findings. Without --type, kraft view events ID also shows what followed: a fix_cycle_started row after a failing lint, or node_completed for lint after a clean one.

Next steps

Copyright © 2026