Skip to content

Latest commit

 

History

History
343 lines (260 loc) · 30.5 KB

File metadata and controls

343 lines (260 loc) · 30.5 KB

Squadrant Reference

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.

Table of contents

Commands

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

Monorepo structure

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 (squadrant command), entry: packages/cli/src/index.ts
  • dist/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).

Architecture

Roles

  • 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 with squadrant crew spawn, send follow-ups with squadrant 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.

Model Routing

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)

Thinking Level (per-role)

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) or squadrant crew spawn --thinking <level> (crew). Precedence, highest first: explicit --thinking flag → defaults.roles.<role>.thinking → omitted — the same rule --model already 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 via squadrant effort) is the crew tokenomics dial — a global, captain-discretion signal about how aggressively to spend tokens. defaults.roles.<role>.thinking is a per-role reasoning-depth setting emitted as a CLI flag. They share the word max and nothing else; changing one does not affect the other.

Runtime Abstraction

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.

Workspace Abstraction

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.

Notifier Abstraction

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.

Lifecycle Sources

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 via squadrant 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).

Control/Captain Channel (#667)

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) — claude is cut over to on: delivery verdicts for crew turns come from an agent receipt via @squadrant/core's control-channel.ts, not pane-scraping. opencode has 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 → shadow needs a daemon bounce; shadow → on does not.
  • captainChannel (off / shadow / on) — on routes captain-bound delivery over the native peer socket, bypassing the pane-defer machine entirely. shadow probes 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-box deferral. Prefer on; shadow is a verification aid, not a safe fallback. (Crew wrapper/receipt text visible in on mode 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 Spawn (Interactive Sub-Sessions)

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.

Answering a Crew's Open Prompt (#592)

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 answer refuses rather than guessing — read the screen with crew read first.

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.

Effort Dial (Tokenomics)

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).

Crew Lifecycle & Delivery

  • 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).
  • 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).
  • 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 / #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).

Telegram (Two-Way, opt-in)

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 async execFile — never a shell passthrough. Unknown commands and freeform text get a /help hint.

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):

  1. Create a bot with @BotFather and copy its token.
  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.
  3. Bind a project to a topic: squadrant telegram link <project> (creates the forum topic and records the binding).
  4. 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>.

Security model (#321) — fail-closed remote control

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:

  1. remoteControl: true — an explicit opt-in master switch (default false).
  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.

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.

Notification tuning (per-project)

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 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.
  • 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).

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.

Remote wake (#403) — operator-side

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 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.

Projection (Cross-Agent Config Sync)

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).

Obsidian Vaults (Hub-and-Spoke)

  • Hub vault (~/squadrant-hub) — cross-project dashboard + hub wiki
  • Spoke vaults — per-project status, learnings, and wiki

Knowledge System (opt-in writes)

  • Statuswrite-status.sh does not exist and nothing else writes status.md today; existing files are left in place as history but are no longer read by squadrant status/standup/retro (#630 tracks a real persisted work-tracking store).
  • Dashboardsquadrant 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.
  • 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-aggregate run.
  • Scripts: wiki-ingest.sh, wiki-query.sh, wiki-log.sh.

Session Continuity

  • 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

~/.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).