Pandora's Router
The life of one lane. Five of the seven stages can stop it, and the word it stops on is the word the tool prints. Select a stage to read that half.
The life of one lane, and every point where it is refused A brief is read, its declared scope is compared against every running lane, the lane opens in its own checkout, the agent works, the close measures seven gates, the branch lands as one merge commit, and the deployed URL is read back. Five refusals leave the line and end: a brief that routes nothing, a lane queued behind an overlapping scope, an open refused because the branch name is taken, a close graded partial with the failing gate named, and a live check that returns no or returns skipped. brief what it will write routes nothing scope check compared by path queued, scopes intersect open its own checkout refused, name in use work the agent runs close gates seven, all measured partial, gate named land one merge commit live check a GET, and the body no, an old build skipped, not a pass yes, serving this build The life of one lane, and every point where it is refused A brief is read, its declared scope is compared against every running lane, the lane opens in its own checkout, the agent works, the close measures seven gates, the branch lands as one merge commit, and the deployed URL is read back. Five refusals leave the line and end: a brief that routes nothing, a lane queued behind an overlapping scope, an open refused because the branch name is in use, a close graded partial with the failing gate named, and a live check that returns no or returns skipped. brief routes nothing scope check queued, scopes intersect open refused, name in use work the agent runs close gates partial, gate named land one merge commit live check no, an old build skipped, not a pass yes, serving this build

Two agents, one codebase, no lost work

Pandora's Router runs many AI coding agents on one codebase at once. It proves their declared file scopes disjoint before any of them start, and it independently verifies what each one claims it finished.

Read the gates Source on GitHub

Most of the field solves parallel agents with isolation. Give each agent its own git worktree or its own container and they stop fighting over one working directory. That's real, and it isn't enough. Two agents in two copies can still rewrite the same file, and nothing notices until the merge, by which point both pieces of work exist and one of them has to lose.

The second half is quieter. An agent's report that it finished is the least reliable signal in the system, and it's the one almost every orchestrator takes at face value. Merged is not shipped. A green build is not a served page. A change nobody can see in a browser is indistinguishable from success until somebody looks.

Declared file scope, before dispatch

A brief declares the files it will write, on one line:

- **Touches:** `src/providers/pool.ts`, `src/providers/limits.ts`, `test/pool-test.mjs`

The allocator normalizes every declared path to one canonical form, then compares paths exactly or as directory containment on whole path segments. Never as a string prefix, which says app contains application/x and is wrong, and never by similarity. Two lanes may run in one repo only when their declared scopes have zero intersection.

Undeclared never means probably fine. A brief with no Touches: line owns its whole repository and serializes against everything in it.

Touches: none is a different claim: that lane writes no repo file, holds no writer slot, blocks nobody, and its close fails by name if it writes one. Where a scope can't be compared exactly, a mid-path glob for instance, it widens to the parent directory. Widening over-reports overlap, which costs a wait. Narrowing would under-report it, which costs somebody's work, so narrowing is never done.

Some paths are global machinery rather than lane-local files: a numbered migrations directory, a deploy-all command that ships every function in one folder, package.json. A repo can declare those exclusive. A lane touching one holds that path alone and leaves the rest of the repo open to everybody else.

The declaration is written into the ledger when the lane opens, and the in-scope gate later checks every file the branch actually touched against it.

Verification, after

An agent says it finished. The close finds out. It measures seven gates, and DONE requires all of them; anything else is PARTIAL with the failing gates named. An eighth input, the report's own STATUS word, can only lower the grade: a report that says PARTIAL in its own words is never graded DONE by gates that happened to pass.

One of those gates is the one none of the tools compared runs. Every other check asks a question about the repository, and all of them can be true while the page a person opens is last week's build. So the close issues a GET and reads what came back. It returns three values, not two:

  • yes The surface answered 200 and carried what it was supposed to carry.
  • no The surface answered and it isn't serving this build. A real red.
  • skipped Nothing was measured, for a named reason. A skip is never a pass, and the grader counts it as a failure.

No URL in policy, a named credential that is unset, a socket that never answered: each of those measured nothing. The value is the literal word skipped and the reason is always named.

Every gate, and what each one refuses.

