Skip to content

Factory

The factory is the controller that turns a labelled issue into runs, narrates progress on the issue, meters each run’s tokens and owns the kill switch. It is the only component that creates runs, so every run has a task and a budget. This page describes the design; what runs today is on the status page.

The factory, from issue to merge. Work comes in three ways: a maintainer labels a GitHub issue factory/ready, a maintainer’s check mark on a Slack draft files one through maku-slack, or RunLore posts an investigate task. Intake snapshots it, and triage picks the task’s class, team and budget. The Task controller, maku-factory, runs the team as sequential agent runs in one room: a scoper that bounds the task or asks its author, a tester whose failing test is the contract, the implementer, the only writer, with a read-only search helper, a completion judge that says met, not yet or impossible, and a reviewer; a splitter turns a hard issue into sub-issues. Kueue admits each sandbox. The implementer opens a PR; a maintainer’s “Request changes” review becomes a new run on the same branch. The merge gate, policy-bot and a separate merger App, merges only low-risk classes with green CI: link fixes and their reverts merge with no human, while dead-links and deps-patch decide in shadow and deps-minor observes; every other PR waits for a human. The run meter revokes a run at its token cap, and the kill switch, a stop ConfigMap or a label on a pinned control issue, pauses intake and stops every task

Source: docs/architecture/ai-platform.drawio, page 5.

From issue to merge

StepWhat happens
IntakeA maintainer adds factory/ready to an issue. The factory takes a snapshot of it, so a later edit does not change a task already under way, and comments with the run id, branch, budget and a watch link. A task already running refuses a second label
Sponsored intakeA second intake kind, api, takes a task from a machine principal: a ZITADEL machine user declared under api.sponsored, never from a request body. Each principal names a sponsor (a maintainer, named in the issue’s footer and on the Task, accountable for what it files), a daily dollar ceiling and a per-task maxCostUSD; every request must carry max_cost, and the Task records who asked, the sponsor, the person it acts for and the cap. The runs are charged to the principal. The Slack App (Slack) and RunLore (RunLore) are its clients; RunLore’s link is off by default. Past caps.tasksPerDay, POST /v1/tasks answers 429 daily_task_cap
TriageOnce per task: which class it is (for example docs-links), which team of roles works it, and how big a budget it gets. A refusal is explained in a comment. What triage predicts only chooses the team and the budget; risk is enforced on the diff at merge time
TeamsRoles run as sequential AgentRuns in one room, on one agent/<task> branch: an implementer, then a reviewer, with a tester in larger teams. Only the implementer writes
ReviseA GitHub review with “Request changes” becomes the input of a new run on the same branch. /factory retry tries again after a failure
Merge gatepolicy-bot posts a status, the factory’s gate decides, the merger App merges. A low-risk class merges with no human (docs-links and revert on cloud-native-ref, at most once a day, behind a revert breaker); every other pull request merges on one maintainer’s approval of its current head. How it decides and the classes: the merge gate; drills and recovery: run auto-merge safely
Scopeloops.<class>.scope is off, steering or scoper; docs and scripts run scoper, every other class is off. steering adds the class’s note (.github/agent-factory/steering/<class>.md) to the implementer’s brief. scoper first runs a read-only triager that turns the issue into a bounded task with acceptance checks, or asks the author the question it could not answer (the user guide). Three counters by repository and class: scoper outcomes, steering reads (used, absent, invalid) and bounce exits (relabel, retry, expired)
Splitloops.<class>.split is set on scripts only, with anyTier: true (example (the reference host)). A row needs scope: scoper on the same row and never applies to docs-links, revert or an oracle class. Its keys: maxChildren (2 to 5, required), familyUSD (the cap on the parent and every part together; default four of the child tier’s task caps, $20.40 on standard) and childTier (default standard). A split is only proposed for a task the triage classifier puts on frontier, and parts open one at a time after a maintainer’s /factory split (user guide). Two counters by repository and class: maku_factory_splits_total (by outcome) and maku_factory_split_children_total (landed, dropped, reverted)
Test-first testerPer class, testFirst: true (it needs checks: true) runs the tester before the implementer: its tests, failing on the base, become the contract the implementer works against. merge.contractCheck names the host’s Test contract 🧪 check, required on a task that has a contract (row 2b) and report-only otherwise; row 2c refuses a head that changed the contract. Reasons: contract_invalid, contract_disputed, contract_modified. holdoutChecks on a live or shadow class names the checks the implementer cannot read. Metrics: maku_factory_contracts_total and maku_factory_class_without_holdout. testFirst is false by default (runbook 21 turns it on); trio is the implementer and the reviewer until the bench. The roles: roles
Kill switchOne task: the label factory/stop. Every task: the stop ConfigMap, or a label on a pinned control issue

