Policy
policy.yaml sets caps, budget, archiving, and defaults for every work item.
It lives at $KRAFT_HOME/templates/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.
The shipped file sets these keys:
loops: {}
default: { attempts: 3, wall_clock_s: 3600 }
findings:
loop_severities: [critical, important]
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 triggers:, defaults: and maxima: as
comments. Kraft ships them commented out because an uncommented value is a
real ceiling. This example is illustrative. Uncomment and edit it to use it:
# triggers:
# - cron: "0 9 * * 1,2,3,4,5"
# repo: /path/to/repo
# chain: default
# title: "Weekday dependency check"
#
# 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 }
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. gemini refuses to
launch under any tool list, and codex and cursor refuse a list that leaves
out the web tools their permission hook never sees.
Top-level keys
| Key | Default | Effect |
|---|---|---|
loops.<name> | none | Sets attempts and wall_clock_s for one named loop; a key that names no live loop is silently unused. |
default | (required); the shipped file sets {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 | 3 | How many work items may be active at once across all repos, however they were started. |
auto_escalate_stuck | true | Whether a stuck stop auto-dispatches an escalation turn on a node that declares no escalation of its own. |
auto_escalate_stuck_cap | 3 | How many times one stuck run is auto-escalated before it is left for a human. |
auto_escalate_delay_s | 0 | Seconds to wait after the triggering event before an auto-escalation fires, so a human about to look is not preempted. |
auto_review_attempts | 1 | How many automated-review attempts one pending gate may spend before it is left to a human. |
forge_cli_timeout_s | 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 | 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 | [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 | off (null); shipped file sets 10 | Dollar cap per work item across all its loops; remove the key to turn it off. |
budget.daily_usd | off (null); shipped file sets 50 | Dollar cap for every work item on this instance since local midnight; remove the key to turn it off. |
rate_limit_retries | 5 | How many times Kraft relaunches a work item after a rejected API rate limit before stopping for a human. |
archive.after_days | off if the key is absent; shipped file sets 30 | Completed and abandoned items older than this auto-archive; keep it in sync with the number the board's Done header shows. |
triggers | none | Cron-fired chain starts; see Inbound triggers. |
defaults | unset | Starting points for timeout_minutes, max_attempts, allowed_harnesses, escalation_harness, escalation_grants and the per-level caps; any scope may move them within maxima. |
maxima | unset (no bound) | The administrator ceiling on policy overrides, with the same keys as defaults plus allowed_tools. |
A budget cap refuses to start the next agent task and cannot interrupt one that is running. See Caps and budgets.
defaults and maxima
| Key | Effect |
|---|---|
timeout_minutes, max_attempts, allowed_harnesses | Operational starting points that any layer may move in either direction, bounded only by maxima. |
escalation_harness | The harnesses.yaml profile an escalation turn runs on, or item; unset is claude. |
escalation_grants | The grants every escalation turn holds; unset is git-commit, git-rebase and git-push, and [] is none. Set it in defaults only. |
work_item, nodes, steps, tasks | Per-level values for time_cap_minutes, total_time_cap_minutes, token_budget and budget_usd; a gate is a node. Unset means unbounded. |
maxima.allowed_tools | A safety field that starts at its maximum and can only be narrowed. |
A narrower level's cap may not exceed a broader one's
(tasks <= steps <= nodes <= work_item). 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
too, with a message naming the level form to use.
Policy fields
| Field | Rule down the layers | Effect |
|---|---|---|
allowed_tools | Only narrows. | Lists tool names the agent may use; the permission gate answers from it, and unset allows every tool while [] allows none. |
deny_tools | Only accumulates. | Tool names the permission gate denies, on top of allowed_tools. |
grants | 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 | 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. A tool policy that needs Kraft's permission hook (Cursor, Codex) is refused under it. See Sandboxed workers. |
token_budget | 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 | As token_budget. | The same cap in dollars, under budget.work_item_usd and budget.daily_usd. |
allowed_harnesses | Within maxima. | Profiles an agent task may select; Kraft refuses others at intake and again at launch. |
escalation_harness | Any value, execution node or broader. | The profile an escalation turn runs on; a name harnesses.yaml does not define is refused. |
max_attempts, timeout_minutes | Within maxima; execution node or broader. | Bound the node's fix loop; a step, task or gate refuses them. |
time_cap_minutes | 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 | 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. |
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.
A harness with no tool-list capability (gemini) refuses to launch under an
allowed_tools list rather than run unrestricted. codex and cursor enforce
the list through Kraft's permission hook, but their
web tools never reach the hook, so they refuse a list that leaves out
WebSearch (codex) or WebFetch and WebSearch (cursor). 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.
wait_timeout_minutes is retired. Kraft refuses it and names
total_time_cap_minutes.