Release v0.19.0 - #719
Merged
Merged
Conversation
Scope fixed to claude + opencode; both control APIs verified live. - ControlChannel port beside the existing LifecycleSource pattern - DeliveryOutcome union replaces confirmedSendToPane's boolean - capability tiers (T0 send / T1 observe / T2 interact) rather than the intersection of the two agents - liveness mapping incl. the origin:"agent" vs "scan" trap that would otherwise silently discard waitingFor:"permission prompt" - shadow-mode rollout so the false-signal fix is measurable, not asserted - four independently revertable delivery slices
Slice 1 (liveness): ClaudePeerRegistrySource + OpencodeSseBridge onto the LifecycleSource port + stale comment fix. Read-only, no behaviour change. Slice 2 (control channel): ControlChannel port with a five-branch DeliveryOutcome + OpencodeHttpChannel over prompt_async + off-by-default per-agent rollout flag. Both are independent and can run as parallel crews.
feat(#667): Slice 2 — ControlChannel port + opencode delivery
feat(#667): slice 1 — native agent liveness
…uest
Legacy GET /permission + POST /session/{id}/permissions/{id} (deprecated
in the server's own OpenAPI doc) are what squadrant's merged code
actually uses and they work. The v2 pair this spec's open-unknowns
table named (GET /api/permission/request, POST
/api/session/{id}/permission/{reqId}/reply) exists and matches its
documented schema, but its backing SessionRunner errors with
ModelUnavailableError before it can ever reach a tool call in
opencode v1.18.18 (reproduced against two unrelated providers), so it
never sees a real pending permission. Recorded live transcripts for
pending/empty/dead-session/dead-request-id on both endpoint pairs.
…er message Reconfirms the 2026-08-08 spike's hold trigger (from-mode="bypass" against a --permission-mode auto receiver) on Claude Code 2.1.233, then goes further and exercises both resolutions live for the first time: - deny (default selection, no navigation): receipt status:"denied", no user turn added to the transcript. - approve (arrow-down to "Deliver", then confirm): receipt status:"delivered", and the message lands as a real type:"user" transcript entry. The confirm key is not a plain \r/\n/space/tab/digit — none of those resolved the prompt despite arrow-key navigation visibly working. Only the Kitty keyboard protocol CSI-u encoding (\x1b[13u) worked. Also documents an environmental trap: spawning a throwaway claude session via `nohup ... &` from inside another Claude Code session leaks CLAUDE_CODE_* env vars into the child and silently breaks its session registry entry; fixed by spawning with a sanitized env. No human-in-the-loop blocker was needed — the earlier "never exercised" unknown was closed entirely via automated pty driving.
docs(#667): close both open-unknowns via live smoke
Slice 3 (claude delivery): ClaudePeerChannel over the UDS session inbox, a receipt listener so held/denied/delivered are observable, T1-confirms-T0 for the silent accept path, --messaging-socket-path at BOTH spawn sites (crew via driver.buildCommand, captain via buildAgentCmd), and per-agent channel selection replacing slice 2's single channel. Slice 4 (chat): captain delivery through the channel behind a separate defaults.captainChannel flag, honest ping outcomes, and Telegram receipts for held/unreachable. Inbound gate hardening stays in #675. Both plans argue from the approved spec and incorporate all three open-unknowns now closed by live smoke (opencode queues mid-turn 204; v2 permission endpoint unreachable so the legacy pair stays; claude held-message approve/deny receipted, confirm key is \x1b[13u).
docs(#667): implementation plans for slices 3 and 4
…CLI channel construction Completes Task 7: readClaudeStatusByCwd/status helper in registry.ts, agents exports for the peer channel, ClaudePeerChannel construction in crew.ts, messagingSocketPath on TaskRecord and at claude crew spawn. NOTE: the crew that wrote this had also disabled the #670 monorepo-checkout guard in squadrantd.ts (isMonorepoCheckout -> return false) so its worktree build could start as a daemon. That change was uncommitted and has been discarded — the guard is intact here. See #682.
Part A (daemon self-heal): when roleFromTemplate() reads "unknown" from the cmux store's launchCommand.arguments AND the session's pid is confirmed alive, parseStoreRecords now falls back to the real argv from the OS process table (ps -o command=) and reclassifies from that. Injected as an optional dependency (isPidAlive/readArgv), same pattern as the existing readFile seam, so store-fingerprint.ts stays pure and unit-testable with no behavior change when the stored argv is intact. Part B (escape hatch): `squadrant heal captain [project] --all` resolves the live captain from the cmux store + process table, writes the corrected sessionId/pid into the LivenessRegistry, then reloads the daemon via write-then-`launchctl kickstart -k` (never bootout, which loses the ~2s KeepAlive respawn race). Idempotent — a project whose registry entry already matches the resolved captain is left untouched — and never invents an entry for a dead/hibernated pid.
heal captain: the running daemon persists its in-memory registry map every liveness tick, and can clobber our applyEntry() write in the race window before `launchctl kickstart -k`'s kill actually lands — silently reporting success on a heal that changed nothing on disk. runHealCaptain now re-reads the on-disk entry after kickstart and retries once (re-write + re-kickstart) if it didn't land; a project still mismatched after the retry is reported as a real failure (exit 1, stderr) instead of a false "healed". readArgvFromPid: cache successful reads per pid. Argv is immutable for a live process, but a session that stays role:"unknown" even after the OS-argv fallback (a legitimate side.research.* session) was re-shelling out to `ps` on every liveness tick for as long as it exists. Only successful reads are cached — a transient ps failure on a genuinely alive pid is still retried next tick, never given up on permanently.
fix(#699): re-adopt a captain whose cmux argv was truncated
…pic fallback
Item A: `squadrant launch <project> --agent <x> --model <y>` — explicit CLI
flags now win over defaults.roles.captain.{agent,model}, mirroring the
explicit-flag-wins precedence crew spawn already uses (#275). Absent flags
fall back to config exactly as before.
Item B: ensureGlobalOpencodeConfig defaults opencode's global config to
model: "anthropic/claude-sonnet-4-5". An opencode/codex/gemini captain
launched with no explicit model silently falls through to that default and
resolves to an Anthropic model — the exact outage manual mode exists to
survive. `squadrant launch` now refuses to boot such a captain, naming the
actual resolved model. Crew spawns get the same detection but only warn
(non-blocking): a crew depending on Anthropic through a fallback agent is
less catastrophic and more often intentional than a captain doing the same.
Known adjacent gap (not fixed here, out of scope): buildAgentCmd's non-claude
branch still emits a one-shot `opencode run "..."`, so once a non-claude
captain clears this guard it answers once and exits (Phase 2.1 interactive
boot work, tracked separately).
…, role-scope comment still stale Parking mid-fix per captain instruction. State: DONE: - isAnthropicModel now also matches bare "claude-*" model names (not just the "anthropic/" prefix), closing the gap where --model claude-sonnet-4-5 sailed through the guard. Tests added and passing. - Added anthropicRefusalMessage(role, workspaceName, agentName, model) as a role-agnostic pure helper, with a test pinning that it behaves identically for captain/command/side (no per-role branching). Exported from model-guard.ts but NOT YET wired into launch.ts. NOT DONE: - launch.ts still builds its own inline refusal message instead of calling anthropicRefusalMessage — needs to be swapped in. - launch.ts's comment above the guard still says "Refuse (not warn) for the captain role", which misdescribes the guard's actual scope (it fires for whatever role launchOne is called with — today only "captain" in practice, since launch.ts only ever calls launchOne with role="captain", but the check itself has no role special-casing). Comment needs rewriting to state this deliberately. - Full pnpm build / pnpm test NOT re-run since these last two edits — do that before considering this fix complete.
Fixes instruction-file drift across projected agent formats where the old strict GitNexus wording was still present. Softens the language in AGENTS.md, GEMINI.md, and .cursor/rules/squadrant.mdc to match CLAUDE.md. The drift occurred because the `gitnexus analyze` CLI command embeds its own static template (which still contains the strict wording) and overwrites the GitNexus block in AGENTS.md. When `squadrant projection` subsequently runs, it propagates this overwritten block to the other agent instruction files. This will need to be fixed in the gitnexus package directly.
…n it The guard's comment claimed the refusal was captain-only, but isBlockedFallback takes no role argument — it can't special-case by role. It fires for whatever role launchOne is called with; today that's only "captain" because launch.ts's call sites all pass that literally, and squadrant command/side spawn agents through separate code paths that never touch this guard. Extracts the inline check into resolveAnthropicRefusal (also swaps in the already-existing anthropicRefusalMessage helper instead of building the message inline) and adds a launch.test.ts case that pins identical refusal behavior across captain/command/side, so a future `if (role === "captain")` around the guard would fail a test instead of silently narrowing scope.
docs: sync gitnexus guidance across agent formats
WIP: #627 item A+B — launch --agent/--model override + Anthropic-fallback guard
Removes all remaining references to GitNexus since the tool is no longer available on the machine. Files modified: - CLAUDE.md: removed GitNexus section. - AGENTS.md: removed GitNexus section. - GEMINI.md: removed GitNexus section. - .cursor/rules/squadrant.mdc: removed GitNexus section. - CHANGELOG.md: removed gitnexus note. - docs/specs/2026-07-28-captain-context-budget.md: stripped references. - docs/specs/2026-06-24-codebase-structure-refactor.md: stripped references. - docs/specs/2026-07-29-persisted-work-tracking.md: replaced gitnexus checks with manual checks. - docs/specs/2026-07-07-captain-liveness-redesign-plan.md: replaced gitnexus instructions. - docs/specs/2026-07-07-captain-liveness-redesign.md: replaced gitnexus instructions. - docs/architecture.html: stripped references. - docs/architecture.vi.html: stripped references. - docs/README.md: stripped references. - docs/superpowers/plans/2026-06-23-telegram-v010-stability.md: replaced gitnexus instructions. - scripts/gen-codex-types.sh: stripped references. - packages/core/src/__tests__/delivery-loop.test.ts: changed dummy project name to testproj. - packages/agents/src/codex/driver.ts: changed gitnexus to mcp in comments. - packages/cli/src/lib/__tests__/canonical-source.test.ts: changed gitnexus to mcp in mock. Files deleted: - .claude/skills/gitnexus/ (6 skills) - .gitnexusignore
- Restored CHANGELOG.md, docs/specs/, docs/superpowers/, and packages/agents/src/codex/driver.ts to their original historical state (preserving records of GitNexus usage and incidents). - Removed the dangling 'and the code knowledge graph' clause from docs/architecture.html and .vi.html completely instead of leaving a vague fragment.
Removed the frozen <claude-mem-context> sections from AGENTS.md, GEMINI.md, and .cursor/rules/squadrant.mdc. This section was 3 months stale and is superseded by the fresh context injected at session start by claude-mem.
chore: completely remove GitNexus
…epo CLAUDE.md CLAUDE.md was the last instruction file outside the projection system, which is exactly why it drifted from AGENTS.md. Bring it in: - packages/agents/src/projection/claude.ts: new emitter, same shape as codex.ts/gemini.ts (marker-guarded merge, user ~/.claude/CLAUDE.md + project <root>/CLAUDE.md, both shared/markdown). Wired into the registry (index.ts) and the doctor/projection CLI commands, matching how codex/gemini/opencode are registered there. Deliberately NOT wired into init.ts's user-level auto-emit-on-init flow — that code already has a comment explicitly scoping it to "non-Claude agents", a prior intentional decision this task didn't ask to reverse. - AGENTS.md: restore the "(captain, crew, and direct edits)" parenthetical to the Coding Discipline section, matching CLAUDE.md's more accurate wording — it names who the rule binds. - CLAUDE.md: regenerated from AGENTS.md via the new emitter, wrapped in squadrant markers. Previously had 3 of AGENTS.md's 6 sections; now has all 6, including Telegram (#65, with its security-gap warning) and Managed ~/.claude/settings.json (#615) that Claude Code — which only auto-loads CLAUDE.md — was previously missing entirely. Verified via the built emitter directly (no daemon/launch involved): a re-emit against the regenerated file is byte-identical (idempotent), and content placed outside the markers survives a re-emit with changed source content. claude.test.ts pins the same guarantee at the unit level, mirroring codex.test.ts's marker-duplication regression test.
Bring CLAUDE.md into the projection system
…ctName (#706) launch.ts's socket-path interpolation read the command-level `project` positional instead of launchOne()'s `projectName` parameter. That positional is only ever set on the single-project launch path — on --all and interactive-parallel launches it's undefined, so every captain collapsed onto the same /tmp/cc-socks/squadrant-captain-undefined.sock. First bind wins; the rest of the captains silently failed to boot. Extracts captainSocketPath() (matching the shouldWireCaptainChannel / resolveAnthropicRefusal pattern already in this file) so the mapping from project name to socket path is unit-testable, and pins the fix with a regression test.
…st call site (#706) Follow-up to d63be10, per review feedback: 1. Deleted the duplicate captainSocketPath() this branch had added to launch.ts. Import the existing one from @squadrant/core instead (packages/core/src/captain-channel.ts), the same way captain-channel-factory.ts already does — a second local copy of the formula was itself a instance of the drift #706 is about, and it dropped core's project-name safety regex. 2. resolveCaptainSocketPath() now throws when the captain channel is enabled but no project name reached the call site, instead of silently falling back to `undefined` (no channel). A captain silently booting without its messaging channel is exactly the kind of quiet failure #667 exists to close; the launch for that one captain now fails loudly and is reported, while the rest of an --all batch continues. 3. Added launch-captain-socket.test.ts, which drives the real launchCommand `--all` action (not just the pure helper) and asserts the two captains it spawns get distinct project-derived socket paths. Reintroducing `${project}` at the buildAgentCmd call site fails this test even though a unit test of the helper alone wouldn't catch it — verified by temporarily reintroducing the bug and confirming this test fails, then restoring the fix. Also added a parity test asserting resolveCaptainSocketPath's output matches @squadrant/core's captainSocketPath exactly, so the two formulas can't silently drift apart again. Verified: `tsc -b --force` across all packages, `pnpm build`, `node dist/index.js --help` (exit 0), and the full monorepo test suite — 2728 passing (was 2723 before this branch; no tests removed).
fix: captain socket path collision on parallel/--all launches (#706)
Resolve the captain's own sessionId from the same registry entry statusFor already reads (keyed by the launch-time socket path) and wire it into ClaudePeerChannel's sessionIdFor, instead of hardcoding undefined. Restores the pid-reuse guard and drops the "Another Claude session sent a message" wrapper on captain-bound sends. Falls back to omitting session_id (never throws) when the captain hasn't registered yet.
squadrant never passed claude's `-n, --name` flag, so every session's display name auto-derived from the cwd basename (e.g. "squadrant-fix-706-67", "helpa-06") — indistinguishable via ListAgents. A crew that could not tell its captain apart from an unrelated session sent its PR report to the wrong one. Adds `@squadrant/shared`'s captainSessionName/crewSessionName (sanitized to the same safe-filename charset captainSocketPath() already enforces) and wires them into both claude launch paths: buildAgentCmd (captain/command, launch.ts) and the claude driver's buildCommand (crew, crew-spawn.ts). Format: "squadrant-captain-<project>" and "squadrant-crew-<project>-<name>". `-n` is claude-only — added as an optional SpawnOptions field only the claude driver reads; codex/gemini/opencode buildCommand implementations were checked and ignore unknown fields safely, so they're unaffected.
…orever A transient listen EACCES at daemon boot used to log once and permanently degrade the daemon to pane delivery for its entire process lifetime, with no retry — a plain restart bound the same socket fine minutes later. - buildCaptainChannelWithRetry() retries buildCaptainChannel() with capped exponential backoff, forever, instead of giving up after one attempt. - sharedReceiptListener() only caches its singleton AFTER a successful start(); previously it cached before start() resolved, so a failed bind poisoned the singleton and every later retry silently returned the never-started listener instead of re-attempting the bind. - squadrantd.ts now calls the retrying wrapper at the #667 slice 4 call site instead of a one-shot .catch(). ping.ts's one-shot buildCaptainChannel() call is untouched — a short-lived CLI command should not retry forever.
Review nits: the default sleep()'s setTimeout must not hold the event loop open on shutdown (unref); the squadrantd.ts call site's .then() needs a .catch() so a throwing log call can't escape as an unhandled rejection (the retry loop itself only stops by resolving).
fix(#712): captain-channel init retries instead of latching pane-only forever
Redraws the monorepo architecture diagram (new dated file, adds a Control/Captain Channel slide) and brings docs/AGENTS.md up to date with the #667 control/captain channel and #333 lifecycle-source work that had shipped but was undocumented. - docs/README.md: relink diagrams/specs, correct stale status entries - AGENTS.md (+ CLAUDE.md/GEMINI.md/.cursor mirrors): new "Captain/ Control Channel (#667)" section - docs/reference.md: new "Lifecycle Sources" and "Control/Captain Channel (#667)" subsections - docs/diagrams/2026-08-22-squadrant-architecture.html: new diagram, old 2026-06-18 one kept and marked superseded (not deleted) - docs/architecture.html: banner pointing stale Control Plane section at reference.md as the living source - 3 specs: added Status-update lines for shipped work, no deletions
shadow was described as a safe verification fallback that keeps comparing against the old inference path. It isn't: the probe result is logged and discarded (captain-channel.ts:67), shadow contributes no liveness of its own, and dropping to shadow hands back the full pane-defer machine (draft/ghost/modal/no-box deferral) — the path behind the ~4h delivery jam on 2026-08-22 (#712). Reworded per captain review to state on/shadow's actual behavior, and decoupled the wrapper-text mention (#711, a sender-identity artifact, not an inherent property of the channel). Applied to AGENTS.md and mirrored to CLAUDE.md/GEMINI.md/.cursor rules, plus reference.md and the architecture diagram's slide 6.
docs: refresh architecture diagram + fill #667 control-channel gap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release v0.19.0 — cut from
develop@afa5a61.82 commits since v0.18.1 (2026-08-13).
Headline
resolveCaptainChannelMode()returns"off"unless configured), so this is opt-in.squadrant-captain-<project>,squadrant-crew-<project>-<name>) and the captain-bound path carriessession_id, restoring the pid-reuse guard.gonefor interactive crews — poisons shadow-mode evidence and defeats T1-confirms-T0 #689 symlinked-path resolution, #697 follow-up: liveness cannot re-adopt a RUNNING captain whose cmux argv was truncated #699 captain re-adoption under truncated cmux argv, captain-channel init failure is one-shot — a transient bind error silently degrades the daemon to pane-only delivery for its entire lifetime #712 captain-channel init retry.Verification
pnpm build— cleanpnpm test— 2751 passed across 208 files, zero deletedKnown open issues (deliberately not blocking)
Verified via git that these do not regress this release:
captainChannelis explicitly turned on, which is not the shipped default.projection emitcommand, not the normal user path.Post-merge checklist
release.yml's npm publish step iscontinue-on-error— a green workflow does not prove publish succeeded (this bit us at v0.13.1). Verify withcurl https://registry.npmjs.org/squadrantrather thannpm view(CDN lag).main→develop.