squadrant 0.25.0 → 0.25.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.
@@ -114,13 +114,8 @@ function getDefaultConfig() {
114
114
  captain: "auto",
115
115
  crew: "auto"
116
116
  },
117
- models: {
118
- command: "opus",
119
- captain: "opus",
120
- crew: "sonnet",
121
- exploration: "haiku",
122
- review: "opus"
123
- },
117
+ // #874: no `models` here — it's deprecated (superseded by `roles`), so a
118
+ // fresh init wrote a key the drift check immediately flagged.
124
119
  roles: {
125
120
  command: { agent: "claude", model: "opus" },
126
121
  captain: { agent: "claude", model: "opus" },
@@ -2449,6 +2444,8 @@ function sanitizePathForPlist(path24) {
2449
2444
  continue;
2450
2445
  if (p.includes("/.claude/plugins/"))
2451
2446
  continue;
2447
+ if (p.includes("/.cmuxterm/cmux-cli-shims/"))
2448
+ continue;
2452
2449
  if (seen.has(p))
2453
2450
  continue;
2454
2451
  seen.add(p);
@@ -2476,18 +2473,7 @@ function resolveAgentBinDirs() {
2476
2473
  });
2477
2474
  }
2478
2475
  function buildDaemonPath(shellPath) {
2479
- const agentDirs = resolveAgentBinDirs();
2480
- const sanitized = sanitizePathForPlist(shellPath);
2481
- if (agentDirs.length === 0)
2482
- return sanitized;
2483
- const parts = [...agentDirs, ...sanitized.split(":")];
2484
- const seen = /* @__PURE__ */ new Set();
2485
- return parts.filter((p) => {
2486
- if (!p || seen.has(p))
2487
- return false;
2488
- seen.add(p);
2489
- return true;
2490
- }).join(":");
2476
+ return sanitizePathForPlist([...resolveAgentBinDirs(), shellPath].join(":"));
2491
2477
  }