Quickstart

Node 22 or newer. Zero npm dependencies, no build step, nothing to install before the tests run.

git clone https://github.com/hoplight-ai/pandoras-router
cd pandoras-router
npm test

Every suite prints its own assertion count as it runs, and how many assert a refusal, so deleting a guard turns them red rather than quietly widening what the tool allows. CI runs the same command on Node 22 and 24 across Linux, macOS and Windows.

Now build a throwaway workspace out of examples/. A workspace is any directory holding a _handoffs/ bridge and your repos:

mkdir -p /tmp/pandoras-demo/_handoffs/_lanes
cp examples/POLICY.md examples/PREFIXES.md examples/CLAIMS.md examples/LANES.md /tmp/pandoras-demo/_handoffs/_lanes/
cp examples/Web-CEILING1-Raise-The-Per-Provider-Cap.md /tmp/pandoras-demo/_handoffs/
PANDORAS_ROOT=/tmp/pandoras-demo node src/bin/router.mjs alloc

You get one lane card: the repo, the branch, the worktree, the dev port, the derived report filename, how that repo deploys, how a deploy gets proved, and the three files the brief declared. Every field is read from a file. None of it is inferred from the brief's wording.

Add a second brief over the same files and run it again:

cp examples/Web-CEILING1-Raise-The-Per-Provider-Cap.md /tmp/pandoras-demo/_handoffs/Web-POOL2-Same-Files.md
PANDORAS_ROOT=/tmp/pandoras-demo node src/bin/router.mjs alloc

One card now says FIRE NOW and the other says QUEUED, naming the lane it waits on and the three paths where the two scopes intersect. That refusal is the whole product.

Configuration

Four files under _handoffs/_lanes/ at your workspace root, all of them tables read by code and prose read by people. Copy them out of examples/ and edit.

FileWhat it holds
POLICY.md One row per repo: writer cap, deploy style, how a deploy is proved, the liveness URL, exclusive paths, traps. A repo with no row here routes nothing, and the allocator says so by name.
PREFIXES.md The filename vocabulary. An unrecognized lifecycle word routes nothing and is printed with the reason, rather than defaulting to live and getting executed twice.
CLAIMS.md The visible lock, one line per active lane. A clean working tree doesn't prove absence; an agent can be open and mid-read for twenty minutes before its first edit. Ships empty.
LANES.md The append-only ledger of every lane opened, landed and closed, carrying each lane's declared scope. Ships empty.

Subcommands

CommandWhat it does
allocPrint ready-to-fire lane cards. Writes nothing.
openOpen a lane from a card: worktree, branch, claim, ledger record. Creates only, deletes nothing.
closeRun the gates and grade. Measures only; --apply renames the brief and records the CLOSE.
landLand a lane on main as one merge commit, with a LAND record linking the change back to its brief and report.
claimTake or release a claim without a lane, for direct work.
applyApply a unified diff atomically, by index, so a concurrent reader never sees a half-written file.

Prior art

Most of the field solves parallel agents with isolation alone, and this project doesn't improve on that. Two of the three entries written up go further, but none did both scope-before-dispatch and verify-after, and none checked that a merged change is actually live at a URL. That gap, rather than either half on its own, is what this fills.

Bernstein does declare owned files, refuse overlapping jobs, and verify completions. It's the closest thing to this that exists, and it got there first. The differences are in where each one puts the burden of proof. Bernstein is demoting file-overlap checking to a legacy fallback in favour of an author-declared parallel_safe flag; it infers a job's file scope from the task's wording when none is declared; an empty declaration silently disables its guard; and a scope violation raises a question rather than refusing. Here, an undeclared scope is the whole repository and serializes, an unparseable scope widens rather than narrows, and a violation is a refusal with the overlapping paths named. Those are judgment calls about false positives against lost work, not a claim that one design is correct.

Agent Orchestrator never stores a status at all. It recomputes state from pull-request and CI facts every time it's asked, so a stored status can never go stale against reality. That's a cleaner idea than a ledger and worth reading for its own sake. This project keeps an append-only ledger instead, because it needs a lane's declared scope and its open timestamp recorded at the moment of dispatch, which no PR or CI fact carries.