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.
@@ -57,10 +57,10 @@ var init_snapshot = __esm({
57
57
  });
58
58
 
59
59
  // packages/cli/src/squadrantd.ts
60
- import { join as join15, dirname as dirname5 } from "path";
61
- import { homedir as homedir10 } from "os";
60
+ import { join as join17, dirname as dirname5 } from "path";
61
+ import { homedir as homedir12 } from "os";
62
62
  import { fileURLToPath as fileURLToPath3 } from "url";
63
- import { readFileSync as readFileSync9 } from "fs";
63
+ import { readFileSync as readFileSync11 } from "fs";
64
64
 
65
65
  // packages/shared/dist/config.js
66
66
  import fs from "fs";
@@ -1688,6 +1688,7 @@ var ERROR_BANNER_RE = [
1688
1688
  /\bmaximum\s+retries\b/i
1689
1689
  ];
1690
1690
  var OPTION_RE = /^[❯>›]?\s*(\d+)\.\s+(.*\S)\s*$/;
1691
+ var PICKER_FOOTER_RE = /↑↓\s*select|enter\s+submit|esc\s+dismiss/i;
1691
1692
  var PURE_CHROME_RE = /^[\s─━│┃╭╮╰╯┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▁▂▃▄▅▆▇█▔▏▕]+$/;
1692
1693
  var STATUS_LINE_RE = /accept edits on|shift\+tab|⏵⏵|\? for shortcuts|esc to interrupt|tokens? (used|left)|context left/i;
1693
1694
  function stripChrome(raw) {
@@ -1731,6 +1732,18 @@ function classifyPaneTail(tail) {
1731
1732
  }
1732
1733
  return { kind: "approval", text: "Crew is awaiting permission approval." };
1733
1734
  }
1735
+ const hasPickerFooter = cleaned.some((c) => c != null && PICKER_FOOTER_RE.test(c));
1736
+ if (options.length >= 2 && hasPickerFooter) {
1737
+ const firstOptCi = options[0].ci;
1738
+ for (let i = firstOptCi - 1; i >= 0; i--) {
1739
+ const c = cleaned[i];
1740
+ if (c == null)
1741
+ continue;
1742
+ if (c.endsWith("?"))
1743
+ return { kind: "question", text: c };
1744
+ }
1745
+ return { kind: "question", text: "Crew is awaiting a choice." };
1746
+ }
1734
1747
  const region = cleaned.filter((c) => c != null).join("\n");
1735
1748
  const q = detectTrailingQuestion(region);
1736
1749
  if (q)
@@ -3262,6 +3275,23 @@ import path10 from "path";
3262
3275
  var TEMPLATES_DIR = path10.join(os4.homedir(), ".config", "squadrant", "templates");
3263
3276
  var STATE_ROOT = path10.join(os4.homedir(), ".config", "squadrant", "state");
3264
3277
 
3278
+ // packages/core/dist/lifecycle-source.js
3279
+ function reduceLifecycle(prev, next) {
3280
+ if (next.origin === "agent") {
3281
+ return next.state;
3282
+ }
3283
+ if (next.state === "needsInput") {
3284
+ return prev?.state ?? "unknown";
3285
+ }
3286
+ if (prev?.state === "needsInput") {
3287
+ return "needsInput";
3288
+ }
3289
+ if (prev?.origin === "agent" && prev.at >= next.at) {
3290
+ return prev.state;
3291
+ }
3292
+ return next.state;
3293
+ }
3294
+
3265
3295
  // packages/agents/dist/drivers/claude.js
3266
3296
  import { execSync as execSync2 } from "child_process";
3267
3297
 
@@ -3480,6 +3510,87 @@ function defaultSpawn() {
3480
3510
  return nodeSpawn("codex", ["app-server"], { stdio: ["pipe", "pipe", "pipe"] });
3481
3511
  }
3482
3512
 
