Skip to content

Known gaps

What is unfinished, what is a deliberate trade, and what the platform will not allow.

18 min readUpdated 13 Sept 2026Reviewed 12 Sept 2026Published 12 Sept 2026/docs/internals/known-gaps

Each of these names whether it is unfinished work, a deliberate trade, a platform limit or something unverified. None of them are discoveries waiting for you. The entries marked found by audit are the ones the earlier version of this page did not have, or had wrong.

Unfinished

  • The 3D composition is a first guess. Headless Chrome rendered the office and that is how the multi-floor building, the camera framing and the floor picker were checked — and three bugs were found that way. What has not been exercised is a human eye on the composition: lighting balance, storey height and fog are all first guesses, and a screenshot suite can only tell you that something was drawn.
  • Large floors are framed rather than navigated. The earlier version of this page said there was no navigation at all, which is wrong: the canvas has orbit controls with damping, zoom from 2.5 to 60 metres, and clicking an employee frames them at a fixed close distance. What is genuinely missing is room-level navigation: no per-room camera preset, no way to jump to the server room or the meeting room, and no bookmarks. The consequence is structural rather than cosmetic. A floor is framed from a distance that grows with the building — the floor distance is the larger of a fixed 26 metres and roughly the longest side of the floor — so past a certain size an individual desk is small on screen and the only way to inspect it is to orbit and zoom by hand.
  • The block planner builds in one style. When a floor runs out of desks it grows rooms out of a kit, and module kinds are tried least-used-first so that a floor gets a varied building rather than nine copies of the same pod. The limit is geometric: every module chain is axis-aligned, the kit has no corner or diagonal piece, and growth walks outward from the core through doorways. A building therefore grows in steps and wings, never around a courtyard.
  • The office asset is regenerated, and no longer the rough one. The furniture script builds the desks, chairs, the meeting table and every named anchor, and then checks its own contract of 21 seats, 13 desks and 7 rooms, exiting non-zero on a mismatch. It is still a reconstruction rather than the asset a person made — the hand-authored original is kept in the repository — but every part is now a bevelled, smooth-shaded shape, both long elevations carry a ribbon of windows, and it draws about 84,000 triangles from roughly a hundred shared meshes rather than the two thousand it once drew from scaled cubes.
  • Found by audit: the block metadata describes two rooms that do not exist. The block sidecar declares 21 distinct room anchors; the built block file contains 19 of them. Anchor_Room_CORRIDOR and Anchor_Room_PASSAGE4 are declared by two corridor modules and are not present as nodes in the built asset, so a consumer resolving a block's declared room would come up empty for those two blocks. The impact is low — both are corridors with zero seats, so nothing can be seated in a room that is not there — but it falsifies a claim the block script's own header makes, that the geometry and the metadata cannot drift because they are emitted in the same pass. It also explains why nothing caught it: the block verifier reads seat and desk anchor nodes and never inspects a block's declared room, so a phantom room is invisible to it. The fix is either two nodes or two fewer declarations; neither is done.
  • Found by audit: repairNote is dead code. The turn prompt accepts a repair note, and renders it as a correction block telling the employee that their previous attempt failed and what went wrong. No caller ever passes one. The field is declared, the rendering is implemented, and the retry path that would use it does not exist — so a turn that fails is not retried with an explanation; it fails as a turn.
  • Found by audit, and since fixed: maxTurnsPerStage used to be a knob nothing turned. Every shipped role declares a per-stage turn budget — the values run from 2 to 4 — and for a long time nothing in the engine read it. It now does: a role that reaches its cap is skipped for the remainder of that stage, and the skip is written to the log by name with the remedy beside it, so a truncated deliberation says so rather than looking like an employee that chose not to speak. The shipped values are set so that every shipped pipeline finishes without reaching them.
  • Updates are found, not pushed. Nothing polls a marketplace on its own; the operator asks, and the plugin directory can be rescanned on demand. There is no scheduled work of any kind — no worker, no cron, no housekeeping interval: the event log's retention window is applied once at boot rather than on a timer. The process does now perform fire-and-forget work that no request asked for — probing each configured vendor for its version, fetching endpoint health lazily, connecting to MCP servers and marketplaces in the background — so "no background worker" is true of scheduled housekeeping and not of outbound work. There is no downgrade and no pinning to a specific plugin version either, so a marketplace that publishes a bad release can only be answered by disabling or removing the plugin. Version strings in manifests are shape-checked but never compared during validation, and the one comparison helper in the source is explicitly documented as not a semantic-version implementation — it treats 1.2 and 1.2.0 as equal.
  • A debate is shown as speeches. Idle employees do walk, and the two facts are separate. A debate produces one attributed speech event per speaker per round plus a closing verdict from the facilitator, each of which becomes a line in the activity feed and an entry in the transcript; nobody is sent to the meeting room for it. The only two things in the entire server that emit a movement event are hiring a role and moving someone's seat, and a re-seat still snaps the body to the new position with no route. What is new, and what an earlier revision of this page flatly denied, is a client-side locomotion layer: an employee whose status is idle gets up and walks a real route, computed on a grid sampled from the loaded floor geometry rather than from authored path data, and walks back to the desk the instant work arrives. It is a rendering behaviour, not an office record — two consoles watching the same floor agree about the work and disagree about the strolls — and it stands down under prefers-reduced-motion or from a switch in the HUD.
  • One employee status is still never set. The status talking — labelled "in a meeting" and given a colour — appears exactly once in the server, in the type declaration. Nothing assigns it. The other six (offline, idle, thinking, working, blocked, error) are real; status colour is real and useful; but a legend that promises a state the server cannot enter is promising something that does not exist. Also worth stating precisely: the floor ring and halo are drawn for the selected employee, not as a status channel, so per-status on the floor means the emissive tint and the body language.

