policy.yaml
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.
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
| Field | Type | Default | Meaning |
|---|---|---|---|
loops.<name> | mapping | none | Sets attempts and wall_clock_s for one named loop. See Loop keys. |
default | mapping | required; 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_concurrent | integer | 3 | How 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_stuck | boolean | true | Whether a stuck stop auto-dispatches an escalation turn on a node that declares no escalation of its own. |
auto_escalate_stuck_cap | integer | 3 | How many times one stuck run is auto-escalated before it is left for you. |
auto_escalate_delay_s | integer | 0 | Seconds to wait after the triggering event before an auto-escalation fires, so a person about to look is not preempted. |
auto_review_attempts | integer | 1 | How many automated-review attempts one pending gate may spend before it is left for you. |
forge_cli_timeout_s | number | 120 | Seconds one forge CLI call (gh, glab, git) may run before Kraft kills it and reports a forge error. Read at startup. |
forge_poll_s | integer | 300 | Seconds between the MR-closed poller's ticks, which ask the forge about every item parked at a merge-request node. Minimum 30. |
findings.loop_severities | list | [critical, important] | Which review-finding severities burn a fix cycle; lower ones are recorded and shown at the human-review gate. |
budget.work_item_usd | number | null (off); shipped 10 | Dollar cap per work item across all its loops. Remove the key to turn it off. |
budget.daily_usd | number | null (off); shipped 50 | Dollar cap for every work item on this instance since local midnight. Remove the key to turn it off. |
rate_limit_retries | integer | 5 | How many times Kraft relaunches a work item after a rejected API rate limit before stopping and waiting for you. |
archive.after_days | integer | unset (off); 0 is also off; shipped 30 | Completed 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.limit | size | unset (off) | The most disk worktrees may use. Over it, a start that needs a new worktree waits. See Storage keys. |
storage.worktrees.quota | size | 80% of limit | Over it, Kraft warns and nothing else. Needs a limit. |
storage.worktrees.auto_cleanup.min_age | age | unset (off); 24h when the block is empty | Over limit, Kraft archives finished items that ended at least this long ago, oldest first. Needs a limit. |
triggers | list | none | Moved to intake.yaml; see Retired and moved keys. |
defaults | mapping | unset | Starting points that any scope may move within maxima. See defaults and maxima. |
maxima | mapping | unset (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:
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,GorT, in either case, and binary:1Gis 1024³ bytes. A bare number such as10is refused, because it would be a 10-byte limit. - Without
limitnothing is measured, warned about or held.quotawithout alimit, or at or above it, is invalid. - Kraft counts
worktrees/,sandbox-git/andsandbox-home/underrun/: 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. Overquota,/api/healthandkraft admin doctorwarn. - Over
limit, a start that would create a new worktree is queued until usage is back under the limit, and its item gets awork_item_storage_heldevent. A resume or retry of an item that already has a worktree is not held. - Unless
auto_cleanupis set, Kraft never archives or deletes anything for the limit.archive.after_daysworks as before. See Limit the disk worktrees use. auto_cleanupturns on automatic clean-up.auto_cleanup: {}ismin_age: 24h, andauto_cleanup:with nothing after it is off. It needs alimit.min_ageis a whole number andhord, in either case:12h,24h,2d. Days are the largest unit, so1wis refused, and so is a bare24.0hmeans no floor.- Over
limit, Kraft archives completed and abandoned items that have a worktree and ended at leastmin_ageago, oldest first, until usage is at or underquota. 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_archivedevent carriesby: autoandreason: storage. - When that cannot free enough, because every finished item is newer than
min_ageor the rest is live work, starts stay held andkraft admin doctorsays 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:
| Key | Names |
|---|---|
<node id>.fix_loop | The fix loop of that exec node; see Fix loop. |
<node id>.on_base_changed | The restarts of that node when a rebase moves the base. |
<gate id>_reject_loop | The rejections of that gate. |
loops:
verification.fix_loop: { attempts: 5, wall_clock_s: 7200 }
Retired and moved keys
| Key | Replaced by | When a file still has it |
|---|---|---|
triggers | schedules: in intake.yaml; see Inbound triggers | Read, so an upgraded file keeps firing; kraft admin doctor names the move. New ones go in intake.yaml. |
wait_timeout_minutes | total_time_cap_minutes, or maxima.tasks.total_time_cap_minutes in policy.yaml | Read, 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:
# 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 adefaultsentry 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.
| Field | Type | Default | Meaning |
|---|---|---|---|
timeout_minutes, max_attempts, allowed_harnesses | integer, integer, list | unset | Operational starting points that any layer may move in either direction, bounded only by maxima. |
escalation_harness | string | claude | The 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_grants | list | [git-commit, git-rebase, git-push] | The grants every escalation turn holds; [] is none. In defaults only, like escalation_harness. |
work_item, nodes, steps, tasks | mapping | unset (unbounded) | Per-level values for time_cap_minutes, total_time_cap_minutes, token_budget and budget_usd; a gate is a node. |
maxima.allowed_tools | list | unset (no bound) | A safety field that starts at its maximum and can only be narrowed. |
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.
| Field | Type | Rule down the layers | Meaning |
|---|---|---|---|
allowed_tools | list | Only narrows. | Tool names the agent may use; the permission gate answers from it. Unset allows every tool, and [] allows none. See Tool lists. |
deny_tools | list | Only accumulates. | Tool names the permission gate denies, on top of allowed_tools. |
grants | list | Only 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. |
sandbox | mapping | Set 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_budget | integer | Within 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_usd | number | As token_budget. | The same cap in dollars, under budget.work_item_usd and budget.daily_usd. |
allowed_harnesses | list | Within maxima. | Harness profiles an agent task may select; Kraft refuses others at intake and again at launch. |
escalation_harness | string | Any 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_minutes | integer | Within maxima; execution node or broader. | Bound the node's fix loop; a step, task or gate refuses them. |
time_cap_minutes | integer | As token_budget. | Running time of the scope; a launch past it is refused and a running process is killed at it. |
total_time_cap_minutes | integer | As 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:
| Harness | Under a list |
|---|---|
gemini, antigravity | Refuses to launch, because it has no tool-list capability. |
codex | Enforces the list through Kraft's permission hook, but its web tools never reach the hook, so a list that leaves out WebSearch is refused. |
cursor | Enforces 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.
Related
- Caps and budgets: how caps, maxima and layers combine.
- Permission gate: how tool lists and grants are enforced.
- Chain file keys: where a chain, node, step or task sets its own
policy:.