Full command list, monorepo layout, architecture deep-dive, Telegram integration, and config schema. If you're new here, start with the README for the pitch and QUICKSTART for a guided first run — come back here when you need the details.
- Commands
- Monorepo structure
- Architecture
- Roles
- Model Routing
- Thinking Level (per-role)
- Runtime Abstraction
- Workspace Abstraction
- Notifier Abstraction
- Lifecycle Sources
- Control/Captain Channel (#667)
- Crew Spawn (Interactive Sub-Sessions)
- Answering a Crew's Open Prompt (#592)
- Effort Dial (Tokenomics)
- Crew Lifecycle & Delivery
- Telegram (Two-Way, opt-in)
- Projection (Cross-Agent Config Sync)
- Obsidian Vaults (Hub-and-Spoke)
- Knowledge System (opt-in writes)
- Session Continuity
- Config
| Command | Description |
|---|---|
squadrant init |
First-time setup — config, hub vault, scripts |
squadrant launch <project> |
Start a specific project captain |
squadrant launch --all |
Launch all captain workspaces |
squadrant command [--task <briefing|learnings-review|wiki-aggregate>] [--agent <a>] |
Spawn a one-shot Command session in a split pane (no persistent Command). |
squadrant status |
Show all project status (no Claude needed) |
squadrant standup |
Daily standup summary (zero LLM tokens) |
squadrant doctor |
Health check — verify dependencies |
squadrant projects list |
List registered projects |
squadrant projects add <name> <path> |
Register a project |
squadrant projects remove <name> |
Unregister a project |
squadrant dashboard [--once] |
Print a one-shot status grid for all projects to the terminal. |
squadrant dashboard --pane [--direction <dir>] [--interval <s>] |
Open a refreshing sidebar pane in the current cmux workspace. |
squadrant dashboard sync-hub [--json] |
Mirror spoke status.md files into {hubVault}/projects/ for Obsidian Dataview. |
squadrant runtime status <project> |
Check if a project's captain workspace is running |
squadrant runtime send <project> <msg> |
Send a message to a captain workspace (auto-Enter) |
squadrant runtime list |
List all workspaces from the active runtime |
squadrant workspace read <project> <path> |
Read a scope-relative file from the project's spoke vault |
squadrant workspace list <project> <dir> |
List entries in a spoke vault directory |
squadrant workspace read --hub <path> |
Read from the hub vault |
squadrant notify <message> |
Send a message to the user via the configured notifier |
squadrant projection emit [--scope user|project] [--project <name>] [--target <name>] [--all] |
Emit squadrant rules + skills to Cursor/Codex/Gemini config files |
squadrant projection diff [same flags] |
Preview projection changes without writing |
squadrant projection list |
Show registered projection targets and their destinations |
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) |
squadrant crew send <project> <name> <message> |
Send a follow-up turn to an existing crew |
squadrant crew answer <project> <name> <option> [--expect <text>] [--text <answer>] |
Deliberately answer a crew's open AskUserQuestion/permission prompt by index or text — never an implicit default (#592) |
squadrant crew read <project> <name> |
Read a crew session's current screen |
squadrant crew close <project> <name> |
Shutdown a crew session (closes its tab) |
squadrant crew list <project> |
List live crews for a project |
squadrant shutdown [project] |
Graceful shutdown |
squadrant effort [max|balance|low] |
Get or set the global crew tokenomics dial (no arg prints current) |
squadrant retro |
Generate a retro (weekly/sprint summary) from daily logs and git (zero tokens) |
squadrant config check |
Detect config drift vs the current default schema |
squadrant heal [--dry-run|daemon] |
Targeted, idempotent remediation for squadrant components (daemon, health) |
squadrant ping <project> "<msg>" |
Fire-and-forget: deliver a message into any registered project's captain pane (no tracked task, no report-back) |
squadrant dispatch <project> "<task>" |
Dispatch a tracked task to any registered project; reports back to the origin on settle. Same group additionally auto-accepts and boots the target captain if down — see cross-project ping & dispatch |
squadrant group dispatch … |
Deprecated alias for squadrant dispatch — kept working, prints a deprecation note |
squadrant cmux … |
cmux integration helpers |
squadrant feedback |
Open opt-in feedback issue |
Six internal packages in a one-way dependency DAG. All are private (not published to npm).
| Package | Owns | Notes |
|---|---|---|
@squadrant/shared |
Config schema, TypeScript types, constants | Leaf lib — zero internal deps |
@squadrant/core |
Daemon logic, state-machine, protocol, AgentDriver interface, task/crew bus |
No concrete drivers — pure interfaces + orchestration |
@squadrant/agents |
AI driver seam — claude, codex, opencode, gemini drivers + registry |
Implements AgentDriver. Add a new AI agent here. |
@squadrant/workspaces |
Runtime (cmux), workspace (obsidian), notifier (cmux) drivers + registries | Implements surface/workspace/notifier seams |
@squadrant/web |
Observability dashboard — bundled HTML/JS served by CLI | Read-only UI; inlined by CLI's tsup build |
@squadrant/cli |
Commands, bin entry, daemon host, templates, plugin dir | Root — depends on all other packages |
Dependency DAG: shared ◄ core ◄ {agents, workspaces, web} ◄ cli
Build outputs (pnpm build via tsup, all internal packages inlined):
dist/index.js— CLI bin (squadrantcommand), entry:packages/cli/src/index.tsdist/squadrantd.js— daemon process, entry:packages/cli/src/daemon-host.ts
See the architecture diagram for a visual overview (6-package DAG, driver seams, lifecycle sources, control/captain channel).
- 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. - Captain (Opus) — project leader, uses Agent Teams + git worktrees
- 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 withsquadrant crew spawn, send follow-ups withsquadrant crew send, close when done. Works with any agent CLI (claude, opencode, and codex are fully interactive; gemini currently print-mode). Uses GSD for complex tasks.
Each role runs on the optimal model for cost/quality tradeoff. Configured in config.json:
- Command/Captain/Review: Opus (coordination + quality)
- Crew: Sonnet (execution)
- Exploration: Haiku (cheap lookups)
Alongside a per-role model, each role can pin a per-role thinking level — how hard the model reasons within a session. It maps to Claude Code's --effort flag and is claude-only: codex / opencode / gemini never receive the flag.
{
"defaults": {
"roles": {
"captain": { "agent": "claude", "model": "fable", "thinking": "medium" },
"crew": { "agent": "claude", "model": "sonnet", "thinking": "high" }
}
}
}- Valid values:
low|medium|high|xhigh|max. - Settable on any role (
command|captain|crew|exploration|side). - Unset ⇒ the flag is omitted entirely ⇒ the agent's own default effort. There is no built-in default.
- Override per invocation with
squadrant launch --thinking <level>(captain/command) orsquadrant crew spawn --thinking <level>(crew). Precedence, highest first: explicit--thinkingflag →defaults.roles.<role>.thinking→ omitted — the same rule--modelalready follows. - An invalid level fails fast with the list of valid values, rather than being passed through for the claude CLI to warn about and silently ignore.
Not the same thing as
defaults.effort.defaults.effort(max|balance|low, set viasquadrant effort) is the crew tokenomics dial — a global, captain-discretion signal about how aggressively to spend tokens.defaults.roles.<role>.thinkingis a per-role reasoning-depth setting emitted as a CLI flag. They share the wordmaxand nothing else; changing one does not affect the other.
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.
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.
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.
Squadrant used to infer whether a captain/crew was alive or idle by scraping terminal titles/pixels — slow, and wrong in ways nobody could tell apart from a true answer. That model was replaced (#333) by a LifecycleSource port with three concrete implementations feeding one core state machine:
CmuxStore— reads cmux's own session store directly; authoritative when the runtime is cmux, distinguishes a user-initiated close from a crash.NativeHook— Claude Code's native hooks (SessionStart/UserPromptSubmit/PreToolUse/Stop/Notification/SessionEnd, wired viasquadrant hooks claude, see Managed settings.json) — the primary source for claude captains/crews.CodexAppServer— Codex's JSON-RPC app-server connection, used when the agent is codex.
A per-tick pid floor (kill(pid,0)) arbitrates when sources disagree; provenance precedence is runtime ≥ agent > scan. This is the ground-truth layer the captain-liveness redesign (v0.15.0) and the control/captain channel below both build on.
Captain-memory write gate (#556). The unmatched PreToolUse hook entry (fires for every tool call, not just AskUserQuestion) doubles as a write gate on the captain's long-term memory directory (~/.claude/projects/<encoded-cwd>/memory/). Inside a crew session (SQUADRANT_CREW_TASK_ID set), a Write/Edit/MultiEdit/NotebookEdit targeting that directory, or a Bash command whose text references a path under it, is denied via hookSpecificOutput.permissionDecision: "deny" and logged to stderr — the pure decision logic (decideCaptainMemoryWrite, packages/agents/src/interactive/claude.ts) takes tool name/input/env/home dir and is unit-tested without spawning claude. Rule: crews report findings in their done/blocked message; captains decide what's durable. Captain/command sessions (no crew env vars) are unaffected.
AFK auto-continue (#616). Claude Code's AFK mode (CLAUDE_AFK_TIMEOUT_MS / CLAUDE_AFK_COUNTDOWN_MS) auto-resolves AskUserQuestion prompts with a synthetic no-answer after an idle timeout. squadrant never enables this itself; it is opt-in per machine via the defaults.claudeEnv overlay (#615). Approval-class prompts are guarded by a safe-option-only clause in the captain/crew templates: if a question resolves without a human answer, treat it as not approval and take only the safe, reversible option (or none). We do not disable AFK outright because it defuses the modal-induced "delivery stuck" precondition of #590 — a modal self-clears long before the stuck threshold fires. The real fix for answering while away is remote answer via Telegram (#486).
The next step past lifecycle sources: use each agent's native control API as ground truth for delivery, not just liveness — did a message I sent actually land and get processed, not just "the pane looks idle now." Screen-scraped delivery inference (confirmedSendToPane: paste → settle → Enter → re-read the screen) produced a long bug tail (#447, #455, #466, #484, #492, #516, #566, #590, #514, #657) because it's an inference that fails silently.
controlChannel(per agent type:off/shadow/on) —claudeis cut over toon: delivery verdicts for crew turns come from an agent receipt via@squadrant/core'scontrol-channel.ts, not pane-scraping.opencodehas a richer native control API (verified —prompt_async→ 204, dead session → 404) but is still shadow/off pending more mileage;pi/gemini/ACP agents don't fit this model and are out of scope.off → shadowneeds a daemon bounce;shadow → ondoes not.captainChannel(off/shadow/on) —onroutes captain-bound delivery over the native peer socket, bypassing the pane-defer machine entirely.shadowprobes but never sends: it logs and discards the probe result, provides no liveness of its own, and falls back to pane delivery — which re-enters draft/ghost/modal/no-boxdeferral. Preferon;shadowis a verification aid, not a safe fallback. (Crew wrapper/receipt text visible inonmode is a sender-identity artifact tracked separately in #711, not an inherent property of the channel.)- Implementation:
@squadrant/core/src/captain-channel.ts,control-channel.ts,lifecycle-source.ts. - Design doc:
specs/2026-08-13-agent-control-channel-design.md. Diagram:diagrams/2026-08-13-agent-control-channel.html.
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>).
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. codex now launches interactively (parity with claude/opencode); gemini currently still launches in print-mode, full interactive support is a follow-up. See docs/specs/archive/2026-05-05-squadrant-thin-redirect-design.md.
squadrant crew send correctly refuses to touch a pane while a crew has an AskUserQuestion/permission modal open — a bare keystroke would confirm whatever option the model happened to highlight (#484). That used to be a dead end: the refusal's own advice ("wait for the prompt to close") was unactionable, since the prompt only closes when answered.
squadrant crew answer <project> <name> <option> is the deliberate escape hatch. It reads the crew's rendered option list back, requires an explicit 1-based index or an exact/prefix text match (never an implicit default), and only then drives the selection (Down/Up from wherever the highlighted row currently sits, then Enter):
$ squadrant crew read myproj video # see the options first
❯ 1. Use main as the base
2. Use the existing stale branch
3. Ask me something else
$ squadrant crew answer myproj video 1
→ selecting 1. "Use main as the base"
✔ Answered myproj:video with 1. "Use main as the base" — prompt closed
--expect "<text>"refuses if the resolved option's label doesn't contain that text — a guard against the option order shifting between renders (it's model-generated, not fixed).--text "<answer>"is for a free-text option (e.g. "Type something."): select it, then type the given answer and submit.- If no option list is visible,
crew answerrefuses rather than guessing — read the screen withcrew readfirst.
squadrant crew reply <project> <id> [message] (control-plane path, keyed by task id instead of crew name) now delivers through the same path as crew send before transitioning task state — never the reverse. If delivery throws (e.g. the prompt is open), the command exits non-zero and no state transition happens; the error points at crew answer.
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 / #381).
- Daemon-direct delivery — crew turns and handoffs are delivered straight to the cmux surface by the daemon. The old
notify-relaysupervisor was deleted; there is no relay process to keep alive (#332). - 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).
stoppedproject status + orphan reap — when a captain goes away, the daemon reaps its orphaned crews and marks the projectstopped(intentional shutdown) rather than leaving stale tabs or faulting (#324 / #323 / #388).- Status: superseded for claude by the control channel. The semantic heartbeat above still runs, but delivery confirmation for claude crews now comes from an agent receipt (
controlChannel=on), not pane inference — see Control/Captain Channel (#667).
Drive squadrant from your phone (#65). When a telegram block is present in config, a daemon-internal bridge:
- 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 below). Best-effort: a Telegram failure never delays or breaks delivery to the captain pane.
- 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). - General command channel — slash commands in the supergroup's General topic run a curated set of squadrant operations from your phone (#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 asyncexecFile— never a shell passthrough. Unknown commands and freeform text get a/helphint.
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).
Setup (recommended):
- Create a bot with @BotFather and copy its token.
- 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. - Bind a project to a topic:
squadrant telegram link <project>(creates the forum topic and records the binding). - Check wiring with
squadrant telegram status.
Setup (manual):
Put the token + ids in config (or export TELEGRAM_BOT_TOKEN) — see the telegram block under Config. Then run squadrant telegram link <project>.
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:
remoteControl: true— an explicit opt-in master switch (defaultfalse).message.from.id ∈ users[]— the sender's Telegram user-id is on the allowlist. An empty/absentuserslist ⇒ control is disabled (fail-closed). Chat membership alone is never enough for control.
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.
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:
- Live mute (
active) — system-tracked session state intelegram-state.json. Flipped by engagement (any message into a topic auto-unmutes),/mute//unmute(Telegram), orsquadrant telegram notify <project> on|off. The live value wins over the config default when present. - Deliberate preferences (
crew,cap) — persistent settings in the per-project config file. Written bysquadrant telegram notify <project> crew <tier>/cap <on|off>(CLI) or/notify crew <tier>//notify cap <on|off>(Telegram, fail-closed behind remote control).
Crew tiers (cumulative) select which lifecycle events reach a topic when active:
| Tier | Events delivered |
|---|---|
none |
nothing |
done_only |
task.done, task.failed |
alert_only (default) |
done_only + task.blocked, task.approval.requested, task.input.requested, task.timeout |
all |
every lifecycle event (incl. progress/heartbeat noise) |
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).
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.
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 for the end-to-end flow.
Interim note (link ↔ daemon 409): the Telegram Bot API allows only one
getUpdatesconsumer at a time. Thesetupwizard pollsgetUpdatesto detect your group/user-id, so run it with the daemon stopped.linkuses onlycreateForumTopic, so it's unaffected.
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.
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).
- Hub vault (
~/squadrant-hub) — cross-project dashboard + hub wiki - Spoke vaults — per-project status, learnings, and wiki
- Status —
write-status.shdoes not exist and nothing else writesstatus.mdtoday; existing files are left in place as history but are no longer read bysquadrant status/standup/retro(#630 tracks a real persisted work-tracking store). - Dashboard —
squadrant dashboard --paneopens a refreshing sidebar pane in cmux that lists every project's live state, queried from the squadrant daemon's task records.squadrant dashboard sync-hubmirrors each spokestatus.mdinto{hubVault}/projects/so the hub vault'sdashboard.mdDataview query renders the same data inside Obsidian. - Handoff files — captain writes when in-flight work needs to survive into tomorrow; skipped on uneventful sessions.
- Daily logs — captain writes when the day produced something worth a log; not on a schedule.
- Learnings — recorded when a captain encounters a genuinely surprising or reusable pattern.
- Wiki — compiled, indexed knowledge pages in spoke vaults (
wiki/pages/); promoted from learnings when worth maintaining. - Hub Wiki — cross-project knowledge aggregated by an on-demand
squadrant command --task wiki-aggregaterun. - Scripts:
wiki-ingest.sh,wiki-query.sh,wiki-log.sh.
- Handoff files — captain writes context on shutdown, reads on startup
- Session freshness — auto-detects new day or template changes, forces fresh context
- claude-mem — cross-session memory via MCP plugin
~/.config/squadrant/config.json
{
"commandName": "command",
"hubVault": "~/squadrant-hub",
"runtime": "cmux",
"workspace": "obsidian",
"notifier": "cmux",
"telegram": {
"botToken": "123456:ABC...",
"supergroupId": -1001234567890,
"chats": [-1001234567890],
"users": [987654321],
"remoteControl": true,
"pollMs": 1000
},
"projects": {
"brove": {
"path": "~/projects/brove",
"captainName": "brove-captain",
"spokeVault": "~/squadrant-hub/spokes/brove",
"host": "local",
"runtime": "cmux",
"workspace": "obsidian",
}
},
"defaults": {
"maxCrew": 5,
"worktreeDir": ".worktrees",
"teammateMode": "in-process",
"permissions": {
"command": "default",
"captain": "acceptEdits"
},
"models": {
"command": "opus",
"captain": "opus",
"crew": "sonnet",
"exploration": "haiku",
"review": "opus"
},
"roles": {
"captain": { "agent": "claude", "model": "fable", "thinking": "medium" }
}
}
}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). pollMs (default 1000) is the inbound long-poll cadence. See Telegram (Two-Way, opt-in).