Back to map/Architecture Dissection

ARCHITECTURE ANALYSIS · 2026-08-18

DeepSeek Harness: An Architecture Dissection

A system that compresses the core loop of an “AI coding assistant” into 1,500 lines and turns the entire product into a 165-line config file. What’s worth learning here isn’t the AI — it’s that it turned architectural rules into something a machine can enforce.

01

What this is

DeepSeek Harness is an agent harness — the layer of software that gives a model hands and feet. The model itself only reads text and emits text; to make it edit code, run commands, or look things up, something has to feed it context, parse which tool it wants to call, actually run that call, feed the result back, log the whole thing, and figure something out when the context no longer fits. That scaffolding layer is the harness.

Most systems of this kind grow into one big main program with features bolted on as modules. This project went the opposite way: it has no main program. Even the model adapter, the tool registry, the session log, and the main loop itself are plugins on equal footing, assembled from a config file.

Composition Patch stack in config: base → profile → user config → CLI overrides The product's shape is decided here — as data, not code Each layer depends on interfaces, not implementations Boundaries Web UI · SDK · editor protocol · external tool bridges Translates internal capabilities into outside protocols Capability seams Model · filesystem · commands · sandbox · terminal · web · subagents · compaction … What each capability is, split from who provides it — 26 in all Product spine Session log Prompt assembly Tool registry Agent interface Main loop ~1500 lines Plugin framework Context · lifecycle · dependency wiring · event dispatch · config loading 3,830 lines in total, and a vendored fork of an outside project
Figure 1: the five layers. Here's the thing to notice — the main loop isn't sitting on top directing everything. It's just an ordinary member of the product spine, on the same footing as the session log and the tool registry beside it. No layer in this system qualifies as the main program.
219standalone packages
198Klines of source
231Klines of tests
~1500lines in the main loop
165config lines for a complete agent
129automated check scripts

There's more test code than source. The so-called main loop is 0.7% of all source — everything else is a replaceable plugin.

02

The verdict

This is an engineering system for turning rules into code. The AI agent is only its first product.

If you take one line away from this: what makes this project unusual is how relentlessly it promotes "team convention" into "machine enforcement" — not written down in a doc and left to good intentions, but arranged so that code violating the convention won't compile or won't pass the checks. Four core ideas hold up the whole building, and the next four sections take one each.

The price it pays is real too: mandatory boilerplate across 219 packages, a pile of abstractions that never paid off, a few places where the docs no longer match the code. I go through these one by one in Section 10, and I verified every one myself.

03

Idea One: Everything Is a Plugin

An analogy

Most software is a building: foundation, load-bearing walls, change things carefully. This project is more like a corkboard — every part pinned to it as an equal, each one labeled with what it needs and what it provides, and the framework does the matchmaking. No load-bearing walls, so any part can be pulled off and swapped out, including the one that looks most central.

The product is a config file, not a program

Problem

One codebase has to ship as several different things: a web version, a one-shot command-line task, a background service for editors, an SDK other programs call. The usual answer is a pile of if branches, or a handful of entry files, and after a while nobody can say what any given form actually has loaded.

Approach

A complete AI coding assistant is a 165-line YAML manifest — which plugins to load, and how each one is configured. At startup the system first empties the root config down to an empty list, then stacks layers on in order:

base bundle → form bundle (web/CLI) → the user's personal config → machine-level config → command-line overrides

Every layer is a patch: find a line by id, replace its config, or insert a new line. No layer is "the main config file" — they all rank the same.

Why it's
smart

What's loaded becomes data you can print, diff, and trace layer by layer. Want to know which layer brought in a given line? The inspection command reuses the same function the assembly uses — it computes a result for the first 1 layer, the first 2 layers, and so on, then diffs them pairwise. So what gets printed can't disagree with what actually runs.

One detail that's genuinely practical: a layer doesn't omit plugins from the shared base, it writes disabled: true with a stated reason. An omitted line quietly comes back the day someone reorders or merges configs; an explicit disable is a statement you can grep for, and it survives reordering.

"What I need" instead of "who goes first"

Problem

The classic plugin-system headache is startup order: A needs B, B needs C, who initializes first? A hand-ordered list breaks the moment someone cuts in line.

Approach

A plugin only declares which services it needs. The framework computes a readiness state for each one — all dependencies present, it activates; any one of them disappears, it unloads itself. Load order isn't choreographed. It falls out of the dependency declarations.

And further: that readiness state records not just whether a dependency is there, but which instance provides it. So when you swap a service for a different implementation, the plugins depending on it restart once, automatically, even though the service name never changed.

Why it's
smart

That makes hot-swapping the normal case, not a special one. Edit one file during development and only the affected subtree reloads — no process restart.

Every registration hands back a return ticket

