Delroy

A local-first agent workspace built the slow way


Most agent projects are a chat box with a tool loop bolted to it. You type, a model answers, it reads a few files, and if it goes wrong you start over. That design has a ceiling, and everyone who has tried to push past it has hit the same wall: one agent, one context window, one shot. Give it something genuinely large and it reads until it runs out of room, then hands you a half-finished map and a confident summary.

Delroy is an attempt to build the thing above that ceiling — and to build it in a way that can be audited rather than admired.

It runs entirely on your machine: a local server exposing around 200 endpoints, a native desktop shell, a web workspace, a command-line interface, and — unusually — a companion app that renders onto a pair of Even G2 smart glasses. Behind all four surfaces sits one runtime: a streaming, multi-step, tool-calling agent loop that can fan itself out into a multi-stage, multi-agent workflow when the work warrants it.

It talks to seven model providers, local and hosted. It has no cloud component of its own. Nothing leaves the machine except the model call you asked for.


1. The idea that organizes everything: the effort ladder

Almost every agent tool asks you to choose a model. Delroy asks a different question — how hard should this try? — and answers everything else from that.

light      low reasoning, a single pass
standard   medium reasoning, a single pass     (the default)
deep       high reasoning, a single pass
ultra      high reasoning AND a multi-stage workflow
max        ultra, plus stages that size themselves from what they find

Five levels, one meaning, on every surface: the chat composer, a backlog task, a scheduled automation, a voice command spoken into the glasses. The level sets reasoning effort, scales your configured turn limits rather than replacing them — the settings page owns the baseline, effort only grades it — sets a wall-clock ceiling for the whole run, from twenty minutes at the lowest level to five hours at the highest, and decides whether a workflow gets built at all.

That last part is deliberately a rule, not a lookup table. Exactly one level is the threshold at which a pipeline appears. Keeping that a rule is what stops “does this surface run stages?” from becoming a question anyone has to look up — and it is why the condition lives behind a single named predicate instead of being spelled out at a dozen call sites.

This is the flavor of the whole codebase. Decisions are made once, given a name, and the reason is written down next to them.


2. Workflows the model designs and the code refuses to break

At the top two levels, Delroy stops running one agent and starts running a workflow: an ordered list of stages, each one agent doing one job, each able to read everything the stages before it produced, the last one always a gate that decides whether the run passed.

The interesting part is who designs it.

The model drafts the semantics. A pure function forces the legality. A routing prompt — which ships as an editable text file rather than as code, so its wording can be tuned without a release — asks the model to shape the workflow around what is actually being asked. Not from a menu:

  • Work that changes code needs planning, the change, a stage that runs the tests, and a review.
  • Work that only looks — an audit, a security review, a question to answer — must have no implementation stage and no test stage. There is nothing to build and nothing to run. It surveys, it analyses, it reports.
  • Work that asks why reproduces the problem first, then explains it. No fix stage unless a fix was asked for.

That last distinction sounds obvious and was learned the hard way. For a long time the workflow level silently assumed every request was a code change, so asking for a security audit produced a planning stage planning an implementation nobody wanted, and a terminal gate reviewing a diff that did not exist.

The model’s draft then passes through a materializer and a linter, both pure functions. They cannot be talked into an illegal shape. Between two and eight stages. Gates never get parallel lanes. The last stage is always a gate, and it is staffed by the reviewer rather than by whoever did the work — nobody marks their own homework. A stage may only nominate an agent that is actually installed, and membership in the installed set is the entire validation: a hallucinated name never reaches the registry at all.

Width discovered at runtime

The highest level adds the thing that makes it more than “the workflow level with a bigger budget”: a stage’s width comes from the run, not from the draft.

A drafted parallel split has to be decided by a classifier that has never seen the codebase, so it can only ever split by kind — “authentication”, “input handling”. A stage that first goes and counts the actual routers can split by the actual work.

