nextv2.0.0
Configuration

policy.yaml

Every field in policy.yaml: caps, budget, concurrency, archiving, defaults and maxima, and the policy fields a layer may set.

policy.yaml sets caps, budget, concurrency, archiving, and defaults for every work item. It lives at $KRAFT_HOME/config/policy.yaml.

kraft admin reload rereads this file with the same validation as startup. If the file does not validate, Kraft keeps the running policy and reload exits 1 naming why. New loop caps apply to loops that start after the reload.

For how caps, maxima and layers combine, see Caps and budgets.

config/policy.yaml
loops: {}
default: { attempts: 3, wall_clock_s: 3600 }

findings:
  loop_severities: [critical, important]

max_concurrent: 3

budget:
  work_item_usd: 10
  daily_usd: 50

rate_limit_retries: 5
forge_cli_timeout_s: 120

archive:
  after_days: 30

The shipped file also holds defaults: and maxima: as comments; see defaults and maxima.

Top-level keys

FieldTypeDefaultMeaning
loops.<name>mappingnoneSets attempts and wall_clock_s for one named loop. See Loop keys.
defaultmappingrequired; shipped {attempts: 3, wall_clock_s: 3600}The attempts and wall_clock_s ceiling for every fix loop not named in loops; a max_attempts on the loop itself wins. Without it policy.yaml does not load, and Kraft refuses new work.
max_concurrentinteger3How many work items may be active at once across all repos, however they were started. A start past the limit is queued, not refused.
auto_escalate_stuckbooleantrueWhether a stuck stop auto-dispatches an escalation turn on a node that declares no escalation of its own.
auto_escalate_stuck_capinteger3How many times one stuck run is auto-escalated before it is left for you.
auto_escalate_delay_sinteger0Seconds to wait after the triggering event before an auto-escalation fires, so a person about to look is not preempted.
auto_review_attemptsinteger1How many automated-review attempts one pending gate may spend before it is left for you.
forge_cli_timeout_snumber120Seconds one forge CLI call (gh, glab, git) may run before Kraft kills it and reports a forge error. Read at startup.
forge_poll_sinteger300Seconds between the MR-closed poller's ticks, which ask the forge about every item parked at a merge-request node. Minimum 30.
findings.loop_severitieslist[critical, important]Which review-finding severities burn a fix cycle; lower ones are recorded and shown at the human-review gate.
budget.work_item_usdnumbernull (off); shipped 10Dollar cap per work item across all its loops. Remove the key to turn it off.
budget.daily_usdnumbernull (off); shipped 50Dollar cap for every work item on this instance since local midnight. Remove the key to turn it off.
rate_limit_retriesinteger5How many times Kraft relaunches a work item after a rejected API rate limit before stopping and waiting for you.
archive.after_daysintegerunset (off); 0 is also off; shipped 30Completed and abandoned items older than this auto-archive. The board's Done header shows the number, and no auto-archive note when the key is absent.
storage.worktrees.limitsizeunset (off)The most disk worktrees may use. Over it, a start that needs a new worktree waits. See Storage keys.
storage.worktrees.quotasize80% of limitOver it, Kraft warns and nothing else. Needs a limit.
storage.worktrees.auto_cleanup.min_ageageunset (off); 24h when the block is emptyOver limit, Kraft archives finished items that ended at least this long ago, oldest first. Needs a limit.
triggerslistnoneMoved to intake.yaml; see Retired and moved keys.
defaultsmappingunsetStarting points that any scope may move within maxima. See defaults and maxima.
maximamappingunset (no bound)The ceiling on policy overrides. See defaults and maxima.

A budget cap refuses to start the next agent task and cannot interrupt one that is running. See Caps and budgets.

Storage keys

storage.worktrees bounds the disk the worktrees use:

