Devplane

Configuration

The complete devplane.toml reference. Every key is read by the code; an unknown key is a parse error rather than a silent default.

devplane.toml lives at the repository root and is committed. That is the point: the commands that decide whether work is finished are the project’s, reviewed like anything else, and never something an agent wrote for itself.

A repository with no devplane.toml still works — the gates are empty, no rule auto-decides anything, and nothing changes.

Devplane reads this file and never writes it. devplane check prints it back in a terminal; , on the board does the same for every registered repository at once, including the one whose file stopped parsing. There is no settings form on purpose: an agent here runs as you, so a write path to [policy] would be the widening path the gate exists to close.

This is the whole file format. Every key below is read by the code and there are no others. An unknown key is a parse error, not a silent default — so one key that does not exist fails the whole file and takes that repository’s permission rules down with it. That is why the reference is checked by a test rather than written by hand.

The complete file

[project]
name          = "saas"
base_branch   = "main"    # discovered from origin/HEAD when unset
default_agent = "claude"  # what `agent = "any"` means here

[workspace]
setup   = "pnpm install --frozen-lockfile"   # run in a new tree
include = [".env", ".env.local"]             # repo-relative paths

[gates]
check   = ["pnpm typecheck", "pnpm lint", "pnpm test -- --run"]
timeout = "10m"           # for the whole gate, not per command
on_fail = "feedback"      # feedback | escalate | ignore
max_feedback_rounds = 2

[gates.named.repro]       # asked for by name from a pipeline step
run     = ["pnpm test -- --run tests/repro"]
expect  = "fail"          # one that passes proved nothing
timeout = "2m"            # optional; the project's otherwise

[policy]                  # evaluated deny → ask → allow
never_auto = ["Bash(rm -rf *)", "Read(.env)"]
always_ask = ["Bash(git push *)"]
auto_allow = ["Read", "Bash(pnpm test *)", "Edit(src/**)"]
max_parallel_runs = 2     # pieces of work with an agent in them
stall_timeout     = "12m" # how long this work may be quiet

[budget]                  # what work may spend before asking
default_usd = 10
feature_usd = 25
max_turns   = 60          # counted here, so it binds every agent
max_runtime = "45m"       # likewise

[transcripts]
keep = true               # the default

[spec]                    # words that mark a question your spec has not answered
open_questions = ["NEEDS CLARIFICATION", "TBD"]

[github]                  # off by default: a push is visible
pull_request = true
draft        = true
ready_label  = "devplane:ready"

[pipelines.feature]
steps = [
  { role = "implement", prompt = "implement", gate = "check" },
  { role = "review", agent = "codex", prompt = "review",
    findings = { back_to = "implement", max = 2 } },
  { role = "verify", prompt = "write-tests", gate = "check" },
  { human = "merge" },
]
devplane check          # what will this file actually do?
devplane check ../lib   # somewhere else
devplane check --json   # exits non-zero on an error; for CI

check is offline and daemon-free, so it runs in a repository nothing is connected to yet.

[project]

KeyTypeDefaultMeaning
namestringthe directory namewhat the board calls it
base_branchstringdiscovered from origin/HEADwhat worktrees branch from
default_agentstringclaudewhat agent = "any" means, and what work start uses

[workspace]

KeyTypeDefaultMeaning
setupstringnonerun once in a new worktree, before the agent starts
includelist of pathsemptygitignored files copied into a new worktree

setup is not a gate: pnpm install says nothing about whether the work is done, so its report is recorded separately.

include takes paths, not globs, and a path that leaves the repository is refused — the file is committed, so it arrives with somebody else’s code, from a fork, a pull request, or a vendored dependency.

Refused by spelling and by where it points:

  • include = ["../../.ssh/id_rsa"] and absolute paths are rejected outright.
  • A symlink out of the repository is rejected too. Git tracks symlinks, so config/local.env -> /home/you/.ssh/id_rsa is a file a repository can ship; following it would copy a private key into the directory an agent is about to read. The source is resolved and must still be inside the repository.
  • Nothing is ever written through a symlink at the destination. The worktree is a checkout of the same repository, so a committed dangling symlink is already sitting there — copying through it would write to whatever it names, anywhere on your disk.

A symlink that stays inside the repository works normally. The rule is containment, not a ban on symlinks.

[gates]

KeyTypeDefaultMeaning
checklist of commandsemptythe definition of done; all must succeed, in order
timeoutduration10mfor the whole gate
on_failfeedback · escalate · ignorefeedbackwhat happens when it is red
max_feedback_roundsinteger2how many times failures go back before a person is asked

Durations are written the way people say them: 30s, 10m, 1h30m. A bare number is seconds.

[gates.named.<name>]

KeyTypeDefaultMeaning
runlist of commandsrequiredthe commands
expectpass · failpassfail inverts the verdict
timeoutdurationthe project’soptional override

check always resolves by that name. Anything else has to be declared, and asking for a gate nobody wrote is an error rather than a silent pass.

See Verified done for how gates run.

[policy]

