Configuration
ferrus reads a single ferrus.toml at the root of your project. ferrus init
scaffolds it with sensible defaults; tune it to your build.
ferrus.toml reference
[checks]
commands = [
"cargo clippy -- -D warnings",
"cargo fmt --check",
"cargo test",
]
[limits]
max_check_retries = 20 # consecutive check failures before state → Failed
max_review_cycles = 3 # reject→fix cycles before state → Failed
max_feedback_lines = 30 # trailing lines per failing command shown in /check and /submit output
wait_timeout_secs = 60 # max duration of one wait_* tool call before it polls again
[agents]
path = ".agents" # root directory for agent skill files
[spec]
directory = "docs/specs" # where /spec writes approved feature specifications
[lease]
ttl_secs = 90 # how long a claimed lease is valid without renewal
heartbeat_interval_secs = 30 # how often agents should call heartbeat
[hq.supervisor]
agent = "claude-code" # claude-code | codex | qwen-code
model = "" # optional override; empty = agent default
[hq.executor]
agent = "codex"
model = ""
[checks]
The check gate is how ferrus decides whether the executor's work is
actually done. These commands run from the directory where ferrus serve
was started, in order, and must all exit with status 0.
Full stdout + stderr is persisted to
.ferrus/logs/check_<attempt>_<ts>.txt. Only a trailing summary
(max_feedback_lines) is inlined into the executor's feedback so task
context doesn't fill up with technical noise.
[checks]
commands = [
"pnpm lint",
"pnpm test -- --run",
"pnpm typecheck",
]
Check commands should be fast and deterministic. If a single check takes minutes, the loop will spend most of its time waiting.
[limits]
| Key | What it bounds |
|---|---|
max_check_retries | How many consecutive check failures the executor may hit before the task moves to Failed. |
max_review_cycles | How many reject → re-implement cycles a task can go through before Failed. |
max_feedback_lines | Trailing lines of each failing command shown inline. |
wait_timeout_secs | Max duration of a single wait_* MCP call. On timeout the tool returns so the agent can poll again. |
[lease]
Only one executor works on a task at a time. The mechanism is an advisory
lease claimed atomically in .ferrus/STATE.json:
ttl_secs— the lease expires if not renewed.heartbeat_interval_secs— how often the executor callsheartbeat.
If an executor crashes, the lease naturally expires and a new executor can be resumed.
[spec]
Where the /spec HQ command writes
approved feature specifications. The supervisor drafts the spec
interactively and calls create_spec to persist it as a Markdown file
under this directory. The path of the most recent spec is also written to
.ferrus/LAST_SPEC_PATH so HQ can offer it as input to the next /task.
[spec]
directory = "docs/specs" # any path inside the project; created on first write
[hq.supervisor] and [hq.executor]
Which coding agent plays which role. Change these to swap backends without touching anything else:
[hq.supervisor]
agent = "claude-code"
[hq.executor]
agent = "codex"
model = "gpt-5-codex-high" # optional; empty = agent default
Use /model inside HQ to update model overrides interactively.
Runtime files (.ferrus/)
| File | Contents |
|---|---|
STATE.json | Current state, lease fields, retry/cycle counters, schema version, timestamp |
STATE.lock | Advisory lock file for atomic claiming (do not delete) |
TASK.md | Task description written by Supervisor |
REVIEW.md | Supervisor rejection notes |
SUBMISSION.md | Executor submission notes |
QUESTION.md / ANSWER.md | Human-in-the-loop Q&A |
CONSULT_REQUEST.md / CONSULT_RESPONSE.md | Supervisor consultation pair |
SPEC_TEMPLATE.md | Read-only feature specification template used by /spec |
LAST_SPEC_PATH | Path of the last spec written by create_spec, used for HQ handoff |
logs/ | Full stdout + stderr per check run; PTY session logs per agent |
STATE.json is written atomically (write-to-tmp + rename) so a crash
mid-write never leaves it corrupt. ferrus init automatically adds
.ferrus/ to your .gitignore.