playmaker-cli 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
playmaker/registry.py ADDED
@@ -0,0 +1,41 @@
1
+ """Map agent name -> handler instance + profile lookup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from playmaker.agents.agy import AgyHandler
8
+ from playmaker.agents.base import AgentHandler
9
+ from playmaker.agents.claude import ClaudeHandler
10
+ from playmaker.agents.codex import CodexHandler
11
+ from playmaker.agents.gemini import GeminiHandler
12
+ from playmaker.state import AGENTS_DIR
13
+
14
+ _HANDLERS: dict[str, AgentHandler] = {
15
+ "claude": ClaudeHandler(),
16
+ "codex": CodexHandler(),
17
+ "agy": AgyHandler(),
18
+ "gemini": GeminiHandler(),
19
+ }
20
+
21
+
22
+ def get_handler(name: str) -> AgentHandler:
23
+ if name not in _HANDLERS:
24
+ raise KeyError(f"unknown agent {name!r}; available: {', '.join(_HANDLERS)}")
25
+ return _HANDLERS[name]
26
+
27
+
28
+ def all_handlers() -> dict[str, AgentHandler]:
29
+ return dict(_HANDLERS)
30
+
31
+
32
+ def find_profile(agent_name: str, project_cwd: Path | None = None) -> Path | None:
33
+ """Project-local profile takes precedence over global."""
34
+ candidates: list[Path] = []
35
+ if project_cwd is not None:
36
+ candidates.append(project_cwd / ".playmaker" / "agents" / f"{agent_name}.md")
37
+ candidates.append(AGENTS_DIR / f"{agent_name}.md")
38
+ for path in candidates:
39
+ if path.exists():
40
+ return path
41
+ return None
playmaker/state.py ADDED
@@ -0,0 +1,173 @@
1
+ """SQLite-backed run state for `playmaker`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sqlite3
7
+ import uuid
8
+ from contextlib import contextmanager
9
+ from datetime import UTC, datetime
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ PLAYMAKER_HOME = Path("~/.playmaker").expanduser()
14
+ DB_PATH = PLAYMAKER_HOME / "state.db"
15
+ LOGS_DIR = PLAYMAKER_HOME / "logs"
16
+ OUTPUTS_DIR = PLAYMAKER_HOME / "outputs"
17
+ AGENTS_DIR = PLAYMAKER_HOME / "agents"
18
+ CONFIG_PATH = PLAYMAKER_HOME / "config.toml"
19
+ QUOTAS_PATH = PLAYMAKER_HOME / "quotas.json"
20
+
21
+ SCHEMA = """
22
+ CREATE TABLE IF NOT EXISTS sessions (
23
+ id TEXT PRIMARY KEY,
24
+ agent TEXT NOT NULL,
25
+ agent_session_id TEXT,
26
+ prompt TEXT NOT NULL,
27
+ cwd TEXT NOT NULL,
28
+ files TEXT,
29
+ status TEXT NOT NULL,
30
+ started_at TEXT NOT NULL,
31
+ finished_at TEXT,
32
+ exit_code INTEGER,
33
+ cost_usd REAL,
34
+ duration_seconds REAL,
35
+ output_path TEXT,
36
+ session_file_path TEXT,
37
+ parent_id TEXT,
38
+ pid INTEGER,
39
+ model TEXT,
40
+ batch_id TEXT
41
+ );
42
+ CREATE INDEX IF NOT EXISTS idx_status ON sessions(status);
43
+ CREATE INDEX IF NOT EXISTS idx_agent ON sessions(agent);
44
+ CREATE INDEX IF NOT EXISTS idx_started ON sessions(started_at DESC);
45
+ """
46
+
47
+
48
+ def now_iso() -> str:
49
+ return datetime.now(UTC).isoformat()
50
+
51
+
52
+ def new_session_id() -> str:
53
+ return str(uuid.uuid4())
54
+
55
+
56
+ @contextmanager
57
+ def connect():
58
+ conn = sqlite3.connect(str(DB_PATH))
59
+ conn.row_factory = sqlite3.Row
60
+ try:
61
+ yield conn
62
+ finally:
63
+ conn.close()
64
+
65
+
66
+ def ensure_dirs() -> None:
67
+ for d in (PLAYMAKER_HOME, LOGS_DIR, OUTPUTS_DIR, AGENTS_DIR):
68
+ d.mkdir(parents=True, exist_ok=True)
69
+
70
+
71
+ def init_db() -> None:
72
+ ensure_dirs()
73
+ with connect() as c:
74
+ c.executescript(SCHEMA)
75
+ # Migration: pre-existing databases may lack newer columns.
76
+ existing_cols = {row["name"] for row in c.execute("PRAGMA table_info(sessions)")}
77
+ if "model" not in existing_cols:
78
+ c.execute("ALTER TABLE sessions ADD COLUMN model TEXT")
79
+ if "batch_id" not in existing_cols:
80
+ c.execute("ALTER TABLE sessions ADD COLUMN batch_id TEXT")
81
+ # Created after migration so it works on pre-existing tables too.
82
+ c.execute("CREATE INDEX IF NOT EXISTS idx_batch ON sessions(batch_id)")
83
+ c.commit()
84
+
85
+
86
+ def insert_session(
87
+ *,
88
+ agent: str,
89
+ prompt: str,
90
+ cwd: str,
91
+ files: list[str] | None = None,
92
+ parent_id: str | None = None,
93
+ model: str | None = None,
94
+ batch_id: str | None = None,
95
+ ) -> str:
96
+ """Insert a new pending session, return its id."""
97
+ sid = new_session_id()
98
+ with connect() as c:
99
+ c.execute(
100
+ """
101
+ INSERT INTO sessions (
102
+ id, agent, prompt, cwd, files,
103
+ status, started_at, parent_id, model, batch_id
104
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
105
+ """,
106
+ (
107
+ sid,
108
+ agent,
109
+ prompt,
110
+ cwd,
111
+ json.dumps(files) if files else None,
112
+ "pending",
113
+ now_iso(),
114
+ parent_id,
115
+ model,
116
+ batch_id,
117
+ ),
118
+ )
119
+ c.commit()
120
+ return sid
121
+
122
+
123
+ def list_batch(batch_id: str) -> list[dict[str, Any]]:
124
+ """All sessions in a dispatch batch, oldest first."""
125
+ with connect() as c:
126
+ rows = c.execute(
127
+ "SELECT * FROM sessions WHERE batch_id = ? ORDER BY started_at ASC",
128
+ (batch_id,),
129
+ ).fetchall()
130
+ return [dict(r) for r in rows]
131
+
132
+
133
+ def update_session(session_id: str, **fields: Any) -> None:
134
+ if not fields:
135
+ return
136
+ cols = ", ".join(f"{k} = ?" for k in fields)
137
+ values = list(fields.values()) + [session_id]
138
+ with connect() as c:
139
+ c.execute(f"UPDATE sessions SET {cols} WHERE id = ?", values)
140
+ c.commit()
141
+
142
+
143
+ def get_session(session_id: str) -> dict[str, Any] | None:
144
+ with connect() as c:
145
+ row = c.execute(
146
+ "SELECT * FROM sessions WHERE id = ? OR id LIKE ?",
147
+ (session_id, f"{session_id}%"),
148
+ ).fetchone()
149
+ return dict(row) if row else None
150
+
151
+
152
+ def list_sessions(
153
+ *,
154
+ status: str | None = None,
155
+ agent: str | None = None,
156
+ limit: int = 50,
157
+ ) -> list[dict[str, Any]]:
158
+ sql = "SELECT * FROM sessions"
159
+ where = []
160
+ params: list[Any] = []
161
+ if status:
162
+ where.append("status = ?")
163
+ params.append(status)
164
+ if agent:
165
+ where.append("agent = ?")
166
+ params.append(agent)
167
+ if where:
168
+ sql += " WHERE " + " AND ".join(where)
169
+ sql += " ORDER BY started_at DESC LIMIT ?"
170
+ params.append(limit)
171
+ with connect() as c:
172
+ rows = c.execute(sql, params).fetchall()
173
+ return [dict(r) for r in rows]
playmaker/watcher.py ADDED
@@ -0,0 +1,145 @@
1
+ """Rich Live TUI for `playmaker watch`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from datetime import UTC, datetime
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from rich.console import Console
11
+ from rich.live import Live
12
+ from rich.table import Table
13
+
14
+ from playmaker import state
15
+ from playmaker.registry import get_handler
16
+
17
+ _ICONS = {
18
+ "pending": "โณ",
19
+ "running": "๐Ÿ”„",
20
+ "done": "โœ…",
21
+ "failed": "โŒ",
22
+ "killed": "๐Ÿ›‘",
23
+ }
24
+
25
+
26
+ def _build_table() -> Table:
27
+ rows = state.list_sessions(limit=50)
28
+ # Keep non-terminal + recently terminated (last 5 minutes).
29
+ now = datetime.now(UTC)
30
+ keep = []
31
+ for r in rows:
32
+ if r["status"] in ("pending", "running"):
33
+ keep.append(r)
34
+ elif r["finished_at"]:
35
+ try:
36
+ finished = datetime.fromisoformat(r["finished_at"])
37
+ except ValueError:
38
+ continue
39
+ if (now - finished).total_seconds() < 300:
40
+ keep.append(r)
41
+
42
+ table = Table(
43
+ title=f"playmaker โ€” live sessions ({len(keep)})",
44
+ show_header=True,
45
+ header_style="bold",
46
+ )
47
+ table.add_column("id", style="cyan", no_wrap=True)
48
+ table.add_column("agent", no_wrap=True)
49
+ table.add_column("status", no_wrap=True)
50
+ table.add_column("age", no_wrap=True)
51
+ table.add_column("activity")
52
+ table.add_column("prompt")
53
+
54
+ for r in keep:
55
+ try:
56
+ started = datetime.fromisoformat(r["started_at"])
57
+ age_s = (now - started).total_seconds()
58
+ except (TypeError, ValueError):
59
+ age_s = 0
60
+ age_str = _format_age(age_s)
61
+ icon = _ICONS.get(r["status"], "?")
62
+ prompt = (r["prompt"] or "").replace("\n", " ")
63
+ if len(prompt) > 60:
64
+ prompt = prompt[:57] + "..."
65
+ activity = _get_activity(r)
66
+ table.add_row(
67
+ r["id"][:8],
68
+ r["agent"],
69
+ f"{icon} {r['status']}",
70
+ age_str,
71
+ activity,
72
+ prompt,
73
+ )
74
+ return table
75
+
76
+
77
+ def _get_activity(row: dict[str, Any]) -> str:
78
+ status = row["status"]
79
+ if status == "running":
80
+ path_str = row.get("session_file_path")
81
+ if not path_str:
82
+ return "starting..."
83
+ path = Path(path_str)
84
+ if not path.exists():
85
+ return "starting..."
86
+ try:
87
+ handler = get_handler(row["agent"])
88
+ turns = handler.parse_session_file(path)
89
+ if not turns:
90
+ return "starting..."
91
+ last = turns[-1]
92
+ if last.tool_calls:
93
+ # show "tool: <name>" of the LAST tool_call's 'name' field
94
+ name = last.tool_calls[-1].get("name", "")
95
+ text = f"tool: {name}"
96
+ elif last.content:
97
+ text = last.content
98
+ else:
99
+ return "starting..."
100
+
101
+ text = text.replace("\n", " ")
102
+ if len(text) > 60:
103
+ text = text[:57] + "..."
104
+ return text
105
+ except Exception:
106
+ return "-"
107
+
108
+ if status == "done":
109
+ out_path_str = row.get("output_path")
110
+ if not out_path_str:
111
+ return "-"
112
+ path = Path(out_path_str)
113
+ if not path.exists():
114
+ return "-"
115
+ try:
116
+ text = path.read_text().replace("\n", " ")
117
+ if len(text) > 60:
118
+ text = text[:57] + "..."
119
+ return text
120
+ except Exception:
121
+ return "-"
122
+
123
+ return "-"
124
+
125
+
126
+ def _format_age(seconds: float) -> str:
127
+ seconds = int(seconds)
128
+ if seconds < 60:
129
+ return f"{seconds}s"
130
+ if seconds < 3600:
131
+ return f"{seconds // 60}m{seconds % 60:02d}s"
132
+ h, rem = divmod(seconds, 3600)
133
+ return f"{h}h{rem // 60:02d}m"
134
+
135
+
136
+ def run() -> None:
137
+ state.init_db()
138
+ console = Console()
139
+ try:
140
+ with Live(_build_table(), console=console, refresh_per_second=1) as live:
141
+ while True:
142
+ time.sleep(1)
143
+ live.update(_build_table())
144
+ except KeyboardInterrupt:
145
+ console.print("[dim]watch stopped[/dim]")
@@ -0,0 +1,335 @@
1
+ Metadata-Version: 2.4
2
+ Name: playmaker-cli
3
+ Version: 0.4.0
4
+ Summary: Playing-coach CLI for orchestrating Claude Code, Codex and Antigravity sub-agents in parallel.
5
+ Project-URL: Homepage, https://github.com/vladsafedev/playmaker
6
+ Project-URL: Repository, https://github.com/vladsafedev/playmaker
7
+ Project-URL: Issues, https://github.com/vladsafedev/playmaker/issues
8
+ Author-email: Vladislav Shulyugin <vladislav.shulyugin@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,ai,antigravity,claude,claude-code,cli,codex,gemini,orchestration,subagents
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: rich>=13.7
25
+ Requires-Dist: typer>=0.12
26
+ Description-Content-Type: text/markdown
27
+
28
+ # playmaker
29
+
30
+ [![CI](https://github.com/vladsafedev/playmaker/actions/workflows/ci.yml/badge.svg)](https://github.com/vladsafedev/playmaker/actions/workflows/ci.yml)
31
+ [![PyPI](https://img.shields.io/pypi/v/playmaker-cli.svg)](https://pypi.org/project/playmaker-cli/)
32
+ [![Python](https://img.shields.io/pypi/pyversions/playmaker-cli.svg)](https://pypi.org/project/playmaker-cli/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
34
+
35
+ **Run Claude Code, Codex and Antigravity as parallel sub-agents from one terminal โ€” and spend three separate quotas instead of one.**
36
+
37
+ You stay in your Claude Code session doing the part only you can do. `playmaker`
38
+ fans the rest out to other agent CLIs as detached processes, tracks them,
39
+ parses their native session files, and pings you when they land.
40
+
41
+ ```console
42
+ $ B=dashboard # one label for the whole fan-out
43
+
44
+ $ playmaker dispatch codex --batch $B --model gpt-5-codex -p "Add PATCH /users/:id โ€ฆ"
45
+ session: 9f2c1a4e-โ€ฆ pid: 48211 (detached)
46
+ $ playmaker dispatch agy --batch $B --model "Gemini 3.5 Flash (High)" -p "pytest coverage for โ€ฆ"
47
+ session: 4b1f9c02-โ€ฆ pid: 48219 (detached)
48
+ $ playmaker dispatch claude --batch $B --model sonnet -p "Update the API docs for โ€ฆ"
49
+ session: c07d5511-โ€ฆ pid: 48244 (detached)
50
+
51
+ # โ€ฆyou keep working in your own session while those three run
52
+
53
+ $ playmaker list
54
+ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“
55
+ โ”ƒ id โ”ƒ agent โ”ƒ status โ”ƒ started โ”ƒ prompt โ”ƒ
56
+ โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ
57
+ โ”‚ 9f2c1a4e โ”‚ codex โ”‚ running โ”‚ 2026-07-27T18:02:09 โ”‚ Add PATCH /users/:id โ€ฆ โ”‚
58
+ โ”‚ 4b1f9c02 โ”‚ agy โ”‚ done โ”‚ 2026-07-27T18:02:11 โ”‚ pytest coverage for โ€ฆ โ”‚
59
+ โ”‚ c07d5511 โ”‚ claude โ”‚ done โ”‚ 2026-07-27T18:02:14 โ”‚ Update the API docs โ€ฆ โ”‚
60
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
61
+
62
+ ๐Ÿ”” playmaker โ€” batch done # one ping for the whole fan-outโ€ฆ
63
+ 3/3 done ยท codex โœ“ ยท agy โœ“ ยท claude โœ“ # โ€ฆclick it to open every output
64
+
65
+ $ playmaker summary 9f2c1a4e # what codex says it did
66
+ $ playmaker continue 9f2c1a4e -p "the 404 branch is missing a test"
67
+ ```
68
+
69
+ ## Why
70
+
71
+ Three reasons to fan work out across agent CLIs instead of grinding through it
72
+ in one serial session:
73
+
74
+ 1. **Wall-clock speed.** A task that decomposes into 3โ€“5 independent
75
+ work-streams (schema, backend, frontend, tests, docs) finishes 2โ€“4ร— faster
76
+ when each stream runs as its own parallel agent.
77
+ 2. **Provider arbitrage.** Codex and Antigravity quotas are entirely separate
78
+ pools from your Anthropic plan. Every slice you hand them is capacity your
79
+ main session never spends โ€” and Antigravity's roster includes Claude
80
+ Sonnet/Opus, so even Claude-quality work can run on Google's pool.
81
+ 3. **Bucket arbitrage inside one plan.** Headless `claude -p` draws on the same
82
+ subscription as your interactive session, but per-model weekly buckets are
83
+ separate โ€” dispatching `--model sonnet` spends Sonnet's usually-idle bucket
84
+ instead of the scarce Opus one.
85
+
86
+ The catch: doing this by hand โ€” terminal tabs, jumping between tools,
87
+ copy-pasting context โ€” is friction. `playmaker` removes the friction; the
88
+ [coach skill](#the-coach-skill) provides the discipline.
89
+
90
+ ## โš ๏ธ Sub-agents skip permission prompts by default
91
+
92
+ By default, playmaker launches Claude and Antigravity sub-agents with
93
+ `--dangerously-skip-permissions` (and Gemini with `--yolo`). This is
94
+ deliberate: a headless agent has no human at the keyboard, so without these
95
+ flags a detached run stalls at the first tool-approval prompt and finishes
96
+ having written nothing.
97
+
98
+ It also means a dispatched agent can run commands and edit files **without
99
+ asking**. Only dispatch prompts you'd be comfortable running unattended, in
100
+ working directories you trust.
101
+
102
+ To opt out for Claude, set this in `~/.playmaker/config.toml`:
103
+
104
+ ```toml
105
+ [agents.claude]
106
+ skip_permissions = false
107
+ ```
108
+
109
+ โ€” with the caveat above: detached runs will then stall on the first permission
110
+ prompt, so this only really makes sense alongside `--sync` workflows or
111
+ allowlist rules you've configured in Claude Code itself.
112
+
113
+ ## Install
114
+
115
+ ```bash
116
+ uv tool install playmaker-cli # or: pipx install playmaker-cli
117
+ ```
118
+
119
+ <details>
120
+ <summary>From source</summary>
121
+
122
+ ```bash
123
+ git clone https://github.com/vladsafedev/playmaker
124
+ cd playmaker
125
+ uv tool install --editable .
126
+ ```
127
+
128
+ </details>
129
+
130
+ Then set up the data directory and install the coach skill:
131
+
132
+ ```bash
133
+ playmaker init # creates ~/.playmaker/ (state.db, logs/, outputs/, config.toml)
134
+ playmaker skill install # drops playmaker-coach into ~/.claude/skills/
135
+ playmaker agents # which agent CLIs are reachable
136
+ ```
137
+
138
+ ## Prerequisites
139
+
140
+ `playmaker` orchestrates external CLIs โ€” install whichever you have access to:
141
+
142
+ | Agent | Install | Notes |
143
+ |---|---|---|
144
+ | **Claude Code** | `npm i -g @anthropic-ai/claude-code` | `--model sonnet` / `opus` / `haiku` |
145
+ | **Codex CLI** | `npm i -g @openai/codex` | `--model gpt-5-codex` |
146
+ | **Antigravity (`agy`)** | bundled with [Antigravity](https://antigravity.google) | models are display names: `--model "Claude Opus 4.6 (Thinking)"` โ€” see `agy models` |
147
+ | **Gemini CLI** (legacy) | `npm i -g @google/gemini-cli` | still supported, superseded by `agy` |
148
+
149
+ At least one is required; `playmaker agents` tells you which it can see.
150
+
151
+ ## The coach skill
152
+
153
+ `playmaker` is the runner. The decision-making โ€” *when* delegation beats doing
154
+ it yourself, *which* agent gets *which* slice, how to size a subtask so
155
+ reviewing it is cheap โ€” lives in
156
+ [**playmaker-coach**](skills/playmaker-coach/SKILL.md), a Claude Code skill
157
+ that ships with the package:
158
+
159
+ ```bash
160
+ playmaker skill install # ~/.claude/skills/playmaker-coach/SKILL.md
161
+ ```
162
+
163
+ Then give any Claude Code session a multi-component task and it activates:
164
+ proposes a split with per-model quota rationale, waits for your approval, fans
165
+ out, and reviews what comes back.
166
+
167
+ ## Commands
168
+
169
+ ```bash
170
+ playmaker agents # who's installed
171
+ playmaker quotas [--refresh] # capacity per provider, per model
172
+
173
+ playmaker dispatch <agent> --prompt "..." # detached by default
174
+ [--model NAME] # forwarded to the agent's own CLI
175
+ [--cwd DIR]
176
+ [--files PATH...]
177
+ [--sync] # block and print the final answer
178
+ [--parent ID] # link lineage to an earlier session
179
+ [--batch LABEL] # group a fan-out; one summary ping
180
+ playmaker continue <id> --prompt "..." # follow-up inside the live session
181
+ [--model NAME] [--files PATH...] [--sync]
182
+
183
+ playmaker list [--status running|done|failed] [--agent NAME] [--limit N]
184
+ playmaker get <id> [--wait] [--poll SECONDS]
185
+ playmaker summary <id> # last 2 assistant messages
186
+ playmaker thread <id> [--last N] [--all] [--role assistant|user|tool]
187
+ [--include-tools] [--max-bytes N] [--follow]
188
+ playmaker logs <id> [--follow] # subprocess stdout for detached runs
189
+ playmaker kill <id>
190
+ playmaker watch # live TUI of sessions
191
+ playmaker skill install [--dir PATH] [--force]
192
+ ```
193
+
194
+ `dispatch`, `continue`, `list`, `get`, `thread` and `quotas` all take `--json`
195
+ for scripting.
196
+
197
+ **`continue` vs a fresh `dispatch`.** `continue` sends a follow-up into the
198
+ agent's existing session, so its reasoning, tool history and file context are
199
+ still live โ€” that's the cheap path for "almost right, fix Y". Start fresh with
200
+ `--parent <id>` only when the old context has become a liability.
201
+
202
+ ## How it works
203
+
204
+ ```mermaid
205
+ flowchart LR
206
+ C["๐Ÿง‘โ€๐Ÿซ coach<br/>your Claude Code session"] -->|dispatch| P(("playmaker"))
207
+ P --> A1["claude -p<br/><i>--model sonnet</i>"]
208
+ P --> A2["codex exec"]
209
+ P --> A3["agy -p"]
210
+ A1 & A2 & A3 --> S[("state.db<br/>outputs/ ยท logs/")]
211
+ S -->|list ยท thread ยท summary| C
212
+ S -.->|batch drained| N["๐Ÿ”” one ping"]
213
+ ```
214
+
215
+ Each dispatch is a detached OS process with its own quota; playmaker owns the
216
+ bookkeeping in between.
217
+
218
+ ```
219
+ ~/.playmaker/
220
+ โ”œโ”€โ”€ state.db SQLite โ€” sessions, status, pids, models, output paths
221
+ โ”œโ”€โ”€ config.toml
222
+ โ”œโ”€โ”€ agents/ optional agent profile markdown (claude.md, codex.md, agy.mdโ€ฆ)
223
+ โ”œโ”€โ”€ outputs/ final output per session โ€” .md, or .json if the agent returned JSON
224
+ โ”œโ”€โ”€ logs/ subprocess stdout for detached runs
225
+ โ””โ”€โ”€ quotas.json latest capacity snapshot
226
+ ```
227
+
228
+ `dispatch` runs the agent's CLI non-interactively, parses its native output,
229
+ and locates the session file the tool writes locally. Empirically:
230
+
231
+ | Agent | Session transcript |
232
+ |---|---|
233
+ | Claude | `~/.claude/projects/<cwd-with-slashes-as-dashes>/<id>.jsonl` |
234
+ | Codex | `~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-<ts>-<thread_id>.jsonl` |
235
+ | Antigravity | `~/.gemini/antigravity-cli/brain/<conversation-id>/.system_generated/logs/transcript_full.jsonl` |
236
+ | Gemini | `~/.gemini/tmp/<cwd-basename>/chats/session-<ts>-<short_id>.{json,jsonl}` |
237
+
238
+ `thread` and `summary` normalize all of them into the same turn list, so every
239
+ agent reads back in one shape.
240
+
241
+ Profiles are optional and discovered, not shipped: drop
242
+ `~/.playmaker/agents/<name>.md` (or `./.playmaker/agents/<name>.md` next to a
243
+ repo, which wins) and it is prepended to every dispatch for that agent.
244
+
245
+ Two quirks worth knowing: **agy**'s shell cwd is a private scratch dir rather
246
+ than your workspace, so playmaker prepends a workspace preamble to every agy
247
+ dispatch; and a wrong `--model` is a silent failure on both **codex** (reports
248
+ `turn.failed` while exiting 0) and **agy** (runs its default model instead) โ€”
249
+ playmaker turns both into real errors.
250
+
251
+ ## Notifications
252
+
253
+ Every detached dispatch pings when it finishes. With
254
+ [`terminal-notifier`](https://github.com/julienXX/terminal-notifier) installed
255
+ (`brew install terminal-notifier`), the notification is **clickable** and opens
256
+ the agent's output file in your editor:
257
+
258
+ ```toml
259
+ [notifications]
260
+ editor = "Zed" # any app name `open -a` accepts
261
+ ```
262
+
263
+ Without terminal-notifier, playmaker falls back to plain `osascript` banners,
264
+ which can't be clicked.
265
+
266
+ **Batches.** Pass the same `--batch <label>` to every dispatch in a fan-out and
267
+ per-dispatch success pings are suppressed โ€” one "N/N done" summary fires when
268
+ the whole batch drains. Failures are the actionable event, so they still ping
269
+ immediately, with a distinct sound (Basso vs. the usual Blow).
270
+
271
+ Each session's final output lands in `~/.playmaker/outputs/<id>.md` (or `.json`
272
+ when the agent returned genuine JSON), so the notification click โ€” and
273
+ `playmaker get` โ€” always have a stable file to open.
274
+
275
+ ## Quotas
276
+
277
+ `playmaker quotas` is token-based: it reads the credentials each CLI already
278
+ stores, no scraping and no browser.
279
+
280
+ ```console
281
+ $ playmaker quotas --refresh
282
+ Claude Max 20x
283
+ Session โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘ 80% left resets in 3h 12m
284
+ Weekly โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 55% left resets in 4d 6h
285
+ Sonnet โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘ 95% left resets in 4d 6h
286
+
287
+ Codex Plus
288
+ Session โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘ 90% left resets in 1h 40m
289
+ Weekly โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 65% left resets in 2d 9h
290
+
291
+ Antigravity (agy)
292
+ Gemini 5h โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 100% left
293
+ Gemini weekly โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘ 95% left
294
+ Claude/GPT 5h โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘ 90% left resets in 2h 05m
295
+ Claude/GPT weekly โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 70% left resets in 5d 1h
296
+ ```
297
+
298
+ The `Weekly` and `Sonnet` rows above are the point: they are **separate
299
+ buckets**. So is every agy row. Routing a subtask is choosing which of them to
300
+ spend.
301
+
302
+ - **Claude** โ€” OAuth usage API; token from the Claude Code Keychain entry.
303
+ - **Codex** โ€” ChatGPT `wham/usage` API; token from `~/.codex/auth.json`.
304
+ - **Antigravity** โ€” prefers agy's **local daemon**
305
+ (`RetrieveUserQuotaSummary` over its embedded gRPC-web endpoint), which is
306
+ the only source for the full categorized breakdown above. Works whenever any
307
+ agy process has the singleton daemon up. Falls back to the OAuth
308
+ `retrieveUserQuota` on the Antigravity backend, which surfaces only coarse
309
+ Gemini buckets and is flagged *daemon offline*. Approach ported from
310
+ [steipete/CodexBar](https://github.com/steipete/CodexBar).
311
+
312
+ Reading these at *model* granularity is the point: they are the load-balancing
313
+ input the coach skill uses to route each subtask.
314
+
315
+ ## Limitations
316
+
317
+ - macOS-only for the Claude quota probe (reads the Claude Code Keychain entry)
318
+ and for notifications. Everything else works on Linux.
319
+ - No remote agents โ€” everything runs as a local subprocess on your machine.
320
+ - Quota probes read undocumented endpoints the vendors can change at any time.
321
+
322
+ ## Contributing
323
+
324
+ Handlers for other agent CLIs are especially welcome โ€” see
325
+ [CONTRIBUTING.md](CONTRIBUTING.md) for the `AgentHandler` contract and what you
326
+ need to know about a CLI before writing one.
327
+
328
+ ```bash
329
+ uv run pytest
330
+ uv run ruff check .
331
+ ```
332
+
333
+ ## License
334
+
335
+ MIT. See [LICENSE](LICENSE).