Skip to main content

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 returns timeout so the agent can poll again
max_parallel_tasks = 1 # maximum number of concurrent executor sessions
max_executor_dispatches = 6 # executor (re)spawns per work phase before state → Failed

[lease]
ttl_secs = 90 # how long a claimed lease is valid without renewal
heartbeat_interval_secs = 30 # how often agents should call heartbeat

[spec]
directory = "docs/specs" # where /create_spec writes approved specs

[repository_graph] # optional; NOT scaffolded by `ferrus init`
enabled = true # opt into the local repository graph + project memory

[hq.supervisor]
agent = "claude-code" # agent for supervisor/reviewer role: claude-code | codex | qwen-code | goose | opencode
model = "" # optional override; empty = agent default

[hq.executor]
agent = "codex" # agent for executor role: claude-code | codex | qwen-code | goose (experimental); opencode executor is experimental/unstable
model = ""

--agents-path (default .agents) is a one-time flag to ferrus init that picks where skill files are written — it is not persisted in ferrus.toml.

[checks]

The check gate is how ferrus decides whether the executor's work is actually done. These commands run in the active task workspace, in order, and must all exit with status 0.

Full stdout + stderr is persisted to .ferrus/logs/check_<attempt>_<scope>_<ts>.txt, where the task/run scope prevents parallel checks from overwriting each other. 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",
]
tip

Check commands should be fast and deterministic. If a single check takes minutes, the loop will spend most of its time waiting.

[limits]

KeyWhat it bounds
max_check_retriesHow many consecutive check failures the executor may hit before the task moves to Failed.
max_review_cyclesHow many reject → re-implement cycles a task can go through before Failed.
max_feedback_linesTrailing lines of each failing command shown inline.
wait_timeout_secsMax duration of a single wait_* MCP call. On timeout the tool returns so the agent can poll again.
max_parallel_tasksHow many tasks may have an executor running at the same time. Each task still advances through its own independent state.
max_executor_dispatchesHow many times HQ will (re)spawn an executor for a single task within one work phase before giving up and marking it Failed. Bounds the respawn loop when a session hits its turn limit and exits without submitting; the counter resets on each fresh rejection back to Addressing.

[lease]

Only one executor works on a given task at a time. The mechanism is an advisory lease claimed atomically in SQLite (ferrus.db):

  • ttl_secs — the lease expires if not renewed.
  • heartbeat_interval_secs — how often the executor calls heartbeat.

If an executor crashes, the lease naturally expires and a new executor can be resumed with /resume or ferrus recover.