So a stage can opt in by naming an artifact that an earlier stage will publish. That earlier stage — one that has genuinely read the repository — produces a work-list, and the later stage fans into one concurrent lane per item. The publishing stage can also nominate which agent staffs each lane, because it is the only party that knows a given slice is a rendering problem rather than a persistence one. The drafter never saw the repo. The surveyor did.

And when the work-list is larger than the lane cap, the widener refuses rather than truncating. Truncating would mean the workstreams past the cap get written by nobody, silently — a run that reports success while quietly dropping a third of the job.


3. The current frontier: ownership instead of isolation

The hardest unsolved problem in multi-agent coding is letting several agents write at once. Delroy has now been through both known answers to it, and the second one is what the project is actively building.

The obvious answer — give each lane its own git worktree — was built, shipped behind a flag, and then deleted. Each lane got its own checkout, wrote its files, committed to its own branch, and a merge step reassembled the work. It failed in a specific and instructive way: a lane that needed a sibling’s class in order to compile could not see it, so it wrote its own stub. Two lanes created the same path with genuinely different APIs, git reported an add/add conflict, the merge rolled back, and the entire run’s work was discarded. In one measured run, 44% of the total spend happened after that first merge failed, and produced exactly one file.

There was no repair available. Preferring either side of the conflict silently breaks the other lane’s code, and there is no marker that reliably identifies a stub as a stub. Prevention was the only option.

The replacement is one shared working tree plus executor-enforced file ownership. No branches, no merge, no conflicts, no rollback. Each lane declares the exact paths it may create or modify, and the tool executor refuses everything else. A lane that needs a sibling’s class opens the sibling’s real file and reads it — which fixes the stub problem at its root rather than repairing its symptom.

The details of that guard are where the engineering actually lives:

  • Two fields, not one optional field. Deriving the restriction from “a path list, or nothing” is fail-open: lose the plumbing anywhere along the chain and you get nothing, which reads as unrestricted. So the scope is a separate field from the path set. A lost list denies everything. An empty set is not the same as an absent one.
  • The guard never sees the model’s raw string. It inspects only the already-resolved, already-contained path — which makes the entire escape class (parent-directory traversal, absolute paths, symlinks pointing outward) structurally unreachable rather than merely filtered. A symlink pointing at a sibling’s file resolves to the sibling and is refused correctly.
  • Path keys are Unicode-normalized and case-folded on case-insensitive volumes — and case-insensitivity is detected by probing the filesystem, not by checking the platform name, because case-sensitive APFS volumes and case-insensitive mounts under Linux both exist, and guessing is wrong in both directions.
  • Glob patterns are refused, not interpreted. Concrete paths are what make “do these two lanes overlap?” a decidable question. With globs it isn’t, and an undecidable overlap check is exactly how two lanes come to own one file.
  • The shell is withheld, not policed. A shell invocation resolves no path, so the ownership guard cannot see it, and parsing shell for write intent is unbounded — an in-place edit, a redirect, a codegen step. So writing lanes simply don’t get a shell. The cost is real and accepted: a writing lane cannot build or test itself. What replaces it is read-only language-server diagnostics, appended automatically after every successful write, and the build belongs to the single-agent stages that follow.

The ownership vocabulary lives in its own standalone module for a layering reason spelled out at the top of the file: the partitioning side, which decides who owns what, and the enforcement side, which refuses a write, must normalize a path identically — because a set built with one spelling and checked with another is a guard that silently passes. Neither of those two modules can import the other. So the shared vocabulary sits below both.


4. What the telemetry changed

Here is the moment that best characterizes how this project is run.

After four rounds of work on parallel implementation — budgets, landing behavior, merge semantics, conflict resolution — runs at the top effort level still produced disappointing results. The natural next move is another round of fixes.

