This is the operator's checklist for verifying a warren cut against the V1 goals (below) + §11.A reap roundtrip + restart-recovery contract before pushing a release.
The contract is split across automated (the harness in
scripts/acceptance/) and manual gates (real claude-code run, GKE
deploy, UI smoke). Run the automated suite on every change; run the
manual gates before a tag.
# Quality gates — same trinity CI runs.
bun test && bun run lint && bun run typecheck
# Automated acceptance harness (in-proc, default).
bun run acceptance
# Container-mode boot smoke (requires Docker, slow first run).
bun run acceptance:containerA green run prints something like:
Acceptance results:
✓ 01 312ms boot + /healthz auth-exempt + /readyz transitions to 200 after refresh
✓ 03 842ms projects management — add / list / refresh / delete
...
12 passed, 0 failed, 0 skipped
In container mode, scenarios that need host-side fixtures skip cleanly:
✓ 01 180ms boot + /healthz auth-exempt + /readyz transitions to 200 after refresh
○ 19 0ms warren on postgres
↳ not supported in container mode
...
✓ 13 42130ms container boot — image builds, supervisor + bwrap flags hold, healthz/readyz/agents respond
2 passed, 0 failed, 11 skipped
CI (.github/workflows/release.yml) runs the same three commands; if
any fails locally, your push will fail there too.
bun test # 489+ tests across 50 files
bun run lint # biome check --error-on-warnings .
bun run typecheck # tsc --noEmitBiome enforces scripts/acceptance/ (per d7a788e). Warnings count as
failures (--error-on-warnings); fix at write time, don't suppress.
Release wiring (.github/workflows/release.yml) auto-tags v$VERSION
and cuts a GitHub release on every push to main where
package.json + src/index.ts agree on a new version. The harness
must be green before bumping VERSION or a broken release ships
unattended.
Default mode. Boots bun run src/server/main.ts + burrow serve as
sibling processes on a temp dir, builds local git fixtures via the
GIT_CONFIG_GLOBAL insteadOf rewrite (no network), and exercises every
HTTP route end-to-end. No Docker, no API keys, ~30s wall-clock.
bun run acceptance # all scenarios
bun run scripts/acceptance/run.ts --only 04,05 # one or more by id
bun run scripts/acceptance/run.ts --stop-on-failure
bun run scripts/acceptance/run.ts --keep-tmp # leave fixtures for inspectionLogging knobs:
| env | effect |
|---|---|
WARREN_ACCEPTANCE_LOG_LEVEL=debug |
verbose harness logs |
WARREN_ACCEPTANCE_WARREN_STDOUT=1 |
passthrough warren server stdout |
WARREN_ACCEPTANCE_WARREN_STDERR=1 |
passthrough warren server stderr |
WARREN_ACCEPTANCE_BURROW_STDOUT=1 |
passthrough burrow stdout |
WARREN_ACCEPTANCE_BURROW_STDERR=1 |
passthrough burrow stderr |
In-proc mode covers scenarios 01–12 (the V1-goals application contract in the next section).
These are the V1 goals the acceptance criteria and scenarios 01–13 verify:
- Single-image deploy:
docker compose upon a home server,kubectl apply -kon a cluster (GKE Autopilot), same Dockerfile. - Web UI for: agent registry, project list, run dispatch, live event tail, basic settings.
- HTTP API mirroring the UI's surface so external scripts can drive warren.
- Custom-agent-as-canopy-prompt: an agent is a single canopy prompt with required sections. Warren auto-discovers agents from a connected canopy repo.
- Runs against project repos cloned into warren's data dir from GitHub URLs.
- Self-* loop: agents read seeds queue, write seeds for follow-ups, record mulch on success/failure, prime mulch on next spawn.
- Durable event log: warren persists every event burrow streams in its SQLite database. Reload-after-crash and post-hoc inspection both work.
Deferred to V2 (context, not built in V1):
- GitHub webhook receiver and signature verification: the event-driven half of the scheduler. The cron half shipped in V1. See
docs/design/scheduler.md. @os-eco/warrenlibrary API exports — internal-onlyClientis fine for V1.
Boots warren via docker compose up -d --build using the canonical
docker-compose.yml plus a generated override that:
- gives the run a unique compose project name + container name,
- maps a random ephemeral host port to the container's
:8080, - supplies
WARREN_API_TOKEN,WARREN_BURROW_NO_AUTH=1, andWARREN_LOG_LEVEL=warninline (no.envfile required at the repo root).
bun run acceptance:container
# or, equivalently:
bun run scripts/acceptance/run.ts --mode containerContainer mode requires Docker on PATH and a running daemon. First run builds the image (~1–3 min on a warm cache); subsequent runs reuse the image layer and complete the boot smoke in ~30s.
What container mode actually verifies (scenarios 01 + 13):
- the image builds (ui-builder + runtime stages),
- the supervisor (
bun run src/supervisor/main.ts) boots burrow under the four bwrap-friendly security flags fromdocker-compose.yml(apparmor=unconfined, seccomp=unconfined, systempaths=unconfined, cap_add=SYS_ADMIN), - warren and burrow start as siblings on
/var/run/burrow.sock, /healthzis auth-exempt and returns 200,GET /agentsreturns the built-in coding agents (claude-code,pi) seeded byseedBuiltinAgents,/readyzreturns a structured{ ok, checks: [...] }body.
Scenarios that depend on host-side fixtures (a sample project repo)
declare modes: ["in-proc"] and skip cleanly in
container mode — the compose harness deliberately doesn't bind-mount
fixtures into the container, since the production deploy is
fixture-free. Scenarios that drive process control (kill warren /
restart warren / kill burrow) also stay in-proc-only because the
in-container supervisor owns burrow lifecycle and would fight
harness-side kills.
macOS Docker Desktop caveat. Container boot succeeds on macOS, but
cap_add: SYS_ADMIN is partial under Docker Desktop's VM. Boot smoke
(scenario 13) holds; dispatching a real run with bwrap nesting is
Linux-only territory and is not asserted here. For nested-bwrap
verification, run acceptance:container on a Linux host. (Under the
k8s runtime there is no bwrap at all — the pod boundary is the
sandbox — so nested-bwrap only applies to the local topology.)
--keep-tmp in container mode leaves the compose stack running
after the harness exits. Tear it down with the printed command:
docker compose -p warren-acceptance-<suffix> down -v
Verifies the first-run path exercised in the first dogfood post-mortem
(below): a real claude-code run, with a real
ANTHROPIC_API_KEY, against a real GitHub repo, completes end-to-end
with state: succeeded + branchPushed: true + non-zero commitsAhead.
--real is a documented opt-in flag on scripts/acceptance/run.ts,
but no --real scenario is implemented today (token cost + flakiness
risk). Drive the gate manually instead, against a long-lived dev
instance of warren:
# 1. Make sure your local stack is up (compose or `bun run src/server/main.ts`)
# and ANTHROPIC_API_KEY + GITHUB_TOKEN are set.
# 2. Register a real project (a writeable repo you control, like a sandbox fork).
warren add-project https://github.com/<you>/<sandbox-repo>.git
# 3. Spawn a claude-code run with a small, scoped prompt.
warren run claude-code <project-name> -p "Add a one-line CHANGELOG entry under [Unreleased] about the V1 acceptance gate. Commit when done."
# 4. Watch the run in the UI (http://localhost:8080) or via:
warren tail <run-id> --followPass criteria.
- Run reaches
state: succeeded(terminal). runRow.branchPushed === true.gh compare main...burrow/<bur-id>reportsahead_by ≥ 1and the diff matches the prompt's intent.- Reap fired
mulch.record.added/updated/skippedevents for any records the agent recorded inside the sandbox; project's persistent.mulch/carries the post-merge state.
If any of the above fail, do not ship — the §4.3 composition flow
is structurally broken in a way the automated harness can't catch
(stub agent has no real toolchain). Re-read the dogfood post-mortems
below for the canonical failure shapes
(warren-67cc, warren-a69a, warren-1eaa, warren-1a09, etc.) and
branchPushed-requires-both-reap-and-sandbox-git (a branchPushed: true does NOT prove the agent committed — it can fire on an
empty-push, surfaced by the reap.empty_push event when
commitsAhead: 0).
Verifies the §10.2 deploy shape on the hosted target: the k8s runtime
on GKE Autopilot, where each run is its own pod and there is no burrow.
This is the cluster equivalent of "did the canonical home-server deploy
just work." The canonical procedure is docs/RUNBOOK-K8S.md;
the pipeline that performs it is .github/workflows/deploy-gke.yml.
# Roll the cluster forward (a published GitHub release does this
# automatically; a manual dispatch of deploy-gke.yml with `deploy: true`
# is the on-demand path). Then check the control plane came up:
kubectl -n warren rollout status deploy/warren --timeout=300s
INGRESS=https://<your-ingress-host>
curl -fsS "$INGRESS/healthz" # 200 expected
curl -fsS "$INGRESS/readyz" \
-H "Authorization: Bearer $WARREN_API_TOKEN" # 200 expectedPass criteria.
kubectl rollout statusreports the Deployment available before the timeout./healthzreturns 200,/readyzreturns 200 with all probes ok (thek8sruntime drops the burrow/bwrap probes — warren-c128).kubectl -n warren logs deploy/warrenshows warren/healthz respondingand the K8sProvider selecting thek8sruntime at boot.- A real claude-code run (per the
--realgate above) completes end-to-end — the run pod schedules, works, and reaps with a pushed branch.
Under k8s there is no burrow and none of the four bwrap flags
(apparmor/seccomp/systempaths/SYS_ADMIN) apply — the pod
boundary is the sandbox and kubelet enforces per-run CPU/memory. See
docs/RUNBOOK-K8S.md for the cluster topology.
The acceptance harness drives the HTTP API exclusively. The UI
(src/ui/) is a separate Vite/React SPA that's bundled into the
runtime image at build time; the only way to catch UI regressions is
to look at it.
Two surfaces to drive against your local stack (or the deployed warren):
# Local stack (in-proc):
bun run src/server/main.ts &
bun run ui:dev # vite at http://localhost:5173
# Or the bundled UI (what production serves at :8080):
bun run build:ui
bun run src/server/main.ts # serves UI from src/ui/dist
open http://localhost:8080Walkthrough — golden path.
- Login screen. Paste
WARREN_API_TOKENinto the bearer-token input (stored in localStorage underwarren.apiToken). The ProjectsPage should load without redirecting back to login. - Agents page. Built-in agents (
claude-code,pi) appear in the registry table. Expand a row to inspect its rendered definition. - Projects page.
Add projectaccepts a github.com URL and produces a row within ~5s; refresh updatesdefaultBranch. - New run. Pick agent + project, type a prompt, submit. The page redirects to RunDetail; the event tail flows in real time and the header badge transitions queued → running → succeeded.
- RunDetail page. Steer form sends a body that appears as a
steer.sentaudit event. Cancel button transitions the run tocancelled. The post-reap header carries either a+N commitsbadge or anempty pushwarning (mx-6aae24); a real-claude-code run also surfaces aPR ↗link ifWARREN_AUTO_OPEN_PRis on (mx-f0f743).
Walkthrough — regressions to watch for.
- Wide tables (Projects, Runs) push the layout past 100vw → fix the
min-w-0on the flex<main>(mx-a8a1df). - RunDetail badge stale after reap → check the events subscription is still wired (warren-d9ad).
- Empty-push warning fires on a real-work run → the agent didn't
commit (warren-f3bb /
branchPushed-requires-both-reap-and-sandbox-git).
These don't fail acceptance but are footguns when interpreting results:
warrenCLI is not on the in-container PATH for scenarios that shellwarren <cmd>from the host (warren-fab1). Container-mode scenarios that need the CLI usebun /app/src/cli/main.ts <cmd>viadocker execinstead of relying on PATH symlinks. The Dockerfile does symlink/usr/local/bin/warren, but the harness doesn'tdocker execinto the container — scenarios that need the CLI declaremodes: ["in-proc"].- Supervisor
--no-authknob is via env, not flag (warren-93ee closed; mx-24f580). Container mode setsWARREN_BURROW_NO_AUTH=1in the override env block. - Runtime image installs curl (mx-4c4bee) — first-run diagnostics
inside the container can shell
curl http://127.0.0.1:8080/healthzdirectly without thebun -e fetch(...)workaround the original warren-bd69 issue described. - macOS Docker Desktop nests bwrap partially — see container-mode caveat above.
The three V1 dogfood passes are the canonical record of how the composition flow failed in the wild. Re-read them before shipping when a manual gate fails.
The first end-to-end exercise of the composition flow against a real claude-code agent surfaced three burrow gaps. Each shipped a fix in the same session.
burrow-7b97:burrow servehad no in-process executor, so runs queued forever. The fix wiredRunDispatcherintostartServerand hookedRunsClient.setOnCreated.burrow-55e3: HTTPburrows.upignored agent-id hints and lefttoolchainPaths: []. The fix acceptsagentsonHttpBurrowUpInputand threads them throughresolveEffectiveAgents. Warren's spawn forwardsagents: [agent.name]to match.burrow-0329:buildBwrapArgvlacked--uid/--gid. The sandboxed process inherited host root, and claude-code refused to run with--dangerously-skip-permissions.
Warren-side fixes:
- Bump
@os-eco/burrow-cliin bothDockerfileandpackage.json+bun.lock. The supervisor'sBun.spawnresolves./node_modules/.bin/burrowbefore PATH, so a Dockerfile-only bump does nothing. - Bundle
@anthropic-ai/claude-codewith an explicit postinstall invocation.bun install -gskips lifecycle scripts. - Add a compose-time
CANOPY_SOURCE_DIRbind mount for local-canopy testing.
Outcome with burrow at 0.2.3: warren run claude-code <prj> -p "..." against warren itself completes in ~5s. The run ends state: succeeded with branchPushed: true and a real model response in run.event seq:4.
Open gaps for V1 after this pass (all open warren seeds):
warren-dcf3: the supervisor does not auto-wireGITHUB_TOKENinto git's credential helper.warren-93ee: the supervisor has no--no-authknob for burrow loopback dev.warren-fab1: thewarrenCLI is not onPATHinside the container.warren-3c40: reap cannot tell "queued, never started" apart from "crashed".warren-bd69: the runtime image lackscurlfor diagnostics.
All five first-run gaps closed, then a second end-to-end run against warren itself surfaced six new structural seams. Burrow patched one mid-session: burrow-e9e7 landed in 0.2.6. The claude-code runtime now default-allows ANTHROPIC_API_KEY plus the OAuth env names, with no project burrow.toml [env] block required. Claude-code is a built-in runtime, so its env contract ships built-in too.
With burrow at 0.2.6, the agent authenticated cleanly (apiKeySource: "ANTHROPIC_API_KEY" in the seq:1 init). It ran for 12m48s and ended with a clean terminal result event: is_error: false, 102 turns, and bun test 417/417 + lint + typecheck all green inside the sandbox.
None of that work landed on the remote. Two compounding bugs explain why:
warren-67cc: the burrow worktree's.gitfile points at<project>/.git/worktrees/<burrowId>. That path lives outside the bwrap mount. The agent cannot rungit commitinside its workspace, so reap had nothing to push.warren-a69a: warren's reap does not transition runs on terminal events. The bridge stores the result event, but the runs row staysrunningforever. Even with a commit, reap would not push.
Mulch records the pattern as mx-runs/branchPushed-requires-both-reap-and-sandbox-git. A branchPushed: true is a compound output of in-sandbox git AND warren reap. Either half failing alone produces the same shape: a stuck running run with no branch.
Other open seeds from this iteration:
warren-5f19:deleteProjectrmrf's the disk before the row delete. The FK onruns.project_idmakes the row delete fail. The system ends in a(row exists, disk gone)state. Recommended fix:ON DELETE SET NULLplus row-first ordering inside a transaction.warren-1eaa:bun install -gruns as root during build. The/usr/local/bin/{sd,ml,cn,sapling,overstory}symlinks point into/root/.bun/..., which userbuncannot traverse. Every os-eco CLI dangles at runtime.warren-5165:.env.exampleand warren's docs claimed canopyenv_passthroughflows viaburrow_config, butparseBurrowConfigonly reads[sandbox].network. Resolved as docs-only: the claude-code env contract is now burrow-built-in viaburrow-e9e7. Project-levelenv_passthroughplumbing waits until a non-built-in runtime needs it.warren-d9ad: the UI's RunDetail badge does not react to incoming events. Even after the reap fix lands, the UI shows stale state until a manual refresh.
Released as 0.1.2 after all six second-dogfood gaps closed. Bumped @os-eco/burrow-cli to 0.2.7 in both Dockerfile and package.json + bun.lock. That pulls in the gitdir-bind fix burrow-7a80: burrow's bwrap profile now binds the host worktree gitdir into the sandbox. An agent at UID 1000 can resolve <workspace>/.git → /<host-path>/.git/worktrees/<id> and run git commit inside its workspace.
Two runs against warren-on-warren produced the cleanest signal yet on the composition flow.
Run 1 (run_a98cfx1fantf, prompt "Work on sd warren-9f65. Use ml") completed succeeded in 6m28s / 49 turns with branchPushed: true. Zero of the agent's work reached the remote: gh compare main...burrow/bur_r9mjn6da9xc9 reported ahead_by: 0, total_commits: 0. The branch ref pointed at main's SHA. Warren reap pushed an unchanged HEAD because the agent never ran git commit.
The thin canopy claude-code prompt (canopy-daf3, "You are a helpful coding assistant. Be concise.") carries no commit contract. Combined with src/runs/reap.ts:257-265 (push-without-commit) and the branchPushed boolean, any successful push flips it true, including a no-op. The result is a silent work-loss shape that even an attentive operator misreads as success. Filed as warren-f3bb (P1: observability fix B, canopy-prompt fix C, docs/design/agent-composition.md doc fix D).
A secondary entanglement: warren-fead. The agent emitted stop_reason=end_turn while it waited on a foreground bun install Monitor. It wrote that it would wait for the monitor, then made no follow-up tool call. The run ended before commit could happen.
Run 2 (run_agpet4ev6e4a, prompt "...Commit and push when you're done") produced the first warren-on-warren success that actually shipped. Branch burrow/bur_0qgh4pwpvgv0 at SHA 15339e61 with ahead_by: 1. A real diff across 5 files: acceptance scenario 04 implementation, lib changes, mulch, seeds. This validates the fix-C scope: instructing the agent to commit suffices for the smoke-test agent.
The "and push" portion proved inert and counterproductive. Agent-side git push failed four times with fatal: could not read Username for 'https://github.com'. Warren's supervisor installs the insteadOf rewrite rule into /root/.gitconfig (src/supervisor/git-credentials.ts:65-71). Burrow's bwrap profile ro-binds /usr, /etc, /lib, /lib64, /bin, /sbin, /opt, but not /root. Same architectural pattern as warren-1eaa: the Bun store at /root/.bun and the git config stay invisible inside the sandbox. Filed as warren-1a09 (P2).
For V1, fix A (canopy prompt instructs commit only, since warren handles the push) and fix D suffice. Fix D documents the contract: the agent commits, reap pushes, the sandbox has no github auth path. Reap pushed the agent's commit because reap runs host-side with /root/.gitconfig visible. The system did the right thing while the agent burned ~5 turns on doomed retries.
| File | Purpose |
|---|---|
scripts/acceptance/run.ts |
Harness entry; argv parsing, mode dispatch |
scripts/acceptance/lib/inproc.ts |
In-proc boot (warren + burrow as host children) |
scripts/acceptance/lib/compose.ts |
Container boot (docker compose up -d --build) |
scripts/acceptance/lib/fixtures.ts |
Local git fixtures + insteadOf rewrites |
scripts/acceptance/lib/burrow-with-stub.ts |
Wraps burrow serve so stub-shell is registered |
scripts/acceptance/lib/stub-agent/agent.sh |
Deterministic no-network stub agent |
scripts/acceptance/lib/assert.ts |
Scenario runner + assertion helpers |
scripts/acceptance/lib/http.ts |
Bearer-aware fetch + NDJSON streaming |
scripts/acceptance/scenarios/01-13 |
The 13 V1-goal acceptance criteria |
package.json |
acceptance + acceptance:container scripts |
When adding a scenario, mirror the existing file shape: a top-level
JSDoc with the criterion + verification surface, an exported Scenario
constant with id, title, modes, and an async run(ctx). Use
WarrenHttp for HTTP, assertEqual/assertTrue for asserts, and
AcceptanceError for thrown failures so the runner's table shows the
message verbatim.
Acceptance (nightly) runs acceptance:nightly daily and supports manual
dispatch. After each run on main, Acceptance health issue updates one
GitHub issue titled Nightly acceptance health.
The first failure opens the issue. A recurring failure reopens it. A successful run closes it.
Identical failures refresh the body without a comment. A change in failing scenarios or workflow steps adds a comment.
Cancelled and skipped runs do not count as recovery. An older rerun cannot supersede a newer nightly. The workflow conclusion controls issue state, including failures after the scenario scoreboard.
The issue includes the run link, commit, scoreboard, and failed steps.
WARREN_ACCEPTANCE_RESULTS tells the harness where to write its JSON
results.
The workflow uploads test-results/acceptance.json for the
reporter. If setup fails or the job times out before this upload, the
reporter uses job results alone.
Personal repositories assign the issue to their owner. The repository
variable NIGHTLY_ISSUE_ASSIGNEE overrides this default and supports an
assignee for organization repositories.
The reporter uses GITHUB_TOKEN
with actions: read, contents: read, and issues: write. It needs no
additional secret.
The reporter runs code from the default branch and treats the artifact as data. It becomes active after merge. A manual acceptance run on a topic branch tests the suite and artifact upload without changing the issue.