One machine runs the station. Everything below is a path on it, and the first question when something is wrong is usually "which of these did I forget exists".
The agents
Each long-lived agent is a directory under ~/agents/<name>/, and the directory is the identity: same files, same memory, every day, however many times the process restarts.
| path | what it is |
|---|---|
~/agents/<name>/home/AGENTS.md | the brief: the whole standing orders for that agent, read at the start of every session |
~/agents/<name>/home/crush.json | the agent's MCP servers: its own Switchboard endpoint, Cairn, and for Rivet and Hailey the phone |
~/agents/<name>/env | its credentials, mode 0600: the Switchboard token, the forge token, the Cairn token, its callsign |
~/agents/<name>/gitconfig | its git identity and the credential helper for the forge, selected with GIT_CONFIG_GLOBAL |
~/agents/<name>/data/ | the crush session database and logs; the agent's memory of what it did |
The agents on shift are rivet (the worker; owns the inbox queue, the webhooks and the phone), voss (the reviewer; owns the reviews queue and nothing else) and mercer (the pathologist; every CI failure becomes a control and a case), with hailey as the bridge officer who reads operator messages and routes work. Adding one is a procedure, not an improvisation: the new-agent skill provisions the endpoint, the forge user, the Cairn token, the directory and the supervisor drop-in, and writes the brief first.
A retired name stays on disk as a symlink to its successor: ~/agents/franklin -> mercer, ~/agents/ivanova -> voss, ~/agents/zathras -> rivet. Resolve the path before reading a file, or one brief reads as two and one agent looks like two. Each of those names holds one directory, one brief, one token and one endpoint. The endpoint slug keeps the name it was registered under, so zathras-de55d19c serves rivet; the slug is an address, not an identity. The symlinks are permanent: they cost nothing and they keep old scripts and stored session paths working.
The repos
| path | what it is |
|---|---|
~/agents/repos/<name> | one persistent clone per repo the station works on, with a gitea remote pointing at the private forge. Repo names lose their hyphens here: the GitHub repo software-observatory is softwareobservatory on gitea |
~/agents/worktrees/<repo>/<task> | where the actual work happens. A reviewer checks a pull request out here and removes it when the todo closes; a worker branches here rather than dirtying the clone |
~/agents/sweeps/ | the working directory of the scheduled one-shot sweeps, with a CLAUDE.md that is their whole brief. It is deliberately not a git checkout and has no forge repo: a sweep observes and files, and a fix it finds becomes an sbpush todo to rivet, who opens the pull request from the repo the fix belongs to |
~/agents/cybrahms/ | the station's GitHub-facing identity: a git config, a GPG key, and the tokens an attended session uses to commit and push as cybrahms |
Never clone into /tmp. A clone there has one remote, the public one, and an agent working in it will reach the end of the job and find it has no way to open the pull request.
The supervisor
harness keeps every agent alive and runs the sweeps on a schedule. It is a user service, and it reads one main file plus a drop-in per harness.
| path | what it is |
|---|---|
~/.config/harness/harness.toml | the daemon's own settings and the drop-in directory |
~/.config/harness/harness.d/<name>.toml | one per agent (a crush session, restart always, its workdir and env file), one per sweep (a claude-code one-shot with a prompt file, a cron schedule, a timeout), or a generic native process kept alive with restart always (scope, the trace viewer; wortling, the German-capture Docker Compose stack) |
~/.config/harness/prompts/<sweep>.md | the prompt a sweep runs with; a fresh session every time, so the prompt is the whole brief |
Drop-ins are not watched: after adding one, harness reload. To see what is running, harness list; to sit with an agent, harness attach <name>.
The scheduled one-shots are medlab-sweep, prompt-audit, station-health, improvement-options, and miner. miner is the daily idea-dig: it widens its search as prior ideas go unpicked, reports a short list of project-idea briefs to Justin on Telegram, and asks to be decommissioned after a week of nothing. Its drop-in is ~/.config/harness/harness.d/miner.toml, its prompt ~/.config/harness/prompts/miner.md, and its workdir + state is ~/agents/miner/.
The authoritative source of truth for what is supervised is the live config (~/.config/harness/harness.toml plus the harness.d/ drop-ins), read back with harness --json list. The pre-2026-09-12 "fleet era" factory repos — pipeline, board, flags, inbox, and fleet — are archived on gitea and are no longer part of the running pipeline; their checked-in docs describe a retired model.
The queue, the pastebin, the phone
| path | what it is |
|---|---|
~/.config/switchboard/env | the queue's own settings: address, database, base URL |
~/.config/switchboard/vend/ | the vended credentials: one env per agent endpoint, one record per webhook, written when each was created |
~/.config/sbpush/env | the ingest URLs sbpush posts to, one per agent that has a generic webhook |
~/.config/tg-bridge/env | the bot token, the paired chat, and the ingest URL the phone delivers to |
The tools
Two directories hold the command-line tools; both are on every agent's path.
| tool | where | what for |
|---|---|---|
forge | ~/.local/bin | gitea from the command line: pull requests, checks, waiting for a merge. Use it instead of curl against the API |
sbpush | ~/.local/bin | post one todo to an agent's inbox from a shell |
sbmcp | ~/.local/bin | call one Switchboard MCP tool as a given endpoint; how a person lists a queue or reads a routing rule |
tg-bridge | ~/.local/bin | the Telegram poller |
crush | ~/.local/bin | the agent runtime the persistent crew runs on |
deaddrop, hacklog, knowledge | ~/.local/bin | the shared notebook, the public build log, the fact store |
harness, switchboard, cairn | ~/go/bin | the supervisor, the queue and the pastebin themselves |
The services
All user services, one launchd plist each under the space.sublayer name. The one exception is wortling, a Docker Compose stack supervised by a generic harness drop-in; its tailnet TLS proxy is still a launchd job, space.sublayer.wortling-proxy, and tailscale serve forwards 8788 to it.
| service | port | what it is |
|---|---|---|
| gitea | 3000 | the forge. Every repo, every pull request, every CI run |
| act-runner | — | gitea's Actions runner: it runs each repo's suite and its deploy target |
| switchboard | 8090 | the queue |
| cairn | 8091 | the pastebin; bodies in MinIO |
| minio | — | object storage for Cairn |
| postgresql 17 | 5432 | one database server for switchboard and cairn |
| caddy | 8443 | TLS on the tailnet: gitea, switchboard and cairn as <name>.sublayer.space |
| cert-sync | — | keeps caddy's tailnet certificates fresh |
| pg-backup | — | the database backups the health sweep checks for |
| harness | — | the supervisor |
| tg-bridge | — | the phone |
| wortling | 8787 (loopback) / 8788 (tailnet) | German capture -> Anki; a Docker Compose api+worker supervised by harness, its tailnet TLS terminated by the space.sublayer.wortling-proxy launchd Caddy |
Nothing on this list is reachable from the public internet. The three public sites, this one, hacklog and medlab, are static directories rsynced onto a separate host by gitea's runner after a green merge to master.
Which file to open
- An agent did something odd: its brief, then its session database under
~/agents/<name>/data/. - A todo went to the wrong agent, or nowhere: the routing rules, read with
sbmcp <env> list_webhook_rules. - An agent is down:
harness list, then its drop-in. - A sweep did not fire: the drop-in's schedule, then
harness listfor the last run. - Something needs a credential: the agent's
env, the vend directory, and never a chat transcript.