How PaperCusp works
PaperCusp is a desktop app that runs fleets of AI agents against real projects — and ships their work through the kind of release discipline a great engineering org would insist on.
Agents are treated as what they are: capable but unreliable processes. So the architecture spends its effort where it pays — coordination, so many agents can work one codebase without chaos, and verification, so nothing unproven ships. This is the engineering tour: it starts high level and drills down, one subsystem at a time. Every mechanism is described as the development source implements it today, so a detail here can be ahead of the latest release.
The desktop app
The shell is a native window hosting a fast web UI; behind it runs the operator, the system's backbone process:
- serves the UI — every screen is a live view over the same state the agents write
- hosts the agents' entire tool surface — every verb an agent can call terminates here
- runs the schedulers and background routines — the recurring machinery has one home
- owns a Postgres instance embedded in the app — the choice that shapes everything
Agents themselves are ordinary CLI coding-agent sessions launched as OS processes — visible terminals arranged on your desktop when you want to watch, headless background sessions when you don't.
- MCP is the umbilical — each session connects back to the operator's tool surface over MCP, so any capable agent CLI can join a fleet without bespoke integration
- projects arrive as pots — packaged repos with their agents, docs, conventions, and work queues
- installed from the Cupboard — the in-app distribution shelf (a black box here; it has its own documentation)
- pots federate peer-to-peer — between your own machines over an encrypted mesh; there is still no central server in the loop
Inside the machine, state reaches your screen through one sync path. Because view state lives in the URL, that path is addressable: an agent can read what you're looking at and drive the same interface you do.
Data leaves the machine only on paths you chose:
- the inference gateway — model calls to hosted providers; models you serve locally (llama-server, vLLM, Ollama) route through the same gateway and never leave the box
- integrations you connect — mail, Slack, calendar, social; each agent call is recorded in the invocation ledger, and publishing to your public identity stays withheld until you enable it
- your own machines — pot federation over an encrypted peer-to-peer mesh
Usage telemetry and the public directory beacon are opt-in and off by default.
// the sync path, from either side of the glass: // a UI pane reads — one declared query, resolved in the operator, pushed over SSE useSyncQuery({ queryName: 'workItems.byHarness', args: { harnessSlug: 'papercusp', states: ['wip'] } }) // any write — agent or human — invalidates precisely; exactly the touched queries re-resolve notifySyncInvalidate('workItems.byHarness') // and because view state lives in the URL, an agent reads + drives the same screen you see ui:get_state {} → { url, pathname, params: { tab: 'items', … }, title, last_seen_at } ui:dispatch { intent: 'set_url', args: { params: { tab: 'decisions' } } }
Recurring work has two engines and one inventory
Much of what the operator does runs on a clock: firing routines, sweeps, watchdogs, cache refreshes. All of it runs on exactly two mechanisms, and every timer on the host can be listed by name.
- the durable tier — DBOS scheduled workflows, backed by Postgres, which survive a restart. Two of
them drive the routines.
routinesTickruns every 30 seconds and does nothing but claim and start each admitted routine that is due;routinesSweepTickruns at 15 and 45 seconds past the minute and carries the periodic sweeps, so a slow sweep never delays a dispatch slot. Each skips its turn when another run of itself is still in flight - the in-process tier — a named wrapper around the platform timer. Each timer has a re-entrancy guard, so a slow tick never stacks a second run on top of itself. A tick that is still running after two skipped fires is logged, and logged again at 4, 8, 16 and so on, so a wedged timer shows up without one line per interval. A heavy timer can skip a cycle while the host is under load, and the wrapper records each timer's last fire and last error
- timers that survive restarts — an in-memory timer restarts its interval every time the host restarts, so a host that restarts more often than the interval never fires it. That was measured: a background host restarted 20 times in 8 hours, and every 30 to 240 minute timer on it read zero fires. So a timer of 15 minutes or longer is aligned by default to a fixed wall-clock slot, offset by a hash of its name so timers do not all fire at once. A restart then loses at most the slot that fell inside its own downtime, and a slot within two minutes of startup is skipped so no fire lands in the startup ramp. Per-connection timers are never aligned, which would make every socket's keepalive fire at the same instant
- one declaration surface — the routines table says what should run, per project, for both tiers,
and never runs anything itself. A blueprint's schedule entries are written into it. An entry marked
tier: ephemeralis a frequent sweep with an interval in seconds; a per-host executor arms one named timer per such routine, plus one heartbeat timer of its own, and records each fire by updating that routine's row, never by inserting one row per fire - nothing ticks unseen —
schedule:inventoryand the Schedules admin page merge five sources into one list: DBOS schedules, routine rows, the named timers armed in this process, a list of known sweeps that are not armed here (with sync-triggered jobs, whose armed state is read from their feature flag rather than assumed), and the timers of other processes. Sibling host processes, such as the background host and staging, are read live from/api/internal/managed-timers, with real last-fire and last-error values. Only the inference gateway, its watchdog and the agent launcher serve no such endpoint, so they fall back to a static list whose fire state reads as unknown - no bare timers — a build check fails on any new bare
setIntervalin the host. Its temporary list of grandfathered files is empty. The permanent exceptions are the wrapper itself, the generic libraries that keep their own timers, browser-side code that runs in the webview, and the separate processes above - shared state has one copy, and a count proves it — a library that keeps state
at module scope, such as a registry, a cache or a pool, has a single copy only while the loader makes one
module record for it. A CommonJS preflight beside the ESM loader, one file reached by package name and by
relative path, a symlinked package, or a bundled copy beside the source can each split it with no error, and
every reader then sees part of the state as if it were all of it. That happened to the timer registry:
@papercusp/scheduled-registrywas evaluated twice in one process, and a timer fired every 2 minutes while missing from the inventory, unnoticed for more than 6 days. Such state is now pinned throughpinModuleStatefrom@papercusp/module-singleton, which keeps it on the realm under aSymbol.forkey and also counts how many times the module body ran.listModuleDuplications()names every key evaluated more than once, and/api/internal/managed-timersadds that list, with one warning line per key, whenever it is not empty. A pin written by hand is just as correct but invisible to the count, which would make an empty list meaningless, so a build check,lint:no-hand-rolled-module-pin, fails on any new one. Its shrink-only list of older hand-written pins is down to 1 of the original 21: the server-sent-events channel in a generic library, kept on purpose so the library can be borrowed without the dependency. On 20 October 2026 the check was failing on three new hand-written pins, one in the process clock and two in a test fixture
Every process the operator starts is on a ledger
The operator launches agent sessions, test runs, builds and shell jobs, and each of those starts processes of its own. Rather than policing every place a process can be spawned, the operator confines the roots: each task it launches runs in its own systemd scope, named after the task's ledger row. Children inherit their parent's control group, and a process cannot leave its group without privileges, so everything a task starts stays inside the scope, including a grandchild that double-forks to detach itself.
- the row comes first — a task is recorded as pending before the fork, so a crash in between leaves a visible row instead of nothing. Once the process is running, the row is stamped with its kernel identity, the boot id plus the process start time, so nothing downstream has to trust a bare process id
- degrade, don't refuse — where systemd cannot provide a scope (no session bus, a container, macOS), the process still starts and is still on the ledger, marked unconfined. Refusing to start work because the accounting layer is missing would turn an observability feature into an outage
- kill by identity, never by name —
processes:kill { taskId }signals the whole scope, so no orphaned grandchild survives. It is refused withidentity_mismatchwhen the recorded identity no longer matches, so a recycled process id can't be hit by mistake. If a polite stop leaves the scope alive, it falls back to a boundedsystemctl stopand reportsincompleteonly if processes still survive. A kill addressed by pid reaches only managed agent processes; a pid that now belongs to anything else is refused - relief without losing work —
processes:freezepauses a whole task tree through the cgroup freezer and resumes it later. Freezing stops the CPU but not the clock, so a wall-clock deadline inside the task can trip the moment it thaws; the result names those deadlines as warnings.processes:limitchanges a running task's memory cap, CPU weight or task limit in place, with no restart, and refuses an unconfined task, because a budget the kernel cannot enforce is not a budget - end states that don't overclaim — when the ledger said running and the kernel disagreed, the task ends as stranded, not exited, because no exit code was ever seen. When systemd confirms the scope was released cleanly but no exit code was recorded, it ends as ended_unobserved
- residue is reported, not killed — the reconciler sorts processes it finds without a live ledger row: unaccounted (inside the operator's slice but on no live row, because something bypassed the ledger or a scope outlived its row), a terminal window a person owns, a window that was closed while its processes kept running, and foreign processes outside the operator's control groups. These classes are report-only. When the classes were designed, 14 of the 15 closed-window scopes on the development machine still held live agent sessions or shared services, so a closed window is not evidence that its processes were abandoned
- enforced at build time — a check fails on any new detached spawn that bypasses the ledger; the few legitimate exceptions, such as a terminal window a person asked for, are listed, each with its reason
Heavy jobs wait for a slot instead of piling onto the machine
Dozens of agents share the development machine's 128 cores, and the expensive things they run (test suites,
typechecks, builds) are mostly the same few commands. Started freely, they arrive together: one herd of 52 test
runs and 35 typechecks drove the one-minute load to 216, and the operator's own heartbeat starts failing at about
1.3 times the core count. So heavy commands go through one wrapper, pc-heavy, which holds a slot for
the whole life of the command, and a check that runs before every shell command sends an unwrapped heavy command
back to it.
- the slot is a kernel lock — a slot is a file lock held on an open file descriptor for as long as the command runs. The kernel drops it the moment the holder exits for any reason, a hard kill included, so a crashed run never leaks a slot and there is no daemon or database to fall out of step. If the slot machinery itself can't be set up, the command runs unthrottled: admission control must never be the reason a legitimate build can't start
- the count follows the load — the floor is one slot per 32 cores, never fewer than two, and every 16 cores of idle capacity in the one-minute load opens one more, up to one per 8 cores. On the development machine that is 4 slots when the box is saturated and 12 when it is idle. A waiting job recomputes the count as it polls, so admission tracks load in both directions
- memory has its own budget — load can look healthy while memory runs short, so the count is also capped at available memory divided by 14 GiB: the largest typecheck measured, 12.74 GiB, plus about a tenth, so one admitted compile fits its slot. When the machine is already reclaiming memory the cap is halved, and it never drops below one slot, so heavy work always moves forward
- a bare typecheck is refused at any load — a TypeScript compile can take 11 GiB on its own while the load still looks fine, so the pre-execution check refuses an unwrapped compile outright and names the wrapper. Other unwrapped heavy commands, test runners and builds, are refused only when the one-minute load is above the core count. The repository's own test and typecheck entry points already go through the wrapper, so they pass
- the release gate gets the box while it judges — the gate re-runs each failing test file on its own to tell a real break from a load flake, and contention there flips verdicts rather than just costing time. While a gate run is live, the slot count is divided by phase: by four while it re-runs failures, by two during the hours-long main sweep, and down to a single slot while it copies its dependency snapshot. A phase it can't read, from an older gate or a missing file, counts as divided by four, so not knowing throttles more. During the two re-run phases a broad test sweep is refused outright with exit 75, while a run scoped to named files is never refused, and scoped test runs and typechecks get a separate lane of one slot
- the snapshot copy can take every slot — when the gate copies its dependency snapshot it asks for all of them. Running heavy jobs are asked to yield: each gets 30 seconds to finish, then a terminate signal, then 30 more seconds before a kill, and reports a machine-readable preempted status that its caller can opt in to retrying a bounded number of times
- waiting is bounded and visible — a waiter polls every slot in turn instead of blocking on one; blocking on a single slot once queued about 20 waiters on slot 0 while two other slots sat empty. It prints a line the moment it starts queueing, so a backgrounded run's empty log isn't mistaken for a dead one. After 15 minutes it runs anyway, or after 30 seconds for a run scoped to named files, so a stuck holder can't wedge the fleet. A waiter its caller terminates before it gets a slot exits with 75, a temporary failure, rather than an ambiguous signal code. Admitted jobs run at lowered CPU and disk priority
- identical runs share one result — when several agents run the same command in the same directory against the same tree, the first becomes the leader and the rest replay its captured output and exit code instead of queueing for slots of their own. The match covers the tree's git state and its submodules, plus any environment variables the caller declares as inputs, so a different tree or input never receives someone else's verdict. A finished result is replayed for 90 seconds. A follower waits for a live leader for up to 30 minutes, and judges it alive by its process identity, not by a timer
- memory pressure pauses a job rather than killing it — after admission the wrapper keeps sampling memory pressure. When pressure is high and available memory is below the larger of 8 GiB and a tenth of the total, it freezes one admitted job, the lowest-priority and newest, choosing only among its own jobs that hold real memory and never the release gate, and at most one per pressure episode. Pressure alone is not enough: on a host with a large page cache it tracks disk activity, and it once froze work while 65 GiB was still free. A frozen job is thawed after 30 seconds of low pressure, and one frozen for 5 minutes while pressure stays high is ended with a timeout record, so a pause can't become a hang
The database changes only through numbered migrations
The shared schema is a sequence of numbered SQL files, starting from one self-contained baseline that creates every table, applied in order. Every boot applies whatever files are still pending, and a ledger table records each file applied. A host that only answers requests runs this step before it serves anything, and refuses to start HTTP if it can't, rather than serve code against a schema it couldn't bring up to date.
- numbers are reserved, not picked —
next-migrationallocates under a Postgres advisory lock: one more than the highest number on disk, reserved, or already applied, recorded in a reservations table, so two agents writing migrations at the same moment can't take the same number. It also refuses a name that nearly matches one applied or reserved moments earlier, which is what several agents racing to fix the same failure look like - drafts are invisible until armed — the new file starts life with a
.DRAFTsuffix, and the runner only applies files ending in.sql, so a boot that happens mid-edit can't run half-written SQL. Arming runs the same per-file checks the boot runs, then publishes the checked snapshot and refuses to overwrite an existing file - SQL and ledger row commit together — an ordinary file's statements and its ledger row run in one
transaction, so it is either applied and recorded or neither. A new file may not carry its own top-level
BEGINorCOMMIT, because an inner commit would end that transaction early - applied means frozen — the ledger is keyed by filename. A sha256 of each file is stored but never compared, so an edit to an applied file would be skipped on every database that already ran it and applied only on fresh ones: two populations silently diverging. Changing applied schema is always a new migration, and an edit hook refuses a new migration file whose number was never reserved, while the fix is still a rename
- careful with locks at boot — the per-file checks refuse, among other things, a trigger change that takes an exclusive table lock without an acknowledgement in the file, so it fails at startup naming the statement instead of stalling every reader. On the shared host, a statement waits at most 15 seconds for a table lock rather than queueing an exclusive request ahead of every reader. The runner gives a lock timeout two more tries, 400 and then 800 milliseconds apart, and when every failure in a pass was contention (a lock timeout, a deadlock or a serialization failure) boot re-runs the pass, up to three passes in all
- a backup is a rendezvous, not a wall — boot first tries the advisory lock that workspace backups
hold. If a backup has it, boot tries again every five seconds, and goes ahead without it as soon as it proves
that no pending file names a table the dump still holds; with nothing pending it goes ahead at once. A new
dump can start after that proof, so when boot goes ahead without the lock, each file is proved again against
the live lock set just before it runs. The proof is a text match against the names of the tables the dump
holds (their indexes and parent tables count too), so it treats its own blind spots as conflicts: a file
that runs dynamic SQL, drops with
CASCADEor acts on a whole schema can't be proved safe, and waits as if it named a held table. The wait has no deadline of its own and fails closed: if boot can't read the pending files or the backup's locks, it counts that as unsafe and keeps waiting. Before the first wait it logs which files it is waiting on and why, so a slow start reads as a named wait, not a hang - each file passes a guard chain just before it runs — on the shared host, boot re-runs the per-file checks and confirms that the file's number is reserved for exactly that filename. A file that carries a pending-code-deploy marker is refused until the deployed release's copy of a file it names contains the exact code text it requires, so a schema change can't land ahead of the writer it was meant to replace. Every migration from number 1007 on is also refused unless the workspace's latest pre-destructive backup receipt is a finished snapshot that includes a good database dump
- a broken file can't keep re-locking — on the shared host a failing file is left pending instead of
halting the operator every agent depends on. A per-host cooldown then skips it for five minutes on every boot
and every release-gate preflight, so a known-broken
ALTERdoesn't re-take its exclusive lock each time. Once the cooldown passes it is retried, and the ledger, not the cooldown file, stays the record of what applied. The desktop's embedded database keeps the stricter default and stops at the first failure
The execution flow
Everything starts in conversation. A goal — "build X", "migrate Y" — lands with an agent, and then:
- scope and route are pinned first — what exactly, built how, run by whom (this session, an existing fleet, a new one) — before anything executes
- the answer becomes a plan — a durable, itemized, dependency-edged execution contract, audited against the full source conversation before it activates (§03)
- activation promotes plan items — into the work-item ledger, where each unit gets an owner, a lifecycle, and — eventually — completion evidence (§04)
From there, no human walks the graph: a fleet drains it, and completed work rides the pipeline out (§11).
// the same flow as verbs — chat is the only freeform step plans:new { slug: 'db-rework', title: 'Rework the schema' } plans:add-item { slug: 'db-rework', phase: 'Phase 1 — schema', text: 'write migration', importance: 'high' } // → P-001; importance has no silent default plans:add-item { slug: 'db-rework', phase: 'Phase 1 — schema', importance: 'normal', items: [{ text: 'backfill rows', blockedBy: ['P-001'] }, …] } // batch form: each result names its new item id; one failure never fails the rest plans:start { slug: 'db-rework' } // activation: audited first, then items PROMOTE to work items // a fleet member pulls — the scheduler walks the DAG, nobody dispatches by hand scheduler:get_next {} → { workItem: { id: 'WI-1204', … }, checkpoint: …, planDecisions: […], premises: … } // context rides the claim in
Six ways to start a plan, one gate in front of them
A plan can be set running six ways: plans:start, an interactive launch
(plans:launch), a manual fire (plans:run-now), arming its schedule
(plans:arm-schedule), a fleet launch (fleet:launch-on-plan), and the scheduled fire
itself. Six separate checks of "is it ready?" would drift into six slightly different answers within a release,
so they share one gate module. Five of the doors call its loader, checkPlanStartable. The scheduled
fire already holds its own copy of the plan, so it calls the same readiness function directly
(evaluatePlanStartReadiness) and runs the governance check itself. A guard test lists the six doors
and fails the build if one stops reaching the gate. A second test fails if any new code calls the scheduled-fire
path without being on that list.
The gate asks three questions, in a fixed order:
- are the inputs ready? — required inputs set and well-formed against the plan's declared schema. A launch or manual fire can pass values for this run only; they are merged over the stored ones key by key, an array replaces rather than appends, and a key passed as undefined cannot blank a stored value. The gate hands back the merged inputs it approved, so the door saves exactly what was checked, not a second merge that could differ
- is the acceptance contract complete? — the plan's acceptance bars (§03) must already have their
identity, falsifier and provenance. The start door (
plans:startandplans:launch) also requires each bar's method before any proof exists; manual and scheduled fires and fleet launches act on plans that are already running. When a bar's snapshot names a different rubric revision than the current one, the gate first tries to re-pin that stale subject and reads the contract once more before refusing - is this exact revision admitted? — asked last on purpose. The first two are things the author can fix; admission is the pot's governance verdict on one revision. Asking it first would send the author to a ratification round for a plan that still couldn't run
A refusal says which kind it is: plan_inputs_not_ready (the first two) or
plan_admission_refused (the third). The remedies differ, and a governance refusal that read like a
missing input would send the reader to fix the wrong thing. A plan that doesn't exist passes the gate untouched,
because every door already has its own "not found" answer and the gate shouldn't invent a second one.
- arming counts, though it runs nothing — it is what makes a plan fire later, unattended. Checking there means a plan with unset required inputs is refused at 14:00 to the person who armed it, instead of failing at 03:00 in a routine nobody is watching
- the fire checks again anyway — the schema or the values can change while a schedule stays armed, so the arm-time answer can go stale. The scheduled fire re-runs readiness and admission before it starts anything, and returns a typed "not started" result. Two refusals are marked retryable: a missing required input, and an admission refusal, because a ratification round can admit the same revision later without anyone editing the plan. Malformed values, a broken or conflicting schema, an unknown template or an incomplete acceptance contract are not
- a schedule that runs a system action is not a start — a schedule can name a deterministic system
action instead of the plan (rubric re-scoring is scheduled this way). Its routine targets that action directly
and never starts or clones the plan, which only carries the cadence, so arming it skips the start gate. Arming
a schedule that does clone the plan is refused with
nothing_to_runwhen every item is already done or dropped, since each fire would copy a finished plan and mint no work - admission is switched on by policy, not a flag — a pot whose signed policy has no plan-admission section passes every plan, so existing pots kept working unchanged. Once the section exists the check fails closed: if no valid certificate can be found for the exact revision, the plan is refused. Editing a plan after it was ratified therefore refuses it as a revision mismatch rather than running a version nobody approved
- governance controls outrank the certificate — pause, revoke and quarantine are checked before the certificate, so a perfectly valid certificate can't outvote them. Each has its own code because the fix differs: a pause lapses on its own, a quarantine clears when the round is recomputed, and a revoke never clears
- shadow mode measures before it enforces — a policy can be set to watch only, and an emergency switch turns enforcement off. Either way the identical verdict is computed and recorded as "would have admitted / refused" without being applied, so the evidence predicts what enforcement would actually do. Admission covers seven doors in all, not six: status flips, promotion and the ready-plan autostart sweep call the admission check directly, and a separate registry and guard keep each of them wired
- topping up a running fleet is not a start — adding members to a fleet already working the plan skips the gate. That fleet's stored claim spec governs the wave, and re-reading the plan's bars there could refuse a valid top-up on an unrelated bar result. A new fleet still takes the full gate before anything is created
- what
plans:startdoes — it refuses, in order: a plan already terminal; invalid dependency edges; a missing conversation audit (§03); the three gate checks; for plans with first-class clauses, a spec-quality score that isn't current for the exact clause set, or missing bar critiques; and a claim lane it can't read. Only then does it approve a draft to ready and promote its items into work items. Starting again is a no-op. Those checks run twice: once as a preflight, and again at commit under the plan's lock, so a revision edited, a schema or input defect, or policy evidence invalidated between the two stops it before anything is approved or promoted. If the lock or the commit-time validation is unavailable, it promotes nothing
The “Now” line is checked against the graph
Every plan carries a ## Now block: where it stands, and what is next. That next pointer
is a second copy of a fact the item graph already owns, and nothing rewrote it when items finished. Measured on
2026-08-31: of 349 plans whose next named a plan item, 208 named only items already done or dropped — and
89 of those were on plans still live.
Replacing the line blindly would throw away real information ("P-001 first: it is the smallest change" carries reasoning no graph can produce). So each read of the plan reconciles it instead, and reports one of five outcomes:
- superseded — every item it names is finished, so the pointer derived from the graph goes first,
marked
⟳ derived, and the author's line is kept verbatim beneath it, labelled superseded and naming the finished items it still points at - ratified — it names at least one live item, so it stands verbatim; the author may have chosen a lower-ranked item on purpose. One case gets a caution appended, naming the finished item and the graph's own pointer: a line that mentions a live item but still directs work at a finished one (“Start P-015” after P-015 shipped). It is the check plan lint already runs when the line is written, run again at read time, the only moment a line that went stale afterwards can be caught
- unverifiable — it names no item of this plan, so the graph has no say over it. Only ids this plan
defines count: a
P-003citing another plan's item is not a claim about this graph - filled or abstained — there was no pointer and the graph supplies one, or the plan has no items and the prose stands
Five outcomes rather than a yes/no, so a pass-through that was never checked can't read as a confirmed one. The graph's own verdict travels with every outcome, and the stored text itself is never modified: anything computed goes above or below it, never in its place. The State line gets the same read-time check. When it still describes an item that has since finished as current work, the read leads with that item's real status and keeps the whole stored State beneath it.
A finished plan says so on its own
A plan's status label (draft, ready, active, shipped) is written by hand, and nothing used to
rewrite it when the work under it finished. It is the same kind of second copy as the Now line, one level up:
a plan whose last item had already closed kept advertising live work. When the fix was written, 205 live plans
(out of 1,692 rows) sat at ready or active with every item already finished. So a plan's real position is now
derived from its item graph on every read, and the label is moved only where the graph is conclusive.
- five verdicts, computed at read time —
active;stalled(items are live but nothing has moved for 30 days, the default threshold);implementation-complete(every item is finished and no acceptance rubric is active);awaiting-acceptance(every item is finished and an acceptance rubric is active); orno-items, which callers must treat as no verdict, never as finished. No active rubric has two opposite causes: acceptance hasn't begun, or it already ran and the plan shipped, since a rubric retires with its plan. The verdict is the same for both, because the graph can't prove the grade passed, but the text names a retired rubric so a shipped plan reads as validated and closed and nobody writes a replacement rubric. The momentum half names the activity it measured from, such as a plan write or a work-item transition, so an “idle for 12 days” can be checked - two transitions, and only two — when every item is finished, a ready or active plan moves to
awaiting-acceptance, and the recorded reason says plainly that this is not a claim the plan passed anything. If an item comes back to life, the plan returns toready, because it is waiting on work again, not on a grader. This rule never marks a plan shipped - a reaction and a backstop — a reaction rule on
plans:set-statusapplies the transition as soon as an item change drains the plan. A sweep every 30 minutes catches plans that drained before the rule existed: for those, the event that would have re-examined them had already fired - contradictions are reported, not hidden — a plan labelled
shippedthat still has live items reads back as contradicted, with the count of live items as the evidence - the exit is filed as work — moving plans into
awaiting-acceptancewould only park them, so a second sweep runs every six hours and files one claimable work item per waiting plan, naming the first thing the acceptance gate would refuse. It asks the same evaluator thatplans:set-plan-statusenforces, so the filing can't disagree with the real refusal. It files at most 25 new items per run; items it already filed are refreshed on every run, whatever the cap
The plan system
Plans live in a shared store every agent can read. A plan carries:
- typed items — discrete units of work with dependency edges
- a "now" pointer — naming the current focus, so nobody guesses what matters this week
- decisions — numbered, addressable rulings recorded the moment a trade-off is settled; a decision
is citable (
plan#D-012), so a peer acting on a ruling re-reads the authority instead of trusting a paraphrase relayed through chat - success bars declared before implementation — the acceptance rubric owns what must become true; the verification method is filled in once the implementation is known
Plans are also where cross-lane disputes go to die: a ruling is recorded as a decision the moment it forms, and rules travel with the work.
The mechanism has teeth. In one observed run, an agent re-reading a recorded ruling mid-execution discovered its own earlier report was wrong — it self-corrected, then swept up several more instances of the same error.
A relayed chat message cannot produce that outcome; an addressable decision can.
A decision is an address, not a message
plans:add-decision allocates the next D-NNN inside a lock, so two agents ruling
at the same moment never mint the same number, and an exact retry of the same title and body is deduplicated
instead of appended twice. The retry check compares only against the plan's most recent decision, and also
requires the same set of plans the ruling affects, so the same words recorded long ago as a different ruling
are never swallowed. Numbers are allocated per plan, so a ruling quoted anywhere else is cited by its full
reference, <slug>#D-NNN. A bare number would name a different ruling in every plan.
- rulings ride the claim — when an agent picks up a plan's work through
scheduler:get_next,work_items:claim,work_items:claim_nextorplan_items:claim, the plan's governing decisions come back inline asplanDecisions. The worker doesn't have to know a ruling exists to receive it - the window holds eight, newest first — a plan with more than eight rulings has a log, not a short list, so a claim carries at most eight, each body cut to 240 characters. Rulings recorded on other plans that name this one as affected take the first slots. The rest go to this plan's most recent decisions, because the newest are the ones that defer, re-scope or overrule an item. The window once kept the eight oldest instead, and a worker handed an item a later ruling had deferred was told it had seen the governing decisions. The claim now reports the full count next to the window and says how many it left out, so eight is never read as all of them
- a ruling made after the claim still arrives — at the start of each turn, a session holding unfinished work on a plan has the same window read again and is shown only decisions it hasn't been shown before: up to three, then a count. A failed read shows nothing and leaves the record of what was shown untouched, so a brief outage can't make every old ruling reappear as new
- a fact about the code should show its measurement — a decision that asserts something about the
tree is expected to carry
Measured: <file:line or command + result>. Without one, the write still lands but comes back flagged; a ruling whose only authority is another decision gets a separate flag. It is a nudge rather than a gate, aimed at the moment a remembered fact is about to become a rule - two detectors, because each is blind where the other looks — the first fires when a decision
names a source-like path (a
.ts,.sqlor.rsfile, say) with no measurement after the label; a label with nothing behind it is a promise, not a measurement. A ruling that cites only<slug>#D-NNNnames no path and would pass in silence, so the second looks for a decision cited as authority, skipping one cited only to overrule it. Left unchecked that chain compounds: the next author cites this ruling in turn, and the chain never touches the code. A body that did measure is exempt from both, because warning someone who measured teaches that the warning is noise
Five checks can refuse the write before anything is appended, and two effects
follow it. Other lanes are told to trust a decision over a paraphrase, so the checks that stop a bad one
run before anything is written, in the order listed here. One repair is made rather than refused: tool-call
markup that a broken client leaked into the body is neutralized (its < swapped for a
full-width <), so a later agent can't mistake it for its own transcript and the ruling is not
lost over a malformed tag.
- the deviation fence is reserved — checked first, against the body as sent. A fenced
deviationblock is the machine record of a deviation from a requirement (below), so a hand-authored decision that carries one fails withdeviation_fence_reserved; only theplans:conformancetransaction may write one.plans:set-decision-bodyrefuses the fence too, so a clean decision can't be rewritten into a forged approval. Removing a fence through the editor is still allowed, because it only lowers authority - a promotion has to resolve — a decision promoted from earlier material passes
promotedFrom: up to 24 typed sources (a memory, a session turn, a work-item, a report, a fact or a plan decision cited as<slug>#D-NNN), each resolved in this workspace before the write. A source that is missing, ambiguous or retracted refuses it. The validated list is stamped as aPromoted-From:line on top of the body, and a body that hand-writes that line is refused.facts:assertanddocs:authortake the same argument - no pointing at a moving target while a candidate is frozen — while the release gate holds a
frozen repair queue (§11), a decision telling agents to pick or test a moving
source as the candidate fails with
frozen_candidate_lineage_violation. The only way through is to reword it: name the frozen repairHead, or negate the instruction, which is allowed so a successor can carry the correction. Facts, work-item and loop checkpoints, and compaction notes go through the same check - a false owner citation is refused — a line can anchor itself to a specific turn with a
[turn:…]reference. If a line claims the owner's authority and its anchor resolves to a turn the system classified as not the owner (a loop fire, a wake, a compaction note), the write fails withfalsified_owner_anchor(§13). A bare owner tag with no anchor only draws an advisory: when the check was built, refusing those would have rejected 432 of the 483 owner-tagged lines in existing decisions.plans:set-decision-body, which rewrites a decision in place, runs the same check, so a clean decision can't be recorded and then rewritten to say something else. Unlike the fence and owner checks, the frozen-candidate check does not run on that rewrite - no append onto a stale base — inside the lock, the write also reads the plan's latest recorded
revision. If the live body is missing decisions that revision holds, appending would reuse their numbers
and overwrite them, so the write fails with
stale_base_decisions_lost, names the missing ids and points at the revision to restore from - a third advisory catches claims of absence — a decision asserting that something does not
exist comes back with
absenceLint, listing each claim with a recheck, unless the body carries aMeasured:line. It stays advisory because about one decision body in ten tripped it when it was measured, and a hand-read sample mixed real gaps with quotations and corrections - after the write: cross-plan reach, and the bar re-read — a decision that lists other plans in
affectsis stored once, in its own plan. Each affected plan gets a cross-reference event and its watchers are notified; a slug that doesn't exist comes back asunknown_affectsinstead of being dropped. Because a ruling can change scope, the same call re-reads whether the plan's success bars are still ready and brings its "now" pointer up to date
A requirement that can't be met is deviated from, not edited away
A plan's ## Requirements blocks, joined with its ## Conformance contract, are
checked while the work is running, not only when it ships. One routine, system:spec-conformance-sweep,
ticks every 15 minutes and runs an active or ready plan's conformance when it has never run, when a commit
touched a path inside the plan's declared footprint, when a linked work-item changed state, when the last run
still showed drift or failed to record, or when six hours have passed. At most four plans run per tick; the
rest keep their trigger and lead the next tick. The state change is read after the run, so writes the run
makes itself can't trigger it again.
- drift has a narrow meaning — each runnable
violatedWhenfalsifier is evaluated, and one that fires is drift. A done-when check that moved from pass to fail is drift too; one that has never passed is progress, not drift. Every verdict is recorded in the results ledger underconformance:<harness>/<plan> - one alarm per requirement, and it blocks — a drifting requirement opens one deduplicated
drift-alarm work-item, linked
blocksto every unfinished work-item mapped to that requirement, and firesplan:drift-alarm:<slug>when it first opens. It closes once every check it rests on measures green, which releases the blocks - a close names the requirements it serves — when the item being closed is mapped to
requirements,
work_items:completeneedscompletion.dischargesorcompletion.contributesToto name them (conformance_requirements_missing,conformance_requirement_unmapped). An open drift alarm on a mapped requirement refuses the close withconformance_drift_alarm_open, and a discharged requirement whose done-when check is executable needs a passing recorded result (conformance_done_when_not_passing). A contribution needs no pass but still respects the alarm. The close answers only for its own item's requirements, so a support item is not blocked by a different item's malformed one - the way out is a recorded deviation —
plans:conformance { op:'propose-deviation' }records the proposal as a decision, andop:'approve-deviation'records the approval as a second one. Each carries the reserveddeviationfence. An approved deviation suppresses its requirement's drift alarm, closing one that is open, and exempts the requirement from the done-when pass at close. A new proposal or approval for a requirement that already has an approved deviation fails withdeviation_already_approved - authority follows where the requirement came from — if a requirement's source refs cite the
owner (a session turn, a turn or owner tag, the word "owner", a directive number), the approval needs an
ownerTurnRefthat verifies as an owner turn, or it fails withdeviation_owner_turn_requiredordeviation_owner_turn_unverified. An ambiguous source counts as the owner: asking the owner once more is cheap, and approving away the owner's requirement is not. Plan-item and decision refs are agent structure, not owner speech - validity is re-derived, not inherited — whether an approval counts is worked out again from the current requirement text on every read, never from the flag the approval recorded. A later edit that adds an owner source to the requirement invalidates an agent's approval instead of carrying it along
Every item has to trace back to something the owner said
The activation audit checks in both directions. Forward, it maps each requirement in the source conversation onto the plan items that carry it. In reverse, it checks that every item still in the plan traces back to something the owner actually said. Forward-only was not enough: an agent could cite one of its own turns as the "source" of an item it had just invented, and the forward map would count that item as covered.
- four labels per item — owner: a mapping to it cites at least one owner turn; derived: declared as following from other items, at least one of which is owner-backed; agent-added: declared as the agent's own addition, with a reason; unresolved: none of these
- only an affirmative owner verdict counts — three turn-origin verdicts qualify:
owner-typed,owner-turnandowner-dialog. An assistant turn, an injected message, a machine surface or a turn of unknown origin never makes an item owner-sourced. Unknown origin is not treated as proof of fabrication either: it simply does not qualify, and the auditor can still declare the item derived or agent-added. Loop replays of an agent's own goal are recorded with speaker "user", so the check reads each turn's recorded origin (§13), not its speaker - derivation can't launder — an item declared derived from sources that are not themselves owner-backed stays unresolved. A declaration can honestly say "I added this"; it cannot manufacture owner backing
- chains resolve, rings don't — derived items are resolved to a fixed point, so a chain derived → derived → owner resolves, while a ring of derivations with no owner anchor never does
- the failed rung is named — an unresolved item carries its reason:
no_mapping(nothing targets it and nothing was declared),no_owner_ref(mappings target it but cite only non-owner turns),derived_source_not_owner_backedoragent_added_reason_missing - refused, not warned — an item with no owner backing and no declaration fails the audit with
item_provenance_missing, and a malformed declaration withitem_provenance_invalid. Both name each offending item and record nothing. The declaration check collects every problem in one pass: an id that is not a current item of the plan, a second declaration for the same item, a derived declaration naming no source, an agent-added one with no reason - grandfathered once, then ratcheted — a plan whose earlier audits predate the check records
enforced: false, with the unresolved items as a warning, until every item resolves. Enforcement only moves one way: once an audit of a plan is enforced, every later audit of it is too
The door re-checks the audit; it doesn't just trust that one was recorded.
An audit is a row written once, and a plan keeps changing after it. So every door that would make a plan's work
executable reads the plan's latest activation audit again at the moment of use, and judges it against the plan as
it is now. Those doors are a write that moves a plan into ready or active (creating it there
included), plans:start, and starting a single item of a draft plan.
- three ways to be refused —
conversation_audit_required: there is no activation audit at all.item_provenance_unresolved: the latest audit was enforced, but the plan now holds an item it never resolved (added, or un-dropped, after it), and each such item is named.plan_declarations_invalid: the audit was gated on the plan's reuse and libraries sections, and while the stored plan is still draft or ready, one of them has since been removed or broken. That last check reads the content the write is about to commit, so a single write can't activate a plan and delete a section in one go - a transition, not every edit — the write door fires only on the move into ready or active.
Editing a plan that is already there is allowed by design;
plans:startruns the same check again each time it is called. Returning a plan from awaiting-acceptance to ready, which internal lifecycle repair does, is not a new activation and skips it - older plans are grandfathered, once — a plan already sitting in ready or active with no audit at all activated before the gate existed. It passes with a logged warning instead of being retroactively blocked, and should be audited on its next material edit. A plan still in draft gets no such pass
- two templates are exempt, and one is reserved — at the status-write door and the item-start door,
rubric plans (activated as part of proposing or ratifying a rubric) and child-seed plans (synthesized when one plan spawns
another, so there is no conversation to audit) skip the check. Because that exemption would be an easy way
around the gate, a write that introduces the child-seed template is refused with
child_seed_template_reservedunless it comes from the seed writer itself - no starting items on an unaudited draft — moving an item from not-started to wip or
done on a draft plan that fails the check is refused with
activation_audit_required, carrying the gate's own code. Work already under way keeps going: an item that is already wip may finish, an item a live work-item already covers counts as in progress, and a done backed by a work-item that has reached a terminal state is accepted as evidence rather than treated as a new start. The rule exists because agents had been working unaudited drafts: one count found 54 draft plans with items in progress or done and no audit
From ambition to verified release
BAR means what must become true; METHOD means how we will check it. Requirements capture the objective before implementation. They seed versioned success bars in the acceptance rubric, mapped into plan items, spec clauses and work contracts. Once the code exists, the author adds a concrete method and structured check for each bar. Filling in a method cannot quietly lower the bar.
- Prove the outcome in its required setting. Bind current evidence to the exact spec revision. A code test, a deployed check and an actual live workflow prove different things; a child plan's completed tasks do not prove its parent's promised outcome.
- Keep the judgments separate. Review the measurement method, independently grade every outcome, then record the acceptance author's response. A disclosure or an unmeasured result cannot satisfy a mandatory outcome.
- Repair failures explicitly. Failed evidence sends work back for repair. A changed success bar requires a recorded amendment, refreshed mappings and new proof and grading where the change makes them stale. Old plans are backfilled from trustworthy requirements or left explicitly unresolved.
Who does each step matters as much as the order. The success bars already exist — the activation audit seeded them from the plan's requirements, before any code was written. At completion the code-truth audit runs first, while the implementation is still warm and gaps are cheap to find; only then does the implementer amend each seeded bar to as-built reality, adding the method that will prove it. Every amendment is recorded, so writing the method cannot quietly lower the bar. Read the figure by its lanes rather than its arrows: the vertical zigzag is the design. Every drop out of the implementer's lane is a point where the system stops trusting the party that did the work.
Asking to ship is also how a grader gets found. When
plans:set-plan-status refuses a shipment because no admissible independent grade exists (the
rubric is ungraded, graded only by its own side, missing an independent verification, or vetted again after
it was graded), the refusal itself recruits one grader. It asks the relevance router first and assigns an
existing agent that clears the router's floor, is reachable and sits outside the implementers' lineage. Only
when no such agent exists does it launch a fresh judge, and that judge descends from an independent system
actor rather than the author, so the lineage guard accepts it. A plan with unfinished items is re-checked
against the grading-only gate, since its remaining non-code steps may still need a grade; any other refusal
recruits nobody, so an unrelated failure can't spawn agents. A dryRun evaluates the whole ship
gate and lists every blocker without writing, recruiting or stamping anything, and the shipment that finally
succeeds retires the plan's acceptance rubric.
draft → ready / active and
seeds those bars, and acceptance is not shipment — a plan whose
work is finished sits in awaiting-acceptance until it has been graded and accepted.Recurring plans: writing a schedule and arming it are separate acts
A plan can recur. plans:set-schedule writes the cadence: an RFC 5545 RRULE or a cron line, a
one-shot fire time, an expiry, and a timezone so calendar-time recurrences stay correct across daylight-saving
changes. It validates the RRULE before storing it, and it makes nothing fire. plans:arm-schedule is
the separate act that does: it materializes the plan's routine and switches the schedule on.
plans:disarm-schedule pauses it again: it switches the schedule off and deactivates the routine
but keeps the written schedule, so re-arming is one call.
- arming has its own autonomy category —
schedule-arm, which the default autonomy policy seeds as never-auto, so an agent running on its own autonomy can't decide by itself to start firing unattended runs. The owner, or a session carrying the owner's authority, arms directly. Unlike the protected category that arms the system's own loops,schedule-armis deliberately left unprotected, so a plan's risk can later earn it more autonomy - a schedule fires one of three things — by default its routine runs
system:plan-run, which runs a copy of the plan as described below. A schedule can instead name a registered blueprint operation, which fires in place of that plain plan run and can pin the exact template revision and content a run must use. Or it can name a plan-schedulable system action, which runs directly, with no agent turn, and clones nothing:rubric-rescore, which re-runs a rubric's executable checks and raises an alert when a criterion falls below its bar, is one. An action that isn't registered, or hasn't opted in to being scheduled, is refused when the schedule is written, not at its first fire, and one schedule can't name both an operation and an action - arming refuses what would only fail unattended — arming runs nothing itself, so its checks run in
front of whoever arms the plan rather than at a 03:00 fire. A plan with no schedule is refused. So is a
cloning schedule whose items are all done or dropped: every fire would copy a finished plan, mint no work
and still look like a healthy run, which one schedule did for 21 days before this check existed. The start
gate
plans:startuses (required inputs, the acceptance-bar contract and admission) is checked too, for the scheduled door. An action schedule clones nothing and skips both checks; an operation schedule skips only the finished-items check - a plan-run fire is a fresh run of a copy — it never runs the template itself. Each one mints an
instance plan (
<template>@run-<token>), a run-ledger row and the instance's items as frontier work items, through the same promotion pathplans:startuses. The instance plan and its ledger row commit in one transaction under a lock keyed to that run; promoting the items is a second, idempotent phase that re-runs on every replay. A crash between the two heals instead of stranding a half-made run or minting its work twice - a replayed fire is the same fire — a scheduled fire's token is the routine's fire time, stamped
when the fire is claimed, so a durable step that re-executes recomputes the same instance and does nothing.
A manual
plans:run-nowhas no cadence to anchor to and always takes a fresh token; two processes that draw the same one are separated under the same lock - overlap and catch-up are declared up front — what to do if the previous run is still going (queue, skip, or cancel the previous one; the default is skip), whether missed fires are skipped or replayed, and an optional per-plan spend cap. A recurrence can also list extra one-off dates and excluded ones, such as holidays. On expiry the schedule deactivates; the plan is not deleted
Work items & scheduling
The work-item ledger is the durable, fleet-visible record of everything in flight, and it exists to enforce one rule: nothing important lives only in a conversation.
- closing is where the discipline bites — a terminal close needs structured evidence, and a bare "done" gets re-opened
- the way in is guarded too — a bug or change claimed through
scheduler:get_nextis scored for freshness against its own code paths and the running build; a likely-stale one (a linked fix, no freshness stamp at all, or a score under 0.6) is routed to a cheap validation lane: reproduce on the current head first, and close only with evidence that it no longer reproduces. The score routes work; it never closes an item by itself - a blocker is typed, never prose — an event, a gate, runtime state, or a human capability, each with a ref something can await or clear; work-item-on-work-item waits are links in the DAG instead
// closing is where the discipline bites — evidence is structured, not prose work_items:complete { id: 'WI-1204', state: 'done', assumptions: 'none', completion: { summary: 'migration 0812 applied + backfill verified', testsRun: 'npm run test:file -- db/migrate-0812.test.ts', testResult: 'passed (14/14)', verifiedHow: 'integration', filesChanged: ['db/sql/0812-rework.sql'] } } // set_state to "done" with no evidence? REFUSED. complete with no test evidence? "proposed" — // and a proposed close that still blocks live work spawns a verifier task; a reject reopens it
A stuck item says exactly what it waits on, in a shape the system can act on. An ask of the owner has to carry the concrete question, the verb that unblocks it, and what the system will do if the owner never answers. It can't degrade into "waiting on Avi" with nothing to answer, and silence is a stated outcome rather than an open-ended wait. A product decision that an agent could make itself becomes a park instead: it opens a peer review of the agent's proposed default, and the stored ref becomes that consult.
// blocked on the owner: the ask is structured — the question, the verb that clears it, // and what happens if nobody ever answers work_items:set_blocker { id: 'WI-1204', kind: 'human', capability: 'credential', ref: 'github_pat', summary: 'publishing the plugin needs a GitHub token with repo scope', nextVerb: 'setup:save_key', defaultIfUnanswered: 'stays parked; the weekly digest re-surfaces it', decideBy: '2026-11-01T00:00:00Z', defaultAction: 'stay_parked' } // → opens an owner question with three fixed choices (provide it through the secret surface, // route it to the credential holder, keep the item parked) and the stated default; past // decideBy with no answer, the typed defaultAction is applied once: stay_parked keeps the ask open // and the item parked; release_to_agents clears the ask and returns the item to the claimable pool // unless another blocker still holds it. The free-text default is never parsed, and a passed // decideBy with no typed action is surfaced rather than applied // → activeBlockers + capabilityDispositions: what to await or obtain, per capability // refused at the write, nothing persisted: an owner ask missing its question, its verb, or its // default-if-unanswered, and "wait for the production deploy" on work the staging build can // already verify. Not a blocker at all: a bare 'WI-1190' as the ref is written as a blocks edge // in the DAG (convertedToEdge), which releases itself when WI-1190 reaches a terminal state work_items:set_blocker { id: 'WI-1204', ref: 'github_pat', clear: true, evidence: 'token saved; plugin listing published' }
A close is graded, not just accepted
A close that passes the schema still isn't taken on trust. work_items:complete compares the
evidence with what the system can check for itself and records a completion authority with the close:
committed only when the evidence is complete and nothing the system can check contradicts it, proposed
otherwise. Complete means a verifiedHow plus a test run or result: filesChanged says what
changed, not that anything was checked. The grade and its
explanation come from a single decision point, so they cannot disagree. The full contract is set out once,
in §12: every check that can downgrade a close, the causal record a bug close must carry,
and all five authority values. Two parts of it belong to the work item's own lifecycle, so they are here:
- the downgrade names its cause — the response lists the exact paths and the rule that fired, and says what does not trigger it: deleting a file still counts as work, and so does an edit git-sync hasn't committed yet; that close lands proposed only until the commit, when git-sync's post-commit settlement upgrades it (how settlement works). An agent told only "insufficient evidence" resends the fields it already sent
- a proposed close that others wait on gets one independent check — if the
item still blocks live work, the close spawns a single verification task, and the dependents wait on it through
ordinary blocks edges, so every existing dependency reader holds them with no new meaning of
"satisfied". The check has to be independent: the claim door refuses the task to its reporter or implementer
(
verification_conflict, permanent for that agent), the close door refuses it to the item's closer, and it closes only with a verdict. Accept lets the dependents proceed; reject reopens the item to its closer, whose own edges hold the dependents again. If git-sync's settlement later upgrades the close to committed, the open task is accepted automatically, so nobody spends a turn re-checking evidence that already qualifies
A checkpoint is hard to lose by accident
A checkpoint is an item's handover note: what is done, what is left, what comes next. It is re-delivered to whoever next picks the item up, including the same agent after a restart, and it can be the only record of days of work. So the write path refuses the common ways of destroying one.
- only the holder writes it — checkpointing an item assigned to someone else is refused
(
not_holder); a note on someone else's item is a comment. This closed a real failure: an agent that had only read an item checkpointed it, took the silent success as proof it held the item, and wrote that false belief into its own handover - a shrinking rewrite is blocked — a plain replace that would cut a substantial checkpoint by more
than half is refused before any data is touched (
shrink_blocked). A short refresh is retried once as an append under the same lock, so the earlier history stays beneath the new note; a deliberate reset has to sayconfirmShrink: true - an append can't silently become a replace — the addendum is joined to the stored note inside the
same locked write that saves it, so there is no separate read of the old text whose failure could drop it.
And an append that the 32,000-character checkpoint cap would trim away is refused
(
append_body_overflow, with the space each part takes) rather than saved without it - the write proves itself — the response carries a hash of the text the store actually persisted, so the caller can confirm in the same call that the note landed byte for byte
- "still current" is a claim the store checks — when nothing has changed, an agent
can attest the stored note instead of rewriting it:
unchanged: trueplus thecontentHasha previous write returned. The attestation passes the same holder and terminal-state checks as a real write, and it is refused if the hash no longer matches the stored note, if the item row moved after the note was written, or once the budget is spent: at most three attestations in a row, on a note no more than two hours old. An attestation can postpone a fresh checkpoint, never replace one - a handover can't point at a moving target — while the release gate
holds a frozen repair queue, the check that guards plan decisions (§03)
runs on checkpoint text too, and a note telling a successor to pick or test a moving source as the candidate is
refused (
frozen_candidate_lineage_violation). Only the text this call supplies is checked, not the history it is appended to, so an agent can still document or retire an old instruction
How the next item gets picked
No human dispatcher hands out work. An agent calls scheduler:get_next, and one atomic database
statement claims the single next eligible item for it. Eligibility has two layers, and they do different jobs:
- global floors — the same for everyone: the item is ready and not blocked, not leased to someone else, admitted to run, matched to the caller's swarm, and not a duplicate. No ordering choice can claim past a floor
- the claim spec — the fleet leader's scheduling judgment, stored as a filter plus a rank that the agent carries or inherits from its fleet. It can only narrow and reorder inside the floors; it can never reach a blocked or taken item. Self-selection stays with the agent, so the leader steers which work runs next without dispatching any of it
- no double-claims — the pick is
FOR UPDATE SKIP LOCKED, so two agents asking at the same instant get two different items rather than a race. A successful claim also declares the agent's intent, so peers see the lane immediately - one claim at a time, by default — an agent already at its limit (one concurrent
claim unless its spec raises
limits.maxConcurrentClaims) is refused with the cap, the ids it holds, and an explicit note that the lane itself was not evaluated, so a full hand is never misread as an empty backlog. A plainly parked claim still counts; an item parked on a typed blocker does not, so recording what it waits on withwork_items:set_blockerfrees the slot while the agent keeps the item - failing closed when the scope is unclear — a fleet member with no inherited spec is told to wind down rather than guess; a caller bound to a plan without a spec is refused; only an unscoped caller gets default ordering, and is warned that it did
- the default rank — four terms, in order: path affinity (work touching files the agent already
holds), severity, priority, then age. The spec
plans:startwrites for its plan narrows the filter and keeps this same rank. Severity sits ahead of age deliberately: under plain oldest-first, criticals waited behind thousands of older nits - how the freshness score is built — every stamped bug or change starts at 0.5. Newer commits touching the item's own paths take 0.35 off, and paths confirmed untouched add 0.25; when the paths can't be measured, that term counts for nothing either way, because many open bugs name no resolvable path and penalizing them would fill the validation lane on ignorance rather than evidence. A runtime build that moved since the observation takes 0.35 off, the same build adds 0.2, and an unknown one takes 0.1 off. A successful reproduction within the last day adds 0.2, one older than a week takes 0.2 off, and none on record takes 0.05 off; an observation older than a week takes 0.1 off, older than a month 0.2. Linked fixing work takes 0.45 off and sends the item to validation whatever the total, and an item with no stamp at all scores 0.2. The total is rounded and held between 0 and 1. The paths term is measured only after the claim commits, in a bounded follow-up step; until that finishes, or if it times out, the term is declared unmeasured rather than read as unchanged. An earlier version fed the running build into both comparisons, so the source term compared a value with itself and always reported no change. The validation lane is also told that a check quoted in the item body was written by the filer, not proven, so it has to be run against a negative control first: a loose one fails toward closing real work
- stopping the line when the gate stays red — once a harness's release gate has
been red for more than 24 hours (an open
gate-red-streakcondition item older than that), self-selection on that harness stops serving new work that isn't repair. Bugs are exempt, because fixes are the line, and so are items opened by an alarm or repair condition; condition items that are ordinary inflow, such as plan promotions, stop with everything else. An owner or leader can still hand out any item by id. Nobody has to switch the throttle off: it lifts when the gate greens and the condition item is settled, andwork_items:claimablereports it as a floor of its own. On 20 October 2026 the main harness was under it: all 249 items open to self-selection were bugs, and the floor's own count of rows held back was 4,179
A miss is a diagnosis, not an empty result. "Nothing for you" splits into a drained backlog versus a stranded one ("N unclaimed items in the pool, but none pass the floors"), with a per-floor breakdown of what is holding them, and when the leader named specific items, which floor refuses each. When the spec's own filter matches no rows at all, the answer says the spec is the problem, not the floors.
Asking without claiming has its own read. An open status is not the same as
claimable, so work_items:claimable runs the same exclusion check scheduler:get_next
runs when it misses, and claims nothing. It returns a count, the rows that pass and, on request, a sample of
what each floor held back, all built on one shared definition and read inside one REPEATABLE READ
transaction, so the three describe the same instant. They used to be separate reads: a peer's claim landing
between them once produced “one claimable, none returned” under a limit of fifty, and the scheduler told the
agent to file a claim-path bug that did not exist. A failed read fails the call rather than reporting zero,
because “could not measure” must stay distinct from “drained”. The answer is relative to the caller: by
default an item you released within the last five minutes is held back from you alone, and a newly filed item
is kept for its filer for up to ten minutes before anyone else can take it. Both rules stay out of the shared
database view, which answers “claimable by someone”. The per-floor counts overlap, so they do not add up to the
gap. On 20 October 2026, 249 of the roughly 214,000 bug, change and task rows the default filter selected in
the main harness passed every floor for one agent; about 205,500 of the rest were agents' turn-end
reflections, which are never claimable.
Across machines, a claim is also a lease. After the local claim, the agent acquires the item's lease from the hive's authority (on a single box that authority is itself): when two swarms contend, exactly one wins, and the loser releases its local claim so the item goes back into the backlog. If the authority is partitioned away, the claim fails open to a local advisory lease, and a reconcile pass resolves any double-claim once the hive reconverges.
The coordination plane
Work runs as fleets — a leader and N members on a plan (§03) — and the plane rests on a few hard bets:
- pull over push — members claim work through a spec the leader tunes; an idle agent next to a non-empty queue is a spec bug, not a dispatch chore
- concurrent editing is safe by design, not by hope — lock arbitration at the write turns collisions into conversations instead of clobbers
- liveness is one shared verdict — every surface agrees on live · parked · draining · suspect · ended · recorded, so "is anyone actually working this?" has exactly one answer
- supervising N agents costs one read, not N check-ins — a leader brief folds every member's liveness, claims, unanswered questions, and context pressure into a single call, so the leader re-orients from live state instead of interviewing the fleet
- gates latch — nobody sleeps forever on a flip they missed
Lock arbitration at the write
Agents edit one shared working tree. Instead of asking each agent to remember to coordinate, the lock is taken at the moment of writing: a hook runs before every file edit and claims a lock on that file. An agent never has to work out whether a peer might be nearby; it makes the edit, and the lock decides who goes first.
- a block says who and why — if another agent holds the file, the edit is refused, and the refusal names the holder and the intent they declared. The blocked agent works on something else, or queues for the lock and ends its turn; its instructions forbid renaming or copying a file to get around a lock
- held for the edit, released after it — a matching hook runs once the edit lands and releases exactly what that edit took. When one message carries several edits, their files are locked together up front and released together when the batch finishes, so a multi-file change is never half-held. A file the agent had already locked on purpose stays locked: the edit's release covers only what the edit itself acquired
- derived files are rebuilt before the lock lets go — when the edited file
is an input to a generated one (the type declarations generated for the repo's enrolled
.mjssources, or the test-domain contract file), the release hook first re-runs that output's deterministic generator, while the source is still locked. The generated copy never lags its source in the shared tree, and nobody is handed a chore whose only correct answer is a command. A generator that fails is recorded, and the lock is released anyway - one rule, every client — the same hook guards Claude Code's edit and write tools, Codex's patch and file-write tools, and the platform's own edit and write tools. OMP agents run an equivalent hook that takes the same locks
- it fails open, and says so — if the operator can't be reached, the hook lets the edit through rather than stall every agent (after retrying a refused connection three times over about two and a half seconds), and leaves a health marker behind. The coordination dashboard reads those markers and shows that enforcement is offline. An operator that answers but grants nothing, or sends a reply the hook can't read, also lets the edit through, and then the agent is told in its own context that this edit is not lock-protected, so an unprotected edit never passes for a healthy grant. One refusal fails closed instead: if the operator rejects the session's identity, the edit is blocked. Arbitration holds only while the platform is up
- a lease as the backstop — every automatic lock is also a 20-minute lease, so a release that never runs (a crashed client, a killed process) cannot strand the file. When an agent asks to be restarted on fresh context, its leftover automatic locks are released in the gap after its last turn ends and before the new process starts, while the locks it took deliberately are kept for the successor. A lock records its owner but not the session that took it, so that gap is the one moment the old session's edits can be told apart from the new one's. A crash gets no such release and waits out the lease
- wait without polling —
locks:acquire { wake_on_grant: true }puts the agent in the queue and wakes it when the lock is granted. Withpending_editit can hand the server the exact edit instead: on grant, the server applies it only if the text it replaces still appears exactly once. If the holder changed that region, nothing is applied and the agent is woken to redo the edit itself - half-finished work isn't committed — the routine that commits and pushes the shared tree skips any path under a live lock and records which holder kept it out, so a file is never committed while an agent is still editing it
- a turn commits as a unit — locks cover one edit, but a refactor spans many. So the commit routine also holds back every file a live session changed through its edit tools during its current turn, and the whole change lands together once that turn ends. The record of each edit is written when its lock is granted, so a file a shell command writes leaves no record and is committed on the next sweep. The hold is bounded: it lifts when the turn ends, when every recorded session of the editor has ended, or an hour after the turn's first edit. That cap is per turn, not per file: once it trips, every file from the turn is released together, so a long turn's source never lands ahead of the file that imports it, and a hung turn cannot keep work out of the tree forever. An editor whose client never records turn ends is not held at all, because nothing could release its edits early
- dead holders don't block forever — a file lock is a lease, so an abandoned one expires on its
own. Named exclusive locks, such as the one the commit routine itself takes, don't wait for the lease: a
request for one whose holder is gone reclaims it on the spot instead of reporting it held. Two checks
decide "gone". For an owner whose name carries a process id on this host (the commit routine, a deploy),
the question is whether that process is still running. For an agent, it is whether the shared liveness
verdict says its session has ended. Only
endedcondemns a holder: a suspect, parked or draining one keeps the lock, and so does one whose liveness can't be read, with the lease as the backstop
Peer awareness — a theory of mind
Coordination is also an awareness problem — a working theory of mind about your peers.
The unit of that awareness is a coupling: a pair of agents whose live state is worth showing each other in detail — what the other is trying to do right now, and how stale that is. Every agent stays readable to everyone; coupling only decides who is worth surfacing, so a peer you are not coupled with costs you nothing.
- mostly derived — two agents become coupled on their own when they contend for the same file (one holds its lock and the other is queued for it, or both are queued; a file has only one holder at a time), are both waiting on the same event, have exchanged directed messages in the last 30 minutes (broadcasts don't count), or sit on either side of a blocked-by edge in the same plan. A fourth family is obligations, recorded at the moment someone enters them: a consult the router assigned between a requester and a responder; a scorecard still open between a grader and the author of the graded work (for up to a week); and a fleet leader and each of its members, active within the last two hours
- direction is kept — a coupling says which way it points, because the reader's next move differs. A peer whose item blocks yours is someone to chase; a peer blocked on yours is waiting on you. A peer who owes you an answer is someone to chase; an answer you owe is unfinished work you may not notice on your own, so it is shown with its age. A leader owes supervision to each member, so a member that died within that two-hour horizon still shows on its leader's side, marked ended — the most actionable obligation of all
- membership is not a coupling — two members of the same fleet are not coupled by that fact. A 30-member fleet would otherwise derive hundreds of edges, and the fleet is already on every roster row. Only the hierarchical leader–member pair is an edge. A derived edge has to be a relation the two agents actually entered, carrying something the roster can't already say
- also declarable —
coord:coupleskips the wait when an agent already knows it is working alongside someone. Any agent may couple any two, including a pair it is not part of: a third agent who spots two peers heading for the same work is exactly the case worth serving - relevance, never access — coupling grants and blocks nothing; it changes whose state is shown, not what may be read or who may be messaged
- a decouple is a mask, not a delete — the shared lock or blocked-by edge is still real, so
deleting the row would let the next derivation pass silently re-create it.
coord:decouplerecords a durable suppression, with its author and reason (and an optional expiry), so "never coupled" and "deliberately decoupled" stay distinguishable
Messages are structured envelopes rather than chat, so confidence and its gaps travel with every claim — and awareness arrives while you work, not in a mailbox you remember to poll.
// a message is an envelope, not a chat line — expectations and gaps travel with it coord:send { to: ['@fleet-leader:db-rework'], expects: 'answer', summary: '0812 backfill: rows with NULL owner — drop or default?', body: [{ text: '…', forYouBecause: { relation: 'owns', ref: 'db-rework' }, couldNotDetermine: [{ what: 'whether NULL owners are load-bearing' }] }] } // expects has NO default — even an FYI must say 'none' out loud. Sent to an explicit agent // id, a message expecting anything else stays UNANSWERED on the leader brief and // fleet:assignments until the recipient answers it — asked-and-ignored is a state the // system can see. An @selector send like this one is not tracked that way
The same don't-let-it-drop principle guards commitments. A pending owner decision or an unmet promise is
recorded as a wall — pinned into every subsequent wake, brief, and carry document until it is
explicitly cleared — so a blocked question survives any number of context resets instead of quietly falling
out of somebody's window. Writing a wall is half of it; the other half is that the owner can read them
all in one query (coord:walls). It is a union over three places walls are already
written, not a store of its own: the walls in every agent's loop carry-note, every work-item parked as
needs-human, and every live standing fact in the wall: slot. Each row is joined
to its agent's liveness and sorted into one of four statuses:
- waiting: the agent is alive and will act on the answer. This is the owner's real work.
- stale: the agent is draining (its heartbeat has gone quiet, but a wake can still reach it).
- stranded: the agent has ended, or its process is confirmed gone. Answering would unblock nothing, because nobody is left waiting on the answer. It needs a respawn, not a reply.
- unknown: no liveness verdict could be read, so the row is not counted as owner work.
Stranded rows are shown by default, flagged rather than hidden. The waiting and stranded counts cover the whole population, not just the page of rows returned, so a truncated list cannot read as "nothing is waiting on you".
You don't have to know who to ask. The consult router (consult:get_feedback) turns
"who knows this?" into one call. It checks the archive of closed consults first and answers instantly,
waking no one (verdict served_from_archive), when one already settled a highly similar question: similarity of at least 0.8, and the
same named plans, rubrics or other durable objects, so a specific archived answer never answers a generic
question or the reverse. A caller whose situation has moved on re-asks with archive_floor: 2
(a similarity no match can reach) to force fresh routing. Otherwise it ranks every agent's real transcript history by relevance and
launches an isolated answer session from the best match's transcript — forked on the same model
backend, converted when it has to cross one. When the expert's session cannot be carried over at all (an
untracked session, or no conversion available), the answer session is briefed instead: a fresh
session under its own identity, given the expert's cited turns as quoted evidence (capped in length) and
never presented as the expert's own continuity. The expert's own live session is never woken, so
asking costs the expert nothing, and a long-dead expert can still answer: the transcript, not the
session, carries the expertise.
- honest below the floor — a responder chosen below the relevance floor (to guarantee at least one answer) is labelled as such, never passed off as an expert; ask for zero minimum and a weak pool comes back as a plain "no one knows more than you do"
- can't-measure ≠ measured-nothing — when relevance could not be computed at all, the verdict
says
relevance_unmeasured, so an outage is never read as "nobody knows" - no manual retries — the dispatcher already walks the allowed model ranks and skips any backend currently at its usage wall
- a launch failure is not "nobody knows" — when experts were selected but no answering session
could be launched for any of them, the verdict is
no_launchable_responder, a dispatch failure kept apart from the measuredno_qualified_responder. Each skipped candidate's reason is recorded on the attempt and the walk moves on; only a list with nobody left ends it, with the reasonno_available_responder. That is why one walled backend heals itself instead of stalling where the owner can see it - a cascade, not a single shot — with several responders selected, the server moves to the next one when the current one replies, declines or lets the consult expire, and carries a bounded digest of the feedback so far, so a silent responder cannot stall the chain
- route without launching —
allowDispatch: falsereturns the ranked candidates as references and launches nothing (verdictretrieval_only)
Two more routes round out the exits. Work that outgrows its holder moves by structured handoff — the item, its checkpoint, and its open questions travel together. And a decision that is genuinely the human's becomes an owner-gated ask, recorded as a wall until answered — routed, never guessed at.
Getting a message to a live session
And delivery is engineered around one hard question: how does a message reach an agent that is a live terminal session? By its liveness — never by hoping it polls a mailbox.
Under that diagram sits a deliberately split mailbox design — an outbox that owns delivery, an inbox that owns history:
- the outbox is the delivery side — every send is written to an append-only Postgres event log before any delivery is attempted, so a crash between "sent" and "seen" loses nothing; from there, delivery is driven by the recipient's liveness verdict, never by hoping it checks mail
- live sessions never poll — the message is injected mid-turn as a typed delta line, landing between tool calls while the agent works
- waking is the sender's call, and it is explicit — a plain send only injects.
wake:'required'means the wake must land, and a parked session is started on a fresh turn with the payload already in hand;wake:'optimistic'is best-effort, and may fail silently only when the sender names the durable backstop that will retry it. Without one, an optimistic miss is reported as loudly as a required one - a miss is loud, and says why — a required wake that reaches nobody returns
recipient_absentwith the cause split out:recipient_dead(the session ended — relaunch it),recipient_alive_not_wakeable(alive but mid-turn — the message still landed; retry the wake) orrecipient_dormant_scheduled(an armed loop will fire — deferred, not lost). There is no silent drop to discover a day later - unread mail escalates itself — a plain send that injects but doesn't wake can still be missed
by a session that is live but heads-down. So a directed message that a live recipient hasn't read within
five minutes fires that recipient's standing inbox wake, which lands when its current turn settles,
never mid-turn, and at most once every ten minutes per recipient. "Directed" is narrow here: a send to an
explicit agent id. Broadcasts,
@audienceselectors and machine senders such as sweeps and watchers never trigger it, and neither does a message the recipient has already replied to. Unread mail alone isn't enough, either: the wake fires only for a recipient that still owes an answer to at least one directed ask. An inbox holding nothing but FYIs (expects:'none') or asks it has already answered wakes no one, and if that owed-an-answer check can't be read, the sweep fails open and wakes anyway. Parked sessions are left alone: an unread inject to a parked session is the sender's choice, and a must-land send already useswake:'required'. A sweep checks every two minutes. Each wake it fires leaves a marker message fromsystem:delivery-ladder, which the woken agent reads first. The same marker is how the sweep counts wakes against the ten-minute cap, so the cap needs no table of its own - the inbox is the query side — catch-up after time away, audience history ("what was my fleet
told while I was down"), and threading: a directed message that
expectssomething stays visibly unanswered until the recipient answers it. A reply or ack threaded to it counts. So does a fresh message sent straight back to the asker, though each one clears at most one open ask, and a reply threaded to a different message doesn't count. By default a threaded reply also wakes the original asker - silence has a deadline too — the dual of a reply-wake:
replyDeadlineSecasks a sweep (every five minutes) to nudge the sender once if no reply has landed in time, replacing the "chase X at 14:00 if silent" note an agent would otherwise carry — and lose — across a compaction. A threaded reply is not the only thing that stops the clock. Before nudging, the sweep checks five other ways the ask may already be settled. The sender may have sent a later directed message to one of the same recipients (a retraction, a follow-up). The recipient may have answered in a fresh message, or by commenting on a work-item the message named. A sender who declared the side effect the ask requires (expectEffect: a checkpoint, a claim release or a terminal close on a named item) may find it recorded in the ledger since the send. Or an@audiencesend may have reached nobody live, so no answer could ever come. In any of these cases the deadline resolves with a quiet note: no nudge and no wake
The event system
The design goal is sleeping safely: an agent that waits should cost nothing while parked and should be un-strandable. Everything in the diagram serves that:
- keys are returned by declaration — because a one-character drift in a hand-typed key is a rendezvous that never happens
- gates latch — arriving late still resolves, instantly
- a timeout wake is not a shrug — it's a cue to go diagnose the stalled emitter
What it looks like in practice — three calls, one rendezvous:
// the LEADER declares the gate up front — the scoped key is RETURNED, never hand-typed events:emit { event: 'schema-migrated', announce: true } → { announced: true, event: 'fleet:db-rework:schema-migrated' } // discoverable in every member's orientation // a MEMBER parks on the returned key — its turn ENDS; parked costs nothing events:await { event: 'fleet:db-rework:schema-migrated', timeout_sec: 1800, on_timeout: 'wake' } // later the leader fires it ONCE — every waiter is re-invoked, payload in hand events:emit { event: 'fleet:db-rework:schema-migrated', summary: 'migration 0812 applied' } → { waiters: 4 } // the announcement LATCHES: a member arriving late resolves instantly
The properties that carry the weight:
- announce: true — declares the gate without firing it; the platform returns the scoped key and
registers it where the members' orientation will surface it — a zero-message rendezvous. The key is the gate
name prefixed with the declarer's narrowest context:
fleet:<slug>:, elseplan:<slug>:(orharness:<slug>:on request), while a global gate keeps its bare name. The rendezvous namespace is flat, so the prefix is what stops two fleets' gates colliding, and only agents in that fleet, plan or harness see the gate. The reply also carries the declaration's generation andlive_awaiters, how many agents are already parked on the key: the leader's readiness read - timeout_sec + on_timeout — every wait is bounded; a timeout wake means "go check whether the emitter is still progressing", never "wait harder"
- payload — the emit's data rides the wake itself, so the woken agent starts with the answer rather than a hint to go fetch it
- waiters — the emit reports how many parked awaits it actually matched;
waiters: 0is a loud "this reached no one", not a log line. When that happens, the emit also looks for an active key that differs only in case or separators (phase3-readyagainstPhase-3_Ready) and names it;events:awaitruns the same check when no declared gate matches its key. The comparison is exact after normalizing, not an edit distance, so it catches the real drift without false alarms - fromMsg — instead of a key, a waiter can name the coord message that declared the gate; the server reads the key off the message, so nobody re-types it. The message must carry exactly one gate, or the call fails naming what it found
- scope: 'hive' — the same key can rendezvous across machines: each federated peer re-fires it into its own local await store
What an agent can wait on — one primitive, many doors
Every wait in the system is a registration on one primitive, watch:create. The
events:await above is literally a preset of it — watch(key, { wake: true, once: true }).
What varies is how a match is delivered and what gets matched:
- delivery —
wake: false(the default) drops the match into the agent's inbox and costs no turn;wake: truere-invokes the agent, delivered down a ladder: injected into a live terminal, else the session resumed, else the inbox. A session that died while every Claude account was walled (rate- or usage-limited) can instead be continued on Codex through a session port. Two outcomes spend no turn: a wake folded into a sibling wake that is delivering anyway (coalesced), and an inbox wake whose messages all predate the agent's own last inbox or orientation read (suppressed-redundant: it would find nothing new) - cardinality —
once: trueclears after the first fire;once: falseis a standing watch, withmin_sleep_secbounding how often it may re-wake (default 60 s for a standing wake watch, 0 for a one-shot, which should wake promptly); a burst of fires inside the window coalesces into one wake. The floor counts only wakes that really re-invoked the agent; an unconfirmed context reset still counts, so the floor never hammers a host that is probably wedged - an exact event key — optionally with a
payload_filtertested against the emitted data ({ queue_depth: { lt: 5 } }), so "wake me when it crosses a threshold" needs no bespoke key per condition. The filter applies only to a waking watch: an inbox subscription has no per-emit matching step to run it in, so asking for one is refused - a topic — a curated area slug, for ambient awareness of a churning part of the system
- an interest — free text, embedded once and matched against peers' new transcript turns by
similarity (default floor 0.55, deliberately above the 0.4 floor the consult router uses: a consult answer is
asked for and an interjection is not, so a false match here is spam;
min_similaritymay move it anywhere between 0.3 and 0.95). Each watch is swept every 120 s by default. Hosts claim due watches ten at a time from skip-locked rows, so concurrent sweepers split the set instead of sweeping it twice, and one sweep delivers at most five matches, coalesced into a single delivery carrying each peer, a 240-character excerpt and the score. Turns are embedded after they are stored, so the sweep scans only the contiguously embedded window: its frontier stays 10 s behind now, and a turn still waiting for its embedding holds the frontier for up to 30 minutes before it is skipped (an unembedded turn is invisible to every vector search). Each turn is therefore scanned exactly once, with no duplicate fires and no silent misses. Five consecutive sweep errors deactivate the watch and fire its key with the error, so the waiter wakes instead of waiting forever - a predicate — for a condition nobody emits: the engine polls a read-only tool every
interval_sec(default 60) under the watcher's own role, reads a dot-path, compares it (eq ne gt gte lt lte exists contains changed) and fires on the false→true edge. Registration runs one evaluation inline — a broken predicate fails immediately, an already-true one fires at once — and an identical predicate joins the existing poller instead of starting a second
Not every pairing is allowed. targetKind is read only on the inbox path, so a topic
subscription or an event-key subscription that asks to wake is refused with both working forms named, rather
than silently registered as something else (a standing watch that re-invokes on a key is wake: true,
once: false with no targetKind). On the inbox path a bare pattern is read as a topic slug;
an event key needs targetKind: 'event', and with once: true it is registered as a
one-shot await whose row the emitter claims atomically, so it too is delivered exactly once. An interest works
in either mode. A predicate is the reverse: it only wakes. The other knobs are policed the same way:
interval_sec belongs only to a predicate or an interest (a wake floor is min_sleep_sec),
min_similarity only to an interest, and an interest cannot also carry a predicate. A misplaced
argument fails loudly instead of being quietly ignored.
Two layers sit on top so the common waits are one call. state:subscribe is a preset over the
predicate watch for registered state cells: leave the condition off and it wakes when the cell's
meaning moves, not merely its raw number. And seven named verbs — deploy:await,
checkpoint:await, work-item:await, service:await-up,
git-sync:await, plan-item:await, fleet:await-drained — each resolve to
events:await, with the key shape taken from the catalog. deploy:await arms both
outcome keys, success and failure, and checkpoint:await arms all three verdicts (passed, held,
inconclusive), so the agent wakes whichever way it goes. events:catalog is the index
behind all of it: per key family, the template, who emits it, whether it fires today, the poll it replaces, and
how many agents are parked on it right now.
// "wake me when the running process is executing MY change" — no key to know, no poll to write state:subscribe { cell: 'git.pipelinePosition', as: 'packages/…/my-file.ts', on: { op: 'eq', value: 'live' } } // 'live' = deployed AND the serving process started after that code last changed; // 'restart-required' (deployed, but still running the old bytes) does not satisfy it
Verified waits — when the producer itself is in doubt
A timeout wake says "go check the emitter". Two opt-ins on events:await make the wait do that
check itself:
- producer_health — the push event stays primary, but at the deadline the sweeper re-reads the producer's own authoritative state. If it is still advancing, the wait is re-parked; if not, the waiter wakes with a stalled or absent diagnosis instead of a bare timeout. Today it supports one producer, the green-checkpoint gate
- progress_lease — for a condition delegated to another owner. The waiter names an authoritative reading (a state cell, a tool-call delta, a checkpoint revision, a managed task's progress or a claim transition), its units, and the cadence it should move at. The first missed beat wakes that owner. After repeated misses (two by default, up to ten) the lease applies the remedy the waiter chose up front: hand back instructions to widen the spec or place the work outside the lane, open and claim an unblock item, or take the work-item over with a compare-and-swap on the expected holder. Waking the owner again is not one of the remedies; that already happened on the first miss
A third check needs no opt-in. When a wait on an announced key times out, the
sweeper classifies the gate's current declaration generation before it delivers the wake, and the wake carries
that classification and one exact next verb. The classifier is a pure function over evidence already on hand:
the generation's own fire latch, the declarer's and any live successor's verdict from the liveness oracle, a
producer-health certificate when one exists, catalog membership and fire history. It never judges liveness from
age alone, and a probe it could not run yields unknown, never a takeover. A generation that is no
longer current, or was superseded, cancelled, expired, or fired against its expected condition, gets an
events:status resync instead. A current one lands in one of seven classes:
- already-complete — this exact generation fired from a real emit, and its expected condition
matched or none was declared. Nothing else counts: a producer that only reports it finished stays
unknownuntil the key itself fires - progressing, expected-idle, unknown — the declarer or a live successor owns the gate,
and its producer is advancing within its cadence, deliberately paused, or can't be proven either way. The next
verb is the waiter's own re-await, pinned with
after_generation, so duplicate timeout wakes cannot stack re-arms - stalled-producer — the owner is live but its producer is stalled or absent. The owner is woken once; if a later sweep finds it still stalled, the takeover (claim the producer's work-item) is surfaced instead of a second wake
- missed-emitter — no live declarer or successor, but something can fire the key: it is a catalogued family, or it or its family has fired before. A steward work-item is filed that owns re-emitting or retiring the gate
- never-fireable — no live owner, no catalog family and no fire history. An advisory is recorded, with no wake and no takeover; the next verb is to cancel the declaration, then await a near-miss key if one is active or declare the gate again
The last three classes escalate, and each escalation runs once per announcement, generation and
classification. A condition item
keyed on that triple makes it idempotent, and the bridge re-reads the declaration just before acting, so a moved
generation stops it. A second timeout for the same triple adopts the existing item and wakes nobody. The bridge
never emits the key, marks a declaration fired, completes a work-item or ships a plan: a timeout stays a timeout
until the exact key fires. events:status on one key returns the same classification, read-only.
Reactions — when one thing settles, fire the next
Waiting is the agent-facing half. The other half is a declarative reaction layer over the tool dispatcher:
a rule says "when tool X settles, fire tool Y", so platform wiring is data rather than glue code scattered
through handlers. Built-in rules come from three sources that feed one registry — the Events file (a
single inspectable list of first-party rules), an emits: field on a tool's own contract
(work_items:complete emits a completion), and plugin/blueprint rules. Those are curated code;
agents add their own through a separate, test-gated path (below). Unless a rule says otherwise, it fires only
when its trigger succeeded.
- in-process by default, durable when switched on — a rule's mode defaults to
durable, but the durable path, which queues each reaction as a workflow that survives a restart, is opt-in behind a switch. Until it is on, every reaction fires in-process. If a durable enqueue ever fails, the engine fires the reaction in-process instead, so it is not dropped either way, and a rule declaredmode: 'sync'always fires in-process. On the durable path the fire step claims a dedup id in an idempotency ledger before dispatching, so a re-delivery is a no-op; if dispatch fails, the claim is released so a retry can re-fire - a failed fire is recorded, and the fixable ones reach the owner — on either route, a failure is written to the reaction ledger with its error and the contributor whose rule it was. The owner is alerted only when the cause is an install-time mistake they can fix: the wearing identity lacks the capability the rule needs, no provider in the pot binds the capability class it fires, or its target is not a registered tool. An ordinary runtime error from the tool itself is recorded but not alerted, and an alerted rule stays quiet for 60 minutes so a hot-looping rule cannot spam
- cascades are bounded — each cause chain is capped at depth 8 with cycle detection, and each third-party contributor at 500 fires per 60-minute window. The depth cap alone can't see N shallow rules from one contributor each firing at depth 1 — a steady, individually legal stream nobody is counting — which is what the budget is for. It is checked before a reaction is routed to the durable or the in-process fire path, because durable is the default mode and a budget wrapped around only the in-process path would leave the default route unbudgeted. First-party rules are exempt, and the budget fails open: it is a fairness control, while the depth cap handles the catastrophic case. A refusal is recorded apart from failures, so it never feeds the very fire count the budget reads. The count is a read of the reaction ledger in Postgres, per contributor within each workspace, not a counter in memory: an in-memory window would reset on every restart, which is just when a runaway contributor would get a fresh allowance
- agents can author local rules, tested before they run —
rules:createstores a rule in Postgres for one pot (the default) or the whole workspace, and every new or edited rule starts disabled.rules:testreplays it over the 50 most recent matching historical triggers by default (limitraises that to 200), plus up to 20 supplied fixtures, without dispatching anything. History is lossy, and the replay says so instead of guessing. A tool-triggered rule replays the invocation ledger, which keeps arguments (unless they were truncated) but no result bodies and no client. An event-triggered rule replays only the latest payload per event key, not every occurrence. A context rule on a turn-start, stop or compaction sink has no history at all, so only fixtures can test it. A sample whose condition or guard reads something the history did not keep comes backunknown, and the replay passes only when at least one sample was eligible and none is unknown, so a rule that reads result bodies needs fixtures that supply them.rules:enablesucceeds only when that replay passed for the rule's current revision, and any edit bumps the revision, clears the proof and disables the rule again. A local rule runs as its author, within the author's current wearer and pot ceilings. Anasyncrule fires a capability-class verb when its trigger matches. Asyncrule runs inline at one client hook sink: it either injects a read-only context provider's output there, or, at the pre-tool sink only, acts as a guard that can refuse a pending call but never approve or rewrite one. A guard must name explicit tools that excluderules:*, so a bad guard cannot block its own recovery, andrules:disableworks on any local rule in scope with no passing test or authorship required. The fire budget is keyed to the rule, not its revision, so an edit does not reset the count.rules:listshows local, built-in, plugin and Cupboard rules side by side with their source and enabled state; a Cupboard rule package stays inert until a blueprint wears it - "why did this fire?" is one call —
events:graphrenders every rule as anon → fireedge with its condition, mode and source;recent: trueadds the reactions that actually fired, each with its cause chain
Auditing the waits — a lost wake is a convictable bug
A wait that never fires looks exactly like a wait on something that hasn't happened yet. Two reads tell them apart:
- events:status — an agent's active awaits, standing subscriptions and recent wakes; or, for one
exact key, its declaration generations, the expected condition, every waiter registration, the wake outcomes
and a watermark/resync verdict. A declaration can state what the gate is supposed to prove — an
expected_sha, or anexpected_predicate(a rule condition the eventual emit payload should match); the two are mutually exclusive. A declaration can also carry an expiry of up to 30 days and lapse if it is never fired. A lapse is visible inevents:statusand is not a latch: nobody is told the gate opened, because it didn't - events:lost-wake-check — scans the await ledger for keys with enough settled awaits (default 5, over a 7-day window) whose real-fire rate is 10% or less. A low rate alone only makes a key a suspect: a rare event legitimately times out a lot. Conviction needs an independent witness that the condition really occurred while no wake went out. The first wired witness reads green-checkpoint verdicts from a separate append-only pipeline-event log, written by a different code path than the emit, and only counts a verdict that landed inside that waiter's own parked window (widened by 30 seconds of clock-skew allowance, which biases it toward convicting). Its denominator is only the awaits actually parked across a verdict instant; it skips awaits whose payload filter binds them to one specific candidate, which are supposed to sleep through another candidate's green, and awaits that were cancelled rather than missed. To convict, at least three awaits must have been parked across a verdict and at least half of them must have missed it. That bar was set between a real defect, in which every parked waiter missed, and a healthy key, on which about one in seventeen did. An "up-to-date" verdict is not evidence: when nothing changed, the gate records it without emitting the green event at all. The verdict instants must come from that separate log, never from the await ledger's own fired rows: in a true lost wake nothing fired, so a ledger-derived set would be empty and would quietly acquit the very defect being hunted. A witness that cannot run counts as no witness, never as a conviction
The blueprint system
Agent behavior is engineered the way code is: inherited, never copied.
- a fix to a base persona propagates — every descendant gets it, instead of forking into drift
- every rendered prompt is a projection — of one canonical source: edit the source and every surface follows; edit a projection and the next render erases you
- that property is the point — it's what keeps a fleet of role variants coherent
// a new blueprint is a validated CHILD of an existing one — never a copied file blueprint:catalog { } → find the parent to extend blueprint:extend { id: 'migration-pot', parent: 'coding', overrides: { knobs: { … }, roles: { … } } } // only what differs → { ok: true, errors: [], warnings: [], yaml: '…' } // the small child override file // the merged result is validated BEFORE you get the YAML; harness:create { blueprint } // then launches from it, and every session renders its layers in a fixed order
A blueprint is checked before it can run, and again when it loads. blueprint:extend
merges your overrides onto a parent, blueprint:create authors a blueprint with no parent, and
blueprint:validate checks either one, by id or as inline YAML. None of them writes a file or
creates a harness, and all three return errors and warnings instead of throwing, so an author can iterate.
The same validator runs again when the loader reads a blueprint, so a malformed one never reaches the durable
pipeline. Beyond the schema, it checks that every declared role is used and a terminal outcome is reachable,
that the turn budget is finite, that every reactive rule carries a condition, and that recursion, when enabled,
declares a trigger and a finite depth. Errors make a blueprint unrunnable; warnings are authoring smells that
don't block. Some rules are errors because a warning once shipped: a schedule on the in-process
ephemeral tier must name a deterministic action, never a whole blueprint run, and its interval can't
exceed 3,600 seconds. That tier's timer restarts from zero on every host boot, and one routine set to six
hours never fired once in 35.7 hours while reporting itself armed.
The kernel is sealed — and it renders last. A session's prompt is a stack of identity documents rendered in a fixed layer order: the client overlay, the domain profession, the fleet posture (leader or member), the definitions of whichever modes are on, practices and collaboration stance, then the per-workspace instance. Beneath a seal sits a domain-neutral kernel that renders last on purpose: a model weights the most recent instruction text most, so the sealed base is the last thing it reads, and a hostile layer can only ever sit above the seal, at lower precedence. Where a layer contradicts the kernel — skip registering the work, route around a lock, fabricate completion evidence — that layer's text is void. The seal is prose, though, and prose is not the defense. Identity text is never an input to authority: what a mode permits and which tools a session may call are computed from the mode registry and enforced as code at the dispatch seat, so no prompt layer can widen them.
Every layer fills a named slot, and the slots have rules. Most slots are exclusive: one profession
per agent, one fleet posture, one stance toward the owner, and one document per mode axis, which is why
auto and ideate compose but two autonomy documents can't. Practices are additive and
stack. The kernel and instance layers are reserved, and no document may claim them. A lint checks the composed
stack, not just one document, at install and again at render. It refuses outright, with no override, anything
it can recognise structurally: a claim on a reserved layer, two different documents on one exclusive slot, a
grant beyond the capability ceiling, an authority-bearing field on a mode document, a missing or failed
attestation, or a forged control marker such as the seal itself. Prose smells, like a phrase that reads as
“skip the work item”, only warn, because matching on wording is blind to paraphrase.
Modes are blueprints with a registry behind them
“Run autonomously”, “ideate”, “drain the backlog”: an agent isn't trusted to remember phrases like these.
Each official mode (auto, cold-auto, ideate, drain,
grade, goal, test, audit) is an entry in a mode registry
generated from validated mode manifests, which agent is in which mode is a row in a database table,
and its long-form operating rules are a blueprint of their own (su.mode-auto,
su.mode-ideate, …). The prompt a session starts with carries only a one-line index of the modes.
A mode's full contract is loaded when the mode is switched on, and it occupies context only while the mode
stays on. Every transition is written to a change log with who set it and why.
- one dial, many overlays — autonomy is the only exclusive axis:
autoandcold-autoare two positions of one dial, so setting one replaces the other, and the change log records which mode was displaced. Every other mode gets its own key, so they stack: an agent can ideate about why its drain is stuck. One pair is refused even across keys: an agent can't holdgoaltogether withgradeortest, because whoever owns a goal must not also judge it. The store refuses that combination itself, somode:set, launch and the admin screen all give the same answer - implications are applied by the write —
drainandgradeimplyauto, andgoalimpliesautoandideate. The samemode:setcall enters the implied modes, but only onto an axis that is still empty, so an agent already incold-autoisn't downgraded to plainauto. Leavinggoalby hand doesn't remove them, because the agent may have been in AUTO before it ever took on the goal. Each implied row records the mode and subject that wrote it, and a direct mode write erases that record. So when the goal itself stops, or passes to a new holder, only the rows that goal wrote are retired; an AUTO the agent chose on its own survives - some modes need a subject, some can be chosen at launch —
goalis meaningless without knowing which goal, so entering it must name one: work items an agent files inherit that goal, and the goal's stop finds its sessions by it.auto,drain,gradeandtestcan be requested before the session even exists, and the launcher registers them durably on the way up. Both lists are derived from flags on each mode's registry entry, never from a list of names, so the next mode that joins either set is covered automatically - scope survives compaction —
mode:setcan carry scope instructions such as “drain all bugs except the p2p ones”. They are delivered with the change and also stored as a standing fact on the target (up to 1,200 characters), which is re-injected word for word on every wake. Turning the mode off retracts it - the owner's word sticks — a peer agent can't override a mode the owner asked for. The attempt writes nothing and comes back as a conflict, downgraded to a request the target sees. A peer that does set a mode on another agent must give a reason, and the target gets the reason and the full contract by message. The change takes effect at the target's next turn boundary
- authority is code, not prose — what a mode permits is computed from the registry, never read from its text. AUDIT's rule, “read-only toward the subject”, is enforced at the tool-dispatch chokepoint, which runs before the superuser exemption. So while an agent is registered in AUDIT, every call that would change a work item or a plan (claiming, completing, setting state, editing a plan, changing its status or its Now line) is refused, even for a superuser session. Filing findings, by creating a work item or commenting, stays allowed. The gate applies to any active AUDIT, whether the owner asked for it or the agent started one itself when a lane stalled; nobody is watching the turn in the second case
One verdict on who decides, and what it overruled
The layers of a prompt can disagree. The generic playbook says to confirm a plan before executing it; an
active AUTO mode says to act and disclose afterwards. Leaving the model to settle that from wording would let
authority depend on whichever sentence it happened to weigh most, so the platform settles it in code. From
the session's registered modes, its route (on its own, placed by the pot, or in a fleet) and its scope, it
builds one structured record, a precedence trace, and appends it to the session's prompt at launch.
The same trace is rebuilt by coord:orient and when a turn's orientation is assembled. Each
decision in it names the value that applies, where that value came from, every competing value it
overruled, and why.
- act or confirm — if any registered mode sits on the autonomy axis, or implies one through the registry, the answer is “act with disclosure”, and the playbook's “confirm before execution” is listed as suppressed. Otherwise confirm-first stands. If the mode registry can't be read, the answer is “unknown”, flagged as a conflict, and never silently the confirm-first default: a failed read must not look like a known posture
- one mission, first match wins — the mission is chosen in a fixed order: an active drain (drive its scoped queue to a terminal state), then leading a fleet, then membership in one, then a bound plan, then work placed by the pot, and last the owner's current task. Every branch suppresses “pick from the generic backlog”. A fleet route also suppresses going solo, because a fleet role sticks until someone explicitly changes it
- propose or build — IDEATE together with AUTO allows inventing and implementing. IDEATE alone allows inventing and proposing, and keeps the execution gate. Without IDEATE, the session stays on the improvements it was directed to make. When the loop state is known, a fourth decision records whether an engine loop carries the work across turns
- a fingerprint, not a timestamp — the trace carries a 24-character watermark: a SHA-256 over the
sorted modes, the route, the scope, the loop state and every decision. The time it was observed is kept out
of the hash, so the same state always produces the same watermark, and a new one means something actually
changed. The trace states its own refresh rule: on a watermark mismatch, or a change of mode, route or
scope, fetch a fresh one with
coord:orientand replace the old one whole, never merging an older trace into a newer one - the compiled prompt is linted before launch — prompt text carries machine-readable rule markers, and the launcher checks the composed text for two values of one rule, a rule or directive repeated, and values that have been retired: “advisory” file locking was replaced by automatic hooks, and a “browser” app runtime by Tauri. It also recognises a few exact phrasings of a file-locking mode, because the contradiction that prompted it had plain prose on one side and a marker on the other, so a markers-only check had called it clean. Two findings refuse the launch outright: contradictory file-locking modes, and an unconditional order to re-run orientation after every compaction, a startup contract the platform has retired. Those match windows are kept small on purpose, so a legitimate condition elsewhere in a long prompt can't launder a nearby order
Blueprints that do work — declared operations
A blueprint can declare more than roles. It can also declare operations: typed, reusable tasks that
a deterministic app, an external binding or a plan schedule submits, getting back a durable handle. An
operation runs on ordinary platform machinery, either one work item or a run of a pinned plan template, so
it inherits the same claims, checkpoints and completion evidence as any other work. Seven operation
verbs — blueprint:submit, status, events, signal,
resume, cancel, result — cover the whole lifecycle, and one schema set feeds the MCP tool, the HTTP
route and the OpenAPI document, so the transports can't drift into separate contracts.
- submission is idempotent — the caller chooses a request key. The same key with the same input replays one durable outcome; a different input under that key is refused, never silently forked into a second run
- the handle pins the exact definition — workspace, harness, operation id, the 64-hex
specification revision it was admitted under, and its target. A plan target also pins the template's
revision and content hash, so a later edit can't change what an in-flight or replayed operation means.
A handle minted in another workspace is refused. A caller can also pass
expectedSpecificationRevision, the revision it showed its user: if the definition has changed since, a new submission is refused before admission, while a replay stays pinned to its original receipt - acceptance is not handling — an input on a declared channel (
blueprint:signal) or the answer to a declared wait (blueprint:resume, by its wait token) is schema-checked and durably accepted. The receipt sayshandled: false: handling is a separate worker fact - terminal is not success — status walks accepted → dispatched → waiting → terminal, and
the outcome is one of succeeded, unverified, failed, cancelled or dropped. Only a result in state
readycarries output: validated, and paired with an evidence reference - every write is keyed, not just submit —
cancel,signalandresumeeach take their own request key under the same replay rule. A cancel must state a reason, and its receipt says whether it took effect (effected). The handle is bound to the caller and scope that submitted it. Events page forward from a numeric cursor, up to 100 per page, so a reader picks up where it stopped. A result that is notreadyis eitherpending, carrying only the live status, or settled as failed, cancelled, dropped or unavailable, and then it carries a reason - a finished work item is not yet a result — when the operation runs as a work item, its output counts only if that item’s completion carries committed or validated authority. A close that merely proposes it is done never produces a ready result
- private input stays off the work item — an operation can declare a private input schema. The
submitter attaches that input to
blueprint:submitand it is stored outside the work item. Only the worker currently holding the item can read it, throughblueprint:read-private-input, and only while the item is not terminal. A read by anyone else is refused asprivate_input_not_holder, and one after the end asprivate_input_terminal. A retention sweep every 15 minutes deletes the input once its item is terminal or missing, and deletes any input seven days after it was stored, even while its item is still open; from then a read is refused asprivate_input_absent. The input is stored before the work item is accepted, so a worker never holds an item whose input is missing, and a replay never stores it again, so deleted content can’t come back. While the stored copy exists, different private input under the same request key is refused as a conflict. The input is capped at 4 MiB and fingerprinted with SHA-256 over its exact JSON text, so a submitter that hashes the value the same way can put that digest in the ordinary input and the worker can check the content against it - a business review is a second signature — an operation can also require a named reviewer
to sign off on its result. The reviewer’s identity comes from the transport’s authentication, never from the
input, and must be one of the principals the operation names, distinct from both the submitter and the
worker, including any identity nested under it: a reviewer
xcan’t review work submitted or done asx/…. The decision is accept, reject or rework, it must give a reason, and it is bound to a 64-hex fingerprint of the exact result. The reviewer sends it asblueprint:signalon the reservedbusiness-reviewchannel, quoting that fingerprint. Only a result validated as succeeded can be reviewed. If the result has changed since the reviewer loaded it, the review is refused, and so is reusing its request key for a different result. Until a decision lands,blueprint:resultreports the review as pending
# declared in the app-agent starter blueprint (papercusp-ops-pots template); {{…}} are its fill-ins operations: - id: "{{OPERATION_ID}}" target: { kind: plan, template: { kind: plan-template, ref: "{{PLAN_TEMPLATE_SLUG}}", revision: …, contentHash: … } } # pinned: a replay resolves the same definition inputSchema: { type: object, required: [ … ], additionalProperties: false } acceptance: { resultSchema: { … } } // the app submits it; the handle is the only thing it has to keep blueprint:submit { harness, operationId, requestKey: 'order-8812', input: { … } } → { handle: { receiptId, specificationRevision: '<64-hex>', target: { kind: 'plan', runId, instanceSlug } } } blueprint:status { handle } → { phase: 'waiting', outcome: null, wait: { name, token } } blueprint:resume { handle, token, response: { … }, requestKey } → { accepted: true, handled: false, wakeQueued: true, replayed: false, eventId } blueprint:result { handle } → { state: 'ready', output: { … }, evidenceRef } // failed · cancelled · dropped · unavailable carry a reason, never output. // blueprint:events pages the history by cursor; blueprint:cancel requires a reason.
Sharing goes through moderation. blueprint:publish sends a locally authored blueprint
to the Comb, the shared catalog other workspaces install from. It keeps its extends: line, so an
install keeps composing upstream improvements instead of freezing a copy. A publication lands pending
until an operator approves it, and because it is a workspace-global write, only an unscoped superuser may
publish. When a child names its parent, the parent resolves from the target project’s own local blueprints
first, then installed ones, then the built-ins. harness:create and pot:create
read the repo’s .papercusp/blueprints as that local tier when they are given a repo path; a bare
blueprint:extend has no project, so it starts at the installed tier. blueprint:catalog
has no project either: it lists only the installed and built-in tiers, and an installed blueprint shadows a
built-in one with the same id.
Context & memory
The window is an assembled artifact, not an accumulation — every slab of it has a store that feeds it and a rule that decides when.
Nothing depends on the agent remembering to ask. State reaches the window through two deliberately different disciplines:
- what an agent must see is pushed — deterministically, verbatim, with no query an agent could forget to issue — so it cannot be missed
- what an agent might need is pulled — by relevance, ranked and capped — so the window never fills with maybes
The split is the design:
Pulled recall: mid-turn, budgeted, and labelled when degraded
Recall used to arrive at one boundary only: when a prompt was submitted. A turn that started with one prompt and then made fifty tool calls got one injection and then nothing, so an agent could spend twenty searches re-deriving a fact that was already in memory. Recall now also fires after each batch of tool calls resolves:
- after the batch, not before it — once the tool results are back, an empty or failed search (the clearest sign an agent is about to re-derive something) can be taken into account rather than guessed at. A variant that fired before each call was built and then deleted
- a teaser, not a briefing — the mid-turn memory budget is an order of magnitude below the
turn-start one (350 characters against 4,000), because it fires per tool call: fifty calls at the
turn-start budget would inject around 200,000 characters. The 350 bounds the memory half only. Pointers
into the wider corpus (work-items, observations, past sessions) ride in a separate section with its own
1,800-character budget: at most six pointers, one line of at most 220 characters each, and a hit whose
matched fragment shares no term with the query is dropped as irrelevant. Those sizes were measured, not
chosen: at three pointers and 900 characters, the one recorded answer to a live question ranked fourth and
was dropped for budget on all seven replayed batches; six and 1,800 admitted it, and eight and 3,000
admitted nothing more. A strict query runs first and a relaxed one fills only the slots it left, because a
single graded query reordered the walk and swapped out 180 admitted lines across 79 of 120 measured turns
while the average count stayed flat. A recall that carries pointers comes to about 2,200 characters. What actually bounds the channel is a cap of 12 recalls per session per rolling
minute (about 26,000 characters a minute at worst), and a dedup ledger shared by every delivery point
means nothing already shown this epoch (at launch, at turn start, in an orientation, on a claim) is shown
again. When the budget runs out, the block says so and points to
memory:searchfor the rest - optional means skippable — several request workers serve the same port, and the per-session cap was first counted inside each worker separately, so in a fleet-wide burst every worker spent its own allowance until ordinary tool responses could no longer return promptly. A host-wide admission gate now sits in front of every optional recall, both mid-turn and when an agent's tool connection initializes: one recall in flight per worker, at most two workers across the host, and the gate never waits. A full gate is a clean skip (the block comes back empty and the tool call carries on), and a two-minute renewable lease frees the slot if a worker dies holding it. The holder renews it every 40 seconds, and the shared store gets 250 milliseconds to grant a slot: a slower store is treated as unhealthy and the recall is skipped, not queued. A deadline can answer before the work behind it finishes. When it does, the slot stays held until that work actually settles, so an abandoned query still counts against the cap
- a returned row is not a delivered row — between the store and the agent sit a relevance floor, near-duplicate collapse and an aggregate size budget, and any of them can discard a hit. Once the store returned five rows, the floor discarded all five, and every agent's orientation recall was silently empty while the search metrics looked fine. The recall canary now runs the same admission function the agents' path uses, so it cannot go green on a pipeline nobody runs
- degraded retrieval is labelled — an agent can't re-query a block prepended to its prompt, so the block has to say when it was not built at full strength. Three checks feed the label. Coverage: each semantic source is judged against the samples the embedding-coverage alarm already writes every 30 minutes (95% of all rows embedded, and 99% of recent ones). A missing sample, or one older than 90 minutes, counts as degraded, because absence of evidence is not health. The alarm runs at :21 and :51 past each hour, so 90 minutes tolerates two missed runs before the label turns. Legs: when one retrieval leg returns rows and the other silently contributes none, the block says which (“effectively lexical-only”). Both legs empty is a query with no matches, not a degradation, so the warning stays rare enough to read. Deadlines: a leg that runs out of time is named in the notice. A degraded block still renders, marked, because a partly covered index is still useful. The section is suppressed only when the vector leg ran, every semantic index is too sparse to be selective, and the lexical leg contributed nothing, with the row counts recorded so a silent section can be diagnosed afterwards. Every threshold is a row count or a coverage ratio, never a raw score, because the stored scores mix two incompatible scales
Compaction — outliving the context window
The layer that makes long-running autonomy real is compaction. When a session nears its context limit:
- the agent drives its own cut — it sets a soft limit for the task (lower to stay sharp, higher for wide-context work), steers to a clean stopping point as usage nears it, and requests the cut itself; a watchdog only nudges past the limit and forces a respawn past a hard ceiling
- it flushes state — to the surfaces above, as the work happens, not as a dying gasp
- then cuts to a fresh context — a carry-respawn: the client is relaunched on a carry document assembled deterministically from the database, with no LLM summary in the loop to drift, and re-orients from it: same identity, same claims, same work, new headroom
- work survives any number of windows — because the context was never the system of record; the database was
- even attribution is engineered — carried directives are provenance-tagged, so "the owner said X" always traces to a turn a human actually typed; an agent's note-to-self can't launder itself into an order through repeated summarization
The numbers behind the first bullet:
- the soft limit leaves room to overshoot — whatever limit an agent sets, 1.2 times that limit has to fit inside the model's window minus a 10,000-token margin, and no limit goes below 20,000. On a one-million-token window the default is 450,000 for a solo session or fleet leader and 250,000 for a fleet member. That is a spending choice, not a technical limit
- the watchdog sweeps every two minutes, outside the agent's turns. At the limit it logs a warning and lists the session's claims that haven't been checkpointed. At 130% of the limit it takes the turn and forces the cut, capped at the window minus the margin. Without the cap, 130% of a 158,000 limit on a 200,000 window would land past the window, after the session had already failed. If the window can't be resolved at all, the cap falls back to 1.2 times the limit, the overshoot the limit's own clamp already guarantees fits
- a forced cut that didn't land is retried — the cut is queued behind the current turn, so a session that keeps taking turns back to back can starve it. If a session is still over the force line six minutes after the force was delivered, the watchdog concludes it never ran and sends it again
- a respawn loop trips a breaker — three forced cuts within 30 minutes without a sustained drop below the limit stop the automatic cuts and raise an alarm naming which failure it is. Churn: the cuts landed, but each fresh session refilled past the limit within minutes, so the limit is set too low. Pinned: the estimate never fell by even 5,000 tokens, so the measurement is broken, not the session
- an estimate that contradicts the window is distrusted — the estimate is file bytes divided by four and can overshoot real tokens by 1.5–2×. Only a live session reading at least twice its resolved window is taken as proof the window was resolved wrong; a session really past its window would have failed, not kept working
- an impossible reading is never acted on — an estimate above 2,000,000 tokens, twice the largest window any shipped model has, is discarded before it can reach the gauge, a warning or a cut, and is re-sampled on the next sweep. Every such reading is dropped, not only the first: a number that cannot be real does not become real by repeating, and a session that really is near its limit reports a believable number again
- a session that goes blind is flagged, an idle one is not — when the estimate comes back empty for three sweeps in a row (about six minutes) and the session made a turn within the last five minutes, an alarm is raised. The activity test came from a measurement: about 146 owners a day were being flagged, a median of two to three hours into their first session, and 38 of the 54 still present had a readable estimate again. The empty reading was an idle stretch, and a session that takes no turns cannot overflow anything
- a frozen Codex reading is ignored — Codex usage is read from a rollout file, and an owner with more than one session row can have its estimate read from a dead predecessor's file that never moves again, which would hold a live session over its limit forever. So before an over-limit reading drives a cut, the watchdog checks two things: the file belongs to a different session, and it stopped changing more than 15 minutes before this session's latest turn. When both hold, the reading is cleared instead of acted on. The check runs only on over-limit readings, because one healthy session under its limit was measured meeting both conditions
The carry document has a fixed order, most important first: identity; session mode (AUTO, the armed loop); the standing carry, meaning open owner directives, the loop's carry-note, each held work item's checkpoint verbatim, never-drop facts and live waits; plan state; open questions to the owner; how the last turn ended; the most recent turns verbatim; and a pointer to the searchable transcript. It is capped at about a tenth of the window. When it doesn't fit, it sheds rather than summarizes: the oldest turns go first, then whole sections become one-line pointers that keep their heading and say where to re-read them. Nothing in it is paraphrased.
- it states what it can't know — it is a snapshot from the moment it was built, and a queued respawn can deliver it up to 30 minutes later. So it says outright that the old session may have kept working after the stamp, and that a finding it later retracted would still appear here, so every claim must be re-checked before acting on it
- recovery arrives with it — it carries a recovery marker stamped with a hash of the session's control state (modes, loop, scope). If the marker is complete and the hash still matches, the successor skips the orientation call entirely; otherwise it makes exactly one. A test pins this on Claude, Codex and OMP alike: zero recovery calls when delivery succeeds, one when it fails, never two
// the memory verbs behind the diagram — each store, one call memory:remember { kind: 'project', content: 'wrangler deploys need node ≥ 25 on this box' } // recalled by MEANING facts:assert { scope: 'workspace', key: 'gate-authority', body: 'plan v75 D-028 governs', ttlSec: 1209600 } // a lifetime is REQUIRED — 14 days, or kind: 'convention' // folded VERBATIM into every relevant orientation until it expires or is retracted work_items:checkpoint { id: 'WI-1204', checkpoint: 'backfill 60% — resume at batch 41' } // re-injected on the item's NEXT claim — yours or a successor's sessions:search { session: 'self', query: 'why did we skip table X?' } // the transcript survives compaction — retrieve, don't re-derive
Standing facts: conclusions with a declared lifetime
A standing fact is a scoped conclusion, folded verbatim into every relevant brief and orientation until it expires or is retracted. Because it is pushed rather than recalled, it carries rules that memories don't:
- no silent default lifetime — an ordinary fact must state how long it holds, including a verified conclusion. Every other lifetime is a named route, not a fallback: a measured snapshot (one that records when it was measured) lives at most 15 minutes; a provisional finding defaults to a day and a suspected one to six hours, so a hedge can't quietly harden into settled fact. A fact anchored to a file may omit its lifetime, because the anchor is what tells a reader it went stale; it is still stored with a 90-day bound. Only conventions and guard rails are permanent by default. Walls and dead ends default to 90 days, a review deadline rather than a clearance, though either may be declared permanent. A claim about current code goes stale, so it expires instead of lingering as truth
- a full scope refuses the writer instead of evicting a stranger — each scope holds up to 5,000 ordinary facts. A new key past that ceiling is refused, and the receipt says nothing was written and nothing was destroyed. Re-asserting a key that already exists replaces it rather than taking a new seat, so a full scope can still be corrected. The bound used to evict instead. At 50 seats, and again at 200, the busiest scopes sat full, so every write by any agent destroyed another author's fact, in one case about 20 minutes after it was written. Raising the number only bought time, so the bound became a refusal. It now sits about 14 times above measured demand, so hitting it points to one runaway writer. Snapshots are a separate partition of 125 per scope, and only that partition evicts, lowest confidence and least remaining life first, so a burst of snapshots can never push out a standing fact. Conventions and the three typed slots take no seat at all. A permanent ordinary fact is still refused, because lifetimes are this store's garbage collector: an expired fact is deleted 30 days later, and one that never expires would never be collected
- walls, dead ends and guard rails survive every cut — each carry document lists them by key, at workspace and owner scope, with an instruction to read the bodies before planning. A dead-end key records an approach already proven not to work, so a fresh session doesn't try it again
- a fact is asked how it could be re-checked — a probe (how to test it) and a falsifier (what result disproves it) fold verbatim beside the fact. A fact written without that pair gets a note on its receipt naming what is missing, unless it is declared undecidable — and an undecidable fact must itself name the probe or measurement that would settle it
Sessions end; the work carries
Sessions themselves are engineered to end, restart, and carry without losing the thread. A session that ends its turn is parked: it spends no tokens until something starts a new turn for it, such as a message sent with a wake, an event it is awaiting, its loop's next fire, a lock it queued for being granted, or the owner typing. An armed loop's wakes then carry in one of two ways:
- warm — each wake is a new turn in the same live context, so the session still has the whole
thread. Every fresh
loop:armdefaults to warm, interactive or headless alike; re-arming a running loop keeps the carry it already had - cold — once the loop has a carry-note, each wake resets the context to that note instead of
the transcript, and by default every eighth fire relaunches the process outright (a Codex session relaunches
on every cold wake). The context stays bounded however long the loop runs, but the note becomes the session's
only memory of the thread, so the cold wake prompt requires the agent to rewrite it with
loop:checkpointbefore each turn ends. A loop goes cold only when it was armed cold, or when its session class has passed the cold-boot drills that prove the carry sufficient. Even then it stays warm with no carry-note to rebuild from, or within 30 minutes of a human typing into the session
Either way, the survival inventory is the same — and none of it lives in the window. What's in it, and where the GUI surfaces each piece: §13.
The tool fabric
The principle: documentation lives at the call site.
- every verb carries its own guidance — and discovery is by intent, so nobody memorizes 900 names
- the invocation ledger keeps the system honest — audits replay what actually happened, and drift detectors reconcile what the docs claim against what the fleet actually calls
- the rails teach as they block — a refusal names the safe form, so the lesson lands at exactly the moment it's needed
Here's what a real tool looks like — this is (lightly trimmed) the actual definition of
locks:queue, the "who's holding what?" diagnostic:
export default defineTool({ name: 'locks:queue', // server:verb — the cross-client identity description: 'Read active locks and pending waiters. … This is a workspace-global diagnostic…', guidance: { // teaching, shipped WITH the verb when: 'Diagnostics — "what are the other agents doing?" or "why am I blocked?". Cheap, MVCC read.', notWhen: 'Before every acquire — locks:acquire already returns busy context.', seeAlso: ['locks:list (named-resource holders, not file-lock contention)', …] }, capability: 'locks:read', // what this verb may touch — gated per role args: z.object({ // a real schema, validated at the door paths: z.array(z.string()).optional(), owner: z.string().optional() }), async handler(args, ctx) { /* one read, one stable shape out */ } })
The properties that matter:
- name —
server:verb, the tool's real identity on every client; every doc and prompt cites it in exactly this form - description + guidance — the documentation lives ON the tool: when, notWhen, and seeAlso route an agent before it can pick the wrong verb — and a prompt-weight gate keeps this text from bloating every session
- capability — the permission this verb exercises; roles are granted capabilities, never raw verb lists
- args — a typed schema validated at the door, so a malformed call is refused with the safe form named — not half-executed
- handler — runs inside the operator with an identity-resolved context; every invocation lands in the ledger the audits replay
One call, from intent to answer
Follow one question — "who is holding this file?" — through the fabric. Each stage is there because a cheaper version of it failed in practice:
- finding the verb —
tools:findsearches the whole catalog by intent with two legs: a lexical leg that is authoritative on exact names, and a semantic leg over an embedding index built at boot, so "feature flag" findsflags:*with no words in common. If the embedder is unavailable the search drops to lexical-only, and the result'ssemanticfield says which leg actually ran - calling what isn't loaded — a trimmed session starts with a seed of tools, not the whole catalog of more than 900. That catalog is generated from the code's tool registry and checked in CI, so the number is measured rather than remembered.
tools:invokecalls any catalog verb by name through the same dispatcher a direct call uses, so it is gated exactly as that direct call would be - a wrong name gets a referral — an unknown tool comes back with the closest catalog matches
(
locks_acquire,locks:acquireandmcp__papercusp-su__locks_acquireall normalize to one key) plus the standing pointer totools:findandtools:invoke. The error is the one text every client shows at the moment of failure; a banner read 100k tokens earlier loses to it - a wrong argument gets a correction — a tool can declare
argRedirects: the fix for a predictable wrong key, shown only on the rejection that needs it, so it adds no prompt weight.locks:queueanswers a singularpathwith "rename it topaths" — a rename, never a drop, because dropping the key would silently widen a one-file question to every lock in the workspace - the answer is bounded — by default each result's text is capped at about 1,500 tokens (~6,000
characters), at one choke point in the MCP handler. The number comes from a per-hop budget: each model hop
gets min(15K, max(8K, window / 26)) tokens, split at the 8K floor into 4K for the model's own output, two
1.5K result slots and 1K for injected coordination. The result cap always uses the 8K floor, because the
handler cannot see the caller's context window, so a bigger window buys a longer session, not bigger
results; the knobs can be retuned per workspace or per session. The full text spills to a scratch file
behind a cursor that the agent pages with
capability:read, up to 4 KB a page, so nothing is lost. Results consumed by programs rather than models skip the cap, and so do delta replies, which the client reassembles from structured JSON that a cut would corrupt. An unexpected error while capping fails soft and returns the original result untouched. A failed spill write never does: the over-budget original is not handed back. The caller gets a boundedoutput_spill_failederror instead, or, for a write that has already committed (coord:sendorwork_items:complete, for example), a projection that keeps its receipt - a burst shares a second budget — calls in one burst also draw on a shared budget of two result
slots, about 12,000 characters. A burst is the output group the client names, normally its model turn; a
client that names none gets a best-effort stand-in, each call from the same agent that starts within 2
seconds of the previous one. Each result is charged at most one slot, so the first call of a burst always
fits, and a small result that spills anyway is told it was not too large on its own: earlier calls in the
burst used the budget. Reading a spill back is exempt, so recovery never spills again. To fan out wide, one
code:runscript returns a single compact result instead of many - the caller shapes it first — every tool accepts a dispatch-level
projection:pickkeeps up to 32 JSON paths, thenpiperuns up to 8 line stages (grep,head,tail,sort,uniq,cut,count). A named view goes one step further: a stable name for a versioned field selection plus a byte budget, with a receipt saying which version applied — a recovery capsule a cold successor can ask for without knowing the paths
// reduce at the source: only the fields the next step reads come back work_items:get { id: 'WI-1204', projection: { pick: ['results[].id', 'results[].workItem.state'] } }
Projection: the caller decides what comes back
A shell user trims every answer with a pipe (| grep x | head -20); a tool returns whatever its
author fixed in advance. Over a seven-day corpus of calls, the file-read tool returned 3.4 times more bytes at
the median than the shell commands it replaced. The gap was not tool overhead: the shell lets the caller choose
the projection on every call. So the platform handed that choice back to the caller once, at the dispatch
layer, instead of adding it tool by tool.
- two domains, in a fixed order —
pickselects fields from the parsed JSON body;piperuns line operators over the rendered text. Pick runs first, because it decides what text there is to filter. Line operators cannot shrink a JSON result, which arrives on a single line: aheadreturns all of it, and agrepthat misses returns nothing at all. The pointer on a spilled result says which of the two will actually reduce that body - it searches the whole result — the host strips
projectionfrom the arguments before the tool's own schema sees them. Unless the caller set a payload tier explicitly, it then dispatches the tool at its full tier, past the tool's own trimming, so a pick can reach fields the default answer would have cut. The result cap applies afterwards, to whatever the projection left. The promotion is invisible in the recorded arguments, so the call's ledger row records how the source was materialized: whether a projection or a named view drove it, how many pick paths it had, and which pipe operators ran. That record is what separates a pipe over one-line JSON, which reduced nothing and then hit the cap at full size, from an ordinary oversized call - a wrong root is repaired only when it is provable — the root a path must address differs from tool
to tool, and no schema announces it: a bare list is addressed as
[].field, an envelope asresults[].field. A path that matches nothing is tried against the real body in re-rooted spellings, and the rewrite is applied only when exactly one candidate provably matches. The answer then says which path was adjusted, so the right spelling is learned rather than silently absorbed. Two or more matches are reported as ambiguous, not guessed; without the repair, the selection would fall back to the full promoted body, which is larger than the unprojected default - a write keeps its outcome — on a write, the outcome fields the result actually carries
(
ok,error,counts, and each row'sokanderror) are kept even when the pick does not name them, so a field selection cannot hide whether the write succeeded - one regex dialect, and one outright refusal —
greppatterns are JavaScript regular expressions, checked before the tool runs, so an invalid one comes back asprojection_invalidwith the parser's own message. POSIX bracket classes are refused even though they compile: in JavaScript[[:digit:]]means one of the characters[ : d i g tfollowed by a literal]. It never matches a digit, but it does matchd], so it returns the wrong rows and leaves nothing in the output to show it
Spending tokens on purpose
Every byte a tool returns is paid for again on each later turn that still carries it. Three mechanisms keep that bill deliberate:
- a compact default — list results are served as TOON, which declares its field list once, right next to the rows: no repeated keys per row, and no legend to look up elsewhere
- positional only where it measured well — a headerless-CSV read format was tried and reverted. In a
measured trial on an audit-log read, a model shifted columns and named the wrong actor in one run of three,
because it had to map bare positions to a column legend held thousands of tokens away. TOON's self-description
costs about 20 tokens once per result. The positional form survives on writes, where it saves the most (output
tokens cost roughly 4–5× input):
work_items:claimaccepts a single CSVrow, with a keyed form as fallback - a weight budget on every verb — each tool's description plus guidance must fit 1,500 characters (hard cap 1,600). The check runs when the catalog registers, on every hot reload, so an over-budget edit shows up in the operator's log the moment it is saved, not hours later as a red gate that holds the whole fleet's deploys. It is tiered by who gets hurt: a budget overrun stays a log warning, because only the editing agent is affected yet; a hard-cap overrun is a guaranteed red gate, so it also files itself into the work queue, once per tool
- a second budget on the argument schema — the schema an agent's client loads for a tool is paid
for too, so each one is held to 8,000 bytes. The few already over it when the rule landed (21 of them,
including heavily used verbs such as
coord:sendandwork_items:complete) are pinned at their measured size and may only shrink. The ratchet is one-sided on purpose: the gate test fails a tool that grows past its ceiling, but a pin that now has slack is only reported, so the next editor lowers it. Failing that case would turn the gate red every time a tool got cheaper, which is the very thing the budget wants
Orchestration that remembers
Some jobs are many calls with logic between them: read ten items, filter them, claim the first that passes.
Done as ten round trips, that re-reads the whole context ten times. code:run takes a short
JavaScript script instead, calling tools as tools.ns.verb(args); only the script's returned
summary re-enters context, while the intermediate results stay in the runtime. dryRun:true
previews the write effects first: write calls are recorded as planned mutations and not executed, while reads
still run.
- a clean success becomes a recipe — a successful, non-dry-run script is saved, and the response
lists existing recipes that already do something similar, so the next agent runs one instead of writing it
again. Clean is strict: nothing is saved when the script referenced an unknown tool, when any write call
failed (a read that merely answered
ok:false, say nothing found, is tolerated), or when the script's authority cannot be proven - similar is not the same as allowed — before a recipe is offered, its script is analyzed for what
it would touch. A script that calls raw SQL, a shell or a nested
code:run, or that callstools:invokewith a computed tool name, can never be proven, so it is neither saved nor offered; run that one throughcode:rundirectly. A recipe pinned to a particular workspace, plan, item, lock, fleet or harness is offered only while your own live session covers it, and context you merely pass in does not count. Each offer carries a proof tied to a hash of the recipe's exact revision, whichrecipes:runchecks again, so a recipe edited after you found it is refused as stale - one hybrid engine finds them —
recipes:searchand that save-time check share it: BM25 over title and description (plain Postgres full-text ranking while the BM25 index is off or missing), fused with embedding similarity by reciprocal rank fusion with k = 60. The save-time check knows the new script's tool set, so it averages that fused score half and half with each recipe's tool-set overlap (Jaccard); a search query has no tool set and ranks on text alone. Otherwise recipes are global: the search ignores which workspace you are in, so only a recipe's own pins narrow who is offered it - shell commands get routed too — a pre-tool hook checks each shell command against a table of audited substitutions ("this read already has a tool"). A pair enters the table only when every command in a frozen sample of real ones from the box has a faithful expression in the tool's actual arguments. The prose table agents read, the seeded rows and the enforcement all generate from those same pairs, and a check fails the build on drift, so the advice and the gate cannot disagree
Reading code before writing it
A text search finds a string. It cannot tell a call from a comment, a re-export from a definition, or a shadowed name from the real one, and a confident empty result looks exactly like a search that missed. So the questions agents ask about code itself go to verbs that answer from parsers and compilers, each one built for a different kind of question:
- structure —
graph:query— a native call graph built from syntax snapshots that are hash-checked against the source (TypeScript, TSX and JavaScript, Rust, Python, Go):callersandcalleesfor one symbol,context→impact→tracefor “what would changing this break”, andcontractsfor who serves an HTTP route, RPC or event across repositories. Every answer states its coverage and freshness, and an empty caller list is reported as a lower bound, not as proof that nothing calls the symbol - type truth —
lsp:query— definitions, references, types, diagnostics and rename previews from pinned compilers (tsserver and rust-analyzer), run by a daemon outside the operator process. A language without a pinned compiler gets an explicit “unsupported”, never a guessed answer - renames that cannot half-land —
lsp:apply— takes the same cursor as a rename preview and writes the compiler's edits all-or-nothing: it locks the whole set of target files at once. If a peer edited a target since the preview, holds a lock on one, or the lock authority is down, it refuses with a typed reason and writes nothing. It measures diagnostics before and after, and for TypeScript it normally cannot prove every call site was found — it says so, rather than implying the rename was total - form —
code:structural— pinned ast-grep matches code by shape (foo($X)), ignoring formatting, strings and comments, and it still works on code that does not typecheck, because it parses rather than compiles. Its taint operation runs Semgrep OSS over named files with caller-declared sources, sinks and sanitizers, and labels the result local static evidence — never a runtime or vulnerability verdict - a radius in one read —
code:pack— packs a subsystem, or a diff together with a bounded report of its compiler consumers, tests, ownership and freshness. Packing comes after retrieval: the other verbs measure the radius first, then it is packed - pushed, not pulled — some of these answers arrive without being asked for. Six legs hook ordinary actions: a grep for one identifier, the first read or edit of a source file, an edit to an exported signature, a failing test run, declaring intent, and completing work. Each leg has its own flag, on by default, and all six draw on one shared budget of 1,500 characters per turn and 250 ms of wall clock. When the budget runs out the push is dropped, so it can thin a turn's context but never stall it
- GitNexus is not the engine — an external graph index once filled this role. Its query path is retired, because its answers came back empty or unranked, and the native engine is the default. Its context and impact operations survive only for optional internal comparisons, and it is not part of what ships
The self-improvement loop
It starts at the moment of friction: the rule is to file the observation mid-task, the instant a workaround happens, before the detail evaporates into transcript prose.
- filing is unconditional — the classic excuses are banned by name: "it was transient," "it recovered," "it didn't block me"
- an uncaught incident is two bugs — the incident, and the watchdog that should have caught it
- a mitigation is never a fix — a band-aid must name its durable fix and who owns it
// the flywheel's intake — filed mid-task, the moment the workaround happens improvements:capture { kind: 'bug', title: 'headless-browser console: daemon-origin errors pollute page verification', body: 'repro: goto example.com → console shows ERR_FILE_NOT_FOUND ×8 …' } // dedup matches are annotated on the new row — normal filing is never declined; // a tool-failure report starts in probation, and a second independent reporter promotes that SAME row; // kind:'bug' is the default auto-implement kind — unless it touches a protected path or keyword // (safety, deploy, migrations), which routes it to a human — and the whole auto lane is an owner // switch that ships off, so the queue feeds itself while fixes stay inside rails
Friction nobody files — the improvement watchdog
Agent discipline is the soft intake. The hard intake does not depend on anyone remembering: every
15 minutes a system routine, the improvement watchdog, sweeps the platform's own telemetry for things
that are observably wrong. A concrete defect, such as a failing test file, a drifted migration, an erroring
tool or a service that is down, is filed into the improvement queue (a bug by default) through the same
capture core an agent's improvements:capture uses, so dedup and topic tagging behave
identically. The watchdog only files. It never builds a fix.
- more than 40 kinds of signal — among them: a test that failed repeatedly and is still failing on its latest run; one tool erroring past a threshold; a watched service down; a database object no migration produces; an engine loop parked long past its fire; every routine overdue at once (the scheduler itself has died); a feature closed as done while its required assertions have no passing test
- defects are filed, measurements are not — about a third of the sources measure throughput or coordination rather than name a defect: a claim held but not progressing, an escalation nobody has answered, spawns timing out, and the stalled loop and dead scheduler above. Filing those as bugs sent code-writing workers at conditions no edit fixes, and one stalled-claim key was filed 60 times in a week. They are still collected and recorded on every tick, and that tick history reaches ideation as trends, but they never open a work item. A source nobody has classified yet counts as a defect, so a new signal is never silently dropped
- anti-flood by construction — three captures per tick by default, filled in two passes. First, highest rank first, no source may take more than two slots, so a storm of red tests cannot crowd out a dead service. Then any slot still free goes to the best leftover signal, so a quiet tick is never wasted. A signal deferred on the previous tick ranks one severity higher, so a persistent minor problem climbs until it lands. Just before filing, each planned red test is read again: a peer may have fixed it while the tick was planning, and a test that is no longer red is not filed
- tunable without a deploy — the caps and firing bars live in the routine's payload and change
through
improvements:set-watchdog-tunables: a dry-run preview, a check after the change is applied that reverts it automatically if it fails, an audit row, and a one-call revert. Unknown keys are rejected rather than ignored. One emergency environment override for the per-tick cap still beats the payload - two weekly miners read the refused calls — a call refused for bad arguments is logged with the
arguments that were sent, and the 15-minute tick deliberately leaves those rows alone. Two weekly routines
read them instead. Each files at most five items a run, always as a
changeagainst the tool, and neither pages anyone. The invalid-args miner groups the last 7 days of refusals by tool and offending key. A pair is filed once 3 different agents, or 10 calls in total, sent it, and the item becomes the evidence for adding an alias, renaming a confusing field or correcting guidance. The rejection scorecard grades each verb on its own: a verb with at least 100 calls in the week is filed when it rejected 10% or more on at least 3 separate days, counting only days with 20 or more calls. Splitting by verb is the point. On the day it was measured the fleet-wide rejection rate was 0.50%, while one verb rejected all 56 of its calls - open is not regressed — each signal carries a stable key. While a matching item is still open, the signal is not filed again. When every match has been resolved, the watchdog re-files only on evidence newer than the fix: older evidence is the lookback window replaying, and collectors that scan a tree which lags a deploy also wait out a cooldown after the fix. Newer evidence beyond that means the problem regressed, and it is re-filed
- a standing condition files once — a key that already fired on the previous tick is something the watchdog has already seen, so it is suppressed even when no item exists for it. Another sweep can retire an item between ticks, and that is how the 60-copy storm happened. The cost is that a key firing on every tick could stay hidden forever, so a key left unfiled through an unbroken 12 hours of tick history earns one re-file: at most two filings a day, and no condition hides for a whole day
- persistence escalates — an open item whose signal keeps firing gets more urgent instead of waiting quietly. After three days of continuous firing (24 hours for a down service or an open fire circuit), its severity rises one rank, once, with a comment and an escalation to the owner. If it is still firing a week later, it is escalated again without a second bump. Closing an item whose signal has stopped is the cautious half: that sweep is an owner switch that ships off, so by default the watchdog escalates but never closes
- schema drift, in four directions — a database and its migrations can disagree four ways, and each has its own signal: a migration the database never applied; a database migrated ahead of the code a still-running process loaded; a live database object that no migration produces (or the reverse); and an applied migration whose file was edited after it ran, so the ledger and the live objects agree but the file does not. The second is not academic: a migration that made a unique index partial caused every write from pre-migration processes to fail at plan time, with an error naming neither the table nor the migration, and took fact writes down fleet-wide for about 25 minutes until the matching code deployed. The check had existed only as a warning at operator boot — which a process already running through the gap never reaches
- planted faults test the loop itself — every 6 hours a drill called the Red Queen checks that this machinery still works end to end. It plants one known fault of a known class into a separate sandbox workspace. It then runs the real watchdog tick, the real triage path and the real resolve path against that fault, applies the class's known remedy, sweeps again to confirm the fault is gone, and always cleans up. It spends no model tokens. Five classes rotate, least recently drilled first: a failing smoke test, a stuck plan, an unresolved escalation, an open fire circuit and a dead routine engine. Each run records how long detection, triage and the fix took, which gives the Learning tab a time-to-self-heal, and it checks for leaks. The default read of the improvement queue must not return the drill, the same collectors swept over the live workspace must not see the planted rows, and the captured item must be marked as a drill. A class whose collector cannot be confined to the sandbox is not drilled yet. In the 14 days to 20 October 2026 it ran 57 drills with no failed leak check, and 51 resolved. Detection missed 5 of 11 open-circuit drills because the watchdog already counted a matching item as open. Triage matched the planted answer on every smoke-test, stuck-plan and escalation drill, but on none of the open-circuit and dead-engine ones: triage sent those to a gate where the drill expected the item to be placed for a fix
- bad runs are mined for what would have helped — every 6 hours a regret miner, behind its own arming flag and the learning governor, reads the last 14 days of finished agent runs and scores how bad each one looks. Burn in the top tenth of the run's role (cost, else output tokens, else transcript size; the top quarter counts for less), a failed or cancelled run, a timeout and a feature that bounced off its validator each add to the score. A run needs 0.35 to be kept, so no one reason is enough on its own, and at most 10 are kept per tick. For each new one the miner reads the saved transcript and finds the first turn where the run went wrong, from four signals: three failing tool calls in a row, the same call with identical input made three times, a turn whose output reaches three times the median of the turns before it, and a human or peer stepping in. Each signal maps to a few candidate rule changes, such as stopping a retry ladder or escalating on the third failure. A governed replay then cuts the run at that turn and continues it once per candidate, with the change added to the system prompt, and a judge scores each continuation against the original, within a spend cap the governor sets. A finding whose best candidate beats the original by a full point on the 10-point rubric is filed to the improvement queue, at most 3 per tick; detection alone never files. Measured on 20 October 2026, it had mined 119 findings since 12 June: 77 output spikes, 5 repeat loops, 2 error loops, 1 rescue, and 34 with no transcript or no signal to replay. Of the 85 marked replayed, 43 came back with scores and 10 cleared the filing bar, yet none had been filed. As the code stands, the filing step reads only the three oldest unfiled findings each tick, and those three have no scores, so it skips them every time and never reaches the ten behind them
- lessons must prove they transfer — every night a transfer harness reads the last 24 hours of transcripts, at most 12 per run, and asks a model for up to 3 lessons from each. Each distillation reserves $0.25 of the learning governor's budget up front. A lesson is deduplicated by a hash of its normalized text and admitted at once as probationary; admission is never gated. The same night up to 5 probationary lessons are tested: a governed replay has a fresh student re-attempt the lesson's source task from the original ask, with the same model, context and rubric, once with the lesson in its system prompt and once without, and a judge scores both on a 10-point scale. The lesson is promoted to validated only if it wins by more than half a point, since a tie earned nothing. A validated lesson that later fails drops back to probationary, and 3 failures with no pass retire it and remove it from memory. A failed or retired lesson also blocks the matching candidate from the fleet's shared knowledge pack. Measured on 20 October 2026, it held 61 lessons since 20 July, 26 from transcripts and 35 from pack candidates, all probationary and none ever tested. The test queue takes the 5 oldest untested lessons, those are pack candidates with no run to replay, and the tick skips them without marking them, so the transcript lessons behind them are never reached
- searches that find nothing become requests — every 6 hours, at 25 past, a SQL-only miner reads the last 30 days of docs, plans and memory searches that returned no results, from this workspace plus the box-wide buckets, and groups them by surface and normalized query into a demand map it rewrites in full each run. A query that has missed at least 3 times for at least 2 distinct callers, and has nothing filed yet, is filed as an improvement, at most 2 per run, with the counts in the body and a stable title so the search-first dedup can match it. A query that is a single identifier is first looked up directly: if a plan, a docs page or an active memory anchor exists under exactly that name, it is filed as a major bug, a resolution gap, because the thing exists and search cannot find it. A filed item's id carries across rewrites, so a query never files twice. The run sits behind a flag and the learning governor, which lets it through on a per-cycle budget of $0, since it spends nothing. Measured on 20 October 2026, it last ran at 10:25 UTC and the map held 16 docs queries, 50 plans queries and no memory queries: every one of the 12,058 memory searches in those 30 days that logged a result count returned at least one result. Since 14 June it has filed 34 items, 8 resolution gaps, all on 14 and 15 June, and 26 missing-knowledge requests, and all 11 filed since 15 August are still open
- new words are mined for missing names — every 6 hours, at 35 past, a second miner reads 30 days of prose coordination traffic (messages, handoffs and escalations) for terms the fleet has coined: hyphenated compounds and two-word content phrases, each counted once per message, after code spans, URLs, paths and ids are stripped and noise such as prefixed words like re-run, or pairs of two common words, is dropped. A term that any tool, table, routine or flag name already contains is not a gap. The rest must have at least 4 mentions in the last 7 days, from at least 3 distinct speakers, at a rate at least 3 times its rate in the earlier weeks, with the earlier count smoothed by one so a brand-new term scores high but finite. The agent-insights corpus only corroborates: it lifts a term's rank and is cited as evidence, but never enters the growth math. Up to 2 per run are filed as abstraction proposals in Scout's proposal shape, deduplicated by watchdog key rather than a ledger, so a term is declined while its item is open. A term that differs from an existing primitive's name in one word, by a plural or a one-letter edit, is filed instead as a bug, a vocabulary resolution gap. It sits behind its own flag and the learning governor on a $0 per-cycle budget. Measured on 20 October 2026, the routine last fired at 10:35 UTC; the miner had filed 32 abstraction proposals since 14 June, 14 of them in the last 30 days, and 21 have not been closed, while no item carries the near-miss title. Those 21 include coinages such as “cold wake”, “bug drain” and “result door” beside everyday phrases such as “root cause”, “files changed” and “remains open”
Failed tool calls file themselves
Every tool call already lands as a row in the invocation ledger. When a call made by an attributed agent ends in an error, a refusal of its input, or a timeout, that same row is marked pending for capture. Right after the row commits, a fast path files it as an improvement. The watchdog's next tick picks up anything the fast path missed. Each row is locked while it is delivered, so the two paths never deliver it at the same time.
- what never files — failures of the improvement tools themselves, so the intake cannot feed on its own errors, and any refusal that already handed the caller a ready-to-send corrected call. That call is still recorded, so repeated misuse of a tool stays visible to the error-rate signal
- four classes, first match wins — structural (the default: a real bug or a wiring gap), transient (timeouts, exhausted connections), caller (bad input, a missing role) and rate-limit (an external provider's quota). Caller and rate-limit failures file as a change, not a bug, so they stay out of the auto-fix lane. One ordered rule table drives both the code and the SQL classifier
- one row per problem, not per build — a structural failure is keyed on its tool, class, error code and field path, never on the message wording, so a defect whose message carries an id or a value stays one row and its count climbs. Transient, rate-limit and caller failures are keyed on the tool alone: a tool that keeps rejecting input gets a single ease-of-use item that names the arguments agents get wrong most, each with a count. An agent's own report uses the same key, so it lands on the same row. Keyed per build, one live defect used to mint a fresh row on every build, about 27 a day
- probation until corroborated — a new report starts as a low-severity observation. More reports from the same agent, or from another build, only add to the row's count. A matching report from a different agent promotes that same row into real work
- born ready when the evidence is direct — a promoted row is ready to implement at once when the corroborating report comes from the platform itself, carries direct evidence, or carries the failing call's own ledger row as its reproduction receipt. Anything else waits in the single agent-review stage every capture passes through
- a 24-hour replay horizon — a failure still pending capture after 24 hours expires instead of being delivered. That matches the error-rate signal's own 24-hour evidence window, so a late replay cannot resurrect a signature that reviewers already dropped
From observations to patterns — the corpus digest
Observations are pre-ideas: an agent's turn-end reflection or a rubric scorecard, filed into a separate lane. By design, an observation row never enters the work queue, triage, or the auto-fix lane, and open is its normal state: the lane is a time series, so an open row does not mean unfinished work. People browse it in an Observations pane that has no claim or dispatch button. Its main machine reader is the corpus digest, a "state of the hive" rollup that the ideation passes read instead of per-turn context.
- deterministic, not a model call — the digest is a pure, regenerable rollup over the change feed, completions and reverts. Friction is clustered lexically, by an exact signature match and then a token-set Jaccard of at least 0.8; an opt-in embedding pass reuses vectors already stored on the rows instead of calling a model. That keeps it cheap and fully testable. The creative leap happens downstream, in the ideators
- meta-patterns, each with a drill-down ref — recurring friction, time and token sinks, chronic deferrals, capability gaps, undone work (status regressions, churniest first), and rubric ratings grouped by rubric, hive and criterion. The Scout cycle adds health lanes on top: watchdog, CI tests, gate and pipeline, coordination, tool telemetry, PTY and workspace hosts, health flaps, browser crashes, spend anomalies, owner corrections, plan health and recent-commit churn. Ideation therefore works from measurements as well as from free text
- new before standing — each cycle's digest is snapshotted, and the next one leads with the patterns the previous snapshot did not have. When consecutive cycles burn a large share of their ideas as duplicates, the digest drops the standing patterns in favor of fresh signal and opens with a saturation banner
- reading is recorded, not closing — each observation the digest has read gets a consumption stamp in its payload. Its status and timestamp are left alone, so unread signal is a field you can count, and the time series stays intact
- two ways to become work — Scout clusters a recurring observation into a pattern and routes an idea grounded on it, or a reviewed intake decision promotes one directly. Promotion mints a separate work item and keeps the observation as evidence, and the decision is tied to the revision that was reviewed, so an observation edited afterwards is refused
- rubric gaps — a friction cluster filed by at least three distinct authors, on at least two separate days, with no active rubric covering it, is flagged as missing a shared standard. A rubric plan is drafted from the gap, under one stable name per theme, so the next cycle finds that draft instead of filing another. The threshold is loose on purpose: it over-proposes, and ratification does the filtering
The auto-implement lane, and what it may not touch
The last hop, from a filed bug to a shipped fix with no human in between, is a risk-tier policy plus an owner switch. The policy is pure logic that answers one question per item: safe to implement without approval, or wait for a human? The switch ships off. It is an owner-authority flag, and until the owner arms it, capture and triage run as normal while the implement routine does nothing. Items stay in the queue for an agent or a person to pick up.
- bug is the only auto kind by default — the policy widens from bug to change to feature only as each tier earns trust. Widening is a config change, not a code change, and an empty list pauses the lane
- one ordered verdict chain, and some items are human before their kind is read — an item already routed to a human, one that triage marked gate or reject, a finding from the replication-liveness detector, and a row authored on another node (only its author can close it) all go to a human first. Then come the kind gate, infrastructure, protected paths and protected keywords, in that order. An item is auto only if it clears every step
- a protected floor operators can only raise — paths that force a human even for a bug: the release gate, the scheduler and workflow engine, git-sync, the lock authority, schema migrations, the flag system, the improvement loop's own code, the learning governor, the change ledger, auth and credentials, the deterministic executor's trusted base, and the capability-dispatch core. While an item has not yet declared which files it touches, keywords in its title or body (migration, deploy, security, credential, feature flag and others) stand in for the paths; once it declares paths and none is protected, a passing mention of a keyword no longer forces a human. Keywords match as words with a few inflections, so deploy still catches deployment but auth no longer catches author. That matters because a keyword hit hides an item from every self-select queue, and the earlier substring match had hidden 96 items whose only hit was a word like author or authority. Operators can add to either list, and there is no remove operation. Only the owner's full-autonomy grant lifts the floor: it removes the path and keyword bars together, reversibly, so the loop may edit and ship even its own gates
- infrastructure is never auto, under any grant — the classifier keys off the signal that filed the item and the paths it names. A down service, an open fire circuit, migration or schema drift in either direction, a stalled loop or stalled feature, an external provider's rate limit, a silent federation peer, or a fix that lives in a Dockerfile always goes to a human, full autonomy included. A code-fix worker cannot repair the environment, so dispatching one would only burn its attempts: before this gate existed, one stalled-feature item used four straight dispatches rediscovering the same cause
- auto means no approval, not no gates — an auto item runs the implement blueprint: one worker that reproduces the bug, fixes it with a regression test and runs the affected tests. It can close the loop as fixed only by reporting the tests it ran (the resolve is refused without them), and it must route itself to a human instead if the right fix reaches a protected surface or grows beyond a small inline change. The change then lands through the release gate like any other
- dispatch is throttled, not fire-and-forget — the implement routine runs every 30 minutes and dispatches one item per run by default. An item claimed within the last two hours counts as in flight, as does any item touched in the last 15 minutes, so two consumers of the same backlog do not race. After three dispatch attempts an item becomes human-only, so a fix that keeps failing is never re-dispatched forever. That schedule describes the routine when it is on. On the deployment measured on 20 October 2026, the implement routine itself had been inactive since 29 September, so it was dispatching nothing at all
- environment deaths are not charged to the item — a worker that dies on a rate limit or a
credential failure does not use up an attempt. If the death reported an exact quota reset time, the item
waits for that time. If two such deaths land within 90 minutes, the whole lane pauses and resumes by itself
once they age out. The watchdog reads the same deaths over a wider six-hour window, so an outage does not
file bugs claiming the lane is broken or its dispatcher has stalled while it lasts. Both ceilings, per run and per item, are
tunable through
improvements:set-auto-policy - a daily digest drains the human lane — items routed to a human wait in a review queue, and the
improvement-human-digestroutine keeps that queue from growing unread. Each run sends the owner the top five open items, with the reasons each ranks where it does, to the coordination inbox and as a desktop and mobile notification. An empty queue sends no digest. The order is blocking impact, not severity or age alone. An explicitblocksedge to other work adds 12 points, a watchdog signal behind the item 5, another open capture of the same friction 4 and an inbound reference 3, and severity adds up to 10 more. That base is multiplied by 1 + log10(1 + age in days), with age capped at 60, so an unanswered high-impact item climbs while a fresh one does not jump the queue. An idea that carries a complete, cheap, falsifiable experiment gets a small bonus, because it is the cheapest to settle. Theimprovements:digesttool, the Learning tab and this routine all call the same ranker, so they cannot list the queue in different orders. The same run also reminds the owner about work items parked in needs-human for 7 days or more. Each reminder names the question, who asked it and what happens if nobody answers. It is sent once per parked episode, and it is dropped when the conversation it points at has been resolved and no other ask is open. The routine ranks and notifies but changes no work state. Its seed script still describes a weekly Monday run, but this pot's row fires it every day
Platform mode: which self-improvement loops an install runs
The loops above fall into two classes. The per-hive loops, the gym and Scout, run inside each hive and work on that hive's own project. The platform-improvement loops work on Papercusp itself. They are the workspace-singleton measurement and frontier loops (calibration, the change ledger, deferral interest, the fleet EKG, graduation, the memory recall canary and precision bench, and others) plus the improvement watchdog, triage and implement routines. Whether an install registers that second class at all is a deployment choice, made by one flag.
- the gate covers registration only — with the flag off, a platform loop's routine row is never written, and the improvement routines are seeded inactive. No loop's logic changes, and each frontier loop keeps its own arming gate on top. Scout's blueprint also declares a workspace singleton, but Scout is exempted by name, so this flag never gates it or the gym
- on by default; the release switch is designed but not yet wired into a build — the flag
defaults on, because development and self-hosted installs run the full loop, and this workspace is
Papercusp improving itself. The design is that a public release build exports
PAPERCUSP_PLATFORM_MODE=off(0andfalsealso work), and a per-flag override still wins over it. Measured on 20 October 2026, no build or packaging script in the repository sets that variable, and the code records a real release-build constant as an open owner decision. Until one lands, a build registers the loops unless the variable or the flag is set off - opting in is one call —
platform:enableis root-only, needsconfirm:trueand a written reason, and records the opt-in in the flag audit log. It runs three steps in order and reports each one, so a partial enable is visible. First it switches the flag on for this install. Next it stands Papercusp's own public repository up as a shared hive, cloned at that moment rather than bundled with the app, and that hive gets its own gym and Scout. Last it writes the platform loops, each seeded on or dark as its blueprint declares; a re-run keeps an armed loop's state. A self-hive that already stands is reported as standing, not as a failure, so the call is safe to re-run - opting in widens no guard — the protected paths, the never-auto rules and the autonomy ceilings still apply, and the blast radius is this install. Anything bound upstream still goes through review
The flywheel's proof is its own queue: the PaperCusp pot inside PaperCusp carries more than a hundred thousand work items of the system improving itself — plus more than a quarter-million observations feeding them.
The release pipeline
- no agent commits — a background sweep commits the whole tree; main's history is by construction a chain of verified states, and a bad deploy rolls itself back
- a red gate is the whole fleet's highest-priority work — it freezes everyone's deploys, so "not my lane" is not a valid answer
- a flake is quarantined only with a filed de-quarantine follow-up — nothing rots silently
- two operators, two truths — a staging operator serves the latest committed tree for
verification while the production operator serves only green
main. The sweep is scheduled every three minutes but commits only when a tick finds work and its lock free, so commits land minutes to tens of minutes apart; the gate judges roughly hourly, and a green pin reaches production within about fifteen minutes; a pre-push hook makes a hand-push tomainimpossible
Why an unattended sweep is safe on a shared tree
Committing everyone's in-progress work on a timer sounds reckless. It is safe because being dirty is never enough to get a path committed: every tick runs a fixed chain of exclusions first. An excluded path stays dirty on disk, and the rest of the tick still lands.
- one sweep per repository at a time — each run holds an exclusive per-harness lock, inside a shared workspace restart barrier, across the whole commit → submodule push → pointer bump → push sequence. If a peer holds it, this tick is skipped rather than racing the index; the next tick retries. If the lock service itself is unreachable, the sweep proceeds unlocked as an explicit degraded mode and tries the lock again next tick
- a file someone is still editing is not committed — dirty paths under a live file lock are left out. One edit often spans several files (source and test, migration and code), so the exclusion extends to the rest of that work-item's edit cohort: an already-unlocked sibling cannot reach the branch alone. A lock taken after staging is caught too, because a last check before each commit unstages any staged path a live lock now covers
- a file that would break the build is quarantined, not committed — before the
git add -A, registered content detectors check each dirty file; an offender is left out of the commit and the detector's fixer agent is dispatched. Size is capped twice: a single file over 95 MB is excluded, and a dirty set past a 250 MB cumulative cap is peeled until it fits (2,261 MB of an extracted app image once arrived as thousands of files, none over 95 MB). The peel takes files of about 1 MB or more that are not declared generated output first, largest first; then regenerable output such as the docs mirror; and small hand-written source last. Peeled files stay dirty for a later sweep, and the cap sits under the publish step's 500 MB limit, so this cheap exclusion fires before a push would have to be refused. The detectors fail open: one that throws lets the file through, so a bug in a check can never stall the fleet's commits - a quarantine takes its importers with it — a clean dirty file that imports a quarantined one is held back too, followed to a fixed point so a chain of imports goes together. Without it the importer commits alone, and every isolated checkout fails on the missing module. That happened once and held the release gate red for about six hours
- a deletion is checked against its importers — a sibling guard refuses to sweep the deletion of a file that a surviving file still imports. It exists because an unchecked sweep of about 34 such deletions once crash-looped the operator for the whole fleet
- a wipe is not committed as work — the importer check is blind to a total wipe, because nothing
survives to import anything. A separate guard therefore judges the deletion set itself: in a repository
of five or more tracked files, deleting at least 80% of them, or deleting its root
package.jsonor.gitignore, is treated as an accident. The files stay committed in git, the working tree stays as it is for someone to restore, and the wipe escalates. It was added after a sweep pushed four emptied submodules as zero-file trees - a merge conflict leaves a clean tree and one resolver — the sweep aborts the merge, records a single escalation for that harness, and spawns one merge-resolver agent for it; the next clean sync clears the escalation. A merge that fails for any other reason is reported as an error and gets no resolver. Neither side of a conflict is silently overwritten
What makes the gate livable at fleet scale
Ten subtleties, all learned the hard way:
- fixing the code does not re-color the gate — the verdict is periodic, so landing a fix and shipping it are three distinct hops — commit, re-judge, deploy — each with its own on-demand lever. Conflating them is how an agent fixes every red test and then stalls, staring at a gate that stays red until the verdict re-runs
- "the gate is red" ≠ "the gate is red about my change" — whether the judged candidate contains a given change is a content question: a blob-containment check against the candidate's tree, exposed as a one-call read — never a timestamp inference, which on an auto-committing tree misattributes routinely. A stale red can even void itself: the gate re-fires onto a newer tip that may already carry the fix, a rescue an impatient manual re-run would discard
- a real red freezes the candidate — instead of chasing a moving tip, the gate opens one repair queue on the exact commit that failed. A fix is written on the shared tree as usual, then admitted onto the frozen candidate by path, and the queue re-tests that repaired head. The candidate converges to green rather than being re-cut while new work keeps arriving
- a red the host caused is not a red about the code — when a run's failures trace to a host fault (the measured case: the gate's own module cache was deleted mid-suite), the verdict is inconclusive, not red, and the whole red apparatus is skipped: re-triage, auto-refire, prefix salvage and fixer dispatch all act on failing files that run cannot vouch for. A run refused for disk headroom is likewise recorded as a non-verdict outcome rather than a red
- a load flake is absorbed, and counted — a file that fails under the checkpoint's 8-fork load but passes in an isolated re-run does not hold the gate. It is surfaced for a ledger that tallies chronically flaky files, so one can be quarantined accountably instead of silently tolerated
- every failing test blocks, unless a written downgrade covers it — a red takes a fixed ladder: fix it, else eject the change it is pinned on, and only then downgrade the one test to non-blocking. A downgrade is a record, refused unless it carries a written reason, an owner and a filed bug. A test in one of seven critical areas (the gate itself, migrations and schema, auth, permissions and secrets, data integrity, typecheck and build of shipped code, the agent control plane, deploy, rollback and release) also needs an acknowledgement naming the area and its risk, and its owner is told at once, not only in the daily digest. Blocking is derived from the live records on every run, so revoking a downgrade makes the test block again, and a red candidate promotes only when every failure it located is covered. The rule fails closed: a failure whose file cannot be resolved to a repository path, a crashed workspace with no per-file list, a case-level downgrade over a file whose failing cases were not named, a critical-area record without its acknowledgement, and a failed read of the records all keep the red
- a planner that would change what reaches main runs in shadow first — two
planners are built to shorten the path to
main. Ejection removes the one commit a new failure is pinned on, so the rest of the batch promotes. It pins a failure only when exactly one commit in the candidate has a radius that selects the failing test, and it confirms the blame by re-running the test on the candidate without that commit: a fail there means the test was already broken, so the commit stays. The composed promotion proof would replace the full-suite re-run that follows a green repair step. It re-runs only the tests in the radius of a file whose compiled JavaScript changed and carries every other first-round result, so a type-only edit, whose compiled output is byte-identical, adds no re-runs. A wrong ejection drops a good commit and a wrong composition promotes a red, so both fail closed: output that cannot be compiled counts as changed, a re-run test with no result counts as failing, and a radius that cannot be computed attributes nothing and keeps the full suite. Both are computed today as a planning pass only, recorded beside the real verdict: nothing is ejected automatically yet, and the verdict stands. In shadow, ejection attributes by radius alone, because the blame check needs a build without the commit. Each planner can act only through its own switch, after at least five recorded shadow runs with no disagreement - a test that flips on one commit is quarantined, with an owner — a test measured both passing and failing on the same commit says nothing about the code, yet it would make a frozen candidate's failing count rise and fall with host weather. The gate moves it to an automatic quarantine: the same record as a downgrade, whose filed bug is the de-quarantine work item. Without an owner and that item the quarantine is refused and the test keeps counting. A flipping test in a critical area is quarantined too, and its owner notified at once. One measurement found 31 such results among 210,286: rare, but each one costs a whole repair round when it counts
- one red does not hold every commit before it — the gate locates the longest prefix of the
judged range whose cumulative diff does not implicate the failure, confirms that prefix with a run of
its own, and only then moves
mainto it. It locates the boundary by bisection: on one 11,544-commit range a linear scan held the gate's run lock for over an hour after the verdict was already known, where bisection needs 14 probes - reds are caught per sync batch, not per verdict — between verdicts, a durable routine fires every five minutes, takes the commits git-sync has landed since its stored cursor, and runs that batch's affected tests and a typecheck over its changed files. A suspected test red is re-run one file at a time before anything is filed, and a file whose result can't be read is undetermined, never red. A confirmed red is filed under a condition key, so the next sighting finds the open item instead of filing a duplicate. The gate's repo-wide lint guards get the same check on their own five-minute routine, but each guard is judged against its own last run, not against green: a guard that is already red can still flag a new offender, a path it names now and did not name last time. A guard's first run is a baseline that files nothing, and a new red goes to its author, found from the edit ledger first, then the commit trailer
The frozen repair queue — one door, hunk-exact
// the candidate that went red is IMMUTABLE; only repairHead moves. Each phase decides // which sha the next gate run materializes: ready-to-test → re-test the frozen candidate itself awaiting-fixer → dispatch a fixer, wait for it, or recover a dead one — no suite run, so the red recorded on the candidate stands ready-to-verify → run the suite on repairHead (the candidate plus admitted fixes) ready-to-promote → promote repairHead blocked → a hold that re-decides to itself every tick; escalated once, on entry. Two reasons only: fixer attempts exhausted, or no fixer can be dispatched. An older 8-hour wall-clock hold was deleted; a row still carrying it is read back as ready-to-verify // the only door a fix takes onto that lineage — a dry run unless confirm:true: release:repair-queue { op: 'admit', paths: ['…/the-fixed-file.ts'] }
- exactly the named paths, nothing else —
admitlands the named paths onto repairHead as one commit, proved by its diff touching those paths and no others. Nothing fast-forwards, merges or catches up to the tip. An older “converge to tip” operation was removed; its name survives only as an alias foradmit, so a stale runbook still reaches the right door - hunk-exact, because files are shared — on an auto-committing shared tree, the integration branch’s copy of a file can carry a stranger’s concurrent edit. While a candidate is frozen, an edit hook ledgers every hunk an agent writes, attributed to its session, and admission rebuilds each path as repairHead’s content plus only the caller’s own hunks, replayed in order. Refusals name an exit instead of widening silently: no ledgered hunk for a path, a hunk whose anchor is gone (naming the other agents who touched the file), an oversize hunk, or a path that is not a plain file. The exits are explicit — include a named peer’s hunks, or admit the whole file with a stated reason, which the ledger records along with every foreign hunk it carries
- three checks before anything publishes — completeness: every relative import in an
admitted TS/JS file is resolved against the admission commit’s tree the way TypeScript would
(extension probe,
.js→.ts,/index) in one batched git lookup, and anything missing comes back as a path to admit too — never added on the caller’s behalf. Lockfile consistency: a lock’s workspace entry must match its manifest’s dependency maps, because once a lock hunk landed without itspackage.jsonhunk, the gate stayed green (the test runner never asks npm) and every later certification build failed; only an inconsistency the admission itself introduces refuses. Fix pre-check: the frozen failure’s test files plus any admitted tests run at the built, still-unpublished commit, refusing an admission that would import a new red — or, when the failure consists only of test files, one that fixes none of them. A red counts as new when the test passed at the previous repair head or did not exist there, so an admission cannot slip in a new failing test; a red the lineage already carried is never blamed on the admission. An unmeasured file never counts as passing - the preview is the commit — admission commits carry a fixed synthetic date, so the dry run and the confirmed write produce the identical sha. Downstream readers recognize that date as a sentinel: one that read it as real once judged a minutes-old candidate to be 26 years old and suppressed a genuine red as unreliable
- an admission schedules its own verify — every persisted admit queues one durable verify of the head it published, after a five-minute quiet window. A later admit moves repairHead, so the earlier waiter wakes to a different head and stands down: only the newest head’s waiter launches, it defers to a run already holding the gate’s lock, and no head is launched twice. Its guards are re-read when the wait ends, never trusted from when it began, and the wait is a durable workflow, so an operator restart cannot drop it. Before this, a queue sat at ready-to-verify until someone fired a run by hand
- each failing leg is its own lane — the queue carries a manifest with one row per failing
leg: the paths that leg is about, whether a fix for them is already on the lineage (green at a later
head, admitted and awaiting re-verification, or still red at the candidate), and the exact
admitcommand that would land one. It is one pure derivation rebuilt on every persisted queue change, so its copies on the gate-red work item, in the pipeline-position read and in the queue read cannot disagree. A leg whose paths are unknown gets no command, because a fabricated one is worse than none. Each leg also opens as a separate claimable work item, and claiming a leg puts its paths in the claimer’s presence so peers can see the lane - an idle queue gets an actor, not an alarm — when no repair round has started for an hour while legs are failing, the queue climbs a ladder: dispatch the fixer if one is awaited; otherwise wake the current gate holder directly, naming the next action for the phase; otherwise raise an urgent item on the owner’s attention surface. A rung that does not apply, or fails, falls straight through to the next; a rung that acts gets 30 minutes to start a round. Each rung fires at most once per idle episode: a new admission or a started round opens a fresh episode, and nothing fires while a checkpoint suite holds the run lock. It replaced a fleet-wide broadcast on every 8-hour age rung: a 65-hour alarm once drew at least four agents into each spending a turn re-reading gate ownership, while the gate already had a live owner
- a queue that stops converging is retired, and its fixes carried — holding one candidate is
right while repair converges and wrong when it doesn’t: one queue ran 24 rounds over 161 hours, its
failing count going from 3 to 28, holding
mainthe whole time. So a queue is retired automatically when its failing count makes no new low in four consecutive rounds, or it is more than 24 hours old. Rounds are compared like with like: a round that stopped before the test sweep, or whose failure count hit the stored cap, neither sets a new low nor counts as a round without one, and an entry any round’s gate left out of its count (a quarantined flaky test, an active downgrade) is removed from every round, so the rounds compared all count one population; once anything is left out, a round whose failing list was not kept is set aside. At most one automatic retirement per install per 24 hours; a second trigger in that window escalates instead. The retirement writes a pending carry in the same locked update that deletes the queue: each admitted path, with the content it admitted. At the next fresh cut every path is checked by content (carried, carried through staging’s newer version, missing, or unverified), and a single missing or unverified path makes the gate judge the staging tip instead. Otherwise the cut, which steps back from the tip to find a quiet commit, could silently drop a fix that had already been admitted - a fix the gate cannot see is counted — a standing sweep looks for the exact failure the
queue exists to prevent: a commit that lands above the frozen candidate and touches a path in its
failing set. Such a commit is often the right fix, filed where the gate cannot read it, so the
finding is a report that routes it to
admit, not a gate. Attribution comes only from the agent trailer git-sync writes into its commits; without one, the finding says “unattributed” rather than naming the identity that swept it - freeze state is a read, not a log line —
gate_health.freezeAndConvergereports{ enabled, state, reason, candidate }, and off (switched off) is deliberately distinct from none (on, nothing frozen). It exists because the mechanism was once effectively off fleet-wide for a day — 20 frozen candidates retired, none resumed — and the only evidence was a grep across run logs. Retiring a queue is a compare-and-swap on its exact identity, allowed only when the read proves it safe
One red gate, one fixer
Because a red gate blocks everyone, it attracts everyone, and the natural failure is ten agents fixing the same red. So ownership is folded into the gate's own state cell: an agent reading the verdict also learns whether someone is already on it, before it files another work item about it.
- ownership is a closed assessment, and an unread one says so — when the claim store answers, the cell reports one of nine states: unowned, claimable, held and live, held but stalled, held by a session that has ended, lease expired, duplicate owners, held by a session the liveness oracle can't yet confirm (draining, suspect or unknown), and blocked by a hold. That last state was added after the cell said “claimable” about an item that the claim call then refused, and agents are still told to believe a refusal over the cell. A held claim is called live only when the session oracle positively says so. An abandoned lease keeps its holder's name forever, so "has a holder" is never read as "someone is working on it". When the store can't be read at all, the assessment is null and an in-band unknown (resolver-failed, with the reason) rides beside it, and the block's thread field is marked unmeasured rather than empty. A measured “unowned” and an unread null are separate values, so a failed read is never rendered as “nobody owns this”
- ownership is orthogonal to redness — "no work item owns this" is equally true of a healthy gate and of a red one nobody has filed yet; the gate's health comes from the verdict cells, never from the ownership block
- held and live means stand down — the agent playbook gives live gate operations (re-runs, repair-queue moves, current-red triage) exactly one fixer. Everyone else sends useful evidence once and returns to their own lane; independent work on the gate's code continues under its own work items
Alive is not the same as moving
The assessment is one pure function, and the order of its checks is load-bearing. Duplicates come first: if more than one open work item carries the gate's condition key, the cell says duplicate owners, whichever row happened to be picked as canonical. A partial unique index on the condition key (work_items_condition_key_uq) now stops a second row from being created, so a non-empty duplicates list means rows from before the index or keys set by hand, and they are surfaced rather than hidden. Then the claim state maps straight through: no object is unowned, and claimable, hold-blocked and lease-expired keep their names. Only a held claim consults anything else.
- a held claim asks the session oracle first — live, parked (wakeable but idle) and recorded (open in the session log but not yet wakeable through the coordination inbox) all count as a positive answer. Draining, suspect, or no answer at all gives held-needs-confirmation; ended gives held-by-ended-session. Both switches are exhaustive, so a new claim state or session state fails the typecheck instead of falling into a default
- then an item-progress lease decides live or stalled — a positive answer only proves the holder's process is alive. A live agent busy with unrelated work would otherwise keep the release-critical claim forever, because its own tool calls keep its activity stamp fresh. So the lease runs on the work item's progress, not the process: 30 minutes, three times the 10-minute presence window, counted from whichever is newer, the item's last genuine progress stamp or the moment it was claimed. Once that much time has passed, the same live holder reads held but stalled
- missing evidence never reads as stalled — if neither timestamp parses there is no age to measure, and the lease is treated as unexpired. The classifier never calls a claim stalled without evidence of how old it is
“Is my change live?” is one call
// "is my change live — and if not, what is the ONE thing blocking it?" one call: dev:pipeline_position { path: 'libs/papercusp/libs/db/sql/1519-code-intel-pushes.sql' } → { positions: { committedLocal: true, onStaging: true, inMain: false, deployed: false }, blockedOn: 'gate', nextAction: "clear the gate's abort condition (disk-headroom) — it renders no verdict until then, and re-firing aborts the same way", changeInCandidate: { judgingContainsPath: null } } // a real response (2026-10-15). judgingContainsPath compares BLOBS: does the judged candidate // carry THIS path's current content? null = not measured (here, the gate aborted on disk // headroom, so nothing is being judged) — never read as false. // nextAction: null is a POSITIVE answer — "waiting is correct" — never a missing one. // each hop has its own lever: git-sync:run (commit now) · // release:checkpoint-run (re-judge now) · release:deploy (ship the green pin)
The same answer separates questions agents used to conflate:
- six stages, each with its own health — committed → pushed → gate → main → deployed → serving.
Each stage is past, pending or not applicable, with a health of advancing, stalled, disabled, broken or
unknown.
blockedOnis the first pending stage on the delivery route, so “stuck” and “just slow” read differently - pushing is a side leg, not a step — the gate cuts its candidate from the locally committed branch and never fetches, so pushed sits on a separate publish plane. It governs off-box durability, and it can be pending without ever being what blocks delivery
- the lever can sit further down —
nextActioncomes from the first pending stage that has a lever, which can be later thanblockedOn;actionOwnernames that stage. A healthy leg that will advance on its own no longer masks a red gate behind it, which once read as nothing to do while the gate held main for the whole fleet - a git fact is not a process fact —
positions.deployedsays the release checkout contains the commit;serving.startedSinceCodeChangesays whether the running process started after the code changed, and names the restart lever when it did not - “synced” is not “pushed” — git-sync can report synced with nothing published;
gitSync.pushedReposis the field that says whether the work reached origin - is the gate judging my change? —
judgingContainsPathanswers only about the path passed in. Pass amarker, a literal string the change introduced, andmarkerJudginganswers about the change itself;missingReasonexplains a false - sweep exposure —
sweepExposuresays whether the next git-sync tick would commit half-finished work, with the time to the next sweep and the dirty-path count for the whole tree: more than one means the sweep cannot cut your change alone. It deliberately offers no lever, because nothing an agent can call makes the commit atomic; the move is to finish the set. A path the last sweep held back because its editor’s turn had not ended is flagged as turn-held - where is it running, runtime by runtime? —
positions.deployedspeaks only for :3070.servingRuntimeslists every runtime that executes the path (the release and staging operators, bg-host, the gateway, the embed sidecar, the desktop shell, psu terminal hosts), each with its own measured build identity and a three-valuedcontainsChange: true, false, or null with a reason, never copied from :3070. The measuring method is recorded per runtime: blob containment, a process start after the file changed, a bundle’s modification time, or, for psu terminal hosts, which load the file once per session at launch, a count of how many live sessions run the current file - which build do I test against? — pass a
testClassand the answer carriescurrentBuildTestRoute: the executable route, the runtime it runs on, its isolation boundary, its prerequisites and what counts as verification. Nine classes are maintained, fromunit-integration(an exact test path) andoperator-api(the staging operator on :3170: separate port, process and checkout, but not a separate database) tobackground-federation, which needs a disposable isolated runtime because bg-host does not run the :3170 bundle, andfinal-release, which only the shipped artifact can answer. Readiness is declared per route (ready, requires prerequisites, gap, release-only), and a gap is not a runnable route - re-read, don’t transcribe — each answer names the state cell that re-answers it, chosen by
where the change is blocked:
gate.greenCheckpoint.verdictat the gate,git.mainBehindStagingat main,deploy.3070.shaat deploy or serving,git.pipelinePositionotherwise. An agent callsstate:readon that cell rather than quoting a number that drifts mid-turn
A deploy is a scripted sequence, and the green gate is its go/no-go
Moving a green commit into what the fleet runs goes through a single chokepoint, and its order is code rather than something an agent has to remember: drain → re-check the pause control and live certification → snapshot → swap the release checkout to the target → build the host bundle → boot check → apply staged migrations → restart → health, serving-commit and path verification → broadcast, rolling back on failure.
- mechanics in code, judgment at the gate — the steps, their order and the rollback are a tested scaffold, because nobody wants an LLM to forget the snapshot. The go/no-go is the green gate itself: every 15 minutes a routine checks whether green main is ahead of the release checkout and, if it is, launches the scripted deploy, with no human or agent in that loop. A release-manager agent remains for interactive and incident use
- it only moves forward — the routine deploys only a fast-forward that brings at least one new commit, or the very first deploy. An earlier gate compared commits for equality, and missed a release checkout sitting briefly ahead of main: it redeployed a plan with zero commits, which moved the checkout backward, git-sync moved it forward again, and the live operator restarted on every cycle. A change that only moves the desktop app's submodule pointer is skipped as well, because the operator never builds it. Going backward is a deliberate rollback or a reviewed forced deploy, never the routine
- green code is not yet live-certified — passing the gate and passing the live federation certification are separate verdicts for the same commit, and an ordinary deploy needs both. When the target is uncertified the routine launches the certification and deploys nothing that tick. A certification that already failed for that exact commit is recorded rather than re-run, since repeating it every tick would be a test storm; a newer target starts a fresh attempt
- a broken green is not retried every tick — a failed deploy rolls the checkout back, so main looks ahead again 15 minutes later. After a failed or rolled-back deploy the routine leaves that same target alone for an hour (a tunable default) or until a different green arrives. Before this, one broken green was rolled back three times in half an hour
- one deploy at a time — the whole plan-and-execute run holds a single-flight lock, so a second run that finds it taken steps aside and exits cleanly instead of racing the shared release checkout. The manual lever and the routine also launch the same named transient unit, so the system refuses a second one while the first is in flight
- it outlives the host it restarts — the routine launches the deploy as a detached transient systemd unit, not a child process, because the deploy restarts the very host the routine runs in. An early auto-deploy that ran as a child died at its own restart step and lost its health probe, its rollback and its broadcast
- nothing touches live until the new tree boots — inside the drain, an ordinary deploy re-reads the pause control and requires exact-commit live certification immediately before its first mutation, since a check made before launch leaves a gap. After the swap it runs a boot check while the old process is still serving, so a checkout that would crash on load is caught there and rollback just reverts the tree. Guarded migrations are judged against the revision still serving, so a deploy cannot vouch for its own code
- the code that runs is built, not checked out — the host bundle the operator executes is deliberately untracked, so swapping the git checkout cannot change it. The deploy rebuilds it explicitly, and the build fails the deploy if it could only keep the previous bytes, since a stale bundle is not a deployed commit. Rollback rebuilds it as well; reverting the source alone would leave the failed target's bundle running
- rollback undoes only what happened — the database snapshot is restored only if migrations actually ran, and the live process is restarted back only if it was cut over. Before that restart the previous release gets the same boot check; if even the old tree will not boot, rollback stops and calls for manual recovery rather than restarting into a crash loop. A failure before cut-over gets the same check on the reverted tree: the old process is still serving and health reads fine, so a tree that will not boot would otherwise stay hidden until the next restart. The deploy only reports itself rolled back when that check passes
- a separate checkout — the deploy moves a dedicated release checkout, never the shared tree agents are editing, so shipping cannot clobber anyone's in-flight work. Path verification then confirms every runtime read resolves inside that checkout
- "live" means the process, not the checkout — a deploy counts as landed only when the serving process reports the target commit on its health endpoint. A checkout that is already at the target while an old process keeps serving is restarted, not skipped
- the default ships only green — the deploy refuses a target that is not the green pin. Deploying a specific un-green commit is a separate, forced path that is loud and audited
- forcing needs the owner's own words — the agent lever's force mode is open only to the operator, mug and architect roles. It is refused without a sign-off reference to a captured owner directive whose text says force and deploy and names the commit, and refused while a live peer holds the gate. Force skips the pause-control and live-certification checks. It does not skip the refusal to ship while unregistered submodules hold uncommitted work, nor the fence on a restricted session's held writes. A successful forced deploy opens a standing tested-is-not-deployed condition, which the next green deploy clears
- the deploy reports its own outcome — the routine that launched it is about to be restarted, so the deploy itself broadcasts, appends the pipeline history row, and emits awaitable events: one for any deploy, one per commit for every commit the fast-forward carried (not just its tip), and a failure event that wakes a waiting agent at once instead of at its timeout
Safety & verification
Verification is layered from the change outward — and even the guards are tested.
A mutation probe deliberately breaks a guarded property to prove the guard can actually fail — because a guard that has never failed is a guard nobody has tested. Even the probe had to be engineered for a shared tree. The obvious idiom (copy the file, mutate it, run the suite, restore it) breaks two ways: a kill mid-run skips the restore, and the auto-commit routine can sweep the mutant up while the suite is still running. The second one happened for real: a committed mutant in which the fix's function was defined but never called — inert, clean to every syntax check, and shaped exactly like a finished change. The probe now mutates a copy and points the test at it. It never writes the tracked file, so there is no dirty window to protect, and afterwards it still checks that the file is byte-for-byte what it was. Where the host allows it, the copy is bind-mounted over the subject's own path inside a private mount namespace, so every route to the subject (a direct path, a sibling import, an alias, a package name) reaches the mutant, and nothing outside that namespace ever sees it. Whatever the guard writes into the checkout during those runs, apart from scratch directories, lands in a throwaway layer and is discarded, so no mutant-built artifact reaches the auto-commit routine, a deployer or a peer.
The probe's own safety rails
A tool whose whole job is to run deliberately broken code needs guards of its own. Each of these was added after a probe did real damage or produced a verdict that couldn't be trusted:
- code that deletes never runs for real by default — the original safety model protected only
the file under test. Then a probe of a cleanup script ran its deliberately corrupted delete logic against
the real filesystem and destroyed about 370 GB of files well outside the repository. Probes now refuse when
the subject file or the test command contains deletion primitives (
rm,rmdir,unlink,shred,truncate,mkfs,ddwith an output file,find -delete). The match is deliberately blunt: the word in a comment refuses the file too. The preferred route puts shims forrm,rmdir,unlink,shred,truncate,mkfsandddfirst on the path; they log what would have been deleted and delete nothing. They intercept by name only, so a call by absolute path such as/bin/rmgets past them, and so doesfind -delete, which never calls them. The alternative runs the real thing in a sandbox where the whole filesystem is read-only apart from one named directory and the probe's own scratch space. Historical mode only logs these matches instead of refusing, because it runs two real versions of the code and never produces a mutant - mutating the tracked file in place needs a fence — it is refused unless the session holds a verified file lock with the intent “mutation probe” (the auto-commit routine re-reads the lock list after staging and unstages locked paths, plus any staged path whose file no longer matches what was staged, which is exactly what a mutant restored before the commit looks like) or explicitly accepts the race. With the lock, the dirty window must fit in 600 seconds so the lock's 1,200-second lease outlives it. Either way only one in-place probe per user runs in a checkout at a time, and evidence binders refuse a path while it carries the probe's lock
- historical mode tests the code that really preceded the fix — instead of guessing a mutation, name a commit, or ask for the last commit that lacks a given literal. Naming a commit also requires a literal that must be absent from it, as proof that it really predates the fix; the content-based form uses its own literal for that. The probe freezes both versions, checks that its positive controls appear in both, calibrates against both, and then requires the guard to pass on the current code and fail on the historical code. Every phase runs against a relocated copy of a frozen snapshot, never the tracked path itself
- an unsound probe is not a verdict — exit 0 means the expected verdict (by default: the mutant was caught), 1 means the guard is weaker than you thought, and 2 means the probe could not be run soundly: a mutation that changed nothing, a mutant that no longer parses, a baseline that selected zero tests or failed on its own. A fourth code, 3, is the one that demands action: an in-place restore failed its check, or the tracked file changed mid-probe into something that is neither the original nor the mutant. Either way the probe overwrites nothing and keeps recovery copies of both versions. Probe runs are tagged so the shared test ledger can keep deliberate failures out of its health figures
- a caught verdict says whether the mutant still loads — the verdict line carries
mutant_parse=:okmeans the mutant was confirmed to be a loadable program, so the catch is about the assertions;uncheckedmeans no syntax checker applied, so a mutant that merely fails to load has not been ruled out. For a recognized test runner, a guard exit of 1 counts as a catch only when its output shows an assertion actually failing, since a setup hook can exit 1 before any assertion runs - a survivor can be the copy's fault, not the guard's — the verdict line also names how the copy
was placed (
relocation=). On a host without the private mount, the mutant goes into a mirror of the tree instead, and a test that reaches the subject through a sibling module can load the original file and report a false survival. The probe prints a warning beside any survivor from a mirrored or flat copy - the probe runs a frozen copy of itself — Bash reads a script as it goes, so a peer editing the probe during a long run could shift the read position and turn a later message into shell commands. The probe copies itself, syntax-checks the copy and runs that. Each run also emits a replay record with its exact arguments and the commit and SHA-256 of the script bytes it executed
- you can't keep re-aiming until something is caught — each probe row records its subject, mutation, before-and-after hashes and the literal test command. When a probe run is bound as evidence, a catch that follows an earlier survivor against the same unchanged test is refused unless it carries a re-aim justification naming every prior survivor and a concrete reason for the new mutation
# the worked example in the script's own header — a separate test pins it as runnable scripts/mutation-probe.sh \ --file scripts/verify-tauri-headless.sh \ --fake-destructive \ --mutate 's/^\s*if port_lost_to_squatter; then PORT_LOST=1; break; fi$//' \ --test 'PROBE_SCRIPT={} npm run test:file -- apps/operator/lib/verify-tauri-headless-bind-retry.test.ts' # {} is wherever the guard will see the mutant (the subject's own path under the overlay, # the mirrored copy otherwise); the verdict line is what gets quoted as evidence # printed as one line; wrapped here → MUTATION_PROBE_RESULT verdict=… expected=caught target=… baseline_exit=… guard_exit=… mutant_parse=… assertion_evidence=… relocation=… baseline_ledger=… copy_baseline_ledger=… mutant_ledger=…
What “done” has to carry
Closing a work item is not a status flip. work_items:complete runs a versioned completion
contract, and it grades the evidence instead of trusting the assertion:
- an unverified close is recorded, but it doesn't count — a close earns committed authority only when it says how it was verified and carries a test run or result. Anything less lands as proposed: kept, but never counted toward burn-down. This replaced a gate that could not fail — it accepted the required summary field as proof, and an audit found 65.6% of 14,663 terminal rows looking gate-approved while carrying no verification evidence at all. A close that says it added tests must also name at least one changed path, or it lands proposed too, and so does a close that declares deferred work without saying which part of the ask it left
- a bug close must record what would have looked different — a successful bug or
capability-gap close is refused, with nothing written, neither a completion record nor a state change,
unless
completion.rootCauseVerificationrecords the hypothesis, an alternative, the test that tells them apart and its exact procedure, what each hypothesis predicted, what was actually observed, and durable evidence references. That is causal-evidence contract v2; a close that passes is stamped with the version, and older v1 records still parse. The alternative must differ from the hypothesis, and the two predictions from each other, once case and punctuation are stripped: a rival that predicts the same thing isn't a rival. The test needs a non-empty procedure, not a prose template. The record goes insidecompletion, as below; sent beside it, it is refused too - declared claims are re-derived, never believed — a claim about the source is re-evaluated against the tree, and one found false drops the close to proposed. A claim the server cannot decide stays silent, because absence of judgement is never judged clean
- present is not the same as true — the evidence test is presence, and a test-result
field that literally reads “3 failed” is present. So more checks compare the close against records
the closer didn't write, and any one that finds a contradiction caps it at proposed: the
closer's own most recent
testing:runwent red in the test-run ledger; a path infilesChangednever existed, or names a real file that shows no sign of this work: clean in the working tree, and no commit to it since the item was opened (the cheap dodge once invented paths cost a grade); or the close's requirement-by-requirement answer quotes the ask in wording that isn't in it verbatim, cites a path that doesn't resolve, or defers work without naming a filed follow-up. Each check stays silent when it can't decide (no run in the window, a run spanning two groups, an unstamped row). A red run is reported first, because it means the work isn't finished, not just that the record is wrong about it - filing is not disposing — a close that cites a follow-up still open with no assignee is capped at proposed: leftover work needs an owner before the work counts as done
- committed means committed, with that exact content — for every path a code close names, it records the content it saw, in the last commit and in the working tree. A changed file the server can't prove is committed with that exact content holds the close at proposed, which is simply what an honest close looks like before git-sync sweeps. Nobody has to close twice: git-sync's completion-settlement reconciler re-checks commit history on every pass and upgrades the close once a commit carries the recorded content, even a later commit that restores it. When that content is in no commit since the close and the working tree no longer holds it either, no sweep can land it, and the close is labelled superseded-before-commit so its closer knows waiting won't help. Settlement is local: a row written on another install is settled by the peer that wrote it, and this one says so instead of advising a second close. Deleting a file counts as work: a declared deletion settles when a commit shows the file gone
- the contract answers row by row — a refused close, and a
validateOnly:truedry run, return the contract the close was judged against: its version and one row per requirement, each marked applicable or not and, when applicable, satisfied or not. A requirement that doesn't apply reports no verdict at all, so not applicable can't be read as passed. A refusal lists every reason it was refused on that call, not just the first, so the caller can fix them all before resending, and it writes neither a completion record nor a state change. A successful close names the contract version it passed. The rows cover declared assumptions, a bug's causal evidence, spec-test adequacy, design evidence, verification coverage, self-review and red-gate lineage, and the list grows by version. The response is built from the same definition the gate checks, so there is no second, prose copy of the contract to drift - universal claims need a census — when an item is shaped as verification (its
title, summary or metadata says verify, audit, accuracy check, fact check, source check, or traceability) and
its close claims totality ("every page", "all claims", "no unverified entries", "fully verified"), the close
must carry
completion.verification.coverage: a non-empty population of distinct names, with each name copied verbatim into exactly one of checked, not checked, or not applicable. A name outside the population, a name placed twice, or a name left out fails the partition. Residue is a separate field, not a fourth bucket, and it must be stated:residue: []says there is none, and omitting it is not read as none. A population entry copied into residue is named in the refusal. The check is decided from the submitted record alone, so it refuses before anything is written. Both triggers are keyword tests, and both must fire: an ordinary code change that reports "all tests passed" isn't shaped as verification. A close that names a different surviving item as its duplicate is exempt, because it claims the item is superseded, not that a population was covered - each spec clause owes proof sized to its risk — spec-test adequacy gives every clause a floor. A clause in a migration or bugfix plan, a clause marked mutation-required, or one whose behavior is authorization, concurrency, lifecycle or migration data integrity owes level 4: a mutation or counterexample bound to that clause with a passing baseline case, or a coverage census that marks level 4 for it. Any other code clause owes level 3: a coverage census that marks level 3 for it, or any level-4 proof. The one exception is an investigation-plan clause with no required test layer, which owes none. A clause owes no executable proof at all when its work item is a non-code kind, its plan item resolves to a non-code deliverable, or it is non-automated and names no test layer, and it then reports not applicable. Only an explicit non-code verdict exempts: a deliverable that can't be read stays code, so a failed read never lowers a floor
- a gate-progress claim must be visible to the gate — while a red release candidate is frozen, a fix committed to the working branch lands above the code being judged, so “I fixed the gate reds” can be locally true and globally false. A close that claims it is refused unless the admission ledger holds the claimed paths on the judged lineage and their legs read admitted or green; closing the gate-red item itself asserts the whole gate is green, so every leg must agree. The refusal hands back the exact admit command to run
- a conformance review can't be closed by its builder — a review item that checks a plan against its spec refuses a successful close by any of the plan's implementers or anyone in their spawn or rebind lineage, and it refuses when that population can't be read at all. Dropping the item stays open to anyone, because a drop claims no review happened
// a bug close carries its causal evidence inside the completion record work_items:complete { id, state: 'done', assumptions: 'none', completion: { summary, filesChanged, testsRun, testResult, verifiedHow, rootCauseVerification: { hypothesis, alternativeHypothesis, distinguishingTest, testResult, testProcedure, predictedObservations: { hypothesis, alternativeHypothesis }, actualObservation, evidenceRefs } } } // leave part of it out and nothing is written: → 'REJECTED — … completion.rootCauseVerification is incomplete for causal-evidence contract v2. Missing: actualObservation, evidenceRefs. … No completion record or state transition was written.' // with it, the close is graded — committed (counts toward burn-down) or proposed (recorded only)
That grade lives in its own column, separate from the lifecycle state, because one column made an evidence-free “done” look exactly like a verified one. It has five values. The legal moves between them, and who may make each one, are written down as a table in code, and unit tests check the table's own invariants:
- proposed — asserted without enough evidence. The item has left the queue, but the claim stays owned by its closer, gets nagged, never returns to the claimable pool, and doesn't count toward burn-down. The closer settles it by closing again with evidence
- committed — asserted with
verifiedHowplus a test run or result, and nothing above contradicted it. The normal good close, and it counts - pending_human — a claim no agent or machine can settle. Only the owner rules on it, either way: committed or invalid. Until then it doesn't count
- invalid — a recorded close that turned out wrong: the item was reopened, the fix didn't hold, or another item replaced it. A proposed, committed or validated close can fall here, and the row keeps the fact that a bad close happened, which a plain reopen used to erase. The item can be closed again afterwards
- validated — a claim corroborated by something independent of whoever did the work: a green run found in the test-run ledger, a cited commit that provably exists, an owner's ratification, or an output contract the platform checks itself. Only the last has a writer today. When an accepted blueprint program finishes, the canonical completion path checks its output against the operation's declared result schema, plus the model attestation and a grader's rubric acceptance when the operation declares them, and only a root that passes closes as validated. A cancelled or failed program closes as committed, an ordinary system close; earlier settlements stamped those validated too, and those rows remain. The upgrade from committed on independent evidence is in the state table but has no writer yet. Writes still aren't checked against the table as they happen, so a source test pins the one writer instead. It counts, and it is rare
Containment: what an agent can't reach
Containment is just as structural:
- every spawned process tree is enrolled — in a task ledger, confined by the OS, and killed by identity, never by name pattern — so an agent can't take down its neighbors (or your desktop) with an over-broad kill
- secrets never enter the tree — credentials live in injected configuration outside the repo, and a pre-call hook refuses an agent's file edit that would write a key-shaped string into a shared tree
- feature flags default on — finished work never ships dark; the rare flag that must stay off is registered with a reason and a review date, and the parked-or-incomplete subset sits under a ceiling that ratchets down (how)
- the truly dangerous sits behind owner gates — irreversible migrations, outward-facing sends, force-deploys past a red gate: a human says yes, loudly, and the ledger remembers who. For a force-deploy the yes is checked, not assumed: the request must cite an owner directive that the prompt hook captured from a turn the owner typed in this workspace, and that directive's own words must say force, deploy and the first twelve characters of the commit being shipped. An agent's paraphrase of permission doesn't pass, and neither does a directive that approved some other commit
Here is what "killed by identity" means in practice: a stop request names its target by a handle the platform issued, never by a process-name pattern. The preferred handle is a task from the ledger; the other two are narrower. A live but unaccounted Papercusp scope can be named, and it is re-probed before it is signalled. A raw pid number is accepted only as a legacy path, and only for an agent process of a known kind (run.sh, omp, claude or the Codex CLI). Two refusals cover everything else. A kind the platform tracks but doesn't stop this way, such as a terminal host or a Next.js server, is refused as protected. A process it doesn't track at all is refused as unsupported, and that includes host services such as the operator itself, which restart only through a guarded restart tool. The unsupported refusal says the process is still running and names the handle that does work, so a refused caller isn't pushed back toward a name-pattern kill. In pid mode every request goes to the audit log, refusals included, and an accepted kill is logged before its signal goes out (a failed audit write is reported, not fatal):
// stop by task id — the ledger knows exactly what it launched, and for which work item processes:kill { taskId: '0mv47ydnf2irlxv1y6z' } // a confined task: its whole OS control group is signalled and re-probed — if a plain SIGTERM leaves // survivors, a bounded service stop follows, and only survivors after that come back as 'incomplete' // an unconfined task: the pid is first re-checked against the identity recorded at launch — → { ok: false, results: [{ taskId: '0mv47…', ok: false, action: 'kill', error: 'identity_mismatch', detail: 'pid 412873 now holds identity …, row recorded … — refusing to signal a recycled pid' }], counts: { … } } // (an older unconfined task with no recorded identity has nothing to compare: it goes through, flagged weak) // pid numbers get reused on a busy machine — a kill aimed at a number could hit a stranger
Testing the desktop UI without touching your screen
Agents check UI changes in the real desktop app, but never in the copy on your monitor. Before this was packaged as one script, agents either fought over the owner's live window, stealing focus in the middle of real work, or found it busy and skipped the UI check because building an isolated instance by hand looked too expensive. Two separate work items hit that same gap in the same week.
- its own display, its own app — the verifier boots a separate copy of the desktop app, with its own dev port and operator sidecar, on an automatically chosen virtual display numbered 90 or higher, with a real GPU behind it so screenshots aren't blank. It never touches the owner's displays. It runs the agent's assertions, then tears down the whole process tree and the display, even when an assertion fails, and exits with the assertion's own exit code. It won't start at all without the calling agent's session id, so every run can be traced to whoever launched it
- ownership is proven before anything is driven — finding a healthy automation bridge on the machine doesn't make it the agent's: the owner's live app can expose one too, and driving it would be destructive. Before handing a bridge over, the verifier matches two launch-provenance markers in the target process's own environment: the agent's session id, and a per-run marker that embeds it, so two rigs one agent starts back to back can't satisfy each other's checks. A bridge the owner launched is never handed to an agent. A multi-step session that keeps the rig up between calls re-checks those markers each time an agent loads the rig's details, because a process number can be reused. That mode is refused inside a managed tool task, which tears down everything it started when the call returns, unless the agent explicitly accepts that the rig dies with the task
- honest about what isn't isolated — by default the copy shares the live database, workspace and WebView profile, and the script says so in capitals: every write is a real write. The header once claimed full isolation; that was false, and it was corrected in place instead of being left for an agent to trust before clicking a destructive button. Opt into isolation and each run gets a throwaway, fully migrated Postgres on its own port, plus its own workspace and WebView profile. The verifier then proves each part: it refuses if the throwaway database picked the live one's port, reads the running sidecar's own environment to confirm the database really is different, and requires a test value in the WebView's local storage to start absent, survive a write and a read, and clear. It even hides the host's GitHub login, because creating a project in that window would otherwise publish a real repository
- a fresh database starts at onboarding, on purpose — an isolated run boots as a genuine first
install, so a page that only exists after setup lands on the first-run tutorial instead of the page under
test. A
readyseed marks onboarding complete through the app's own setup endpoint, reads it back, and refuses to hand over the window if the flag didn't flip. It creates nothing else, and it is refused without isolation, because the seed is a write and would land on the owner's real database - a run that can't see your change refuses instead of failing — the window loads a frozen snapshot of the shared front-end bundle, so a rebuild can't swap code underneath it mid-run. The flip side is that a snapshot taken before your edit was built tests the old code, and one shared build has taken 26 minutes. Two checks turn that into a refusal (exit 3, kept distinct from a failed assertion): name the files you edited and the verifier waits, before freezing, for a build that started after all of them; or name a literal string your change introduced and it searches the frozen bundle for it after boot
- an empty page can't pass a negative check — “the error banner is gone” is also true of a page that hasn't loaded yet. So an assertion that something is absent is refused (exit 2) unless it also names something that must be present, and both are checked in the same evaluation, so nothing can change in between. A separate settle step waits until data sync is idle and the page has stopped changing before anything is measured. It has three outcomes, settled, still busy or unknown, and anything it can't recognize is reported as unknown, never as settled
- a browser can't wander in — the operator's internal ports (3055, 3070 and 3170) are the desktop's content source, not a website. A browser pointed at them never finishes loading and has wedged the shared browser daemon. So the browser-verification tool's own command-line client is patched to refuse those ports before it contacts its daemon, which covers the plain shell invocation too, and a pre-call hook blocks the dedicated browser-navigation tools from opening them
A ratified design is compared, not eyeballed
Interface work has its own design phase, served by a plugin of fifteen tools. A designer agent searches the component registry before inventing a component, reads colors, spacing and timings from a token store in the Design Tokens Community Group format, and writes the screen as a structured spec. The spec is checked against a schema and linted for known anti-patterns: validation or lint errors refuse the submission, and warnings go to the design critique. A reviewer's later verdict either keeps the accepted spec or sends the feature back for a new designer. Once a design is approved as an image, the implementation can be held to it.
- the reference is immutable and revisioned —
ratify_referencerecords an approved image as a feature's visual reference, with where it came from, the capture environment it is contracted at, and the cases that must pass. The caller submits the revision number it believes it is creating, and a stale number is refused rather than reallocated. Who ratified comes from the calling session, never from the arguments. A new revision invalidates evidence bound to the old one - the verdict is decided in one place —
compare_rendercompares one captured render with the active reference. The engine reports how many pixels differ; whether that passes is decided in one place, by arithmetic over those counts under a versioned policy: a pass when the share of differing pixels is at most the policy's maximum. The engine's own within-threshold flag is never read - three verdicts, and invalid is one of them — pass, fail or invalid. A comparison that was attempted but can't yield pass or fail is stored as invalid with a reason from a closed list of eleven (dimensions that disagree, the wrong capture environment, an engine error or timeout, among others), so it is evidence and never a silent pass. A capture taken in an environment the reference isn't contracted at, or of a surface the reference doesn't require, is recorded as invalid before any engine runs. The engine would still return a number for the wrong surface, and in the worst case that number is zero, a perfect score for building the wrong thing
- a refusal is not a result — when no comparison could happen at all (the reference was never ratified, its image can't be read, no comparison engine is installed in this host, or the approved image's bytes changed after approval), the call is refused and nothing enters the evidence table, so a misconfigured host can't pass for a measurement
- no mask that isn't applied — an earlier version accepted regions to ignore, stored them with the evidence, and never applied them. The field was removed, and a caller still sending it is refused loudly: recording it would claim an exclusion that never happened, and dropping it would hide that every pixel was compared
- sketches don't gate — a reference class whose images can't be compared with a real render is advisory-only. Its pixel ratio is stored as invalid, with the reason that it can't gate, and the remedy is to ratify a derived reference that can
- where a design stands is a read —
get_design_evidencereturns the active revision and its history, every stored comparison split into the deterministic result and advisory prose, which required cases have current evidence, and how much stored evidence is stale, so nobody re-runs a comparison to find out
Flags default on, and the dark ones are rationed
A feature flag lets unfinished or risky work merge without running. Left alone, the same mechanism becomes a parking lot: finished work stays off “to be safe” and nobody ever turns it on. So the default is inverted in code. Every flag lives in one registry, and no flag's default is written by hand: it is derived as on unless the flag appears in a separate dark map, where each entry has to name a case and give a reason. A snapshot test proved the inversion changed no flag's value. Measured on main at d740fc3802: 285 registered flags, 35 of them dark, and exactly those 35 default to off. There is no “finished, waiting for someone to flip it” case. Finished and tested means it ships on, and flipping it and watching it work is part of the task.
- four cases, two populations — parked (a deliberate scope cut) and incomplete (unverified or unsafe) are work an agent could finish and turn on. Owner-authority and cutover are safety switches, permanent or for a staged migration, that only the owner may turn on. Every case belongs to exactly one of the two sets, the suite checks that partition, and a case outside both makes the lane lookup throw rather than fall into whichever clock it happens to check first
- safety switches are never rationed — an earlier single ceiling counted owner-reserved kill-switches together with parked work, so adding a real safety switch cost exactly as much as hiding finished work. The guard's actual incentive became leaving a mis-defaulted safety flag silently on. The ceiling was raised three times running for that same reason (22 → 24 → 25 → 26) before the two populations were split. The 23 owner-authority and cutover entries are not rationed at all, so a new safety switch never has to compete for a parking budget
- the parking lot has a ratchet — parked and incomplete entries together are held under a high-watermark, now 12, and main sits exactly at it (8 parked, 4 incomplete). A new entry in either case fails the build unless an existing one graduates first or the owner explicitly raises the ceiling. Nothing lowers the number automatically: each graduation lowers it by hand in the same change, so a freed slot is spent rather than banked as headroom for the next park. The last two graduations took the ceiling from 14 to 13 (a gateway OAuth proxy, once its production canary passed) and from 13 to 12 (a log-snapshot producer, once its cross-machine catch-up proof ran). A raise happens only on an explicit owner directive, recorded in the source next to the number. The owner has bought a slot that way twice, from 13 to 14, both times by asking for new work behind a testing flag
- two review clocks, and only one can go red — parked and incomplete entries carry a hard review date, and a test fails the shared gate once it lapses, because agents can act on those. Its message says to walk every parked and incomplete flag, turn on the ones that are ready, and only then move the date: “Don't just bump the date — the review is the point.” Owner-authority and cutover entries carry an advisory date of their own, but lapsing it only puts the review in front of the owner, because no agent may graduate them. One shared date used to gate all four cases, so a red could come entirely from entries no agent could change, which taught agents to bump the date
- warned before it goes red — an operator watchdog reads the same constants and files one item into the owner's improvement queue when the parking review is due within 14 days or the count is within 2 of the ceiling, and marks it major once either is breached. Its title carries no counts or dates, so every tick lands on the same row, and a resolved finding stays quiet for 24 hours instead of re-firing on unchanged inputs every 15 minutes. A date that won't parse counts as due now
- a review re-derives, it doesn't inherit — a review re-checks every parked and incomplete entry against the current tree. The most recent one, on 6 October, graduated none: all eight parked flags were explicit owner scope cuts, so flipping one would have reversed an owner decision rather than finished a feature, and none of the four incomplete flags had yet passed the proof it is waiting for
- a review reads the live state, not just the prose — reading each entry's case and reason
can't show a flag that is already on. One review, on 9 August, concluded a flag “stays dark” in the same
cycle it was resolving on through a runtime override. So
npm run audit:dark-flagscompares every dark entry with its live state: the stored override rows and the running process's environment. Only overrides that take effect count. A global override row, where one exists, shadows every per-workspace row, and an environment value wins over both. A parked or incomplete flag that resolves on anywhere is reported as critical, and the run exits non-zero. An owner-reserved or cutover flag switched on by override is expected, and is listed only for visibility: the override is the sanctioned way an install switches one of those on, while parked or incomplete work has no sanctioned way to be live. Its output names its own blind spot: an override set on the PostHog side is invisible to it. Reviews run it by hand; no gate does. In a run on 18 October, no parked or incomplete flag was on anywhere, and four owner-reserved switches were on through the global override - comments and routes must agree with the map — a lint parses each flag's own comment and fails the build on a new mismatch: a comment claiming the flag is off by default, listed as dark, or “ships dark until” something, for a flag that is not in the dark map. That mismatch had hidden five times as a stale registration. Exceptions need a reason on a pending list, and that list is empty. A route test derives every flag-gated route from the router source and checks that a gate on a dark flag really refuses while the flag reads off, so a “not shipping” reason can't be a comment the router ignores
PaperCusp is in private testing — request access · home
The carry inventory
Kill any session mid-task — a crash, a context cut, a machine restart — and the question that matters is: what does its successor need to resume on task rather than start over?
// a carried CLAIM travels with the probe that falsifies it — so a successor can // tell an asserted claim from a verified one, and re-check instead of re-trust work_items:checkpoint { id: 'WI-1204', checkpoint: 'backfill 60% — resume at batch 41', checks: [{ claim: 'live mp3 is the 140.5s build', recheck: 'curl -sI …/arch-tour.mp3 | grep content-length', verified: '21:07Z — 1,124,589 bytes, matches' }] } // rendered on every re-injection: ✓ VERIFIED (evidence attached) · ? PREDICTED (run the probe // first) · ⚠ CONTESTED (the "evidence" itself contradicts the claim, e.g. "STILL RUNNING") // plus [scope: …] beside the badge: what the probe actually observed (one file, one window) // — a claim quantified wider than that ("every", "all day") is NOT verified by it // the staging build adds falsifier: — a probe says how to LOOK; a falsifier says what you'd // have to see to know the claim is WRONG
The carry document: assembled, never summarized
When a session's context fills, the agent picks a clean stopping point and requests its own respawn; a watchdog only backstops one that doesn't. The host relaunches the CLI on a carry document built straight from the database, with no LLM summarizer anywhere in the path:
- fixed slots — an identity block, the session's mode flags and armed loop, its claimed plan items, its open questions to the owner, its still-running background jobs, the standing carry brief (loop note and its checks, held work-item checkpoints, open owner directives, facts, armed awaits), a note on how the last turn ended, a verbatim tail of the latest turns, and a pointer into the searchable transcript
- the brief carries what a cold reader can't re-derive — besides the owner-scoped facts, it lists the keys of every never-drop fact (guard rails, dead ends, walls) folded at workspace scope as well, because a fleet-wide guard rail is asserted there and an owner-only fold never reached it. It also lists work the session filed or commented on but no longer holds, under a deferred, unclaimed heading, so a successor re-checks it before declaring the lane idle
- it degrades by dropping to a pointer, never by paraphrase — what doesn't fit becomes a reference to the full text, as tool outputs in the tail already are. Nothing is rewritten into a plausible-sounding summary, and each slot is read independently, so a failing reader empties its own slot rather than the document. Under a size budget the standing brief sheds first, then the tail ages by dropping its oldest turns, then plan state and owner asks. The head (identity, mode flags, the recovery marker, still-running jobs), the last-turn note and the transcript pointer are never shed
- it says how stale it may be — the document is stamped with its build time, and delivery can lag that by up to 30 minutes. It calls itself a lower bound: the predecessor may have kept working after the stamp, so the successor looks for newer evidence on disk and on the work-item before redoing anything, and for a later correction before repeating any finding, since a retracted claim rides the same window. A held checkpoint or loop note written more than two minutes before the session's last recorded turn is marked superseded, with both times, instead of reading as current. A continuation directive the session itself withdrew gets a retraction guard, so the successor doesn't reissue it
- recovery arrives with it — the document carries a recovery marker stamped with a hash of the session's whole normalized control state — its modes and their activation, loop, wake source, carry mode, route and scope among them — but not delivery metadata such as the generation number or timestamps, so a restart that only bumps the generation is not mistaken for a real change. A successor whose current control state matches knows it is already oriented and skips the re-orientation round-trip; a mismatch tells it to re-orient exactly once
- the owner's last message is delivered, not quoted — the final turn the owner typed is found by its provenance stamp, not by guessing who is speaking. If no reply followed it, it becomes the successor's actual first prompt instead of a line in the tail, because a quoted copy reads as history that was already handled. The note on the last turn also says whether the cut landed on a settled turn or was forced mid-turn, and whether the session asked for it. When the tail is too short to show whether the owner's requests were answered, the note says the history is truncated and points at the transcript instead of treating the work as closed. The flag clears only when a count of the session's owner turns comes back zero, so an unreadable count can never hide a real owner question. A watchdog can cut cleanly without the session ever flushing its state, so that successor checks how fresh its checkpoints are instead of trusting them. The watchdog's routine cut also steps back once rather than cut across an unanswered owner message
- running jobs are listed, with an honest lifetime — shell jobs the session started that the task ledger still shows unfinished get their own slot, because unlike everything else here they keep using machine capacity while nobody watches. Their state comes from the ledger, never from their output, and the slot says plainly that a job outlives a respawn but may not survive an operator restart, so the successor re-checks before relying on one
Provenance: so a note-to-self never becomes an order
Attribution is the first thing to rot under repeated hand-offs. Each re-read resolves ambiguity toward the higher-authority reading, so an agent's own caution ("don't close this until the UI pass is done") can harden, a few carries later, into "the owner set an explicit gate". Pointed the other way, the same drift manufactures a false permission. So provenance is mechanical, not prose:
- every injected turn is stamped at the source, and a ledger decides — each system injector (a
loop fire, a wake, a compaction continuation, a coordination inject, a fleet kickoff) prefixes its prompt
with a turn-origin envelope (
⟦turn-origin:<origin> nonce:<id>⟧) that survives verbatim in the transcript. Before it types, it also writes the nonce, the origin and a hash of the payload to a short-lived trust ledger: ten minutes by default, longer for a row whose wake is scheduled to fire later. The ledger is a machine-local file rather than a database row, so the check takes milliseconds and still works with the operator down. A prompt hook classifies every submitted turn against that ledger, never by the text alone. An envelope with no ledger row is an unverified claim (a spoof or a replay) and is trusted as neither reading. A turn with no envelope and no ledger match is the affirmative shape of the owner typing, which is why every injector must enrol: an unenrolled one would be read as the owner - carried directives cite a turn, not just a tag — a durable turn reference
(
[turn:<session>@<time>]) names the recorded transcript turn a directive came from. The platform verifies it by loading that turn from the writer's own transcript and classifying what was recorded. If a quote is attached, it also checks that the quoted words actually appear in that turn. For the turn being written from, the agent writes[turn:self]and the platform expands it at write time: no wake or tool result states the current turn's timestamp, so a hand-guessed one anchors to nothing. A hand-written[owner:…]tag on its own only asserts: the carry-write lint flags it unless the same line carries a turn reference, or the write happens during an owner-typed turn and the tag carries that turn's own date, when the platform has just witnessed the owner speaking and verifies the tag itself. A tag dated any other day cites a directive this turn does not contain, so it is not cleared. Neither is owner-attribution phrasing or a bare gate imperative - unverifiable owner authority is marked, downgraded or refused — never stored as clean. How
strict the response is depends on how much harm a wrong attribution does on that surface:
- a work-item or loop checkpoint is still stored, but under a
⚠ UNVERIFIED OWNER ATTRIBUTIONline, so every successor reads it as agent-authored - a standing fact is kept but downgraded to suspected, with
owner_attribution_unverifiedstamped as its source - a plan Decision is refused outright (
falsified_owner_anchor) when it claims owner authority on a line whose turn reference resolves to a machine turn (a loop fire, a compaction note)
- a work-item or loop checkpoint is still stored, but under a
- the lint reads more than owner tags — and it stays advisory. It runs on checkpoints, facts and
compaction notes with a three-second budget, and any failure degrades to the plain lint, because a carry
write is often made near a context limit and must never be blocked by its own advisory. It also flags:
- a bare gate imperative with no source, which a successor would simply obey
- a claimed completed action ("messaged the fleet leader", "posted evidence to WI-…") with no
turn reference. The exception is a claim proven by a successful
coord:sendorevents:awaitcall in the writer's own tool ledger - a causal mechanism asserted about a named artifact with no probe, hedge or tag. Once it is filed as a work-item, a guessed cause directs a peer's labour
- a deploy dependency ("waiting on main", ":3070", "a deploy") with no probe attached, since most code here also runs on runtimes a deploy never touches
- an owner tag claiming a question dialog was answered, which gets the sharpest note of all. A session once died with its question still open, and a later note carried it forward as answered
- search results carry the verdict — a transcript hit that sits in the user's slot but was injected by the system comes back labelled machine-injected, so a loop fire can never be cited as "the owner said X"
Owner directives: captured verbatim, closed with a reason
The owner's own words are the one input no summary may paraphrase, so every owner turn is recorded the moment it arrives:
- every owner turn is a directive — a prompt hook writes it as an open row, verbatim, with no triage step that could quietly decide a question wasn't an instruction. Open directives lead every successor's orientation, ahead of its own agenda
- closing one takes a disposition and a note — done (say how) or declined (say
why). A note under four characters once trimmed (blank, whitespace, a bare "ok") is refused, because a
disposition without evidence is how an order gets silently dropped with extra steps. A done note made
of nothing but links is refused too (
url_only_done_note): a link can support the account of what was verified, but it cannot be the whole account. Up to fifty closes can go in one batch, and each entry is judged on its own, so one foreign id cannot sink the rest - a directive belongs to the session it was typed into — the owner often pastes one message into
several sessions, and each copy is its own row. Only four kinds of session may close a copy:
- its addressee, the session it was typed into
- a session holding an unfinished work-item linked to it
- if the addressee has ended, the live leader of the fleet it was in, which inherits it as its own obligation
- any session at all, when there is no live addressee, holder or leader, because the copy is then unhandled. It sits on the owner's "nobody is handling these" list, and someone has to be able to adopt it
foreign_directive). The refusal names the addressee and holders and points toorders:clear, so an agent cannot sweep up its peers' obligations by matching text. When ownership cannot be confirmed at all, the close is refused too (verdict_unavailable, retryable): a refused close can be retried, but a wrong one silently stops the directive showing for its real addressee. For the same reason, a liveness verdict of unknown never reroutes a directive. A mistaken close is undone with a reopen that takes its own reason and records which close it undid, never by editing the row. A reopen is open to whoever made the close and to anyone the same rule allows, and it refuses a directive that is already open - not yours, off your banner — a directive addressed to another session that keeps showing to you
is taken off your own banner with
orders:clearand a reason. It stays open for its addressee and every other session - long directives are summarized, never cut — a directive over 500 characters shows to other
agents only through a summary of at most 200 characters, written with
orders:summarizeby the session that received it and labelled with that session's id, so it is never mistaken for the owner's own words. A shorter directive always shows verbatim, and summarizing one is refused, as is summarizing a directive that is already closed
Reports: finished work the owner can find later
A checkpoint, a fact and a decision are written for the next agent. An audit, a review, a postmortem or an
analysis is written for the owner, and left in a transcript it is never found again. Any agent session can
publish one into the Reports library with reports:publish, which returns a stable report id and
the path that opens it in the app.
- origin is stamped, not passed — the arguments have no author, pot or session field. The server fills them in from the publishing session, so a caller cannot forge who wrote a report
- too long is refused, never cut — the body is Markdown, with Mermaid diagrams allowed, up to 512 KB. A longer body is refused with its exact overage, because a reader could not tell a truncated audit from one that simply ended
- the owner reads it unless it is widened — a report can be read by the owner (the default), by its pot, or by the whole workspace
- a correction is a new row — nothing is edited in place. Publishing with
supersedesadds a row that inherits the original's lineage. The library lists the latest row of each lineage, every earlier body stays readable by id, and superseding an id that does not exist is refused rather than starting an orphaned history - withdrawn, not deleted —
reports:retiretakes a report the caller authored out of the default list; there is no hard delete - publishing is an event — each publish fires
report:published:<id>, so another agent can wait for it. If that notification fails, the report is still published and the result says so
A successor re-orients and continues. Warm or cold (§08), it rebuilds from the inventory: same identity, same claims, same task, new window.
And the GUI is not a dashboard bolted onto the side — it renders the same store the successor reads (§01). What you watch is the carry.