Your infrastructure as a colony on Mars.
Every project in production, every repository and every Stripe account exists as a module you can walk up to. When something falls over, you see it from the far side of the valley. The data is real: it comes off your own machine and never travels anywhere.

pnpm install
cp data/projects.example.json data/projects.json # your inventory; gitignored
pnpm start # collect, then open the colony
pnpm test # no network
pnpm start is pnpm run collect && pnpm run dev. Node 22 or newer.
The colony draws your machine: the projects you list, the repos under
~/Sites, the gh and stripe sessions already logged in. With none of those
yet, the world is a small colony over whatever it could read — every module
that does appear is real.
| Habitat dome | A project in production. Cyan is answering, magenta is a warning, a white strobe is down |
| Greenhouse | A repository. Warnings are open issues and PRs |
| Ice extractor | A Stripe account |
| Workbench | A repo that only exists on this disk, with no remote to speak for it |
| Civic avenue | What HQ knows about itself: last check per source, headline figures, power as data freshness |
| Quarry | Work in local — the commits that haven't left your machine |
| Terraforming plant | How far the MRR is from the goal you set. The valley greens as that figure climbs, and at 0 the world is bare Mars, which is the honest reading |
| Rover | Drive it with C. Boost included |
Not one built volume is without data behind it. The full vocabulary — what each
piece stands for and where it comes from — lives in
docs/COLONIA.md; the visual inventory opens at
/catalog.html.
| Drag | Pan the map |
| Right button | Orbit |
| Wheel | Zoom to the cursor |
WASD / arrows | Move, relative to the view |
Q E | Turn · R Shift+F tilt · 0 or F frames the colony |
| Double click | Travel to that spot |
C | Drive the rover · Space boost · Escape let go |
L | Every name at once · also the toggle beside the search field |
P | Table panel |
G | Force a collection now |
| Search field | Jump to any module, civic building or sector by name |
| Toggle beside it | Turns on the name of every single thing, spaced so none covers another |
There is no toolbar on purpose: the world takes the whole screen and the scheduler collects by itself. What chrome there is has a job — the search field reaches a module without hunting for it, and the activity log flies you to whatever just happened. A payment you click gets celebrated and never shown again; anything bad opens the module's card instead.
| Source | Every | How |
|---|---|---|
| Production | 60 s | A real HTTP request to each domain in data/projects.json |
| Local repos | 5 min | Reading git off disk |
| GitHub | 15 min | The gh CLI session |
| Stripe | 60 min | Stripe CLI profiles, in live mode |
| Certificates | 12 h | TLS handshake |
Those rhythms are measured, not guessed, and the reasoning for each one is
written down in src/lib/sources.ts. The important one:
the limit that actually binds on Stripe is not the 100 requests per second,
it's the monthly read allowance, 10,000 a month. Polling every five minutes
burned 432 % of it.
A source failing never wipes its data: the last valid value is kept and flagged stale.
TLS_WARN_DAYS in src/lib/sources.ts.mcp/server.mjs is HQ's board. It decides nothing. It says
what is pending and lets whichever agent connects decide what to do about it —
like a board on the wall of a base: the list of breakdowns is in plain sight and
whoever walks past takes one.
Ten tools, over stdio:
| Tool | What it does |
|---|---|
pending_work | Everything asking for attention, sorted by urgency: services down, certificates about to expire, open issues and PRs. Items someone has already claimed are marked, not hidden |
colony_status | Module counts, what's down, when each source was last collected, who is working right now |
issue_detail | Open issues of a repo, with body and author |
pr_detail | Open PRs of a repo, with the changed lines of the diff |
comment | Comment on an issue or PR |
close_issue | Close an issue, with the reason posted as a comment first |
merge_pr | Squash-merge a PR and delete the branch |
propose_work | Put something on the board no collector found. Nobody can take it until a person approves it with pnpm board |
claim_work | Write down that you're taking something on |
finish_work | Close a job you took, saying how it went |
What it can touch is the safety model. On GitHub it can comment, close and merge — all things that undo: an issue reopens, a comment gets deleted, a merge gets reverted. There is nothing here that restarts a server, renews a certificate or touches production, because that doesn't undo. Safety isn't the agent behaving well; it's which tools exist. More gets added deliberately and one at a time.
claim_work and finish_work write to data/agents.json. The collector reads
that file into the snapshot, and the rover hangar shows it: who is out on
the surface and on what. That's how the colony can say "one agent out there"
without lying — because there is one, and the board is what proves it.
Any MCP client speaks to it the same way: node mcp/server.mjs over stdio,
with HQ_HOME pointing at this repo. It needs a snapshot to read (pnpm run collect at least once) and the gh CLI signed in for the GitHub tools.
Claude Code, one line:
claude mcp add hq-colony -e HQ_HOME=/path/to/hq -- node /path/to/hq/mcp/server.mjs
Claude Desktop, Cursor, or any client with a JSON config — copy
mcp.json.example and fix the paths:
{
"mcpServers": {
"hq-colony": {
"command": "node",
"args": ["/path/to/hq/mcp/server.mjs"],
"env": { "HQ_HOME": "/path/to/hq" }
}
}
}
A scheduled agent (Hermes or similar) is how this repo actually runs it:
ops/hermes/ collects on a cadence, compares the signals against
what an agent has already answered for, and only wakes the model when there is
new work. Empty output costs nothing.
Reads go through the same gh session as the collector, so there's no key in
here either.
What a connected agent has already done, from data/agents.json: merged four
Dependabot PRs on cloudstudio/laravel-html-crawler (two of them only after the
token got the workflow scope it was missing), triaged two Nova 4 issues on
cloudstudio/resource-generator and cloudstudio/nova-card-extended, and found
the TLS threshold bug above — workee.es and a client's site had been flagged
for weeks and both were renewing right on time.
?publicOpen the colony with ?public and you get the real snapshot with the parts
that aren't yours to show taken out (src/lib/redact.ts):
"private": true in data/projects.json disappear, along with
the projects in them;localRepos, without this disk's
paths or the last commit message;Everything else stays: your domains, your repos with their real stars, the service that is actually down right now. This is for recording and screenshots. It is deliberately not a made-up world — a fabricated map would prove nothing about the map being real, which is the whole point of the project.
The dev server is for working on the colony; server/serve.mjs is for living
in it. It serves the build and the same API the Vite plugin serves — one
implementation, in server/api.mjs, because two copies of it would drift the
first week.
pnpm run build
node server/serve.mjs # http://localhost:5199
A LaunchAgent starts it at login and brings it back if it dies, so the colony is up after a reboot without anyone remembering to start it:
cp ops/es.cloudstudio.hq.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/es.cloudstudio.hq.plist
The plist sets PATH on purpose. launchd hands a job /usr/bin:/bin and
nothing else, and the collectors shell out to gh, stripe and git. The
scheduler rounds learned the same lesson the hard way: one of them ran for ten
minutes collecting nothing, comparing nothing and waking the model anyway, and
exited 0 so everything upstream believed it had worked.
A round that has missed three of its own turns is not late, it is broken, and
the hangar says so. This exists because of a real morning: Hermes kept a stale
module in memory across an update, its scheduler threw on every tick, and
hermes cron status went on reporting that jobs would fire automatically. It
took someone asking to find out. The one thing worse than a watchman asleep is
a watchman asleep reporting that all is well.
There is no key in this repository. It uses the sessions already on your machine:
gh CLI, already authenticated.stripe itself.data/snapshot.json is in .gitignore on purpose: it holds local paths,
identities and figures. It lives in data/, outside Vite's public/, so a
build can never freeze a copy of it into dist/. data/projects.json is
gitignored for the same reason — it names your clients and your hosts; the repo
ships data/projects.example.json instead.
data/projects.json is the single source of truth for what's in production, and
it's edited by hand. Everything else fills itself in on each collection.
{
"id": "new",
"name": "New",
"url": "https://new.com",
"repo": "cloudstudio/new",
"area": "products",
"stack": ["Laravel 12"],
"host": "forge:cloudstudio",
"stripe": null
}
Each area is its own sector in the colony. It appears on its own as soon as it
has at least one project.
The local collector reads every git repo directly under ~/Sites for the
aggregate figures the quarry and workshop show. Getting a module of your own
in the Workbench sector is opt-in: name the directory in localRepos at the
bottom of the same file.
"localRepos": ["hq"]
The whole directory is deliberately not turned into modules. There is client work in there with no business on a map that gets shown.
HQ is very good at reporting what is happening and would be useless without a way of saying how far along you are. That is one fraction: the MRR the products collect, against a monthly goal you chose.
"mrrGoal": 3000
One number in data/projects.json, typed by a person; the MRR is collected
from Stripe. The fraction between them is what the terraforming plant reads and
what greens the valley: nothing at 0, a green valley at 1. No goal set means no
gauge — rather than a gauge reading zero against a target nobody chose.
pnpm test
Two hundred and some, in test/, and none of them touch the network. They
cover the parts where being wrong has a cost outside the screen:
redact | what ?public must never let through: a client's name, a private domain, a payer, a server IP, a path off this disk, a word an agent wrote |
sources | the certificate threshold and the refresh rhythms, including the Stripe read allowance |
events | a refund is not a sale; a sale ages out of the log; a stale reading is not an outage; a new account's history is not today's news |
entities | one built volume per real thing, and the state each one is in |
stripe | quantity multiplies the price, and an account is not the product billing through it |
merge | a partial reading keeps the last valid value and never becomes a zero; euros and dollars are never one total |
mcp | the board over stdio, and a claim letting go of itself |
board | one reader and one writer for the board, atomic, shared by all three processes |
local | real git repos in a tmpdir: no remote, no commits, not a repo at all |
sanitize | the form cannot corrupt the inventory, and hand-typed config rides through a save |
labels | that turning every name on never puts one label on top of another |
bake | a pivot flagged keep survives the merge whole, role and joint included |
terraform | that at 0 the world is untouched, and that things arrive in an order that means something |
rounds | that a dead scheduler is noticed, and that a merely late round is not cried wolf over |
Several of these are regression tests with a story: the certificate threshold, the 90-day sale window and the reused Stripe account were all live bugs. The comment above each says which.
The graphics have no tests. A frozen drone and a flying one look identical to a type checker, so that check is still a person opening the page.
test/ the tests, no network
collect/ collectors: production, GitHub, Stripe, local repos
merge.mjs the snapshot's arithmetic, pure and tested
server/ always-on: the API, and the process a LaunchAgent starts
ops/ the LaunchAgent and the Hermes rounds
mcp/server.mjs the board: what is pending, and the tools to act on it
lib/ shared by every process: repo root, the board, thresholds
data/ projects.json (hand-edited, gitignored; example provided),
snapshot.json and agents.json (machine state, gitignored)
src/lib/ shared snapshot: events, rhythms, geometry, formatting
sources.ts refresh rhythms and the TLS warning threshold
redact.ts what `?public` takes out
src/mars/ the colony
palette.ts palette and state language
entities.ts from snapshot to buildable
colony.ts layout, sectors and plots
modules.ts one module per real entity
civic.ts what HQ knows about itself
terrain.ts valley, craters, wall and roads
fleet.ts rover, astronauts, drones, mining crew and effects
world.ts assembly and repainting
hud.ts DOM layer anchored to the world
panel.ts table view
pieces.ts piece library
catalog.ts the inventory page at /catalog.html
main.ts boot, controls and loop
MIT. See LICENSE.
TypeScript
70.7%
JavaScript
23.5%
HTML
4.9%
Your infrastructure as a colony on Mars.
Every project in production, every repository and every Stripe account exists as a module you can walk up to. When something falls over, you see it from the far side of the valley. The data is real: it comes off your own machine and never travels anywhere.