config/policy.yaml
storage:
  worktrees:
    limit: 10G
    quota: 8G
    auto_cleanup:     # optional; the block being there turns it on
      min_age: 24h
  • A size is a whole number and a unit, K, M, G or T, in either case, and binary: 1G is 1024³ bytes. A bare number such as 10 is refused, because it would be a 10-byte limit.
  • Without limit nothing is measured, warned about or held. quota without a limit, or at or above it, is invalid.
  • Kraft counts worktrees/, sandbox-git/ and sandbox-home/ under run/: what archiving an item removes. Logs, databases, attachments and Docker's images and volumes are not counted.
  • The figure is measured at startup and every 10 minutes, and matches du. An archive or abandon takes that item's size off it at once.
  • At or under quota, nothing happens. Over quota, /api/health and kraft admin doctor warn.
  • Over limit, a start that would create a new worktree is queued until usage is back under the limit, and its item gets a work_item_storage_held event. A resume or retry of an item that already has a worktree is not held.
  • Unless auto_cleanup is set, Kraft never archives or deletes anything for the limit. archive.after_days works as before. See Limit the disk worktrees use.
  • auto_cleanup turns on automatic clean-up. auto_cleanup: {} is min_age: 24h, and auto_cleanup: with nothing after it is off. It needs a limit.
  • min_age is a whole number and h or d, in either case: 12h, 24h, 2d. Days are the largest unit, so 1w is refused, and so is a bare 24. 0h means no floor.
  • Over limit, Kraft archives completed and abandoned items that have a worktree and ended at least min_age ago, oldest first, until usage is at or under quota. One archive runs at a time, and one item's failure is logged and skipped.
  • Archiving removes the worktree and any uncommitted changes in it. A branch with unpushed commits is kept, and the item stays restorable under Archived. Its work_item_archived event carries by: auto and reason: storage.
  • When that cannot free enough, because every finished item is newer than min_age or the rest is live work, starts stay held and kraft admin doctor says how many finished items were too recent.
  • A running item can pass the limit between two measurements. The limit stops the next start, not work in flight.

kraft admin reload applies a change.

Loop keys

loops maps a loop key to its own attempts and wall_clock_s. A key that names no live loop is silently unused, so spell it exactly:

KeyNames
<node id>.fix_loopThe fix loop of that exec node; see Fix loop.
<node id>.on_base_changedThe restarts of that node when a rebase moves the base.
<gate id>_reject_loopThe rejections of that gate.
config/policy.yaml
loops:
  verification.fix_loop: { attempts: 5, wall_clock_s: 7200 }

Retired and moved keys

KeyReplaced byWhen a file still has it
triggersschedules: in intake.yaml; see Inbound triggersRead, so an upgraded file keeps firing; kraft admin doctor names the move. New ones go in intake.yaml.
wait_timeout_minutestotal_time_cap_minutes, or maxima.tasks.total_time_cap_minutes in policy.yamlRead, with a warning in the log. In defaults it does not load. A save of a chain that sets it, or of a work item's override, is refused with the name to use.

defaults and maxima

Kraft ships these commented out, because an uncommented value is a real ceiling. This example is illustrative. Uncomment and edit it to use it:

config/policy.yaml
# defaults:
#   timeout_minutes: 60
#   max_attempts: 3
#   allowed_harnesses: [codex, claude]
#   escalation_harness: claude
#   work_item: { time_cap_minutes: 480, total_time_cap_minutes: 2880, budget_usd: 20 }
#   nodes:     { time_cap_minutes: 180 }
#   steps:     { time_cap_minutes: 120 }
#   tasks:     { time_cap_minutes: 90 }
# maxima:
#   timeout_minutes: 180
#   allowed_harnesses: [codex, claude]
#   work_item: { time_cap_minutes: 1440, token_budget: 2000000, budget_usd: 25 }
#   tasks:     { time_cap_minutes: 240, total_time_cap_minutes: 10080 }

defaults takes timeout_minutes, max_attempts, allowed_harnesses, escalation_harness, escalation_grants and the per-level caps. maxima takes allowed_tools, allowed_harnesses, timeout_minutes, max_attempts and the per-level caps.

Two rules hold for both sections:

  • A narrower level's cap may not exceed a broader one's (tasks <= steps <= nodes <= work_item), and a defaults entry may not exceed its level's maximum. Kraft refuses either when it reads the file, naming both.
  • A cap written flat (defaults: time_cap_minutes: 30) is refused, with a message naming the level form to use.