KeyTypeDefaultMeaning
never_autolist of rulesemptyrefused; never overridable by auto_allow
always_asklist of rulesemptyput in front of a person, whatever else matches
auto_allowlist of rulesemptyanswered without asking anyone
max_parallel_runsintegerunlimitedpieces of work with an agent in them
stall_timeoutdurationthe machine’show long this project’s work may be quiet

The rule syntax has a page of its own.

max_parallel_runs counts pieces of work with an agent in them, not runs. A declared chain is one unit however many steps it has taken — its steps share a worktree and run one after another, so they are not the colliding agents the limit exists to prevent. Sessions you started yourself are never counted.

stall_timeout is per project because the number is a statement about the work: a repository whose suite takes twelve minutes and one that answers in seconds cannot share a threshold. Unset inherits the machine’s rather than defaulting to one of its own.

[budget]

Three bounds on two kinds of axis. Work that passes any of them stops and raises a cost_spike item naming which one.

KeyTypeMeaningAlways fires?
default_usdnumberany kind that names no figure of its ownno
quick_usd, chore_usd, bug_usd, feature_usdnumberper kindno
max_turnsnumberagent turns this work may takeyes
max_runtimedurationhow long it may run, from when it startedyes

Zero means “off”, not “a ceiling of nothing”.

Why two of them are marked “no”

A ceiling in dollars only bites when the agent reports what it spent, and that is optional twice over. The Agent Client Protocol marks the cost field optional. And the OpenTelemetry GenAI semantic conventions — the dialect GitHub Copilot and Codex emit — have no notion of money anywhere in them, so a run observed that way reports tokens, models and durations and no dollars. That is not an agent declining to answer; it is a schema with nowhere to put the answer.

max_turns and max_runtime exist because of that. A turn is an API request Devplane saw itself, and elapsed time is arithmetic on the work’s own timestamp, so neither depends on an agent’s cooperation and both bind every agent on every provider. They are checked before the money bound.

If a _usd ceiling is the only bound you set, devplane check warns — and devplane work show says whether your agent reports a cost at all, rather than printing $0.00 and letting you assume you are covered. A guard that may not fire has to say so.

[transcripts]

KeyTypeDefaultMeaning
keepbooltruewhether what a driven agent says in this repository is written down

On by default, because such a run has no window of its own and the board would otherwise be able to say that a tool ran and not one word about why — and because the protocol streams the text to Devplane regardless, so discarding it was never a privacy measure.

Off writes nothing, including the prompts Devplane itself sent, and does not stop the agent reading anything. Sessions Devplane merely watches are unaffected either way.

[spec]

KeyTypeDefaultMeaning
open_questionslist of stringsemptywords that mark a question the specification has not answered

Counted per gate for the specification a piece of work names with --spec, alongside its - [ ] task list, and shown on the board beside the verdict.

The words are yours. NEEDS CLARIFICATION is Spec Kit’s spelling, TBD is everybody’s, and the next tool will have a third — so the list is empty unless your repository writes one. Matched per line and case-insensitively.

Nothing else about the specification is interpreted: the outline is the Markdown headings and the progress is the boxes, because those are the only things the frameworks in this category agree on.

[github]

KeyTypeDefaultMeaning
pull_requestboolfalseopen one when the gates pass
draftbooltrueopen it as a draft
ready_labelstringnonewhich issues devplane issues --ready offers

Off by default because pushing a branch is the first thing Devplane does that other people can see.

[pipelines.<kind>]

Keyed by the work kind it serves: quick, chore, bug, feature.

[gates]
check = ["cargo test"]

[gates.named.repro]
run    = ["cargo test --test repro"]
expect = "fail"

[pipelines.bug]
steps = [
  { role = "reproduce", prompt = "repro", gate = "repro" },
  { role = "fix", prompt = "fix", gate = "check" },
  { role = "review", agent = "any", prompt = "review",
    findings = { back_to = "fix", max = 1 } },
  { human = "merge" },
]
KeyMeaning
rolewhat the step is for, and the name back_to refers to
agentan agent id, or any for the project’s default
prompta template in .devplane/prompts/<name>.md, or the text itself
gatecheck, or a name from [gates.named]
findings.back_toan earlier role, which must declare a gate
findings.maxhow many times work may go back
findings.filewhere the reviewer writes them (default .devplane/findings.md)
findings.onlywords that make a finding worth returning the work for; empty (the default) means every finding counts
humansuspends the chain until devplane work approve

A step is one or the other: setting both role and human is an error, and so is giving a human step a prompt.

See Pipelines.

Machine-wide files

~/.devplane/policy.toml takes the same [policy] shape, so a rule moves between the two by cutting and pasting it.

~/.devplane/agents.toml adds agents by name — see Driving agents.

Environment

VariableWhat
DEVPLANE_HOMEwhere the database, token and daemon record live (default ~/.devplane)
DEVPLANE_PORTthe port serve asks for (default 47831)
DEVPLANE_CLAUDE_BINthe claude binary, when it is not where Devplane looks
DEVPLANE_NOTIFY0 turns desktop notifications off
DEVPLANE_LOGtracing filter, e.g. devplane=debug
DEVPLANE_UIserve the board from this file instead of the copy compiled into the binary — for working on the page
CLAUDE_CONFIG_DIRwhich Claude Code config connect writes to

Something wrong or out of date? Edit this page.