Problem

Unloading is where plugins break: event listeners left attached, files left open, timers left running. And the unload can arrive while initialization is still mid-flight.

Approach

The framework's rule: anything that changes the world has to go through one registration interface, and it hands back a return ticket. Unloading a plugin = redeeming all of its tickets in reverse order.

Nesting is handled too: register inside another registration, and the child ticket is automatically moved off the plugin's top-level list and hung under the parent ticket — so nested cleanup unwinds along the nesting. Every step of async initialization checks whether the state has changed first, so an unload can interrupt an initialization in progress.

Why it's
smart

Cleanup stops being a matter of each plugin author's discipline and becomes one mechanism at the framework level. This project also fixed three reentrancy bugs in that part of the upstream framework — all of them the "unload arrives halfway through init" variety.

04

How plugins get wired to the agent

This is the spot people get stuck on, because it runs exactly opposite to the intuition that a main program calls into modules.

First, clear one misconception out of the way: there is no thing called the agent that "uses" plugins. The main loop is itself a plugin, on the same level as every other one. It runs because there are three fixed channels between it and the other plugins — and each of those three channels runs in a different direction. That is exactly what makes it confusing.

One · Call a service Main loop calls A service The main loop declares only four needs — a model, tools, a log, a prompt. The framework wires them up. It has no idea who provides them. Two · Register a contribution Plugin Main loop Registry registers reads Plugins put tools, prompt sections, and model adapters in; the loop reads what's registered now. Three · Intercept an event Main loop fires Plugin allow / rewrite / veto At a few fixed moments the loop asks around. A plugin can rewrite this step, or veto it outright. This is how a plugin changes behavior.
Figure 2: three channels, three directions. In the first, the main loop goes and asks for something. In the second, plugins put things in and the main loop reads passively. In the third, the main loop asks and plugins answer back. The third one matters most — it lets plugins change what the main loop does, while the main loop has no idea anyone is listening.

Dropped into one real execution

Lay those three channels over a single "model request + tool call" pass and you get the diagram below. The middle column is the skeleton the main loop hardcodes — seven steps, fixed order. The left and right columns are what plugins hang off it.

What plugins register The main loop's fixed skeleton Where plugins can step in ① Open a turn, claim the input ② Assemble prompt and tool list ③ Propose this step ④ Derive history from the log ⑤ Stream the model call ⑥ Run tool calls one by one ⑦ Append it all to the log Prompt sections persona · workspace · time · skill catalog ctx.systemPrompt Tool definitions shell · files · web · subtasks · todos ctx.tools Model adapters DeepSeek · record/replay · other vendors ctx.llm Compaction policy context nearly full → summarize a chunk Instruction injection adds the project's AGENTS.md to this step agent/pre-step Retry policy wraps the whole model call llm/stream Permissions · sandbox · hooks allow · deny · require human approval tools/pre-execute Persistence · UI rendering follows the log read-only, no writes back session/event The seven middle steps are hardcoded in the main loop, about 1500 lines; it has no idea who is in the columns beside it. Swap any box on the left or the right and the product behaves differently, without touching a line in the middle.
Figure 3: where the plugins hang in one execution. The left column runs on the "register" channel — what the main loop reads is everything registered at that moment, so giving one session its own set of tools just means registering them in that session's scope. The right column runs on the "intercept" channel — the main loop asks at fixed moments, and plugins can rewrite or even veto. The small grey text is the real name of each attachment point, so you can go grep for it later.

Three things people get wrong

Why "tool definitions" connects to both ② and ⑥

Note

The same tool registry gets read twice, for different purposes: step ② reads it to write the tools' documentation into the prompt (telling the model which tools are available), and step ⑥ reads it to actually run them.

This isn't redundancy. It's an important constraint: both reads must see the same list. Every visibility question in the project — what the prompt lists, what can be executed, what shows up in the generated SDK — goes through the same resolution function. Otherwise you get "the model was told this tool exists, and calling it says it doesn't," the kind of thing that makes a model give up on correcting itself entirely.

"Intercept" isn't a callback. It's a chain you can take over midway

Note

The plugins in the right column aren't just "being notified." Take step ③: the main loop hands over "these messages I'm about to send to the model," and each listening plugin can choose to pass it to the next one, rewrite it and pass it on, or veto the step outright. This is where the compaction policy notices the context is about to fill up and goes off to summarize first.

In web frameworks this pattern is called middleware (the onion model); the project kept the name its own framework ships with, waterfall — it didn't invent the concept.

The same plugin can apply to just one session

Note

Registration doesn't have to be global. Every live agent has its own registration scope, and a plugin can register into just one agent's scope. So things with the same name get "shadowed by the nearest one" — one session's bash tool can be a restricted version while every other session still sees the original.

