squadrant 0.12.1 → 0.13.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/dist/index.js CHANGED
@@ -622,8 +622,41 @@ function resolveWorktreeBase(repoRoot, fallback = "develop") {
622
622
  return fallback;
623
623
  }
624
624
  function addWorktree(spec) {
625
- const wt = worktreePath(spec.repoRoot, spec.worktreeDir, spec.project, spec.name);
626
- execFileSync2("git", ["-C", spec.repoRoot, "worktree", "add", wt, "-b", crewBranch(spec.name), spec.base], { stdio: "pipe" });
625
+ const originalBranch = crewBranch(spec.name);
626
+ let targetName = spec.name;
627
+ let targetBranch = originalBranch;
628
+ let branchExists = false;
629
+ try {
630
+ execFileSync2("git", ["-C", spec.repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${originalBranch}`], { stdio: "pipe" });
631
+ branchExists = true;
632
+ } catch {
633
+ }
634
+ if (branchExists) {
635
+ const log = execFileSync2("git", ["-C", spec.repoRoot, "log", "--oneline", `${spec.base}..${originalBranch}`], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
636
+ if (!log) {
637
+ execFileSync2("git", ["-C", spec.repoRoot, "branch", "-D", originalBranch], { stdio: "pipe" });
638
+ } else {
639
+ let suffix = 2;
640
+ while (true) {
641
+ const candidate = `${spec.name}-${suffix}`;
642
+ const candidateBranch = crewBranch(candidate);
643
+ let candidateExists = false;
644
+ try {
645
+ execFileSync2("git", ["-C", spec.repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${candidateBranch}`], { stdio: "pipe" });
646
+ candidateExists = true;
647
+ } catch {
648
+ }
649
+ if (!candidateExists) {
650
+ targetName = candidate;
651
+ targetBranch = candidateBranch;
652
+ break;
653
+ }
654
+ suffix++;
655
+ }
656
+ }
657
+ }
658
+ const wt = worktreePath(spec.repoRoot, spec.worktreeDir, spec.project, targetName);
659
+ execFileSync2("git", ["-C", spec.repoRoot, "worktree", "add", wt, "-b", targetBranch, spec.base], { stdio: "pipe" });
627
660
  return wt;
628
661
  }
629
662
  function removeWorktree(repoRoot, wtPath) {
@@ -2700,6 +2733,12 @@ var init_crew_spawn = __esm({
2700
2733
  }
2701
2734
  });
2702
2735
 
2736
+ // packages/core/dist/lifecycle-source.js
2737
+ var init_lifecycle_source = __esm({
2738
+ "packages/core/dist/lifecycle-source.js"() {
2739
+ }
2740
+ });
2741
+
2703
2742
  // packages/core/dist/index.js