[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 selected spec and milestone are tracked per task in ferrus.db.

[spec]
directory = "docs/specs" # any path inside the project; created on first write

[repository_graph]

The optional repository graph and project memory indexes. This namespace is not written by ferrus init — add it yourself to opt in:

[repository_graph]
enabled = true

Everything else has a default. Omitting the namespace entirely means the optional indexes stay off and ordinary orchestration is unaffected.

Namespaces

NamespacePurposeAffects snapshot identity
[repository_graph]enabled, backend selectionNo — operational
[repository_graph.source]Included content, untracked/generated/vendor policy, ordered ignore rules, sensitive pathsYes
[repository_graph.analyzers]Enabled extractors and their semantic settingsYes
[repository_graph.index_limits]Limits that can include, skip, or truncate a file during extractionYes
[repository_graph.query_limits]Result, byte, depth, duration, snippet, and diagnostic budgetsNo
[repository_graph.retention]Snapshot and failed-build cleanup policyNo
[repository_graph.memory]Authorized project-memory sources and extraction policyMemory revision only
[repository_graph.semantic]Reserved for future embedding/chunking policySemantic projection only
[repository_graph.remote]Reserved endpoint, credential reference, upload policyNo
[repository_graph.telemetry]Privacy-safe query metricsNo

The "affects snapshot identity" column is the one that matters day to day: changing a query budget or retention rule never invalidates your index, while changing what gets indexed does. Configuration is hashed as a canonical semantic projection, not as raw TOML — whitespace, key ordering, and explicitly writing out a default all produce the same digest. Credentials, tokens, and endpoints never participate in identity and never appear in diagnostics.

[repository_graph.source]

[repository_graph.source]
include = ["**/*"] # set-like; order is not significant
rules = [".git/**", ".ferrus/**"] # ordered ignore/negation rules; order IS significant
sensitive = [ # never read, never extracted, never uploaded
"**/.env", "**/.env.*",
"**/*.key", "**/*.pem", "**/*.p12", "**/*.pfx",
"**/id_rsa", "**/id_ed25519",
]
include_untracked = true
include_generated = false
include_vendor = false

Sensitive-path policy is applied before extraction, not after.

[repository_graph.index_limits]

Bounds one indexing pass. Defaults:

KeyDefault
max_files100000
max_directories100000
max_file_bytes2097152 (2 MiB)
max_total_bytes536870912 (512 MiB)
max_facts_per_file100000
max_parser_duration_ms2000
max_resolved_relationships1000000
max_resolver_duration_ms10000
max_diagnostics1000

[repository_graph.query_limits]

Bounds every CLI and MCP read. The configured cap always wins over a caller's requested budget; exceeding one returns the deterministic prefix that fits plus an explicit truncation reason.

KeyDefault
max_results100
max_bytes262144 (256 KiB)
max_snippet_bytes32768 (32 KiB)
max_depth3
max_duration_ms2000
max_diagnostics50

[repository_graph.retention]

[repository_graph.retention]
max_snapshots = 5
max_failed_builds = 10

Snapshots pinned by an active task are retained regardless.

[repository_graph.telemetry]

[repository_graph.telemetry]
enabled = true

Emits one structured tracing metric per query: tool name, repository/task/run/ snapshot identities, freshness, duration, result count, response bytes, truncation reason, diagnostic count, and error category. The metric types cannot represent request text, filters, repository paths, snippets, or source bodies. Off by default.

[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. See Supported agents for the full backend list, including the experimental goose and opencode adapters.

Runtime files

Ferrus separates human-readable project artifacts from machine-local runtime state. SQLite is the runtime source of truth; Markdown files are scoped task intent and run artifacts, not a mirrored state machine.

PathContents
.ferrus/Project-local templates, task/run artifacts, agent registry, and logs
~/.ferrus/projects/<project-id>/Machine-local project metadata, the ferrus.db SQLite database, and global logs

.ferrus/

FileContents
project.tomlLocal pointer to ~/.ferrus/projects/<project-id>/
agents.jsonRuntime registry for agent sessions, statuses, PIDs, and log ownership
TASK.mdTask drafting template
CONSULT_TEMPLATE.mdRead-only consultation request template
SPEC_TEMPLATE.mdRead-only feature specification template
tasks/<task-id>.mdNumbered task intent artifact
runs/<task-id>/SUBMISSION.mdExecutor submission notes
runs/<task-id>/REVIEW.mdSupervisor review or rejection notes
runs/<task-id>/QUESTION.md / ANSWER.mdHuman-in-the-loop Q&A
runs/<task-id>/CONSULT_REQUEST.md / CONSULT_RESPONSE.mdSupervisor consultation pair
runs/<task-id>/PATCH.diffPatch produced from an isolated executor workspace
runs/<task-id>/INTEGRATION_ERROR.mdRecoverable patch or integration-check failure context
logs/Scoped check output and PTY session logs per agent

~/.ferrus/projects/<project-id>/

FileContents
project.tomlProject id, name, workspace path, .ferrus path, git metadata, timestamps, schema version
ferrus.dbSQLite source of truth for tasks, runs, events, leases, counters, and project runtime state
repo-graph.dbDerived repository graph facts. Optional and rebuildable — safe to delete
project-memory.dbDerived project memory revisions. Optional and rebuildable — safe to delete
archive/specs/<spec-slug>-<closed-at>/Completed-spec archives written by /archive-spec: manifest.toml, a copy of spec.md, and the relocated tasks/ and runs/ artifacts
logs/Reserved for machine-local logs that should not be committed

ferrus init automatically adds .ferrus/ to your .gitignore. On HQ startup, ferrus marks dead active runs as interrupted, preserves leases backed by live runs, releases other expired leases, and resumes recoverable task flows — the same recovery ferrus recover runs on demand. Migrating an existing pre-0.3 project (STATE.json-based) is a one-time ferrus migrate — see the migration guide.