That's how "a different capability set per session" is implemented: a preset config is mounted once, and each session joins it through the parent-child relationship between scopes, rather than every session mounting its own copy.

In one sentence

The main loop provides a fixed skeleton of timing (when to assemble the prompt, when to send the request, when to run tools); plugins provide everything on that skeleton (which tools exist, what the prompt says, which model to use) and every policy on that skeleton (can this one go out, can this tool run, should the result be rewritten).

So the standard answer to "add a feature" in this project isn't "change the main loop." It's "write a plugin and hang it on one of the documented attachment points."

05

Idea two: the log is the truth

An analogy

A session isn't a conversation sitting in some variable. It's a ledger you can only append to, never edit. Every time you want to ask the model something, you recompute "what the model should see" from that ledger. So "what the model sees" always equals "what's written in the ledger" — there is no second copy of the truth.

Model history is computed, not stored

Problem

If the conversation history lives in an array, then "replay a session", "fork a session", "recover from a crash" and "render the UI" each need their own logic — and sooner or later they drift apart. What the model saw stops matching what you see on screen, and that kind of bug is brutal to track down.

Approach

There is only one ledger. When it's time to send a request, you fold the ledger from the top and compute the message list. UI, replay, fork, persistence — all of them are different folds over the same ledger.

The rule fits in one line: "model-visible ⟺ logged". Anything that reaches a model request — workspace instructions, time information, the system prompt itself — has to become an entry in the ledger first. Want a new kind of model-visible input? Define a new log event first.

Why it's
smart

There's code in the project whose only job is guarding this rule: before every request it asserts that "the message list about to go out" and "the message list computed by replaying the log" are byte-for-byte identical, and throws if they aren't. That turns a slogan into a check that actually runs.

But note: I verified this — the check does not run in the product by default. It's only mounted in tests and demos. See section 10.

Compaction masks; it doesn't delete

Problem

Once a conversation gets long it has to be compacted, or it won't fit in the model's context window. But the usual approach replaces old messages with a summary — so conversation the user already read vanishes from the screen.

Approach

Don't tear pages out. Compaction appends a special entry: "for entries 12 through 47, this summary is what counts".

Then the same ledger has two readings. The model-facing one honors that masking entry and sees the summary; the human-facing one reads only the entries that "were appended in the first place" and ignores the mask entirely.

Why it's
smart

One set of data, two projections, each taking what it needs — and neither needs extra storage or syncing. Compaction is purely additive, so it stays auditable forever: you can tell exactly what got summarized away.

On an entry it doesn't recognize, refuse rather than skip

Problem

After an upgrade, what should an old build do when it reads a log written by a new one? "Skip what you don't recognize" is the most common answer, and the most dangerous — it silently restores a hollowed-out session, and the user has no idea.

Approach

The default is the opposite: an unrecognized entry type means refusing to rebuild the whole session, unless the author explicitly marked that entry as "ignorable".

Why it's
smart

Of the two failure modes, it picks the cheaper one. Forgetting the marker ⇒ over-refusal (inconvenient, but visible); the other way around ⇒ silent data loss (invisible, and irreversible). Which way the default points is itself the design.

06

Idea Three: Types Are Law

This is the part of the project I think is most worth stealing, and it has nothing to do with AI — any language with a type system can do it.

Let the return type make the composition law hold by itself

Problem

Before a tool runs, it has to clear a series of safety checks (permissions, sandbox, user policy). The classic way to write this is a middleware chain, where each link can allow, deny, or pass to the next.

But middleware chains have a hidden trap: order changes the verdict. A plugin that inserts itself at the front of the line can allow something before anyone else gets a chance to deny it. Bugs like this throw nothing, make no noise, and are usually security bugs.

Approach

The project splits safety checks into two things that live side by side:

  • Middleware chains — kept, for the things that genuinely need to wrap a call: timeouts, retries, instrumentation.
  • Guards — the new thing. Their type only lets them return a denial reason or "no objection." There is no "allow" option at all.

So composing guards is just "one no means no" over an unordered set. Whatever the registration order, whoever cuts the line, the verdict is identical.

Why it's
smart

Order-independence is normally a property you write in the docs and trust people to respect. Here it's encoded in the type signature. Try to violate it and the code won't compile. Reviewers no longer have to look for this class of problem — the compiler already looked.

It's a textbook case of promoting an invariant from a comment to a language construct. It ports straight to any permission system, validation pipeline, or policy engine.

Defaults have to land somewhere explicit

Problem

Defaults like "if no timeout was passed, use 60 seconds" usually sit scattered inside the implementation (timeout ?? 60000). The result is that nobody can say what parameters a call actually ran with, and debugging means reading the source.

Approach

