Move a card to Todo. Come back to a merged pull request.
foreman is an autonomous build loop for your Linear board.
It plans, builds, reviews and merges the cards you pick up, on your own machine, while nobody is watching.
A card you move into Todo is the only dispatch authorisation. Everything after
that happens without you.
flowchart LR
You(["You move a card<br/>Backlog → Todo"]) -->|needs-plan| Plan["Plan<br/><small>draw the change, post it, wait for sign-off</small>"]
You -->|no label| Build
Plan -->|signed off| Build["Build<br/><small>fresh worktree · judge size · implement · test · open PR</small>"]
Build --> Review["Review<br/><small>one light pass by a session that did not write it</small>"]
Review -->|blocking finding| Fix["Fix<br/><small>once · merges on green checks</small>"] --> Gate
Review -->|clean| Gate{"Evidence gate<br/><small>CI green · no risky paths</small>"}
Gate -->|pass| Merge(["Merged · deploy watched · Done"])
Gate -->|risky path| Human(["Parked for a human"])
- Small cards skip planning. A card without the
needs-planlabel goes straight to a build agent. It judges the size itself: a small change is implemented directly, and a larger one is planned as a graph on the card and then built in the same session. Addneeds-planto a card you want to sign off before anything is built. - Review is light. One reviewer reads the diff once and files findings as
blockingornote. Only a concrete bug, a security hole, a weakened check, data loss or a broken build blocks. A blocking finding buys one fix, and that fix merges on green checks without a second review.
Every few days a cleanup agent reads main and files one planned card for
the quality work that review no longer blocks on.
- You stay in charge of what gets built. No agent can put work into
Todoor take it out ofBacklog. A failed card waits for you to re-triage it; it never loops. - Nobody reviews their own work. The diff is reviewed by a session that did not write it, and the merge is gated on what GitHub holds now, never on a working tree.
- It holds no state. Each tick re-derives every card's position from Linear,
ghandgit. Kill it, restart it or upgrade it mid-build and nothing is lost. - It lives outside the project it builds. A repository becomes buildable by
adding one
board.toml. - Know what runs on your host. Build agents run on your machine, in a throwaway git worktree, with permissions bypassed: they run the target's own bootstrap and test command there. The worktree, a per-role deny list, the required checks and the review are what contain them — not a sandbox (see Security). What never runs on your host is CI: it runs on GitHub-hosted runners, and a merged commit reaches this machine only when foreman pulls it. foreman never merges a pull request from outside the repository it builds.
- Plan → Build → Review → Merge as Linear columns, each stage a detached
agent in its own git worktree. Planning is a separate stage only for cards
labelled
needs-plan. - A light review by a session that did not write the diff: one round, one reviewer, gating only on blocking findings, with one fix.
- Evidence-based merges: required checks, risky paths from the diff, and deploy step conclusions, read from GitHub.
- Deploy watching, including queued deploys and a
fast-tracklabel copied from the card to the pull request. - Scheduled cleanup that files one well-planned card instead of blocking merges.
- Linear priority orders the queue; boards take turns round-robin.
- One tick, every board — a single foreman serves all your projects, each board with its own concurrency limit.
- Dispatch to whichever agent works. A stage's model is an ordered list of
candidates —
opencode:foundry/gpt-5.6-sol,claude:opus— and the tick spawns the first not rate-limited, falling back across models and across CLIs. See Models and fallback. - A watchdog (
supervise.sh) that restarts a stuck tick and can never dispatch a card itself. - Preflight refuses to dispatch when the machine cannot build, without blaming the card.
- Release-gated updates. A merge to
maincuts a GitHub Release — unless the commit saysRelease: skip— and foreman follows the latest one.
You need git, gh, Python 3, a Linear API key, a Linear MCP config, and one
supported harness.
git clone https://github.com/praveenVnktsh/foreman.git ~/.foreman/install
~/.foreman/install/bin/install.sh --harness claude
~/.foreman/install/bin/install-skills.sh
install -m 600 ~/.config/linear.key ~/.foreman/linear.key
cp path/to/linear-mcp.json ~/.foreman/mcp.json
~/.foreman/install/bin/boardctl add myproject --repo ~/Developer/myprojectThen keep it running and up to date with systemd user timers (Linux; on macOS use launchd or cron):
~/.foreman/install/bin/install-service.sh
~/.foreman/install/bin/install-self-update.sh
~/.foreman/install/bin/install-dashboard.shThe dashboard is one page saying what the machine is doing — what is stuck, what
is running, whether the host is fit — plus a box for sending a message the tick
reads on its next pass. It binds loopback; tailscale serve --bg --set-path /foreman 8429 puts it on your tailnet and nowhere else.
The clone goes at ~/.foreman/install and nowhere else: foreman derives its
home from where the clone sits, so a clone one directory deeper puts the home
somewhere the linear.key and mcp.json above are not.
install-skills.sh is required: the tick runs /board, and the harness only
finds skills in its own skills directory. The release branch, the rate-limit
fallback and the other harnesses are covered in
docs/INSTALLING.md.
One foreman serves every board. Each stage names its model in
foreman.toml, and [fallback] lists the models it may fall down through,
strongest first. A tier is model or harness:model, so the tick falls back
across providers and across CLIs:
[models]
plan = "opencode:foundry/gpt-6-astra"
build = "opencode:foundry/gpt-5.6-sol"
[fallback]
tiers = ["opencode:foundry/gpt-6-astra", "opencode:foundry/gpt-5.6-sol", "claude:opus"]
[fallback.floor]
plan = "claude:opus" # the weakest model the plan stage may reachdispatch.sh spawns the first tier whose model is not marked rate-limited,
never below the stage's floor. A model is marked when a spawn refuses it, or
when an agent dies mid-run on an API rate-limit error, and unmarked when the
cooldown passes. Because one tick may spawn on more than one harness, the agent
registry ($HARNESS_SH list) merges every harness it can use. The design is in
docs/specs.
Add a board.toml at its root. Only [linear], [checks] and [test] are
needed; everything else is optional.
[linear]
team = "My Team" # the team NAME, not its key
project = "myproject"
[checks]
required = ["Tests"] # job names that must pass
ci_workflow = "CI" # the workflow's `name:`
[test]
command = "tests/run-all.sh"
[risk]
paths = ["migrations/"] # a diff touching these waits for a human
[docs]
required = ["STYLEGUIDE.md"] # every agent reads these first
[limits]
max_concurrent = 1foreman builds itself: this repository's own board.toml is a working example. The full contract is in the design spec.
flowchart LR
Cron(["cron / systemd"]) --> Sup["supervise.sh<br/><small>watchdog</small>"]
Sup --> Tick["tick agent<br/><small>/board skill</small>"]
Tick --> Rec["reconcile<br/><small>Linear · gh · agents</small>"]
Rec --> Dis["dispatch<br/><small>worktree · brief · spawn</small>"]
Dis --> Agents["plan · build · review · cleanup<br/><small>detached agents</small>"]
Agents --> GH[("GitHub<br/>PRs · checks · deploys")]
Rec -.->|next tick re-derives| Rec
One tick end to end, with every file it touches, is drawn in docs/board-flow.md.
| Read | For |
|---|---|
| docs/INSTALLING.md | Install, models and fallback, releases, migration |
| docs/board-flow.md | What one tick does, file by file |
| docs/specs/ | Why the loop is shaped the way it is |
| AGENTS.md | The map for anyone, or any agent, changing this code |
| STYLEGUIDE.md | How code and prose are written here |
foreman is public, so CI runs only on GitHub-hosted runners and a merged commit reaches your machine only when foreman pulls it. foreman never merges a pull request from outside the repository it builds, and it pins every merge to the one head the board judged, so a head that moved since is never merged. Build agents do run the target's bootstrap and tests on your host; see Why foreman for what contains them.
The deny list is a partial layer. Every card agent runs as your user with
permissions bypassed. skills/board/dispatch.sh gives each one Claude Code
permissions.deny rules, which Claude Code applies even in bypass mode.
- What it blocks: Claude Code's file-editing tools, and the Bash commands it
recognises as file writes (redirections,
tee,sed -i), on foreman's install,boards.toml,foreman.toml, every*.key, the inbox, each board'sHALT,ids.envandlast-cleanup, everyhistory.jsonl, and every tracked top-level entry of the board's main checkout with its.git/configand.git/hooks/. Plan, build and cleanup agents are also kept off every card'scards/directory, so no author can write its own review. A reviewer is not, because it writes its review there. - What it does not block: any other program the agent runs.
python3 -c, a test script or a build step still writes wherever your user can. - Claude only. The Codex and OpenCode adapters drop
--settings, so their agents get no deny list.
Full confinement means running the agents as a separate unix user that cannot write foreman's files. That is an infrastructure choice for the operator, and foreman does not make it for you.
Issues and pull requests are welcome. Start with AGENTS.md and
run tests/run-all.sh before opening one.
MIT. Extracted from a board orchestrator that grew up inside one private project, and generalised until nothing about that project remained.
