The project's README from github.com/Station-Sciences/bot-crossing. As one markdown file: README.md.
Every coding-agent thread on this machine is a little astronaut. They walk out of the ship, claim
a plot for their repo, and build something. When one needs you it stops and holds a ? over
its head; click it and the thread opens back in whichever harness it came from.
It reads the harness's own files, on your own machine. Nothing is uploaded, there is no
account, and it never writes to a harness at all — data/colony.json, where the map lives,
is the only file it writes anywhere.
Status: published as-is. I built this for myself and cannot promise to maintain it — issues and PRs are welcome but may go unanswered, and forking is an entirely reasonable thing to do. CONTRIBUTING.md sets out what to expect.
npm install && npm run dev
Needs Node 22.13 or newer. npm test runs the suite.
npm run dev is the whole thing: the API lives inside the Vite dev server, so there is no
second process. For a built version, npm start (build + serve) or npm run serve if
dist/ already exists. Binds to 127.0.0.1 by default, and answers only its own page — see
Keeping it local.
macOS, Linux and Windows. Opening a thread, revealing a folder and starting a new session
all go through a harness:// deep link handed to the OS opener — open(1) on macOS,
xdg-open on Linux, ShellExecute on Windows. The scanning half was portable already. On Linux,
where a desktop app often is not installed, the scheme is checked first and a terminal running
the harness's own CLI opens instead when nothing answers it.
A harness is whatever actually runs your threads. Bot Crossing reads each one's local session files through a small adapter, so support is per-harness and mostly a matter of somebody writing that adapter.
| Harness | Status |
|---|---|
| Claude Code (Anthropic) | ✅ Supported — desktop app and CLI, including worktrees and live-process detection |
| Codex (OpenAI) | ✅ Supported — desktop, VS Code and CLI sessions, opened through codex:// |
| OpenCode | ⬜ Not yet |
| Antigravity CLI (Google) | ⬜ Not yet — the successor to Gemini CLI, which Google stopped serving individual accounts on 18 June 2026 |
| Cursor (Anysphere) | ✅ Supported — agent transcripts; the composer/sidebar threads are not read yet |
| Amp (Sourcegraph) | ⬜ Not yet |
| Aider | ⬜ Not yet |
| Goose (Block) | ⬜ Not yet |
| Qwen Code (Alibaba) | ⬜ Not yet |
| Amazon Q Developer CLI | ⬜ Not yet |
Every harness that is installed shows up at once — the colony is the union of all of them, and an astronaut carries the name of the harness it belongs to.
One new file in server/harnesses/, one line in its index.mjs, and nothing else. The
interface is small and written down in full, along with the thread shape, the ground rules,
and how to find where a given harness keeps its sessions:
If you add one, a PR is very welcome — see CONTRIBUTING.md first, which is
honest about how much support I can offer. If landing it means editing the scanner or anything
under src/, please mention that: it means the seam needs widening, and I would rather fix that
than have you work around it.
| In the colony | In your threads |
|---|---|
| One hex zone | One repo. Bigger repos claim more tiles — one per seven threads, grown as a contiguous blob from the middle outward. A zone stays where it is: see below |
| One astronaut + one building | One session |
| How finished a building looks | How large its transcript is, on a log scale |
| Scaffolding | Somebody is at that site right now |
| Walking out of the ship | A thread that just appeared |
| Walking back into the ship | You archived it |
The map is only useful if you can learn it, so the layout is sticky. The previous arrangement is an input to the next one: a repo that still needs the same number of tiles keeps exactly the tiles it had, one that grew keeps them and claims neighbours, and one that shrank gives back whatever it claimed most recently — so growing and shrinking again returns a zone to precisely the shape it started in. Only a repo that has never been placed is placed at all, and it takes the innermost tiles still free.
A zone's origin is its root tile rather than the centre of the tiles it happens to hold,
so gaining one does not drag its buildings, its crew and its name sideways; the new tile
simply appears alongside. And the arrangement is written to data/colony.json, so the map
you have learned survives a reload — including for a repo whose last thread you archived,
which comes back to the same ground when you start a new one.
The version before this was a pure function of the thread counts: one session appearing anywhere changed the sort order, the order decided the tiles, and the whole colony re-laid itself out. A zone you were watching could jump to the far side of the map because a different repo gained a thread.
Inside the colony the terrain is gentle but not flat — it runs from about -0.3 to +0.24 on the Moon, and half again as far on Mars. A deck's top face has to sit above the roughest ground any plot can be dealt, or the ground comes through it: the slab reads as sunken, props standing on it are buried to the waist, and every surface where the two meet tears. So the slab is 0.45 tall, and everything on a plot — buildings, kerbs, clutter, boots — is measured from that one number rather than from a height of its own.
Ground scatter has to miss the plots, and the order it happens in is the whole problem: the world is built before the first roster arrives, so at that moment there are no plots to miss. Boulders and trees would end up under decks laid on top of them afterwards, poking through in fragments. The scatter is therefore rebuilt whenever a zone's footprint changes — cheap, because it no longer drags the terrain mesh along with it.
Astronaut behaviour is a strict precedence rather than a set of independent flags, so a thread can only ever be doing one thing. First match wins:
| Signal | What the astronaut does | Badge |
|---|---|---|
| Errored | Slumps, red eyes, fault light stutters | ! |
| Running now | Hammers away at its building, sparks fly | ⚒ |
| PR merged | Jumps, confetti, heart eyes | ✓ |
| Unread | Stops and waits on you | ? |
| Nothing for 3 days | Sits down and sleeps, z bubbles |
— |
| Anything else | Potters around its plot | — |
Only the states that want something from you get a badge. With most of a real thread list
sitting quiet, a symbol over every astronaut buries the one ? that actually matters.
Zone names follow the same rule: a plot shows its name only while somebody there is working, waiting or stuck. Everything else is nameless until you point at it. The plate itself is just text over a soft halo with a small accent dot — no panel, no outline.
Astronauts route rather than drift. Buildings and the landing pad are rasterised into a navigation grid whenever the roster changes, and the crew walks it with A*, string-pulled afterwards so they take the corners they actually need instead of a visible staircase.
Two guarantees, deliberately independent:
Measured over the live colony: 288 path legs, 0 crossing a building, and 0 penetrations across 78,000 agent-frames. A typical path costs 6 µs (most are a clear straight shot and skip the search); the worst frame when a poll invalidates every route at once is 0.6 ms.
They also push each other apart, so a busy plot is a crowd rather than a pile. That spacing is measured against the widest thing an astronaut wears — the helmet, at 0.95 units — because holding a crowd at less than that is a crowd standing inside itself, which is what the first version did at 0.72. Arrival is derived from the same number and is deliberately larger: an astronaut that had to get closer than its neighbours would let it could never finish arriving, and would shoulder at the crowd for as long as its thread existed.
Standing spots are placed clear of the building's own blocked radius rather than at a fixed distance from it, and checked against the navigation grid — a spot inside a wall is a spot the crew can never reach, and the astronaut sent to it walks at that wall forever. Measured over the live colony: 68 of 68 astronauts settled, nobody closer than 1.14 units, no standing spot left inside an obstacle. As a last resort an astronaut that has been blocked for six seconds adopts the ground it got to instead of pushing on.
All of the chrome is one panel on the right — the name, the counts, and every repo. There is no top bar and no strip along the bottom: a colony is a place, and a place reads better without a frame around it.
An astronaut, a zone's deck, the name plate over it, or a repo in that list — all four drill into the same repo. Picking somebody is also picking the zone they are standing on.
The repo, at the top, whether or not anybody is selected:
C) starts a fresh thread in that folder. It is the same
claude://code/new?folder=… deep link Finder's "New Claude Code Session Here" quick
action uses, so the desktop app opens an empty session with the repo as its workspace —
nothing is resumed and nothing is written.The thread, when an astronaut is selected, in a card parked beside that astronaut
rather than in the panel: its face, title, worktree, branch, model, last activity, and how
far along its building is. The answer to "what is this one doing" belongs next to the thing
you clicked, so the card follows its astronaut around the screen — preferring its right,
flipping to its left rather than sliding under the sidebar, and never leaving the window.
It is moved with a transform rather than with left/top, the one geometric change a
browser makes without touching layout, so following a walking astronaut costs nothing.
V), on a thread that is asking for you, puts its hand down. The harness only
counts a thread as read once it has been focused in its own app, so one you answered in a
terminal waves for good. This records when you looked, and the thread starts asking again the
moment it does something newer.Quiet repos fold away by default: a repo where every thread has been silent for three days comes off the map, which on a machine with a few harnesses and a lot of checkouts is most of them. The sidebar keeps a count and a way back, and a repo returns to its own ground the moment a thread in it wakes up. Hide dormant repos in settings turns it off.
Only one button in the panel is ever the accent colour: whichever action is the immediate
one. Esc steps outward a notch at a time — the thread first, then its zone.
Opening uses claude://claude.ai/epitaxy/<local_…>, which navigates the desktop app to a
thread it already has. claude://resume is the fallback for threads that only exist as a CLI
transcript: it imports the transcript, which creates a second untitled session and rewrites
the .jsonl, so it is only ever used when there is nothing to navigate to.
Archiving carries a deliberate one-writer discipline: the browser owns
data/colony.json and PUTs it whole, /api/archive only touches Claude Code's records. If
both wrote it, a save from a page holding older state would silently drop every archive made
since that page loaded. Claude Code also rewrites its session records from memory and can
stomp the flag, so the colony re-asserts it on every scan — an archive that gets stomped comes
back within one poll.
Nothing is ever written to your Claude Code data except that one isArchived field. The
folder buttons only ever hand a path to open.
The deep links above are the Claude Code adapter's business, not the colony's — another
harness plugs its own in, and a harness with no deep link simply greys the button out. See
server/harnesses/README.md.
Plots are keyed by the folder's name, which is all the colony needs to draw one, so the path
is read back off the threads standing there. Where a name is ambiguous — ~/workspaces/1/foo
and ~/workspaces/2/foo, which is what you get keeping parallel copies instead of worktrees —
it grows leftward until it is not, and you get 1/foo and 2/foo on separate ground. Only
names that actually collide change, because the name is also the key your saved layout is
stored under and disambiguating everything would move every plot on the map. A repo that has
moved or gone since the last scan fails at the server rather than handing open a dead path.
Name plates are hit-tested in screen space rather than raycast: they are billboarded in the vertex shader, so a raycast would test the quad where it was authored rather than where it ended up. That test deliberately ignores whether the plate is currently faded in — pointing at where a quiet project's name would be is exactly what makes it appear.
Navigation is Google Earth's, including both of the things that make Earth feel like Earth:
| Drag | Grabs the ground. The point under your cursor stays pinned there for the whole drag |
| Right-drag (or ⌃ / ⇧ / middle-drag) | Tilt and rotate. Up tilts toward the horizon |
| Scroll | Zooms at the cursor, not at the screen centre |
| Two fingers | Pinch to zoom, drag to pan — both anchored between your fingers |
| Arrows, + / − | Move and zoom from the keyboard |
Orbit mode (rail button, or O) |
Earth's auto-rotate: a slow sweep around whatever is centred, about two minutes a revolution. It drives the heading only, so you can keep dragging, tilting and zooming while it runs |
Both anchors are exact, not approximate: a 200px drag holds its grabbed point to 0.00 world units, and dollying 62→37 holds the cursor's point to 0.01.
Optionally, letting go can ease the angle back to the nearest clean isometric heading after a couple of seconds. Your position and zoom are never touched — going home on its own would fight you; tidying the angle after you stop does not. It is off by default, because a camera that moves when you did not ask it to is startling the first time you meet it. Turn it on under View → Return to isometric.
| Key | Does |
|---|---|
H / ⌘\ |
Hide every panel. The colony still reads: status lives above the astronauts' heads |
S |
Settings |
N |
Fly to the next astronaut waiting on you |
Enter / A |
Open / archive the selected thread |
V |
Mark the selected thread viewed, so it stops asking |
C |
New conversation in the open zone's folder |
O |
Orbit mode |
Tab |
Next planet |
L |
Next time of day |
P |
Screenshot |
0 |
Reset the view |
Esc |
Deselect, and close the zone sidebar |
? |
Help |
Three worlds — Luna, Mars, Terra — and a full day/night cycle you can scrub, let run, or set to Live, which follows this machine's own clock so the colony's light matches the light out of your window. A planet is a bag of colours and two switches; terrain, scatter, sky and lighting all read from the same preset, so a fourth world is a data change rather than a code change.
Rather than shipping an HDR environment map, the sky shader is the environment map. A
second copy of the sky dome — sharing the same uniforms, so it is always the sky you are
actually standing under — is rendered into a prefiltered radiance map with PMREMGenerator
and bound as scene.environment. That is what gives metal something to reflect and
dielectrics a directional ambient, and it is why the colony changes character through the
day rather than just changing brightness: at dusk on Mars the panels pick up the sky, on the
Moon they stay hard and neutral.
It regenerates only when the sky has actually moved, and never more than a few times a second. Measured cost: 0.16 ms/frame. Off on Potato and Low; the intensity is a slider.
Materials are properly PBR underneath it. Roughness and metalness are looked up per atlas cell, so a single merged building geometry holds painted panel, brushed metal and photovoltaic glass and each behaves correctly — the ten building recipes never had to learn about PBR.
The solar arc is tilted, so noon puts the sun 54° above the horizon and off to one side rather
than at the zenith. That is load-bearing rather than decorative: a sun directly overhead puts
N·L at zero on every vertical wall in the colony, and they go black with only ambient to
catch. The old procedural buildings were curved enough to hide it; a kit of flat-walled
modules is not.
Eye colours, lamps, windows, crop rows and plot kerbs are all authored above 1.0 so the bloom pass picks them out. The threshold is deliberately high (0.92) — only those things clear it, so lit surfaces stay crisp instead of going hazy.
The colony is built out of two CC0 asset packs by Kay Lousberg, plus the project's own shaders on top of them.
| Pack | Used for | Licence |
|---|---|---|
| KayKit : Space Base Bits | Every building, the landing pads, rovers, and the crates and drums stacked around each plot | CC0 |
| KayKit : Character Animations | The crew's body and all fifteen animation clips they play | CC0 |
| KayKit : Forest Nature Pack | Terra's trees, bushes and grass, and the boulders on every world | CC0 |
CC0 asks for nothing, but crediting Kay costs noth