Filling in defaults is promoted into a public, named conversion step: from a request (most fields optional) to a spec (every field required). The execution function's type only accepts a spec, so a half-filled request can't get in at all.

That same defaulting step also clamps the upper bounds — ask for a 10-hour timeout and it's quietly pulled down to the maximum the deployment allows, rather than throwing or being let through.

Why it's
smart

"What parameters did this call actually use" becomes a concrete value you can print, log, and assert on. The defaulting policy lives in one function instead of scattering into a dozen ??.

Write the egress allowlist as a type

Problem

The backend forwards some internal events to the browser. Which ones may cross and which may not is usually an array plus a comment — and then the forwarding code and the frontend subscription code each keep their own copy, and the two slowly drift.

Approach

The allowlist is one constant array, constrained by the type system (every entry must be an event that really exists, and must meet the requirements for being forwardable). The runtime forwarding loop and the set of legal subscription keys on the frontend are both derived from that one array.

Why it's
smart

Adding an event is a one-line change, that line is type-checked, and the two ends can't drift. The pattern works for any allowlist sitting on an inside/outside boundary.

Package structure enforced by a parser

Problem

The project got burned by this for real: one plugin file had an extra line, a default export. The loader's rule is "if there's a default export, take only the default export," so this plugin's dependency declaration was thrown out with everything else. The plugin started up in an environment with no services at all, and the service crashed the moment it hit a real editor.

At the time this package had 178 green unit tests and 100% line coverage. So why didn't they catch it? Because every test hand-built the plugin object, and the "take the default export" logic only runs in the real loader.

Approach

Two lines of defense went in after the incident. First: a check script parses every package with the TypeScript parser and forces each one to satisfy a list of structural requirements — including "no default exports." All 219 packages comply.

Second, written into the testing policy: every externally visible plugin must have a test that goes through the real loading path. Tests on hand-built objects don't count.

Why it's
smart

This is the full loop from incident to machine-enforced defense. And it yields a more general lesson along the way: line coverage proves the code ran. It doesn't prove the feature works the way it ships.

07

Idea Four: Seams

The project calls a swappable capability a seam. A seam needs all three of: an interface declaration (what the capability is), at least one implementation, and at least one consumer. Miss any one of them and it isn't a seam.

Honestly, this is just programming to an interface, expressed through npm package boundaries. The real value is the knock-on effect: the filesystem and process execution share one abstraction for "the world you execute in", so pointing them at a remote sandbox moves Bash, the terminal, and the language service along with them — you don't write a remote version of each feature separately.

Sandboxing: containment is reported, not assumed

Problem

Process isolation differs a lot across operating systems. Assuming "I called the sandbox, so I'm safe now" turns into a dangerous illusion on some platforms.

Approach

The sandbox interface has exactly one method: hand over the command string you're about to run, get back a replacement command string plus three facts:

  • Containment level: full or partial. "Partial" is a state that really occurs — on Windows, hard links let one file be reached from several paths, so it's statically classified as partial; the Linux kernel-level backend probes for itself, based on the kernel version it actually negotiated.
  • That backend's own denial wording. Different sandboxes print different error text when they refuse, so the caller matches only the wording returned by the backend actually in use this time — taking the union across all backends would claim denials some backends can't even produce.
  • How to tell "the sandbox itself broke" from "the command was denied".
Why it's
clever

That last one comes from a real incident: the Linux kernel sandbox prints a line of notice when it can only contain partially, and that line got misread as "the child process failed to run". The fix was to list it explicitly as informational output, and filter it out before deciding anything is fatal.

The more general principle: "the infrastructure broke" has to be decided before "the operation was denied", because the former means the operation never happened at all. To the caller, those two mean completely different things.

A seam gets no escape hatch

Problem

When you wrap a complex protocol — the Language Server Protocol, say — the path of least effort is "expose a few common operations, plus one generic raw-call interface just in case".

Approach

The language-service seam exposes four operations: go to definition, find references, go to implementation, hover info. The result types are a closed set of two. No raw protocol calls, no process control, no document management. Want a fifth operation? You change the interface, every implementation, and every consumer at once, and the compiler watches you until you're done.

Why it's
clever

The moment that just-in-case escape hatch exists, the seam is a seam in name only — everyone routes around the normalized interface and uses it directly, and a few months later you can't swap the implementation anymore. Better narrow and closed.

But the abstraction only half pays off

The project declares 26 seams in total. I counted: 14 of them have exactly one implementation, and 1 has no implementation in the repo at all. So 58% of the seams have never actually been swapped — and by the project's own rule ("every abstraction needs a current owner and a current need"), those are speculative abstractions.

The ones that actually paid for themselves: subtask delegation (6 implementations, covering in-process fork, in-process fresh start, and shelling out to the external Claude Code / Codex CLIs), web search (4), model adaptation / command execution / filesystem (3 each).

