squadrant 0.13.5 → 0.14.1

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.
package/README.md CHANGED
@@ -1,310 +1,79 @@
1
1
  # Squadrant
2
2
 
3
- Multi-project orchestration layer for coding agents. One command session controls everything.
3
+ Run your coding agents like an engineering team. You're the manager
4
+ Squadrant gives you **captains** who plan and delegate, and **crews** who
5
+ write the code.
4
6
 
5
- > **Direction:** Squadrant is moving to support all major coding agents — Claude Code, Codex, Cursor, Gemini CLI — not just Claude Code. Claude Code is the reference implementation today; other agents land through the plugin system's driver abstractions and the upcoming cross-agent projection layer ([#31](https://github.com/tu11aa/squadrant/issues/31)). See [`docs/specs/2026-04-24-multi-agent-direction.md`](docs/specs/2026-04-24-multi-agent-direction.md).
6
-
7
- ## How It Works
7
+ ## The mental model
8
8
 
9
9
  ```
10
- squadrant launch <project> → Captain (per project, in cmux)
11
- squadrant launch --all → Every Captain
12
- squadrant command --task briefingOne-shot Command session in a split pane
13
- (also: --task learnings-review | wiki-aggregate)
14
- Captain squadrant crew spawn→ Crew (new tab in the captain workspace, fresh agent CLI)
10
+ You
11
+
12
+ ├── squadrant launch <project> Captain (project A)
13
+ │ │
14
+ │ └── squadrant crew spawn Crew, Crew,
15
+
16
+ └── squadrant launch <project-2> → Captain (project B)
17
+ └── …
15
18
  ```
16
19
 
17
- 1. **`squadrant init`** first-time setup
18
- 2. **`squadrant launch <project>`** start the project's captain in cmux
19
- 3. **`squadrant launch --all`** — start every captain at once
20
- 4. **`squadrant command --task briefing`** on-demand Command session for cross-project work (optional; spawns in a split pane and exits when done)
21
- 5. **`squadrant status`** quick status check without spawning anything
20
+ - **You**set direction, make the calls only you can make.
21
+ - **Captain**one per project, lives in cmux, plans and delegates. Your
22
+ standing project lead.
23
+ - **Crew** a fresh interactive sub-session the captain spawns to actually
24
+ write the code, then reports back and signals when it's done or blocked.
22
25
 
23
- ## Install
26
+ One person, operating like an engineering org.
24
27
 