2704
2743
  var init_dist2 = __esm({
2705
2744
  "packages/core/dist/index.js"() {
@@ -2732,6 +2771,7 @@ var init_dist2 = __esm({
2732
2771
  init_launch_workspace();
2733
2772
  init_side_session();
2734
2773
  init_crew_spawn();
2774
+ init_lifecycle_source();
2735
2775
  }
2736
2776
  });
2737
2777
 
@@ -3561,17 +3601,361 @@ var init_daemon_cmux = __esm({
3561
3601
  }
3562
3602
  });
3563
3603
 
3604
+ // packages/workspaces/dist/cmux-daemon/cmux-store-source.js
3605
+ import { join as join13 } from "path";
3606
+ import { homedir as homedir8 } from "os";
3607
+ import { watch, readdirSync as readdirSync3, readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
3608
+ function parseLifecycleState(s) {
3609
+ if (s === "running" || s === "idle" || s === "needsInput" || s === "unknown") {
3610
+ return s;
3611
+ }
3612
+ return "unknown";
3613
+ }
3614
+ function defaultIsPidAlive(pid) {
3615
+ try {
3616
+ process.kill(pid, 0);
3617
+ return true;
3618
+ } catch {
3619
+ return false;
3620
+ }
3621
+ }
3622
+ function defaultListFiles(dir) {
3623
+ try {
3624
+ return readdirSync3(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
3625
+ } catch {
3626
+ return [];
3627
+ }
3628
+ }
3629
+ function defaultReadFile(path29) {
3630
+ try {
3631
+ return readFileSync8(path29, "utf-8");
3632
+ } catch {
3633
+ return void 0;
3634
+ }
3635
+ }
3636
+ function defaultWatchDir(dir, cb) {
3637
+ const w = watch(dir, (_event, filename) => {
3638
+ if (typeof filename === "string" && filename.endsWith("-hook-sessions.json")) {
3639
+ cb();
3640
+ }
3641
+ });
3642
+ return () => w.close();
3643
+ }
3644
+ var CmuxStoreSource;
3645
+ var init_cmux_store_source = __esm({
3646
+ "packages/workspaces/dist/cmux-daemon/cmux-store-source.js"() {
3647
+ CmuxStoreSource = class {
3648
+ name = "cmux-store";
3649
+ stateDir;
3650
+ debounceMs;
3651
+ isPidAlive;
3652
+ listFiles;
3653
+ readFile;
3654
+ fileExists;
3655
+ watchDir;
3656
+ scheduleTimer;
3657
+ cancelTimer;
3658
+ log;
3659
+ deps;
3660
+ stopWatcher;
3661
+ debounceTimer;
3662
+ /** taskId → last reported snapshot (for snapshot() liveness floor). */
3663
+ cache = /* @__PURE__ */ new Map();
3664
+ constructor(opts = {}) {
3665
+ this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join13(homedir8(), ".cmuxterm");
3666
+ this.debounceMs = opts.debounceMs ?? 50;
3667
+ this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive;
3668
+ this.listFiles = opts.listFiles ?? defaultListFiles;
3669
+ this.readFile = opts.readFile ?? defaultReadFile;
3670
+ this.fileExists = opts.fileExists ?? existsSync10;
3671
+ this.watchDir = opts.watchDir ?? defaultWatchDir;
3672
+ this.scheduleTimer = opts.scheduleTimer ?? setTimeout;
3673
+ this.cancelTimer = opts.cancelTimer ?? clearTimeout;
3674
+ this.log = opts.log ?? (() => {
3675
+ });
3676
+ }
3677
+ start(deps) {
3678
+ this.deps = deps;
3679
+ this.scan();
3680
+ try {
3681
+ this.stopWatcher = this.watchDir(this.stateDir, () => this.scheduleDebounced());
3682
+ } catch (e) {
3683
+ this.log(`cmux-store: failed to watch ${this.stateDir}: ${e.message}`);
3684
+ }
3685
+ }
3686
+ stop() {
3687
+ if (this.debounceTimer !== void 0) {
3688
+ this.cancelTimer(this.debounceTimer);
3689
+ this.debounceTimer = void 0;
3690
+ }
3691
+ this.stopWatcher?.();
3692
+ this.stopWatcher = void 0;
3693
+ this.deps = void 0;
3694
+ this.cache.clear();
3695
+ }
3696
+ /** Returns the last-reported snapshot for a known crew (liveness floor). */
3697
+ snapshot(taskId) {
3698
+ return this.cache.get(taskId);
3699
+ }
3700
+ // ── private ─────────────────────────────────────────────────────────────────
3701
+ scheduleDebounced() {
3702
+ if (this.debounceTimer !== void 0) {
3703
+ this.cancelTimer(this.debounceTimer);
3704
+ }
3705
+ this.debounceTimer = this.scheduleTimer(() => {
3706
+ this.debounceTimer = void 0;
3707
+ this.scan();
3708
+ }, this.debounceMs);
3709
+ }
3710
+ scan() {
3711
+ if (!this.deps)
3712
+ return;
3713
+ for (const filename of this.listFiles(this.stateDir)) {
3714
+ this.scanFile(filename);
3715
+ }
3716
+ }
3717
+ scanFile(filename) {
3718
+ const deps = this.deps;
3719
+ const filePath = join13(this.stateDir, filename);
3720
+ const lockPath = `${filePath}.lock`;
3721
+ if (this.fileExists(lockPath)) {
3722
+ this.log(`cmux-store: skipping ${filename} (locked)`);
3723
+ return;
3724
+ }
3725
+ const raw = this.readFile(filePath);
3726
+ if (!raw)
3727
+ return;
3728
+ let parsed;
3729
+ try {
3730
+ parsed = JSON.parse(raw);
3731
+ } catch {
3732
+ this.log(`cmux-store: failed to parse ${filename}`);
3733
+ return;
3734
+ }
3735
+ for (const session of Object.values(parsed.sessions ?? {})) {
3736
+ this.processSession(session, deps);
3737
+ }
3738
+ }
3739
+ processSession(session, deps) {
3740
+ if (!session.sessionId || !session.cwd || typeof session.pid !== "number")
3741
+ return;
3742
+ const hint = {
3743
+ cwd: session.cwd,
3744
+ pid: session.pid,
3745
+ sessionId: session.sessionId
3746
+ };
3747
+ const resolved = deps.resolve(hint);
3748
+ if (!resolved)
3749
+ return;
3750
+ let alive = this.isPidAlive(session.pid);
3751
+ if (!alive && session.isRestorable === true && session.agentLifecycle === "idle") {
3752
+ alive = true;
3753
+ }
3754
+ const snap = {
3755
+ taskId: resolved.id,
3756
+ state: parseLifecycleState(session.agentLifecycle),
3757
+ alive,
3758
+ // "agent": the store carries the agent's own reported lifecycle state,
3759
+ // not a scan inference. needsInput from the store is authoritative.
3760
+ origin: "agent",
3761
+ at: Math.floor((session.updatedAt ?? 0) * 1e3),
3762
+ pid: session.pid,
3763
+ ...session.lastBody ? { detail: { note: session.lastBody } } : {}
3764
+ };
3765
+ this.cache.set(resolved.id, snap);
3766
+ deps.report(snap);
3767
+ }
3768
+ };
3769
+ }
3770
+ });
3771
+
3772
+ // packages/workspaces/dist/native-hooks/native-hook-source.js
3773
+ import { join as join14 } from "path";
3774
+ import { homedir as homedir9 } from "os";
3775
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
3776
+ function installClaudeHooks(opts = {}) {
3777
+ const settingsPath = opts.settingsPath ?? join14(homedir9(), ".claude", "settings.json");
3778
+ const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
3779
+ const readFile6 = opts.readFile ?? defaultReadFile2;
3780
+ const writeFile5 = opts.writeFile ?? defaultWriteFile;
3781
+ const log = opts.log ?? (() => {
3782
+ });
3783
+ let settings = {};
3784
+ const raw = readFile6(settingsPath);
3785
+ if (raw) {
3786
+ try {
3787
+ settings = JSON.parse(raw);
3788
+ } catch {
3789
+ log(`native-hook: failed to parse ${settingsPath} \u2014 hooks section will be reset`);
3790
+ }
3791
+ }
3792
+ if (typeof settings.hooks !== "object" || settings.hooks === null || Array.isArray(settings.hooks)) {
3793
+ settings.hooks = {};
3794
+ }
3795
+ const hooks = settings.hooks;
3796
+ let changed = false;
3797
+ for (const [eventName, sub, matcher] of CLAUDE_HOOK_EVENTS) {
3798
+ if (!Array.isArray(hooks[eventName])) {
3799
+ hooks[eventName] = [];
3800
+ }
3801
+ const entries = hooks[eventName];
3802
+ const command = `${hookCmd} claude ${sub}`;
3803
+ const hookMatcher = matcher ?? "";
3804
+ const alreadyPresent = entries.some((m) => Array.isArray(m.hooks) && m.hooks.some((h) => typeof h.command === "string" && h.command === command));
3805
+ if (!alreadyPresent) {
3806
+ entries.push({ matcher: hookMatcher, hooks: [{ type: "command", command, timeout: 10 }] });
3807
+ changed = true;
3808
+ }
3809
+ }
3810
+ if (changed) {
3811
+ writeFile5(settingsPath, JSON.stringify(settings, null, 2));
3812
+ }
3813
+ return settingsPath;
3814
+ }
3815
+ function mapSubToLifecycle(sub) {
3816
+ switch (sub) {
3817
+ case "session-start":
3818
+ return "running";
3819
+ case "prompt-submit":
3820
+ return "running";
3821
+ case "pre-tool-use":
3822
+ return "running";
3823
+ case "stop":
3824
+ return "idle";
3825
+ case "notification":
3826
+ return "needsInput";
3827
+ case "ask-question":
3828
+ return "needsInput";
3829
+ case "session-end":
3830
+ return "session-end";
3831
+ default:
3832
+ return null;
3833
+ }
3834
+ }
3835
+ function extractDetail(sub, payload) {
3836
+ if (!payload || typeof payload !== "object")
3837
+ return void 0;
3838
+ const p = payload;
3839
+ if (sub === "notification") {
3840
+ const note = typeof p.message === "string" ? p.message : void 0;
3841
+ return note ? { note } : void 0;
3842
+ }
3843
+ if (sub === "pre-tool-use") {
3844
+ const tool = typeof p.tool_name === "string" ? p.tool_name : void 0;
3845
+ return tool ? { tool } : void 0;
3846
+ }
3847
+ return void 0;
3848
+ }
3849
+ function defaultReadFile2(path29) {
3850
+ try {
3851
+ return readFileSync9(path29, "utf-8");
3852
+ } catch {
3853
+ return void 0;
3854
+ }
3855
+ }
3856
+ function defaultWriteFile(path29, content) {
3857
+ mkdirSync6(path29.replace(/\/[^/]+$/, ""), { recursive: true });
3858
+ writeFileSync7(path29, content, "utf-8");
3859
+ }
3860
+ var CLAUDE_HOOK_EVENTS, DEFAULT_HOOK_CMD, NativeHookSource;
3861
+ var init_native_hook_source = __esm({
3862
+ "packages/workspaces/dist/native-hooks/native-hook-source.js"() {
3863
+ CLAUDE_HOOK_EVENTS = [
3864
+ ["SessionStart", "session-start"],
3865
+ ["UserPromptSubmit", "prompt-submit"],
3866
+ ["PreToolUse", "pre-tool-use"],
3867
+ ["Stop", "stop"],
3868
+ ["Notification", "notification"],
3869
+ ["PreToolUse", "ask-question", "AskUserQuestion"],
3870
+ ["SessionEnd", "session-end"]
3871
+ ];
3872
+ DEFAULT_HOOK_CMD = "squadrant hooks";
3873
+ NativeHookSource = class {
3874
+ name = "native-hook";
3875
+ hookInstall;
3876
+ log;
3877
+ deps;
3878
+ /** taskId → last-reported snapshot, for snapshot() liveness floor. */
3879
+ cache = /* @__PURE__ */ new Map();
3880
+ constructor(opts = {}) {
3881
+ this.hookInstall = opts.hookInstall ?? {};
3882
+ this.log = opts.log ?? (() => {
3883
+ });
3884
+ }
3885
+ start(deps) {
3886
+ this.deps = deps;
3887
+ }
3888
+ stop() {
3889
+ this.deps = void 0;
3890
+ this.cache.clear();
3891
+ }
3892
+ /** Returns the last-reported snapshot for a known crew (liveness floor poll). */
3893
+ snapshot(taskId) {
3894
+ return this.cache.get(taskId);
3895
+ }
3896
+ /**
3897
+ * Install squadrant-owned hooks into ~/.claude/settings.json.
3898
+ * Idempotent — safe to call on every project init or crew spawn.
3899
+ * Returns the path to the settings file.
3900
+ */
3901
+ install() {
3902
+ return installClaudeHooks(this.hookInstall);
3903
+ }
3904
+ /**
3905
+ * Receive a lifecycle hook event from the daemon and report a LifecycleSnapshot.
3906
+ *
3907
+ * The daemon's 'squadrant hooks claude <sub>' CLI subcommand calls this after
3908
+ * reading SQUADRANT_CREW_TASK_ID from the hook's process environment — the only
3909
+ * collision-proof correlation key (blueprint §2.2 priority 1).
3910
+ *
3911
+ * @param sub Sub-event alias: "session-start" | "prompt-submit" | "stop" | …
3912
+ * @param taskId SQUADRANT_CREW_TASK_ID extracted from the hook process env.
3913
+ * @param pid Optional: OS pid from the hook's process env or argv.
3914
+ * @param payload Optional: parsed JSON payload from hook stdin (best-effort detail).
3915
+ */
3916
+ handleHook(sub, taskId, pid, payload) {
3917
+ if (!this.deps)
3918
+ return;
3919
+ const mapped = mapSubToLifecycle(sub);
3920
+ if (mapped === null) {
3921
+ this.log(`native-hook: unknown sub '${sub}' for task ${taskId} \u2014 ignored`);
3922
+ return;
3923
+ }
3924
+ const isSessionEnd = mapped === "session-end";
3925
+ const state = isSessionEnd ? "unknown" : mapped;
3926
+ const detail = extractDetail(sub, payload);
3927
+ const snap = {
3928
+ taskId,
3929
+ state,
3930
+ alive: !isSessionEnd,
3931
+ origin: "agent",
3932
+ at: Date.now(),
3933
+ ...pid !== void 0 ? { pid } : {},
3934
+ ...detail ? { detail } : {}
3935
+ };
3936
+ this.cache.set(taskId, snap);
3937
+ this.deps.report(snap);
3938
+ }
3939
+ };
3940
+ }
3941
+ });
3942
+
3564
3943
  // packages/workspaces/dist/crew-pane.js
3565
3944
  import net from "net";
3566
3945
  async function settleInputBox(runtime, pane) {
3567
3946
  let prev = await runtime.readPaneScreen(pane) ?? "";
3947
+ let sawContent = parseDraftFromScreen(prev) !== "" && parseDraftFromScreen(prev) !== null;
3568
3948
  for (let i = 0; i < SETTLE_MAX_POLLS; i++) {
3569
3949
  await new Promise((r) => setTimeout(r, SETTLE_POLL_MS));
3570
3950
  const cur = await runtime.readPaneScreen(pane) ?? "";
3951
+ const draft = parseDraftFromScreen(cur);
3952
+ if (draft !== "" && draft !== null)
3953
+ sawContent = true;
3571
3954
  if (cur === prev)
3572
- return;
3955
+ return sawContent;
3573
3956
  prev = cur;
3574
3957
  }
3958
+ return sawContent;
3575
3959
  }
3576
3960
  function getFreePort() {
3577
3961
  return new Promise((resolve3, reject) => {
@@ -3609,17 +3993,26 @@ async function resolveCaptainWorkspace(project) {
3609
3993
  async function confirmedSendToPane(runtime, pane, message) {
3610
3994
  const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
3611
3995
  await runtime.pasteToPane(pane, message);
3612
- await settleInputBox(runtime, pane);
3996
+ let sawDraft = await settleInputBox(runtime, pane);
3613
3997
  await runtime.sendKeyToPane(pane, "Enter");
3998
+ let repasted = false;
3614
3999
  for (let attempt = 0; attempt < SUBMIT_RETRY_LIMIT; attempt++) {
3615
4000
  await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
3616
4001
  const afterScreen = await runtime.readPaneScreen(pane) ?? "";
3617
4002
  const draft = parseDraftFromScreen(afterScreen);
3618
- if (draft === "")
4003
+ if (draft !== "" && draft !== null)
4004
+ sawDraft = true;
4005
+ if (draft === "" && sawDraft)
3619
4006
  return;
3620
4007
  if (draft === null && afterScreen !== preSendScreen)
3621
4008
  return;
3622
- await settleInputBox(runtime, pane);
4009
+ const settled = await settleInputBox(runtime, pane);
4010
+ if (settled)
4011
+ sawDraft = true;
4012
+ if (!sawDraft && !repasted) {
4013
+ repasted = true;
4014
+ await runtime.pasteToPane(pane, message);
4015
+ }
3623
4016
  await runtime.sendKeyToPane(pane, "Enter");
3624
4017
  }
3625
4018
  }
@@ -3653,18 +4046,27 @@ async function sendFirstTurnWhenReady(runtime, pane, task, preLaunchScreen, acce
3653
4046
  return;
3654
4047
  }
3655
4048
  await runtime.pasteToPane(pane, task);
3656
- await settleInputBox(runtime, pane);
4049
+ let sawDraft = await settleInputBox(runtime, pane);
3657
4050
  await runtime.sendKeyToPane(pane, "Enter");
3658
4051
  const retryLimit = acceptanceConfig?.retryLimit ?? SUBMIT_RETRY_LIMIT;
4052
+ let repasted = false;
3659
4053
  for (let attempt = 0; attempt < retryLimit; attempt++) {
3660
4054
  await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
3661
4055
  const afterScreen = await runtime.readPaneScreen(pane) ?? "";
3662
4056
  const draft = parseDraftFromScreen(afterScreen);
3663
- if (draft === "")
4057
+ if (draft !== "" && draft !== null)
4058
+ sawDraft = true;
4059
+ if (draft === "" && sawDraft)
3664
4060
  return;
3665
4061
  if (draft === null && afterScreen !== preSendScreen)
3666
4062
  return;
3667
- await settleInputBox(runtime, pane);
4063
+ const settled = await settleInputBox(runtime, pane);
4064
+ if (settled)
4065
+ sawDraft = true;
4066
+ if (!sawDraft && !repasted) {
4067
+ repasted = true;
4068
+ await runtime.pasteToPane(pane, task);
4069
+ }
3668
4070
  await runtime.sendKeyToPane(pane, "Enter");
3669
4071
  }
3670
4072
  }
@@ -3692,7 +4094,9 @@ var dist_exports = {};
3692
4094
  __export(dist_exports, {
3693
4095
  CMUX_TIMEOUT: () => CMUX_TIMEOUT,
3694
4096
  CmuxEventsBridge: () => CmuxEventsBridge,
4097
+ CmuxStoreSource: () => CmuxStoreSource,
3695
4098
  DaemonCmux: () => DaemonCmux,
4099
+ NativeHookSource: () => NativeHookSource,
3696
4100
  NotifierRegistry: () => NotifierRegistry,
3697
4101
  RuntimeRegistry: () => RuntimeRegistry,
3698
4102
  WorkspaceRegistry: () => WorkspaceRegistry,
@@ -3705,8 +4109,10 @@ __export(dist_exports, {
3705
4109
  deriveRunState: () => deriveRunState,
3706
4110
  findCrew: () => findCrew,
3707
4111
  getFreePort: () => getFreePort,
4112
+ installClaudeHooks: () => installClaudeHooks,
3708
4113
  isInsideCmux: () => isInsideCmux,
3709
4114
  listProjectCrews: () => listProjectCrews,
4115
+ mapSubToLifecycle: () => mapSubToLifecycle,
3710
4116
  resolveCaptainWorkspace: () => resolveCaptainWorkspace,
3711
4117
  sendFirstTurnWhenReady: () => sendFirstTurnWhenReady
3712
4118
  });
@@ -3717,6 +4123,8 @@ var init_dist3 = __esm({
3717
4123
  init_workspaces2();
3718
4124
  init_events_bridge();
3719
4125
  init_daemon_cmux();
4126
+ init_cmux_store_source();
4127
+ init_native_hook_source();
3720
4128
  init_crew_pane();
3721
4129
  }
3722
4130
  });
@@ -4646,13 +5054,99 @@ var init_app_server_client = __esm({
4646
5054
  }
4647
5055
  });
4648
5056
 
5057
+ // packages/agents/dist/codex/codex-app-server-source.js
5058
+ function toSnapshot(ev) {
5059
+ const now = Date.now();
5060
+ switch (ev.type) {
5061
+ // ── running: a turn is live ──────────────────────────────────────────────
5062
+ case "task.started":
5063
+ case "task.reattached":
5064
+ case "task.turn.started":
5065
+ case "task.delta":
5066
+ case "task.progress":
5067
+ return { taskId: ev.id, state: "running", alive: true, origin: "agent", at: now };
5068
+ // ── idle: turn ended, crew alive, awaiting next input ────────────────────
5069
+ // task.failed: the turn ended with an error, but the crew process is alive.
5070
+ // task.session.ended: process is gone (alive:false) — signals liveness loss.
5071
+ case "task.turn.completed":
5072
+ return { taskId: ev.id, state: "idle", alive: true, origin: "agent", at: now };
5073
+ case "task.failed":
5074
+ return { taskId: ev.id, state: "idle", alive: true, origin: "agent", at: now };
5075
+ case "task.session.ended":
5076
+ return { taskId: ev.id, state: "idle", alive: false, origin: "agent", at: now };
5077
+ // ── needsInput: crew is blocked on a human ───────────────────────────────
5078
+ case "task.approval.requested":
5079
+ return {
5080
+ taskId: ev.id,
5081
+ state: "needsInput",
5082
+ alive: true,
5083
+ origin: "agent",
5084
+ at: now,
5085
+ detail: { note: ev.question, reason: ev.kind }
5086
+ };
5087
+ case "task.input.requested":
5088
+ return {
5089
+ taskId: ev.id,
5090
+ state: "needsInput",
5091
+ alive: true,
5092
+ origin: "agent",
5093
+ at: now,
5094
+ detail: { note: ev.question }
5095
+ };
5096
+ // ── terminal / notify-only — ignored ────────────────────────────────────
5097
+ // task.done, task.blocked, task.cancelled: terminal state from crew signal only.
5098
+ // task.session, task.stalled, task.quiet, task.idle, task.timeout, etc.: no-op.
5099
+ default:
5100
+ return null;
5101
+ }
5102
+ }
5103
+ var CodexAppServerSource;
5104
+ var init_codex_app_server_source = __esm({
5105
+ "packages/agents/dist/codex/codex-app-server-source.js"() {
5106
+ CodexAppServerSource = class {
5107
+ name = "codex-appserver";
5108
+ deps;
5109
+ /** taskId → last reported snapshot (for snapshot() liveness floor). */
5110
+ cache = /* @__PURE__ */ new Map();
5111
+ start(deps) {
5112
+ this.deps = deps;
5113
+ }
5114
+ stop() {
5115
+ this.deps = void 0;
5116
+ this.cache.clear();
5117
+ }
5118
+ /** Returns the last-reported snapshot for a known crew (liveness floor). */
5119
+ snapshot(taskId) {
5120
+ return this.cache.get(taskId);
5121
+ }
5122
+ /**
5123
+ * Feed a ControlEvent from CodexInteractiveDriver into this source.
5124
+ * The daemon wires: emit = (ev) => { source.observe(ev); handle(ev); }
5125
+ *
5126
+ * All events that carry lifecycle meaning for a codex crew are mapped to a
5127
+ * LifecycleSnapshot and reported. Events that are terminal signals (task.done,
5128
+ * task.cancelled, task.blocked) or notify-only (task.stalled, task.quiet, etc.)
5129
+ * are ignored — terminal state still comes exclusively from `squadrant crew signal`
5130
+ * (anti-#2576 invariant).
5131
+ */
5132
+ observe(ev) {
5133
+ const snap = toSnapshot(ev);
5134
+ if (!snap || !this.deps)
5135
+ return;
5136
+ this.cache.set(snap.taskId, snap);
5137
+ this.deps.report(snap);
5138
+ }
5139
+ };
5140
+ }
5141
+ });
5142
+
4649
5143
  // packages/agents/dist/codex/config.js
4650
5144
  import { readFile as readFile5 } from "fs/promises";
4651
- import { homedir as homedir9 } from "os";
4652
- import { join as join14 } from "path";
5145
+ import { homedir as homedir11 } from "os";
5146
+ import { join as join16 } from "path";
4653
5147
  async function resolveCodexModel() {
4654
- const home = process.env["CODEX_HOME"] ?? join14(homedir9(), ".codex");
4655
- const configPath = join14(home, "config.toml");
5148
+ const home = process.env["CODEX_HOME"] ?? join16(homedir11(), ".codex");
5149
+ const configPath = join16(home, "config.toml");
4656
5150
  let text;
4657
5151
  try {
4658
5152
  text = await readFile5(configPath, "utf8");
@@ -5173,9 +5667,9 @@ var init_sse_bridge = __esm({
5173
5667
 
5174
5668
  // packages/agents/dist/interactive/claude.js
5175
5669
  import { execSync as execSync7 } from "child_process";
5176
- import { readFileSync as readFileSync8 } from "fs";
5177
- import { homedir as homedir10 } from "os";
5178
- import { join as join15 } from "path";
5670
+ import { readFileSync as readFileSync10 } from "fs";
5671
+ import { homedir as homedir12 } from "os";
5672
+ import { join as join17 } from "path";
5179
5673
  function probeClaudeSettingsFlag() {
5180
5674
  try {
5181
5675
  const help = execSync7("claude --help", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
@@ -5226,11 +5720,11 @@ function deriveTranscriptPath(sessionId, cwd) {
5226
5720
  if (!sessionId || !cwd)
5227
5721
  return null;
5228
5722
  const escaped = cwd.replace(/[^a-zA-Z0-9]/g, "-");
5229
- return join15(homedir10(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
5723
+ return join17(homedir12(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
5230
5724
  }
5231
5725
  function readLastAssistantText(transcriptPath) {
5232
5726
  try {
5233
- const raw = readFileSync8(transcriptPath, "utf-8");
5727
+ const raw = readFileSync10(transcriptPath, "utf-8");
5234
5728
  const lines = raw.split(/\r?\n/);
5235
5729
  for (let i = lines.length - 1; i >= 0; i--) {
5236
5730
  const line = lines[i].trim();
@@ -5347,6 +5841,18 @@ function classifyPaneTail(tail) {
5347
5841
  }
5348
5842
  return { kind: "approval", text: "Crew is awaiting permission approval." };
5349
5843
  }
5844
+ const hasPickerFooter = cleaned.some((c) => c != null && PICKER_FOOTER_RE.test(c));
5845
+ if (options.length >= 2 && hasPickerFooter) {
5846
+ const firstOptCi = options[0].ci;
5847
+ for (let i = firstOptCi - 1; i >= 0; i--) {
5848
+ const c = cleaned[i];
5849
+ if (c == null)
5850
+ continue;
5851
+ if (c.endsWith("?"))
5852
+ return { kind: "question", text: c };
5853
+ }
5854
+ return { kind: "question", text: "Crew is awaiting a choice." };
5855
+ }
5350
5856
  const region = cleaned.filter((c) => c != null).join("\n");
5351
5857
  const q = detectTrailingQuestion(region);
5352
5858
  if (q)
@@ -5356,8 +5862,13 @@ function classifyPaneTail(tail) {
5356
5862
  if (c != null && ERROR_BANNER_RE.some((re) => re.test(c)))
5357
5863
  errLine = c;
5358
5864
  }
5359
- if (errLine)
5865
+ if (errLine) {
5866
+ const isRetrying = cleaned.some((c) => c != null && RETRYING_RE.test(c));
5867
+ const isExhausted = cleaned.some((c) => c != null && EXHAUSTED_RE.test(c));
5868
+ if (isRetrying && !isExhausted)
5869
+ return null;
5360
5870
  return { kind: "error", text: errLine.slice(0, 200) };
5871
+ }
5361
5872
  return null;
5362
5873
  }
5363
5874
  function stripChrome(raw) {
@@ -5374,7 +5885,7 @@ function stripChrome(raw) {
5374
5885
  return null;
5375
5886
  return trimmed;
5376
5887
  }
5377
- var ERROR_BANNER_RE, OPTION_RE, PURE_CHROME_RE, STATUS_LINE_RE;
5888
+ var ERROR_BANNER_RE, RETRYING_RE, EXHAUSTED_RE, OPTION_RE, PICKER_FOOTER_RE, PURE_CHROME_RE, STATUS_LINE_RE;
5378
5889
  var init_pane_classifier = __esm({
5379
5890
  "packages/agents/dist/interactive/pane-classifier.js"() {
5380
5891
  init_claude2();
@@ -5387,7 +5898,10 @@ var init_pane_classifier = __esm({
5387
5898
  /\bretr(?:y|ies)\s+(?:exhausted|limit\s+(?:reached|exceeded))\b/i,
5388
5899
  /\bmaximum\s+retries\b/i
5389
5900
  ];
5901
+ RETRYING_RE = /\bRetrying\b|\battempt\s+\d+\s*\/\s*\d+/i;
5902
+ EXHAUSTED_RE = /\bretr(?:y|ies)\s+(?:exhausted|limit\s+(?:reached|exceeded))\b|\bmaximum\s+retries\b/i;
5390
5903
  OPTION_RE = /^[❯>›]?\s*(\d+)\.\s+(.*\S)\s*$/;
5904
+ PICKER_FOOTER_RE = /↑↓\s*select|enter\s+submit|esc\s+dismiss/i;
5391
5905
  PURE_CHROME_RE = /^[\s─━│┃╭╮╰╯┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▁▂▃▄▅▆▇█▔▏▕]+$/;
5392
5906
  STATUS_LINE_RE = /accept edits on|shift\+tab|⏵⏵|\? for shortcuts|esc to interrupt|tokens? (used|left)|context left/i;
5393
5907
  }
@@ -5618,6 +6132,7 @@ var dist_exports2 = {};
5618
6132
  __export(dist_exports2, {
5619
6133
  AppServerClient: () => AppServerClient,
5620
6134
  CapabilityRegistry: () => CapabilityRegistry,
6135
+ CodexAppServerSource: () => CodexAppServerSource,
5621
6136
  CodexInteractiveDriver: () => CodexInteractiveDriver,
5622
6137
  HEADLESS_ERROR_TAIL: () => HEADLESS_ERROR_TAIL,
5623
6138
  MARKER_END: () => MARKER_END,
@@ -5656,6 +6171,7 @@ var init_dist4 = __esm({
5656
6171
  init_drivers();
5657
6172
  init_projection2();
5658
6173
  init_app_server_client();
6174
+ init_codex_app_server_source();
5659
6175
  init_driver();
5660
6176
  init_normalize();
5661
6177
  init_sse_bridge();
@@ -5672,11 +6188,11 @@ var init_dist4 = __esm({
5672
6188
  // packages/cli/src/index.ts
5673
6189
  init_dist();
5674
6190
  init_dist2();
5675
- import { Command as Command28 } from "commander";
5676
- import { readFileSync as readFileSync11, existsSync as existsSync11, writeFileSync as writeFileSync9 } from "fs";
6191
+ import { Command as Command29 } from "commander";
6192
+ import { readFileSync as readFileSync13, existsSync as existsSync12, writeFileSync as writeFileSync10 } from "fs";
5677
6193
  import { fileURLToPath as fileURLToPath6 } from "url";
5678
- import { dirname as dirname7, join as join23 } from "path";
5679
- import { homedir as homedir15 } from "os";
6194
+ import { dirname as dirname7, join as join26 } from "path";
6195
+ import { homedir as homedir18 } from "os";
5680
6196
 
5681
6197
  // packages/cli/src/commands/doctor.ts
5682
6198
  init_dist();
@@ -5693,10 +6209,10 @@ import chalk3 from "chalk";
5693
6209
  // packages/cli/src/commands/health-view.ts
5694
6210
  init_dist2();
5695
6211
  init_dist2();
5696
- import { homedir as homedir8 } from "os";
5697
- import { join as join13 } from "path";
6212
+ import { homedir as homedir10 } from "os";
6213
+ import { join as join15 } from "path";
5698
6214
  import chalk2 from "chalk";
5699
- var SOCK = join13(homedir8(), ".config", "squadrant", "squadrant.sock");
6215
+ var SOCK = join15(homedir10(), ".config", "squadrant", "squadrant.sock");
5700
6216
  async function queryHealth(project) {
5701
6217
  try {
5702
6218
  const reply = await sendRequest(SOCK, { kind: "health", project });
@@ -6530,9 +7046,9 @@ init_dist4();
6530
7046
  import { Command as Command8 } from "commander";
6531
7047
  import { createConnection as createConnection3 } from "net";
6532
7048
  import { randomUUID as randomUUID4 } from "crypto";
6533
- import { homedir as homedir12 } from "os";
6534
- import { join as join17 } from "path";
6535
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
7049
+ import { homedir as homedir14 } from "os";
7050
+ import { join as join19 } from "path";
7051
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
6536
7052
 
6537
7053
  // packages/cli/src/commands/crew-output.ts
6538
7054
  function tailLines(text, maxLines = 40, maxBytes = 4096) {
@@ -6590,11 +7106,11 @@ init_dist2();
6590
7106
  import { Command as Command6 } from "commander";
6591
7107
  import chalk8 from "chalk";
6592
7108
  import { createConnection as createConnection2 } from "net";
6593
- import { homedir as homedir11 } from "os";
6594
- import { join as join16 } from "path";
7109
+ import { homedir as homedir13 } from "os";
7110
+ import { join as join18 } from "path";
6595
7111
  import { createInterface } from "readline";
6596
7112
  function socketPath() {
6597
- return process.env.SQUADRANTD_SOCK ?? join16(homedir11(), ".config", "squadrant", "squadrant.sock");
7113
+ return process.env.SQUADRANTD_SOCK ?? join18(homedir13(), ".config", "squadrant", "squadrant.sock");
6598
7114
  }
6599
7115
  function rule(width, ch = "\u2500") {
6600
7116
  return ch.repeat(Math.max(0, width));
@@ -6830,7 +7346,7 @@ var crewChatCommand = new Command7("chat").description("[DEPRECATED] alias for `
6830
7346
  });
6831
7347
 
6832
7348
  // packages/cli/src/commands/crew-control.ts
6833
- var SOCK2 = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
7349
+ var SOCK2 = join19(homedir14(), ".config", "squadrant", "squadrant.sock");
6834
7350
  var CODEX_FIRST_TURN_DELAY_MS = 1500;
6835
7351
  async function sendCodexFirstTurn(taskId, text) {
6836
7352
  await new Promise((r) => setTimeout(r, CODEX_FIRST_TURN_DELAY_MS));
@@ -6935,10 +7451,10 @@ function buildSignalRequest(signal, o) {
6935
7451
  return { kind: "event", project, event };
6936
7452
  }
6937
7453
  function defaultWriteResult(id, payload) {
6938
- const dir = join17(homedir12(), ".config", "squadrant", "state", "_results");
6939
- mkdirSync6(dir, { recursive: true });
6940
- const file = join17(dir, `${id}.txt`);
6941
- writeFileSync7(file, payload);
7454
+ const dir = join19(homedir14(), ".config", "squadrant", "state", "_results");
7455
+ mkdirSync7(dir, { recursive: true });
7456
+ const file = join19(dir, `${id}.txt`);
7457
+ writeFileSync8(file, payload);
6942
7458
  return file;
6943
7459
  }
6944
7460
  function addControlPlaneCrewCommands(crew) {
@@ -7037,8 +7553,8 @@ addControlPlaneCrewCommands(crewControlCommand);
7037
7553
 
7038
7554
  // packages/cli/src/lib/per-crew-settings.ts
7039
7555
  init_dist4();
7040
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
7041
- import { join as join18 } from "path";
7556
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
7557
+ import { join as join20 } from "path";
7042
7558
  var CREW_PERMISSION_ALLOWLIST = [
7043
7559
  // git — read + safe mutations (reset/clean/config intentionally excluded)
7044
7560
  "Bash(git status:*)",
@@ -7129,24 +7645,24 @@ function mergeCrewPermissions(settings) {
7129
7645
  return next;
7130
7646
  }
7131
7647
  function writePerCrewSettingsLocal(o) {
7132
- const dir = join18(o.projectCwd, ".claude");
7133
- mkdirSync7(dir, { recursive: true });
7134
- const file = join18(dir, "settings.local.json");
7648
+ const dir = join20(o.projectCwd, ".claude");
7649
+ mkdirSync8(dir, { recursive: true });
7650
+ const file = join20(dir, "settings.local.json");
7135
7651
  let existing = {};
7136
7652
  try {
7137
- const raw = healStaleCockpitRefs(readFileSync9(file, "utf-8"));
7653
+ const raw = healStaleCockpitRefs(readFileSync11(file, "utf-8"));
7138
7654
  existing = JSON.parse(raw);
7139
7655
  } catch {
7140
7656
  }
7141
7657
  const withHooks = mergeClaudeHooks(existing, o.hookCmd ?? "squadrant crew _hook");
7142
7658
  const merged = mergeCrewPermissions(withHooks);
7143
- writeFileSync8(file, JSON.stringify(merged, null, 2));
7659
+ writeFileSync9(file, JSON.stringify(merged, null, 2));
7144
7660
  return file;
7145
7661
  }
7146
7662
  function writePerCrewOpencodeConfig(o) {
7147
- const dir = join18(o.stateRoot, o.project, o.taskId);
7148
- mkdirSync7(dir, { recursive: true });
7149
- const file = join18(dir, "opencode.json");
7663
+ const dir = join20(o.stateRoot, o.project, o.taskId);
7664
+ mkdirSync8(dir, { recursive: true });
7665
+ const file = join20(dir, "opencode.json");
7150
7666
  const config = {
7151
7667
  permission: {
7152
7668
  read: "allow",
@@ -7161,7 +7677,7 @@ function writePerCrewOpencodeConfig(o) {
7161
7677
  external_directory: { "**": "allow" }
7162
7678
  }
7163
7679
  };
7164
- writeFileSync8(file, JSON.stringify(config, null, 2));
7680
+ writeFileSync9(file, JSON.stringify(config, null, 2));
7165
7681
  return file;
7166
7682
  }
7167
7683
 
@@ -7454,8 +7970,8 @@ init_dist();
7454
7970
  init_dist3();
7455
7971
  import { Command as Command11 } from "commander";
7456
7972
  import { execSync as execSync10 } from "child_process";
7457
- import { homedir as homedir14 } from "os";
7458
- import { join as join20 } from "path";
7973
+ import { homedir as homedir16 } from "os";
7974
+ import { join as join22 } from "path";
7459
7975
  import chalk12 from "chalk";
7460
7976
 
7461
7977
  // packages/web/dist/read-status.js
@@ -7651,9 +8167,9 @@ function mergeSnapshot(daemon, external, now) {
7651
8167
  // packages/web/dist/probes.js
7652
8168
  init_dist();
7653
8169
  init_dist();
7654
- import { join as join19 } from "path";
7655
- import { homedir as homedir13 } from "os";
7656
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
8170
+ import { join as join21 } from "path";
8171
+ import { homedir as homedir15 } from "os";
8172
+ import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
7657
8173
  import { execFile as execFile3 } from "child_process";
7658
8174
  var DEFAULT_TIMEOUT_MS = 2e3;
7659
8175
  var AGENT_CLIS = ["claude", "codex", "gemini", "opencode"];
@@ -7689,7 +8205,7 @@ function vaultProbe(run, dir) {
7689
8205
  return { state: "unknown", detail: "no vault configured" };
7690
8206
  if (!run.pathExists(dir))
7691
8207
  return { state: "gone", detail: "vault directory missing" };
7692
- if (!run.pathExists(join19(dir, ".obsidian")))
8208
+ if (!run.pathExists(join21(dir, ".obsidian")))
7693
8209
  return { state: "gone", detail: "no .obsidian/ (not a vault)" };
7694
8210
  return { state: "alive" };
7695
8211
  } catch {
@@ -7757,13 +8273,13 @@ async function runExternalProbes(run, timeoutMs = DEFAULT_TIMEOUT_MS) {
7757
8273
  const sessions = probeSessions(run);
7758
8274
  return { cmux: cmux2, agentClis, vaults, config: { parseable, projectPaths, sessions } };
7759
8275
  }
7760
- var SESSIONS_PATH = join19(homedir13(), ".config", "squadrant", "sessions.json");
8276
+ var SESSIONS_PATH = join21(homedir15(), ".config", "squadrant", "sessions.json");
7761
8277
  function onPath(cli) {
7762
8278
  const dirs = (process.env.PATH ?? "").split(":").filter(Boolean);
7763
- return dirs.some((d) => existsSync10(join19(d, cli)));
8279
+ return dirs.some((d) => existsSync11(join21(d, cli)));
7764
8280
  }
7765
8281
  function readSessionsHashes() {
7766
- const raw = JSON.parse(readFileSync10(SESSIONS_PATH, "utf-8"));
8282
+ const raw = JSON.parse(readFileSync12(SESSIONS_PATH, "utf-8"));
7767
8283
  const hashes = Object.values(raw.workspaces ?? {}).map((w) => w.templateHash).filter((h) => typeof h === "string" && h.length > 0);
7768
8284
  return [...new Set(hashes)];
7769
8285
  }
@@ -7777,7 +8293,7 @@ function defaultProbeRunners() {
7777
8293
  }
7778
8294
  }),
7779
8295
  probeOnPath: async (cli) => onPath(cli),
7780
- pathExists: (p) => existsSync10(p),
8296
+ pathExists: (p) => existsSync11(p),
7781
8297
  loadConfig: () => loadConfig(),
7782
8298
  loadSessionsHashes: () => readSessionsHashes()
7783
8299
  };
@@ -8430,7 +8946,7 @@ async function startWebServer(opts) {
8430
8946
 
8431
8947
  // packages/cli/src/commands/dashboard.ts
8432
8948
  init_dist();
8433
- var SOCK3 = join20(homedir14(), ".config", "squadrant", "squadrant.sock");
8949
+ var SOCK3 = join22(homedir16(), ".config", "squadrant", "squadrant.sock");
8434
8950
  function detectCurrentWorkspace2() {
8435
8951
  const out = execSync10(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
8436
8952
  const match = out.match(/workspace:\d+/);
@@ -8514,13 +9030,87 @@ init_dist();
8514
9030
  init_dist4();
8515
9031
  init_dist3();
8516
9032
  init_dist2();
8517
- init_dist2();
8518
9033
  import { Command as Command12 } from "commander";
8519
9034
  import { execSync as execSync11 } from "child_process";
8520
9035
  import fs20 from "fs";
8521
9036
  import path23 from "path";
8522
9037
  import os12 from "os";
8523
9038
  import chalk13 from "chalk";
9039
+
9040
+ // packages/cli/src/commands/launch-interactive.ts
9041
+ import checkbox, { Separator } from "@inquirer/checkbox";
9042
+ function getYesterday() {
9043
+ const d = /* @__PURE__ */ new Date();
9044
+ d.setDate(d.getDate() - 1);
9045
+ return d.toISOString().slice(0, 10);
9046
+ }
9047
+ function partitionByYesterday(entries, yesterday) {
9048
+ const y = [];
9049
+ const r = [];
9050
+ for (const e of entries) {
9051
+ if (e.lastLaunched === yesterday) {
9052
+ y.push(e);
9053
+ } else {
9054
+ r.push(e);
9055
+ }
9056
+ }
9057
+ return { yesterday: y, rest: r };
9058
+ }
9059
+ async function selectCaptainsInteractive(entries, yesterday = getYesterday()) {
9060
+ const { yesterday: yesterdayEntries, rest: restEntries } = partitionByYesterday(entries, yesterday);
9061
+ if (yesterdayEntries.length === 0) {
9062
+ const result2 = await checkbox({
9063
+ message: "Select captains to launch:",
9064
+ choices: entries.map((e) => ({
9065
+ name: `${e.captainName} (${e.projectName})`,
9066
+ value: e.projectName,
9067
+ checked: false
9068
+ })),
9069
+ pageSize: 20
9070
+ });
9071
+ return result2;
9072
+ }
9073
+ const initialChoices = [
9074
+ new Separator("\u2500\u2500 Opened yesterday \u2500\u2500"),
9075
+ ...yesterdayEntries.map((e) => ({
9076
+ name: `${e.captainName} (${e.projectName})`,
9077
+ value: e.projectName,
9078
+ checked: true
9079
+ })),
9080
+ new Separator(),
9081
+ { name: "Show all projects", value: "__show_all__", checked: false }
9082
+ ];
9083
+ const result = await checkbox({
9084
+ message: "Select captains to launch:",
9085
+ choices: initialChoices,
9086
+ pageSize: 20
9087
+ });
9088
+ if (result.includes("__show_all__")) {
9089
+ const allChecked = yesterdayEntries.map((e) => e.projectName);
9090
+ const result2 = await checkbox({
9091
+ message: "Select captains to launch (all projects):",
9092
+ choices: [
9093
+ new Separator("\u2500\u2500 Opened yesterday \u2500\u2500"),
9094
+ ...yesterdayEntries.map((e) => ({
9095
+ name: `${e.captainName} (${e.projectName})`,
9096
+ value: e.projectName,
9097
+ checked: allChecked.includes(e.projectName)
9098
+ })),
9099
+ ...restEntries.map((e) => ({
9100
+ name: `${e.captainName} (${e.projectName})`,
9101
+ value: e.projectName,
9102
+ checked: false
9103
+ }))
9104
+ ],
9105
+ pageSize: 30
9106
+ });
9107
+ return result2;
9108
+ }
9109
+ return result;
9110
+ }
9111
+
9112
+ // packages/cli/src/commands/launch.ts
9113
+ init_dist2();
8524
9114
  var CMUX_APP = "/Applications/cmux.app";
8525
9115
  var TEMPLATES_DIR4 = path23.join(os12.homedir(), ".config", "squadrant", "templates");
8526
9116
  var SESSIONS_PATH2 = path23.join(os12.homedir(), ".config", "squadrant", "sessions.json");
@@ -8604,12 +9194,43 @@ var launchCommand = new Command12("launch").description(
8604
9194
  }
8605
9195
  console.log("");
8606
9196
  } else if (!project) {
8607
- console.error(
8608
- chalk13.red(
8609
- "\n \u2718 Specify a project name, or pass --all to launch every captain.\n For one-shot Command tasks, use `squadrant command --task <briefing|learnings-review|wiki-aggregate>`.\n"
8610
- )
8611
- );
8612
- process.exit(1);
9197
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
9198
+ console.error(
9199
+ chalk13.red(
9200
+ "\n \u2718 Specify a project name, or pass --all to launch every captain.\n For one-shot Command tasks, use `squadrant command --task <briefing|learnings-review|wiki-aggregate>`.\n"
9201
+ )
9202
+ );
9203
+ process.exit(1);
9204
+ }
9205
+ ensureCmuxReady();
9206
+ const sessions = loadSessions(SESSIONS_PATH2);
9207
+ const entries = Object.entries(config.projects).map(([name, proj]) => ({
9208
+ projectName: name,
9209
+ captainName: proj.captainName,
9210
+ lastLaunched: sessions.workspaces[proj.captainName]?.lastLaunched ?? null
9211
+ }));
9212
+ const selected = await selectCaptainsInteractive(entries);
9213
+ if (selected.length === 0) {
9214
+ console.log(chalk13.yellow("\n No captains selected.\n"));
9215
+ return;
9216
+ }
9217
+ console.log(chalk13.bold(`
9218
+ Launching ${selected.length} captain workspace(s) in parallel
9219
+ `));
9220
+ await Promise.all(selected.map(async (name) => {
9221
+ const proj = config.projects[name];
9222
+ const projPath = resolveHome(proj.path);
9223
+ const spokePath = resolveHome(proj.spokeVault);
9224
+ if (!fs20.existsSync(spokePath)) {
9225
+ const spokeDriver = new WorkspaceRegistry({ obsidian: createObsidianDriver }).forProject(name, config);
9226
+ await ensureSpokeLayout(spokeDriver);
9227
+ console.log(chalk13.cyan(` \u2714 Created spoke vault at ${spokePath}`));
9228
+ }
9229
+ console.log(chalk13.bold(`
9230
+ Captain: ${proj.captainName} (${name})`));
9231
+ await launchOne(proj.captainName, "captain", projPath, config.defaults.permissions?.captain || "auto", false, true, name);
9232
+ }));
9233
+ console.log("");
8613
9234
  } else {
8614
9235
  if (!config.projects[project]) {
8615
9236
  console.error(
@@ -9606,7 +10227,7 @@ init_dist2();
9606
10227
  import { Command as Command22 } from "commander";
9607
10228
  import fs25 from "fs";
9608
10229
  import { fileURLToPath as fileURLToPath5 } from "url";
9609
- import { dirname as dirname5, join as join21 } from "path";
10230
+ import { dirname as dirname5, join as join23 } from "path";
9610
10231
  import chalk22 from "chalk";
9611
10232
  function runConfigCheck(opts) {
9612
10233
  const raw = JSON.parse(fs25.readFileSync(opts.configPath, "utf-8"));
@@ -9742,7 +10363,7 @@ configCommand.command("set").description("Write a config value by dotted key (e.
9742
10363
  }
9743
10364
  });
9744
10365
  function readPkgVersion2() {
9745
- const pkgPath = join21(dirname5(fileURLToPath5(import.meta.url)), "..", "package.json");
10366
+ const pkgPath = join23(dirname5(fileURLToPath5(import.meta.url)), "..", "package.json");
9746
10367
  return JSON.parse(fs25.readFileSync(pkgPath, "utf-8")).version;
9747
10368
  }
9748
10369
 
@@ -10013,12 +10634,12 @@ var effortCommand = new Command26("effort").description("Get or set the global c
10013
10634
  // packages/cli/src/commands/telegram.ts
10014
10635
  init_dist();
10015
10636
  init_dist2();
10016
- import { join as join22, dirname as dirname6 } from "path";
10637
+ import { join as join24, dirname as dirname6 } from "path";
10017
10638
  import { emitKeypressEvents } from "readline";
10018
10639
  import { Command as Command27 } from "commander";
10019
10640
  import chalk27 from "chalk";
10020
10641
  function defaultStateRoot() {
10021
- return join22(dirname6(DEFAULT_CONFIG_PATH), "state");
10642
+ return join24(dirname6(DEFAULT_CONFIG_PATH), "state");
10022
10643
  }
10023
10644
  async function questionMasked() {
10024
10645
  return new Promise((resolve3) => {
@@ -10339,25 +10960,88 @@ telegramCommand.command("send").argument("<project>", "project whose topic recei
10339
10960
  }
10340
10961
  });
10341
10962
 
10963
+ // packages/cli/src/commands/hooks.ts
10964
+ init_dist2();
10965
+ init_dist4();
10966
+ import { Command as Command28 } from "commander";
10967
+ import { join as join25 } from "path";
10968
+ import { homedir as homedir17 } from "os";
10969
+ var SOCK4 = join25(homedir17(), ".config", "squadrant", "squadrant.sock");
10970
+ async function sendToSock(req) {
10971
+ await sendRequest(SOCK4, req);
10972
+ }
10973
+ function mapHookSub(sub, payload, taskId) {
10974
+ switch (sub) {
10975
+ case "session-start":
10976
+ case "prompt-submit":
10977
+ case "pre-tool-use":
10978
+ return { type: "task.progress", id: taskId, note: sub };
10979
+ case "stop":
10980
+ return mapClaudeHookToEvent("Stop", payload, taskId);
10981
+ case "notification":
10982
+ return mapClaudeHookToEvent("Notification", payload, taskId);
10983
+ case "ask-question": {
10984
+ const q = typeof payload?.question === "string" ? payload.question : "awaiting input";
10985
+ return { type: "task.input.requested", id: taskId, requestId: 0, question: q };
10986
+ }
10987
+ case "session-end":
10988
+ return mapClaudeHookToEvent("SessionEnd", payload, taskId);
10989
+ default:
10990
+ return null;
10991
+ }
10992
+ }
10993
+ function hooksCommand() {
10994
+ const hooks = new Command28("hooks").description("(internal) receive lifecycle hook events from agent processes");
10995
+ hooks.command("claude <sub>", { hidden: true }).description("internal: bridge a NativeHookSource claude hook to squadrantd").action(async (sub) => {
10996
+ const taskId = process.env.SQUADRANT_CREW_TASK_ID;
10997
+ const project = process.env.SQUADRANT_CREW_PROJECT;
10998
+ if (!taskId || !project) {
10999
+ process.exit(0);
11000
+ }
11001
+ let stdin = "";
11002
+ try {
11003
+ for await (const chunk of process.stdin) stdin += chunk;
11004
+ } catch {
11005
+ }
11006
+ let payload = void 0;
11007
+ if (stdin.trim()) {
11008
+ try {
11009
+ payload = JSON.parse(stdin);
11010
+ } catch {
11011
+ }
11012
+ }
11013
+ const ev = mapHookSub(sub, payload, taskId);
11014
+ if (!ev) {
11015
+ process.exit(0);
11016
+ }
11017
+ try {
11018
+ await sendToSock({ kind: "event", project, event: ev });
11019
+ } catch {
11020
+ }
11021
+ process.exit(0);
11022
+ });
11023
+ return hooks;
11024
+ }
11025
+
10342
11026
  // packages/cli/src/index.ts
10343
11027
  init_dist();
10344
11028
  init_dist();
10345
11029
  init_dist();
10346
11030
  var __dirname = dirname7(fileURLToPath6(import.meta.url));
10347
- var pkg = JSON.parse(readFileSync11(join23(__dirname, "..", "package.json"), "utf-8"));
11031
+ var pkg = JSON.parse(readFileSync13(join26(__dirname, "..", "package.json"), "utf-8"));
10348
11032
  ensureRuntimeSynced({
10349
- sourceRoot: join23(__dirname, ".."),
10350
- runtimeRoot: join23(homedir15(), ".config", "squadrant")
11033
+ sourceRoot: join26(__dirname, ".."),
11034
+ runtimeRoot: join26(homedir18(), ".config", "squadrant")
10351
11035
  });
10352
11036
  if (process.argv[2] !== "config") {
10353
11037
  try {
10354
- const cfgPath = join23(homedir15(), ".config", "squadrant", "config.json");
10355
- if (existsSync11(cfgPath)) {
10356
- const cfg = JSON.parse(readFileSync11(cfgPath, "utf-8"));
11038
+ const cfgPath = join26(homedir18(), ".config", "squadrant", "config.json");
11039
+ if (existsSync12(cfgPath)) {
11040
+ const cfg = JSON.parse(readFileSync13(cfgPath, "utf-8"));
10357
11041
  if (needsCheck(cfg, pkg.version)) {
10358
11042
  const items = detectDrift(cfg, getDefaultConfig());
10359
11043
  if (items.length === 0) {
10360
- writeFileSync9(cfgPath, JSON.stringify(withStamp(cfg, pkg.version), null, 2) + "\n");
11044
+ writeFileSync10(cfgPath, JSON.stringify(withStamp(cfg, pkg.version), null, 2) + "\n");
10361
11045
  } else {
10362
11046
  const from = cfg._squadrantVersion ?? "an earlier version";
10363
11047
  process.stderr.write(
@@ -10376,7 +11060,7 @@ if (process.argv[2] !== "config") {
10376
11060
  if (!process.env.SQUADRANT_DAEMON_SKIP) {
10377
11061
  ensureDaemon();
10378
11062
  }
10379
- var program = new Command28();
11063
+ var program = new Command29();
10380
11064
  program.name("squadrant").description("Multi-project orchestration for your coding agents (Claude, Codex, opencode, Gemini)").version(pkg.version);
10381
11065
  program.addCommand(doctorCommand);
10382
11066
  program.addCommand(initCommand);
@@ -10403,6 +11087,7 @@ program.addCommand(groupCommand);
10403
11087
  program.addCommand(cmuxCommand);
10404
11088
  program.addCommand(effortCommand);
10405
11089
  program.addCommand(telegramCommand);
11090
+ program.addCommand(hooksCommand());
10406
11091
  program.parseAsync().catch((e) => {
10407
11092
  process.stderr.write(`error: ${e instanceof Error ? e.message : String(e)}
10408
11093
  `);