2492
2478
  function renderPlist(nodeBin, daemonEntry, pathEnv = "") {
2493
2479
  const logPath2 = join8(homedir5(), ".config", "squadrant", "squadrantd.log");
@@ -9448,6 +9434,7 @@ init_dist2();
9448
9434
  init_dist2();
9449
9435
  init_dist2();
9450
9436
  init_dist2();
9437
+ init_dist2();
9451
9438
  import { join as join27, dirname as dirname6, resolve as resolve3 } from "path";
9452
9439
  import { homedir as homedir14 } from "os";
9453
9440
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -11739,7 +11726,8 @@ var RuntimeRegistry = class {
11739
11726
  };
11740
11727
 
11741
11728
  // packages/workspaces/dist/notifiers/cmux.js
11742
- import { execFile as execFileCb, execSync as execSync7 } from "child_process";
11729
+ init_dist();
11730
+ import { execFile as execFileCb, execFileSync as execFileSync7 } from "child_process";
11743
11731
  import { promisify as promisify2 } from "util";
11744
11732
  var execFile3 = promisify2(execFileCb);
11745
11733
  function createCmuxNotifier(_scope) {
@@ -11747,7 +11735,7 @@ function createCmuxNotifier(_scope) {
11747
11735
  name: "cmux",
11748
11736
  async probe() {
11749
11737
  try {
11750
- execSync7("squadrant runtime status --command", { encoding: "utf-8", stdio: "pipe" });
11738
+ execFileSync7(resolveCmuxBin(), ["capabilities"], { encoding: "utf-8", stdio: "pipe", timeout: CMUX_TIMEOUT });
11751
11739
  return { installed: true, reachable: true };
11752
11740
  } catch (err) {
11753
11741
  const code = err.code;
@@ -11957,9 +11945,9 @@ import { homedir as homedir11 } from "os";
11957
11945
  // packages/workspaces/dist/cmux-daemon/store-fingerprint.js
11958
11946
  init_dist();
11959
11947
  init_dist2();
11960
- import { execFileSync as execFileSync7 } from "child_process";
11948
+ import { execFileSync as execFileSync8 } from "child_process";
11961
11949
  function defaultExecPs(pid) {
11962
- return execFileSync7("ps", ["-o", "command=", "-p", String(pid)], {
11950
+ return execFileSync8("ps", ["-o", "command=", "-p", String(pid)], {
11963
11951
  encoding: "utf-8",
11964
11952
  stdio: ["ignore", "pipe", "ignore"]
11965
11953
  });
@@ -12129,6 +12117,7 @@ var CmuxStoreSource = class {
12129
12117
  debounceMs;
12130
12118
  lockRetryMs;
12131
12119
  maxLockRetries;
12120
+ rewatchMs;
12132
12121
  isPidAlive;
12133
12122
  listFiles;
12134
12123
  readFile;
@@ -12140,6 +12129,7 @@ var CmuxStoreSource = class {
12140
12129
  deps;
12141
12130
  stopWatcher;
12142
12131
  debounceTimer;
12132
+ rewatchTimer;
12143
12133
  /** filename → pending lock-retry timer (one per locked file). */
12144
12134
  lockRetryTimers = /* @__PURE__ */ new Map();
12145
12135
  /** filename → re-scan attempts used in the current lock episode. */
@@ -12153,6 +12143,7 @@ var CmuxStoreSource = class {
12153
12143
  this.debounceMs = opts.debounceMs ?? 50;
12154
12144
  this.lockRetryMs = opts.lockRetryMs ?? 50;
12155
12145
  this.maxLockRetries = opts.maxLockRetries ?? 3;
12146
+ this.rewatchMs = opts.rewatchMs ?? 5e3;
12156
12147
  this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
12157
12148
  this.listFiles = opts.listFiles ?? defaultListFiles;
12158
12149
  this.readFile = opts.readFile ?? defaultReadFile;
@@ -12168,18 +12159,17 @@ var CmuxStoreSource = class {
12168
12159
  this.active = true;
12169
12160
  this.lastError = null;
12170
12161
  this.scan();
12171
- try {
12172
- this.stopWatcher = this.watchDir(this.stateDir, () => this.scheduleDebounced());
12173
- } catch (e) {
12174
- this.lastError = e.message;
12175
- this.log(`cmux-store: failed to watch ${this.stateDir}: ${e.message}`);
12176
- }
12162
+ this.armWatcher();
12177
12163
  }
12178
12164
  stop() {
12179
12165
  if (this.debounceTimer !== void 0) {
12180
12166
  this.cancelTimer(this.debounceTimer);
12181
12167
  this.debounceTimer = void 0;
12182
12168
  }
12169
+ if (this.rewatchTimer !== void 0) {
12170
+ this.cancelTimer(this.rewatchTimer);
12171
+ this.rewatchTimer = void 0;
12172
+ }
12183
12173
  for (const timer of this.lockRetryTimers.values()) {
12184
12174
  this.cancelTimer(timer);
12185
12175
  }
@@ -12200,6 +12190,36 @@ var CmuxStoreSource = class {
12200
12190
  return { active: this.active, error: this.lastError };
12201
12191
  }
12202
12192
  // ── private ─────────────────────────────────────────────────────────────────
12193
+ /** Watch for subsequent changes, debounced. */
12194
+ armWatcher() {
12195
+ try {
12196
+ this.stopWatcher = this.watchDir(this.stateDir, () => this.scheduleDebounced(), (err) => this.onWatchError(err));
12197
+ } catch (e) {
12198
+ this.lastError = e.message;
12199
+ this.log(`cmux-store: failed to watch ${this.stateDir}: ${e.message}`);
12200
+ }
12201
+ }
12202
+ /**
12203
+ * The watcher failed after starting (macOS: a refused FSEventStreamStart()
12204
+ * surfaces as EMFILE). Drop it and re-arm later instead of letting the error
12205
+ * escape as an uncaughtException that kills the daemon.
12206
+ */
12207
+ onWatchError(err) {
12208
+ this.lastError = err.message;
12209
+ this.log(`cmux-store: watcher error on ${this.stateDir}: ${err.message} \u2014 re-arming in ${this.rewatchMs}ms`);
12210
+ this.stopWatcher?.();
12211
+ this.stopWatcher = void 0;
12212
+ if (!this.active || this.rewatchTimer !== void 0)
12213
+ return;
12214
+ this.rewatchTimer = this.scheduleTimer(() => {
12215
+ this.rewatchTimer = void 0;
12216
+ if (!this.active)
12217
+ return;
12218
+ this.lastError = null;
12219
+ this.scan();
12220
+ this.armWatcher();
12221
+ }, this.rewatchMs);
12222
+ }
12203
12223
  scheduleDebounced() {
12204
12224
  if (this.debounceTimer !== void 0) {
12205
12225
  this.cancelTimer(this.debounceTimer);
@@ -12322,12 +12342,13 @@ function defaultReadFile(path24) {
12322
12342
  return void 0;
12323
12343
  }
12324
12344
  }
12325
- function defaultWatchDir(dir, cb) {
12345
+ function defaultWatchDir(dir, cb, onError) {
12326
12346
  const w = watch(dir, (_event, filename) => {
12327
12347
  if (typeof filename === "string" && filename.endsWith("-hook-sessions.json")) {
12328
12348
  cb();
12329
12349
  }
12330
12350
  });
12351
+ w.on("error", onError);
12331
12352
  return () => w.close();
12332
12353
  }
12333
12354
 
@@ -12350,10 +12371,36 @@ var CLAUDE_HOOK_EVENTS = [
12350
12371
  // this at all; without it the watchdog reports a plain stall (wrong story).
12351
12372
  ["StopFailure", "stop-failure"]
12352
12373
  ];
12353
- var DEFAULT_HOOK_CMD = "squadrant hooks";
12354
- var GATE_HOOK_CMD = "squadrant gate";
12355
- function hookCommandFor(eventName, sub, hookCmd) {
12356
- return eventName === "PermissionRequest" ? `${GATE_HOOK_CMD} claude ${sub}` : `${hookCmd} claude ${sub}`;
12374
+ var DEFAULT_CLI_CMD = "squadrant";
12375
+ function hookCommandFor(eventName, sub, hookCmd, gateCmd) {
12376
+ return eventName === "PermissionRequest" ? `${gateCmd} claude ${sub}` : `${hookCmd} claude ${sub}`;
12377
+ }
12378
+ function isSquadrantOwnedCommand(command, sub) {
12379
+ if (typeof command !== "string")
12380
+ return false;
12381
+ const m = command.match(/^(.*) (?:hooks|gate) claude (\S+)$/);
12382
+ if (!m || m[2] !== sub)
12383
+ return false;
12384
+ const prefix = m[1];
12385
+ return prefix === DEFAULT_CLI_CMD || /squadrant\/dist\/index\.js'?$/.test(prefix);
12386
+ }
12387
+ function removeStaleOwnedHandlers(entries, sub, keep) {
12388
+ let removed = false;
12389
+ for (let i = entries.length - 1; i >= 0; i--) {
12390
+ const entry = entries[i];
12391
+ if (!Array.isArray(entry?.hooks))
12392
+ continue;
12393
+ const before = entry.hooks.length;
12394
+ entry.hooks = entry.hooks.filter((h) => {
12395
+ const cmd = h?.command;
12396
+ return cmd === keep || !isSquadrantOwnedCommand(cmd, sub);
12397
+ });
12398
+ if (entry.hooks.length !== before)
12399
+ removed = true;
12400
+ if (entry.hooks.length === 0)
12401
+ entries.splice(i, 1);
12402
+ }
12403
+ return removed;
12357
12404
  }
12358
12405
  function removeCommandHandlers(entries, command) {
12359
12406
  let removed = false;
@@ -12414,7 +12461,9 @@ function removeForeignGateHandlers(entries) {
12414
12461
  }
12415
12462
  function installClaudeHooks(opts = {}) {
12416
12463
  const settingsPath = opts.settingsPath ?? join25(homedir13(), ".claude", "settings.json");
12417
- const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
12464
+ const cliCmd = opts.cliCmd ?? DEFAULT_CLI_CMD;
12465
+ const hookCmd = opts.hookCmd ?? `${cliCmd} hooks`;
12466
+ const gateCmd = `${cliCmd} gate`;
12418
12467
  const readFile7 = opts.readFile ?? defaultReadFile2;
12419
12468
  const writeFile6 = opts.writeFile ?? defaultWriteFile;
12420
12469
  const log = opts.log ?? (() => {
@@ -12436,12 +12485,13 @@ function installClaudeHooks(opts = {}) {
12436
12485
  let changed = false;
12437
12486
  const repaired = [];
12438
12487
  const migrated = [];
12488
+ const repointed = [];
12439
12489
  for (const [eventName, sub, matcher] of CLAUDE_HOOK_EVENTS) {
12440
12490
  if (!Array.isArray(hooks[eventName])) {
12441
12491
  hooks[eventName] = [];
12442
12492
  }
12443
12493
  const entries = hooks[eventName];
12444
- const command = hookCommandFor(eventName, sub, hookCmd);
12494
+ const command = hookCommandFor(eventName, sub, hookCmd, gateCmd);
12445
12495
  const hookMatcher = matcher ?? "";
12446
12496
  if (eventName === "PermissionRequest") {
12447
12497
  const legacy = `${hookCmd} claude ${sub}`;
@@ -12467,11 +12517,14 @@ function installClaudeHooks(opts = {}) {
12467
12517
  }
12468
12518
  }
12469
12519
  }
12520
+ const wasRepointed = removeStaleOwnedHandlers(entries, sub, command);
12521
+ if (wasRepointed)
12522
+ changed = true;
12470
12523
  const alreadyPresent = entries.some((m) => Array.isArray(m.hooks) && m.hooks.some((h) => typeof h.command === "string" && h.command === command));
12471
12524
  if (!alreadyPresent) {
12472
12525
  entries.push({ matcher: hookMatcher, hooks: [{ type: "command", command, timeout: 10 }] });
12473
12526
  changed = true;
12474
- repaired.push(`${eventName}/${sub}`);
12527
+ (wasRepointed ? repointed : repaired).push(`${eventName}/${sub}`);
12475
12528
  }
12476
12529
  }
12477
12530
  if (repaired.length > 0 && hadExistingSettings) {
@@ -12480,6 +12533,9 @@ function installClaudeHooks(opts = {}) {
12480
12533
  if (migrated.length > 0) {
12481
12534
  log(`native-hook: migrated ${migrated.length} PermissionRequest hook(s) to the U7 permission gate in ${settingsPath} [${migrated.join(", ")}]`);
12482
12535
  }
12536
+ if (repointed.length > 0) {
12537
+ log(`native-hook: repointed ${repointed.length} squadrant hook(s) to ${cliCmd} in ${settingsPath} [${repointed.join(", ")}]`);
12538
+ }
12483
12539
  if (opts.claudeEnv && Object.keys(opts.claudeEnv).length > 0) {
12484
12540
  if (typeof settings.env !== "object" || settings.env === null || Array.isArray(settings.env)) {
12485
12541
  settings.env = {};
@@ -13129,6 +13185,9 @@ function startSquadrantd(opts = {}) {
13129
13185
  const nativeHookSource = new NativeHookSource({
13130
13186
  log,
13131
13187
  hookInstall: {
13188
+ // #872: absolute node + CLI paths — Claude runs hooks via /bin/sh with its
13189
+ // own PATH, which often lacks a version-managed Node's global bin.
13190
+ cliCmd: `${shellQuote(process.execPath)} ${shellQuote(CLI_BIN)}`,
13132
13191
  claudeEnv: loadConfig().defaults.claudeEnv,
13133
13192
  // #P6 (§15#5): only an explicit gate mode "on" lets squadrant remove a
13134
13193
  // foreign auto-gate PermissionRequest handler; auto/absent leaves it alone.