Instead, a per-turn telemetry log kept outside version control (so it survives code reverts) was analyzed across 351 recorded turns. The finding reframed everything:

  • One representative run: 33 turns, 48.3 million characters, 52 minutes — and only 8 turns changed a single file.
  • Across the last three runs, 82 million of 131 million characters went to turns that changed nothing at all.
  • The read-to-write ratio per turn was roughly 100:1 — around 330,000 input tokens against 3,000 output tokens. Lanes were burning their entire budget re-orienting themselves, then landing with a report instead of code.
  • Broken down by agent: one engineer wrote files in 13 of 31 turns. The test generator wrote files in zero of five. The code reviewer, zero of three.

The merge machinery was where the bugs lived. It was not why runs produced nothing.

Rule adopted: cost and landing being fixed is not the same as results arriving. Measure files changed per turn, not characters spent.

That measurement retired an entire architecture and chose its replacement. It also produced a downstream discovery that no amount of code reading would have found: the staffing prompt was telling the model to choose whoever best understands the piece rather than whoever would implement it — actively selecting advisors for lanes that would later be required to write.

Similar measurement work found that 8% of turns burned 58% of total spend; that context compaction was thrashing on nearly every step — over 1,500 elisions across 200 steps — because a hysteresis watermark had been set inside the wrong conditional, destroying the provider’s prompt cache each time; and that raising the conversation budget to fix it would have cost 2.5× more, not less. That last one is precisely why it was measured instead of assumed.


5. Safety as a layer, not a vibe

Delroy runs code on your machine. The permission architecture reflects that.

Five permission modes, with ask — not auto — as the default:

mode behavior
auto full access; an explicit opt-in the composer surfaces in yellow
ask mutating tools pause for approval (default)
accept-edits gates like ask but exempts file edits — “let it edit, ask before it runs things”
read no mutation
plan read-only exploration that must produce a reviewable plan

Plan mode is enforced, not suggested: an agent cannot submit a plan without having first either asked the user a question or explicitly declared the decisions it made unilaterally. The gate counts the cards actually drawn, not the intent.

Underneath, the guards are placed with a discipline that reads as scar tissue — because it is:

  • One credential blocklist, shared by every tool that touches a file. It exists as its own module because the guard was originally built inside the wrong one of two coexisting file-tool implementations — and the dispatcher preferred the other one for in-project paths. So a known in-project environment file, the single most likely credential store in any repository, went straight through. The general lesson was written down because it recurs: when one tool name has two executors, the guard belongs in a module both import, never inside one of the executors.
  • Untrusted content is fenced. Web pages, search results, external tool output and on-screen text are wrapped in sentinel markers, and forged markers are stripped from the content first, so an injection cannot spoof a source it did not come from. The module is candid about what it is: an honest mitigation, not a guarantee. The value is that the boundary is explicit, and an instruction found inside it is something the model should surface rather than obey.
  • Every shell and version-control invocation is classified before it runs. A representative bug found and fixed: a bare stash command has no sub-action, so it classified as a read — while the same command with its explicit sub-action classified as a write. The bare form bypassed all three boundaries.

And the layering rules are stated explicitly in the modules themselves. The policy layer may never import the runtime or the server. The effort module imports one dataclass and nothing else of ours. The agent runtime is dependency-injected — the caller supplies the completion callable and the tool dispatcher — so it imports nothing from the server at all. These aren’t aspirations in a README. They’re written at the top of each file with the reason attached, and the import graph obeys them.


6. Reach: the parts that don’t fit in a chat box