08

How the loop actually runs

Strip away the plugin shell and the real working loop has three layers, with clean terminology:

  1. Turn — one pass of "drain the pending inputs". A user message opens a turn, and the turn does not end until nothing is left outstanding.
  2. Step — one model request, plus every tool call that request sets off. A turn contains zero or more steps (the model saying "let me look at the file first" is one step; answering after it has looked is another).
  3. Round — the outer strategy iteration, like "let a fresh agent try again". Rounds belong to the strategy. They are not the same thing as turns.

Three words, three referents, never used interchangeably. It looks like a small thing. But in a system with subtasks, background tasks, and goal tracking, if you cannot say clearly which execution this one counts as, timeouts, retries, and billing all go wrong.

Tools run concurrently, results commit in the order the model expects

Problem

The model may ask for five tool calls at once. Running them serially is too slow; running them in parallel scrambles the order of the results — and the model reads those results in the order it listed them.

What they do

Dispatch can overlap, but commits are strictly ordered: the scheduler only advances along the run of calls that have finished contiguously. If the third one finishes first, it still waits on the first two.

Two extra touches: before each next call is started, re-decide whether it may run concurrently — so a tool registry that changes mid-run forms a barrier against calls not yet started; and on interruption, write a synthetic error result for the calls that never got to run, so that stretch of log still replays complete (every call has a matching result).

Why it's
smart

Execution order and presentation order are fully decoupled. The pattern holds anywhere you need to present a deterministic order outward while running concurrently inside.

Code Mode: turn every tool into one SDK document

Problem

Every tool call costs the model a round trip: it emits the call request → the system executes it → the result is stuffed back into the context → ask the model again. Ten things means ten round trips, and both the token cost and the latency add up.

What they do

Change the presentation: the tool list handed to the model holds exactly one tool, called "run code", while the system prompt carries the TypeScript (or Python) interface declarations for every tool. The model writes a program and gets all ten things done in one run.

The key is that each tool call inside that program still flows back through the same safety pipeline — permissions, sandbox, approvals, none of them dropped, and every sub-call is logged. But only the final output, curated by the model itself, enters the conversation history.

Why it's
smart

It does not open a side channel around the safety checks for the sake of performance — the usual way this kind of optimization goes bad. The policy layer is reused whole; the only thing that changes is the form the tools take in front of the model.

Cancellation has three sources, fused into one

Problem

A run can need to abort for three reasons: the user hit stop, this plugin got unloaded, the whole system is shutting down. Handle them separately and you miss the combinations, especially "unloaded halfway through initialization".

What they do

The three sources fuse into a single cancellation signal, registered before any resource is created, paired with a reverse-order teardown (which runs exactly once even under concurrent calls).

Why it's
smart

"Create the resource first, wire the cleanup second" is a common source of leaks — whatever goes wrong in that window has nobody to handle it. Here the order is flipped: the cleanup is in place first, even when it has nothing to clean up yet.

Orthogonal outcomes are reported independently

Problem

A command can be "timed out" and "exit code 0" at the same time — because it caught the termination signal and then exited normally. Nest the "timed out" flag inside the "failed" branch and the caller reads a run that was cut in half as a clean success.

What they do

Timed out or not, which signal arrived, exit code — each gets its own field, and none of them is nested inside another's branch.

Why it's
smart

The project wrote this one into a list called "defensive patterns" — every entry in it is a class of bug that actually happened, written up as a rule to keep it from coming back. Because the entries are earned rather than brainstormed, the list stays short, which means people actually read it.

09

The Engineering System

This part has nothing to do with AI, but it may be the most portable asset in the whole project. The core idea in one line: nearly every rule written into the contributor guidelines has a script that mechanically enforces it; and those scripts are real code with their own unit tests.

The generator doubles as the staleness checker

Problem

Docs generated from code — API catalogs, dependency graphs, config listings — always go stale. So you write a checker to verify they are current, and then the checker itself slowly drifts from the generator, and you get "the check passes but the file is wrong."

Approach

Don't write a second program. Checking = the same generator with one extra flag, regenerating in memory and comparing byte for byte against the file in the repo.

Why it's
clever

"Check" and "generate" cannot disagree, by construction. That's a structural move that deletes a whole class of bugs, not a patch on one.

Kind-dispatched checks must fail closed

Problem

A check like "every public function needs a doc comment" has to walk every form an export can take in the code. The language ships an upgrade, a new syntax appears, the checker doesn't recognize it and falls into the default branch — and waves it through. From that moment it has degraded into sampling, and nobody finds out.

Approach

Hit a construct you don't recognize and throw, with an error that says: I don't know what this is, come extend me.

