squadrant 0.22.0 → 0.23.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) {
@@ -7736,6 +7769,12 @@ function resolveGateMode(env, gate) {
7736
7769
  return fromEnv;
7737
7770
  return gate?.mode && isGateMode(gate.mode) ? gate.mode : "auto";
7738
7771
  }
7772
+ function resolveGateEngine(env, gate) {
7773
+ const fromEnv = env.SQUADRANT_GATE_ENGINE;
7774
+ if (fromEnv !== void 0 && isGateEngine(fromEnv))
7775
+ return fromEnv;
7776
+ return gate?.engine && isGateEngine(gate.engine) ? gate.engine : "router";
7777
+ }
7739
7778
  function resolveGatePolicy(env, gate) {
7740
7779
  const fromEnv = env.SQUADRANT_GATE_POLICY;
7741
7780
  if (fromEnv !== void 0 && isGatePolicy(fromEnv))
@@ -8120,6 +8159,59 @@ var init_permission_gate = __esm({
8120
8159
  }
8121
8160
  });
8122
8161
 
8162
+ // packages/core/dist/auto-gate.js
8163
+ import { createAutoGate, projectGateConfig } from "@squadrant-ai/auto-gate";
8164
+ function gateForProjection(config) {
8165
+ const gate = config.defaults.gate;
8166
+ if (!gate)
8167
+ return {};
8168
+ const out = {};
8169
+ if (gate.mode)
8170
+ out.mode = gate.mode;
8171
+ if (gate.tools)
8172
+ out.tools = gate.tools;
8173
+ if (gate.deny)
8174
+ out.deny = gate.deny;
8175
+ return out;
8176
+ }
8177
+ function createSquadrantAutoGate(deps) {
8178
+ const env = deps.env ?? process.env;
8179
+ const taskId = env.SQUADRANT_CREW_TASK_ID;
8180
+ const project = deps.project ?? env.SQUADRANT_CREW_PROJECT;
8181
+ const mode = resolveGateMode(env, deps.config.defaults.gate);
8182
+ const projected = projectGateConfig({ gate: { ...gateForProjection(deps.config), mode } });
8183
+ const enabled = mode === "on";
8184
+ const blockedSignal = async (ctx) => {
8185
+ if (!taskId || !project || !deps.sendBlocked)
8186
+ return;
8187
+ const event = {
8188
+ type: "task.blocked",
8189
+ id: taskId,
8190
+ reason: ctx.reason,
8191
+ question: `crew needs permission to run ${ctx.tool}`
8192
+ };
8193
+ await deps.sendBlocked(project, event);
8194
+ };
8195
+ const host = createAutoGate({
8196
+ config: projected,
8197
+ readIntent: (p) => readUserIntent(p) ?? void 0,
8198
+ blockedSignal,
8199
+ ...deps.decide ? { decide: deps.decide } : {},
8200
+ ...project ? { project } : {},
8201
+ env,
8202
+ log: deps.log
8203
+ });
8204
+ if (!enabled) {
8205
+ return { config: host.config, decideClaudeHookPayload: async () => void 0 };
8206
+ }
8207
+ return host;
8208
+ }
8209
+ var init_auto_gate = __esm({
8210
+ "packages/core/dist/auto-gate.js"() {
8211
+ init_permission_gate();
8212
+ }
8213
+ });
8214
+
8123
8215
  // packages/core/dist/group-dispatch.js
8124
8216
  import { randomUUID as randomUUID4 } from "crypto";
8125
8217
  import { createConnection as createConnection2 } from "net";
@@ -8526,10 +8618,11 @@ async function runCrewAnswer(project, name, option, runtime, workspaceId, deps,
8526
8618
  if (!crew) {
8527
8619
  throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
8528
8620
  }
8529
- const options = await deps.readModalOptions(crew);
8530
- if (!options) {
8621
+ const modal = await deps.readModalOptions(crew);
8622
+ if (!modal || modal.options.length === 0) {
8531
8623
  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
8624
  }
8625
+ const options = modal.options;
8533
8626
  const target = resolveOption(options, option);
8534
8627
  if (opts?.expect && !target.label.toLowerCase().includes(opts.expect.toLowerCase())) {
8535
8628
  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 +8634,9 @@ ${describeOptions(options)}`);
8541
8634
  log(`\u2192 selecting ${target.index}. "${target.label}"`);
8542
8635
  const current = options.find((o) => o.highlighted) ?? options[0];
8543
8636
  const steps = target.index - current.index;
8544
- const key = steps >= 0 ? "Down" : "Up";
8637
+ const forwardKey = modal.axis === "horizontal" ? "Right" : "Down";
8638
+ const backwardKey = modal.axis === "horizontal" ? "Left" : "Up";
8639
+ const key = steps >= 0 ? forwardKey : backwardKey;
8545
8640
  for (let i = 0; i < Math.abs(steps); i++) {
8546
8641
  await runtime.sendKeyToPane(crew, key);
8547
8642
  }
@@ -8913,6 +9008,7 @@ __export(dist_exports, {
8913
9008
  CREW_GONE_MS: () => CREW_GONE_MS,
8914
9009
  CREW_STALE_MS: () => CREW_STALE_MS,
8915
9010
  CaptainDelivery: () => CaptainDelivery,
9011
+ DEFAULT_BLOCKED_RENUDGE_MS: () => DEFAULT_BLOCKED_RENUDGE_MS,
8916
9012
  DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS: () => DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS,
8917
9013
  DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS: () => DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS,
8918
9014
  DEFAULT_GATE_DENY_RULES: () => DEFAULT_GATE_DENY_RULES,
@@ -8995,6 +9091,7 @@ __export(dist_exports, {
8995
9091
  createRouterService: () => createRouterService,
8996
9092
  createRouterShim: () => createRouterShim,
8997
9093
  createRunCommand: () => createRunCommand,
9094
+ createSquadrantAutoGate: () => createSquadrantAutoGate,
8998
9095
  createStore: () => createStore,
8999
9096
  createSurfaceLivenessProbe: () => createSurfaceLivenessProbe,
9000
9097
  createTelegramBridge: () => createTelegramBridge,
@@ -9136,6 +9233,7 @@ __export(dist_exports, {
9136
9233
  resolveCurrentProject: () => resolveCurrentProject,
9137
9234
  resolveGate: () => resolveGate,
9138
9235
  resolveGateDenyRules: () => resolveGateDenyRules,
9236
+ resolveGateEngine: () => resolveGateEngine,
9139
9237
  resolveGateMode: () => resolveGateMode,
9140
9238
  resolveGatePolicy: () => resolveGatePolicy,
9141
9239
  resolveGateTools: () => resolveGateTools,
@@ -9241,6 +9339,7 @@ var init_dist2 = __esm({
9241
9339
  init_router_resolution();
9242
9340
  init_captain_router();
9243
9341
  init_permission_gate();
9342
+ init_auto_gate();
9244
9343
  init_restart_daemon();
9245
9344
  init_group_dispatch();
9246
9345
  init_launch_workspace();
@@ -12149,6 +12248,48 @@ function removeCommandHandlers(entries, command) {
12149
12248
  }
12150
12249
  return removed;
12151
12250
  }
12251
+ var FOREIGN_AUTO_GATE_MARKER = "auto-gate-managed";
12252
+ function isForeignAutoGateCommand(command) {
12253
+ if (typeof command !== "string")
12254
+ return false;
12255
+ if (command.includes(FOREIGN_AUTO_GATE_MARKER))
12256
+ return true;
12257
+ const first = command.trim().split(/\s+/)[0] ?? "";
12258
+ return /(^|[/\\])auto-gate$/.test(first);
12259
+ }
12260
+ function findForeignGateHandlers(entries) {
12261
+ const found = [];
12262
+ for (const entry of entries) {
12263
+ const hooks = entry?.hooks;
12264
+ if (!Array.isArray(hooks))
12265
+ continue;
12266
+ for (const h of hooks) {
12267
+ const cmd = h?.command;
12268
+ if (isForeignAutoGateCommand(cmd))
12269
+ found.push(cmd);
12270
+ }
12271
+ }
12272
+ return found;
12273
+ }
12274
+ function removeForeignGateHandlers(entries) {
12275
+ const removed = [];
12276
+ for (let i = entries.length - 1; i >= 0; i--) {
12277
+ const entry = entries[i];
12278
+ if (!Array.isArray(entry?.hooks))
12279
+ continue;
12280
+ entry.hooks = entry.hooks.filter((h) => {
12281
+ const cmd = h?.command;
12282
+ if (isForeignAutoGateCommand(cmd)) {
12283
+ removed.push(cmd);
12284
+ return false;
12285
+ }
12286
+ return true;
12287
+ });
12288
+ if (entry.hooks.length === 0)
12289
+ entries.splice(i, 1);
12290
+ }
12291
+ return removed;
12292
+ }
12152
12293
  function installClaudeHooks(opts = {}) {
12153
12294
  const settingsPath = opts.settingsPath ?? join25(homedir13(), ".claude", "settings.json");
12154
12295
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
@@ -12186,6 +12327,23 @@ function installClaudeHooks(opts = {}) {
12186
12327
  changed = true;
12187
12328
  migrated.push(`${eventName}/${sub}`);
12188
12329
  }
12330
+ const foreign = findForeignGateHandlers(entries);
12331
+ if (foreign.length > 0) {
12332
+ if (opts.gateMode === "on") {
12333
+ const removed = removeForeignGateHandlers(entries);
12334
+ if (removed.length > 0)
12335
+ changed = true;
12336
+ for (const command2 of removed) {
12337
+ log(`native-hook: removed foreign auto-gate PermissionRequest handler in ${settingsPath} [#P6 \xA715#5]: ${command2}`);
12338
+ opts.recordGateAction?.({ action: "removed-foreign-gate", command: command2 });
12339
+ }
12340
+ } else {
12341
+ for (const command2 of foreign) {
12342
+ 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}`);
12343
+ opts.recordGateAction?.({ action: "left-foreign-gate", command: command2 });
12344
+ }
12345
+ }
12346
+ }
12189
12347
  }
12190
12348
  const alreadyPresent = entries.some((m) => Array.isArray(m.hooks) && m.hooks.some((h) => typeof h.command === "string" && h.command === command));
12191
12349
  if (!alreadyPresent) {
@@ -12216,11 +12374,57 @@ function installClaudeHooks(opts = {}) {
12216
12374
  changed = true;
12217
12375
  }
12218
12376
  }
12377
+ if (typeof settings.env === "object" && settings.env !== null && !Array.isArray(settings.env)) {
12378
+ if (reconcileSubagentModels(settings.env, log))
12379
+ changed = true;
12380
+ }
12219
12381
  if (changed) {
12220
12382
  writeFile6(settingsPath, JSON.stringify(settings, null, 2));
12221
12383
  }
12222
12384
  return settingsPath;
12223
12385
  }
12386
+ var SUBAGENT_MODEL_KEY = "CLAUDE_CODE_SUBAGENT_MODEL";
12387
+ var SMALL_FAST_MODEL_KEY = "ANTHROPIC_SMALL_FAST_MODEL";
12388
+ function isCustomUpstream(baseUrl) {
12389
+ if (typeof baseUrl !== "string" || baseUrl.length === 0)
12390
+ return false;
12391
+ try {
12392
+ return new URL(baseUrl).hostname.toLowerCase() !== "api.anthropic.com";
12393
+ } catch {
12394
+ return false;
12395
+ }
12396
+ }
12397
+ function isAnthropicModelId(value) {
12398
+ if (typeof value !== "string")
12399
+ return false;
12400
+ const v2 = value.trim().toLowerCase();
12401
+ return v2 === "sonnet" || v2 === "opus" || v2 === "haiku" || v2.startsWith("claude-");
12402
+ }
12403
+ function reconcileSubagentModels(env, log) {
12404
+ if (!isCustomUpstream(env.ANTHROPIC_BASE_URL))
12405
+ return false;
12406
+ const model = env.ANTHROPIC_MODEL;
12407
+ if (typeof model !== "string" || model.length === 0)
12408
+ return false;
12409
+ let changed = false;
12410
+ for (const key of [SUBAGENT_MODEL_KEY, SMALL_FAST_MODEL_KEY]) {
12411
+ const current = env[key];
12412
+ if (current === void 0) {
12413
+ env[key] = model;
12414
+ changed = true;
12415
+ log(`native-hook: set ${key}='${model}' to match ANTHROPIC_MODEL on a custom upstream`);
12416
+ } else if (isAnthropicModelId(current)) {
12417
+ if (current !== model) {
12418
+ env[key] = model;
12419
+ changed = true;
12420
+ log(`native-hook: aligned ${key} '${String(current)}' \u2192 '${model}' for a custom upstream`);
12421
+ }
12422
+ } else {
12423
+ log(`native-hook: ${key} '${String(current)}' is a custom model id \u2014 left untouched`);
12424
+ }
12425
+ }
12426
+ return changed;
12427
+ }
12224
12428
  function mapSubToLifecycle(sub) {
12225
12429
  switch (sub) {
12226
12430
  case "session-start":
@@ -12786,7 +12990,15 @@ function startSquadrantd(opts = {}) {
12786
12990
  log
12787
12991
  });
12788
12992
  const cmuxStoreSource = new CmuxStoreSource({ log });
12789
- const nativeHookSource = new NativeHookSource({ log, hookInstall: { claudeEnv: loadConfig().defaults.claudeEnv } });
12993
+ const nativeHookSource = new NativeHookSource({
12994
+ log,
12995
+ hookInstall: {
12996
+ claudeEnv: loadConfig().defaults.claudeEnv,
12997
+ // #P6 (§15#5): only an explicit gate mode "on" lets squadrant remove a
12998
+ // foreign auto-gate PermissionRequest handler; auto/absent leaves it alone.
12999
+ gateMode: loadConfig().defaults.gate?.mode
13000
+ }
13001
+ });
12790
13002
  ctx.codexDriver = codexDriver;
12791
13003
  ctx.opencodeBridge = opencodeBridge;
12792
13004
  ctx.cmuxEventsBridge = cmuxEventsBridge;