The runtime is the spine, but the surface area is what makes Delroy a workspace rather than a tool:

  • Agents are first-class, portable artifacts. Any agent can delegate to any other through one unified delegation tool, inheriting permission mode and approval gates. Agents can create, revise, and retool other agents. Delegation depth is a configurable number with a shared allowance ledger rather than a hardcoded fact — so an entire delegation tree draws on one budget.
  • Backlog and autopilot — a cross-project to-do list with dependencies, a fail-closed judge, per-task effort selection, and capture-from-chat.
  • Automations — scheduled prompts and workflows, where a scheduled prompt is modeled as a synthetic single-stage pipeline rather than as a second execution path.
  • Model Context Protocol support, with a local registry snapshot and a curated floor, searching in roughly twelve milliseconds.
  • Computer use — real desktop control, with its own driver layer and audit log.
  • Workflow automation integration, a template registry, and a visual pipeline builder.
  • Voice — spoken approvals with context-aware biasing and a silence gate.
  • The Even G2 glasses app — chat, effort selection, permission modes, task lists, live run mirroring, plan review and approval, and spoken answers to questions, all rendered on a monochrome heads-up display. That display can draw exactly four non-ASCII glyphs, which is why one single module is the only place in the app permitted to contain a non-ASCII character, every screen renders through one sanitizing boundary, and a test parses every other module and fails the build on a stray glyph. Cross-device mirroring is reference-counted, so stopping a run from your face stops it on your desk.

7. Why this is not a vibe-coded project

The phrase deserves a real answer rather than a protest, so here is the evidence.

The numbers. Roughly 72,000 lines of Python in the core, against roughly 52,000 lines of tests across 130 test files — 3,075 tests, currently collected. Plus 28 frontend test files and 41 glasses test files, together another 600-plus tests. Every one of the fifty-odd core modules opens with a docstring that explains what it is for and what it must never import. Just under 400 commits since mid-June 2026.

A green baseline that is defended, not declared. The suite runs at 0 failed, 0 warnings, 0 skipped — and getting there required discovering that a green baseline is only as honest as its warning filters. Round eight closed with ten consecutive clean runs while 49 resource warnings fired the entire time, invisible because a warning raised during garbage collection — exactly when an unclosed handle is reclaimed — gets downgraded by the test runner into a different, non-failing warning class. Round nine hunted all 49 to zero by bisecting the file list rather than filtering them, and adopted the rule: promoting resource warnings to errors is not by itself a leak gate, because garbage-collection-time warnings are downgraded. The release check must also assert the downgraded count is zero.

Nine documented hardening rounds, each one a multi-front audit in which every finding had to supply either a reproduction that was actually run, with real output, or an explicit instance argument naming concrete callers. Round nine’s ledger: 23 raw findings → 20 confirmed, 3 refuted, 16 deduplicated items.

The refutations are the proof the pass was real rather than ceremonial. One “resource leak” turned out to be intended design — a session-scoped singleton owning the webhooks for every deployed workflow, where the proposed stop control would have killed all of them at once. One “race condition” named a method with zero callers, guarded by a lock it didn’t know about. One finding fabricated the tool names it was about; no such tools exist. Each was written down as refuted rather than quietly dropped.

Falsification as the primary bug-finder. Every fix is verified by breaking it on purpose and confirming a test fails. The discipline pays for itself in an unexpected way:

A falsification that passes is a coverage gap, not a pass.

Three separate wirings — a cancellation signal, a contract check, and a status flag being forwarded across a module boundary — were each broken deliberately, and the entire 3,000-test suite stayed green. All three were untested seams. This single technique has now found five of them across two rounds, and it is the most reliable bug-finder in the project.

Corrections are recorded, including the expensive ones. One round’s planning document contains the sentence “an earlier draft of this document called it a production leak; that was wrong and is corrected here.” Another records: “I misdiagnosed this. Building what I first described would have changed nothing about the run.” A third records that two of its own fixes were the direct cause of a later live failure — a per-turn limit had been scaled by two, and a workflow is many turns, so multiplying a per-turn ceiling multiplied the entire run’s cost.

An ordering rule that only comes from having gotten it wrong: never ship a budget cut before the truncation-is-visible fix. Halving a step limit before stages were able to report “I stopped early” would have converted the one stage that was actually working into a silent partial success that looked like a clean pass.