25
- ```bash
26
- npm i -g squadrant # global `squadrant` CLI (alias: `squad`)
27
- squadrant init
28
- squadrant doctor
29
- ```
28
+ > Squadrant is also moving to support every major coding agent — Claude Code,
29
+ > Codex, Cursor, Gemini CLI not just Claude Code. Claude Code is the
30
+ > reference implementation today; other agents land through the plugin
31
+ > system's driver abstractions and the upcoming cross-agent projection layer
32
+ > ([#31](https://github.com/tu11aa/squadrant/issues/31)). See
33
+ > [`docs/specs/2026-04-24-multi-agent-direction.md`](docs/specs/2026-04-24-multi-agent-direction.md).
30
34
 
31
- Or from source:
35
+ ## Why you'd want it
32
36
 
33
- ```bash
34
- git clone https://github.com/tu11aa/squadrant.git
35
- cd squadrant
36
- pnpm install
37
- pnpm build # produces dist/index.js (the squadrant bin)
38
- npm link # symlinks the global `squadrant`/`squad` to dist/index.js
39
- squadrant init
40
- squadrant doctor
41
- ```
37
+ - **Run several projects at once** — one captain per project, each with its
38
+ own context, all supervised from the same session.
39
+ - **Delegate instead of micromanage** — hand a task to a crew and let it
40
+ work; you don't watch every keystroke.
41
+ - **Agents signal you, not the other way around** — crews report **CREW
42
+ DONE** / **CREW BLOCKED** / **CREW NEEDS INPUT** instead of you polling a
43
+ pane to see if it's finished.
44
+ - **Drive it from your phone** — optional two-way Telegram bridge, so you can
45
+ delegate and get notified without being at your desk.
42
46
 
43
- > Squadrant was formerly published/developed as `claude-cockpit`; it was rebranded in 0.9.0 as it grew into a multi-agent orchestration layer.
47
+ ## What it feels like
44
48
 
45
- ## Prerequisites
46
-
47
- - [Claude Code](https://claude.ai/code) >= 2.1.32
48
- - [cmux](https://cmux.dev) (macOS terminal for coding agents)
49
- - [Obsidian](https://obsidian.md) (status tracking)
50
- - Node.js >= 18
51
-
52
- ### Required Integrations
53
-
54
- ```bash
55
- # Claude Memory — cross-session continuity
56
- /plugin marketplace add thedotmack/claude-mem
57
- /plugin install claude-mem
58
-
59
- # Task Master — PRD decomposition (works via Max subscription)
60
- npm install -g task-master-ai
61
-
62
- # GSD — wave-based execution for crew (fresh context per step)
63
- npx get-shit-done-cc@latest --claude --global
64
49
  ```
50
+ squadrant launch brove
51
+ → Captain boots in cmux
65
52
 
66
- See `core/plugins.md` for full plugin setup.
67
-
68
- ### Obsidian Plugins
69
-
70
- See `obsidian/plugins.md` for Dataview, Templater setup.
71
-
72
- ## Commands
73
-
74
- | Command | Description |
75
- |---------|-------------|
76
- | `squadrant init` | First-time setup — config, hub vault, scripts |
77
- | `squadrant launch <project>` | Start a specific project captain |
78
- | `squadrant launch --all` | Launch all captain workspaces |
79
- | `squadrant command [--task <briefing\|learnings-review\|wiki-aggregate>] [--agent <a>]` | Spawn a one-shot Command session in a split pane (no persistent Command). |
80
- | `squadrant status` | Show all project status (no Claude needed) |
81
- | `squadrant standup` | Daily standup summary (zero LLM tokens) |
82
- | `squadrant doctor` | Health check — verify dependencies |
83
- | `squadrant projects list` | List registered projects |
84
- | `squadrant projects add <name> <path>` | Register a project |
85
- | `squadrant projects remove <name>` | Unregister a project |
86
- | `squadrant dashboard [--once]` | Print a one-shot status grid for all projects to the terminal. |
87
- | `squadrant dashboard --pane [--direction <dir>] [--interval <s>]` | Open a refreshing sidebar pane in the current cmux workspace. |
88
- | `squadrant dashboard sync-hub [--json]` | Mirror spoke `status.md` files into `{hubVault}/projects/` for Obsidian Dataview. |
89
- | `squadrant runtime status <project>` | Check if a project's captain workspace is running |
90
- | `squadrant runtime send <project> <msg>` | Send a message to a captain workspace (auto-Enter) |
91
- | `squadrant runtime list` | List all workspaces from the active runtime |
92
- | `squadrant workspace read <project> <path>` | Read a scope-relative file from the project's spoke vault |
93
- | `squadrant workspace list <project> <dir>` | List entries in a spoke vault directory |
94
- | `squadrant workspace read --hub <path>` | Read from the hub vault |
95
- | `squadrant notify <message>` | Send a message to the user via the configured notifier |
96
- | `squadrant projection emit [--scope user\|project] [--project <name>] [--target <name>] [--all]` | Emit squadrant rules + skills to Cursor/Codex/Gemini config files |
97
- | `squadrant projection diff [same flags]` | Preview projection changes without writing |
98
- | `squadrant projection list` | Show registered projection targets and their destinations |
99
- | `squadrant crew spawn <project> <task> [--name <n>] [--direction tab\|right\|left\|up\|down] [--agent <a>]` | Spawn an interactive crew sub-session (tab in the captain workspace by default; `--direction` for a pane) |
100
- | `squadrant crew send <project> <name> <message>` | Send a follow-up turn to an existing crew |
101
- | `squadrant crew read <project> <name>` | Read a crew session's current screen |
102
- | `squadrant crew close <project> <name>` | Shutdown a crew session (closes its tab) |
103
- | `squadrant crew list <project>` | List live crews for a project |
104
- | `squadrant shutdown [project]` | Graceful shutdown |
105
- | `squadrant effort [max\|balance\|low]` | Get or set the global crew tokenomics dial (no arg prints current) |
106
- | `squadrant retro` | Generate a retro (weekly/sprint summary) from daily logs and git (zero tokens) |
107
- | `squadrant config check` | Detect config drift vs the current default schema |
108
- | `squadrant heal [--dry-run\|daemon]` | Targeted, idempotent remediation for squadrant components (daemon, health) |
109
- | `squadrant group dispatch …` | Cross-project intra-group operations (dispatch a task to a sibling project) |
110
- | `squadrant cmux …` | cmux integration helpers |
111
- | `squadrant feedback` | Open opt-in feedback issue |
112
-
113
- ## Monorepo structure
114
-
115
- Six internal packages in a one-way dependency DAG. All are private (not published to npm).
116
-
117
- | Package | Owns | Notes |
118
- |---|---|---|
119
- | `@squadrant/shared` | Config schema, TypeScript types, constants | Leaf lib — zero internal deps |
120
- | `@squadrant/core` | Daemon logic, state-machine, protocol, `AgentDriver` interface, task/crew bus | No concrete drivers — pure interfaces + orchestration |
121
- | `@squadrant/agents` | AI driver seam — `claude`, `codex`, `opencode`, `gemini` drivers + registry | Implements `AgentDriver`. Add a new AI agent here. |
122
- | `@squadrant/workspaces` | Runtime (cmux), workspace (obsidian), notifier (cmux) drivers + registries | Implements surface/workspace/notifier seams |
123
- | `@squadrant/web` | Observability dashboard — bundled HTML/JS served by CLI | Read-only UI; inlined by CLI's tsup build |
124
- | `@squadrant/cli` | Commands, bin entry, daemon host, templates, plugin dir | Root — depends on all other packages |
125
-
126
- **Dependency DAG:** `shared ◄ core ◄ {agents, workspaces, web} ◄ cli`
127
-
128
- **Build outputs** (`pnpm build` via tsup, all internal packages inlined):
129
- - `dist/index.js` — CLI bin (`squadrant` command), entry: `packages/cli/src/index.ts`
130
- - `dist/squadrantd.js` — daemon process, entry: `packages/cli/src/daemon-host.ts`
131
-
132
- See the [architecture diagram](docs/diagrams/2026-06-18-squadrant-monorepo-architecture.html) for a visual overview.
133
-
134
- ## Architecture
135
-
136
- ### Roles
137
-
138
- - **Command** (Opus) — *on-demand* cross-project session. Spawned by `squadrant command --task <briefing|learnings-review|wiki-aggregate>` in a split pane; exits when the task completes. No persistent Command process.
139
- - **Captain** (Opus) — project leader, uses Agent Teams + git worktrees
140
- - **Crew** (Sonnet by default) — interactive sub-session running as a new tab in the captain's workspace (or a split pane via `--direction`). Each crew is named (`crew-1`, `crew-2`, …) and stays idle between turns waiting for the captain's next message — same model as a Claude Agent Team subagent. Spawn with `squadrant crew spawn`, send follow-ups with `squadrant crew send`, close when done. Works with any agent CLI (claude and opencode are fully interactive; codex/gemini currently print-mode). Uses GSD for complex tasks.
141
-
142
- ### Model Routing
143
-
144
- Each role runs on the optimal model for cost/quality tradeoff. Configured in `config.json`:
145
- - Command/Captain/Review: Opus (coordination + quality)
146
- - Crew: Sonnet (execution)
147
- - Exploration: Haiku (cheap lookups)
148
-
149
- ### Runtime Abstraction
150
-
151
- Workspaces run on a pluggable **runtime driver** (currently only `cmux`). Each project may override the global default via its `runtime` field. Bash scripts call `squadrant runtime <op>` to talk to the configured runtime instead of any specific binary. New runtimes (tmux, Docker, SSH) are added as driver files in `@squadrant/workspaces` (`packages/workspaces/runtimes/`) — see `docs/specs/archive/2026-04-20-plugin-system-runtime-design.md`.
152
-
153
- ### Workspace Abstraction
154
-
155
- Vault storage (hub + per-project spokes) runs behind a pluggable **workspace driver** (currently only `obsidian`). Filesystem operations — `read`, `write`, `list`, `exists`, `mkdir` — go through the driver instead of `fs` directly. Each project may override the global default via its `workspace` field. Bash scripts call `squadrant workspace <op>` to read/write vault data without hardcoding paths. New backends (Notion, plain-md, S3) are added as driver files in `@squadrant/workspaces` (`packages/workspaces/workspaces/`) — see `docs/specs/archive/2026-04-21-plugin-system-workspace-design.md`.
156
-
157
- ### Notifier Abstraction
158
-
159
- User-facing notifications run behind a pluggable **notifier driver** (currently only `cmux`). Escalations and other "tell the user" events go through `squadrant notify <message>`. The default `CmuxNotifier` delegates to `squadrant runtime send --command` — the abstraction exists as a swap-point for future Slack/Discord/email/pager drivers. Notifier is global (no per-project override). See `docs/specs/archive/2026-04-21-plugin-system-notifier-design.md`.
160
-
161
- ### Crew Spawn (Interactive Sub-Sessions)
162
-
163
- Crew is the captain's equivalent of an Agent Team subagent — but runtime-agnostic. The captain spawns a crew via `squadrant crew spawn <project> "<task>" [--name <n>]`, which opens a new tab in the captain's cmux workspace, boots an interactive Claude session (no `-p`), and sends the task as the first turn. The crew works on it and **stays idle** waiting for follow-ups. The captain drives the session with `squadrant crew send/read/close/list`, addressing each crew by its tab title (`🔧 <project>:<name>`).
53
+ You (in the captain pane): "build the export-to-CSV feature"
54
+ → Captain spawns a crew: squadrant crew spawn brove "build export-to-CSV"
164
55
 
165
- Pass `--direction right|left|up|down` to use a split pane instead of a tab. State lives in the surface buffer + git; tabs die with the captain workspace on `squadrant shutdown`. Non-Claude agents (codex/gemini) currently still launch in print-mode; full interactive support is a follow-up. See [`docs/specs/archive/2026-05-05-squadrant-thin-redirect-design.md`](docs/specs/archive/2026-05-05-squadrant-thin-redirect-design.md).
56
+ Crew works in its own tab, writes the code, runs tests
166
57
 
167
- ### Effort Dial (Tokenomics)
168
-
169
- `squadrant effort max|balance|low` is a single global dial that biases how aggressively crews consume tokens — a captain-discretion signal, not mechanical routing. `max` favors quality/tokens, `low` biases toward economy (e.g. preferring opencode for cheap work); `balance` sits between. Run `squadrant effort` with no argument to print the current setting. The value lives in config and is honored by captains via the captain-ops playbook ([#317](https://github.com/tu11aa/squadrant/issues/317) / [#381](https://github.com/tu11aa/squadrant/pull/381)).
170
-
171
- ### Crew Lifecycle & Delivery
172
-
173
- - **Daemon-direct delivery** — crew turns and handoffs are delivered straight to the cmux surface by the daemon. The old `notify-relay` supervisor was deleted; there is no relay process to keep alive ([#332](https://github.com/tu11aa/squadrant/issues/332)).
174
- - **Semantic heartbeat** — crews emit a lifecycle signal the captain reads as **CREW IDLE / QUIET / STALLED**, distinguishing "waiting for you" from "wedged" without scraping the pane ([#354](https://github.com/tu11aa/squadrant/issues/354)).
175
- - **`stopped` project status + orphan reap** — when a captain goes away, the daemon reaps its orphaned crews and marks the project `stopped` (intentional shutdown) rather than leaving stale tabs or faulting ([#324](https://github.com/tu11aa/squadrant/issues/324) / [#323](https://github.com/tu11aa/squadrant/issues/323) / [#388](https://github.com/tu11aa/squadrant/pull/388)).
176
-
177
- ### Telegram (Two-Way, opt-in)
178
-
179
- Drive squadrant from your phone ([#65](https://github.com/tu11aa/squadrant/issues/65)). When a `telegram` block is present in config, a daemon-internal bridge:
180
-
181
- - **Outbound** — pushes each project's crew lifecycle events (CREW DONE / FAILED / BLOCKED / APPROVAL / INPUT / TIMEOUT / IDLE) and other captain notifications to that project's Telegram forum topic, filtered by a per-project **crew tier** (see [Notification tuning](#notification-tuning-per-project) below). Best-effort: a Telegram failure never delays or breaks delivery to the captain pane.
182
- - **Inbound (project topic)** — a message you send in a project's topic is delivered into that project's **captain pane** as a labeled `📩 [from Telegram]` message; the captain decides what to do with it. With remote control enabled (see below), if no captain is alive the daemon **auto-launches** one, then delivers ([#403](https://github.com/tu11aa/squadrant/issues/403)).
183
- - **General command channel** — slash commands in the supergroup's **General topic** run a curated set of squadrant operations from your phone ([#402](https://github.com/tu11aa/squadrant/issues/402)). Available: `/help`, `/status`, `/projects`, `/crews <project>`, `/launch <project>`, `/effort [max|balance|low]`, `/config get <key>`, `/config set <key> <value>`, `/spawn <project> <task…>`. Each maps to a validated CLI argv run via async `execFile` — never a shell passthrough. Unknown commands and freeform text get a `/help` hint.
184
-
185
- Absent the config block, the bridge is never constructed — zero behavior change. No runtime SDK is added (plain `fetch`; `@grammyjs/types` is a dev-only type dependency).
186
-
187
- **Setup (recommended):**
188
- 1. Create a bot with [@BotFather](https://t.me/BotFather) and copy its token.
189
- 2. Run `squadrant telegram setup` — it prompts for the bot token (input hidden), validates it via the Bot API, auto-detects your supergroup id, captures your Telegram user-id, and offers to enable remote control.
190
- 3. Bind a project to a topic: `squadrant telegram link <project>` (creates the forum topic and records the binding).
191
- 4. Check wiring with `squadrant telegram status`.
192
-
193
- **Setup (manual):**
194
- Put the token + ids in config (or export `TELEGRAM_BOT_TOKEN`) — see the `telegram` block under [Config](#config). Then run `squadrant telegram link <project>`.
195
-
196
- #### Security model (#321) — fail-closed remote control
197
-
198
- The control surfaces (auto-launch + General command channel) are **off by default** and gated by two independent checks. A control action runs **only when both** hold:
199
-
200
- 1. `remoteControl: true` — an explicit opt-in master switch (default `false`).
201
- 2. `message.from.id ∈ users[]` — the sender's Telegram **user-id** is on the allowlist. An empty/absent `users` list ⇒ control is disabled (fail-closed). Chat membership alone is **never** enough for control.
202
-
203
- When remote control is off (the default after upgrade), behavior is **exactly v1**: project-topic messages queue to the captain pane (no auto-launch), and General-topic slash commands are rejected with `⛔ not authorized`. Inbound text is always treated as data; only the curated registry maps to actions, and `/config set` is restricted to a default-deny writable-key allowlist (currently just `defaults.effort`) — **secrets (`botToken`, `users`, `chats`, `supergroupId`) can never be written over Telegram.**
204
-
205
- #### Notification tuning (per-project)
206
-
207
- Notifications resolve through a **layered config**: built-in defaults → global `config.json` (`telegram.notify`) → per-project `~/.config/squadrant/projects/<name>.json`, merged per key (overriding one key never resets its siblings). Two axes are independent:
208
-
209
- - **Live mute (`active`)** — system-tracked *session* state in `telegram-state.json`. Flipped by engagement (any message into a topic auto-unmutes), `/mute` / `/unmute` (Telegram), or `squadrant telegram notify <project> on|off`. The live value wins over the config default when present.
210
- - **Deliberate preferences (`crew`, `cap`)** — persistent settings in the per-project config file. Written by `squadrant telegram notify <project> crew <tier>` / `cap <on|off>` (CLI) or `/notify crew <tier>` / `/notify cap <on|off>` (Telegram, fail-closed behind remote control).
211
-
212
- **Crew tiers** (cumulative) select which lifecycle events reach a topic when active:
213
-
214
- | Tier | Events delivered |
215
- |---|---|
216
- | `none` | nothing |
217
- | `done_only` | `task.done`, `task.failed` |
218
- | `alert_only` *(default)* | `done_only` + `task.blocked`, `task.approval.requested`, `task.input.requested`, `task.timeout` |
219
- | `all` | every lifecycle event (incl. progress/heartbeat noise) |
220
-
221
- **`cap`** (default `on`) gates explicit captain pushes via `squadrant telegram send` — set `cap off` for a project to stop the captain DM-ing you there (independent of idle-mute; an explicit push is not dropped just because the topic is idle-muted).
222
-
223
- Config (deliberate prefs) and state (live toggles) are kept separate by design: `/unmute` flips your session, it does **not** rewrite your config file. An absent `projects/<name>.json` behaves exactly as the global defaults — the layer is fully additive, no migration. See `docs/superpowers/specs/2026-06-23-per-project-layered-config-design.md`.
224
-
225
- #### Remote wake (#403) — operator-side
226
-
227
- Auto-launch boots a captain when the **daemon is already running**. Waking a *sleeping Mac* from your phone (Wake-on-LAN / a relay that nudges the machine) is operator-side infrastructure, out of scope for this repo — see [#403](https://github.com/tu11aa/squadrant/issues/403) for the end-to-end flow.
228
-
229
- > **Interim note (link ↔ daemon 409):** the Telegram Bot API allows only one `getUpdates` consumer at a time. The `setup` wizard polls `getUpdates` to detect your group/user-id, so run it with the daemon stopped. `link` uses only `createForumTopic`, so it's unaffected.
230
-
231
- ### Projection (Cross-Agent Config Sync)
232
-
233
- Squadrant rules (Karpathy principles, captain-ops) and per-project AGENTS.md emit to each supported agent's canonical path via `squadrant projection emit`. User-level projection pushes squadrant's skills to `~/.cursor/rules/squadrant-global.mdc`, `~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md`. Project-level projection pushes a managed project's own `AGENTS.md` into `{project}/CLAUDE.md`, `{project}/.cursor/rules/squadrant.mdc`, `{project}/GEMINI.md` — zero squadrant-global content leaks into the project repo. Shared files use `<!-- squadrant:start --> ... <!-- squadrant:end -->` markers; dedicated files overwrite. See `docs/specs/archive/2026-04-24-plugin-system-projection-design.md`.
234
-
235
- The user-level projection now also inlines `templates/captain.generic.md` and `templates/crew.generic.md` as `## Captain Role` / `## Crew Role` sections inside the squadrant marker block, so non-Claude agents (Codex, Gemini, Cursor) load the same role descriptions Claude Code loads via `--append-system-prompt-file`. See `docs/specs/archive/2026-05-05-multi-agent-template-parity-plan.md` (#45).
236
-
237
- ### Obsidian Vaults (Hub-and-Spoke)
238
-
239
- - **Hub vault** (`~/squadrant-hub`) — cross-project dashboard + hub wiki
240
- - **Spoke vaults** — per-project status, learnings, and wiki
241
-
242
- ### Knowledge System (opt-in writes)
243
-
244
- - **Status (opt-in)** — captains record `{spokeVault}/status.md` via `write-status.sh` (also written by the captain session-end hook) when there's something worth noting (a blocker, "starting work on X"). Not on a schedule.
245
- - **Dashboard** — `squadrant dashboard --pane` opens a refreshing sidebar pane in cmux that lists every project's live state, queried from the squadrant daemon's task records. `squadrant dashboard sync-hub` mirrors each spoke `status.md` into `{hubVault}/projects/` so the hub vault's `dashboard.md` Dataview query renders the same data inside Obsidian.
246
- - **Handoff files** — captain writes when in-flight work needs to survive into tomorrow; skipped on uneventful sessions.
247
- - **Daily logs** — captain writes when the day produced something worth a log; not on a schedule.
248
- - **Learnings** — recorded when a captain encounters a genuinely surprising or reusable pattern.
249
- - **Wiki** — compiled, indexed knowledge pages in spoke vaults (`wiki/pages/`); promoted from learnings when worth maintaining.
250
- - **Hub Wiki** — cross-project knowledge aggregated by an on-demand `squadrant command --task wiki-aggregate` run.
251
- - Scripts: `wiki-ingest.sh`, `wiki-query.sh`, `wiki-log.sh`.
252
-
253
- ### Session Continuity
254
-
255
- - **Handoff files** — captain writes context on shutdown, reads on startup
256
- - **Session freshness** — auto-detects new day or template changes, forces fresh context
257
- - **claude-mem** — cross-session memory via MCP plugin
58
+ 🎉 CREW DONE — captain reviews, merges or follows up
59
+ ```
258
60
 
259
- ## Config
61
+ Full walkthrough, including your first captain and crew, step by step: see
62
+ [QUICKSTART.md](QUICKSTART.md).
260
63
 
261
- `~/.config/squadrant/config.json`
64
+ ## Install
262
65
 
263
- ```json
264
- {
265
- "commandName": "command",
266
- "hubVault": "~/squadrant-hub",
267
- "runtime": "cmux",
268
- "workspace": "obsidian",
269
- "notifier": "cmux",
270
- "telegram": {
271
- "botToken": "123456:ABC...",
272
- "supergroupId": -1001234567890,
273
- "chats": [-1001234567890],
274
- "users": [987654321],
275
- "remoteControl": true,
276
- "pollMs": 1000
277
- },
278
- "projects": {
279
- "brove": {
280
- "path": "~/projects/brove",
281
- "captainName": "brove-captain",
282
- "spokeVault": "~/squadrant-hub/spokes/brove",
283
- "host": "local",
284
- "runtime": "cmux",
285
- "workspace": "obsidian",
286
- }
287
- },
288
- "defaults": {
289
- "maxCrew": 5,
290
- "worktreeDir": ".worktrees",
291
- "teammateMode": "in-process",
292
- "permissions": {
293
- "command": "default",
294
- "captain": "acceptEdits"
295
- },
296
- "models": {
297
- "command": "opus",
298
- "captain": "opus",
299
- "crew": "sonnet",
300
- "exploration": "haiku",
301
- "review": "opus"
302
- }
303
- }
304
- }
66
+ ```bash
67
+ npm i -g squadrant # global `squadrant` CLI (alias: `squad`)
68
+ squadrant init
69
+ squadrant doctor
305
70
  ```
306
71
 
307
- The `telegram` block is **optional** — omit it and the Telegram bridge is never constructed. `botToken` may be left out of the file and supplied via the `TELEGRAM_BOT_TOKEN` env var instead. `chats` is the inbound `chat_id` allowlist; `users` is the per-user-id allowlist for **control** actions and `remoteControl` (default `false`) is the master opt-in for auto-launch + the General command channel — both must be set for any remote control to act (fail-closed, [#321](https://github.com/tu11aa/squadrant/issues/321)). `pollMs` (default `1000`) is the inbound long-poll cadence. See [Telegram (Two-Way, opt-in)](#telegram-two-way-opt-in).
72
+ See [QUICKSTART.md](QUICKSTART.md) for prerequisites, the from-source build,
73
+ and a guided first run.
74
+
75
+ > Squadrant was formerly published/developed as `claude-cockpit`; it was
76
+ > rebranded in 0.9.0 as it grew into a multi-agent orchestration layer.
308
77
 
309
78
  ## Supported Agents
310
79
 
@@ -316,7 +85,19 @@ The `telegram` block is **optional** — omit it and the Telegram bridge is neve
316
85
  | Gemini CLI | ✅ projection (skills + roles) | Captain/crew roles inlined into `~/.gemini/GEMINI.md` (#45). |
317
86
  | opencode | ✅ driver + projection (interactive crew) | `opencode run "<prompt>"` with `--format json` / `-m <model>`; AGENTS.md projects to `~/.config/opencode/AGENTS.md`. |
318
87
 
319
- Cross-agent config sync (one canonical source → agent-specific formats) is tracked in [#31](https://github.com/tu11aa/squadrant/issues/31).
88
+ Cross-agent config sync (one canonical source → agent-specific formats) is
89
+ tracked in [#31](https://github.com/tu11aa/squadrant/issues/31).
90
+
91
+ ## Where to go next
92
+
93
+ - [QUICKSTART.md](QUICKSTART.md) — hands-on first run: install, first
94
+ captain, first crew, optional Telegram
95
+ - [Architecture diagram](docs/diagrams/2026-06-18-squadrant-monorepo-architecture.html)
96
+ — visual overview of the monorepo
97
+ - [docs/reference.md](docs/reference.md) — full command table, monorepo
98
+ structure, architecture deep-dive, Telegram integration, config schema
99
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — setup, branching, and conventions for
100
+ contributing
320
101
 
321
102
  ## Inspirations
322
103
 
package/dist/index.js CHANGED
@@ -3773,6 +3773,8 @@ var init_cmux_store_source = __esm({
3773
3773
  debounceTimer;
3774
3774
  /** taskId → last reported snapshot (for snapshot() liveness floor). */
3775
3775
  cache = /* @__PURE__ */ new Map();
3776
+ active = false;
3777
+ lastError = null;
3776
3778
  constructor(opts = {}) {
3777
3779
  this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join13(homedir8(), ".cmuxterm");
3778
3780
  this.debounceMs = opts.debounceMs ?? 50;
@@ -3788,10 +3790,13 @@ var init_cmux_store_source = __esm({
3788
3790
  }
3789
3791
  start(deps) {
3790
3792
  this.deps = deps;
3793
+ this.active = true;
3794
+ this.lastError = null;
3791
3795
  this.scan();
3792
3796
  try {
3793
3797
  this.stopWatcher = this.watchDir(this.stateDir, () => this.scheduleDebounced());
3794
3798
  } catch (e) {
3799
+ this.lastError = e.message;
3795
3800
  this.log(`cmux-store: failed to watch ${this.stateDir}: ${e.message}`);
3796
3801
  }
3797
3802
  }
@@ -3804,11 +3809,16 @@ var init_cmux_store_source = __esm({
3804
3809
  this.stopWatcher = void 0;
3805
3810
  this.deps = void 0;
3806
3811
  this.cache.clear();
3812
+ this.active = false;
3807
3813
  }
3808
3814
  /** Returns the last-reported snapshot for a known crew (liveness floor). */
3809
3815
  snapshot(taskId) {
3810
3816
  return this.cache.get(taskId);
3811
3817
  }
3818
+ /** Read-only source health (B4 — dashboard visibility into which sources are up). */
3819
+ health() {
3820
+ return { active: this.active, error: this.lastError };
3821
+ }
3812
3822
  // ── private ─────────────────────────────────────────────────────────────────
3813
3823
  scheduleDebounced() {
3814
3824
  if (this.debounceTimer !== void 0) {
@@ -3989,6 +3999,7 @@ var init_native_hook_source = __esm({
3989
3999
  deps;
3990
4000
  /** taskId → last-reported snapshot, for snapshot() liveness floor. */
3991
4001
  cache = /* @__PURE__ */ new Map();
4002
+ active = false;
3992
4003
  constructor(opts = {}) {
3993
4004
  this.hookInstall = opts.hookInstall ?? {};
3994
4005
  this.log = opts.log ?? (() => {
@@ -3996,15 +4007,21 @@ var init_native_hook_source = __esm({
3996
4007
  }
3997
4008
  start(deps) {
3998
4009
  this.deps = deps;
4010
+ this.active = true;
3999
4011
  }
4000
4012
  stop() {
4001
4013
  this.deps = void 0;
4002
4014
  this.cache.clear();
4015
+ this.active = false;
4003
4016
  }
4004
4017
  /** Returns the last-reported snapshot for a known crew (liveness floor poll). */
4005
4018
  snapshot(taskId) {
4006
4019
  return this.cache.get(taskId);
4007
4020
  }
4021
+ /** Read-only source health (B4). Purely push-driven — never errors on its own. */
4022
+ health() {
4023
+ return { active: this.active, error: null };
4024
+ }
4008
4025
  /**
4009
4026
  * Install squadrant-owned hooks into ~/.claude/settings.json.
4010
4027
  * Idempotent — safe to call on every project init or crew spawn.
@@ -5229,17 +5246,24 @@ var init_codex_app_server_source = __esm({
5229
5246
  deps;
5230
5247
  /** taskId → last reported snapshot (for snapshot() liveness floor). */
5231
5248
  cache = /* @__PURE__ */ new Map();
5249
+ active = false;
5232
5250
  start(deps) {
5233
5251
  this.deps = deps;
5252
+ this.active = true;
5234
5253
  }
5235
5254
  stop() {
5236
5255
  this.deps = void 0;
5237
5256
  this.cache.clear();
5257
+ this.active = false;
5238
5258
  }
5239
5259
  /** Returns the last-reported snapshot for a known crew (liveness floor). */
5240
5260
  snapshot(taskId) {
5241
5261
  return this.cache.get(taskId);
5242
5262
  }
5263
+ /** Read-only source health (B4). Purely push-driven — never errors on its own. */
5264
+ health() {
5265
+ return { active: this.active, error: null };
5266
+ }
5243
5267
  /**
5244
5268
  * Feed a ControlEvent from CodexInteractiveDriver into this source.
5245
5269
  * The daemon wires: emit = (ev) => { source.observe(ev); handle(ev); }
@@ -8552,7 +8576,7 @@ function collect(snap) {
8552
8576
  ];
8553
8577
  const daemonStates = [];
8554
8578
  const projStates = [];
8555
- let errors = 0, behind = 0, crewAgeMs = 0;
8579
+ let errors = 0, behind = 0, crewAgeMs = 0, undelivered = 0, maxDeferCount = 0;
8556
8580
  if (snap.daemon !== "unreachable") {
8557
8581
  const t0 = snap.daemon.tier0;
8558
8582
  daemonStates.push("alive");
@@ -8563,12 +8587,15 @@ function collect(snap) {
8563
8587
  projStates.push(c.state);
8564
8588
  if (c.kind === "crew" && c.lastSeenMs != null)
8565
8589
  crewAgeMs = Math.max(crewAgeMs, now - c.lastSeenMs);
8590
+ if (c.kind === "crew" && c.detail?.startsWith("undelivered"))
8591
+ undelivered++;
8566
8592
  }
8567
8593
  for (const p of snap.daemon.tier2.projects) {
8568
8594
  if (p.delivery.behind > 0)
8569
8595
  projStates.push("stale");
8570
8596
  if (p.store.corruptCount > 0)
8571
8597
  projStates.push("gone");
8598
+ maxDeferCount = Math.max(maxDeferCount, p.deferral.maxDeferCount);
8572
8599
  }
8573
8600
  behind = liveDeliveryBehind(snap.daemon);
8574
8601
  }
@@ -8580,7 +8607,9 @@ function collect(snap) {
8580
8607
  overall: tally([...daemonStates, ...projStates, ...envStates]),
8581
8608
  errors,
8582
8609
  behind,
8583
- crewAgeMs
8610
+ crewAgeMs,
8611
+ undelivered,
8612
+ maxDeferCount
8584
8613
  };
8585
8614
  }
8586
8615
  function sectionHead(title, sub) {
@@ -8607,11 +8636,14 @@ function renderOverview(snap, col) {
8607
8636
  const word = linkLost ? "LINK LOST" : MASTER_WORD[mc];
8608
8637
  const out = [`<section class="panel" data-panel="overview" role="tabpanel" aria-label="Overview">`];
8609
8638
  out.push(sectionHead("System Health", `${col.overall.alive} of ${col.overall.total} monitored components are alive \u2014 the ring shows the full breakdown by state.`));
8639
+ if (col.undelivered > 0) {
8640
+ out.push(banner("warn", `\u26A0 ${col.undelivered} CREW${col.undelivered > 1 ? "S" : ""} UNDELIVERED \u2014 first turn may not have landed; re-send the task or check the spawn (see Projects tab)`));
8641
+ }
8610
8642
  out.push(`<div class="hero a-${mc}">`, `<div class="gauge">`, donut(col.overall), `<div class="gauge-core"><span class="gauge-n" data-countup="monitored" data-value="${col.overall.total}">${col.overall.total}</span><span class="gauge-word">${esc(word)}</span><span class="gauge-cap">monitored</span></div>`, `</div>`, `<div class="legend">`, legendRow("alive", col.overall.alive), legendRow("stale", col.overall.stale), legendRow("gone", col.overall.gone), ...col.overall.stopped > 0 ? [legendRow("stopped", col.overall.stopped)] : [], legendRow("unknown", col.overall.unknown), `</div>`, `</div>`);
8611
8643
  out.push(sectionHead("Health by Tier", "How many components are healthy in each layer of the stack."));
8612
8644
  out.push(`<div class="tier-grid">`, tierCard("Daemon", col.daemonT, "squadrantd process, build freshness & log volume (Tier 0)"), tierCard("Projects", col.projT, "captains, crews & message data plane (Tier 1/2)"), tierCard("Environment", col.envT, "agent CLIs, vaults & config integrity (Tier 3/4)"), `</div>`);
8613
8645
  out.push(sectionHead("Live Trends", "Magnitudes tracked over time \u2014 each sparkline builds as new updates arrive."));
8614
- out.push(`<div class="trend-grid">`, trendCard("errors", "Daemon log", `${col.errors}`, "errors the daemon logged in the last window"), trendCard("behind", "Delivery lag", `${col.behind}`, "messages captains have not read yet"), trendCard("crewAge", "Crew heartbeat", linkLost ? "\u2014" : fmtDur(col.crewAgeMs), "time since the quietest crew was last seen"), `</div>`);
8646
+ out.push(`<div class="trend-grid">`, trendCard("errors", "Daemon log", `${col.errors}`, "errors the daemon logged in the last window"), trendCard("behind", "Delivery lag", `${col.behind}`, "messages captains have not read yet"), trendCard("crewAge", "Crew heartbeat", linkLost ? "\u2014" : fmtDur(col.crewAgeMs), "time since the quietest crew was last seen"), trendCard("defers", "Delivery defers", `${col.maxDeferCount}`, "highest in-flight retry count for any project's captain delivery (#484/#466)"), `</div>`);
8615
8647
  out.push(`</section>`);
8616
8648
  return out.join("");
8617
8649
  }
@@ -8647,10 +8679,12 @@ function renderProjects(snap, now) {
8647
8679
  const comps = d.tier1.filter((c) => c.project === project);
8648
8680
  const dp = d.tier2.projects.find((p) => p.project === project);
8649
8681
  const captainStopped = comps.some((c) => c.kind === "captain" && c.state === "stopped");
8682
+ const deferralState = !captainStopped && dp && dp.deferral.stuck ? "gone" : !captainStopped && dp && dp.deferral.maxDeferCount > 0 ? "stale" : "alive";
8650
8683
  const rollupStates = [
8651
8684
  ...comps.map((c) => c.state),
8652
8685
  !captainStopped && dp && dp.delivery.behind > 0 ? "stale" : "alive",
8653
- dp && dp.store.corruptCount > 0 ? "gone" : "alive"
8686
+ dp && dp.store.corruptCount > 0 ? "gone" : "alive",
8687
+ deferralState
8654
8688
  ];
8655
8689
  const rollup = worst(rollupStates);
8656
8690
  out.push(`<article class="card" data-rollup="${rollup}">`);
@@ -8662,7 +8696,7 @@ function renderProjects(snap, now) {
8662
8696
  }
8663
8697
  if (dp) {
8664
8698
  const counts = Object.entries(dp.store.byState).map(([s, n]) => pill("unknown", `${n} ${s}`)).join("");
8665
- out.push(`<div class="dp-grid">`, `<div class="dp-block"><span class="dp-l">mailbox</span><span class="dp-v"><span class="mono">${dp.mailbox.maxSeq}</span> entries \xB7 ${fmtBytes(dp.mailbox.sizeBytes)} \xB7 rotated ${dp.mailbox.rotationCount} \xB7 oldest ${fmtAge(dp.mailbox.oldestEntryAgeMs)}</span></div>`, `<div class="dp-block"><span class="dp-l">captain delivery</span>${deliveryBar(dp.delivery.behind, dp.mailbox.maxSeq)}<span class="dp-v"><span class="mono">${dp.delivery.behind}</span> behind</span></div>`, `<div class="dp-block"><span class="dp-l">task store</span><span class="chips">${counts || `<span class="dim small">no tasks</span>`}${dp.store.corruptCount > 0 ? pill("gone", `${dp.store.corruptCount} corrupt`) : ""}</span></div>`, `</div>`);
8699
+ out.push(`<div class="dp-grid">`, `<div class="dp-block"><span class="dp-l">mailbox</span><span class="dp-v"><span class="mono">${dp.mailbox.maxSeq}</span> entries \xB7 ${fmtBytes(dp.mailbox.sizeBytes)} \xB7 rotated ${dp.mailbox.rotationCount} \xB7 oldest ${fmtAge(dp.mailbox.oldestEntryAgeMs)}</span></div>`, `<div class="dp-block"><span class="dp-l">captain delivery</span>${deliveryBar(dp.delivery.behind, dp.mailbox.maxSeq)}<span class="dp-v"><span class="mono">${dp.delivery.behind}</span> behind${dp.deferral.maxDeferCount > 0 ? ` \xB7 <span class="mono">${dp.deferral.maxDeferCount}</span> deferred${dp.deferral.stuck ? ` ${pill("gone", "delivery stuck")}` : ""}` : ""}</span></div>`, `<div class="dp-block"><span class="dp-l">task store</span><span class="chips">${counts || `<span class="dim small">no tasks</span>`}${dp.store.corruptCount > 0 ? pill("gone", `${dp.store.corruptCount} corrupt`) : ""}</span></div>`, `</div>`);
8666
8700
  }
8667
8701
  out.push(`</article>`);
8668
8702
  }
@@ -8684,7 +8718,14 @@ function renderDaemon(snap) {
8684
8718
  const sweep = t0.sweep.ageMs == null ? "awaiting first sweep" : `last ${fmtAge(t0.sweep.ageMs)} \xB7 ${fmtDur(t0.sweep.cadenceMs)} cadence`;
8685
8719
  const sweepState = t0.sweep.ageMs == null ? "unknown" : "alive";
8686
8720
  const logState = t0.log.errorCount > 0 ? "stale" : "alive";
8687
- out.push(`<div class="instr-grid">`, instr("process", `<span class="mono">squadrantd</span> ${statePill("alive")}`, `<span class="instr-sub">pid ${t0.pid} \xB7 v${esc(t0.version)}</span>`), instr("uptime", `<span class="mono">${fmtDur(t0.uptimeMs)}</span>`), instr("build", `${pill(buildState, t0.build.state)}`, t0.build.state === "stale" ? remediation("npm run build && squadrant heal daemon") : ""), instr("sweep", `${pill(sweepState, sweep)}`), `</div>`);
8721
+ const tg = t0.telegram;
8722
+ const tgState = !tg.configured ? "unknown" : !tg.polling ? "gone" : tg.lastError ? "stale" : "alive";
8723
+ const tgLabel = !tg.configured ? "not configured" : !tg.polling ? "poll loop stopped" : tg.lastError ? `polling \xB7 ${tg.lastError}` : `polling \xB7 last ok ${fmtAge(tg.lastSuccessfulPollAt == null ? null : snap.generatedAt - tg.lastSuccessfulPollAt)}`;
8724
+ out.push(`<div class="instr-grid">`, instr("process", `<span class="mono">squadrantd</span> ${statePill("alive")}`, `<span class="instr-sub">pid ${t0.pid} \xB7 v${esc(t0.version)}</span>`), instr("uptime", `<span class="mono">${fmtDur(t0.uptimeMs)}</span>`), instr("build", `${pill(buildState, t0.build.state)}`, t0.build.state === "stale" ? remediation("npm run build && squadrant heal daemon") : ""), instr("sweep", `${pill(sweepState, sweep)}`), instr("telegram", `${pill(tgState, tgLabel)}`), ...t0.lifecycleSources.map((s) => {
8725
+ const state = !s.active ? "gone" : s.error ? "stale" : "alive";
8726
+ const label = !s.active ? "inactive" : s.error ? s.error : "active";
8727
+ return instr(s.name, `${pill(state, label)}`);
8728
+ }), `</div>`);
8688
8729
  out.push(`<div class="trend-grid">`, [
8689
8730
  `<div class="trend">`,
8690
8731
  `<div class="trend-head"><span class="trend-l">Log errors</span><span class="trend-v">${pill(logState, `${t0.log.errorCount}`)}</span></div>`,
@@ -8744,6 +8785,7 @@ function metricsBlob(col, linkLost) {
8744
8785
  errors: linkLost ? 0 : col.errors,
8745
8786
  behind: linkLost ? 0 : col.behind,
8746
8787
  crewAgeMs: linkLost ? 0 : col.crewAgeMs,
8788
+ maxDeferCount: linkLost ? 0 : col.maxDeferCount,
8747
8789
  alive: col.overall.alive,
8748
8790
  stale: col.overall.stale,
8749
8791
  gone: col.overall.gone,
@@ -8963,7 +9005,7 @@ var CLIENT_JS = `
8963
9005
  (function(){
8964
9006
  var activeTab='overview';var hist={};var prev={};
8965
9007
  function pushHist(k,v,t){var a=hist[k]||(hist[k]=[]);if(a.length&&a[a.length-1].t===t)return;a.push({t:t,v:v});if(a.length>48)a.shift();}
8966
- function ingest(){var el=document.getElementById('squadrant-metrics');if(!el)return;var m;try{m=JSON.parse(el.textContent);}catch(e){return;}pushHist('errors',m.errors,m.t);pushHist('behind',m.behind,m.t);pushHist('crewAge',Math.round(m.crewAgeMs/1000),m.t);}
9008
+ function ingest(){var el=document.getElementById('squadrant-metrics');if(!el)return;var m;try{m=JSON.parse(el.textContent);}catch(e){return;}pushHist('errors',m.errors,m.t);pushHist('behind',m.behind,m.t);pushHist('crewAge',Math.round(m.crewAgeMs/1000),m.t);pushHist('defers',m.maxDeferCount,m.t);}
8967
9009
  function sparkSVG(a){var W=100,H=28,p=2;if(!a.length)return'';var vs=a.map(function(x){return x.v;});var mx=Math.max.apply(null,vs),mn=Math.min.apply(null,vs);if(mx===mn)mx=mn+1;var n=a.length;var pts=a.map(function(x,i){var px=n>1?p+i/(n-1)*(W-2*p):W/2;var py=H-p-(x.v-mn)/(mx-mn)*(H-2*p);return[px,py];});var d=pts.map(function(pt,i){return(i?'L':'M')+pt[0].toFixed(1)+' '+pt[1].toFixed(1);}).join(' ');var last=pts[pts.length-1];var area=d+' L'+last[0].toFixed(1)+' '+H+' L'+pts[0][0].toFixed(1)+' '+H+' Z';return'<path class="spark-area" d="'+area+'"/><path class="spark-line" d="'+d+'"/><circle class="spark-dot" cx="'+last[0].toFixed(1)+'" cy="'+last[1].toFixed(1)+'" r="2"/>';}
8968
9010
  function drawSparks(){var ns=document.querySelectorAll('[data-spark]');for(var i=0;i<ns.length;i++){ns[i].innerHTML=sparkSVG(hist[ns[i].getAttribute('data-spark')]||[]);}}
8969
9011
  function applyTab(){var ts=document.querySelectorAll('[data-tab]');for(var i=0;i<ts.length;i++){var on=ts[i].getAttribute('data-tab')===activeTab;ts[i].setAttribute('aria-selected',on?'true':'false');ts[i].classList.toggle('on',on);}var ps=document.querySelectorAll('[data-panel]');for(var j=0;j<ps.length;j++){var on2=ps[j].getAttribute('data-panel')===activeTab;ps[j].hidden=!on2;}}