squadrant 0.16.4 → 0.16.6

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.
@@ -490,6 +490,9 @@ function stampAttempt(rec, patch, now) {
490
490
  attempts.push(last);
491
491
  return { ...rec, attempts };
492
492
  }
493
+ function isStickyAttention(state) {
494
+ return state === "blocked" || state === "review";
495
+ }
493
496
  function nextPendingTool(current, ev, now) {
494
497
  if (ev.note === "agent.hook.PreToolUse")
495
498
  return { name: ev.tool ?? "tool", since: now };
@@ -497,6 +500,11 @@ function nextPendingTool(current, ev, now) {
497
500
  return void 0;
498
501
  return current;
499
502
  }
503
+ function nextPendingMonitor(current, ev, now) {
504
+ if (ev.note === "agent.hook.PreToolUse" && ev.tool === "Monitor")
505
+ return { since: now };
506
+ return current;
507
+ }
500
508
  function reduce(rec, ev, now) {
501
509
  if (ev.type === "task.reopened") {
502
510
  return { ...rec, state: "working", question: void 0, error: void 0, lastHeartbeat: now, lastEvent: ev.type };
@@ -513,20 +521,23 @@ function reduce(rec, ev, now) {
513
521
  sessionId: ev.sessionId ?? rec.sessionId,
514
522
  question: void 0,
515
523
  // resuming after a blocked→reply clears the question
516
- pendingTool: void 0
524
+ pendingTool: void 0,
517
525
  // #354: a new turn closes any prior tool window
526
+ pendingMonitor: void 0
527
+ // #594a: same reset — a new turn moots any prior watch
518
528
  };
519
529
  case "task.progress": {
520
530
  const pendingTool = nextPendingTool(rec.pendingTool, ev, now);
521
- if (rec.state === "blocked")
522
- return { ...rec, lastHeartbeat: now, lastEvent: ev.type, pendingTool };
523
- const b = { ...base, pendingTool };
531
+ const pendingMonitor = nextPendingMonitor(rec.pendingMonitor, ev, now);
532
+ if (isStickyAttention(rec.state))
533
+ return { ...rec, lastHeartbeat: now, lastEvent: ev.type, pendingTool, pendingMonitor };
534
+ const b = { ...base, pendingTool, pendingMonitor };
524
535
  if (rec.state === "awaiting-input" || rec.state === "stalled")
525
536
  return { ...stampAttempt(b, {}, now), state: "working" };
526
537
  return stampAttempt(b, {}, now);
527
538
  }
528
539
  case "heartbeat":
529
- if (rec.state === "blocked")
540
+ if (isStickyAttention(rec.state))
530
541
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
531
542
  if (rec.state === "awaiting-input")
532
543
  return { ...base, state: "working" };
@@ -534,8 +545,13 @@ function reduce(rec, ev, now) {
534
545
  case "task.blocked":
535
546
  if (rec.state === "blocked")
536
547
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
537
- return { ...base, state: "blocked", question: ev.question, pendingTool: void 0 };
548
+ return { ...base, state: "blocked", question: ev.question, pendingTool: void 0, pendingMonitor: void 0 };
549
+ case "task.review":
550
+ return { ...base, state: "review", reviewNote: ev.message, pendingTool: void 0, pendingMonitor: void 0 };
538
551
  case "task.done":
552
+ if (rec.state === "review" && ev.source !== "approve") {
553
+ return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
554
+ }
539
555
  return { ...base, state: "done", resultRef: ev.resultRef, parseWarning: ev.parseWarning };
540
556
  case "task.failed":
541
557
  return { ...base, state: "failed", error: ev.error, exitCode: ev.exitCode };
@@ -546,19 +562,19 @@ function reduce(rec, ev, now) {
546
562
  case "task.session":
547
563
  return stampAttempt(base, { resumeRef: ev.resumeRef }, now);
548
564
  case "task.turn.started":
549
- return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0 };
565
+ return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0, pendingMonitor: void 0 };
550
566
  case "task.turn.completed":
551
- if (rec.state === "blocked")
567
+ if (isStickyAttention(rec.state))
552
568
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
553
- if (rec.pendingTool)
569
+ if (rec.pendingTool || rec.pendingMonitor)
554
570
  return stampAttempt(base, {}, now);
555
- return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0 };
571
+ return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0, pendingMonitor: void 0 };
556
572
  case "task.delta":
557
573
  return stampAttempt(base, {}, now);
558
574
  // heartbeat-only
559
575
  case "task.input.requested":
560
576
  case "task.approval.requested":
561
- return { ...stampAttempt(base, {}, now), state: "blocked", question: ev.question, pendingTool: void 0 };
577
+ return { ...stampAttempt(base, {}, now), state: "blocked", question: ev.question, pendingTool: void 0, pendingMonitor: void 0 };
562
578
  case "task.reattached":
563
579
  return stampAttempt(base, {}, now);
564
580
  case "task.first-turn.confirmed":
@@ -579,15 +595,22 @@ function reduce(rec, ev, now) {
579
595
 
580
596
  // packages/core/dist/watchdog.js
581
597
  var TOOL_STALL_BUDGET_MS = 10 * 60 * 1e3;
582
- function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS) {
598
+ var MONITOR_STALL_BUDGET_MS = 60 * 60 * 1e3;
599
+ function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS, monitorStallMs = MONITOR_STALL_BUDGET_MS) {
583
600
  if (rec.state !== "working")
584
601
  return null;
585
602
  if (rec.mode === "interactive") {
586
- if (!rec.pendingTool)
587
- return null;
588
- if (now - rec.pendingTool.since <= toolStallMs)
589
- return null;
590
- return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
603
+ if (rec.pendingTool) {
604
+ if (now - rec.pendingTool.since <= toolStallMs)
605
+ return null;
606
+ return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
607
+ }
608
+ if (rec.pendingMonitor) {
609
+ if (now - rec.pendingMonitor.since <= monitorStallMs)
610
+ return null;
611
+ return { ...rec, state: "stalled", lastEvent: "watchdog.monitor-stall" };
612
+ }
613
+ return null;
591
614
  }
592
615
  const liveness = rec.attempts.at(-1)?.lastHeartbeatAt ?? rec.lastHeartbeat;
593
616
  if (now - liveness <= rec.heartbeatBudgetMs)
@@ -597,7 +620,7 @@ function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS) {
597
620
  function recoverStall(rec, now) {
598
621
  if (rec.state !== "stalled")
599
622
  return null;
600
- return { ...rec, state: "working", lastHeartbeat: now, lastEvent: "watchdog.recover", pendingTool: void 0 };
623
+ return { ...rec, state: "working", lastHeartbeat: now, lastEvent: "watchdog.recover", pendingTool: void 0, pendingMonitor: void 0 };
601
624
  }
602
625
 
603
626
  // packages/core/dist/daemon/reduce.js
@@ -606,8 +629,8 @@ var DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS = 6e4;
606
629
  var DEFAULT_TASK_TIMEOUT_MS = 8 * 60 * 60 * 1e3;
607
630
  var TERMINAL_RECORD_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
608
631
  var TERMINAL_RECORD_KEEP_PER_PROJECT = 20;
609
- var ATTENTION_STATES = /* @__PURE__ */ new Set(["done", "blocked", "failed", "stalled", "awaiting-input"]);
610
- var REAPABLE_SURFACE_STATES = /* @__PURE__ */ new Set(["working", "stalled", "awaiting-input", "blocked"]);
632
+ var ATTENTION_STATES = /* @__PURE__ */ new Set(["done", "blocked", "review", "failed", "stalled", "awaiting-input"]);
633
+ var REAPABLE_SURFACE_STATES = /* @__PURE__ */ new Set(["working", "stalled", "awaiting-input", "blocked", "review"]);
611
634
  var IDLE_DEBOUNCE_MS = 12e3;
612
635
  function shortId(id) {
613
636
  return id.slice(0, 8);
@@ -629,6 +652,10 @@ function formatMessage(rec, event) {
629
652
  }
630
653
  case "blocked":
631
654
  return `CREW BLOCKED ${tag}: ${(rec.question ?? "(no question)").trim()}`;
655
+ case "review": {
656
+ const note = (rec.reviewNote ?? "").trim();
657
+ return `CREW REVIEW ${tag}: ${note || "ready for review"} \u2014 run 'squadrant diff ${rec.project} ${rec.name ?? rec.id}' then 'squadrant crew approve' or send feedback.`;
658
+ }
632
659
  case "failed":
633
660
  return `CREW FAILED ${tag}: ${(rec.error ?? "(no error)").trim()}`;
634
661
  case "stalled": {
@@ -699,6 +726,7 @@ var KNOWN_EVENT_TYPES = /* @__PURE__ */ new Set([
699
726
  "task.progress",
700
727
  "heartbeat",
701
728
  "task.blocked",
729
+ "task.review",
702
730
  "task.done",
703
731
  "task.failed",
704
732
  "task.session",
@@ -899,7 +927,7 @@ function createDaemon(deps) {
899
927
  store.delete(r.project, r.id);
900
928
  continue;
901
929
  }
902
- if (!TERMINAL_STATES.has(r.state)) {
930
+ if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state)) {
903
931
  const ceiling = deps.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
904
932
  if (t - r.createdAt > ceiling) {
905
933
  const prevState = r.state;
@@ -944,7 +972,7 @@ function createDaemon(deps) {
944
972
  const idle = evaluateStall(r, t);
945
973
  if (idle) {
946
974
  store.put(idle);
947
- const synthEvent = idle.pendingTool ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: idle.pendingTool.name, elapsedMs: t - idle.pendingTool.since } : { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs };
975
+ const synthEvent = idle.pendingTool ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: idle.pendingTool.name, elapsedMs: t - idle.pendingTool.since } : idle.pendingMonitor ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: "Monitor", elapsedMs: t - idle.pendingMonitor.since } : { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs };
948
976
  firePush(deps, r.project, r.state, idle, synthEvent, lastCaptainTurnAt.get(r.id));
949
977
  continue;
950
978
  }
@@ -2074,9 +2102,11 @@ function buildSurfaceProbe(ctx, probes, daemonCmux) {
2074
2102
  // packages/core/dist/delivery/defer-delivery.js
2075
2103
  var DeferDelivery = class extends Error {
2076
2104
  draft;
2077
- constructor(draft = null) {
2105
+ reason;
2106
+ constructor(draft = null, reason = "draft") {
2078
2107
  super("deferred: captain composing");
2079
2108
  this.draft = draft;
2109
+ this.reason = reason;
2080
2110
  this.name = "DeferDelivery";
2081
2111
  }
2082
2112
  };
@@ -2094,6 +2124,7 @@ var CaptainDelivery = class {
2094
2124
  deferCounts = /* @__PURE__ */ new Map();
2095
2125
  lastContent = /* @__PURE__ */ new Map();
2096
2126
  stableCounts = /* @__PURE__ */ new Map();
2127
+ lastReason = /* @__PURE__ */ new Map();
2097
2128
  constructor(opts) {
2098
2129
  this.opts = opts;
2099
2130
  }
@@ -2116,29 +2147,40 @@ var CaptainDelivery = class {
2116
2147
  this.deferCounts.delete(seq);
2117
2148
  this.stableCounts.delete(seq);
2118
2149
  this.lastContent.delete(seq);
2150
+ this.lastReason.delete(seq);
2119
2151
  return { delivered: true };
2120
2152
  } catch (e) {
2121
2153
  if (e instanceof DeferDelivery) {
2122
2154
  this.deferCounts.set(seq, deferCount + 1);
2123
2155
  const content = e.draft;
2156
+ let stableCount;
2124
2157
  if (content && content === this.lastContent.get(seq)) {
2125
- this.stableCounts.set(seq, (this.stableCounts.get(seq) ?? 0) + 1);
2158
+ stableCount = (this.stableCounts.get(seq) ?? 0) + 1;
2159
+ this.stableCounts.set(seq, stableCount);
2126
2160
  } else {
2161
+ stableCount = 0;
2127
2162
  this.stableCounts.set(seq, 0);
2128
2163
  }
2129
2164
  this.lastContent.set(seq, content);
2130
- return { deferred: true };
2165
+ const reason = e.reason !== "draft" ? e.reason : stableCount >= this.opts.stableProbePolls ? "stable" : "draft";
2166
+ this.lastReason.set(seq, reason);
2167
+ return { deferred: true, reason };
2131
2168
  }
2132
- return { deferred: true };
2169
+ this.lastReason.set(seq, "unknown");
2170
+ return { deferred: true, reason: "unknown" };
2133
2171
  }
2134
2172
  }
2135
2173
  /** Read-only. Never mutates — safe to poll from the snapshot assembler every tick. */
2136
2174
  stats() {
2137
2175
  let maxDeferCount = 0;
2138
- for (const c of this.deferCounts.values())
2139
- if (c > maxDeferCount)
2176
+ let reason;
2177
+ for (const [seq, c] of this.deferCounts) {
2178
+ if (c > maxDeferCount) {
2140
2179
  maxDeferCount = c;
2141
- return { maxDeferCount, stuck: maxDeferCount >= this.opts.maxDefers };
2180
+ reason = this.lastReason.get(seq);
2181
+ }
2182
+ }
2183
+ return { maxDeferCount, stuck: maxDeferCount >= this.opts.maxDefers, reason };
2142
2184
  }
2143
2185
  };
2144
2186
 
@@ -2238,6 +2280,10 @@ function createDelivery(ctx, daemonCmux) {
2238
2280
  const notifyFault = ctx.notifyFault ?? (() => {
2239
2281
  });
2240
2282
  const defaultNotify = async (args) => {
2283
+ const fresh = store.get(args.project, args.record.id);
2284
+ if (fresh && TERMINAL_STATES.has(fresh.state) && fresh.state !== args.record.state) {
2285
+ return;
2286
+ }
2241
2287
  try {
2242
2288
  await appendToMailbox({
2243
2289
  stateRoot,
@@ -2327,16 +2373,19 @@ function createDelivery(ctx, daemonCmux) {
2327
2373
  log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
2328
2374
  await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
2329
2375
  } else {
2330
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred`);
2376
+ const { maxDeferCount } = d.stats();
2377
+ if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
2378
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
2379
+ }
2331
2380
  break;
2332
2381
  }
2333
2382
  }
2334
2383
  const stuck = d.stats().stuck;
2335
2384
  if (stuck && !stuckNotified.has(project)) {
2336
2385
  stuckNotified.add(project);
2337
- const { maxDeferCount } = d.stats();
2338
- log(`delivery stuck project=${project} deferCount=${maxDeferCount}`);
2339
- const text = `\u26A0\uFE0F DELIVERY STUCK: an in-progress draft (or ghost text) in your input box has blocked pending notification(s) for ${maxDeferCount}+ retries. Your input is never touched \u2014 this keeps retrying safely and will deliver automatically once you submit or clear it.`;
2386
+ const { maxDeferCount, reason } = d.stats();
2387
+ log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
2388
+ const text = reason === "modal" ? `\u26A0\uFE0F DELIVERY STUCK: a modal question is open in your captain pane and has blocked pending notification(s) for ${maxDeferCount}+ retries. This keeps retrying safely and will deliver automatically once you answer or dismiss it.` : `\u26A0\uFE0F DELIVERY STUCK: an in-progress draft (or ghost text) in your input box has blocked pending notification(s) for ${maxDeferCount}+ retries. Your input is never touched \u2014 this keeps retrying safely and will deliver automatically once you submit or clear it.`;
2340
2389
  appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`delivery stuck alert failed project=${project}: ${e.message}`));
2341
2390
  Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
2342
2391
  telegramBridge?.pushRaw(project, text);
@@ -2841,8 +2890,12 @@ var REGISTRY = {
2841
2890
  build: (a) => a[0] ? ok("crews", ["crew", "list", a[0]]) : usage("crews", "usage: /crews <project>")
2842
2891
  },
2843
2892
  launch: {
2893
+ // --headless (#586, same reason as #520 on the boot-if-down path): runCommand
2894
+ // execs this argv from the daemon, which has no CMUX_WORKSPACE_ID and no
2895
+ // terminal — a plain `launch` would open the cmux GUI app and exit 0 before
2896
+ // the workspace is ever launched.
2844
2897
  usage: "/launch <project>",
2845
- build: (a) => a[0] ? ok("launch", ["launch", a[0]]) : usage("launch", "usage: /launch <project>")
2898
+ build: (a) => a[0] ? ok("launch", ["launch", a[0], "--headless"]) : usage("launch", "usage: /launch <project>")
2846
2899
  },
2847
2900
  effort: {
2848
2901
  usage: "/effort [max|balance|low]",
@@ -3027,6 +3080,9 @@ ${ev.message}` : "");
3027
3080
  case "task.blocked":
3028
3081
  return `\u{1F6A7} [${project}] CREW BLOCKED \xB7 ${ev.id}
3029
3082
  ${ev.question}`;
3083
+ case "task.review":
3084
+ return `\u{1F440} [${project}] CREW REVIEW \xB7 ${ev.id}` + (ev.message ? `
3085
+ ${ev.message}` : "");
3030
3086
  case "task.idle":
3031
3087
  return `\u{1F4A4} [${project}] CREW IDLE \xB7 ${ev.id}`;
3032
3088
  case "task.failed":
@@ -3223,6 +3279,7 @@ var DONE_ONLY = /* @__PURE__ */ new Set(["task.done", "task.failed"]);
3223
3279
  var ALERTS = /* @__PURE__ */ new Set([
3224
3280
  ...DONE_ONLY,
3225
3281
  "task.blocked",
3282
+ "task.review",
3226
3283
  "task.approval.requested",
3227
3284
  "task.input.requested",
3228
3285
  "task.timeout"
@@ -4658,6 +4715,18 @@ function cmux(args) {
4658
4715
  );
4659
4716
  });
4660
4717
  }
4718
+ function cmuxStdin(args, input) {
4719
+ return new Promise((resolve2, reject) => {
4720
+ const child = execFile2(resolveCmuxBin(), args, { encoding: "utf-8", timeout: CMUX_TIMEOUT, env: { ...process.env, CMUX_QUIET: "1" } }, (err, stdout) => {
4721
+ if (err) {
4722
+ reject(err.code === "ETIMEDOUT" ? new CmuxTimeoutError(args.join(" ")) : err);
4723
+ return;
4724
+ }
4725
+ resolve2(stdout.trim());
4726
+ });
4727
+ child.stdin.end(input);
4728
+ });
4729
+ }
4661
4730
  function parseList(output) {
4662
4731
  let parsed;
4663
4732
  try {
@@ -5016,9 +5085,9 @@ function createCmuxDriver() {
5016
5085
  }
5017
5086
  const draft = parseDraftFromScreen(screen);
5018
5087
  if (draft === null)
5019
- throw new DeferDelivery(null);
5088
+ throw new DeferDelivery(null, "no-box");
5020
5089
  if (hasModalOptionList(screen))
5021
- throw new DeferDelivery(null);
5090
+ throw new DeferDelivery(null, "modal");
5022
5091
  if (draft === "") {
5023
5092
  await deliver();
5024
5093
  return;
@@ -5059,6 +5128,37 @@ function createCmuxDriver() {
5059
5128
  }
5060
5129
  throw new DeferDelivery(draft);
5061
5130
  },
5131
+ async showDiff(opts) {
5132
+ const source = opts.source ?? "branch";
5133
+ const args = ["diff"];
5134
+ if (source === "staged") {
5135
+ args.push("--staged");
5136
+ } else if (source === "unstaged") {
5137
+ args.push("--unstaged");
5138
+ } else {
5139
+ args.push("--branch", "--base", opts.base);
5140
+ if (opts.lastTurn)
5141
+ args.push("--last-turn");
5142
+ }
5143
+ args.push("--cwd", opts.cwd, "--workspace", opts.workspaceId, "--layout", opts.layout ?? "split");
5144
+ if (opts.title)
5145
+ args.push("--title", opts.title);
5146
+ if (opts.focus === false)
5147
+ args.push("--no-focus");
5148
+ else
5149
+ args.push("--focus", "true");
5150
+ await cmux(args);
5151
+ },
5152
+ async showPatch(opts) {
5153
+ const args = ["diff", "-", "--workspace", opts.workspaceId, "--layout", opts.layout ?? "split"];
5154
+ if (opts.title)
5155
+ args.push("--title", opts.title);
5156
+ if (opts.focus === false)
5157
+ args.push("--no-focus");
5158
+ else
5159
+ args.push("--focus", "true");
5160
+ await cmuxStdin(args, opts.patch);
5161
+ },
5062
5162
  async listSurfaces(workspaceId) {
5063
5163
  let output;
5064
5164
  try {
@@ -5648,6 +5748,7 @@ function installClaudeHooks(opts = {}) {
5648
5748
  });
5649
5749
  let settings = {};
5650
5750
  const raw = readFile6(settingsPath);
5751
+ const hadExistingSettings = raw !== void 0;
5651
5752
  if (raw) {
5652
5753
  try {
5653
5754
  settings = JSON.parse(raw);
@@ -5660,6 +5761,7 @@ function installClaudeHooks(opts = {}) {
5660
5761
  }
5661
5762
  const hooks = settings.hooks;
5662
5763
  let changed = false;
5764
+ const repaired = [];
5663
5765
  for (const [eventName, sub, matcher] of CLAUDE_HOOK_EVENTS) {
5664
5766
  if (!Array.isArray(hooks[eventName])) {
5665
5767
  hooks[eventName] = [];
@@ -5671,6 +5773,26 @@ function installClaudeHooks(opts = {}) {
5671
5773
  if (!alreadyPresent) {
5672
5774
  entries.push({ matcher: hookMatcher, hooks: [{ type: "command", command, timeout: 10 }] });
5673
5775
  changed = true;
5776
+ repaired.push(`${eventName}/${sub}`);
5777
+ }
5778
+ }
5779
+ if (repaired.length > 0 && hadExistingSettings) {
5780
+ log(`native-hook: repaired ${repaired.length} missing squadrant hook(s) in ${settingsPath} [${repaired.join(", ")}] \u2014 WARNING: blocked-signalling or lifecycle tracking may have been broken until this run`);
5781
+ }
5782
+ if (opts.claudeEnv && Object.keys(opts.claudeEnv).length > 0) {
5783
+ if (typeof settings.env !== "object" || settings.env === null || Array.isArray(settings.env)) {
5784
+ settings.env = {};
5785
+ }
5786
+ const env = settings.env;
5787
+ for (const [key, value] of Object.entries(opts.claudeEnv)) {
5788
+ if (key in env) {
5789
+ if (env[key] !== value) {
5790
+ log(`native-hook: claudeEnv key '${key}' already set to '${String(env[key])}' in ${settingsPath} \u2014 not overwriting with '${value}'`);
5791
+ }
5792
+ continue;
5793
+ }
5794
+ env[key] = value;
5795
+ changed = true;
5674
5796
  }
5675
5797
  }
5676
5798
  if (changed) {
@@ -5707,9 +5829,9 @@ var NativeHookSource = class {
5707
5829
  cache = /* @__PURE__ */ new Map();
5708
5830
  active = false;
5709
5831
  constructor(opts = {}) {
5710
- this.hookInstall = opts.hookInstall ?? {};
5711
5832
  this.log = opts.log ?? (() => {
5712
5833
  });
5834
+ this.hookInstall = { log: this.log, ...opts.hookInstall };
5713
5835
  }
5714
5836
  start(deps) {
5715
5837
  this.deps = deps;
@@ -6021,7 +6143,7 @@ function startSquadrantd(opts = {}) {
6021
6143
  log
6022
6144
  });
6023
6145
  const cmuxStoreSource = new CmuxStoreSource({ log });
6024
- const nativeHookSource = new NativeHookSource({ log });
6146
+ const nativeHookSource = new NativeHookSource({ log, hookInstall: { claudeEnv: loadConfig().defaults.claudeEnv } });
6025
6147
  ctx.codexDriver = codexDriver;
6026
6148
  ctx.opencodeBridge = opencodeBridge;
6027
6149
  ctx.cmuxEventsBridge = cmuxEventsBridge;