3513
+ // packages/agents/dist/codex/codex-app-server-source.js
3514
+ var CodexAppServerSource = class {
3515
+ name = "codex-appserver";
3516
+ deps;
3517
+ /** taskId → last reported snapshot (for snapshot() liveness floor). */
3518
+ cache = /* @__PURE__ */ new Map();
3519
+ start(deps) {
3520
+ this.deps = deps;
3521
+ }
3522
+ stop() {
3523
+ this.deps = void 0;
3524
+ this.cache.clear();
3525
+ }
3526
+ /** Returns the last-reported snapshot for a known crew (liveness floor). */
3527
+ snapshot(taskId) {
3528
+ return this.cache.get(taskId);
3529
+ }
3530
+ /**
3531
+ * Feed a ControlEvent from CodexInteractiveDriver into this source.
3532
+ * The daemon wires: emit = (ev) => { source.observe(ev); handle(ev); }
3533
+ *
3534
+ * All events that carry lifecycle meaning for a codex crew are mapped to a
3535
+ * LifecycleSnapshot and reported. Events that are terminal signals (task.done,
3536
+ * task.cancelled, task.blocked) or notify-only (task.stalled, task.quiet, etc.)
3537
+ * are ignored — terminal state still comes exclusively from `squadrant crew signal`
3538
+ * (anti-#2576 invariant).
3539
+ */
3540
+ observe(ev) {
3541
+ const snap = toSnapshot(ev);
3542
+ if (!snap || !this.deps)
3543
+ return;
3544
+ this.cache.set(snap.taskId, snap);
3545
+ this.deps.report(snap);
3546
+ }
3547
+ };
3548
+ function toSnapshot(ev) {
3549
+ const now = Date.now();
3550
+ switch (ev.type) {
3551
+ // ── running: a turn is live ──────────────────────────────────────────────
3552
+ case "task.started":
3553
+ case "task.reattached":
3554
+ case "task.turn.started":
3555
+ case "task.delta":
3556
+ case "task.progress":
3557
+ return { taskId: ev.id, state: "running", alive: true, origin: "agent", at: now };
3558
+ // ── idle: turn ended, crew alive, awaiting next input ────────────────────
3559
+ // task.failed: the turn ended with an error, but the crew process is alive.
3560
+ // task.session.ended: process is gone (alive:false) — signals liveness loss.
3561
+ case "task.turn.completed":
3562
+ return { taskId: ev.id, state: "idle", alive: true, origin: "agent", at: now };
3563
+ case "task.failed":
3564
+ return { taskId: ev.id, state: "idle", alive: true, origin: "agent", at: now };
3565
+ case "task.session.ended":
3566
+ return { taskId: ev.id, state: "idle", alive: false, origin: "agent", at: now };
3567
+ // ── needsInput: crew is blocked on a human ───────────────────────────────
3568
+ case "task.approval.requested":
3569
+ return {
3570
+ taskId: ev.id,
3571
+ state: "needsInput",
3572
+ alive: true,
3573
+ origin: "agent",
3574
+ at: now,
3575
+ detail: { note: ev.question, reason: ev.kind }
3576
+ };
3577
+ case "task.input.requested":
3578
+ return {
3579
+ taskId: ev.id,
3580
+ state: "needsInput",
3581
+ alive: true,
3582
+ origin: "agent",
3583
+ at: now,
3584
+ detail: { note: ev.question }
3585
+ };
3586
+ // ── terminal / notify-only — ignored ────────────────────────────────────
3587
+ // task.done, task.blocked, task.cancelled: terminal state from crew signal only.
3588
+ // task.session, task.stalled, task.quiet, task.idle, task.timeout, etc.: no-op.
3589
+ default:
3590
+ return null;
3591
+ }
3592
+ }
3593
+
3483
3594
  // packages/agents/dist/codex/config.js
3484
3595
  import { readFile as readFile5 } from "fs/promises";
3485
3596
  import { homedir as homedir8 } from "os";
@@ -4709,17 +4820,346 @@ var DaemonCmux = class {
4709
4820
  }
4710
4821
  };
4711
4822
 
