Agent contract¶
The coding agent is any shell command: a CLI coding tool or a plain command. It must be able to:
- read the task from the
FORGEO_TASKenvironment variable, - work on the repository from the current working directory,
- report its outcome through its exit code.
Anything a CLI agent can do works:
agent_command: "claude -p \"$FORGEO_TASK\""
Environment¶
The agent is launched with the repository as its working directory, and the process environment is augmented as follows:
| Variable | Meaning |
|---|---|
FORGEO_TASK |
The full instruction for this run: the project context when task_context is configured, then title, blank line, description, and an "Acceptance criteria:" list when present. |
FORGEO_REPO |
The absolute path of the repository. |
FORGEO_BRANCH |
The branch everything is committed to (default main). |
every agent_env key |
Any extra variables from agent_env in the config. |
| inherited environment | The daemon's own environment. |
FORGEO_* variables are set unconditionally and take precedence over both the
inherited environment and agent_env.
The task is not the whole picture¶
A task description is isolated by design: it describes one unit of work, not
the project. When task_context is set (see
Configuration), Forgeo prepends the contents
of that file — the high-level project overview — to FORGEO_TASK before the
task, under a # Project context heading, followed by the task under a
# Task heading. The file is re-read on every run, so an agent's own updates
to it are seen by the next cycle.
For a refactoring run (empty backlog) the same contract applies: the refactor
prompt arrives as FORGEO_TASK (with the context prepended when configured),
with the task id REFACTOR and title "Refactoring pass".
Exit codes¶
The exit code decides the outcome of the run:
| Exit code | Outcome | What happens |
|---|---|---|
0 |
SUCCESS | Everything is committed (git add -A && git commit) with the message <title> (#<id>), pushed when a remote is set, and the task is marked COMPLETED. |
no_changes_exit_code (default 3) |
SUCCESS, no changes | The agent explicitly reports the task needs no code change: the task is marked COMPLETED without a commit (and the run record notes why). Only accepted when the working tree is clean. |
blocked_exit_code (default 2) |
BLOCKED | The agent needs a human decision. Partial work is committed as <title> [partial], the agent's reason is persisted on the task (blocker_reason), optional Telegram and/or webhook notifications are sent, and the task is marked BLOCKED. BLOCKER.md is rendered from the backlog's BLOCKED tasks on the next cycle — real per-task reasons, never generic text — and disappears once the last one is resolved (reopen it from the web console). |
| anything else | ERROR | Changes are discarded (git reset --hard + git clean -fd), the failure is logged, and the task is marked FAILED. |
The blocked exit code is configurable via blocked_exit_code in
forgeo.yaml, and the no-change exit code via
no_changes_exit_code.
A FAILED task stays FAILED until a human reopens it — unless the retry
policy is enabled (failed_retry_max, see Configuration),
in which case Forgeo moves the task back to OPEN after
failed_retry_wait_cycles cycles and runs it again, incrementing its retry
count. BLOCKED is never retried automatically: it always waits for a human.
The no-change contract¶
Forgeo cannot tell "the agent deliberately made no changes" from "the agent
did nothing". A SUCCESS exit that produces no changes is therefore not a
valid completion for a task:
- exiting
0while leaving the working tree unchanged is retried immediately (no_changes_retry_max, see Configuration) and, once that budget is spent, the task is markedBLOCKED— neverFAILEDand never silentlyCOMPLETED. The only acceptable outcome for a run that ends without code changes is a blocked task awaiting human review; - to complete a task without touching the code, exit
no_changes_exit_code(default3). The working tree must be clean — an agent that reports "no changes" while leaving uncommitted work behind fails instead.
Refactoring passes are the exception: when the backlog is empty, a refactor that finds nothing to improve is a normal, successful run (the default refactor prompt already says "if nothing needs refactoring, make no changes").
Timeouts¶
agent_timeout_seconds (optional) kills the agent process after the given
number of seconds:
- on timeout the process is killed and the task fails as an ERROR
(
error: timed out after <n>s); - output captured before the kill is kept in the run log;
- when unset (
null), the agent runs to completion — there is no default timeout.
A run that overruns interval_minutes is never killed by the schedule. The
daemon skips the next iteration instead: only one agent runs at a time, and an
iteration that wakes up while the previous run is still active is skipped, not
interrupted.
Tip
Set agent_timeout_seconds to something comfortably above your expected
agent runtime. For long interactive agents it is often safer to leave it
unset and rely on the skip-on-overlap behavior.
Output¶
The agent's stdout and stderr are captured live, prefixed with [stdout] /
[stderr], and retained in the run result. To keep memory bounded, only the
last 1000 lines are kept for a chatty agent. On a BLOCKED result, the
agent's questions (falling back to the captured output lines) are stored on
the task as blocker_reason and become the "what the agent needs" section of
BLOCKER.md (up to the last 10 lines), rendered on the next cycle.
Git contract¶
The agent should not commit or push anything itself — Forgeo does that, based on the exit code. The working contract is:
- make your changes in the repository;
- do not run
git add,git commit,git push, or reset the tree; - exit
0to have your changes committed and pushed as one commit; - exit
no_changes_exit_codewhen the task needs no code change (never exit0with an empty tree — that run is retried and then markedBLOCKED); - exit
blocked_exit_codeto have partial work preserved and a blocker written; - exit anything else to have your changes discarded.
A good way to keep this contract front and center is to embed it in the
agent_command prompt itself, e.g.:
agent_command: >
opencode run --auto "Work on the repository at the current working directory.
Make the code changes requested below and nothing else. Do NOT run
git commit, git push, or git add -A — Forgeo commits your work.
Verify with the test suite where applicable.
Read AGENTS.md at the start of the session, and CONTEXT.md if present;
if your change materially affects the project overview or conventions,
update AGENTS.md and CONTEXT.md accordingly.
$FORGEO_TASK"
Concurrency¶
Only one agent runs at a time per forgeo:
- the daemon holds a per-forgeo lock (
backlog.lock) — a secondstartoronceis refused while it is held; - each cycle holds a per-iteration lock (
backlog.run) — a wake-up that finds a run still in progress is skipped.