Concepts
Kraft's whole vocabulary is five words: work item, chain, node, hook point, adapter — plus two ways a chain stops: gate and cap.
Work item
A work item is one unit of work and produces one merge request. You create
one from the UI, the CLI (kraft item create), or an MCP tool
(create_work_item). It always lands paused — see
Agent integration for why an agent can never skip that.
Chain
A work item enters as a chain: an ordered list of nodes, materialized
from a YAML template in templates/. Which template it uses is either named
explicitly with --chain (kraft item create "..." --chain quick-task) or
comes from the repo's default_chain_template in repos.yaml.
The simplest shipped template, quick-task, is three nodes with no gates at all:
id: quick-task
nodes:
- { id: env_setup, tasks: [on.env.prepare], gate_after: null }
- { id: implementation, tasks: [on.implementation.start], gate_after: null }
- { id: verify, tasks: [on.test.run], gate_after: null }
The shipped default template is the real one — spec and plan gates, a fix
loop on test failures, a rebase at the start of every node that writes or measures code, CI
watched with its own fix loop, a human-review gate, then merge:
id: default
nodes:
- { id: spec, steps: [[on.mr.rebase], [on.spec.requested]], gate_after: spec_approval }
- { id: plan, steps: [[on.mr.rebase], [on.plan.requested]], gate_after: plan_approval }
- { id: chain_review, tasks: [on.chain.review_ready], gate_after: chain_finalized, auto_escalate: true }
- { id: implementation, steps: [[on.mr.rebase], [on.env.prepare], [on.implementation.start], [on.repos.scan]], gate_after: null }
- { id: verify, steps: [[on.mr.rebase], [on.env.prepare], [on.test.run, on.review.local.run]], fix_loop: verify_fix_loop, gate_after: null }
- { id: open_mr, steps: [[on.mr.rebase], [on.mr.describe], [on.mr.open]], gate_after: null, rebase_bounce_to: verify }
- { id: mr_checks, steps: [[on.ci.poll], [on.review.mr.run]], fix_loop: ci_fix_loop, gate_after: null, rebase_bounce_to: verify }
- { id: human_review, tasks: [on.human_review.requested], gate_after: human_review_approval, reject_to: implementation }
- { id: merge, steps: [[on.mr.sync], [on.merge]], gate_after: null, rebase_bounce_to: verify }
- { id: post_merge_watch, tasks: [on.merge.watch], gate_after: null }
A node's work is one or more groups of tasks. tasks: [a, b] is a single
group: a and b are dispatched together and the node is measured once both
have settled. steps: [[a], [b]] is two groups, run in order — b is not
dispatched at all if a fails, and it sees whatever a left behind. A node
declares one key or the other, never both.
Reach for steps when the second task needs the first task's result: the
default chain's implementation node runs the implementing agent and then
scans submodules, so the scan reads a worktree the agent has actually
touched rather than one it has not started on.
Every node that authors or measures code rebases onto the fetched tip of the
target branch as its first step. A work item can run for hours while other work
merges, and a spec written against stale code propagates into the plan and the
implementation, where a later rebase does not undo it. The rebase is cheap and
does nothing when the branch is already current. implementation and verify
then re-run on.env.prepare, because a rebase can land a new lockfile.
open_mr is the one node that stops on it: if its rebase moves the branch, the
merge request is not opened and the chain returns to verify, because the tests
that passed measured a base that no longer exists.
A node's optional fields change how the chain behaves around it:
| Field | Means |
|---|---|
gate_after |
Names a gate id. After this node runs, the chain halts and the work item becomes needs_human until someone approves, rejects, or steers. |
fix_loop |
Names a loop in policy.yaml's loops: map. A failure here re-runs the node instead of escalating, up to that loop's attempts/wall_clock_s cap. |
on_failure |
A repair pass for this node — extra hook points dispatched once, after every task in the node has settled and the node still failed, before the next fix-loop attempt. Not a retry. A repair that is really about one task belongs on that task's binding instead (see Configuration) — it travels with the task into every chain and costs one task's re-dispatch rather than the whole node's. |
rebase_bounce_to |
If this node's own git operation actually moves the branch, the chain jumps back to the named node (almost always verify) instead of continuing over a diff nothing has re-tested. |
reject_to |
Where a gate's "reject with a note" re-enters the chain — human_review's rejection walks back to implementation with the reviewer's note as the steer. |
auto_escalate |
Notifies a person immediately when this node's gate opens, instead of waiting quietly on the board for someone to notice. |
auto_escalate_stuck |
Whether this node stopping for a reason that is not a pending gate — a fix loop out of attempts — dispatches an escalation turn before a human is asked. A different mechanism and a different trigger from auto_escalate; defaults on. |
auto_escalate_delay_s |
Seconds to hold either escalation back after the event that triggered it, so a human already about to look at the board is not preempted by an agent. 0, the default, fires immediately. |
Hook point and adapter
Each node names one or more hook points — on.test.run, on.mr.open,
on.review.local.run — and each hook point is bound to an adapter by
templates/registry.yaml. Four kinds of adapter exist:
agent(src/kraft/adapters/) — runs a headless coding agent in a git worktree, on whichever harness the binding names (claudeby default;codexandgeminialso ship), optionally with a named skill and an artifact it's expected to produce (a spec, a plan, a review brief).subprocess(src/kraft/adapters/subprocess.py) — runs a literal command, likeon.test.run's[uv, run, pytest, -q].builtin(src/kraft/builtins.py) — work Kraft does itself in Python: preparing a worktree, scanning for touched submodules, rebasing before the merge request opens.forge(src/kraft/adapters/forge/) — talks to GitHub or GitLab (backend: autoresolves per repo from theforgerecorded in that repo'srepos.yamlentry): opening the merge request, polling CI, syncing, merging, watching the post-merge pipeline.
Rebinding a hook point to a different adapter, or a different skill, is a
registry.yaml edit — see Configuration.
Composing a template
A custom template doesn't have to restate the shipped ones. extends: <id>
starts from another template's already-resolved node list, then remove,
insert_before, and insert_after edit it — never both extends and a
nodes: list on the same template:
id: quick-task-with-security-review
extends: quick-task
insert_after: { verify: [{ id: security_review, tasks: [on.review.security.run] }] }
(on.review.security.run is a real hook, registered but in no shipped
chain — see Configuration.)
remove names node ids to drop (unknown ids reject at load); insert_before/
insert_after are maps of an existing node id to a list of new node dicts
spliced in beside it (an unknown anchor id, or a naming collision with an
existing node, also rejects at load). A chain resolves extends recursively,
so a template can extend a template that itself extends another — but not
itself, directly or through a cycle.
Gate
A gate is where a human decision belongs. A node with gate_after: <id>
stops the chain the moment it finishes; the work item's status becomes
needs_human and the board shows it under Needs you. From there:
- Approve — the chain continues to the next node.
- Reject, with a note — the chain re-enters at the node named by
reject_to(or re-runs the same producing node if none is set), carrying the note as a steer. - Steer, without rejecting — for a paused-mid-flight item, not a gate.
Cap
Every retry loop is bounded. policy.yaml's loops: map names each loop's
attempts and wall_clock_s ceiling — verify_fix_loop, ci_fix_loop,
ci_wait, rebase_bounce, rebase_conflict. A top-level default: key,
sibling to loops: rather than inside it, covers any fix_loop name not
listed there. Hitting either bound stops the chain and escalates to a person
with the full trace, rather than looping forever on a defect the agent can't
actually fix.
Where this is enforced
docs/intent/ states what the system is supposed to do as numbered
requirements, each with an enforced-by: line naming the test that pins it —
see ARCHITECTURE.md.