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.
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 — an in-app distribution shelf
- pots federate peer-to-peer — between your own machines over an encrypted mesh; there is still no central server in the loop
Everything the operator sends out passes through one of two audited doors:
- the sync path — state to your screen; because this door is addressable, an agent can read what you're looking at and drive the same interface you do
- the inference gateway — model calls to providers: the only traffic that leaves the machine at all
Both doors exist so that flow is deliberate and observable rather than ad-hoc.
// 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.list', args: { state: 'wip' } }) // any write — agent or human — invalidates precisely; exactly the touched queries re-resolve notifySyncInvalidate('workItems.list') // and because view state lives in the URL, an agent reads + drives the same screen you see ui:get_state {} → { url: '/plans/db-rework?tab=items' } ui:dispatch { url: '/plans/db-rework?tab=decisions' }
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', text: 'write migration' } // × N items, with dependency edges 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 {} → { item: 'WI-1204', checkpoint: …, planDecisions: […] } // context rides the claim in
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.
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 audit runs first, while the implementation is still warm and code-truth gaps are cheap to find. The rubric is authored afterwards, against as-built reality — never at plan creation, because a bar written before the work describes the plan rather than the result. 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.
draft → active, and acceptance is not shipment — a plan whose
work is finished sits in awaiting-acceptance until it has been graded and accepted.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 stale claim can be routed through reproduce-before-implement, so a fleet never builds a fix for a bug that already died
// closing is where the discipline bites — evidence is structured, not prose work_items:complete { id: 'WI-1204', state: 'done', 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'] } } // a bare "done" without evidence? the state write REFUSES — and a leader audit re-opens fakes
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 · dead, 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
Peer awareness — a theory of mind
Coordination is also an awareness problem — a working theory of mind about your peers.
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 ≠ 'none' ⇒ the message stays visibly UNANSWERED on every leader surface // until a reply threads back to it — asked-and-ignored is a state the system can see
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.
You don't have to know who to ask. The consult router (consult:get_feedback) turns
"who knows this?" into one call: it searches every agent's real transcript history and wakes the
best-qualified peer — or answers instantly from the archive when a closed consult already settled the
question. Below the relevance floor it is honest: "no one knows more than you do," never a costumed
expert.
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 lands in a durable queue first, 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; a parked session is woken into a fresh turn with the payload already in hand
- a miss is loud — a send to a dead session reports
recipient_absentback to the sender, who relaunches or reroutes; there is no silent drop to discover a day later - 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 reply wakes the original asker, and a message that
expectsan answer stays visibly unanswered on every leader surface until a reply threads back to it
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 } → { key: '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 (auto-prefixed per fleet, so two fleets' gates can't collide) and registers it where every member's orientation will surface it — a zero-message rendezvous
- 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 - scope: 'hive' — the same key can rendezvous across machines: each federated peer re-fires it into its own local await store
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 role is a validated CHILD of an existing one — never a copied prompt file blueprint:catalog { } → find the parent to extend blueprint:extend { parent: 'su-engineer', name: 'migration-runner', override: { mission: 'drain the migration queue', tools: { deny: ['release:deploy'] } } } // the child is validated against the parent's contract, versioned in the catalog, // and every session launched from it renders base + overlay + instance — in order
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:
Compaction — outliving the context window
The layer that makes long-running autonomy real is compaction. When a session nears its context limit:
- it flushes state — to the surfaces above, as the work happens, not as a dying gasp
- then cuts to a fresh context — that re-orients from them: 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 memory verbs behind the diagram — each store, one call memory:remember { text: 'wrangler deploys need node ≥ 25 on this box' } // recalled by MEANING facts:assert { key: 'gate-authority', body: 'plan v75 D-028 governs', ttlDays: 14 } // folded VERBATIM into every relevant orientation until 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
Sessions end; the work carries
Sessions themselves are engineered to end, restart, and carry without losing the thread.
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 550 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 the active locks and pending waiters for a workspace…', guidance: { // teaching, shipped WITH the verb when: 'Diagnostics — "why am I blocked?". Cheap, MVCC read.', notWhen: 'Before every acquire — locks:acquire already returns busy context.', seeAlso: ['locks:list (named resources, not file 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
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 is search-first; a second independent reporter promotes the SAME row; // and kind:'bug' is eligible for the auto-implement lane — the queue feeds itself
The flywheel's proof is its own queue: the PaperCusp pot inside PaperCusp carries thousands of work items of the system improving itself.
The release pipeline
- no agent holds commit rights — 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 quarantined flake files its own follow-up — nothing rots silently
What makes the gate livable at fleet scale
Two subtleties, both 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
“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: 'db/sql/0812-rework.sql' } → { positions: { committed ✓, staging ✓, main ✗, deployed ✗ }, blockedOn: 'green-checkpoint RED (2 files)', nextAction: 'fix the reds, then re-judge', changeInCandidate: { judgingContainsPath: true } } // a content check, not a timestamp guess // each hop has its own lever: git-sync:run (commit now) · // release:checkpoint-run (re-judge now) · release:deploy (ship the green pin)
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.
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, with lint and pre-call hooks watching the boundary
- feature flags default on — finished work never ships dark, with a shrink-only registry (and expiry dates) for the rare flag that must stay off
- 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
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 (with evidence) vs ? PREDICTED (run the probe first)
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.