Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
0104b45
Port the factory loop from the factory-control-plane branch (#1008)
pcapriolo-yc Sep 9, 2026
8cd8dd7
QM-31: Bootstrap the factory source clone separately from the work re…
pcapriolo-yc Sep 10, 2026
3b7085a
QM-32: create the software factory loop when the factory config is ap…
pcapriolo-yc Sep 10, 2026
84bb7d8
Give the factory sandbox the credentials and identity the wrapper nee…
pcapriolo-yc Sep 11, 2026
2ebf5b9
Mark the factory wrapper's environment as a sandbox (#1087)
pcapriolo-yc Sep 11, 2026
d090c9e
Merge remote-tracking branch 'origin/main' into integration-sync-main
pcapriolo-yc Sep 11, 2026
639e635
Carry the wrapper's redacted diagnostics on a run that ends without a…
pcapriolo-yc Sep 11, 2026
95ab0fc
Drop the unused converge-vector evaluator (#1109)
pcapriolo-yc Sep 11, 2026
e38ae66
Document the software factory loop (#1111)
pcapriolo-yc Sep 11, 2026
dce81d3
Name all three factory credentials on the admin card (#1110)
pcapriolo-yc Sep 11, 2026
2d951ab
Merge remote-tracking branch 'origin/qm-29-port-factory-loop' into in…
pcapriolo-yc Sep 11, 2026
6070535
Sync the factory integration branch with main (#1112)
pcapriolo-yc Sep 11, 2026
445d57a
Build the local sandbox image for the docker host's architecture (#1113)
pcapriolo-yc Sep 11, 2026
220b95e
Install lsof and procps in the sandbox base image (#1121)
pcapriolo-yc Sep 11, 2026
89d9d47
QM-35: Post factory run updates to the configured Slack channel (#1114)
pcapriolo-yc Sep 11, 2026
47d4a87
QM-36: Let every org admin administer the org-scoped loop (#1118)
pcapriolo-yc Sep 11, 2026
67b01f3
Retry transient read failures while polling the factory wrapper (#1133)
pcapriolo-yc Sep 12, 2026
dbda600
Derive the wrapper session id from the item, not the attempt (#1138)
pcapriolo-yc Sep 12, 2026
7174048
Wait for CI to settle before the forge evaluation judges a PR (#1144)
pcapriolo-yc Sep 12, 2026
ac4fdb8
Clear sibling group labels before the ship step adds ready-for-review…
pcapriolo-yc Sep 12, 2026
0fd9ff7
Park a ready item in place when its auto-ship fails (#1147)
pcapriolo-yc Sep 12, 2026
29c9494
Merge remote-tracking branch 'origin/main' into integration-sync-main-2
Sep 12, 2026
d7471ae
QM-1: Add a delete button beside open on the /files page (#1140)
pcapriolo-yc Sep 12, 2026
ac800c7
Merge remote-tracking branch 'origin/qm-29-port-factory-loop' into in…
Sep 12, 2026
b16aac4
Sync the factory integration branch with main (#1148)
pcapriolo-yc Sep 12, 2026
2d10c86
Outlast a sandbox API outage while polling the factory wrapper (#1389)
pcapriolo-yc Sep 18, 2026
5366817
Ship the factory wrapper inside this repository (#1427)
pcapriolo-yc Sep 19, 2026
7592868
QM-58: Schedule the factory loop to fire every five minutes (#1450)
pcapriolo-yc Sep 19, 2026
c0fdb13
QM-59: Pin npm 11.16.0 in the sandbox tool preflight before the repo …
pcapriolo-yc Sep 19, 2026
5626c4e
QM-60: Carry the reviewer's bounded summary into failed-iteration tra…
pcapriolo-yc Sep 19, 2026
beff96f
QM-61: Hold the self-verify blocking bar to the factory test-economy …
pcapriolo-yc Sep 20, 2026
840c5fc
QM-62: Defer a factory loop fire while that loop has a run in progres…
pcapriolo-yc Sep 20, 2026
6dc4658
QM-63: Prune foreign .io-agent-* work dirs in the wrapper prologue (#…
pcapriolo-yc Sep 20, 2026
bc283e1
QM-64: Flush the sandbox filesystem on the trail poll and before the …
pcapriolo-yc Sep 20, 2026
c6df8f3
QM-65: Make the plan reviewer's summary lead with the objection (#1458)
pcapriolo-yc Sep 20, 2026
020f7b8
QM-66: Add a Docker-free proof-stack script that boots core, web UI, …
pcapriolo-yc Sep 21, 2026
22ae2a3
QM-67: Show each loop's queue counts on the Loops list row (#1470)
pcapriolo-yc Sep 21, 2026
9eed6a6
QM-68: Re-run a red GitHub Actions check once per head before sending…
pcapriolo-yc Sep 21, 2026
7ea887a
QM-51: Skip a sweeper tick while the previous sweep is still in fligh…
pcapriolo-yc Sep 21, 2026
14085b6
QM-69: Re-send an exec when the Sprites provider reports the process …
pcapriolo-yc Sep 21, 2026
d5b3c71
QM-46: Drive Slack runtime reconciliation from one installation snaps…
pcapriolo-yc Sep 21, 2026
0e7e13f
QM-54: Refresh the web chat transcript on return when no live run rem…
pcapriolo-yc Sep 21, 2026
701bd1f
QM-70: Judge factory test economy by a minimal killing set of named b…
pcapriolo-yc Sep 22, 2026
2f7b077
QM-71: Trim the web chat background-return tests to one per named bug…
pcapriolo-yc Sep 22, 2026
7775592
QM-72: Trim the Slack runtime reconciliation tests to one per named b…
pcapriolo-yc Sep 22, 2026
9657bbb
QM-77: Extract a shared delivery-stream test harness and migrate the …
pcapriolo-yc Sep 22, 2026
aeb5afe
QM-73: Resolve the factory sandbox's model auth from core config inst…
pcapriolo-yc Sep 22, 2026
dfb844f
QM-74: Resolve the factory's Slack bot token from the org installatio…
pcapriolo-yc Sep 22, 2026
a280db6
QM-75: Resolve the factory's GitHub token from the loop owner's conne…
pcapriolo-yc Sep 22, 2026
dd30ee4
QM-76: Resolve the factory's Linear token from the loop owner's conne…
pcapriolo-yc Sep 22, 2026
8fbd9a0
Match the Linear curl form exactly in the wrapper header test (#1529)
pcapriolo-yc Sep 22, 2026
5cff301
QM-78: Record the factory wrapper's stage trail on the loop item as t…
pcapriolo-yc Sep 23, 2026
8551243
QM-79: Serve an assembly-line board snapshot for a factory loop from …
pcapriolo-yc Sep 23, 2026
fffff6c
QM-81: Size the Sprites sandbox from SPRITES_RAM_MB and SPRITES_CPUS …
pcapriolo-yc Sep 23, 2026
8c86df1
QM-82: Ignore factory work directories in prettier (#1567)
pcapriolo-yc Sep 23, 2026
a62a22b
QM-84: Hand the factory loop's playbook to every wrapper run as IO_FA…
pcapriolo-yc Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ data/
test/live-slack/out/
deploy/stacks/.generated/
deploy/helm/templates/
factory/
.io-agent-*/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ messages, and screenshots for organization identifiers before it pushes. Nothing
- [`cli/README.md`](./cli/README.md) — the `qm` CLI and the deployment directory contract
- [`docs/deploy-directory.md`](./docs/deploy-directory.md) — the deployment directory in full
- [`docs/porter.md`](./docs/porter.md) — running qm on Porter
- [`docs/factory.md`](./docs/factory.md) — the software factory loop: credentials, config, and what a run does
- [`.env.example`](./.env.example) — every knob, documented in place
- [`plugins/`](./plugins) — the surfaces (Slack, web UI, admin, portal)

Expand Down
99 changes: 99 additions & 0 deletions docs/factory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# The software factory loop

The factory is a loop with `surface: "factory"`. Each item is a Linear ticket. The loop runs a
coding-agent wrapper inside a sandbox, and the wrapper works the ticket to a pull request against
the configured repository. The loop then reads the forge to judge whether the pull request has
converged, and ships it by marking it ready and moving the ticket.

## Prerequisites

Linear access is not a pasted credential. Every fire resolves the loop owner's Linear connector
token for `api.linear.app`, so an org admin must have registered the Linear OAuth client and the
loop owner — the admin who applied the config, see "Ownership" — must have connected Linear once
through the connector flow. Without that grant the fire fails before any sandbox work with
`linear: the loop owner has not connected Linear`. Linear's OAuth scopes are not per-team: the
connector's `read` and `write` grant covers every team the owner can reach, not just the
configured one.

GitHub access is not a pasted credential either. Every fire resolves the loop owner's GitHub connector
token for `api.github.com`, so an org admin must have registered the GitHub OAuth client and the
loop owner — the admin who applied the config, see "Ownership" — must have connected GitHub once
through the connector flow. Without that grant the fire fails before any sandbox work with
`github: the loop owner has not connected GitHub`. The connector's `repo` and `read:org` scopes
are what the run can reach on the forge.

Model calls made by `claude` inside the sandbox use core's own Anthropic configuration —
`ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, or the
`CLAUDE_AUTH_CREDENTIAL` keychain credential — and not a pasted secret. A deployment with none of
them fails the fire with `model auth: core has no Anthropic credential configured`.

## Configuration

The Software factory card in the admin console, at org scope, writes the factory config and
creates the loop on Apply. Applying again finds the same loop. Clear removes the config and leaves
the loop and its cron, whose every fire then fails with `factory_config_missing`; disable or
delete the loop from the Loops page to stop it.

| Field | Meaning |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `forge`, `publishProject`, `targetBranch` | Where pull requests open |
| `repoCloneUrl`, `repoSetupCmd` | The subject repository and the command run in it before work starts |
| `linearTeamId` | The team whose `Auto-Triage` tickets the loop enumerates |
| `sourceAppDirs`, `sourceTestRe` | Which paths the run may change and which files count as tests |
| `verifyTestsCmd`, `verifyTestFileCmd`, `verifyLintCmd` | The verification commands |
| `proofStartCmd`, `proofBaseUrlCmd` | Optional browser proof: bring the app up and print its URL |
| `bugbotRequired` | Whether convergence needs a Bugbot review of the exact head; the loop enforces it, the wrapper-side check is off until a Bugbot user id is configured |
| `followupsEnabled` | Whether the run may file follow-up tickets |

Apply also gives the loop a cron that fires it every five minutes, so tickets are picked up
without anyone asking. Applying again reuses that cron. Fire it early from the Loops page, or
through `POST /v1/loops/:id/fire`. Three consecutive failed fires quarantine the loop, which on a
five-minute cron is fifteen minutes: connect the loop owner's Linear and GitHub before Apply, and
re-enable a quarantined loop from the Loops page. A fire that lands while a run is still working
claims nothing and records `deferred: <ticket> is still in progress` — the factory works one
ticket at a time, and the queued tickets are picked up by the first fire after the run ends.

## What a run does

1. Preflight probes the sandbox for `bash git gh jq curl node npm claude`.
2. Bootstrap fetches this repository at depth 1 into `/workspace/qm-source` in the sandbox, at
the full commit this core was built from (`GIT_SHA`), so the wrapper under `factory/` and the
loop always come from the same commit. A core with no build commit, or an abbreviated one,
fetches `main`. A warm sandbox re-fetches instead of re-cloning.
3. The wrapper starts from `/workspace/qm-source/factory/.claude/io-coding-agent-js.sh` with
the subject repository as its working directory. It clones the subject repository, runs the
setup command, and drives the ticket through understand, plan, implement, verify, review,
proof, and ship.
4. The loop parses the wrapper's stdout for the pull request and reads the forge until CI is green
and no review blocks it. A run that ends without a pull request returns the item to work with
the wrapper's last diagnostic lines as its reason, secrets masked.

## Sandbox size

The sandbox is created at the Sprites account default — 8 GB of RAM, no swap, and an overlayfs
root that refuses a swapfile. Review and Proof run three review agents at once, and a UI ticket
also boots core, the web UI, the portal and Chromium on the same machine, which peaks above that
and gets the VM OOM-killed; the wrapper disappears with exit 137 and the run's work is lost. Set
`SPRITES_RAM_MB` and `SPRITES_CPUS` to size it — `SPRITES_RAM_MB=16384` and `SPRITES_CPUS=4` on
the factory deployment. Both are deployment-wide and apply to every Sprite that deployment
creates. An existing Sprite keeps the size it was created with, so an operator deletes it and the
next run provisions it at the configured size.

## Security posture

The wrapper runs as root inside the sandbox with `IS_SANDBOX=1`, which lets `claude` run with
`--dangerously-skip-permissions`. The model therefore executes tool calls with no permission gate,
and its process environment holds the loop owner's Linear connector token, the loop owner's GitHub
connector token, and core's model credential. The blast radius is the sandbox plus whatever those
tokens can reach: everything the owner's Linear grant reaches under the connector's `read` and
`write` scopes, everything the owner's GitHub grant reaches under the connector's `repo` and `read:org` scopes,
and core's Anthropic account. Admin-supplied commands in the config
also run in that environment, so the admin console is the trust boundary.

Secrets never appear in a command string or in the run's recorded reason. The bootstrap
authenticates through `GIT_CONFIG_*` environment entries, which git does not write to disk.

## Ownership

The loop is owned by the admin who first applied the config. Administration of an org-scoped loop
by other org admins is a known gap.
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default tseslint.config(
"plugins/web-ui/public/",
".claude/",
".context/",
"factory/",
],
},
js.configs.recommended,
Expand Down
20 changes: 20 additions & 0 deletions factory/.claude/commands/review_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Review Plan

You are a staff-level engineer with over 15 years of experience, a deep understanding of the React
lifecycle as well as modern TypeScript and Node.js practices, and an obsession with writing perfectly
clean/organized/easy-to-maintain code.

Now, your job is to do an extremely thorough code review of a proposed implementation plan for a new
feature.

Your main focus: code organization, reusability, patterns/style, maintainability, antifragility -
ideally someone with little-to-context could come in contact with these files, easily understand
what's going on (by reading the code alone, not necessarily through comments), and easily build onto
the existing framework without accidentally breaking anything and without breaking any of the
patterns in place. Please perform your review, again stepping back and thinking about the
overall framework/organization/patterns, and suggest your changes. Be thorough, be nitpicky, and
lean into the obsession.

Also, make sure to look closely at the original feature request as it compares to the plan.
Will the current changes fulfill the mandate of the initial feature request? Pay close
attention, and also scrutinize the plan for bugs.
Loading
Loading