BeeverForge core architecture
Lifecycle of a ticket
A ticket in the board’s pickup column becomes a job: a durable workflow that sizes it, prepares a box, has one agent implement it, runs the project’s own gates, has a separate agent review it, opens a pull request and follows it to the merge and, where configured, through promotion.
Job states#
The states are the JobState enumeration in beeverforge/contracts/state.py. That module declares the states only; it holds no transition table. The transitions are made by the job runner (orchestrator/machine.py), the job workflow (runtime/temporal/workflow.py) and the promotion path (orchestrator/promotion.py). The diagram shows the transitions those modules make.
- A job can also end at
donestraight frommergedwhen nothing is configured after the merge, and fromtodowhen sizing splits the ticket into children. planningis reached only when the project setsplanner_stage: true.- The promotion path after
mergedruns only when promotion is enabled for the job. By default a job stops at the pull request’s merge. - Any working state can park.
needs_refinementmeans the ticket itself needs a person;failedmeans the environment’s setup failed, not the ticket;blockedholds a decision the agent asked a person to make;on_holdcovers the other impediments. readyappears only as a board-column mapping.ci_waitingandstaging_deployingare declared in the enumeration and not assigned anywhere.
The card on the board has its own, smaller lifecycle with an explicit transition table; it is described on Engineering principles.
Stations, end to end#
1. Poll and slots#
A Temporal schedule starts the poll workflow every 3 minutes by default; a tick that would overlap a running one is skipped. Each tick starts at most the free slots: BEEVERFORGE_MAX_CONCURRENT_JOBS (default 1) minus the jobs already in flight, taken in board order. A project whose box needs a proof and has no valid one is not picked up; the poll states the reason.
2. Sizing and automatic split#
Before the job runs, a sizing pass reads the ticket and the code (ADR-0013). It is on by default. Its verdict is one of fit, unclear or split. Unclear parks the ticket in needs_refinement. Split files the children in the pickup column and closes the parent; if the split itself fails, the ticket parks with the proposal for a person to split by hand. A sizing pass that cannot judge does not stop the job: the job runs unsized and the card says that sizing did not run.
3. Ask before spending#
A project can have the job gather context before it spends an implementation pass and ask on the card when the gathered context leaves a question open (ADR-0048). This is off by default: it needs preflight.gather: true and okf_gate: enforce. When it asks, the job ends as skipped with the note that it asked before starting, and an unanswered question is chased once after 48 hours.
4. Spec gate#
The spec gate is deterministic. It requires an objective and acceptance criteria, and refuses a ticket whose in-scope and out-of-scope lists overlap. A refusal parks the ticket in needs_refinement with the reason. It runs after sizing.
5. Preparation and setup#
In preparing, the job prepares its workspace in the box and runs the manifest’s setup: commands, with a timeout of 1,800 seconds. A setup failure ends the job as failed, because the environment, not the ticket, needs attention.
6. Plan and a single executor#
One executor agent plans and implements the ticket (ADR-0014). A separate planner pass runs only when the project sets planner_stage: true; its default is off.
7. Validation gates and bounded repair#
The job runs the project’s declared validate: gates in the box. While a blocking gate is red, the agent repairs, up to repair_max_attempts (default 2, at most 10) and within the cost ceiling and the ticket’s effort budget (effort_budget_turns, default 400). A job still red after that parks in on_hold with its partial work on the branch.
Suppression repair (ADR-0011). When the diff adds a suppression such as noqa, type: ignore, pragma: no cover or nosec, the job runs up to suppression_repair_max_attempts passes (default 1) to fix the cause instead. A pass that breaks a gate parks the job; suppressions that remain are shown to the reviewer.
8. Independent review#
A reviewer agent, in its own session, receives the ticket, the diff, the gate results and the constraints, and never the executor’s account of its work. review_mode is advisory by default: findings are posted and do not block. Only with review_mode: blocking does a rejection with actionable findings trigger one repair pass (review_repair_max_attempts, default 1) followed by a fresh review (ADR-0006).
9. Pull request#
The job publishes the branch and opens the pull request. Opening is find-then-update: if an open pull request already exists for the branch, it is reused and its title and body updated, so a retry does not create a second one. The agent’s own summary goes into the pull request labelled as a claim that no gate checks.
10. CI-aware repair#
While the pull request is open, the workflow watches it, whichever merge policy applies (ADR-0004). It polls every 2 minutes for the first hour and every 15 minutes after that. A red CI triggers up to 2 repair passes; a branch that has fallen behind its base is updated up to 6 times; when no check has reported after 10 minutes, the job says so instead of reading silence as green. After 14 days without a merge the job parks.
11. Merge on the current base#
With the default merge_policy: human, a person merges. With merge_policy: auto, the job merges only when the gates are green, no blocking review rejected it, no hard suppression remains, the change is not assessed high-risk, it touches no protected path, the floor and the diff could be read and the test count did not shrink, among the conditions in orchestrator/merge_policy.py. It first brings the branch onto the current base and re-runs the gates if the base moved (ADR-0003).
12. The floor frees at merge#
The job keeps its slot until its pull request merges or, when promotion is enabled, until promotion ends; a skip also frees it. Finishing the agent pass does not free the slot, and a parked job keeps it (ADR-0007, ADR-0010). The next ticket therefore starts from a base that includes this one.
13. Deploy watch and promotion#
When the manifest configures post_merge_deploy, a detached child workflow observes the project’s own deployment every minute and reports the outcome; it does not take a slot and does not block (ADR-0005). When promotion is enabled, the job verifies staging, then waits in awaiting_prod_approval for a person, for up to 3 days before parking. The approval reaches the workflow as a sealed answer that is verified before it is acted on and can be spent once. After approval the job tags the release, verifies production and ends done, or rolls back and parks.
14. Park with options#
A job that cannot continue parks and moves its card to the board’s attention column with a comment that says why. The options offered are resume, which continues the preserved work, and skip, which frees the slot, plus any decision options the job asked about. A parked job keeps its slot. A plain impediment that nobody answers within 3 days is skipped automatically, which frees the slot so the queue cannot jam; a decision park holds until it is answered. A job is recorded as skipped when a person skips it, when it asked before starting (station 3), or when its card was withdrawn, for example closed as not planned, while it waited in the queue; the automatic skip leaves it on_hold. A job that hits a rate limit is paused and resumes itself with a backoff that grows from 30 minutes to 2 hours, at most 48 times, then parks.
Sources#
beeverforge/contracts/state.pybeeverforge/orchestrator/machine.pybeeverforge/orchestrator/promotion.pybeeverforge/orchestrator/merge_policy.pybeeverforge/runtime/temporal/workflow.pybeeverforge/runtime/temporal/poller.pybeeverforge/runtime/temporal/schedule.pybeeverforge/runtime/temporal/io.pybeeverforge/contracts/manifest.pybeeverforge/adapters/forge/github.py- ADR-0003, ADR-0004, ADR-0005, ADR-0006, ADR-0007, ADR-0010, ADR-0011, ADR-0013, ADR-0014, ADR-0048