FieldTypeDefaultMeaning
timeout_minutes, max_attempts, allowed_harnessesinteger, integer, listunsetOperational starting points that any layer may move in either direction, bounded only by maxima.
escalation_harnessstringclaudeThe harness profile in harnesses.yaml that an escalation turn runs on, or item. In defaults only: a maxima that names it does not load, and Kraft refuses new work.
escalation_grantslist[git-commit, git-rebase, git-push]The grants every escalation turn holds; [] is none. In defaults only, like escalation_harness.
work_item, nodes, steps, tasksmappingunset (unbounded)Per-level values for time_cap_minutes, total_time_cap_minutes, token_budget and budget_usd; a gate is a node.
maxima.allowed_toolslistunset (no bound)A safety field that starts at its maximum and can only be narrowed.
Do not set maxima.allowed_tools unless every harness you use can run under it. A safety field set only in maxima binds every task. See Tool lists for the harnesses that refuse a list.

Policy fields

These keys go in a policy: block on a repo (repos.yaml), a chain, a node, a step, a task, or a work item's override. Kraft applies the layers from the broadest to the narrowest. The "Rule down the layers" column says how a layer may change what it inherits: the safety fields only narrow, and the operational ones move either way within maxima. Kraft refuses a layer that breaks its row's rule, and names the scope.

FieldTypeRule down the layersMeaning
allowed_toolslistOnly narrows.Tool names the agent may use; the permission gate answers from it. Unset allows every tool, and [] allows none. See Tool lists.
deny_toolslistOnly accumulates.Tool names the permission gate denies, on top of allowed_tools.
grantslistOnly accumulates; an item override can drop one, never add one.Named git operations a task is guaranteed: git-commit, git-rebase, git-push. See Grants.
sandboxmappingSet once, never changed or removed, its resources included.Runs every process of the whole work item in docker run, within its resource limits. Two scopes with different sandboxes are refused. See Sandboxed workers.
token_budgetintegerWithin its level's maxima; a child's never above its parent's.Tokens (input plus output) the scope's launches may spend before the next launch is refused.
budget_usdnumberAs token_budget.The same cap in dollars, under budget.work_item_usd and budget.daily_usd.
allowed_harnesseslistWithin maxima.Harness profiles an agent task may select; Kraft refuses others at intake and again at launch.
escalation_harnessstringAny value, execution node or broader.The harness profile an escalation turn runs on; a name harnesses.yaml does not define is refused.
max_attempts, timeout_minutesintegerWithin maxima; execution node or broader.Bound the node's fix loop; a step, task or gate refuses them.
time_cap_minutesintegerAs token_budget.Running time of the scope; a launch past it is refused and a running process is killed at it.
total_time_cap_minutesintegerAs time_cap_minutes.Wall-clock time of the scope, including waits and gates, less only a manual pause. For a wait it is the wait's timeout.

A launch into a sandbox with neither network: nor unrestricted_network: true is refused. Which tool policies a sandbox refuses depends on its network:; see Tool policy.

Tool lists

allowed_tools and deny_tools hold tool names, never permission rules. Use a bare tool (Bash, Read) or one exact MCP tool (mcp__kraft__report_progress). Kraft refuses a scoped rule (Bash(git *)), a glob (mcp__github__*) or a whole server (mcp__github) wherever the list is read, and the message names the field and the name to write instead.

What a harness does under an allowed_tools list:

HarnessUnder a list
gemini, antigravityRefuses to launch, because it has no tool-list capability.
codexEnforces the list through Kraft's permission hook, but its web tools never reach the hook, so a list that leaves out WebSearch is refused.
cursorEnforces the list through the hook, but its web tools never reach it, so a list that leaves out WebFetch and WebSearch is refused.

Allowing Bash allows its read-only use without a gate ask. Claude runs a read-only shell command (cat, ls, git status) itself, so it can read any file the worktree holds, gitignored ones included.

Copyright © 2026