Why it's
clever

The entire value of an exhaustiveness check is that one guarantee: "nothing unchecked can exist." A branch that silently passes pulls the guarantee out from under you while you have no idea.

The real log is the test fixture

Problem

Testing an AI agent is expensive: every run calls the real model, slow and nondeterministic. So you want record-and-replay — which usually means inventing a recording format.

Approach

Don't invent a format. The session log itself is the replay material. Recording = run it for real once and keep the log file.

Three lessons come with it. When something can't be derived from the log, fail loudly and name the fix — no quiet degradation. At the end of a test, assert that the recording was fully consumed (a test that makes fewer model calls still passes every output comparison; the transcript just gets shorter). And for big, frequently-changing content like the system prompt, compare it verbatim in exactly one scenario and compare reconstructions everywhere else — otherwise one prompt edit churns hundreds of expectation files, and review turns into a rubber stamp.

Why it's
clever

A fixture can't describe a run the system could never actually produce — because the system produced it. And the same file doubles as the diff a human reads at review time.

Decision records: 1372 of them, format checked by machine

Problem

"Why was it designed this way" is the first knowledge to evaporate. Design docs either never get written, or go stale the moment they're finished and start misleading people.

Approach

Every non-trivial change ships a decision record in the same PR. The taxonomy is encoded in the file path (status/category/date-title), and both dimensions are closed in code — want a new category, you edit the code.

Three hard rules: "alternatives considered" is a required section; a central index file is explicitly forbidden (that kind of file is guaranteed to go stale); finished records move into a cryptographically sealed archive that every doc check skips, with a stated rule that an archived record is never authority for current behavior.

Why it's
clever

"A decision that doesn't record what it beat is an invitation to re-litigate it" — making alternatives mandatory goes straight at the most useless way to write a design doc.

And "freeze the archive, and say out loud that it isn't authoritative" solves a different problem: an old design record is useful as history and toxic as authority. Treat the two separately, and make the distinction enforceable.

10

What to Steal

Steal outrightHolds at any scale, in any language, and costs almost nothing
Let the return type carry the composition law

If a policy hook can only return "deny" or "abstain", composition is order-independent. Put that property in the type instead of the docs, and let the compiler guarantee it rather than the reviewer.

Defaulting has to happen explicitly

Promote scattered ?? default into one public conversion: from a "mostly optional request" to a "fully required spec", with the run function accepting only the latter.

Decouple execution order from presentation order

Run concurrently, commit in the order the caller expects. On interruption, backfill synthetic results for the parts that never ran, so the record stays complete.

Append-only log, multiple projections

Don't delete history to compact it. Append a masking record, so the "machine view" and the "human view" become two readings of the same data.

When you read something you don't recognize, refuse

Between over-refusing and silently dropping data, default to the first — the second is invisible and irreversible.

Report orthogonal outcomes independently

Timeout, signal, and exit code each get their own field, never nested. Otherwise the caller reads a run that was cut in half as a clean success.

Infrastructure failure outranks a business denial

"The lock is broken" and "you don't have permission" mean completely different things to the caller, so the first one has to be decided first.

The generator doubles as the staleness checker

Add a flag that regenerates in memory and compares byte for byte. Never write a separate checker for generated output.

Type-dispatched checks fail closed

Error out on a construct you don't recognize. Passing it through silently quietly downgrades the check to sampling on the day the language gets an upgrade.

Real output is the test case

Don't invent a second recording format; and assert that the recording is consumed in full.

Pin large, frequently changing content in exactly one place

Everywhere else, rebuild and compare. Otherwise one change churns hundreds of expected files, and review inevitably becomes a rubber stamp.

Justify every coverage exclusion, one by one

And next to numbers like concurrency caps and retry counts, write down the failure mode they prevent — otherwise the next person will "optimize" it away.

Make decision records require an "alternatives" section

Encode the category in the path, ban a central index, freeze the archive and state explicitly that it is not current authority.

Override, don't omit

Switch it off explicitly in the shared base and say why. An omitted line comes back quietly on the day someone reorders the config.

If it doesn't fit, error — don't truncate structured data

Truncating a JSON snapshot produces input that looks valid and is actually broken.

Leave no general-purpose escape hatch in a seam

Better to expose only four closed operations. Leave one "raw call" escape hatch and a few months later you can't swap the implementation any more.

Only pays off at scaleEarns its keep only with multiple deployment shapes, multiple teams, or a long lifespan
Everything is a plugin, composed as a patch stack

The product shape becomes data. That assumes you actually have several shapes to maintain. With only one shape, this layer of indirection is pure cost.

The three-role capability seam

Do it only for capabilities whose implementation really does get swapped. 15 of this project's 26 seams never were — by its own rules, those are speculative abstractions.

