v1.4.0next
Project

Architecture

One FastAPI process, the executor inside it, the worker sessions it spawns, and a map of src/kraft/ for contributors.

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 (or kraft admin start) runs uvicorn with the FastAPI app from src/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 kraft CLI and the MCP server all call the same HTTP API. The MCP server is kraft admin mcp: a separate stdio process that your coding agent starts, with one tool per CLI verb. Both go through src/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 waiting or rate_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.db holds work items, sessions, gates, review threads and an append-only events table. All writes go through one writer queue (src/kraft/db.py). The WebSocket, notifications and the search index each follow the events tail. run/index.db is the full-text and vector search index over your repos' documents.
  • Outside tools. Kraft calls git for worktrees and rebases, gh or glab for merge requests and CI, and bd for beads, each as a subprocess with your own login.

From filing to merge

  1. kraft item create posts to POST /api/work-items (api/routes/work_items.py). executor/entry.py runs intake: it resolves the chain (templates/), freezes it and the effective policy onto a new row (store/work_items.py), and copies attachments to run/attachments/. The item lands paused.
  2. kraft item resume (api/routes/lifecycle.py) claims the item with a conditional update from paused to active, checks the slot limit in the same statement, and spawns executor.run as a task.
  3. executor/walk.py loads the chain, cuts the worktree (builtins.py), and walks. For each exec node, executor/dispatch.py runs its steps. An agent task goes through adapters/agent.py, which builds the command line from the harness profile (harness.py, harnesses/*.yaml), then adapters/subprocess.py, which launches it, watches its log, and reads its result file and usage.
  4. At a gate node, executor/gates.py sets needs_human and writes gate_requested. The walk task ends. The event reaches the board over the WebSocket and, if you configured one, an outbound notification (notify.py).
  5. kraft item approve (api/routes/gates.py) checks for open must-fix threads, sets the item active again, and spawns a new walk at the node after the gate.
  6. Forge nodes (adapters/forge/) open the draft merge request and read CI. A pending pipeline is an external wait (waits.py): the item goes waiting, the walk task ends, and the wait scheduler re-enters the walk when the next check is due.
  7. 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 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.

BoundaryEnforced by
Network clients to the APIapi/perimeter.py (the local-client check and auth middleware), auth.py (password login and sessions), the bearer token in run/
A worker to KraftThe 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 machineNothing, 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 forgeYour 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

PathWhat 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.pykraft admin mcp: one MCP tool per client call.
api/__init__.pyBuilds the FastAPI app, installs middleware, serves the SPA.
api/startup.pyThe lifespan: opens the database, reattaches sessions, starts the pollers.
api/deps.pyHelpers shared by routes, including spawn, which starts a walk.
api/perimeter.pyWho 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.pyChecks a config file the way saving it would.
ws.pyFans committed events out to WebSocket clients.
render.pyTerminal formatting for the CLI.

Running a chain

PathWhat it does
executor/entry.pyIntake, attachments, and closing beads when an item completes.
executor/walk.pyThe walk: node by node, with recovery, the fix loop, escalation and every stop.
executor/dispatch.pyRuns a node's steps and tasks, measures the result, collects findings, and asks the fix-loop judge.
executor/gates.pyOpening gates, approval and rejection, agent gate review, and automatic escalation of stuck stops.
executor/stops.pyWrites each kind of stop, and guards every claim so an active item always has a walk behind it.
executor/resuming.pyResuming after a restart, and addressing a steer to paused tasks.
executor/retry.py, store/forks.py, templates/forks.pyA retry forks the run at a task, step or node path.
executor/prompts.pyThe text Kraft adds to an agent's prompt: steers, failures, findings, review threads, progress.
executor/context.pyWalk results and the Steer object.
executor/fallback.pyMoving a rate-limited or unavailable agent task to its next fallback: entry.
executor/read_only.pyChecks that a read_only step changed nothing.
builtins.pyWork Kraft does itself: the worktree, setup, rebases, changed-test scopes.
waits.pyExternal waits and the scheduler that re-enters them.
rate_limit_retry.pyRe-enters a rate_limited item after its reset.
caps.py, cap_levels.pyTime caps per scope, and the poller that stops parked items.
policy.pyLoads policy.yaml: loops, caps, budget, triggers, escalation settings.
findings.pyThe finding schema reviewers report, and its identity across rounds.
escalate.pyEscalation turns: the agent a stopped item hands to.
auto_escalate_delay.pyFires a delayed automatic escalation.
gate_review.pyAn agent reviewing a gate before a person sees it.
review.pyThe one producer of "what changed" for the gate viewer and review agents.
review_reply.pyThe agent that answers review threads.
node_runs.pyPins the commit each node run started and ended on.
progress.pyReads a plan's ## Task N headings for "Task 3 of 6".

Templates

PathWhat it does
templates/models.pyThe typed chain schema: tasks, steps, nodes, chains, canonical paths.
templates/library.pyLoads the template directory and resolves extends.
templates/environment.pyWorkspaces, areas, targets and harness profiles a chain runs against.
templates/revision.pyPlan-driven chain revision.
templates/retry.pyPolicy-bounded overrides a retry may carry.
templates/catalogue.py, templates/positions.pyThe library as a list for the UI, and YAML positions for editor errors.
overrides.pyValidates 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

PathWhat it does
harness.py, harnesses/*.yamlA harness: how to launch one agent CLI, described as data.
adapters/agent.py, adapters/profiles.pyResolve an agent task to a harness and profile, and build its command line and context.
adapters/subprocess.pyLaunch a session, watch its log, read its result file, settle its status.
adapters/artifact_notes.pyWhat each document kind is for, in the prompt.
adapters/hook_install.py, permission_hooks.py, permission_rules.py, grants.py, registration.pyThe 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.pyToken, cost and time per session, and reading a session's log.
worker/env.py, worker/steering.pyA worker's environment, and the standards injected into its prompt.
worker/reattach.pyAdopting sessions that survived a restart.
worker/worktree_read.pyReading a file an agent wrote, safely.
worker/backends/, worker/sandbox.pySandboxed sessions in Docker or Podman, and host git hardening.
worker/egress.py, worker/channel.py, worker/ca.py, worker/inject.py, worker/callback.pyThe egress proxy for sandboxed sessions: per-session channels, its CA, injected credentials, and what a worker's callbacks may reach.
worker/refstore.pyA sandboxed worker's private ref store.
worker/session_mcp.py, worker/shim.py, worker/bin/kraftThe MCP server and kraft shim a sandboxed worker calls back through.

Forge and trackers

PathWhat it does
adapters/forge/run.pyPicks the backend and runs a forge node against one or several repos.
adapters/forge/gh.py, adapters/forge/glab.pyGitHub through gh, GitLab through glab.
adapters/forge/git.py, adapters/forge/mr.py, adapters/forge/ci.py, adapters/forge/models.pyLocal git, the merge request's title and body, reading a pipeline, and the shared types.
automated_review.pyWhich automated reviewer a repo expects.
adapters/beads.py, intake.pybd calls, and auto-intake from bd ready.
triggers.pyCron triggers from policy.yaml.

State and configuration

PathWhat it does
db.pyThe schema, its migrations, and the single-writer Database.
events.pyAppending 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.pyReading and writing the YAML files, and their JSON Schemas for editors.
capabilities.pyWhat this version can do that an older seeded config cannot.
index/The search index: chunking, local embeddings, ingest, and the query service.
analytics.pyThe numbers behind the Analytics view.
archive.pyAuto-archive of old ended items.
notify.pyOutbound notifications.
auth.pyPassword login and sessions for off-localhost access.
doctor.py, update.py, init.pykraft admin doctor, update and init.
intent.pyChecks 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

ToChange
Add a CLI verb or MCP toolA 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 routeA handler on api_router in the matching api/routes/*.py. A new module is imported in api/__init__.py.
Add a harnessA YAML file in harnesses/. See Adding a harness.
Add a builtin actionA member of BuiltinAction in templates/models.py, its branch in executor/dispatch.py, and the work in builtins.py.
Add a forge action or backendForgeAction in templates/models.py and adapters/forge/run.py; a backend beside gh.py and glab.py.
Change a shipped chain or library componenttemplates/chains/*.yaml and templates/library.yaml at the repo root. Run kraft admin templates lint.
Change what an agent is toldThe skill in skills/<name>/SKILL.md, or Kraft's own notes in executor/prompts.py.
Add a policy.yaml keyPolicyInput and Policy in policy.py, the default in templates/policy.yaml, and Policy.
Add a repos.yaml keyRepoEntry in config.py, and Repos.
Change the databaseA new numbered migration in db.py, and the queries in store/.
Change how a stop is handledexecutor/walk.py and executor/stops.py.
Add a status transitionThe write in store/, through write_status in store/_common.py, and the door in api/routes/lifecycle.py.
Add a doctor checkdoctor.py.
Change a screenfrontend/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.

Copyright © 2026