A control plane for your work across GitHub: one repo that holds the map of your projects and the orchestration of AI agent sessions run against them, using thurbox. The defining constraint is that the control plane holds the plan and the log, and never holds the workers' branches — real work happens in thurbox worker sessions, each in its own git worktree in a real repo. What accumulates here is playbooks (reusable recipes) and run logs (what actually happened).
Click Use this template to make your own. It holds context and intent, not code.
From a fresh clone:
- Use this template, then clone your new repo.
- Edit
registry/owners.txt— your GitHub username, plus any orgs you belong to, one per line. The sync refuses to run while the file has no active entries, rather than emit an empty map. ./scripts/sync-registry.sh— writesregistry/repos.generated.yamlfrom your liveghsession. Commit the result../scripts/install-extension.sh— rendersextension.tomland installs the thurbox extension.- Open the
fleetsession in thurbox and give it a goal.
Requires gh (authenticated), jq, and thurbox-cli.
Three things are yours to change, in descending order of how likely you are to want to:
registry/owners.txt— required, and covered in the Quickstart above. It is the only edit a fresh clone actually needs.- The agent and model — optional.
- The name
fleet— optional, and leaving it alone is a fine answer.
extension.toml.in's [[agents]] block pins the model:
[[agents]]
name = "fleet"
command = "claude"
args = ["--model", "claude-opus-4-8"]Edit that block and re-run ./scripts/install-extension.sh. command can point
at a different CLI entirely — codex, say — as long as the resume_args,
fork_args, and new_session_args beneath it still carry thurbox's {id}
token. That token is what makes the session resume by its own id instead of by
"the last session in this directory".
Editing agents.toml after install works too, and takes effect immediately: a
reinstall keeps existing entries, so your edit is not clobbered. But the
manifest is where the entry is generated from, and that makes it the durable
place. An agents.toml edit lives on one machine and does not survive
extension uninstall; a manifest edit is committed, and every install of your
control plane gets it.
fleet names the session, not you and not your repo. Your control plane can
be called mission-control while the session it runs is still fleet — nothing
reads the repo name. Keeping fleet is a perfectly good default. Renaming is
optional, not a setup step you have overlooked.
If you do rename it, the name is not in one place. It is in six, and they move together:
extension.toml.in
name = "fleet" (1) the extension id — the argument to every
`thurbox-cli extension ...` command, so it
also appears in the three hints
scripts/install-extension.sh prints on exit
and in its own header comment
[[agents]] name = "fleet" (2) the agent id, registered in agents.toml
[[sessions]] name = "fleet" (3) the session's name
[[sessions]] agent = "fleet" ... bound to the agent in (2)
derived, not edited
~/.config/thurbox/extensions/fleet/
(4) the extension home. The manifest has no
`home` key, so thurbox derives it from (1)
prose that has to agree
FLEET.md (5) the [[files]] payload, whose text opens
"You are the **fleet** session"
--to fleet / --for fleet (6) the mailbox address workers send to and the
lead drains. In FLEET.md, in this README,
and in extension.toml.in's header comment
Only (4) is not a line you edit; it follows from (1). The rest are.
The failure mode is a partial rename. Change (1) and the extension installs
as mission-control; leave (6) and every worker brief still mails its result
--to fleet. Whether that send bounces or lands in an inbox nobody drains, the
lead never sees it — the worker did the work, the PR exists, and nothing
surfaces it. So:
- Change all six.
- If the extension is already installed under the old name, uninstall it first:
thurbox-cli extension uninstall fleet. Skip this and you get two registered extensions and two self-healing sessions, each faithfully recreating itself. ./scripts/install-extension.shthurbox-cli extension status <newname>to confirm.
FLEET.md's filename is a separate question. The manifest names it four
times — one [[files]] path and three [[symlinks]] targets — so renaming the
file means editing those four lines too. You do not have to: renaming the
session does not require renaming the file, and FLEET.md is a fine name for
the standing context of a session called anything.
Worker session names are not among the six and not coupled to them. They are
free-form strings that nothing resolves, and by the convention in
orchestration/playbooks/ and orchestration/runs/_TEMPLATE.md each is an
imperative sentence describing the work — Document the customization surface —
carrying neither a fleet prefix nor a repo prefix. A rename never reaches
them. The same goes for this README's own prose: it is documentation, so it
follows a rename rather than driving one.
registry/
owners.txt The GitHub owners the map covers, one per line.
repos.generated.yaml Auto-synced index of every repo (owners → repos).
GENERATED — do not hand-edit.
context/
_TEMPLATE.md Copy this to add a project.
<repo>.md Curated notes: purpose, relations, active goals.
orchestration/
playbooks/
_TEMPLATE.md Copy this to add a reusable orchestration recipe.
<name>.md A repeatable way to run thurbox for a class of work.
runs/
_TEMPLATE.md Copy this per orchestration run.
<date>-<slug>.md Log of one run: goal, sessions, outcomes.
scripts/
install-extension.sh Renders extension.toml, then installs it.
sync-registry.sh Regenerates repos.generated.yaml from the GitHub API.
sync-checkout.sh Fast-forwards main when that is unambiguously safe.
trust-thurbox-dir.sh Seeds Claude Code workspace trust for a worktree.
.github/workflows/
ci.yml PR checks feeding a single "All Checks" gate.
registry/repos.generated.yaml is a machine view — regenerated from GitHub, so
it never drifts. Everything a human (or an agent) actually needs to understand
a project lives in registry/context/<repo>.md, which the sync never touches.
That is where judgement goes: what a project is for, how it relates to the
others, what is parked and why.
The sync runs locally. It enumerates every repo you can reach using your own
gh session and keeps the ones owned by an owner in registry/owners.txt, so
there is no cloud PAT and no CI secret to manage. Refresh the map whenever you
like, then push:
./scripts/sync-registry.sh
git commit -am "chore(registry): sync" && git pushThe control plane drives thurbox directly — it does not depend on any external orchestration skill.
- Clarify the goal. Vague goals produce vague workers.
- Pick or write a playbook in
orchestration/playbooks/. - Open a run log from
orchestration/runs/_TEMPLATE.md, named<YYYY-MM-DD>-<slug>.md. - One worker session per unit of work. Each targets a real repo and its own git worktree. Workers share no context with the lead and none with each other, so every prompt states the goal, the constraints, and what "done" looks like, from scratch.
- Record outcomes as they happen — session name, repo, intent, PR. The run log is the source of truth for what happened, not your memory of it.
- Review the PRs, then delete each session as it closes out.
Fast-forward a target repo's base branch before spawning a worker against it.
A stale local main is inherited by the new worktree: the worker does correct
work and its PR arrives conflicting.
A worker finishes by mailing its result to the lead:
thurbox-cli message send --to '<lead>' --kind result --body '<PR url or NOT_APPLICABLE>'and the lead drains the inbox exactly-once:
thurbox-cli message inbox --for '<lead>' --claim --jsonQuote the address. A session name is an imperative sentence, spaces and all, so an unquoted one is split by the shell and mails somewhere else.
This beats polling gh pr list on three counts. It is exact — the worker
names its own artifact instead of you inferring it from a PR list that may
contain someone else's. It is immediate — message send wakes the
recipient, so the lead never polls at all. And it can report "not
applicable", which a PR poll can never distinguish from "still working": the
absence of a PR is not a signal. Because the payload travels through thurbox's
durable database rather than a tmux pane, it also survives scrollback, TUI
chrome, and line-wrapping — all of which make pane-scraping fragile.
See orchestration/playbooks/_TEMPLATE.md
for the anatomy of a playbook, CLAUDE.md for how an agent should
operate inside this repo, and
.claude/skills/thurbox-session/SKILL.md
for the detailed driving surface: spawning, prompting, completion detection,
cleanup.
The repo ships extension.toml.in, so thurbox can keep the
control plane running as a first-class session:
./scripts/install-extension.shThat registers exactly two things:
- A
fleetagent inagents.toml—claudepinned to Opus, resuming and forking by thurbox's session id like the stockclaudeagent. - A long-lived
fleetsession, whose standing context isFLEET.md(symlinked toCLAUDE.md/AGENTS.md/GEMINI.mdin the extension home). thurbox self-heals it: delete the session and it comes back. Because it is a real session, workers can mail their results to it withthurbox-cli message send --to fleet.
Payload files land in ~/.config/thurbox/extensions/fleet/; the registry,
playbooks, and run logs stay in your checkout, where they are versioned.
[[sessions]] repo_path must be an absolute path to your clone, for two
reasons — one historical, one permanent.
Older thurbox did not expand ~ there: ExtensionDef::resolved_for_home()
substituted the {home} token but never called expand_tilde, so a leading
tilde was taken literally and the session landed in a directory named ~.
thurbox#782 fixed that, first
shipping in 0.174.2. But min_thurbox_version here is 0.113.0, so anyone on a
thurbox between that and the fix still hits the bug, and the manifest has to
work across the whole range it claims to support.
Independently of that bug, no token spells "my clone". {home} is substituted,
but it resolves to the extension home, not your checkout — while the session
must open the checkout, because it needs registry/ and orchestration/ in
hand.
A template cannot hardcode a path that exists on one machine. So the manifest
ships as extension.toml.in with a __REPO_PATH__ placeholder, and
scripts/install-extension.sh renders it to a gitignored extension.toml
carrying your clone's real path. Re-run the installer after moving the clone.
There are deliberately no automations. The one scheduled candidate — the
registry sync — commits and pushes to main, so it stays a manual
./scripts/sync-registry.sh run by a human who reads the diff.
thurbox-cli extension status fleet # per-resource health
thurbox-cli extension deactivate fleet # the real off-switch
thurbox-cli extension uninstall fleet # reverse the installMIT — see LICENSE.