Typed RPC generated at build time

Each side generates its own validators, and no type information goes over the wire. Big payoff, but it nails the build into a fixed sequence you can't rearrange.

Runtime architecture assertions

What's worth stealing is the assertion itself — what the model sees must equal what replaying the log produces. What isn't worth stealing is giving two hundred packages an assertion slot each.

100% coverage per file

Better than a repo-wide percentage (a big file can't subsidize a small one), but it only holds if you're willing to write a reason for every exclusion.

Don't stealThis project is still paying for these itself
Making every composable unit a published package

17 packages have under 100 lines of source. The entire runtime of the "persona" package is one function call; "tool render mode" is 6 lines. Naming a session takes 4 packages and 1408 lines, two of which are byte-for-byte identical apart from one anonymous function.

Full bilingual doc pairing

1078 sidecar files just for validation, three files to touch for every doc change, and the checker itself admits it "cannot tell whether the two sides are really saying the same thing".

Mandatory boilerplate plus duplication detection switched off

185 files are marked "ignore for duplication detection" — code that had to be copied to satisfy an ownership check then demands that duplication detection be turned off. That signal contradicts itself.

11

Costs and Weak Spots

I ran the commands myself to check every item below. None of this is "the architecture is bad" — it's the real bill this architecture runs up, plus a few places where the docs no longer match the code.

Those nice runtime assertions don't run in the product by default

Every one of the 219 packages has a slot for an assertions module. But 184 of them (84%) are empty — carrying a single line that says "this package has no runtime invariants, because…"; only 35 actually contain checks.

More to the point: I couldn't find the assertion system loaded in any shipped configuration. It runs only in unit tests (auto-mounted through a clever but fragile framework-level injection) and in one demo program (which mounts 4). This is a test-time mechanism, not a production-time defense.

What the "every package must have a slot" check actually buys you is that every package had to think once about whether anything was worth asserting — not two hundred lines of defense.

The flagship relationship diagram isn't really generated from the code

The project ships a "capability seam overview" under a banner reading "generated by script, do not edit by hand." But its contents come from a hand-written 56-row data array inside the generator script. The only machine check compares whether the set of service names lines up; who implements each one and who uses it is never checked.

Measured drift: of the 130 package names this table references, 9 don't exist at all (some are renames nobody followed through on, some are invented out of thin air). The byte-for-byte "staleness check" can't see any of it — because the data lives in the script itself.

To be fair, a note at the end of the doc does say "services are discovered from the code, roles are classified by hand in the script." It isn't lying. But the inference "generated, therefore provably true" doesn't hold for this file.

The browser-side backend is a monolith you can't extend

There's a 3744-line hand-written file — the largest runtime file in the repo, depending directly on 27 internal packages, with one function whose returned object literal alone holds about 1700 lines of methods.

There's no registration mechanism here: any capability that wants to be callable from the browser has to go edit that function. That's the exact inverse of the "everything goes through registration" pattern in the rest of the project — and it also happens to sit on the coverage check's exclusion list.

There's a package dependency cycle, and the generated dependency graph can't see it by construction

I measured a four-package cycle. The script that generates the dependency graph reads only one kind of dependency declaration (and the docs call that "the authoritative signal for runtime dependencies"), so 1089 edges make it into the graph and another 205 edges never enter it at all — and the two edges that close this cycle are among those 205. Nowhere in the code-health checks is there any cycle detection.

The denominator behind "100% per-file coverage" is curated

The exclusion list keeps roughly 23% of the lines out of the count, including the entire "let the AI modify its own runtime" subsystem (15,000 lines) and the 3744-line monolith above.

One detail says more than the number does: an explanatory comment sits on an exclusion whose path doesn't exist at all — when the package group was renamed, new exclusion lines were added, but the comment stayed next to the old path. So the two largest exclusions that actually bite look like they were written with no reason at all.

Three places where the docs no longer match the code

  • The repo layout map (the one every contributor reads before starting) lists a directory name that doesn't exist, while the package group that does exist never shows up in the map at all. The same wrong name was written into the test config too, where it became an exclusion that will never match anything.
  • The "pre-release stance" section says "remove this section at the first tagged release" and "there are no external consumers." But the repo already has release tags, and all 219 packages are configured to publish publicly. This expired section is still directing everyone's compatibility decisions.
  • A formatting rule claimed to be guarded by a Git hook (exactly one trailing newline at end of file) — I tested it, and that hook doesn't check this case. The codebase really does have no violations right now. That's discipline, not that check.

"Misconfiguration fails loud" has an exception

The spec says "misconfiguration fails loud at load, never silently skip a missing referent." But the config-overlay implementation handles all four kinds of "target not found" by emitting a warning and skipping — and the parameter that collects those warnings defaults to an empty function, which the CLI launch path never passes.

The result: one typo'd id in a user config is a silent no-op — the same class of failure as that "filesystem plugin was permanently switched off" incident. (No shipped configuration has a dangling target today, so this is latent, not live.)

Deflating the vocabulary

In fairness, two terms that sound inventive are ordinary practice:

"waterfall" wasn't invented here — it's the dispatch mode that ships with the framework the project uses, and its semantics are standard onion-style middleware. "capability seam" reads as ports and adapters, just expressed through package boundaries, and across every check script I couldn't find a single one enforcing that three-role structure — it's a convention that gets named and drawn, not a mechanism a machine enforces.

What is genuinely unusual is the thing sitting right next to it: the type-level monotonic guard, and a carefully built scoped event routing mechanism.

One more bill, this one purely about volume: the check scripts are about 28,600 lines of code themselves, which makes them software with their own bugs; the contributor spec carries a standing budget of 1900 words of body text; and one routine change may have to touch a decision record, a Chinese counterpart, a paired validation file, a package README, a block of pasted types, and a regenerated catalog, all at once.

12

A note on confidence

The conclusions in this report rest on three kinds of evidence, and they do not all deserve the same trust:

SourceConfidenceWhat it covers
Implementation files I read in full myselfHighThe three main-loop files, the session log's projection mechanism, the scoping mechanism, the tool registry's events and types, the command-execution seam, the assertion system and the script that enforces it, assorted config files, the framework's list of local modifications, four incident postmortems, the documentation standards and the decision-record standards
Parallel deep-read subtasksMediumFramework core and loader, the model seam, the filesystem, the sandbox, subtask orchestration, prompt assembly and compaction, typed RPC, composition and self-modification, the engineering system. I checked the key claims; I did not re-verify every citation one by one
Adversarial review, then verified separately by meHighEvery item in Section 10 came out this way: raised by the review, then confirmed by commands I ran myself

Coverage. Of the 11 deep-read tasks in the first round, 7 were lost to network drops. Split into smaller scopes and re-run, all 11/11 finished. 15 subsystems covered in total.

Two places where I drew no conclusion: the subtask "persistent session" mechanism (a 1483-line file whose body the deep read explicitly skipped); and the 70,000 lines of web frontend (I read only the architecture doc). Wherever the report touches those two, it stays deliberately hedged.

Known defects in this report: one subtask reported line numbers that were off — the lines it cites run past the actual end of that file, so the mechanism description is right but the citation isn't trustworthy; another took the "total file count" of the decision records as the "active count". Counts like these I re-counted for the main text.

13

Code index

If you're going to open the code and read along, here's where each section of this piece points. Expand to see.

The core loop and the session log

packages/core/agent-loop/src/index.ts (lifecycle, and the merge of three cancellation sources), agent.ts (the turn/step driver; line 341 is the "single source of model history" call), tool-calls.ts (concurrent scheduling, in-order submission), invariant.ts (the "model-visible ⟺ logged" assertion).

packages/core/session/src/surface.ts — the two projections, append and mask. types.ts lines 405-422 are the contract that says: read a record you don't recognize, refuse it.

Types as law: the three places

The monotonic guard: packages/core/tools/src/index.ts lines 703-711 (the type definition and the comment that explains it), lines 1117-1128 (the composition logic).

Request/spec split: packages/shell/shell/src/index.ts lines 84-99.

The typed egress allowlist: packages/api/remotes/src/remote-events.ts.

The package-structure AST check: scripts/package-invariants.ts.

The plugin framework (vendored)

vendor/cordis/src/reflect.ts (service resolution: it only looks "upward", which explains the second bug in that export incident), fiber.ts (lifecycle, return tickets, readiness), events.ts (the four dispatch modes).

vendor/loader/src/ and vendor/include/src/ — config to plugin tree, the patch algorithm, and the evaluation scope of !!js expressions.

vendor/README.md — 18 numbered local modifications, several of them real fixes to lifecycle and reentrancy problems in the upstream framework.

Seams and the execution world

docs/capability-seams.md — the table of all 26 seams (with the caveat from section 10: the data in that table is hand-written).

packages/sandbox/, packages/fs/, packages/shell/, packages/lsp/ — sandbox, filesystem, command execution, language services, in that order.

The engineering system

scripts/ — 129 scripts. run-gates.ts is the scheduler, verify-* are the checks, gen-* are the generators (add --check and a generator turns into a staleness check).

.agents/notes/ — 1372 decision records, plus the spec for writing them (README.md).

docs/postmortem/ — four incident postmortems; 0001 (default exports) and 0002 (config expressions) are the two this piece keeps coming back to.

docs/defensive-patterns.md — the list where every entry is a real bug.