Architecture
Kraft is one Python process on your machine. It serves a React SPA and an HTTP API, walks each work item's chain, and spawns a coding agent or a command for each task.
This page is for contributors and for anyone who wants to know what runs
where. It shows how the parts fit and where each one is enforced in the code.
Everything Kraft knows is in one SQLite database, run/orchestrator.db, beside
a search index it can rebuild.
For what a work item does, step by step and status by status, read
How a work item runs first. For the words,
see Vocabulary. For which module under src/kraft/
holds what, see the Source map.
The parts
- One process.
kraft(orkraft admin start) runs uvicorn with the FastAPI app fromsrc/kraft/api/. The same process serves the built SPA, answers the API under/api, pushes events over a WebSocket, and runs every work item's walk as an asyncio task. There is no queue, broker or second server. - Clients. The SPA, the
kraftCLI and the MCP server all call the same HTTP API. The MCP server iskraft admin mcp: a separate stdio process that your coding agent starts, with a tool for mostitemandviewverbs and forrepo connect(every tool butpermission_requestis also a CLI subcommand). Both go throughsrc/kraft/client/, so a verb and its tool cannot drift apart. - The executor.
src/kraft/executor/walks a work item's frozen chain node by node. It runs builtin and forge tasks inside the process and launches agent and subprocess tasks as child processes. It is started by an API request (resume, retry, approve, reject, skip) or by a poller. - Worker sessions. Each agent task is a headless agent CLI (Claude Code, Codex, Gemini, Antigravity, Cursor, OpenCode or Amp) started in the item's git worktree, with an environment built from an allowlist. With a sandbox configured, it runs in a container (Docker or Podman) instead, and reaches the network only through Kraft's egress proxy. A worker talks back to Kraft for permission decisions, plan progress and review replies.
- Pollers. Background tasks in the same process re-enter items that are
waitingorrate_limited, stop parked items whose time cap ran out, fire schedules, pick up ready issues from beads when auto-intake is on, fire delayed auto-escalations, archive old ended items, and notice a merge request closed on the forge without merging. - Stores.
run/orchestrator.dbholds work items, sessions, gates, review threads and an append-onlyeventstable. All writes go through one writer queue (src/kraft/db.py). The WebSocket, notifications and the search index each follow theeventstail.run/index.dbis the full-text and vector search index over your repos' documents. - Outside tools. Kraft calls
gitfor worktrees and rebases,ghorglabfor merge requests and CI, andbdfor beads, each as a subprocess with your own login.
From filing to merge
The table follows one work item. Its second column gives the numbered steps of the diagram above.
| Step | What happens | Diagram | Where in src/kraft/ |
|---|---|---|---|
| 1. File | kraft item create posts the item. Intake resolves its chain, freezes the chain and the effective policy onto a new row, and copies attachments. The item lands paused. | 1 to 2 | api/routes/work_items.py, executor/entry.py, store/work_items.py; chains in config/chains/; copies in run/attachments/ |
| 2. Resume | kraft item resume claims the item with a conditional update from paused to active, checks the slot limit in the same statement, and starts its walk as a task. | 3 to 5 | api/routes/lifecycle.py, executor.run |
| 3. Walk | The walk loads the chain, cuts the worktree, and runs each node's steps. An agent task builds its command line from the harness profile, then launches it, watches its log, and reads its result file and usage. | 6 to 8 | executor/walk.py, builtins.py, executor/dispatch.py, adapters/agent.py, harness.py, harnesses/*.yaml, adapters/subprocess.py |
| 4. Gate | At a gate node the item becomes needs_human and Kraft writes gate_requested. The walk task ends. The event reaches the board over the WebSocket and, if you configured one, a notification. | 9 to 10 | executor/gates.py, notify.py |
| 5. Approve | kraft item approve checks for open must-fix threads, sets the item active again, and starts a new walk at the node after the gate. | 11 to 14 | api/routes/gates.py |
| 6. Merge request | Forge nodes open the draft merge request and read CI. A pending pipeline is an external wait: the item goes waiting, the walk task ends, and the wait scheduler re-enters the walk when the next check is due. | 15 to 16 | adapters/forge/, waits.py |
| 7. Finish | After final_review, forge nodes mark the merge request ready, wait for any required approval, merge, and watch the post-merge pipeline. The walk marks the item completed and closes its issues in beads. | 17 to 19 | adapters/forge/, executor/entry.py |
What a restart does to running items
Pause an item before you run kraft admin stop or restart. A clean stop
ends each running agent, and the item then stops and waits for you. See
A restart ends running agents
for the commands.
A restart does not lose finished work. On startup, worker/reattach.py finds
the sessions that were running, adopts the ones still alive, and resumes each
item's walk from the node and step it had reached. A session outlives a crash
or a kill -9 of the server, because each agent runs in its own process
group.
A clean stop is different: it cancels every item's task, and the task kills
its agent's process group on the way out. After a kraft admin stop or
restart there is nothing left to adopt.
Trust boundaries
Four lines matter. The Security page states what each one does and does not stop by default; this is only where each one lives in the code.
| Boundary | Enforced by |
|---|---|
| Network clients to the API | api/perimeter.py (the local-client check and auth middleware), auth.py (password login and sessions), the bearer token in run/ |
| A worker to Kraft | The permission gate (api/routes/sessions.py, permission_hooks.py, permission_rules.py, grants.py), and the self-action guard: client/context.py in the CLI and MCP server, and forbid_self_action in api/deps.py for a worker that identifies itself |
| A worker to your machine | Nothing, unless the repo sets sandbox:. Then worker/backends/docker.py, worker/sandbox.py, worker/refstore.py, assert_on_branch in adapters/forge/git.py before every host-side commit, rebase or push from its worktree, and for network: the egress proxy in worker/egress.py, worker/channel.py and worker/inject.py |
| Kraft to your forge | Your own gh or glab login, used as is by adapters/forge/ |
A sandbox covers a workspace's members as it covers the root. Where each hold lives in the code is under Workspace members on the Source map.
State on disk
State lives in $KRAFT_HOME (default ~/.kraft). run/ holds the databases,
session logs and result files, worktrees, attachment copies, the process id
file, and the sandbox's sockets, CA and ref stores. config/ holds the YAML the
Templates and Settings screens edit. Kraft seeds config/ from the shipped
defaults on first run and never overwrites it, so an upgrade cannot clobber an
edited policy.
Data and privacy lists every
file in run/ with its mode. Configuration lists
what is in config/.
Related
- Source map: which module under
src/kraft/holds what, and where to change a given thing. - Security: what each trust boundary does and does not stop by default.
- How a work item runs: the flow above in terms of statuses and gates.