Tagteam
A collaboration framework for structured AI-to-AI handoffs with human oversight. One AI leads, another reviews, and you arbitrate — the whole cycle runs phase by phase from a roadmap.
How it works
- Lead (one AI agent) plans each phase and implements the approved plan.
- Reviewer (a second AI agent) reviews both the plan and the implementation.
- Arbiter (you, the human) breaks ties and approves phases.
Work progresses phase by phase. Each phase is listed in docs/roadmap.md and goes through two review cycles: plan, then implementation. If the two agents can't make progress in 10 rounds, control escalates to the human arbiter.
State is tracked in handoff-state.json (current turn) and docs/handoffs/<phase>_<type>_rounds.jsonl + _status.json (per-cycle rounds). Either agent can pick up where the other left off at any time.
Quick Start
pip install tagteam
cd ~/projects/myproject
tagteam quickstart
You'll be prompted for your two agent names, then quickstart sets up the workspace and starts a handoff session. It auto-detects the best terminal backend available on your machine:
- iTerm2 (macOS, default when iTerm2 is installed) — opens three labeled tabs in a single window, auto-launching iTerm2 if it isn't already running.
- tmux (Linux, WSL, or macOS without iTerm2) — creates one
tmuxsession with three labeled panes. - manual (anywhere else, including Windows without WSL) — prints the three commands for you to run in terminals you open yourself.
When quickstart finishes it prints what to paste into the Lead and Reviewer agents to kick off the first handoff. Override the auto-detection with --backend iterm2|tmux|manual if you need a specific one.
Running a handoff
Single phase — start a plan review, let the watcher handle the back-and-forth, and stop when the phase completes.
/handoff start my-phase
Full roadmap — run all incomplete phases end-to-end.
/handoff start --roadmap
/handoff start --roadmap api-gateway
| Command | Purpose | Who |
|---|---|---|
/handoff |
Auto-detects role + state, does the right thing | Both |
/handoff start [phase] |
Begin a new phase (plan + review cycle) | Lead |
/handoff start [phase] impl |
Begin implementation review for a phase | Lead |
/handoff status |
Orientation, status check, drift reset | Both |
Human-in-the-loop — add --confirm to pause for approval before each automatic send.
tagteam watch --mode notify --confirm
Other platforms
tmux (explicit invocation)
tagteam quickstart --backend tmux
Creates one tmux session named tagteam with three labeled panes (Lead, Watcher, Reviewer). Attach later with tmux attach -t tagteam.
Windows / manual fallback
On Windows without WSL, terminal automation (iTerm2/tmux) isn't available. You have two options:
- Headless mode (recommended, fully automated) — no terminals to drive at all; each turn is a fresh
claude -p/codex execprocess. See Headless mode below. - Manual fallback — quickstart prints the commands for you to run yourself in three terminals:
tagteam quickstart --backend manual
You can also run each step individually:
tagteam setup
tagteam init
tagteam session start --backend manual
tagteam watch --mode notify
Advanced setup (run each step yourself)
tagteam setup # copy skills, templates, docs
tagteam init # interactive agent config → tagteam.yaml
tagteam session start # create terminals and auto-launch agents
Options:
tagteam session start --no-launch— create terminals but don't start agentstagteam session start --backend <name>— force a specific backendtagteam session kill— close the current session
Manual mode: you can always run handoffs without any automation by pasting
/handoffoutput between agents yourself.
Headless mode (opt-in)
Instead of typing commands into long-lived agent terminals, the watcher can spawn each turn as a fresh process through the agent's own signed-in CLI (claude -p for Claude, codex exec for Codex — subscription auth, no API keys):
tagteam watch --mode headless # never auto-detected; explicit opt-in only
tagteam tail # follow the in-flight turn like CI logs
On every turn flip the orchestrator composes a bounded context (the handoff skill contract + handoff-state.json + the last 3 rounds), pipes it to the agent on stdin, streams the agent's structured output to .tagteam/turns/<phase>_<type>_r<N>_<role>_<ts>.log (human-readable) and .events.jsonl (raw), and — because the agent still writes its own round with tagteam cycle add — verifies that the expected round landed before dispatching the other agent. Per-turn token usage is recorded in the project DB (usage table) for later phases to surface.
When something goes wrong (turn timeout — 60 min by default; nonzero exit; the agent exited without writing its round; or the CLI could not be started at all), the watcher pauses dispatch, writes .tagteam/headless-paused.json with the reason and log path, and sends a notification. It never retries silently. To resume: read the log, fix anything needed, delete the marker; the watcher picks up on its next tick.
tagteam watch --mode headless --turn-timeout 90 --tail-rounds 5 --confirm
tagteam cycle rounds --phase my-phase --type plan --tail 2 # last 2 entries only
Per-role options live under agents.<role>.headless in tagteam.yaml (all optional):
agents:
lead:
name: Claude
headless:
provider: claude # claude | codex (inferred from command/name if omitted)
executable: /opt/bin/claude # default: `claude` on PATH
args: ["--model", "opus"] # a YAML list; validated — no positionals, no reserved flags
reviewer:
name: Codex
headless:
args: ["-c", "approval_policy=untrusted"]
Defaults are the least-privileged unattended settings that still let the agent edit the repo and run the cycle CLI: Claude runs with --permission-mode acceptEdits --allowedTools Bash Read Edit Write Glob Grep; Codex with --sandbox workspace-write -c approval_policy=never. Anything you put in args is checked against a per-provider option table so a stray token can never become the prompt or override tagteam's own flags.
Interactive modes are unchanged — headless is a peer mode. It is also the path for Windows: it needs only subprocess, and the test suite runs on windows-latest in CI (a real signed-in CLI smoke on Windows is best-effort; see the Phase 31 findings doc).
Arbiter controls (any watcher mode)
tagteam pause --reason "reviewing by hand" # every watcher mode holds dispatch
tagteam resume # clears the hold; the owed turn is re-dispatched once
tagteam cancel-turn # kill the in-flight headless turn → outcome 'cancelled', then paused
tagteam interject "prefer the smaller diff" # note for the next turn (--to lead|reviewer to target a role)
tagteam interject --list # pending / delivered / retired notes for this cycle
tagteam interject --retire 3 # close a note without delivering it
tagteam usage [--json] # per-turn tokens; roll-ups by role, by cycle, totals
- pause/resume use the same marker file the engine writes on a failed turn (
.tagteam/headless-paused.json), soresumealso tells you what failed and where the log is. - cancel-turn never signals a PID it cannot bind to the recorded turn: it checks the child's and the watcher's creation identities (recorded at spawn) and the parent pid, and if anything is stale or unverifiable it just removes the stale metadata and says so.
- interject notes are stored with provenance (who, when, which cycle/round/turn was owed) in the project DB and go into the next eligible turn's prompt under an
ARBITER INTERJECTIONSheading (headless) or show up asinterjectionsontagteam cycle rounds(interactive). A note is scoped to the cycle it was written for; delivery is stamped only when the receiving turn succeeds.--to reviewerwaits for the reviewer's turn. - Retries (
tagteam watch --mode headless --turn-retries N, default 0) re-run a failed turn only when it provably did nothing: the outcome isspawn_failed/nonzero_exit/timeoutand a content-sensitive repo fingerprint (HEAD + index + worktree, recursively through every gitlink) and the handoff state are unchanged.no_round/cancelledare never retried; any git failure or unmerged index fails closed. Only.gitignored paths are outside the fingerprint. - Per-role turn timeouts:
agents.<role>.headless.timeout_minutes. - Notifications work on macOS (osascript), Windows (toast,
msgfallback) and Linux (notify-send);TAGTEAM_NO_NOTIFY=1silences them. tagteam rollback X.Y.Zprints the revert recipe for your install (uv tool or pip, thentagteam upgrade) and runs it only with--yes.
Escalations: the briefer and tagteam rule
When a cycle escalates (ESCALATE, NEED_HUMAN, or auto-escalation after 10 stale rounds) you are the arbiter. Opt in to the escalation briefer and the watcher will spawn one headless turn that writes you a decision brief — each side's position, the actual crux, what it checked, a recommendation with confidence, and the exact ruling commands:
# tagteam.yaml
briefer:
enabled: true # opt-in; absent = off (0.9.0 behavior)
# provider: claude # default: the lead's provider
# args: ["--model", "..."] # try a lighter model; usage is recorded under role "briefer"
# timeout_minutes: 15
tagteam brief # the brief for the CURRENT escalation event (never an older one)
tagteam brief --list # every attempt (auto/manual, status, path)
tagteam brief --generate # run the briefer now (manual attempt; also the retry path)
tagteam rule approve --content "…" # arbiter takes the reviewer's seat: closes the cycle
tagteam rule request-changes --content "…" # hands the turn back to the lead (no auto re-escalation)
tagteam rule answer --to reviewer --content "…" # for NEED_HUMAN: answer delivered as an interjection, cycle re-armed
Briefs land in docs/escalations/<phase>_<type>_r<N>_<event>-a<attempt>.md (unique per escalation event and attempt; …_latest.md is an alias) and in the project DB. It fires at most once automatically per escalation event (a pre-spawn claim guarantees this even with two watchers), never retries on its own, never pauses the loop, and its tokens show up in tagteam usage. Everything it does is read-only except writing the brief file.
The Cockpit
A browser dashboard built around the arbiter's actual job — does anything need me? then is it healthy and what is it doing? — over the data the headless engine, controls and briefer record:
tagteam serve --theme cockpit --dir ~/projects/myproject # http://localhost:8080
- Now strip — phase / type / round, whose turn and for how long, the in-flight headless turn (with a
tagteam taildrawer), the pause hold, watcher liveness, queued notes, and the connection mode (Live via SSE / Polling fallback / Disconnected). - Needs you — one card per thing only the human can do: an escalation with its Phase 33 brief and Approve / Request changes, a needs-human question with Answer, a hold with Resume, a missing/failed brief with Generate brief, a stale in-flight or missing watcher with the CLI to run. Empty when nothing needs you — and it says so.
- Watch tabs — Feed (live round stream: entries, rulings, interjections, briefs), Diff (scope-diff of the current submission, per file, capped), Usage (round-over-round churn with the round-10 line, burn by role / cycle / process, and the Claude subscription-window signal), Notes (interjections: queue one, retire one).
The Now strip's watcher chip is project-bound: with serve.theme: cockpit in tagteam.yaml (or tagteam watch --pidfile) the watcher keeps an identity-checked .tagteam/watcher.json for its lifetime; otherwise the cockpit finds the watcher by process scan (cwd = the project) and the in-flight turn's runner identity. A bare tagteam watch writes nothing new.
Every button is the CLI command with the same effect (tagteam pause, resume, interject, cancel-turn, brief --generate, rule …) — final actions confirm by showing the exact CLI line the server will run, and every action reports the CLI's own message. Recorded as by = web:<user>. Set serve: {theme: cockpit} in tagteam.yaml to make it the default for a project.
Security note. Cockpit mode binds 127.0.0.1 by default; a per-run token is embedded in the page and required as X-Tagteam-Token on every POST (Origin/Referer must match the server; no * CORS). That stops cross-site POSTs and non-browser clients that have not read the page — it is not remote-access authentication. --host 0.0.0.0 deliberately exposes the server on the network; the page token is then the only guard, so do that only on a network you trust.
The Saloon (theme)
The original western-themed dashboard survives as a theme — bare tagteam serve (no --theme, no config key) is unchanged from 0.10.0: the Saloon at /, all interfaces, no token, no cockpit endpoints. In cockpit mode it lives at /?theme=saloon (and works there, token included).
tagteam serve --dir ~/projects/myproject # legacy Saloon (0.10.0-identical)
Configuration
Agents are defined in tagteam.yaml:
agents:
lead:
name: claude
command: claude
reviewer:
name: codex
command: codex
CLI Reference
tagteam quickstart # Setup + init + session start
tagteam session start # Auto-detect backend, launch agents
tagteam session start --backend manual # Force manual backend
tagteam session start --no-launch # Create terminals, skip agent launch
tagteam session kill
tagteam init
tagteam setup
tagteam state
tagteam state diagnose
tagteam watch --mode notify
tagteam watch --mode headless # spawn each turn as a fresh agent process
tagteam watch --pidfile # keep .tagteam/watcher.json for the cockpit's liveness strip
tagteam tail # follow the in-flight headless turn
tagteam cycle rounds --phase P --type plan --tail 3
tagteam pause --reason "..." / tagteam resume / tagteam cancel-turn
tagteam interject "note" [--to lead|reviewer] / --list / --retire ID
tagteam usage [--json]
tagteam serve [--theme cockpit] [--host H] [--port N] [--max-sse N] # dashboard; cockpit is opt-in
tagteam brief [--list | --generate | --event KEY]
tagteam rule approve|request-changes|answer [--content ...] [--to lead|reviewer]
tagteam rollback 0.8.0 [--yes]
tagteam roadmap phases
tagteam serve --dir .
tagteam upgrade
tagteam --help
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tagteam-0.11.0.tar.gz.
File metadata
- Download URL: tagteam-0.11.0.tar.gz
- Upload date:
- Size: 425.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
366ef432e67190908c97adb1d2459929d0ea6fe3332026d088954d21d7f31fa6
|
|
| MD5 |
e0cce3edcee7038dc04f0c4cc133c0fd
|
|
| BLAKE2b-256 |
9b71b49687fc89e993c3750898ab05c89e2ac019edc205627db06ea06a2801b1
|
Provenance
The following attestation bundles were made for tagteam-0.11.0.tar.gz:
Publisher:
publish.yml on jblacketter/tagteam
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tagteam-0.11.0.tar.gz -
Subject digest:
366ef432e67190908c97adb1d2459929d0ea6fe3332026d088954d21d7f31fa6 - Sigstore transparency entry: 2484153419
- Sigstore integration time:
-
Permalink:
jblacketter/tagteam@86786870d10e8df3149564abaddff069932c1a8f -
Branch / Tag:
refs/tags/v0.11.0 - Owner: https://github.com/jblacketter
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@86786870d10e8df3149564abaddff069932c1a8f -
Trigger Event:
push
-
Statement type:
File details
Details for the file tagteam-0.11.0-py3-none-any.whl.
File metadata
- Download URL: tagteam-0.11.0-py3-none-any.whl
- Upload date:
- Size: 332.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
34c505c35ed6082eeeeac234a9b150b787ea7d44f6fd388d02eebd72a6a95754
|
|
| MD5 |
b329a27c243b91577f324bc16bb5dad2
|
|
| BLAKE2b-256 |
44177b444dbafb72af77a7dbc32e31774d2078ff2819a4ac38e460dd1f3c1572
|
Provenance
The following attestation bundles were made for tagteam-0.11.0-py3-none-any.whl:
Publisher:
publish.yml on jblacketter/tagteam
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tagteam-0.11.0-py3-none-any.whl -
Subject digest:
34c505c35ed6082eeeeac234a9b150b787ea7d44f6fd388d02eebd72a6a95754 - Sigstore transparency entry: 2484153462
- Sigstore integration time:
-
Permalink:
jblacketter/tagteam@86786870d10e8df3149564abaddff069932c1a8f -
Branch / Tag:
refs/tags/v0.11.0 - Owner: https://github.com/jblacketter
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@86786870d10e8df3149564abaddff069932c1a8f -
Trigger Event:
push
-
Statement type: