squadrant 0.12.0 → 0.13.0

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";
@@ -89,14 +89,14 @@ function getDefaultConfig() {
89
89
  models: {
90
90
  command: "opus",
91
91
  captain: "opus",
92
- crew: "opus",
92
+ crew: "sonnet",
93
93
  exploration: "haiku",
94
94
  review: "opus"
95
95
  },
96
96
  roles: {
97
97
  command: { agent: "claude", model: "opus" },
98
98
  captain: { agent: "claude", model: "opus" },
99
- crew: { agent: "claude", model: "opus" },
99
+ crew: { agent: "claude", model: "sonnet" },
100
100
  exploration: { agent: "claude", model: "haiku" },
101
101
  side: { agent: "claude", model: "opus" }
102
102
  },
@@ -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";
@@ -4394,8 +4505,14 @@ function createCmuxDriver() {
4394
4505
  }
4395
4506
  },
4396
4507
  async sendToPane(pane, message) {
4397
- await cmux(["send", "--workspace", pane.workspaceId, "--surface", pane.surfaceId, sanitizeForCmuxSend(message)]);
4398
- await cmux(["send-key", "--workspace", pane.workspaceId, "--surface", pane.surfaceId, "Enter"]);
4508
+ await this.pasteToPane(pane, message);
4509
+ await this.sendKeyToPane(pane, "Enter");
4510
+ },
4511
+ async pasteToPane(pane, text) {
4512
+ await cmux(["send", "--workspace", pane.workspaceId, "--surface", pane.surfaceId, sanitizeForCmuxSend(text)]);
4513
+ },
4514
+ async sendKeyToPane(pane, key) {
4515
+ await cmux(["send-key", "--workspace", pane.workspaceId, "--surface", pane.surfaceId, key]);
4399
4516
  },
4400
4517
  async readPaneScreen(pane) {
4401
4518
  try {
@@ -4703,17 +4820,346 @@ var DaemonCmux = class {
4703
4820
  }
4704
4821
  };
4705
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
+
4706
5152
  // packages/workspaces/dist/crew-pane.js
4707
5153
  import net from "net";
4708
5154
 
4709
5155
  // packages/cli/src/squadrantd.ts
4710
5156
  var SELF_PATH2 = fileURLToPath3(import.meta.url);
4711
- var CLI_BIN = join15(dirname5(SELF_PATH2), "index.js");
4712
- 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");
4713
5159
  function readPkgVersion() {
4714
5160
  try {
4715
- const pkgPath = join15(dirname5(SELF_PATH2), "..", "package.json");
4716
- 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";
4717
5163
  } catch {
4718
5164
  return "unknown";
4719
5165
  }
@@ -4751,6 +5197,7 @@ function startSquadrantd(opts = {}) {
4751
5197
  ctx.broadcast = broadcast;
4752
5198
  ctx.schedulePromotion = schedulePromotion;
4753
5199
  ctx.cancelPromotionsFor = cancelPromotionsFor;
5200
+ const codexAppServerSource = new CodexAppServerSource();
4754
5201
  const codexDriver = opts.codexDriver ?? new CodexInteractiveDriver({
4755
5202
  emit: (ev) => {
4756
5203
  const found = ctx.store.listAll().find((r) => r.id === ev.id);
@@ -4770,6 +5217,7 @@ function startSquadrantd(opts = {}) {
4770
5217
  ctx.schedulePromotion(ev.id, ev.requestId, "approval", ev.question);
4771
5218
  } else if (ev.type === "task.reattached")
4772
5219
  ctx.broadcast(ev.id, { type: "reattached", taskId: ev.id });
5220
+ codexAppServerSource.observe(ev);
4773
5221
  }
4774
5222
  });
4775
5223
  const opencodeBridge = opts.opencodeBridge ?? new OpencodeSseBridge({
@@ -4794,9 +5242,11 @@ function startSquadrantd(opts = {}) {
4794
5242
  (r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && r.cwd === hook.cwd
4795
5243
  );
4796
5244
  },
4797
- cursorFile: join15(stateRoot, "cmux-events.seq"),
5245
+ cursorFile: join17(stateRoot, "cmux-events.seq"),
4798
5246
  log
4799
5247
  });
5248
+ const cmuxStoreSource = new CmuxStoreSource({ log });
5249
+ const nativeHookSource = new NativeHookSource({ log });
4800
5250
  ctx.codexDriver = codexDriver;
4801
5251
  ctx.opencodeBridge = opencodeBridge;
4802
5252
  ctx.cmuxEventsBridge = cmuxEventsBridge;
@@ -4824,7 +5274,135 @@ function startSquadrantd(opts = {}) {
4824
5274
  activeHeadlessKills.delete(handle.kill);
4825
5275
  }
4826
5276
  });
4827
- 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;
4828
5406
  }
4829
5407
  if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {
4830
5408
  void (async () => {
@@ -4833,7 +5411,7 @@ if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {
4833
5411
  process.stdout.write("squadrantd: launchd-managed daemon entry (no CLI args). Use `squadrant` for commands.\n");
4834
5412
  process.exit(0);
4835
5413
  }
4836
- const sock = join15(homedir10(), ".config", "squadrant", "squadrant.sock");
5414
+ const sock = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
4837
5415
  if (await isDaemonSocketLive(sock)) {
4838
5416
  process.stderr.write(`[squadrantd] refusing to start: a live daemon already owns ${sock}
4839
5417
  `);