Deliberate trades and limits

  • No authentication. Everything is unauthenticated and intended for localhost. There is no user, no session, no token and no permission model in the orchestrator. Every command on the socket — hiring, firing, changing a floor's budget, installing a plugin, submitting work — is available to anything that can open a connection. The default bind address is loopback, and that is the entire control. One thing has been tightened and it is worth stating precisely because it is easy to mistake for authentication: a request that arrives with a browser Origin header is refused with a 403 on any state-changing method unless that origin is loopback. That closes the drive-by case, where a page on any website makes your browser write to a local orchestrator. It does not close the local case, and the project says so: a token would not help, because any process on the same machine that can reach the port could read it, so the office is not a security boundary against its own machine.
  • The workspace boundary protects one project from another. It does not protect the machine from the operator. This is the entry the earlier version of this page got most wrong, so here is the real picture. There is a single path resolver, and it is stronger than a lexical check: it rejects parent-directory escapes, absolute paths outside the root, Windows drive-relative tricks, NTFS alternate data streams and reserved device names, and then resolves the real path of both the root and the target and refuses any symlink, junction or mount point it meets on the way out. That last part is a fix, not a design note — the lexical version could be defeated by a directory junction, which is not exotic on a machine with a package manager on it. It also refuses a write into the repository's own .git directory. Eight of the fifteen built-in tools call it: the ones that take a path. The rest are not equivalent to each other and should not be described as if they were:
    • think and todo_write touch no path at all — one is a scratchpad, the other writes the run's plan — so there is nothing to confine.
    • web_search and web_fetch are guarded by address rather than by path: a separate module refuses loopback, private, link-local, CGNAT and cloud-metadata addresses on every redirect hop, and caps the body. That is a real guard, and it deliberately does not defend against DNS rebinding, which the module documents as a residual.
    • run_shell is confined only by its working directory. The command is spawned through the platform shell with the workspace root as the working directory, and the shell text is never resolved against the workspace. Arbitrary shell text can read or write anywhere the process can reach, on any drive, including outside the workspace entirely. Two things mitigate it and neither contains it: the approval round trip, and a child environment scrubbed of credential-named variables.
    • git is likewise not a path-confined tool. It runs as an argument vector against the repository, so it is split by what a command can do: inspection never asks, saving work does, and destructive or hook-skipping commands are refused outright.
    • recall reads the memory ledger rather than the filesystem, and is scoped by the office's own containment rule instead of by a path.
    Two consequences follow, and both matter. First, the approval round trip is the sole control on the most dangerous tool in the system, and the auto-approve switch skips it — enabling that switch converts a supervised capability into an unsupervised one, and it now covers shell, writing git and vendor delegation together. Second, two separate pieces of the project's own writing claimed otherwise: the resolver module's header comment said every filesystem and shell tool funnels its target path through it, and the project's README said every tool was confined to the run's workspace root. The README has since been corrected to say fourteen path-taking tools; the honest number is fifteen built-in tools of which eight take paths.
  • Plugin-supplied tools are not confined by the host at all. The host wraps a plugin tool so the engine sees an ordinary tool, and what it forwards is the workspace root, the plugin's settings, an abort signal and a logger. It does not resolve paths, does not reject escapes, and does not check that a returned path is inside the workspace. The workspace root is handed to the plugin as information, not enforced as a boundary. Since a code plugin already runs in the orchestrator's process with the orchestrator's authority, this is consistent rather than surprising — but the result is that "one choke point" describes the built-in tools and not the tool list an installation actually has once a plugin is loaded. Nothing can fix this from the host side: confining somebody else's program is not a thing a wrapper does.
  • MCP tools are not confined either, and that is a deliberate trade. An MCP server is a separate program that may be a filesystem, a database or a deployment system, and its tools receive their arguments verbatim: the workspace root is never consulted. So MCP tools are granted to nobody by default — the grant variable's default is literally none — and the first call to a newly connected server asks a human once per connection. Connecting a server therefore widens nobody's reach until an operator says whose it is.
  • Found by audit: DEV3D_RUN_BUDGET_USD is read and never used. The variable is parsed into the configuration object and referenced nowhere else in the source: the only two occurrences are the field's declaration and the line that reads the environment. A run's real ceiling comes from the floor's own budget instead — an explicit per-submission amount if one was given, otherwise the floor's default per-run amount, which defaults to 5 dollars. So setting the environment variable changes nothing, and the number you set in a floor's budget settings is the one that governs. The variable is documented here as unused rather than removed, because that is its present state.
  • Found by audit: the hard budget ceiling is checked before stages, and two of the four stage modes can overshoot it. The ceiling is checked before each stage with a greater-or-equal comparison, and crossing it fails the whole run with an error naming the stage it refused to start — that part is strict and good. The gap is inside a stage. A stage is given an abort reason it can consult between turns, but only the debate and review-loop modes ever consult it. The single and parallel modes run their turns to completion, so a stage that has already spent past the ceiling will keep taking turns and can finish well beyond the limit. The overshoot is bounded by one stage, not by one turn, and parallel stages are the widest case because several employees run at the configured concurrency.
  • A turn that runs out of room is now recorded as failed, which it was not before. This entry used to describe a real trap: a model that hit its output cap mid-turn recorded an error saying the work product was incomplete while its status stayed done, no error event reached the wire, and the employee returned to idle — so run status, stage status, employee status and the absence of an error all agreed that everything was fine. Reporting it as finished hid the difference between converging and being cut off. Both cases — the output limit and the eight-round-trip tool budget — now produce a failed turn that keeps whatever it produced. If you automate on run status, reading each turn's error field is still the safer habit, but the status no longer actively lies.
  • activate() is not sandboxed. A code plugin runs in the orchestrator's process with the orchestrator's authority. Its manifest permissions are now enforced for two capabilities — registering a tool requires tools, and subscribing to the event stream requires events, each refused with a message naming the missing permission — but the permission list is still a consent record rather than a sandbox, and the entry module itself is ordinary code with the orchestrator's reach. Only install code plugins from somewhere you trust.
  • A plugin manages everything except UI code. A plugin can contribute a provider, models, skills, routing rules, tools, role templates, pipelines, settings and console panels — all as validated data, so that contribution points are wired rather than advertised. What it cannot do is ship anything that runs in the browser: panels are drawn from a closed set of widget kinds, so there is no plugin markup and no plugin script in the interface.
  • Found by audit: a plugin routing rule that names a tier does move the router's walk. The project's own release notes say a rule can reorder candidates within a tier but never move a turn to another tier, and one comment among the router's scoring constants says the same. The operative code disagrees, and so does a second comment a few hundred lines later, which says plainly that a tier-naming rule pulls that tier to the front of the walk and that this is the only way a hint can move the chosen tier. Here is what actually happens: the policy target is clamped into the role's band before hints are read, hinted tiers are then placed at the front of the walk, and walk position feeds the tier-affinity term in the score. A hinted tier outside the role's declared band is therefore not clamped, because clamping has already happened. A plugin rule naming a tier two bands above the role's ceiling can move the router there, and a rule that applies to many task classes can re-price a whole pipeline.

    Two related corrections. avoidModelIds is a score penalty of 0.12, not an exclusion: an avoided model can still be chosen if it wins on the other terms. And a rule that declares no task class applies to every task class, because the filter is an undefined-or-equal test — so a rule written casually, without a task class, has the widest possible blast radius.
  • Found by audit: the router's capability relaxation is a real degradation path with real reason strings. Capability filtering is not a hard failure. If nothing in the catalog meets the request, the router drops the tool and vision requirements and tries again; if still nothing, it drops the context requirement as well; if still nothing, it falls back to the entire catalog. Each stage records its own explanation, which ends up in the routing reason on the turn: that no model met the capability requirements and tool and vision requirements were dropped, that neither capability nor context requirements were met and both were dropped, or that no model satisfied the request and the full catalog was used. This is the honest way to degrade, and it is still a degradation: a request that asked for tool calling can be served by a model that cannot call tools, and the turn will then answer from the model alone. Nothing in the interface shouts about it; you find it by reading the reason string.
  • Direct messages carry no tools. A conversation outside a pipeline is routed on the role's default tier with the task-class mapping deliberately removed, and answers from the model alone. It cannot read the workspace, and the prompt tells the model to say so rather than claim to have read, run or written anything.
  • A plan is local to one browser. Planning sessions live in that browser's local storage, because nothing has been commissioned and so nothing belongs in the office records. The cost is that two consoles cannot collaborate on one draft, and clearing site data discards them. The stored shape carries no version field, tolerant parsing is the only migration, and there is no storage event listener — so two tabs open on the same plan are last writer wins.
  • A live panel is fetched on demand, not pushed. Opening a console triggers the first read, and the console then polls on the interval the manifest asks for, with a floor of five seconds and a default of thirty. Nothing is fetched while no console is open, and the server refuses to read more than twelve live panels at once, saying so in the response rather than pretending the panel is empty. A panel's first paint can therefore be a moment behind, and a closed console contributes nothing.
  • Prices are routing estimates. The catalog prices exist so that routing and cost reporting can be ordered plausibly. They are not billing truth, and the settings screen is where an installation corrects them.
  • A run cannot be moved between floors. The floor is frozen onto the run at submit time, which is what keeps the run record truthful after a floor is renamed or closed. The cost is one-way: closing a floor does not migrate its history anywhere.