pnpm install
cp data/projects.example.json data/projects.json # your inventory; gitignored
pnpm start # collect, then open the colony
pnpm test # no network
pnpm start is pnpm run collect && pnpm run dev. Node 22 or newer.
The colony draws your machine: the projects you list, the repos under
~/Sites, the gh and stripe sessions already logged in. With none of those
yet, the world is a small colony over whatever it could read — every module
that does appear is real.
| Habitat dome | A project in production. Cyan is answering, magenta is a warning, a white strobe is down |
| Greenhouse | A repository. Warnings are open issues and PRs |
| Ice extractor | A Stripe account |
| Workbench | A repo that only exists on this disk, with no remote to speak for it |
| Civic avenue | What HQ knows about itself: last check per source, headline figures, power as data freshness |
| Quarry | Work in local — the commits that haven't left your machine |
| Terraforming plant | How far the MRR is from the goal you set. The valley greens as that figure climbs, and at 0 the world is bare Mars, which is the honest reading |
| Rover | Drive it with C. Boost included |
Not one built volume is without data behind it. The full vocabulary — what each
piece stands for and where it comes from — lives in
docs/COLONIA.md; the visual inventory opens at
/catalog.html.
| Drag | Pan the map |
| Right button | Orbit |
| Wheel | Zoom to the cursor |
WASD / arrows | Move, relative to the view |
Q E | Turn · R Shift+F tilt · 0 or F frames the colony |
| Double click | Travel to that spot |
C | Drive the rover · Space boost · Escape let go |
L | Every name at once · also the toggle beside the search field |
P | Table panel |
G | Force a collection now |
| Search field | Jump to any module, civic building or sector by name |
| Toggle beside it | Turns on the name of every single thing, spaced so none covers another |
There is no toolbar on purpose: the world takes the whole screen and the scheduler collects by itself. What chrome there is has a job — the search field reaches a module without hunting for it, and the activity log flies you to whatever just happened. A payment you click gets celebrated and never shown again; anything bad opens the module's card instead.
| Source | Every | How |
|---|---|---|
| Production | 60 s | A real HTTP request to each domain in data/projects.json |
| Local repos | 5 min | Reading git off disk |
| GitHub | 15 min | The gh CLI session |
| Stripe | 60 min | Stripe CLI profiles, in live mode |
| Certificates | 12 h | TLS handshake |
Those rhythms are measured, not guessed, and the reasoning for each one is
written down in src/lib/sources.ts. The important one:
the limit that actually binds on Stripe is not the 100 requests per second,
it's the monthly read allowance, 10,000 a month. Polling every five minutes
burned 432 % of it.
A source failing never wipes its data: the last valid value is kept and flagged stale.
TLS_WARN_DAYS in src/lib/sources.ts.mcp/server.mjs is HQ's board. It decides nothing. It says
what is pending and lets whichever agent connects decide what to do about it —
like a board on the wall of a base: the list of breakdowns is in plain sight and
whoever walks past takes one.
Ten tools, over stdio:
| Tool | What it does |
|---|---|
pending_work | Everything asking for attention, sorted by urgency: services down, certificates about to expire, open issues and PRs. Items someone has already claimed are marked, not hidden |
colony_status | Module counts, what's down, when each source was last collected, who is working right now |
issue_detail | Open issues of a repo, with body and author |
pr_detail | Open PRs of a repo, with the changed lines of the diff |
comment | Comment on an issue or PR |
close_issue | Close an issue, with the reason posted as a comment first |
merge_pr | Squash-merge a PR and delete the branch |
propose_work | Put something on the board no collector found. Nobody can take it until a person approves it with pnpm board |
claim_work | Write down that you're taking something on |
finish_work | Close a job you took, saying how it went |
What it can touch is the safety model. On GitHub it can comment, close and merge — all things that undo: an issue reopens, a comment gets deleted, a merge gets reverted. There is nothing here that restarts a server, renews a certificate or touches production, because that doesn't undo. Safety isn't the agent behaving well; it's which tools exist. More gets added deliberately and one at a time.
claim_work and finish_work write to data/agents.json. The collector reads
that file into the snapshot, and the rover hangar shows it: who is out on
the surface and on what. That's how the colony can say "one agent out there"
without lying — because there is one, and the board is what proves it.
Any MCP client speaks to it the same way: node mcp/server.mjs over stdio,
with HQ_HOME pointing at this repo. It needs a snapshot to read (pnpm run collect at least once) and the gh CLI signed in for the GitHub tools.
Claude Code, one line:
claude mcp add hq-colony -e HQ_HOME=/path/to/hq -- node /path/to/hq/mcp/server.mjs
Claude Desktop, Cursor, or any client with a JSON config — copy
mcp.json.example and fix the paths:
{
"mcpServers": {
"hq-colony": {
"command": "node",
"args": ["/path/to/hq/mcp/server.mjs"],
"env": { "HQ_HOME": "/path/to/hq" }
}
}
}
A scheduled agent (Hermes or similar) is how this repo actually runs it:
ops/hermes/ collects on a cadence, compares the signals against
what an agent has already answered for, and only wakes the model when there is
new work. Empty output costs nothing.
Reads go through the same gh session as the collector, so there's no key in
here either.
What a connected agent has already done, from data/agents.json: merged four
Dependabot PRs on cloudstudio/laravel-html-crawler (two of them only after the
token got the workflow scope it was missing), triaged two Nova 4 issues on
cloudstudio/resource-generator and cloudstudio/nova-card-extended, and found
the TLS threshold bug above — workee.es and a client's site had been flagged
for weeks and both were renewing right on time.
?publicOpen the colony with ?public and you get the real snapshot with the parts
that aren't yours to show taken out (src/lib/redact.ts):
"private": true in data/projects.json disappear, along with
the projects in them;localRepos, without this disk's
paths or the last commit message;Everything else stays: your domains, your repos with their real stars, the service that is actually down right now. This is for recording and screenshots. It is deliberately not a made-up world — a fabricated map would prove nothing about the map being real, which is the whole point of the project.
The dev server is for working on the colony; server/serve.mjs is for living
in it. It serves the build and the same API the Vite plugin serves — one
implementation, in server/api.mjs, because two copies of it would drift the
first week.
pnpm run build
node server/serve.mjs # http://localhost:5199
A LaunchAgent starts it at login and brings it back if it dies, so the colony is up after a reboot without anyone remembering to start it:
cp ops/es.cloudstudio.hq.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/es.cloudstudio.hq.plist
The plist sets PATH on purpose. launchd hands a job /usr/bin:/bin and
nothing else, and the collectors shell out to gh, stripe and git. The
scheduler rounds learned the same lesson the hard way: one of them ran for ten
minutes collecting nothing, comparing nothing and waking the model anyway, and
exited 0 so everything upstream believed it had worked.
A round that has missed three of its own turns is not late, it is broken, and
the hangar says so. This exists because of a real morning: Hermes kept a stale
module in memory across an update, its scheduler threw on every tick, and
hermes cron status went on reporting that jobs would fire automatically. It
took someone asking to find out. The one thing worse than a watchman asleep is
a watchman asleep reporting that all is well.
There is no key in this repository. It uses the sessions already on your machine:
gh CLI, already authenticated.stripe itself.data/snapshot.json is in .gitignore on purpose: it holds local paths,
identities and figures. It lives in data/, outside Vite's public/, so a
build can never freeze a copy of it into dist/. data/projects.json is
gitignored for the same reason — it names your clients and your hosts; the repo
ships data/projects.example.json instead.
data/projects.json is the single source of truth for what's in production, and
it's edited by hand. Everything else fills itself in on each collection.
{
"id": "new",
"name": "New",
"url": "https://new.com",
"repo": "cloudstudio/new",
"area": "products",
"stack": ["Laravel 12"],
"host": "forge:cloudstudio",
"stripe": null
}
Each area is its own sector in the colony. It appears on its own as soon as it
has at least one project.
The local collector reads every git repo directly under ~/Sites for the
aggregate figures the quarry and workshop show. Getting a module of your own
in the Workbench sector is opt-in: name the directory in localRepos at the
bottom of the same file.
"localRepos": ["hq"]
The whole directory is deliberately not turned into modules. There is client work in there with no business on a map that gets shown.
HQ is very good at reporting what is happening and would be useless without a way of saying how far along you are. That is one fraction: the MRR the products collect, against a monthly goal you chose.
"mrrGoal": 3000
One number in data/projects.json, typed by a person; the MRR is collected
from Stripe. The fraction between them is what the terraforming plant reads and
what greens the valley: nothing at 0, a green valley at 1. No goal set means no
gauge — rather than a gauge reading zero against a target nobody chose.
pnpm test
Two hundred and some, in test/, and none of them touch the network. They
cover the parts where being wrong has a cost outside the screen:
redact | what ?public must never let through: a client's name, a private domain, a payer, a server IP, a path off this disk, a word an agent wrote |
sources | the certificate threshold and the refresh rhythms, including the Stripe read allowance |
events | a refund is not a sale; a sale ages out of the log; a stale reading is not an outage; a new account's history is not today's news |
entities | one built volume per real thing, and the state each one is in |
stripe | quantity multiplies the price, and an account is not the product billing through it |
merge | a partial reading keeps the last valid value and never becomes a zero; euros and dollars are never one total |
mcp | the board over stdio, and a claim letting go of itself |
board | one reader and one writer for the board, atomic, shared by all three processes |
local | real git repos in a tmpdir: no remote, no commits, not a repo at all |
sanitize | the form cannot corrupt the inventory, and hand-typed config rides through a save |
labels | that turning every name on never puts one label on top of another |
bake | a pivot flagged keep survives the merge whole, role and joint included |
terraform | that at 0 the world is untouched, and that things arrive in an order that means something |
rounds | that a dead scheduler is noticed, and that a merely late round is not cried wolf over |
Several of these are regression tests with a story: the certificate threshold, the 90-day sale window and the reused Stripe account were all live bugs. The comment above each says which.
The graphics have no tests. A frozen drone and a flying one look identical to a type checker, so that check is still a person opening the page.
test/ the tests, no network
collect/ collectors: production, GitHub, Stripe, local repos
merge.mjs the snapshot's arithmetic, pure and tested
server/ always-on: the API, and the process a LaunchAgent starts
ops/ the LaunchAgent and the Hermes rounds
mcp/server.mjs the board: what is pending, and the tools to act on it
lib/ shared by every process: repo root, the board, thresholds
data/ projects.json (hand-edited, gitignored; example provided),
snapshot.json and agents.json (machine state, gitignored)
src/lib/ shared snapshot: events, rhythms, geometry, formatting
sources.ts refresh rhythms and the TLS warning threshold
redact.ts what `?public` takes out
src/mars/ the colony
palette.ts palette and state language
entities.ts from snapshot to buildable
colony.ts layout, sectors and plots
modules.ts one module per real entity
civic.ts what HQ knows about itself
terrain.ts valley, craters, wall and roads
fleet.ts rover, astronauts, drones, mining crew and effects
world.ts assembly and repainting
hud.ts DOM layer anchored to the world
panel.ts table view
pieces.ts piece library
catalog.ts the inventory page at /catalog.html
main.ts boot, controls and loop
MIT. See LICENSE.
TypeScript
70.7%
JavaScript
23.5%
HTML
4.9%