squadrant 0.22.0 → 0.24.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.
@@ -94,6 +94,9 @@ function isGateMode(v2) {
94
94
  function isGatePolicy(v2) {
95
95
  return GATE_POLICIES.includes(v2);
96
96
  }
97
+ function isGateEngine(v2) {
98
+ return GATE_ENGINES.includes(v2);
99
+ }
97
100
  function getDefaultConfig() {
98
101
  return {
99
102
  commandName: "\u{1F3DB}\uFE0F command",
@@ -177,12 +180,13 @@ function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
177
180
  function resolveHome(p) {
178
181
  return p.startsWith("~") ? p.replace("~", os.homedir()) : p;
179
182
  }
180
- var GATE_MODES, GATE_POLICIES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH, DEFAULT_CREW_ROUTING_RULES;
183
+ var GATE_MODES, GATE_POLICIES, GATE_ENGINES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH, DEFAULT_CREW_ROUTING_RULES;
181
184
  var init_config = __esm({
182
185
  "packages/shared/dist/config.js"() {
183
186
  init_config_io();
184
187
  GATE_MODES = ["on", "off", "auto"];
185
188
  GATE_POLICIES = ["deny-dangerous", "ask-on-doubt"];
189
+ GATE_ENGINES = ["router", "auto-gate"];
186
190
  DEFAULT_CONFIG_PATH = process.env.SQUADRANT_CONFIG || path2.join(os.homedir(), ".config", "squadrant", "config.json");
187
191
  CONFIG_DIR = path2.dirname(DEFAULT_CONFIG_PATH);
188
192
  DAEMON_SOCK_PATH = path2.join(CONFIG_DIR, "squadrant.sock");
@@ -1070,10 +1074,11 @@ function formatDelegationReport(rec, originProject, targetProject) {
1070
1074
  return null;
1071
1075
  }
1072
1076
  }
1073
- function firePush(deps, project, prev, next, event, lastCaptainTurnAt) {
1077
+ function firePush(deps, project, prev, next, event, lastCaptainTurnAt, prevRec) {
1074
1078
  if (!deps.notify)
1075
1079
  return;
1076
- if (prev === next.state)
1080
+ const questionChanged = prev === "blocked" && next.state === "blocked" && prevRec != null && (prevRec.question ?? "").trim() !== (next.question ?? "").trim();
1081
+ if (prev === next.state && !questionChanged)
1077
1082
  return;
1078
1083
  if (!ATTENTION_STATES.has(next.state))
1079
1084
  return;
@@ -1111,6 +1116,7 @@ function createDaemon(deps) {
1111
1116
  const { store, now } = deps;
1112
1117
  const lastCaptainTurnAt = /* @__PURE__ */ new Map();
1113
1118
  const quietNotifiedAt = /* @__PURE__ */ new Map();
1119
+ const blockedRenudgedFor = /* @__PURE__ */ new Map();
1114
1120
  const resendAttemptedAt = /* @__PURE__ */ new Map();
1115
1121
  const firstTurnUndeliveredBudgetMs = deps.firstTurnUndeliveredBudgetMs ?? DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS;
1116
1122
  const firstTurnResendCooldownMs = deps.firstTurnResendCooldownMs ?? DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS;
@@ -1131,7 +1137,7 @@ function createDaemon(deps) {
1131
1137
  const next = reduce(cur, event, now());
1132
1138
  if (next !== cur) {
1133
1139
  store.put(next);
1134
- firePush(deps, project, cur.state, next, event, lastCaptainTurnAt.get(next.id));
1140
+ firePush(deps, project, cur.state, next, event, lastCaptainTurnAt.get(next.id), cur);
1135
1141
  }
1136
1142
  return next;
1137
1143
  }
@@ -1283,6 +1289,8 @@ function createDaemon(deps) {
1283
1289
  }
1284
1290
  const recoveryPromises = [];
1285
1291
  for (const r of store.listAll()) {
1292
+ if (r.state !== "blocked")
1293
+ blockedRenudgedFor.delete(r.id);
1286
1294
  if (TERMINAL_STATES.has(r.state) && t - r.lastHeartbeat > TERMINAL_RECORD_TTL_MS) {
1287
1295
  store.delete(r.project, r.id);
1288
1296
  continue;
@@ -1345,6 +1353,27 @@ function createDaemon(deps) {
1345
1353
  continue;
1346
1354
  }
1347
1355
  }
1356
+ if (r.state === "blocked" && !r.operatorHold) {
1357
+ const threshold = deps.blockedRenudgeMs ?? DEFAULT_BLOCKED_RENUDGE_MS;
1358
+ const blockedFor = t - r.lastHeartbeat;
1359
+ const episodeKey = r.question ?? "";
1360
+ if (blockedFor > threshold && blockedRenudgedFor.get(r.id) !== episodeKey) {
1361
+ blockedRenudgedFor.set(r.id, episodeKey);
1362
+ if (deps.notify) {
1363
+ const tag = crewTag(r);
1364
+ const mins = Math.max(1, Math.round(blockedFor / 6e4));
1365
+ const message = `CREW BLOCKED REMINDER ${tag}: still blocked ~${mins}min (reminder \u2014 same question, not a new one): ${(r.question ?? "(no question)").trim()}`;
1366
+ const synthEvent = { type: "task.blocked", id: r.id, reason: "renudge", question: r.question ?? "" };
1367
+ try {
1368
+ const p = deps.notify({ project: r.project, message, record: r, event: synthEvent });
1369
+ if (p && typeof p.catch === "function")
1370
+ p.catch(() => {
1371
+ });
1372
+ } catch {
1373
+ }
1374
+ }
1375
+ }
1376
+ }
1348
1377
  if (r.mode === "interactive" && r.state === "submitted" && !r.firstTurnConfirmedAt) {
1349
1378
  const undeliveredMs = t - r.createdAt;
1350
1379
  if (undeliveredMs > firstTurnUndeliveredBudgetMs) {
@@ -1434,7 +1463,7 @@ function createDaemon(deps) {
1434
1463
  }
1435
1464
  };
1436
1465
  }
1437
- var DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS, DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS, DEFAULT_TASK_TIMEOUT_MS, TERMINAL_RECORD_TTL_MS, TERMINAL_RECORD_KEEP_PER_PROJECT, ATTENTION_STATES, REAPABLE_SURFACE_STATES, IDLE_DEBOUNCE_MS, KNOWN_EVENT_TYPES;
1466
+ var DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS, DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS, DEFAULT_BLOCKED_RENUDGE_MS, DEFAULT_TASK_TIMEOUT_MS, TERMINAL_RECORD_TTL_MS, TERMINAL_RECORD_KEEP_PER_PROJECT, ATTENTION_STATES, REAPABLE_SURFACE_STATES, IDLE_DEBOUNCE_MS, KNOWN_EVENT_TYPES;
1438
1467
  var init_reduce = __esm({
1439
1468
  "packages/core/dist/daemon/reduce.js"() {
1440
1469
  init_dist();
@@ -1442,6 +1471,7 @@ var init_reduce = __esm({
1442
1471
  init_watchdog();
1443
1472
  DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS = 12e4;
1444
1473
  DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS = 6e4;
1474
+ DEFAULT_BLOCKED_RENUDGE_MS = 30 * 60 * 1e3;
1445
1475
  DEFAULT_TASK_TIMEOUT_MS = 8 * 60 * 60 * 1e3;
1446
1476
  TERMINAL_RECORD_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
1447
1477
  TERMINAL_RECORD_KEEP_PER_PROJECT = 20;
@@ -3572,8 +3602,11 @@ function buildRouterEnv(creds, model) {
3572
3602
  if (headers)
3573
3603
  env.ANTHROPIC_CUSTOM_HEADERS = headers;
3574
3604
  }
3575
- if (model)
3605
+ if (model) {
3576
3606
  env.ANTHROPIC_MODEL = model;
3607
+ env.CLAUDE_CODE_SUBAGENT_MODEL = model;
3608
+ env.ANTHROPIC_SMALL_FAST_MODEL = model;
3609
+ }
3577
3610
  return env;
3578
3611
  }
3579
3612
  function mergeClaudeEnvRouterSettings(routerEnv, claudeEnv) {
@@ -4456,8 +4489,10 @@ async function resolveAndPersistOpencodeCaptain(opts) {
4456
4489
  sleep: opts.sleep,
4457
4490
  fetchImpl: opts.fetchImpl
4458
4491
  });
4459
- if (!sessionId)
4492
+ if (!sessionId) {
4493
+ opts.log?.(`opencode captain session resolution timed out for ${opts.project} (${opts.directory}) \u2014 captain.json has no session id yet; the daemon's own re-resolve fills it in once a session appears`);
4460
4494
  return null;
4495
+ }
4461
4496
  writeCaptainAddress(opts.stateRoot, opts.project, {
4462
4497
  agent: "opencode",
4463
4498
  port: opts.port,
@@ -4477,6 +4512,33 @@ var init_opencode_session = __esm({
4477
4512
  function discoverCaptainSurface(surfaces, captainTitle) {
4478
4513
  return surfaces.find((s) => s.title === captainTitle) ?? null;
4479
4514
  }
4515
+ async function reresolveOpencodeAddress(rec) {
4516
+ const live = discoverLiveOpencodeServer({ directory: rec.directory, sessionId: rec.sessionId });
4517
+ const port = live?.port ?? rec.port;
4518
+ if (port === void 0)
4519
+ return null;
4520
+ const newest = newestSessionInDirectory(await listSessions(port), rec.directory, Date.parse(rec.launchedAt));
4521
+ const sessionId = newest ?? live?.sessionId ?? rec.sessionId;
4522
+ if (!sessionId || port === rec.port && sessionId === rec.sessionId)
4523
+ return null;
4524
+ return { port, sessionId };
4525
+ }
4526
+ async function healStaleOpencodeAddress(stateRoot, project, log, logPrefix) {
4527
+ const rec = readCaptainAddress(stateRoot, project);
4528
+ if (!rec)
4529
+ return false;
4530
+ const resolved = await reresolveOpencodeAddress(rec);
4531
+ if (!resolved)
4532
+ return false;
4533
+ const current = readCaptainAddress(stateRoot, project);
4534
+ if (!current || current.launchedAt !== rec.launchedAt) {
4535
+ log(`captain-channel ${project}: skipped stale-address heal \u2014 a newer launch record won the race`);
4536
+ return false;
4537
+ }
4538
+ writeCaptainAddress(stateRoot, project, { ...current, port: resolved.port, sessionId: resolved.sessionId });
4539
+ log(`captain-channel ${project}: ${logPrefix} \u2192 port ${resolved.port}`);
4540
+ return true;
4541
+ }
4480
4542
  async function reapOrphanedCrews(store, project, isSurfaceAlive) {
4481
4543
  let reaped = 0;
4482
4544
  for (const r of store.list(project)) {
@@ -4701,27 +4763,7 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4701
4763
  log(`captain-channel ${project}: threw, falling back to pane \u2014 ${e.message}`);
4702
4764
  }
4703
4765
  if (agent === "opencode" && mode !== "off") {
4704
- const rec = readCaptainAddress(stateRoot, project);
4705
- let healed = false;
4706
- if (rec?.port) {
4707
- const fresh = newestSessionInDirectory(await listSessions(rec.port), rec.directory, Date.parse(rec.launchedAt));
4708
- if (fresh && fresh !== rec.sessionId) {
4709
- writeCaptainAddress(stateRoot, project, { ...rec, sessionId: fresh });
4710
- healed = true;
4711
- }
4712
- }
4713
- if (!healed && rec) {
4714
- const live = discoverLiveOpencodeServer({ directory: rec.directory, sessionId: rec.sessionId });
4715
- if (live && (live.port !== rec.port || live.sessionId && live.sessionId !== rec.sessionId)) {
4716
- writeCaptainAddress(stateRoot, project, {
4717
- ...rec,
4718
- port: live.port,
4719
- sessionId: live.sessionId ?? rec.sessionId
4720
- });
4721
- log(`captain-channel ${project}: re-resolved opencode address \u2192 port ${live.port}`);
4722
- healed = true;
4723
- }
4724
- }
4766
+ const healed = await healStaleOpencodeAddress(stateRoot, project, log, "re-resolved opencode address");
4725
4767
  if (healed) {
4726
4768
  try {
4727
4769
  const retry = await deliverToCaptain(project, text, { channel: picked, mode, log });
@@ -5579,11 +5621,15 @@ function parseCommand(text) {
5579
5621
  }
5580
5622
  return entry.build(args);
5581
5623
  }
5582
- var WRITABLE_CONFIG_KEYS, EFFORT_MODES, REGISTRY;
5624
+ var WRITABLE_CONFIG_KEYS, EFFORT_MODES, GATE_MODES2, CONFIG_VALUE_VALIDATORS, REGISTRY;
5583
5625
  var init_commands = __esm({
5584
5626
  "packages/core/dist/telegram/commands.js"() {
5585
- WRITABLE_CONFIG_KEYS = ["defaults.effort"];
5627
+ WRITABLE_CONFIG_KEYS = ["defaults.effort", "defaults.gate.mode"];
5586
5628
  EFFORT_MODES = /* @__PURE__ */ new Set(["max", "balance", "low"]);
5629
+ GATE_MODES2 = /* @__PURE__ */ new Set(["on", "off", "auto"]);
5630
+ CONFIG_VALUE_VALIDATORS = {
5631
+ "defaults.gate.mode": { valid: GATE_MODES2, usage: "usage: /config set defaults.gate.mode <on|off|auto>" }
5632
+ };
5587
5633
  REGISTRY = {
5588
5634
  status: { usage: "/status", build: () => ok("status", ["status"]) },
5589
5635
  projects: { usage: "/projects", build: () => ok("projects", ["projects", "list"]) },
@@ -5630,6 +5676,10 @@ var init_commands = __esm({
5630
5676
  message: `\u26D4 '${key}' is not writable over Telegram. Allowed: ${WRITABLE_CONFIG_KEYS.join(", ")}`
5631
5677
  };
5632
5678
  }
5679
+ const validator = CONFIG_VALUE_VALIDATORS[key];
5680
+ if (validator && !validator.valid.has(value)) {
5681
+ return usage("config", validator.usage);
5682
+ }
5633
5683
  return ok("config", ["config", "set", key, value]);
5634
5684
  }
5635
5685
  return usage("config", "usage: /config get <key> | /config set <key> <value>");
@@ -7736,6 +7786,12 @@ function resolveGateMode(env, gate) {
7736
7786
  return fromEnv;
7737
7787
  return gate?.mode && isGateMode(gate.mode) ? gate.mode : "auto";
7738
7788
  }
7789
+ function resolveGateEngine(env, gate) {
7790
+ const fromEnv = env.SQUADRANT_GATE_ENGINE;
7791
+ if (fromEnv !== void 0 && isGateEngine(fromEnv))
7792
+ return fromEnv;
7793
+ return gate?.engine && isGateEngine(gate.engine) ? gate.engine : "router";
7794
+ }
7739
7795
  function resolveGatePolicy(env, gate) {
7740
7796
  const fromEnv = env.SQUADRANT_GATE_POLICY;
7741
7797
  if (fromEnv !== void 0 && isGatePolicy(fromEnv))
@@ -8120,6 +8176,62 @@ var init_permission_gate = __esm({
8120
8176
  }
8121
8177
  });
8122
8178
 
8179
+ // packages/core/dist/auto-gate.js
8180
+ import { createAutoGate, projectGateConfig, resolveApiKey } from "@squadrant-ai/auto-gate";
8181
+ function gateForProjection(config) {
8182
+ const gate = config.defaults.gate;
8183
+ if (!gate)
8184
+ return {};
8185
+ const out = {};
8186
+ if (gate.mode)
8187
+ out.mode = gate.mode;
8188
+ if (gate.tools)
8189
+ out.tools = gate.tools;
8190
+ if (gate.deny)
8191
+ out.deny = gate.deny;
8192
+ return out;
8193
+ }
8194
+ function hasAutoGateCredential(env = process.env, home) {
8195
+ return Boolean(resolveApiKey({ env, ...home !== void 0 ? { home } : {} }));
8196
+ }
8197
+ function createSquadrantAutoGate(deps) {
8198
+ const env = deps.env ?? process.env;
8199
+ const taskId = env.SQUADRANT_CREW_TASK_ID;
8200
+ const project = deps.project ?? env.SQUADRANT_CREW_PROJECT;
8201
+ const mode = resolveGateMode(env, deps.config.defaults.gate);
8202
+ const projected = projectGateConfig({ gate: { ...gateForProjection(deps.config), mode } });
8203
+ const enabled = mode === "on";
8204
+ const blockedSignal = async (ctx) => {
8205
+ if (!taskId || !project || !deps.sendBlocked)
8206
+ return;
8207
+ const event = {
8208
+ type: "task.blocked",
8209
+ id: taskId,
8210
+ reason: ctx.reason,
8211
+ question: `crew needs permission to run ${ctx.tool}`
8212
+ };
8213
+ await deps.sendBlocked(project, event);
8214
+ };
8215
+ const host = createAutoGate({
8216
+ config: projected,
8217
+ readIntent: (p) => readUserIntent(p) ?? void 0,
8218
+ blockedSignal,
8219
+ ...deps.decide ? { decide: deps.decide } : {},
8220
+ ...project ? { project } : {},
8221
+ env,
8222
+ log: deps.log
8223
+ });
8224
+ if (!enabled) {
8225
+ return { config: host.config, decideClaudeHookPayload: async () => void 0 };
8226
+ }
8227
+ return host;
8228
+ }
8229
+ var init_auto_gate = __esm({
8230
+ "packages/core/dist/auto-gate.js"() {
8231
+ init_permission_gate();
8232
+ }
8233
+ });
8234
+
8123
8235
  // packages/core/dist/group-dispatch.js
8124
8236
  import { randomUUID as randomUUID4 } from "crypto";
8125
8237
  import { createConnection as createConnection2 } from "net";
@@ -8526,10 +8638,11 @@ async function runCrewAnswer(project, name, option, runtime, workspaceId, deps,
8526
8638
  if (!crew) {
8527
8639
  throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
8528
8640
  }
8529
- const options = await deps.readModalOptions(crew);
8530
- if (!options) {
8641
+ const modal = await deps.readModalOptions(crew);
8642
+ if (!modal || modal.options.length === 0) {
8531
8643
  throw new Error(`Crew '${name}' has no interactive option prompt visible right now \u2014 nothing to answer. Read its screen with 'squadrant crew read ${project} ${name}' to check its state.`);
8532
8644
  }
8645
+ const options = modal.options;
8533
8646
  const target = resolveOption(options, option);
8534
8647
  if (opts?.expect && !target.label.toLowerCase().includes(opts.expect.toLowerCase())) {
8535
8648
  throw new Error(`Refusing: option ${target.index} is "${target.label}", which does not contain expected text "${opts.expect}". Option order is model-generated and can shift between renders \u2014 re-check with 'squadrant crew read ${project} ${name}'.
@@ -8541,7 +8654,9 @@ ${describeOptions(options)}`);
8541
8654
  log(`\u2192 selecting ${target.index}. "${target.label}"`);
8542
8655
  const current = options.find((o) => o.highlighted) ?? options[0];
8543
8656
  const steps = target.index - current.index;
8544
- const key = steps >= 0 ? "Down" : "Up";
8657
+ const forwardKey = modal.axis === "horizontal" ? "Right" : "Down";
8658
+ const backwardKey = modal.axis === "horizontal" ? "Left" : "Up";
8659
+ const key = steps >= 0 ? forwardKey : backwardKey;
8545
8660
  for (let i = 0; i < Math.abs(steps); i++) {
8546
8661
  await runtime.sendKeyToPane(crew, key);
8547
8662
  }
@@ -8913,6 +9028,7 @@ __export(dist_exports, {
8913
9028
  CREW_GONE_MS: () => CREW_GONE_MS,
8914
9029
  CREW_STALE_MS: () => CREW_STALE_MS,
8915
9030
  CaptainDelivery: () => CaptainDelivery,
9031
+ DEFAULT_BLOCKED_RENUDGE_MS: () => DEFAULT_BLOCKED_RENUDGE_MS,
8916
9032
  DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS: () => DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS,
8917
9033
  DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS: () => DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS,
8918
9034
  DEFAULT_GATE_DENY_RULES: () => DEFAULT_GATE_DENY_RULES,
@@ -8995,6 +9111,7 @@ __export(dist_exports, {
8995
9111
  createRouterService: () => createRouterService,
8996
9112
  createRouterShim: () => createRouterShim,
8997
9113
  createRunCommand: () => createRunCommand,
9114
+ createSquadrantAutoGate: () => createSquadrantAutoGate,
8998
9115
  createStore: () => createStore,
8999
9116
  createSurfaceLivenessProbe: () => createSurfaceLivenessProbe,
9000
9117
  createTelegramBridge: () => createTelegramBridge,
@@ -9051,6 +9168,7 @@ __export(dist_exports, {
9051
9168
  freshTrace: () => freshTrace,
9052
9169
  gateCacheKey: () => gateCacheKey,
9053
9170
  getDaemonPid: () => getDaemonPid,
9171
+ hasAutoGateCredential: () => hasAutoGateCredential,
9054
9172
  hasModalOptionList: () => hasModalOptionList,
9055
9173
  healCmdFor: () => healCmdFor,
9056
9174
  inboundBody: () => inboundBody,
@@ -9136,6 +9254,7 @@ __export(dist_exports, {
9136
9254
  resolveCurrentProject: () => resolveCurrentProject,
9137
9255
  resolveGate: () => resolveGate,
9138
9256
  resolveGateDenyRules: () => resolveGateDenyRules,
9257
+ resolveGateEngine: () => resolveGateEngine,
9139
9258
  resolveGateMode: () => resolveGateMode,
9140
9259
  resolveGatePolicy: () => resolveGatePolicy,
9141
9260
  resolveGateTools: () => resolveGateTools,
@@ -9241,6 +9360,7 @@ var init_dist2 = __esm({
9241
9360
  init_router_resolution();
9242
9361
  init_captain_router();
9243
9362
  init_permission_gate();
9363
+ init_auto_gate();
9244
9364
  init_restart_daemon();
9245
9365
  init_group_dispatch();
9246
9366
  init_launch_workspace();
@@ -12149,6 +12269,48 @@ function removeCommandHandlers(entries, command) {
12149
12269
  }
12150
12270
  return removed;
12151
12271
  }
12272
+ var FOREIGN_AUTO_GATE_MARKER = "auto-gate-managed";
12273
+ function isForeignAutoGateCommand(command) {
12274
+ if (typeof command !== "string")
12275
+ return false;
12276
+ if (command.includes(FOREIGN_AUTO_GATE_MARKER))
12277
+ return true;
12278
+ const first = command.trim().split(/\s+/)[0] ?? "";
12279
+ return /(^|[/\\])auto-gate$/.test(first);
12280
+ }
12281
+ function findForeignGateHandlers(entries) {
12282
+ const found = [];
12283
+ for (const entry of entries) {
12284
+ const hooks = entry?.hooks;
12285
+ if (!Array.isArray(hooks))
12286
+ continue;
12287
+ for (const h of hooks) {
12288
+ const cmd = h?.command;
12289
+ if (isForeignAutoGateCommand(cmd))
12290
+ found.push(cmd);
12291
+ }
12292
+ }
12293
+ return found;
12294
+ }
12295
+ function removeForeignGateHandlers(entries) {
12296
+ const removed = [];
12297
+ for (let i = entries.length - 1; i >= 0; i--) {
12298
+ const entry = entries[i];
12299
+ if (!Array.isArray(entry?.hooks))
12300
+ continue;
12301
+ entry.hooks = entry.hooks.filter((h) => {
12302
+ const cmd = h?.command;
12303
+ if (isForeignAutoGateCommand(cmd)) {
12304
+ removed.push(cmd);
12305
+ return false;
12306
+ }
12307
+ return true;
12308
+ });
12309
+ if (entry.hooks.length === 0)
12310
+ entries.splice(i, 1);
12311
+ }
12312
+ return removed;
12313
+ }
12152
12314
  function installClaudeHooks(opts = {}) {
12153
12315
  const settingsPath = opts.settingsPath ?? join25(homedir13(), ".claude", "settings.json");
12154
12316
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
@@ -12186,6 +12348,23 @@ function installClaudeHooks(opts = {}) {
12186
12348
  changed = true;
12187
12349
  migrated.push(`${eventName}/${sub}`);
12188
12350
  }
12351
+ const foreign = findForeignGateHandlers(entries);
12352
+ if (foreign.length > 0) {
12353
+ if (opts.gateMode === "on") {
12354
+ const removed = removeForeignGateHandlers(entries);
12355
+ if (removed.length > 0)
12356
+ changed = true;
12357
+ for (const command2 of removed) {
12358
+ log(`native-hook: removed foreign auto-gate PermissionRequest handler in ${settingsPath} [#P6 \xA715#5]: ${command2}`);
12359
+ opts.recordGateAction?.({ action: "removed-foreign-gate", command: command2 });
12360
+ }
12361
+ } else {
12362
+ for (const command2 of foreign) {
12363
+ log(`native-hook: foreign auto-gate PermissionRequest handler present in ${settingsPath} but squadrant's gate is '${opts.gateMode ?? "auto"}' (not 'on') \u2014 foreign entry left untouched [#P6 \xA715#5]: ${command2}`);
12364
+ opts.recordGateAction?.({ action: "left-foreign-gate", command: command2 });
12365
+ }
12366
+ }
12367
+ }
12189
12368
  }
12190
12369
  const alreadyPresent = entries.some((m) => Array.isArray(m.hooks) && m.hooks.some((h) => typeof h.command === "string" && h.command === command));
12191
12370
  if (!alreadyPresent) {
@@ -12216,11 +12395,57 @@ function installClaudeHooks(opts = {}) {
12216
12395
  changed = true;
12217
12396
  }
12218
12397
  }
12398
+ if (typeof settings.env === "object" && settings.env !== null && !Array.isArray(settings.env)) {
12399
+ if (reconcileSubagentModels(settings.env, log))
12400
+ changed = true;
12401
+ }
12219
12402
  if (changed) {
12220
12403
  writeFile6(settingsPath, JSON.stringify(settings, null, 2));
12221
12404
  }
12222
12405
  return settingsPath;
12223
12406
  }
12407
+ var SUBAGENT_MODEL_KEY = "CLAUDE_CODE_SUBAGENT_MODEL";
12408
+ var SMALL_FAST_MODEL_KEY = "ANTHROPIC_SMALL_FAST_MODEL";
12409
+ function isCustomUpstream(baseUrl) {
12410
+ if (typeof baseUrl !== "string" || baseUrl.length === 0)
12411
+ return false;
12412
+ try {
12413
+ return new URL(baseUrl).hostname.toLowerCase() !== "api.anthropic.com";
12414
+ } catch {
12415
+ return false;
12416
+ }
12417
+ }
12418
+ function isAnthropicModelId(value) {
12419
+ if (typeof value !== "string")
12420
+ return false;
12421
+ const v2 = value.trim().toLowerCase();
12422
+ return v2 === "sonnet" || v2 === "opus" || v2 === "haiku" || v2.startsWith("claude-");
12423
+ }
12424
+ function reconcileSubagentModels(env, log) {
12425
+ if (!isCustomUpstream(env.ANTHROPIC_BASE_URL))
12426
+ return false;
12427
+ const model = env.ANTHROPIC_MODEL;
12428
+ if (typeof model !== "string" || model.length === 0)
12429
+ return false;
12430
+ let changed = false;
12431
+ for (const key of [SUBAGENT_MODEL_KEY, SMALL_FAST_MODEL_KEY]) {
12432
+ const current = env[key];
12433
+ if (current === void 0) {
12434
+ env[key] = model;
12435
+ changed = true;
12436
+ log(`native-hook: set ${key}='${model}' to match ANTHROPIC_MODEL on a custom upstream`);
12437
+ } else if (isAnthropicModelId(current)) {
12438
+ if (current !== model) {
12439
+ env[key] = model;
12440
+ changed = true;
12441
+ log(`native-hook: aligned ${key} '${String(current)}' \u2192 '${model}' for a custom upstream`);
12442
+ }
12443
+ } else {
12444
+ log(`native-hook: ${key} '${String(current)}' is a custom model id \u2014 left untouched`);
12445
+ }
12446
+ }
12447
+ return changed;
12448
+ }
12224
12449
  function mapSubToLifecycle(sub) {
12225
12450
  switch (sub) {
12226
12451
  case "session-start":
@@ -12786,7 +13011,15 @@ function startSquadrantd(opts = {}) {
12786
13011
  log
12787
13012
  });
12788
13013
  const cmuxStoreSource = new CmuxStoreSource({ log });
12789
- const nativeHookSource = new NativeHookSource({ log, hookInstall: { claudeEnv: loadConfig().defaults.claudeEnv } });
13014
+ const nativeHookSource = new NativeHookSource({
13015
+ log,
13016
+ hookInstall: {
13017
+ claudeEnv: loadConfig().defaults.claudeEnv,
13018
+ // #P6 (§15#5): only an explicit gate mode "on" lets squadrant remove a
13019
+ // foreign auto-gate PermissionRequest handler; auto/absent leaves it alone.
13020
+ gateMode: loadConfig().defaults.gate?.mode
13021
+ }
13022
+ });
12790
13023
  ctx.codexDriver = codexDriver;
12791
13024
  ctx.opencodeBridge = opencodeBridge;
12792
13025
  ctx.cmuxEventsBridge = cmuxEventsBridge;