Platform limits

  • A code plugin cannot be unloaded from memory. Node cannot unload an ES module, so disabling a code plugin withdraws its contributions and calls its deactivate hook, but leaves the module resident until the process restarts. The host says this rather than pretending otherwise.
  • The runtime floor is Node 24, and the reason is the platform, not taste. Persistence is built on Node's own SQLite module and the suites run TypeScript directly under the built-in test runner, both of which need a 24-series runtime. There is no polyfill and no native database driver to install, which is the trade: a higher runtime floor in exchange for a zero-dependency stack.
  • One suite case cannot run in a confined sandbox. The assertion that an approved shell command actually executes needs a child process with piped streams, which a confined sandbox blocks. It reports itself as skipped with that reason printed beside it rather than failing, which means a run of that suite always shows one skip. Do not read that skip as a failure, and do not read it as coverage either: on a machine that permits child processes, that case runs and has to pass.

Unverified

  • The planning page has only been verified in mock mode. The transport, the multi-turn replay, the brief handoff and the submit-to-run path were all driven end to end against the scripted provider. What has not been exercised is a real model's planning conversation: the questions a frontier model asks about an ambiguous brief, and whether the drafted brief is good enough to submit unedited.
  • No suite exercises a live provider. Every run in every test is scripted: the engine tests construct the registry in mock mode, and the provider-facing tests inject their own fetch. Real latency, real refusal behaviour, real rate limiting, real context limits and real pricing are all unexercised by the evidence on Verification. The decision logic is well covered; the wire to a vendor is not.
  • Nothing here has been tested adversarially. No suite attempts an escape, a malformed frame that is not syntactically invalid, a hostile plugin, or a concurrency race. The confinement and budget gaps listed above were found by reading the source, not by a failing test, which is a statement about the state of the evidence as much as about the code.
  • The composition has not been judged. The office has been rendered headlessly and screenshotted, and that caught real bugs in the building, the camera and the floor picker. It has not been reviewed by a person with an opinion about lighting, proportion or how a large building should feel to stand in front of.

In progress

  • The marketplace is a contract, not a website. The catalog format and the install routes are the whole integration on the orchestrator's side. The catalogue site itself is a separate concern — which is what this site is for.

Two of these are worth repeating, because they are the ones people get wrong. Nothing in the orchestrator authenticates, so it belongs on localhost — and the socket is not just a viewer: it can hire, fire, spend and install. And a code plugin runs with the orchestrator's full authority, with no runtime gate behind the permission list and no confinement of the tools it provides, so only install one from somewhere you trust.

Linked from

Did this page answer your question?