The developer’s view of the same journey is in the user guide.

Two clients reach the same intake without a label. Slack drafts the issue in a thread and a maintainer’s check mark files it; RunLore hands the factory an incident and checks, after the fix merges, that the alert is gone.

Two clients of the factory. Slack intake (a user links their Slack account once, by mentioning the factory’s app with link): a mention of the app in a Slack thread, naming owner/repo and what; maku-slack drafts the issue with a title, what is wrong and acceptance; the author tunes the draft by replying; a maintainer’s check mark on the latest draft, anyone else’s being refused, has maku-factory file the issue and start the work, and the thread then follows the room: triaged, run started, PR opened, waiting, ended. Agent-to-agent work with RunLore: an alert fires and RunLore investigates in its incident thread; it posts an investigate task through POST /v1/tasks as an internal principal, and a triager proposes a fix; a maintainer’s factory/publish on the stub issue files the public fix issue; a maintainer’s factory/ready starts the fix task, which runs the team to a merged PR; after a soak RunLore re-checks the alert and posts its verdict to the post-merge monitor, where resolved is healthy and persisting pauses the class and opens a follow-up. Signed webhooks carry the task, room, PR and merge events back to the incident thread. RunLore’s own task never opens a PR

Source: docs/architecture/ai-platform.drawio, page 6.

Dependency bumps

A class that declares author.adopt takes Renovate’s pull requests instead of authoring bumps: a Task with no run, decided by the same gate. deps-patch and deps-minor run in shadow on cloud-native-ref. What is checked: adopted classes.

After a merge

A monitored class’s merges stay under watch, whoever merged them, and a regression or a follow-up fix counts against the breaker like a revert. With merge.approveToMerge, a maintainer’s approval of the current head is the merge. The monitor: after a merge; the approval: approve-to-merge.

Components and software

ComponentSoftwareWhat it doesWhy this software
Task controllerA Go controller (controller-runtime)Turns a labelled issue into a task: snapshot, a room, the team’s runs on its branch; narrates on the issue. Starts a reviewer after the implementer, and turns “Request changes” into a new runThe only component that creates runs, so every run has a task and a budget
AdmissionKueueQueues sandboxes so a burst of tasks waits instead of overloading the node pool. Until it lands, the factory’s own caps bound concurrencyThe Kubernetes-native job queue, with quotas
Run meter and kill switchPart of the controllerThe run meter revokes any run, hand-launched ones included, at its token cap. The agent-factory-stop ConfigMap pauses intake and stops every task; so does a label on a pinned control issueControls that act from outside the sandbox
Merge gatepolicy-bot, the factory’s gate and a merger GitHub Apppolicy-bot posts policy-bot: main; the factory’s gate decides each head; the merger App (ogenki-agent-merger[bot]) merges the head the gate decided or a maintainer approved; only the factory’s own revert PRs use GitHub’s auto-merge. A separate App holds the right to merge, and only itThe policy lives in the repository and is reviewable; the right to merge is isolated from everything else
Admission policyKyvernoDenies AgentRun creation to anyone but the factoryOne path in, so no run escapes its budget

Controls

BoundaryMechanism
SpendPer-run deadline; the factory’s run meter revokes a run at its token cap; token budgets at the gateway; at most 20 tasks a day
MergeOnly low-risk classes (docs-links, revert) merge themselves, live since v2.0, with a revert breaker; everything else merges on one maintainer’s approval of the current head (merge gate)
Stopkubectl -n agent-system create configmap agent-factory-stop pauses intake and stops every task; one label on a pinned control issue also refuses new runs and revokes every running one

The sandbox, identity and GitHub boundaries are on Runtime → Security boundaries. The factory design has the full merge policy, gate paths and the five-layer kill switch.