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. Everything it knows is in one SQLite database. This page shows how
those parts fit, and where in src/kraft/ to change each one.
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.
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 one tool per CLI verb. 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, 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 cron triggers, pick up ready beads when auto-intake is on, fire delayed auto-escalations, and archive old ended items. - 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
kraft item createposts toPOST /api/work-items(api/routes/work_items.py).executor/entry.pyruns intake: it resolves the chain (templates/), freezes it and the effective policy onto a new row (store/work_items.py), and copies attachments torun/attachments/. The item landspaused.kraft item resume(api/routes/lifecycle.py) claims the item with a conditional update frompausedtoactive, checks the slot limit in the same statement, and spawnsexecutor.runas a task.executor/walk.pyloads the chain, cuts the worktree (builtins.py), and walks. For each exec node,executor/dispatch.pyruns its steps. An agent task goes throughadapters/agent.py, which builds the command line from the harness profile (harness.py,harnesses/*.yaml), thenadapters/subprocess.py, which launches it, watches its log, and reads its result file and usage.- At a gate node,
executor/gates.pysetsneeds_humanand writesgate_requested. The walk task ends. The event reaches the board over the WebSocket and, if you configured one, an outbound notification (notify.py). kraft item approve(api/routes/gates.py) checks for open must-fix threads, sets the itemactiveagain, and spawns a new walk at the node after the gate.- Forge nodes (
adapters/forge/) open the draft merge request and read CI. A pending pipeline is an external wait (waits.py): the item goeswaiting, the walk task ends, and the wait scheduler re-enters the walk when the next check is due. - 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 itemcompletedand closes its beads (executor/entry.py).
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.
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 workspace member is a worktree of its connected repository
(_setup_submodules in builtins.py), so host git in it reads that
repository's config, not a gitdir under the root's worktree. It also reads the
member's own config.worktree when that repository sets
extensions.worktreeConfig. A sandboxed container sees each member as it
sees the root: its refs go through a ref store of its own (prepare_stores in
worker/refstore.py), and its .git and the commondir, gitdir and
config.worktree of its admin directory are mounted read-only
(_gitdir_mounts in worker/backends/docker.py), from paths derived from the
connected repository and never read from the member's .git. For a sandboxed item,
refuse_planted_repos in executor/stops.py checks each member against the
checkout Kraft made (foreign_members in worker/sandbox.py) before host git
runs there, and a task's launch mounts exactly the checkout that check
returned. Every other sandboxed launch (a gate review, an escalation turn, a
review reply, the setup command) takes its checkout from sandbox_checkout,
which runs the same check and leaves out any member that fails it, so the
launch does not start. Each directory from the worktree down to a member is
mounted onto itself, so no container can rename it and put another .git in
the member's place.
Module map
Every path below is under src/kraft/.
Serving
| Path | What it does |
|---|---|
cli/ | The kraft command: item, view, repo and admin verb groups, and starting the server. |
client/ | The one HTTP client. transport.py is the wire, actions.py changes things, reads.py reads, context.py knows which item a worker stands in. |
mcp.py | kraft admin mcp: one MCP tool per client call. |
api/__init__.py | Builds the FastAPI app, installs middleware, serves the SPA. |
api/startup.py | The lifespan: opens the database, reattaches sessions, starts the pollers. |
api/deps.py | Helpers shared by routes, including spawn, which starts a walk. |
api/perimeter.py | Who may call the API at all. |
api/routes/ | One module per resource: work_items, lifecycle (pause, resume, retry, skip, abandon, complete, cancel, escalate), gates, review, board, artifacts, sessions (logs, permission requests, the WebSocket), repos, settings, harnesses, search, auth, check. |
api/config_check.py | Checks a config file the way saving it would. |
ws.py | Fans committed events out to WebSocket clients. |
render.py | Terminal formatting for the CLI. |
Running a chain
| Path | What it does |
|---|---|
executor/entry.py | Intake, attachments, and closing beads when an item completes. |
executor/walk.py | The walk: node by node, with recovery, the fix loop, escalation and every stop. |
executor/dispatch.py | Runs a node's steps and tasks, measures the result, collects findings, and asks the fix-loop judge. |
executor/gates.py | Opening gates, approval and rejection, agent gate review, and automatic escalation of stuck stops. |
executor/stops.py | Writes each kind of stop, and guards every claim so an active item always has a walk behind it. |
executor/resuming.py | Resuming after a restart, and addressing a steer to paused tasks. |
executor/retry.py, store/forks.py, templates/forks.py | A retry forks the run at a task, step or node path. |
executor/prompts.py | The text Kraft adds to an agent's prompt: steers, failures, findings, review threads, progress. |
executor/context.py | Walk results and the Steer object. |
executor/fallback.py | Moving a rate-limited or unavailable agent task to its next fallback: entry. |
executor/read_only.py | Checks that a read_only step changed nothing. |
builtins.py | Work Kraft does itself: the worktree, setup, rebases, changed-test scopes. |
waits.py | External waits and the scheduler that re-enters them. |
rate_limit_retry.py | Re-enters a rate_limited item after its reset. |
caps.py, cap_levels.py | Time caps per scope, and the poller that stops parked items. |
policy.py | Loads policy.yaml: loops, caps, budget, triggers, escalation settings. |
findings.py | The finding schema reviewers report, and its identity across rounds. |
escalate.py | Escalation turns: the agent a stopped item hands to. |
auto_escalate_delay.py | Fires a delayed automatic escalation. |
gate_review.py | An agent reviewing a gate before a person sees it. |
review.py | The one producer of "what changed" for the gate viewer and review agents. |
review_reply.py | The agent that answers review threads. |
node_runs.py | Pins the commit each node run started and ended on. |
progress.py | Reads a plan's ## Task N headings for "Task 3 of 6". |
Templates
| Path | What it does |
|---|---|
templates/models.py | The typed chain schema: tasks, steps, nodes, chains, canonical paths. |
templates/library.py | Loads the template directory and resolves extends. |
templates/environment.py | Workspaces, areas, targets and harness profiles a chain runs against. |
templates/revision.py | Plan-driven chain revision. |
templates/retry.py | Policy-bounded overrides a retry may carry. |
templates/catalogue.py, templates/positions.py | The library as a list for the UI, and YAML positions for editor errors. |
overrides.py | Validates per-item model and per-node overrides. |
skill.py, skills/ | The method an agent follows (spec, plan, code review, and so on), injected through its system prompt. |
Agents and workers
| Path | What it does |
|---|---|
harness.py, harnesses/*.yaml | A harness: how to launch one agent CLI, described as data. |
adapters/agent.py, adapters/profiles.py | Resolve an agent task to a harness and profile, and build its command line and context. |
adapters/subprocess.py | Launch a session, watch its log, read its result file, settle its status. |
adapters/artifact_notes.py | What each document kind is for, in the prompt. |
adapters/hook_install.py, permission_hooks.py, permission_rules.py, grants.py, registration.py | The permission gate across harnesses: hook install, tool policy written into each CLI's config, named grants, and the permission tool's name. |
usage.py, logs.py | Token, cost and time per session, and reading a session's log. |
worker/env.py, worker/steering.py | A worker's environment, and the standards injected into its prompt. |
worker/reattach.py | Adopting sessions that survived a restart. |
worker/worktree_read.py | Reading a file an agent wrote, safely. |
worker/backends/, worker/sandbox.py | Sandboxed sessions in Docker or Podman, and host git hardening. |
worker/egress.py, worker/channel.py, worker/ca.py, worker/inject.py, worker/callback.py | The egress proxy for sandboxed sessions: per-session channels, its CA, injected credentials, and what a worker's callbacks may reach. |
worker/refstore.py | A sandboxed worker's private ref store. |
worker/session_mcp.py, worker/shim.py, worker/bin/kraft | The MCP server and kraft shim a sandboxed worker calls back through. |
Forge and trackers
| Path | What it does |
|---|---|
adapters/forge/run.py | Picks the backend and runs a forge node against one or several repos. |
adapters/forge/gh.py, adapters/forge/glab.py | GitHub through gh, GitLab through glab. |
adapters/forge/git.py, adapters/forge/mr.py, adapters/forge/ci.py, adapters/forge/models.py | Local git, the merge request's title and body, reading a pipeline, and the shared types. |
automated_review.py | Which automated reviewer a repo expects. |
adapters/beads.py, intake.py | bd calls, and auto-intake from bd ready. |
triggers.py | Cron triggers from policy.yaml. |
State and configuration
| Path | What it does |
|---|---|
db.py | The schema, its migrations, and the single-writer Database. |
events.py | Appending to and reading the events table. |
store/ | Every query, by table: work_items, chain, gates, sessions, counters, budget, review, repos, forks. _common.py holds the rule that an ended item never becomes runnable. |
paths.py | $KRAFT_HOME and the run/ layout. |
config.py, config_schemas.py | Reading and writing the YAML files, and their JSON Schemas for editors. |
capabilities.py | What this version can do that an older seeded config cannot. |
index/ | The search index: chunking, local embeddings, ingest, and the query service. |
analytics.py | The numbers behind the Analytics view. |
archive.py | Auto-archive of old ended items. |
notify.py | Outbound notifications. |
auth.py | Password login and sessions for off-localhost access. |
doctor.py, update.py, init.py | kraft admin doctor, update and init. |
intent.py | Checks the intent tree in docs/intent/ (a CI check, not part of the server). |
Outside src/kraft/: the SPA is in frontend/src/ (views/ per screen),
the packaged default templates are in templates/, the agent plugins are in
plugins/, the VS Code extension is in vscode/, and contributor scripts are
in dev/.
Where to change things
| To | Change |
|---|---|
| Add a CLI verb or MCP tool | A function in client/actions.py or client/reads.py, its verb in cli/item.py or cli/view.py, its tool in mcp.py, and the CLI reference. |
| Add an API route | A handler on api_router in the matching api/routes/*.py. A new module is imported in api/__init__.py. |
| Add a harness | A YAML file in harnesses/. See Adding a harness. |
| Add a builtin action | A member of BuiltinAction in templates/models.py, its branch in executor/dispatch.py, and the work in builtins.py. |
| Add a forge action or backend | ForgeAction in templates/models.py and adapters/forge/run.py; a backend beside gh.py and glab.py. |
| Change a shipped chain or library component | templates/chains/*.yaml and templates/library.yaml at the repo root. Run kraft admin templates lint. |
| Change what an agent is told | The skill in skills/<name>/SKILL.md, or Kraft's own notes in executor/prompts.py. |
Add a policy.yaml key | PolicyInput and Policy in policy.py, the default in templates/policy.yaml, and Policy. |
Add a repos.yaml key | RepoEntry in config.py, and Repos. |
| Change the database | A new numbered migration in db.py, and the queries in store/. |
| Change how a stop is handled | executor/walk.py and executor/stops.py. |
| Add a status transition | The write in store/, through write_status in store/_common.py, and the door in api/routes/lifecycle.py. |
Add a doctor check | doctor.py. |
| Change a screen | frontend/src/views/. |
Before a pull request, see Contributing.
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 and CA. templates/ holds the YAML the
Settings screens edit. templates/ is seeded from the packaged defaults on
first run and never overwritten after, so an upgrade cannot clobber an edited
policy. See Configuration for what's in those
files.