A residual-risk register that names every accepted tradeoff, categorizes it — working as intended, documented fail-safe, or maintainability debt — and states the specific trigger that should reopen it. It says out loud that the main server module is too large and that two of its routes are over-coupled, with the phased extraction plan already written down separately.

A release checklist with exactly one manual gate, and the reason it must stay manual: it depends on which security header a real embedded browser engine emits, which cannot be unit-tested. Every failure mode of that check is documented as fail-closed — no header falls back to a loopback check and is allowed; a spurious one produces a loud 403 on first launch, never a silent authentication bypass. The checklist also insists that the opt-in live transport suite be run at least once per release, because a suite that has only ever skipped is not evidence of anything.

Competence is measured, not asserted. A graded evaluation harness runs 38 fixture tasks — 36 single-turn, 2 full-pipeline. Fix a failing test. Rename a constant. Answer a question about an unfamiliar codebase. Find a value buried in a huge file. Respect the stated scope. Admit what you don’t know. Refuse to touch a protected file. Survive context compaction. Each seeds a throwaway project, builds the same system prompt a deployed agent would receive, runs one real turn, then applies programmatic checks and records pass/fail, tool calls, model calls, characters sent, and wall time. The stated purpose: so that changes to prompts, truncation, compaction, or routing land with numbers instead of vibes.

The residual-risk register opens with the project’s own assessment of its own history, and it is the most honest line anywhere in the repository:

Seven hardening rounds took Delroy from vibe-coded to a client with a green baseline.

It started as one. It was not left as one.


8. Where it’s going

The near-term work is finishing what section 3 describes: making parallel writing actually produce parallel results.

The machinery is landed and passing. What remains is genuinely open, and it is worth stating precisely rather than optimistically:

  1. The model does not yet reliably publish file-level work-lists. A planning stage is asked to emit, per workstream, an identifier, a focus, and a complete list of files. When it emits nothing, the widener finds no work-list, and the implementation stage falls back to a single agent — the machinery is correct and completely inert. That demand is now a validated contract rather than a paragraph in a prompt, which routes a miss into the correction-turn machinery that already existed. If one re-ask still isn’t enough, the next lever is the wording of the demand itself: four required fields including a complete file list may simply be too much to ask for in a single artifact.
  2. A live canary is owed. Offline tests can prove the partitioning, the enforcement, and the failure semantics. They cannot prove that a model will cooperate. The pass criterion is a single number: the share of lane turns that change at least one file, currently 24%. If it doesn’t rise, the machinery is fine and exactly one prompt needs changing.
  3. Budgets need rebalancing against real workloads. In the best run to date, the survey, planning and implementation stages consumed the entire run budget between them, leaving nothing for integration, testing or QA. That run succeeded at implementation — every one of the project’s 87 tests passing — and still recorded as failed, because an exhausted ledger let the graph keep walking: sixteen further stage executions each started, found nothing left to spend, reported themselves incomplete, and routed to rework. That specific failure is fixed. The budget question behind it is not.

Beyond that: extracting the over-coupled routes along their documented plan, continuing to bring the main server module down in size, and pushing the evaluation suite from correctness toward judgment.


The shape of the thing

Delroy is a local agent workspace whose distinguishing bet is that the run should decide its own shape. Not a fixed pipeline. Not a single agent with a bigger context window. A model drafts the semantics, a pure materializer forces the legality, a stage that has actually read the code decides how wide the next stage goes and who staffs it, and the executor refuses any write outside the lane that owns the path.

The other bet is on method. Guards go in the module that both callers import. Fixes are verified by breaking them. Architectures are retired by measurement rather than by taste. Refutations get written down next to confirmations. And when a fix turns out to have caused the next failure, that goes in the document too.

Neither bet is finished. Both are legible, tested, and written down — which is the actual difference the phrase “not vibe-coded” is trying to name.


Figures are a point-in-time measurement of the working tree, not a release tag.