agent-backbone brings agents across AI CLIs and projects into one personal team. It is a control plane for terminal AI agents on one machine. You run Claude Code, Codex, Gemini CLI, OpenCode, Deep Code or Aider in tmux sessions; the backbone starts those sessions, knows whether each one is ready to receive input, delivers text to them safely, and connects them to GitHub Issues, to inbound sources they subscribe to (Gmail today), to people through integrations (Telegram today), and to each other.
It is not an agent framework (it does not call models), not a workflow engine (there are no DAGs), and not a dashboard (it feeds one).
Communication, coordination and continuity let specialists collaborate and carry experience into later projects. Starting practices help agents retain reasoning, apply lessons in context and revise them after feedback. You can change those policies as your team develops. Expertise depends on the models, useful records, investigation and judgment; Backbone does not automatically learn or validate knowledge on their behalf.
A directory plus a runtime. Agents are discovered, not declared: the
first backbone agent start from a directory records it.
$ cd ~/code/app && backbone agent start
app: ready — claude repo acme/app
- Name — the directory name (pass
agent start NAMEto override). It is the tmux session name, the value offor:<name>labels, and thefrom:identity in messages the agent sends. Discovery removes leading punctuation so the name starts with a letter or digit (_appbecomesapp); a name containing only punctuation becomesagent. Internal underscores and dots are preserved. - Runtime —
claude,codex,gemini,opencode,deepcode,aiderorshell; a known agent reuses its saved runtime. New agents useagents.default_runtime(initiallyclaude) unless explicitly selected. - Repository — read from
git remote origin. An agent whose directory is a GitHub checkout owns that repository. - Watches — other repositories the agent wants to hear about
(
backbone agent watch orch acme/app acme/web). Inside its own session an agent can subscribe itself:backbone agent watch acme/app(the name defaults to$BACKBONE_AGENT). - Subscriptions — inbound events from a source the agent
wants (
backbone agent subscribe desk gmail "from:alerts@example.com" --priority high): a source, a filter in that source's own query language, and a priority (normalwaits for the prompt, batched;highreaches a working Claude Code, Codex or OpenCode agent on its next tool call through hook context, or on Codex when a long-running command exits; other runtimes first when ready).
There are no prescribed roles or hierarchies. An orchestrator is an ordinary agent whose directory is its own repository and which watches the others. The one grouping that exists is a swarm: a coordinator plus members sharing a worktree to complete a single issue — and its members are still ordinary agents.
The database is the only record of agents; backbone agent set|watch|forget
edit it and the running backbone picks the change up immediately.
Every repository an agent owns or watches is tracked on its own. There is no coordination repository and nothing to configure per repository: GitHub credentials are set once, and the backbone polls (or accepts webhooks for) every tracked repository.
Four relationships decide routing for an issue in repository R:
| Relationship | How it comes about | Effect |
|---|---|---|
| owner | the agent's directory is R (swarm members excepted: their worktree is R, but they are not owners) | unlabelled issues are its work (sole owner) or are announced to all owners |
for:<agent> |
a label on the issue | goes to that agent's queue |
from:<agent> |
a label on the issue | comments and the close are reported back to the opener |
| watch | backbone agent watch |
informational notice about new issues; for: labels in R route to it |
What an agent is doing, in a vocabulary shared by every runtime:
| State | Meaning |
|---|---|
starting |
Session exists; runtime not at its prompt yet |
idle |
At the prompt, nothing running |
busy |
Working on a prompt |
waiting_for_human |
Blocked on a person — reason is plan (plan approval), permission (tool permission prompt) or question (AskUserQuestion, or any dialog seen on the terminal) |
blocked |
Waiting on a usage limit (reason: quota) or a provider capacity/rate-limit failure (reason: provider). detail preserves the error and retry/reset time. Recovery may require waiting or choosing another model. |
unknown |
No trustworthy signal |
offline |
No tmux session (reported by the API; not a stored state) |
Where it comes from, in order: agent start writes starting when the
session is created (trusted for two minutes at most; the first hook write
replaces it, and a visible prompt clears it). Hooks the runtime itself
runs (Claude Code, Codex, Gemini CLI and OpenCode) write
<data_dir>/state/<agent>.json on every
transition; a fresh hook state is authoritative. When there is no hook state or it is
older than timing.stale_threshold_seconds (5 min), the backbone reads
the terminal through the runtime's module (prompt visible, busy
marker, permission prompt). Every reading keeps its evidence, shown by
backbone agent inspect.
Derived from the state plus the terminal, right before anything is pasted:
| Condition | Meaning | Deliverable? |
|---|---|---|
offline |
no session, or an inbox-only agent | no — queued (an inbox-only agent reads its messages with backbone inbox) |
waiting_for_human |
agent is asking a person something | no — queued |
agent_working |
starting, busy, or blocked on a usage limit or provider failure | no — queued (never bypassed) |
human_reading |
someone is selecting text or reading tmux scrollback | no — queued, even with priority |
unattended_copy_mode |
the pane is in tmux copy mode and no client is viewing it | no — queued, even with priority, until someone attaches and presses q |
human_typing |
someone typed text into the prompt | no — queued, unless priority |
settling |
hook reported idle less than timing.grace_period_seconds ago, or a fresh session's brief (Aider) is not delivered yet |
not yet, unless priority; the brief goes first even then |
ready |
idle, empty prompt | yes |
unknown |
no signal either way | yes, best effort |
tmux copy mode preserves scrollback and text selection. Backbone observes it
without cancelling it: otherwise-ready agents report human_reading, and
messages wait until the human leaves copy mode (normally q or Escape).
Monitoring and priority delivery never clear a selection. The agent's own state
still takes precedence: a busy agent remains agent_working while you read.
tmux keeps a pane in copy mode after its last client detaches. When no client
is viewing the pane's window, the agent reports unattended_copy_mode instead:
messages still wait, and backbone agent inspect and backbone doctor say to
attach to the session and press q. A sender whose message expires in the
queue is told the condition that held it, so they can tell whether a resend
would meet it too.
The only thing that ever enters an agent's terminal. Every message carries a provenance envelope so the agent — and anyone reading the transcript — knows where it came from:
[via:backbone from:elias] review PR 12 and summarise the risks
[via:github issue:42] New issue targeting you: acme/app#42 [bug] "Fix flaky auth test" (from planner, blocking). Link: https://…
[via:telegram from:alice] status?
[via:gmail] New gmail messages matching your subscriptions. …
- 199a4f2c3d1e0b7a · from «Alerts <alerts@example.com>» · subject «Outage: build queue stalled» · 2026-09-17 14:02Z · https://mail.google.com/mail/#all/199a4f2c3d1e0b7a
One attempt to hand a message to a session, recorded with its kind
(issue, comment, review, pull_request, direct_message, watch,
escalation, subscription, steer), repository, outcome and a preview — direct
messages included. A steer (backbone tell --steer) is never queued or
pasted: it is offered to a working agent's hook and its record moves from
offered to handed_off, not_taken or cancelled. What cannot be delivered now is queued in the database and
delivered by the background jobs — a message that waited at least two
minutes is delivered with (queued N min ago) (N h from two hours) after
its envelope, so a review or comment drained after a long busy stretch
does not read as current; queued messages expire after
timing.queue_expiry_minutes (30), except direct messages, active swarm coordination, inbox holds, an agent's startup brief and subscription batches (facts, not conversation: they grow while the agent
is busy and are retired only when delivered). Nothing expires for an agent whose
queue is held by an unconfirmed (uncertain) delivery: that wait measures the
hold, not whether the message is still wanted. Once the hold is acknowledged,
the waiting messages get a full expiry window from that moment.
Direct messages wait until delivered or acknowledged through the inbox, even across
service restarts; retiring the recipient registration retires its pending messages.
An expired notification leaves a
delivery with outcome expired (kind, source and preview kept), so
agent inspect shows what never arrived, and the recipient and each registered
agent that sent one get a [via:backbone] notice listing them (source
queue-expiry; notices never expire). A sender without a Backbone inbox, such as
GitHub or an external caller, keeps only that record. The sender is told whether a row
exists (stored), whether the same message from them was already
waiting (already_queued), or whether storing it failed (failed) —
"queued" is never claimed for a message that is not in the database.
The same message means the same source event (a comment or review id) or
the same sender with the same text; two senders with identical text are two
messages. Issue deliveries are additionally
claimed so concurrent jobs cannot deliver the same issue twice. Delivery
checks and pastes are serialized per session within the running server;
different sessions can proceed concurrently. A blocked queue drain retains its
leased row instead of inserting an age-stamped copy.
Every inbound GitHub event (webhook or poll) and every subscribed source
item (a matched Gmail message) is stored before it is
routed, with what the backbone did about it. That table is the activity
feed (GET /api/events, backbone status shows the last event per
repository) and the dedup record used by overlapping polls. A separate durable
cursor per repository preserves an incomplete batch across restarts, independent
of event retention. A durable outbox records every eligible recipient before
the first delivery, then records delivered or queued receipts. A failed queue
write leaves that recipient pending; replay and the retry job resume only
unresolved recipients. See delivery receipts
for retention and the external-terminal crash boundary.
An agent's short account of its goal, accomplishments, blockers and next steps. Agents publish at meaningful milestones; the database preserves their reports so people and orchestrators can read a shared feed without interrupting work. Report age and missing reports are explicit. This authored account is separate from the runtime's measured state and from operational diagnostics. See Agent progress reports for publishing tools and enforced length limits.
A directory with a SKILL.md that an agent's CLI loads on demand. The
store (skills.store, ~/skills) holds the one shared copy of each;
a skill's frontmatter names the tags of the agents that receive it
(all for everyone, agent:NAME for one). At launch, and for a running
agent whenever a skill or its tags change, the backbone links the selected
skills into the directory the runtime reads. It never changes a skill the
repository owns, and links one kept where only another CLI reads it, so the
repository's skills reach whichever CLI runs the agent. See Skills.
Everything tunable is a key in the database with a built-in default:
backbone config list. Secrets (API key, tokens) are never in the
database; they live in <data_dir>/.env or the environment, and are
kept out of the agent sessions the backbone starts.
Background loops inside the backbone process:
| Job | Every | Does |
|---|---|---|
agent-monitor |
timing.monitor_interval_seconds (60 s) |
refresh agents/settings, read every agent's state once and mirror it to the database, stall and dead-session reports, plan-waiting alerts, queue drain, next pending issue to idle agents, Socket.IO snapshot |
delivery-retry |
timing.retry_interval_seconds (5 min) |
retry failed issue deliveries, drain the queue |
steer-settle |
15 s | record what became of each steer offer: handed_off, not_taken when the turn ended first or after 300 s, cancelled when the session was replaced |
agent-transitions |
5 s | explicit one-time stop/restart requests (agent restart): stop the session, start the replacement when its wait is over, hand it the continuation message |
github-poll |
github.poll_interval_seconds (60 s) |
poll intake only |
sources-poll |
sources.poll_interval_seconds (60 s) |
only with a configured source: search each source for new items matching any subscription filter and deliver them |
github-backfill |
starts at startup | webhook intake only: catch up on downtime; delivery-retry retries unfinished repositories until catch-up succeeds |
dependency-sync |
10 min, and at startup | with GitHub: record the sub-issue links of open queued issues, for dependency priority and "Dependencies resolved" notices |
prune |
6 h | delete old deliveries, events and completed queue bodies; rotate the hook action log |
integrations-sync |
5 min | re-provision each integration's per-agent surfaces, such as Telegram topics |
report-notifications |
30 s | post new progress reports to the integrations |
report-audio |
30 s | send report voice messages, when telegram.report_audio is on |
upgrade-watch |
60 s | restart the backbone itself onto new code when the installed version changes, or the checkout moves on the branch it started on (not when it switches branches), with no active upgrade hold and nothing being routed; off with backbone.restart_on_upgrade |
- What agents do. It hands an agent text; the agent's instructions (CLAUDE.md, AGENTS.md, …) decide how it reacts. GitHub → What an agent is expected to do is the whole protocol.
- Who may talk to whom. Any agent can message any agent through the API.
- Whether to restart a dead agent. It reports; it never restarts on its
own. The one restart it performs is the explicit
agent restartan agent or a person asked for.
Runtime registration is declared in services/runtimes/catalog.json: IDs,
detection order and hook files feed configuration, the runtime registry and
hook installation from the same data. Runtime behavior remains in each runtime's
module; shipped hook scripts stay standalone.
See message checkpoints for safe mid-turn coordination, acknowledgement and retention.