Autonomous orchestration for AI-driven software development.
智铸:AI 驱动的软件工程自动化控制器。
Early development (Phase 2). The first real AI development loop is wired end to end: GitHub issue → Claude Code implementation → controller push and PR → OpenCode review → Claude Code remediation → repeated review →
READY_FOR_MERGE. Merging is opt-in and controller-owned: by default the loop stops atREADY_FOR_MERGEfor a human; with the merge safety gate open (safety.allow_merge: truein config and--allow-mergeon the CLI) the controller itself verifies the PR on GitHub and merges it. Agents never merge. Nothing here is production ready yet.
AutoForge is not another coding agent. It is a deterministic orchestration layer that drives existing agents through the software lifecycle and refuses to trust anything they claim without checking it against GitHub:
Issue → ANALYZE_EXECUTE (Claude Code) → controller pushes, opens PR → REVIEW (OpenCode)
→ clean ───────────────────────────────▶ READY_FOR_MERGE
→ findings ────────────────────────────▶ FIX (Claude Code) → REVIEW …
→ excessive / low-finding stagnation ──▶ REPLAN_REEXECUTE (OpenCode)
→ controller pushes, opens replacement PR
→ REVIEW round 1
├─ merge gate closed (default): stops here; human merges
└─ gate open: controller verifies on GitHub
→ MERGE (gh pr merge by the controller, no agent)
→ UPDATE_EPIC (agent returns the progress text
and the roadmap section and picks the next
issue; the controller posts the progress comment,
splices the section into the EPIC body every N
merges, reads both back, and verifies the next
issue on GitHub, or DONE)
AutoForge itself never writes business code. It holds the workflow state
machine, selects which provider, model and effort runs each phase, renders
strictly validated prompts from file templates, invokes claude /
opencode through provider adapters and gh through a typed client (argv
lists only, never a shell), parses the machine-readable CONTROL_RESULT
block from agent output, and verifies every claim on GitHub before it
advances state. Every invocation is logged, redacted, under
<state dir>/logs/<run-id>/; the state directory defaults to .autoforge/.
It is a single-machine client tool, not a distributed service: one process
on your own machine, driving your git, gh, and agent CLIs under your
credentials, with local per-checkout state and a single
OS-level repository lock instead of any coordinator. The expected failure
modes are a Ctrl-C, a killed process, and a reboot, which is why side
effects are checkpointed before they are performed. What is remote and
concurrent is GitHub and the humans acting on it, which is why nothing
GitHub-side is believed without being read back. AutoForge never commits to
the default branch.
Each line is a summary; the linked document is the precise statement.
- Read the security boundaries before a first run. Agents run as you,
with network access and your model credentials, and are not sandboxed
by default, so a deliberate read of your credential files by absolute
path is outside the credential guarantee below. On a public repository
anyone who can comment on the issue or PR an agent reads can try to
steer it, and everything an agent reads goes to your model vendor. Run
AutoForge on issues whose authors you trust, as a dedicated user or in
a VM or container, or under
sandbox.mode: required. Security boundaries lists what AutoForge protects and what it does not, the GitHub token permissions it needs and the recommended settings; SECURITY.md says how to report a vulnerability. - Agent output is a claim; the controller decides. State advances only
on a strictly validated
CONTROL_RESULTblock plus independent verification, never on prose such as "done" or "LGTM". An invalid transition fails explicitly, and when recovery cannot determine the safe state the run entersBLOCKEDrather than guess. (CONTROL_RESULT protocol, workflow) - GitHub is read back, not believed. Each phase re-reads what its agent claims to have done, or what the controller did on its behalf: the implementation commit is the worktree's own HEAD and descends from the default branch, and the PR the controller opened for it is open, at that HEAD, on the issue's branch; the review comment the controller rendered from the reviewer's result is on that PR, exactly once, for the right round and SHA; the fix commit is the fixer's own HEAD, descends from the reviewed HEAD and is the PR's head once the controller has pushed it; the follow-up issues the controller created or recorded a deferral in are the one open issue per finding; the EPIC update exists, and the next issue is open and a native sub-issue of the EPIC. (GitHub safety)
- Agents never merge. Every agent prompt carries the rule "never merge a
pull request". The merge is performed by the controller itself
(
gh pr merge --<method> --match-head-commit <reviewed HEAD>), with no agent invocation. - Merging is off by default and opt-in.
MERGEis reachable only when bothsafety.allow_merge: trueis set in config and--allow-mergeis passed on the CLI; otherwise the run stops atREADY_FOR_MERGEand a human merges. Before merging, the controller re-verifies the PR on GitHub and fails closed: a conclusive negative isBLOCKED, inconclusive data keeps the phase for a bounded number ofresume --allow-mergere-checks and then isBLOCKED, and nothing is merged in either case. By default a PR that changes a protected path (.github/workflows/) is never merged unattended, and a required check is trusted only if it ran the base branch's job and step structure.doctorchecks that the default branch still requires the CI check; the merge gate itself does not yet consult branch rules. The merge is counted only after GitHub reportsMERGEDat the reviewed HEAD into the reviewed base. (Merge safety) - Reviews are bound to a revision. A clean review is bound to the PR it
was posted on (by repository and number), its HEAD SHA, its base branch
and its merge base. If the HEAD, the base branch or the merge base moves
after the review, the run goes back to
REVIEWinstead of merging, and a different PR than the reviewed one isBLOCKED. (Review binding) - State is durable and runs are resumable. State is written atomically
and side effects are checkpointed before they are performed.
resumere-enters the interrupted phase and reconciles with GitHub first; a corrupt state file fails loudly instead of being reset. One controller runs per checkout under an OS file lock keyed by the git common dir (linked worktrees share it; independent clones are not coordinated). Review rounds, stagnation and a cumulative step budget bound every run, so it cannot loop forever. (State and recovery) - Agents are separated from your checkout and shell; sandboxing is
opt-in. An agent runs in a per-issue
git worktreethe controller creates, with an allow-listed environment rather than the operator's whole shell, and the controller blocks if its own checkout's HEAD or branch moved while the agent ran. Whatever an agent leaves running is killed when its invocation ends, and what the kill could not remove is reported rather than hidden; the orphans of a process that makes itself a subreaper, and anything handed to a service manager (systemd-run,at), escape this unseen (ADR 0002, Security boundaries). By default the agent is not sandboxed: it runs as the operator, with network access and the allow-listed provider credentials. (Usage)sandbox.mode: required(Linux,bwrap) runs every agent, validation command and pre-merge command inside an outer sandbox, or refuses it: an empty root with only the system runtime, the workspace, read-only git metadata, a private home and what the provider declares, so controller state, other worktrees and your credential files are out of reach. It still runs as your UID, and agents keep the host network. A host that cannot enforce one is refused before the step, with nothing saved, charged or launched;preferredmeansrequired;doctorshows which this host gets (ADR 0005, Usage, Configuration). - Agents hold no GitHub credential. The controller makes every GitHub
write itself; agents, validation commands and pre-merge commands get no
GitHub or git credential through anything AutoForge controls: no token
variable
ghor git reads can be allow-listed (a token you forward under another name is warned about, and is yours to keep out),ghfinds no login, credential helpers, askpass and the SSH agent and keys are switched off where repository configuration cannot switch them back on, curl and wget read no configured header, a launch's shells read none of your zsh startup files, the provider CLIs load no MCP server, plugin or hook, and a launch is refused (bydoctortoo) while a repository setting,~/.netrc, a login shell's startup file or an organization connected to OpenCode would still hand one over. The agent reads the GitHub data its phase needs from files the controller wrote, redacted and bounded. A deliberate same-user read of a credential file by absolute path is outside this guarantee. (GitHub safety) - Logs and CLI output are redacted. Tokens, API keys and authorization headers are masked (a baseline, with no claim of completeness), and no environment dump is ever written. (Secrets and logging)
Compatibility has the full matrix and the evidence for each entry.
- Linux on x86_64. macOS, Linux ARM64 and WSL2 are untested; Windows is
unsupported (AutoForge's lock needs POSIX
fcntl) - CPython 3.11 or 3.12, the versions CI tests; 3.13 and later are untested.
uvfetches one when the host has none (--python 3.12below) uv(install)git, andgh(GitHub CLI 2.48.0 or newer, authenticated): the client lists branch rules and a PR's changed files withgh api --paginate --slurp, which older releases reject;autoforge doctorrefuses an olderghup frontclaude(Claude Code CLI) foranalyze_execute/fixprofilesopencode(OpenCode CLI) 2.0.0 or newer for thereview_*,replan_reexecuteandupdate_epicprofiles; 1.x is refused bydoctor(see OpenCode profiles)pi1.0.0 or newer, only if a profile you route to usesprovider: pi, holding a DeepSeek API key or signed in with your ChatGPT subscription (see the Pi operator guide).doctorchecks only the agent CLIs your configured profiles actually reach- a state directory (
.autoforge/by default;--state-dirorstate_dirin config) on a filesystem with hard links: run logs and other controller files are published bylink(2)so that a crash never leaves one half-written, and there is no non-atomic fallback. vfat/exFAT and some FUSE, SMB/CIFS and overlay mounts have no hard links; a run, andautoforge doctor, refuse such a state directory and say why
Run autoforge doctor to check all of the above (read-only apart from
creating the state directory if it is missing and a probe file it removes).
CLI flag syntax in autoforge.example.yaml was checked against the locally
installed CLIs (Claude Code 2.1.263, OpenCode 2.0.23, gh 2.100.0); the
versions verified since are in Compatibility.
The distribution is autoforge-cli; the command it installs is
autoforge. autoforge on PyPI is an unrelated project: never
pip install autoforge.
A git commit, or a wheel built from one, installs from any commit at or
after the one that introduced autoforge-cli (#256; an earlier commit
installs a package without its prompt templates); the PyPI method installs
a published version.
From a git commit. Install the command into its own environment with
uv tool. Pin a full commit SHA, or the tag of a published version
(v<version>):
uv tool install --python 3.12 'git+https://github.com/iceboundrock/AutoForge@<commit>'
autoforge --versionuv tool install --reinstall with another commit replaces it, and
uv tool uninstall autoforge-cli removes it. Add the YAML extra with
'autoforge-cli[yaml] @ git+https://github.com/iceboundrock/AutoForge@<commit>'.
From a built wheel. From a checkout of the commit you want:
uv build # dist/autoforge_cli-<version>.tar.gz and -py3-none-any.whl
uv tool install --python 3.12 dist/autoforge_cli-<version>-py3-none-any.whlFrom PyPI. The published versions are at
https://pypi.org/project/autoforge-cli/, and the
releases page lists
them. The first, 0.1.0a0 (2026-10-28), is #295's registration
prerelease: a pre-Alpha snapshot, not validated for unattended production
use, and not the Public Alpha
(#255). Each file
PyPI serves was built and uploaded by this repository's pypi-publish
workflow and carries a PEP 740 attestation naming that workflow. Install
only after checking that the PyPI project page links back to this
repository:
uv tool install --python 3.12 --prerelease allow autoforge-cli--prerelease allow is needed while every published version is a
prerelease (0.x, Releasing);
'autoforge-cli==0.1.0a0' pins one. A published version carries only the
fixes of the commit it was built from, while security fixes land on main
(Security policy).
Releasing
describes how a version is built and published.
Whichever way it was installed, run autoforge from a checkout of the
repository whose issues it drives; autoforge doctor checks the rest.
Continue a run with the controller that created it, or a newer one: a
controller refuses a run a newer one wrote (exit 2, nothing written) and
names the code it imported, and doctor's controller row shows which
code a command line runs
(A run a newer controller wrote).
For development on AutoForge itself, from a clone of this repository:
uv sync # create .venv, install autoforge-cli (editable) + dev tools
uv sync --extra yaml # optional: full YAML config support (PyYAML)
uv run autoforge --helpuv run autoforge is the same command, run from that clone's .venv.
Development has the checks CI runs.
To configure profiles, bounds or the merge gate, copy a starter
configuration from examples/,
or autoforge.example.yaml (every key), to autoforge.yaml and pass it
with --config before the subcommand; see Configuration.
New to AutoForge? Getting started
walks through a first run on a repository of your own: prerequisites,
installing, a repository (a new private one, or an existing one you may
change) with an EPIC and an issue, a starter configuration, what each
doctor row means, a dry run, a real run, and the common first-run
failures with their fixes. The commands in brief:
Run these from a checkout of the repository the issues belong to. URLs must
be HTTPS GitHub issue URLs, and the EPIC and the issue must be in that same
repository (cross-repo runs are rejected). The run records the issue's
native parent on GitHub as its EPIC; --epic is used only for an issue
with no parent (Which EPIC a run records).
# 1. check the environment (writes nothing but a state-dir probe)
autoforge doctor
# 2. preview, in the checkout: reads its repository, the issue and its EPIC
# through gh, writes nothing
autoforge run --epic https://github.com/owner/repo/issues/1 \
--issue https://github.com/owner/repo/issues/2 --dry-run
# 3. create a run and drive it until READY_FOR_MERGE / BLOCKED / FAILED
autoforge run --epic https://github.com/owner/repo/issues/1 \
--issue https://github.com/owner/repo/issues/2
# 4. inspect it, and continue after an interruption
autoforge status
autoforge resumeWith the merge gate closed (the default) the run stops at
READY_FOR_MERGE with a banner naming the issue, PR, review round and
reviewed HEAD, and a human merges the PR. A push to the PR branch after
that review sends the run back to REVIEW of the new head on the next
resume. Only with safety.allow_merge: true in config does autoforge resume --allow-merge let the
controller verify the PR on GitHub and merge it itself.
A run in BLOCKED stays there until you fix the cause and run
autoforge unblock --reason "...", which re-inspects GitHub and either
re-enters the one phase that inspection supports or refuses. When a fixer
asked the controller to post an acceptance comment on the issue, the run
waits for you to approve or decline that exact comment
(unblock --approve-action / --decline-action). When it asked for a
comment on another issue (one the run's issue references, or the EPIC),
you post it yourself, bind an existing comment or withdraw it
(unblock --bind-evidence / --decline-action); the controller never posts
there, and verifies the comment on GitHub. A fixer may also ask the controller
to replace the description of the run's own PR, which it does by default
(safety.pr_body_updates); an edit made outside that plan stops the run and is
never written over. When a run stops because the
PR's implementation marker names another run, or none (pr-owner), stop
the other controller, then autoforge unblock --take-over <PR URL> and
resume hand the PR to this run. When status warns that GitHub
now names another parent for the issue than the EPIC the run recorded,
autoforge rebind-epic ... --dry-run previews moving a run stopped at
READY_FOR_MERGE or BLOCKED to that parent, before its UPDATE_EPIC
writes to the old one. Every command,
where a run stops, and how to recover are in
Running the remote workflow.
To work through an EPIC's issues several at a time,
autoforge epic run https://github.com/owner/repo/issues/1 runs up to
scheduler.max_parallel (1-4) of them at once, admitted in dependency order
from GitHub's native sub-issues and "blocked by" links, each in its own
worker, namespace and worktree. Every worker stops at its settlement
boundary (READY_FOR_MERGE, BLOCKED, DONE, FAILED), so nothing is
merged without a human. epic status shows each child's category and next
step without taking a lock, epic stop drains it, epic resume
recovers after a crash, a kill or a reboot, and epic recheck sends an
issue settled at READY_FOR_MERGE whose PR was pushed to back to review
(Running an EPIC in parallel).
It is experimental: it is verified offline only and has not yet driven a
real EPIC (Current maturity).
The documentation index lists everything by intent. The most common entry points:
| I want to… | Read |
|---|---|
| make a first run on a repository of my own, step by step | Getting started, starter configurations in examples/ |
know which OS, Python, git, gh, agent CLI and sandbox host it runs on |
Compatibility |
| run the remote workflow, merge, unblock, troubleshoot or recover a run | Running the remote workflow, troubleshooting and recovery |
| run phases through Pi on a DeepSeek API key or a ChatGPT subscription | Pi operator guide |
| configure profiles, bounds and the merge gate | Configuration, autoforge.example.yaml |
| know what AutoForge protects, which GitHub token it needs, or report a vulnerability | Security boundaries, SECURITY.md |
| understand the phases and legal transitions | Workflow |
| know what is verified on GitHub, and what gates a merge | GitHub safety |
| know how state, locking and crash recovery work | State and recovery |
| see how the code is divided | Architecture, module map |
| contribute, run the checks, write tests | Development, Testing |
| cut or publish a release | Releasing |
| read the design records | the index; ADR 0001 (superseded by ADR 0009 but for what it says of safefs.py), ADR 0002, ADR 0003, ADR 0004, ADR 0005, ADR 0006, ADR 0007 (partly implemented), ADR 0008, ADR 0009 |
Coding agents working on this repository start at AGENTS.md.
Supported now (Phase 2): GitHub issue → Claude Code implementation → the
controller's push and PR, read back via gh → OpenCode review with
round-based model routing → Claude Code remediation of finding IDs →
repeated review with SHA binding → READY_FOR_MERGE, entered only in the
write that issues the clean round's review certificate (below); the
bounded REVIEW/FIX loop with stagnation detection and the
REPLAN_REEXECUTE transaction;
journal-first recovery of an interrupted push or PR creation;
bounded correction retry for malformed results; doctor; redacted
per-invocation logs.
Opt-in, and only on a host that can enforce it: the outer sandbox on
Linux (sandbox.mode, off by default), with its capability probe,
pre-launch gate and doctor rows. It needs Linux 6.12+ with Landlock ABI 6,
unprivileged user, PID and IPC namespaces (some distributions restrict
them), and a bubblewrap that can mount an overlay (0.11 or later)
(ADR 0005).
sandbox.mode: disabled is not sandboxed. Its boundary is tested across
every provider path on a capable host (make test-sandbox). Hosted CI
measures its runner on every run (its bwrap, user-namespace policy and
Landlock ABI, in the job summary) and requires the real-backend tests
wherever the runner can enforce; where it cannot, they skip with the
probe's reason. It is experimental until a
make test-sandbox run on a capable host is recorded at the release
candidate.
Opt-in, off until configured: controller-run validation of the exact
candidate
(ADR 0006).
validation.commands is empty by default, so nothing is validated until
you configure it. Then the controller runs each command itself against the
exact commit an agent hands back, in a byte-exact export of its tree, and
keeps each outcome in the state file as a durable ValidationRecord,
reused for the same candidate, command and policy. A candidate is pushed
or published only once every command has passed on it. Before a reviewer
launches, the controller requires records for the candidate under review
(it validates a PR head that has none, such as a human's push), shows
their outcomes in the review prompt's evidence section, and refuses a
clean verdict over a failing record. A record says what the controller
observed when it ran a command, not that the change is correct, that
GitHub CI passed or that the result holds on another host; under
sandbox.mode: disabled every command runs unsandboxed as your UID.
Always on, with or without validation: the review certificate a clean
round issues on its way to READY_FOR_MERGE. It states which candidate
and revision were reviewed clean, which validation set the reviewer was
presented, and that every record in it passed; with nothing configured the
set is empty, and the certificate certifies a clean review and no
validation. It is a prerequisite, not permission to merge: it is
not a GitHub approval or a CI result, and it is never shown to an agent or
published.
Experimental: implemented and verified, but exercised only against the
in-memory fake: the controller-owned MERGE step behind the merge safety
gate (off by default), including its pre-merge verification and its
review-certificate check (#175), and UPDATE_EPIC. Every claim these
phases depend on is re-read from GitHub before state advances, but no
automated test has ever merged a real PR or edited a real EPIC, so treat
an opened gate as unproven against real services. The supported path
keeps the gate closed: a human reviews and merges the PR the run leaves at
READY_FOR_MERGE. The gate refuses a certificate that does not apply to
the live PR revision or to the validation configuration in force, and
still re-reads every fact it decides on from GitHub. A run saved in
READY_FOR_MERGE or MERGE before certificates existed holds none: the
gate refuses it while its PR is open (BLOCKED, nothing merged), and
unblock sends it to a new validated REVIEW round.
Experimental: implemented and verified offline: the parallel EPIC
scheduler (autoforge epic run|resume|status|stop|archive|recheck, ADR 0008). Its
crash windows are tested by killing real scheduler and worker processes at
each one, but against the in-memory GitHub and scripted agents only, and it
has not yet driven a real EPIC: run it on an EPIC you watch, with
epic status open.
Experimental: acceptance actions on other issues, which the operator
posts or binds (unblock --bind-evidence, #243). They are tested offline
and have been used once for real, in a run on this repository. They stay
experimental until they are used on a repository other than AutoForge.
Experimental and opt-in: Pi as a provider (provider: pi, the
Pi operator guide). No built-in profile uses it. Its one
recorded live run, a review on DeepSeek (#135), was in LOCAL mode, which is
gone; no live run of the
live smoke test's
REMOTE review is recorded yet.
The Public Alpha release claims these capabilities at exactly these levels
(#255, conditions
C1–C7). The registration prerelease 0.1.0a0 on PyPI (#295) is not the
Public Alpha; it predates LOCAL's removal, and this section does not
describe it.
Explicitly not yet: unattended production operation; the controller
feeding CI results into the review prompt (the reviewer is asked to inspect
gh pr checks itself, and the merge gate reads the checks); the merge gate
consulting branch rules (doctor reads them, the gate does not); dequeuing a
PR from a merge queue (the controller refuses to merge into queue-protected
branches instead).