4823
+ // packages/workspaces/dist/cmux-daemon/cmux-store-source.js
4824
+ import { join as join15 } from "path";
4825
+ import { homedir as homedir10 } from "os";
4826
+ import { watch, readdirSync as readdirSync3, readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
4827
+ var CmuxStoreSource = class {
4828
+ name = "cmux-store";
4829
+ stateDir;
4830
+ debounceMs;
4831
+ isPidAlive;
4832
+ listFiles;
4833
+ readFile;
4834
+ fileExists;
4835
+ watchDir;
4836
+ scheduleTimer;
4837
+ cancelTimer;
4838
+ log;
4839
+ deps;
4840
+ stopWatcher;
4841
+ debounceTimer;
4842
+ /** taskId → last reported snapshot (for snapshot() liveness floor). */
4843
+ cache = /* @__PURE__ */ new Map();
4844
+ constructor(opts = {}) {
4845
+ this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir10(), ".cmuxterm");
4846
+ this.debounceMs = opts.debounceMs ?? 50;
4847
+ this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
4848
+ this.listFiles = opts.listFiles ?? defaultListFiles;
4849
+ this.readFile = opts.readFile ?? defaultReadFile;
4850
+ this.fileExists = opts.fileExists ?? existsSync10;
4851
+ this.watchDir = opts.watchDir ?? defaultWatchDir;
4852
+ this.scheduleTimer = opts.scheduleTimer ?? setTimeout;
4853
+ this.cancelTimer = opts.cancelTimer ?? clearTimeout;
4854
+ this.log = opts.log ?? (() => {
4855
+ });
4856
+ }
4857
+ start(deps) {
4858
+ this.deps = deps;
4859
+ this.scan();
4860
+ try {
4861
+ this.stopWatcher = this.watchDir(this.stateDir, () => this.scheduleDebounced());
4862
+ } catch (e) {
4863
+ this.log(`cmux-store: failed to watch ${this.stateDir}: ${e.message}`);
4864
+ }
4865
+ }
4866
+ stop() {
4867
+ if (this.debounceTimer !== void 0) {
4868
+ this.cancelTimer(this.debounceTimer);
4869
+ this.debounceTimer = void 0;
4870
+ }
4871
+ this.stopWatcher?.();
4872
+ this.stopWatcher = void 0;
4873
+ this.deps = void 0;
4874
+ this.cache.clear();
4875
+ }
4876
+ /** Returns the last-reported snapshot for a known crew (liveness floor). */
4877
+ snapshot(taskId) {
4878
+ return this.cache.get(taskId);
4879
+ }
4880
+ // ── private ─────────────────────────────────────────────────────────────────
4881
+ scheduleDebounced() {
4882
+ if (this.debounceTimer !== void 0) {
4883
+ this.cancelTimer(this.debounceTimer);
4884
+ }
4885
+ this.debounceTimer = this.scheduleTimer(() => {
4886
+ this.debounceTimer = void 0;
4887
+ this.scan();
4888
+ }, this.debounceMs);
4889
+ }
4890
+ scan() {
4891
+ if (!this.deps)
4892
+ return;
4893
+ for (const filename of this.listFiles(this.stateDir)) {
4894
+ this.scanFile(filename);
4895
+ }
4896
+ }
4897
+ scanFile(filename) {
4898
+ const deps = this.deps;
4899
+ const filePath = join15(this.stateDir, filename);
4900
+ const lockPath = `${filePath}.lock`;
4901
+ if (this.fileExists(lockPath)) {
4902
+ this.log(`cmux-store: skipping ${filename} (locked)`);
4903
+ return;
4904
+ }
4905
+ const raw = this.readFile(filePath);
4906
+ if (!raw)
4907
+ return;
4908
+ let parsed;
4909
+ try {
4910
+ parsed = JSON.parse(raw);
4911
+ } catch {
4912
+ this.log(`cmux-store: failed to parse ${filename}`);
4913
+ return;
4914
+ }
4915
+ for (const session of Object.values(parsed.sessions ?? {})) {
4916
+ this.processSession(session, deps);
4917
+ }
4918
+ }
4919
+ processSession(session, deps) {
4920
+ if (!session.sessionId || !session.cwd || typeof session.pid !== "number")
4921
+ return;
4922
+ const hint = {
4923
+ cwd: session.cwd,
4924
+ pid: session.pid,
4925
+ sessionId: session.sessionId
4926
+ };
4927
+ const resolved = deps.resolve(hint);
4928
+ if (!resolved)
4929
+ return;
4930
+ let alive = this.isPidAlive(session.pid);
4931
+ if (!alive && session.isRestorable === true && session.agentLifecycle === "idle") {
4932
+ alive = true;
4933
+ }
4934
+ const snap = {
4935
+ taskId: resolved.id,
4936
+ state: parseLifecycleState(session.agentLifecycle),
4937
+ alive,
4938
+ // "agent": the store carries the agent's own reported lifecycle state,
4939
+ // not a scan inference. needsInput from the store is authoritative.
4940
+ origin: "agent",
4941
+ at: Math.floor((session.updatedAt ?? 0) * 1e3),
4942
+ pid: session.pid,
4943
+ ...session.lastBody ? { detail: { note: session.lastBody } } : {}
4944
+ };
4945
+ this.cache.set(resolved.id, snap);
4946
+ deps.report(snap);
4947
+ }
4948
+ };
4949
+ function parseLifecycleState(s) {
4950
+ if (s === "running" || s === "idle" || s === "needsInput" || s === "unknown") {
4951
+ return s;
4952
+ }
4953
+ return "unknown";
4954
+ }
4955
+ function defaultIsPidAlive2(pid) {
4956
+ try {
4957
+ process.kill(pid, 0);
4958
+ return true;
4959
+ } catch {
4960
+ return false;
4961
+ }
4962
+ }
4963
+ function defaultListFiles(dir) {
4964
+ try {
4965
+ return readdirSync3(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
4966
+ } catch {
4967
+ return [];
4968
+ }
4969
+ }
4970
+ function defaultReadFile(path17) {
4971
+ try {
4972
+ return readFileSync9(path17, "utf-8");
4973
+ } catch {
4974
+ return void 0;
4975
+ }
4976
+ }
4977
+ function defaultWatchDir(dir, cb) {
4978
+ const w = watch(dir, (_event, filename) => {
4979
+ if (typeof filename === "string" && filename.endsWith("-hook-sessions.json")) {
4980
+ cb();
4981
+ }
4982
+ });
4983
+ return () => w.close();
4984
+ }
4985
+
4986
+ // packages/workspaces/dist/native-hooks/native-hook-source.js
4987
+ import { join as join16 } from "path";
4988
+ import { homedir as homedir11 } from "os";
4989
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
4990
+ var CLAUDE_HOOK_EVENTS = [
4991
+ ["SessionStart", "session-start"],
4992
+ ["UserPromptSubmit", "prompt-submit"],
4993
+ ["PreToolUse", "pre-tool-use"],
4994
+ ["Stop", "stop"],
4995
+ ["Notification", "notification"],
4996
+ ["PreToolUse", "ask-question", "AskUserQuestion"],
4997
+ ["SessionEnd", "session-end"]
4998
+ ];
4999
+ var DEFAULT_HOOK_CMD = "squadrant hooks";
5000
+ function installClaudeHooks(opts = {}) {
5001
+ const settingsPath = opts.settingsPath ?? join16(homedir11(), ".claude", "settings.json");
5002
+ const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
5003
+ const readFile6 = opts.readFile ?? defaultReadFile2;
5004
+ const writeFile5 = opts.writeFile ?? defaultWriteFile;
5005
+ const log = opts.log ?? (() => {
5006
+ });
5007
+ let settings = {};
5008
+ const raw = readFile6(settingsPath);
5009
+ if (raw) {
5010
+ try {
5011
+ settings = JSON.parse(raw);
5012
+ } catch {
5013
+ log(`native-hook: failed to parse ${settingsPath} \u2014 hooks section will be reset`);
5014
+ }
5015
+ }
5016
+ if (typeof settings.hooks !== "object" || settings.hooks === null || Array.isArray(settings.hooks)) {
5017
+ settings.hooks = {};
5018
+ }
5019
+ const hooks = settings.hooks;
5020
+ let changed = false;
5021
+ for (const [eventName, sub, matcher] of CLAUDE_HOOK_EVENTS) {
5022
+ if (!Array.isArray(hooks[eventName])) {
5023
+ hooks[eventName] = [];
5024
+ }
5025
+ const entries = hooks[eventName];
5026
+ const command = `${hookCmd} claude ${sub}`;
5027
+ const hookMatcher = matcher ?? "";
5028
+ const alreadyPresent = entries.some((m) => Array.isArray(m.hooks) && m.hooks.some((h) => typeof h.command === "string" && h.command === command));
5029
+ if (!alreadyPresent) {
5030
+ entries.push({ matcher: hookMatcher, hooks: [{ type: "command", command, timeout: 10 }] });
5031
+ changed = true;
5032
+ }
5033
+ }
5034
+ if (changed) {
5035
+ writeFile5(settingsPath, JSON.stringify(settings, null, 2));
5036
+ }
5037
+ return settingsPath;
5038
+ }
5039
+ function mapSubToLifecycle(sub) {
5040
+ switch (sub) {
5041
+ case "session-start":
5042
+ return "running";
5043
+ case "prompt-submit":
5044
+ return "running";
5045
+ case "pre-tool-use":
5046
+ return "running";
5047
+ case "stop":
5048
+ return "idle";
5049
+ case "notification":
5050
+ return "needsInput";
5051
+ case "ask-question":
5052
+ return "needsInput";
5053
+ case "session-end":
5054
+ return "session-end";
5055
+ default:
5056
+ return null;
5057
+ }
5058
+ }
5059
+ var NativeHookSource = class {
5060
+ name = "native-hook";
5061
+ hookInstall;
5062
+ log;
5063
+ deps;
5064
+ /** taskId → last-reported snapshot, for snapshot() liveness floor. */
5065
+ cache = /* @__PURE__ */ new Map();
5066
+ constructor(opts = {}) {
5067
+ this.hookInstall = opts.hookInstall ?? {};
5068
+ this.log = opts.log ?? (() => {
5069
+ });
5070
+ }
5071
+ start(deps) {
5072
+ this.deps = deps;
5073
+ }
5074
+ stop() {
5075
+ this.deps = void 0;
5076
+ this.cache.clear();
5077
+ }
5078
+ /** Returns the last-reported snapshot for a known crew (liveness floor poll). */
5079
+ snapshot(taskId) {
5080
+ return this.cache.get(taskId);
5081
+ }
5082
+ /**
5083
+ * Install squadrant-owned hooks into ~/.claude/settings.json.
5084
+ * Idempotent — safe to call on every project init or crew spawn.
5085
+ * Returns the path to the settings file.
5086
+ */
5087
+ install() {
5088
+ return installClaudeHooks(this.hookInstall);
5089
+ }
5090
+ /**
5091
+ * Receive a lifecycle hook event from the daemon and report a LifecycleSnapshot.
5092
+ *
5093
+ * The daemon's 'squadrant hooks claude <sub>' CLI subcommand calls this after
5094
+ * reading SQUADRANT_CREW_TASK_ID from the hook's process environment — the only
5095
+ * collision-proof correlation key (blueprint §2.2 priority 1).
5096
+ *
5097
+ * @param sub Sub-event alias: "session-start" | "prompt-submit" | "stop" | …
5098
+ * @param taskId SQUADRANT_CREW_TASK_ID extracted from the hook process env.
5099
+ * @param pid Optional: OS pid from the hook's process env or argv.
5100
+ * @param payload Optional: parsed JSON payload from hook stdin (best-effort detail).
5101
+ */
5102
+ handleHook(sub, taskId, pid, payload) {
5103
+ if (!this.deps)
5104
+ return;
5105
+ const mapped = mapSubToLifecycle(sub);
5106
+ if (mapped === null) {
5107
+ this.log(`native-hook: unknown sub '${sub}' for task ${taskId} \u2014 ignored`);
5108
+ return;
5109
+ }
5110
+ const isSessionEnd = mapped === "session-end";
5111
+ const state = isSessionEnd ? "unknown" : mapped;
5112
+ const detail = extractDetail(sub, payload);
5113
+ const snap = {
5114
+ taskId,
5115
+ state,
5116
+ alive: !isSessionEnd,
5117
+ origin: "agent",
5118
+ at: Date.now(),
5119
+ ...pid !== void 0 ? { pid } : {},
5120
+ ...detail ? { detail } : {}
5121
+ };
5122
+ this.cache.set(taskId, snap);
5123
+ this.deps.report(snap);
5124
+ }
5125
+ };
5126
+ function extractDetail(sub, payload) {
5127
+ if (!payload || typeof payload !== "object")
5128
+ return void 0;
5129
+ const p = payload;
5130
+ if (sub === "notification") {
5131
+ const note = typeof p.message === "string" ? p.message : void 0;
5132
+ return note ? { note } : void 0;
5133
+ }
5134
+ if (sub === "pre-tool-use") {
5135
+ const tool = typeof p.tool_name === "string" ? p.tool_name : void 0;
5136
+ return tool ? { tool } : void 0;
5137
+ }
5138
+ return void 0;
5139
+ }
5140
+ function defaultReadFile2(path17) {
5141
+ try {
5142
+ return readFileSync10(path17, "utf-8");
5143
+ } catch {
5144
+ return void 0;
5145
+ }
5146
+ }
5147
+ function defaultWriteFile(path17, content) {
5148
+ mkdirSync6(path17.replace(/\/[^/]+$/, ""), { recursive: true });
5149
+ writeFileSync7(path17, content, "utf-8");
5150
+ }
5151
+
4712
5152
  // packages/workspaces/dist/crew-pane.js
4713
5153
  import net from "net";
4714
5154
 
4715
5155
  // packages/cli/src/squadrantd.ts
4716
5156
  var SELF_PATH2 = fileURLToPath3(import.meta.url);
4717
- var CLI_BIN = join15(dirname5(SELF_PATH2), "index.js");
4718
- var DAEMON_SOCK = join15(homedir10(), ".config", "squadrant", "squadrant.sock");
5157
+ var CLI_BIN = join17(dirname5(SELF_PATH2), "index.js");
5158
+ var DAEMON_SOCK = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
4719
5159
  function readPkgVersion() {
4720
5160
  try {
4721
- const pkgPath = join15(dirname5(SELF_PATH2), "..", "package.json");
4722
- return JSON.parse(readFileSync9(pkgPath, "utf-8")).version ?? "unknown";
5161
+ const pkgPath = join17(dirname5(SELF_PATH2), "..", "package.json");
5162
+ return JSON.parse(readFileSync11(pkgPath, "utf-8")).version ?? "unknown";
4723
5163
  } catch {
4724
5164
  return "unknown";
4725
5165
  }
@@ -4757,6 +5197,7 @@ function startSquadrantd(opts = {}) {
4757
5197
  ctx.broadcast = broadcast;
4758
5198
  ctx.schedulePromotion = schedulePromotion;
4759
5199
  ctx.cancelPromotionsFor = cancelPromotionsFor;
5200
+ const codexAppServerSource = new CodexAppServerSource();
4760
5201
  const codexDriver = opts.codexDriver ?? new CodexInteractiveDriver({
4761
5202
  emit: (ev) => {
4762
5203
  const found = ctx.store.listAll().find((r) => r.id === ev.id);
@@ -4776,6 +5217,7 @@ function startSquadrantd(opts = {}) {
4776
5217
  ctx.schedulePromotion(ev.id, ev.requestId, "approval", ev.question);
4777
5218
  } else if (ev.type === "task.reattached")
4778
5219
  ctx.broadcast(ev.id, { type: "reattached", taskId: ev.id });
5220
+ codexAppServerSource.observe(ev);
4779
5221
  }
4780
5222
  });
4781
5223
  const opencodeBridge = opts.opencodeBridge ?? new OpencodeSseBridge({
@@ -4800,9 +5242,11 @@ function startSquadrantd(opts = {}) {
4800
5242
  (r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && r.cwd === hook.cwd
4801
5243
  );
4802
5244
  },
4803
- cursorFile: join15(stateRoot, "cmux-events.seq"),
5245
+ cursorFile: join17(stateRoot, "cmux-events.seq"),
4804
5246
  log
4805
5247
  });
5248
+ const cmuxStoreSource = new CmuxStoreSource({ log });
5249
+ const nativeHookSource = new NativeHookSource({ log });
4806
5250
  ctx.codexDriver = codexDriver;
4807
5251
  ctx.opencodeBridge = opencodeBridge;
4808
5252
  ctx.cmuxEventsBridge = cmuxEventsBridge;
@@ -4830,7 +5274,135 @@ function startSquadrantd(opts = {}) {
4830
5274
  activeHeadlessKills.delete(handle.kill);
4831
5275
  }
4832
5276
  });
4833
- return startDaemon(ctx, { ...opts, launchHeadless }, PKG_VERSION);
5277
+ const h = startDaemon(ctx, { ...opts, launchHeadless }, PKG_VERSION);
5278
+ if (!process.env.VITEST) {
5279
+ const prevSnaps = /* @__PURE__ */ new Map();
5280
+ const storeDeps = {
5281
+ resolve: (hint) => {
5282
+ if (!hint.cwd && hint.pid == null) return void 0;
5283
+ return store.listAll().find(
5284
+ (r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && (r.cwd === hint.cwd || hint.pid != null && r.pid === hint.pid)
5285
+ );
5286
+ },
5287
+ report: (snap) => {
5288
+ const found = store.listAll().find((r) => r.id === snap.taskId);
5289
+ if (!found) return;
5290
+ const prev = prevSnaps.get(snap.taskId);
5291
+ const newState = reduceLifecycle(prev, snap);
5292
+ const changed = !prev || newState !== prev.state;
5293
+ prevSnaps.set(snap.taskId, snap);
5294
+ if (!snap.alive) {
5295
+ void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.session.ended", id: snap.taskId } });
5296
+ return;
5297
+ }
5298
+ if (!changed) return;
5299
+ if (newState === "idle") {
5300
+ void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.turn.completed", id: snap.taskId, turnId: "cmux-store" } });
5301
+ } else if (newState === "running") {
5302
+ void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.progress", id: snap.taskId } });
5303
+ } else if (newState === "needsInput") {
5304
+ const question = snap.detail?.note ?? snap.detail?.reason ?? "crew awaiting input";
5305
+ void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.blocked", id: snap.taskId, reason: "needsInput", question } });
5306
+ }
5307
+ },
5308
+ log
5309
+ };
5310
+ try {
5311
+ cmuxStoreSource.start(storeDeps);
5312
+ } catch (e) {
5313
+ log(`cmux store source start failed: ${e.message}`);
5314
+ }
5315
+ try {
5316
+ nativeHookSource.install();
5317
+ } catch (e) {
5318
+ log(`native hook install failed: ${e.message}`);
5319
+ }
5320
+ const hookPrevSnaps = /* @__PURE__ */ new Map();
5321
+ const hookDeps = {
5322
+ resolve: (hint) => {
5323
+ if (!hint.cwd && hint.pid == null) return void 0;
5324
+ return store.listAll().find(
5325
+ (r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && (r.cwd === hint.cwd || hint.pid != null && r.pid === hint.pid)
5326
+ );
5327
+ },
5328
+ report: (snap) => {
5329
+ const found = store.listAll().find((r) => r.id === snap.taskId);
5330
+ if (!found) return;
5331
+ const prev = hookPrevSnaps.get(snap.taskId);
5332
+ const newState = reduceLifecycle(prev, snap);
5333
+ const changed = !prev || newState !== prev.state;
5334
+ hookPrevSnaps.set(snap.taskId, snap);
5335
+ if (!snap.alive) {
5336
+ void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.session.ended", id: snap.taskId } });
5337
+ return;
5338
+ }
5339
+ if (!changed) return;
5340
+ if (newState === "idle") {
5341
+ void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.turn.completed", id: snap.taskId, turnId: "native-hook" } });
5342
+ } else if (newState === "running") {
5343
+ void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.progress", id: snap.taskId } });
5344
+ } else if (newState === "needsInput") {
5345
+ const question = snap.detail?.note ?? snap.detail?.reason ?? "crew awaiting input";
5346
+ void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.blocked", id: snap.taskId, reason: "needsInput", question } });
5347
+ }
5348
+ },
5349
+ log
5350
+ };
5351
+ try {
5352
+ nativeHookSource.start(hookDeps);
5353
+ } catch (e) {
5354
+ log(`native hook source start failed: ${e.message}`);
5355
+ }
5356
+ const codexPrevSnaps = /* @__PURE__ */ new Map();
5357
+ const codexSourceDeps = {
5358
+ resolve: () => void 0,
5359
+ // taskId comes from ControlEvent.id; resolve() unused
5360
+ report: (snap) => {
5361
+ const found = store.listAll().find((r) => r.id === snap.taskId);
5362
+ if (!found) return;
5363
+ const prev = codexPrevSnaps.get(snap.taskId);
5364
+ const newState = reduceLifecycle(prev, snap);
5365
+ const changed = !prev || newState !== prev.state;
5366
+ codexPrevSnaps.set(snap.taskId, snap);
5367
+ if (!snap.alive) {
5368
+ void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.session.ended", id: snap.taskId } });
5369
+ return;
5370
+ }
5371
+ if (!changed) return;
5372
+ if (newState === "idle") {
5373
+ void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.turn.completed", id: snap.taskId, turnId: "codex-appserver" } });
5374
+ } else if (newState === "running") {
5375
+ void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.progress", id: snap.taskId } });
5376
+ } else if (newState === "needsInput") {
5377
+ const question = snap.detail?.note ?? snap.detail?.reason ?? "crew awaiting input";
5378
+ void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.blocked", id: snap.taskId, reason: "needsInput", question } });
5379
+ }
5380
+ },
5381
+ log
5382
+ };
5383
+ try {
5384
+ codexAppServerSource.start(codexSourceDeps);
5385
+ } catch (e) {
5386
+ log(`codex app-server source start failed: ${e.message}`);
5387
+ }
5388
+ }
5389
+ const origStop = h.stop.bind(h);
5390
+ h.stop = async () => {
5391
+ try {
5392
+ cmuxStoreSource.stop();
5393
+ } catch {
5394
+ }
5395
+ try {
5396
+ nativeHookSource.stop();
5397
+ } catch {
5398
+ }
5399
+ try {
5400
+ codexAppServerSource.stop();
5401
+ } catch {
5402
+ }
5403
+ return origStop();
5404
+ };
5405
+ return h;
4834
5406
  }
4835
5407
  if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {
4836
5408
  void (async () => {
@@ -4839,7 +5411,7 @@ if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {
4839
5411
  process.stdout.write("squadrantd: launchd-managed daemon entry (no CLI args). Use `squadrant` for commands.\n");
4840
5412
  process.exit(0);
4841
5413
  }
4842
- const sock = join15(homedir10(), ".config", "squadrant", "squadrant.sock");
5414
+ const sock = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
4843
5415
  if (await isDaemonSocketLive(sock)) {
4844
5416
  process.stderr.write(`[squadrantd] refusing to start: a live daemon already owns ${sock}
4845
5417
  `);