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.
package/dist/index.js CHANGED
@@ -98,6 +98,9 @@ function isGateMode(v2) {
98
98
  function isGatePolicy(v2) {
99
99
  return GATE_POLICIES.includes(v2);
100
100
  }
101
+ function isGateEngine(v2) {
102
+ return GATE_ENGINES.includes(v2);
103
+ }
101
104
  function isThinkingLevel(v2) {
102
105
  return THINKING_LEVELS.includes(v2);
103
106
  }
@@ -198,13 +201,14 @@ function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
198
201
  function resolveHome(p) {
199
202
  return p.startsWith("~") ? p.replace("~", os.homedir()) : p;
200
203
  }
201
- var ROUTER_KINDS, GATE_MODES, GATE_POLICIES, THINKING_LEVELS, CONTROL_CHANNEL_MODES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH, DEFAULT_CREW_ROUTING_RULES;
204
+ var ROUTER_KINDS, GATE_MODES, GATE_POLICIES, GATE_ENGINES, THINKING_LEVELS, CONTROL_CHANNEL_MODES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH, DEFAULT_CREW_ROUTING_RULES;
202
205
  var init_config = __esm({
203
206
  "packages/shared/dist/config.js"() {
204
207
  init_config_io();
205
208
  ROUTER_KINDS = ["opencode-go", "openrouter", "ccr", "litellm", "custom"];
206
209
  GATE_MODES = ["on", "off", "auto"];
207
210
  GATE_POLICIES = ["deny-dangerous", "ask-on-doubt"];
211
+ GATE_ENGINES = ["router", "auto-gate"];
208
212
  THINKING_LEVELS = ["low", "medium", "high", "xhigh", "max"];
209
213
  CONTROL_CHANNEL_MODES = /* @__PURE__ */ new Set(["off", "shadow", "on"]);
210
214
  DEFAULT_CONFIG_PATH = process.env.SQUADRANT_CONFIG || path2.join(os.homedir(), ".config", "squadrant", "config.json");
@@ -221,7 +225,12 @@ var init_config = __esm({
221
225
 
222
226
  // packages/shared/dist/provider-preset.js
223
227
  function isProviderPresetId(v2) {
224
- return v2 === "a" || v2 === "b" || v2 === "c" || v2 === "d";
228
+ return v2 === "a" || v2 === "b" || v2 === "c";
229
+ }
230
+ function normalizePresetId(v2) {
231
+ if (v2 === "d")
232
+ return "c";
233
+ return isProviderPresetId(v2) ? v2 : void 0;
225
234
  }
226
235
  function presetCrewRouting(id) {
227
236
  const base = (tier) => {
@@ -244,17 +253,10 @@ function presetCrewRouting(id) {
244
253
  { ...base("daily"), agent: "opencode", model: OPENCODE_MODEL }
245
254
  ];
246
255
  case "c":
247
- return [
248
- { ...base("extreme"), agent: "claude", backend: "proxy", model: ROUTER_PRESET_MODEL },
249
- { ...base("hard"), agent: "claude", backend: "proxy", model: ROUTER_PRESET_MODEL },
250
- codex("mobile"),
251
- { ...base("daily"), agent: "opencode", model: OPENCODE_MODEL }
252
- ];
253
- case "d":
254
256
  return ["extreme", "hard", "mobile", "daily"].map(codex);
255
257
  }
256
258
  }
257
- function providerPresetDefaults(id, router) {
259
+ function providerPresetDefaults(id) {
258
260
  switch (id) {
259
261
  case "a":
260
262
  return {
@@ -283,28 +285,6 @@ function providerPresetDefaults(id, router) {
283
285
  };
284
286
  }
285
287
  case "c": {
286
- if (!router) {
287
- throw new Error("preset C (claude harness + router backend) requires a router config \u2014 set defaults.router first");
288
- }
289
- const routed = { agent: "claude", backend: "proxy", model: ROUTER_PRESET_MODEL };
290
- return {
291
- roles: {
292
- command: { ...routed },
293
- captain: { ...routed },
294
- crew: { ...routed },
295
- exploration: { ...routed },
296
- side: { ...routed }
297
- },
298
- // Auto mode's classifier is hardcoded to Sonnet 5 and fails closed on a
299
- // router upstream — every claude session must use a manual mode so the
300
- // U7 gate owns the prompt.
301
- permissions: { command: "default", captain: "default", crew: "default" },
302
- crewRouting: { rules: presetCrewRouting("c") },
303
- router,
304
- gate: { mode: "on" }
305
- };
306
- }
307
- case "d": {
308
288
  const codex = { agent: "codex" };
309
289
  return {
310
290
  roles: {
@@ -315,7 +295,7 @@ function providerPresetDefaults(id, router) {
315
295
  side: { ...codex }
316
296
  },
317
297
  permissions: {},
318
- crewRouting: { rules: presetCrewRouting("d") }
298
+ crewRouting: { rules: presetCrewRouting("c") }
319
299
  };
320
300
  }
321
301
  }
@@ -325,13 +305,11 @@ function detectProviderPresetId(config) {
325
305
  if (crew?.agent === "opencode")
326
306
  return "b";
327
307
  if (crew?.agent === "codex")
328
- return "d";
329
- if (crew?.backend && crew.backend !== "native")
330
308
  return "c";
331
309
  return "a";
332
310
  }
333
311
  function applyProviderPreset(config, id, opts) {
334
- const preset = providerPresetDefaults(id, opts.router);
312
+ const preset = providerPresetDefaults(id);
335
313
  const next = structuredClone(config);
336
314
  const changes = [];
337
315
  if (opts.overwrite) {
@@ -357,21 +335,9 @@ function applyProviderPreset(config, id, opts) {
357
335
  next.defaults.permissions = { ...preset.permissions };
358
336
  changes.push("defaults.permissions");
359
337
  }
360
- if (preset.router) {
361
- if (opts.overwrite || next.defaults.router === void 0) {
362
- next.defaults.router = structuredClone(preset.router);
363
- changes.push("defaults.router");
364
- }
365
- }
366
- if (preset.gate) {
367
- if (opts.overwrite || next.defaults.gate === void 0) {
368
- next.defaults.gate = { ...preset.gate };
369
- changes.push("defaults.gate");
370
- }
371
- }
372
338
  return { config: next, changes };
373
339
  }
374
- var PROVIDER_PRESETS, ROUTER_PRESET_MODEL, DEFAULT_ROUTER_KIND, DEFAULT_ROUTER_BASE_URL, OPENCODE_MODEL;
340
+ var PROVIDER_PRESETS, OPENCODE_MODEL;
375
341
  var init_provider_preset = __esm({
376
342
  "packages/shared/dist/provider-preset.js"() {
377
343
  init_config();
@@ -390,20 +356,11 @@ var init_provider_preset = __esm({
390
356
  },
391
357
  {
392
358
  id: "c",
393
- label: "Claude harness + router backend (advanced)",
394
- summary: "Claude harness against a router upstream. The U7 gate replaces auto mode.",
395
- requiresAnthropic: false
396
- },
397
- {
398
- id: "d",
399
359
  label: "Codex (ChatGPT Pro)",
400
360
  summary: "All roles run on the codex harness.",
401
361
  requiresAnthropic: false
402
362
  }
403
363
  ];
404
- ROUTER_PRESET_MODEL = "deepseek-v4.1-flash";
405
- DEFAULT_ROUTER_KIND = "opencode-go";
406
- DEFAULT_ROUTER_BASE_URL = "https://opencode.ai/zen/go";
407
364
  OPENCODE_MODEL = "opencode-go/deepseek-v4.1-flash";
408
365
  }
409
366
  });
@@ -1657,14 +1614,12 @@ __export(dist_exports, {
1657
1614
  DEFAULT_CONFIG_PATH: () => DEFAULT_CONFIG_PATH,
1658
1615
  DEFAULT_CREW_ROUTING_RULES: () => DEFAULT_CREW_ROUTING_RULES,
1659
1616
  DEFAULT_NOTIFY: () => DEFAULT_NOTIFY,
1660
- DEFAULT_ROUTER_BASE_URL: () => DEFAULT_ROUTER_BASE_URL,
1661
- DEFAULT_ROUTER_KIND: () => DEFAULT_ROUTER_KIND,
1617
+ GATE_ENGINES: () => GATE_ENGINES,
1662
1618
  GATE_MODES: () => GATE_MODES,
1663
1619
  GATE_POLICIES: () => GATE_POLICIES,
1664
1620
  MANAGED_TARGETS: () => MANAGED_TARGETS,
1665
1621
  PROVIDER_PRESETS: () => PROVIDER_PRESETS,
1666
1622
  ROUTER_KINDS: () => ROUTER_KINDS,
1667
- ROUTER_PRESET_MODEL: () => ROUTER_PRESET_MODEL,
1668
1623
  SAFE_KINDS: () => SAFE_KINDS,
1669
1624
  SOCKET_CONTROL_MODE_PATH: () => SOCKET_CONTROL_MODE_PATH,
1670
1625
  SPOKE_SUBDIRS: () => SPOKE_SUBDIRS,
@@ -1705,6 +1660,7 @@ __export(dist_exports, {
1705
1660
  isBackendMode: () => isBackendMode,
1706
1661
  isCacheStale: () => isCacheStale,
1707
1662
  isDaemonCachedKey: () => isDaemonCachedKey,
1663
+ isGateEngine: () => isGateEngine,
1708
1664
  isGateMode: () => isGateMode,
1709
1665
  isGatePolicy: () => isGatePolicy,
1710
1666
  isNewerVersion: () => isNewerVersion,
@@ -1720,6 +1676,7 @@ __export(dist_exports, {
1720
1676
  mirrorDir: () => mirrorDir,
1721
1677
  mirrorFlat: () => mirrorFlat,
1722
1678
  needsCheck: () => needsCheck,
1679
+ normalizePresetId: () => normalizePresetId,
1723
1680
  notifyIfUpdateAvailable: () => notifyIfUpdateAvailable,
1724
1681
  parseSection: () => parseSection,
1725
1682
  parseThinkingLevel: () => parseThinkingLevel,
@@ -2027,10 +1984,11 @@ function formatDelegationReport(rec, originProject, targetProject) {
2027
1984
  return null;
2028
1985
  }
2029
1986
  }
2030
- function firePush(deps, project, prev, next, event, lastCaptainTurnAt) {
1987
+ function firePush(deps, project, prev, next, event, lastCaptainTurnAt, prevRec) {
2031
1988
  if (!deps.notify)
2032
1989
  return;
2033
- if (prev === next.state)
1990
+ const questionChanged = prev === "blocked" && next.state === "blocked" && prevRec != null && (prevRec.question ?? "").trim() !== (next.question ?? "").trim();
1991
+ if (prev === next.state && !questionChanged)
2034
1992
  return;
2035
1993
  if (!ATTENTION_STATES.has(next.state))
2036
1994
  return;
@@ -2068,6 +2026,7 @@ function createDaemon(deps) {
2068
2026
  const { store, now } = deps;
2069
2027
  const lastCaptainTurnAt = /* @__PURE__ */ new Map();
2070
2028
  const quietNotifiedAt = /* @__PURE__ */ new Map();
2029
+ const blockedRenudgedFor = /* @__PURE__ */ new Map();
2071
2030
  const resendAttemptedAt = /* @__PURE__ */ new Map();
2072
2031
  const firstTurnUndeliveredBudgetMs = deps.firstTurnUndeliveredBudgetMs ?? DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS;
2073
2032
  const firstTurnResendCooldownMs = deps.firstTurnResendCooldownMs ?? DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS;
@@ -2088,7 +2047,7 @@ function createDaemon(deps) {
2088
2047
  const next = reduce(cur, event, now());
2089
2048
  if (next !== cur) {
2090
2049
  store.put(next);
2091
- firePush(deps, project, cur.state, next, event, lastCaptainTurnAt.get(next.id));
2050
+ firePush(deps, project, cur.state, next, event, lastCaptainTurnAt.get(next.id), cur);
2092
2051
  }
2093
2052
  return next;
2094
2053
  }
@@ -2240,6 +2199,8 @@ function createDaemon(deps) {
2240
2199
  }
2241
2200
  const recoveryPromises = [];
2242
2201
  for (const r of store.listAll()) {
2202
+ if (r.state !== "blocked")
2203
+ blockedRenudgedFor.delete(r.id);
2243
2204
  if (TERMINAL_STATES.has(r.state) && t - r.lastHeartbeat > TERMINAL_RECORD_TTL_MS) {
2244
2205
  store.delete(r.project, r.id);
2245
2206
  continue;
@@ -2302,6 +2263,27 @@ function createDaemon(deps) {
2302
2263
  continue;
2303
2264
  }
2304
2265
  }
2266
+ if (r.state === "blocked" && !r.operatorHold) {
2267
+ const threshold = deps.blockedRenudgeMs ?? DEFAULT_BLOCKED_RENUDGE_MS;
2268
+ const blockedFor = t - r.lastHeartbeat;
2269
+ const episodeKey = r.question ?? "";
2270
+ if (blockedFor > threshold && blockedRenudgedFor.get(r.id) !== episodeKey) {
2271
+ blockedRenudgedFor.set(r.id, episodeKey);
2272
+ if (deps.notify) {
2273
+ const tag = crewTag(r);
2274
+ const mins = Math.max(1, Math.round(blockedFor / 6e4));
2275
+ const message = `CREW BLOCKED REMINDER ${tag}: still blocked ~${mins}min (reminder \u2014 same question, not a new one): ${(r.question ?? "(no question)").trim()}`;
2276
+ const synthEvent = { type: "task.blocked", id: r.id, reason: "renudge", question: r.question ?? "" };
2277
+ try {
2278
+ const p = deps.notify({ project: r.project, message, record: r, event: synthEvent });
2279
+ if (p && typeof p.catch === "function")
2280
+ p.catch(() => {
2281
+ });
2282
+ } catch {
2283
+ }
2284
+ }
2285
+ }
2286
+ }
2305
2287
  if (r.mode === "interactive" && r.state === "submitted" && !r.firstTurnConfirmedAt) {
2306
2288
  const undeliveredMs = t - r.createdAt;
2307
2289
  if (undeliveredMs > firstTurnUndeliveredBudgetMs) {
@@ -2391,7 +2373,7 @@ function createDaemon(deps) {
2391
2373
  }
2392
2374
  };
2393
2375
  }
2394
- 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;
2376
+ 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;
2395
2377
  var init_reduce = __esm({
2396
2378
  "packages/core/dist/daemon/reduce.js"() {
2397
2379
  init_dist();
@@ -2399,6 +2381,7 @@ var init_reduce = __esm({
2399
2381
  init_watchdog();
2400
2382
  DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS = 12e4;
2401
2383
  DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS = 6e4;
2384
+ DEFAULT_BLOCKED_RENUDGE_MS = 30 * 60 * 1e3;
2402
2385
  DEFAULT_TASK_TIMEOUT_MS = 8 * 60 * 60 * 1e3;
2403
2386
  TERMINAL_RECORD_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
2404
2387
  TERMINAL_RECORD_KEEP_PER_PROJECT = 20;
@@ -4529,8 +4512,11 @@ function buildRouterEnv(creds, model) {
4529
4512
  if (headers)
4530
4513
  env.ANTHROPIC_CUSTOM_HEADERS = headers;
4531
4514
  }
4532
- if (model)
4515
+ if (model) {
4533
4516
  env.ANTHROPIC_MODEL = model;
4517
+ env.CLAUDE_CODE_SUBAGENT_MODEL = model;
4518
+ env.ANTHROPIC_SMALL_FAST_MODEL = model;
4519
+ }
4534
4520
  return env;
4535
4521
  }
4536
4522
  function mergeClaudeEnvRouterSettings(routerEnv, claudeEnv) {
@@ -5413,8 +5399,10 @@ async function resolveAndPersistOpencodeCaptain(opts) {
5413
5399
  sleep: opts.sleep,
5414
5400
  fetchImpl: opts.fetchImpl
5415
5401
  });
5416
- if (!sessionId)
5402
+ if (!sessionId) {
5403
+ 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`);
5417
5404
  return null;
5405
+ }
5418
5406
  writeCaptainAddress(opts.stateRoot, opts.project, {
5419
5407
  agent: "opencode",
5420
5408
  port: opts.port,
@@ -5434,6 +5422,33 @@ var init_opencode_session = __esm({
5434
5422
  function discoverCaptainSurface(surfaces, captainTitle) {
5435
5423
  return surfaces.find((s) => s.title === captainTitle) ?? null;
5436
5424
  }
5425
+ async function reresolveOpencodeAddress(rec) {
5426
+ const live = discoverLiveOpencodeServer({ directory: rec.directory, sessionId: rec.sessionId });
5427
+ const port = live?.port ?? rec.port;
5428
+ if (port === void 0)
5429
+ return null;
5430
+ const newest = newestSessionInDirectory(await listSessions(port), rec.directory, Date.parse(rec.launchedAt));
5431
+ const sessionId = newest ?? live?.sessionId ?? rec.sessionId;
5432
+ if (!sessionId || port === rec.port && sessionId === rec.sessionId)
5433
+ return null;
5434
+ return { port, sessionId };
5435
+ }
5436
+ async function healStaleOpencodeAddress(stateRoot, project, log, logPrefix) {
5437
+ const rec = readCaptainAddress(stateRoot, project);
5438
+ if (!rec)
5439
+ return false;
5440
+ const resolved = await reresolveOpencodeAddress(rec);
5441
+ if (!resolved)
5442
+ return false;
5443
+ const current = readCaptainAddress(stateRoot, project);
5444
+ if (!current || current.launchedAt !== rec.launchedAt) {
5445
+ log(`captain-channel ${project}: skipped stale-address heal \u2014 a newer launch record won the race`);
5446
+ return false;
5447
+ }
5448
+ writeCaptainAddress(stateRoot, project, { ...current, port: resolved.port, sessionId: resolved.sessionId });
5449
+ log(`captain-channel ${project}: ${logPrefix} \u2192 port ${resolved.port}`);
5450
+ return true;
5451
+ }
5437
5452
  async function reapOrphanedCrews(store, project, isSurfaceAlive) {
5438
5453
  let reaped = 0;
5439
5454
  for (const r of store.list(project)) {
@@ -5658,27 +5673,7 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
5658
5673
  log(`captain-channel ${project}: threw, falling back to pane \u2014 ${e.message}`);
5659
5674
  }
5660
5675
  if (agent === "opencode" && mode !== "off") {
5661
- const rec = readCaptainAddress(stateRoot, project);
5662
- let healed = false;
5663
- if (rec?.port) {
5664
- const fresh = newestSessionInDirectory(await listSessions(rec.port), rec.directory, Date.parse(rec.launchedAt));
5665
- if (fresh && fresh !== rec.sessionId) {
5666
- writeCaptainAddress(stateRoot, project, { ...rec, sessionId: fresh });
5667
- healed = true;
5668
- }
5669
- }
5670
- if (!healed && rec) {
5671
- const live = discoverLiveOpencodeServer({ directory: rec.directory, sessionId: rec.sessionId });
5672
- if (live && (live.port !== rec.port || live.sessionId && live.sessionId !== rec.sessionId)) {
5673
- writeCaptainAddress(stateRoot, project, {
5674
- ...rec,
5675
- port: live.port,
5676
- sessionId: live.sessionId ?? rec.sessionId
5677
- });
5678
- log(`captain-channel ${project}: re-resolved opencode address \u2192 port ${live.port}`);
5679
- healed = true;
5680
- }
5681
- }
5676
+ const healed = await healStaleOpencodeAddress(stateRoot, project, log, "re-resolved opencode address");
5682
5677
  if (healed) {
5683
5678
  try {
5684
5679
  const retry = await deliverToCaptain(project, text, { channel: picked, mode, log });
@@ -6536,11 +6531,15 @@ function parseCommand(text) {
6536
6531
  }
6537
6532
  return entry.build(args);
6538
6533
  }
6539
- var WRITABLE_CONFIG_KEYS, EFFORT_MODES, REGISTRY;
6534
+ var WRITABLE_CONFIG_KEYS, EFFORT_MODES, GATE_MODES2, CONFIG_VALUE_VALIDATORS, REGISTRY;
6540
6535
  var init_commands = __esm({
6541
6536
  "packages/core/dist/telegram/commands.js"() {
6542
- WRITABLE_CONFIG_KEYS = ["defaults.effort"];
6537
+ WRITABLE_CONFIG_KEYS = ["defaults.effort", "defaults.gate.mode"];
6543
6538
  EFFORT_MODES = /* @__PURE__ */ new Set(["max", "balance", "low"]);
6539
+ GATE_MODES2 = /* @__PURE__ */ new Set(["on", "off", "auto"]);
6540
+ CONFIG_VALUE_VALIDATORS = {
6541
+ "defaults.gate.mode": { valid: GATE_MODES2, usage: "usage: /config set defaults.gate.mode <on|off|auto>" }
6542
+ };
6544
6543
  REGISTRY = {
6545
6544
  status: { usage: "/status", build: () => ok("status", ["status"]) },
6546
6545
  projects: { usage: "/projects", build: () => ok("projects", ["projects", "list"]) },
@@ -6587,6 +6586,10 @@ var init_commands = __esm({
6587
6586
  message: `\u26D4 '${key}' is not writable over Telegram. Allowed: ${WRITABLE_CONFIG_KEYS.join(", ")}`
6588
6587
  };
6589
6588
  }
6589
+ const validator = CONFIG_VALUE_VALIDATORS[key];
6590
+ if (validator && !validator.valid.has(value)) {
6591
+ return usage("config", validator.usage);
6592
+ }
6590
6593
  return ok("config", ["config", "set", key, value]);
6591
6594
  }
6592
6595
  return usage("config", "usage: /config get <key> | /config set <key> <value>");
@@ -8693,6 +8696,12 @@ function resolveGateMode(env, gate) {
8693
8696
  return fromEnv;
8694
8697
  return gate?.mode && isGateMode(gate.mode) ? gate.mode : "auto";
8695
8698
  }
8699
+ function resolveGateEngine(env, gate) {
8700
+ const fromEnv = env.SQUADRANT_GATE_ENGINE;
8701
+ if (fromEnv !== void 0 && isGateEngine(fromEnv))
8702
+ return fromEnv;
8703
+ return gate?.engine && isGateEngine(gate.engine) ? gate.engine : "router";
8704
+ }
8696
8705
  function resolveGatePolicy(env, gate) {
8697
8706
  const fromEnv = env.SQUADRANT_GATE_POLICY;
8698
8707
  if (fromEnv !== void 0 && isGatePolicy(fromEnv))
@@ -9077,6 +9086,62 @@ var init_permission_gate = __esm({
9077
9086
  }
9078
9087
  });
9079
9088
 
9089
+ // packages/core/dist/auto-gate.js
9090
+ import { createAutoGate, projectGateConfig, resolveApiKey } from "@squadrant-ai/auto-gate";
9091
+ function gateForProjection(config) {
9092
+ const gate = config.defaults.gate;
9093
+ if (!gate)
9094
+ return {};
9095
+ const out = {};
9096
+ if (gate.mode)
9097
+ out.mode = gate.mode;
9098
+ if (gate.tools)
9099
+ out.tools = gate.tools;
9100
+ if (gate.deny)
9101
+ out.deny = gate.deny;
9102
+ return out;
9103
+ }
9104
+ function hasAutoGateCredential(env = process.env, home) {
9105
+ return Boolean(resolveApiKey({ env, ...home !== void 0 ? { home } : {} }));
9106
+ }
9107
+ function createSquadrantAutoGate(deps) {
9108
+ const env = deps.env ?? process.env;
9109
+ const taskId = env.SQUADRANT_CREW_TASK_ID;
9110
+ const project = deps.project ?? env.SQUADRANT_CREW_PROJECT;
9111
+ const mode = resolveGateMode(env, deps.config.defaults.gate);
9112
+ const projected = projectGateConfig({ gate: { ...gateForProjection(deps.config), mode } });
9113
+ const enabled = mode === "on";
9114
+ const blockedSignal = async (ctx) => {
9115
+ if (!taskId || !project || !deps.sendBlocked)
9116
+ return;
9117
+ const event = {
9118
+ type: "task.blocked",
9119
+ id: taskId,
9120
+ reason: ctx.reason,
9121
+ question: `crew needs permission to run ${ctx.tool}`
9122
+ };
9123
+ await deps.sendBlocked(project, event);
9124
+ };
9125
+ const host = createAutoGate({
9126
+ config: projected,
9127
+ readIntent: (p) => readUserIntent(p) ?? void 0,
9128
+ blockedSignal,
9129
+ ...deps.decide ? { decide: deps.decide } : {},
9130
+ ...project ? { project } : {},
9131
+ env,
9132
+ log: deps.log
9133
+ });
9134
+ if (!enabled) {
9135
+ return { config: host.config, decideClaudeHookPayload: async () => void 0 };
9136
+ }
9137
+ return host;
9138
+ }
9139
+ var init_auto_gate = __esm({
9140
+ "packages/core/dist/auto-gate.js"() {
9141
+ init_permission_gate();
9142
+ }
9143
+ });
9144
+
9080
9145
  // packages/core/dist/group-dispatch.js
9081
9146
  import { randomUUID as randomUUID4 } from "crypto";
9082
9147
  import { createConnection as createConnection2 } from "net";
@@ -9483,10 +9548,11 @@ async function runCrewAnswer(project, name, option, runtime, workspaceId, deps,
9483
9548
  if (!crew) {
9484
9549
  throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
9485
9550
  }
9486
- const options = await deps.readModalOptions(crew);
9487
- if (!options) {
9551
+ const modal = await deps.readModalOptions(crew);
9552
+ if (!modal || modal.options.length === 0) {
9488
9553
  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.`);
9489
9554
  }
9555
+ const options = modal.options;
9490
9556
  const target = resolveOption(options, option);
9491
9557
  if (opts?.expect && !target.label.toLowerCase().includes(opts.expect.toLowerCase())) {
9492
9558
  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}'.
@@ -9498,7 +9564,9 @@ ${describeOptions(options)}`);
9498
9564
  log(`\u2192 selecting ${target.index}. "${target.label}"`);
9499
9565
  const current = options.find((o) => o.highlighted) ?? options[0];
9500
9566
  const steps = target.index - current.index;
9501
- const key = steps >= 0 ? "Down" : "Up";
9567
+ const forwardKey = modal.axis === "horizontal" ? "Right" : "Down";
9568
+ const backwardKey = modal.axis === "horizontal" ? "Left" : "Up";
9569
+ const key = steps >= 0 ? forwardKey : backwardKey;
9502
9570
  for (let i = 0; i < Math.abs(steps); i++) {
9503
9571
  await runtime.sendKeyToPane(crew, key);
9504
9572
  }
@@ -9870,6 +9938,7 @@ __export(dist_exports2, {
9870
9938
  CREW_GONE_MS: () => CREW_GONE_MS,
9871
9939
  CREW_STALE_MS: () => CREW_STALE_MS,
9872
9940
  CaptainDelivery: () => CaptainDelivery,
9941
+ DEFAULT_BLOCKED_RENUDGE_MS: () => DEFAULT_BLOCKED_RENUDGE_MS,
9873
9942
  DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS: () => DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS,
9874
9943
  DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS: () => DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS,
9875
9944
  DEFAULT_GATE_DENY_RULES: () => DEFAULT_GATE_DENY_RULES,
@@ -9952,6 +10021,7 @@ __export(dist_exports2, {
9952
10021
  createRouterService: () => createRouterService,
9953
10022
  createRouterShim: () => createRouterShim,
9954
10023
  createRunCommand: () => createRunCommand,
10024
+ createSquadrantAutoGate: () => createSquadrantAutoGate,
9955
10025
  createStore: () => createStore,
9956
10026
  createSurfaceLivenessProbe: () => createSurfaceLivenessProbe,
9957
10027
  createTelegramBridge: () => createTelegramBridge,
@@ -10008,6 +10078,7 @@ __export(dist_exports2, {
10008
10078
  freshTrace: () => freshTrace,
10009
10079
  gateCacheKey: () => gateCacheKey,
10010
10080
  getDaemonPid: () => getDaemonPid,
10081
+ hasAutoGateCredential: () => hasAutoGateCredential,
10011
10082
  hasModalOptionList: () => hasModalOptionList,
10012
10083
  healCmdFor: () => healCmdFor,
10013
10084
  inboundBody: () => inboundBody,
@@ -10093,6 +10164,7 @@ __export(dist_exports2, {
10093
10164
  resolveCurrentProject: () => resolveCurrentProject,
10094
10165
  resolveGate: () => resolveGate,
10095
10166
  resolveGateDenyRules: () => resolveGateDenyRules,
10167
+ resolveGateEngine: () => resolveGateEngine,
10096
10168
  resolveGateMode: () => resolveGateMode,
10097
10169
  resolveGatePolicy: () => resolveGatePolicy,
10098
10170
  resolveGateTools: () => resolveGateTools,
@@ -10198,6 +10270,7 @@ var init_dist2 = __esm({
10198
10270
  init_router_resolution();
10199
10271
  init_captain_router();
10200
10272
  init_permission_gate();
10273
+ init_auto_gate();
10201
10274
  init_restart_daemon();
10202
10275
  init_group_dispatch();
10203
10276
  init_launch_workspace();
@@ -13683,6 +13756,37 @@ function parseModalOptions(screen) {
13683
13756
  }
13684
13757
  return options.length > 0 ? options : null;
13685
13758
  }
13759
+ function parseOpencodePermissionOptions(screen) {
13760
+ if (!screen)
13761
+ return null;
13762
+ if (!/△\s*Permission required/.test(screen))
13763
+ return null;
13764
+ const lines = screen.split(/\r?\n/);
13765
+ const bar = lines.find((l) => /⇆\s*select/.test(l));
13766
+ if (!bar)
13767
+ return null;
13768
+ let region = bar.split("\u21C6")[0];
13769
+ const fullscreenIdx = region.toLowerCase().lastIndexOf("fullscreen");
13770
+ if (fullscreenIdx !== -1) {
13771
+ region = region.slice(0, fullscreenIdx).replace(/\s+\S+\s*$/, "");
13772
+ }
13773
+ region = region.replace(/^[\s┃│╭╮╰╯|]+/, "").trim();
13774
+ if (!region)
13775
+ return null;
13776
+ const labels = region.split(/\s{2,}/).map((s) => s.trim()).filter(Boolean);
13777
+ if (labels.length === 0)
13778
+ return null;
13779
+ return labels.map((label, i) => ({ index: i + 1, label, highlighted: i === 0 }));
13780
+ }
13781
+ function parseModal(screen) {
13782
+ const claude = parseModalOptions(screen);
13783
+ if (claude)
13784
+ return { options: claude, axis: "vertical" };
13785
+ const opencode = parseOpencodePermissionOptions(screen);
13786
+ if (opencode)
13787
+ return { options: opencode, axis: "horizontal" };
13788
+ return null;
13789
+ }
13686
13790
  function readInputBoxRaw(screen, opts) {
13687
13791
  if (!screen)
13688
13792
  return null;
@@ -14892,6 +14996,47 @@ function removeCommandHandlers(entries, command) {
14892
14996
  }
14893
14997
  return removed;
14894
14998
  }
14999
+ function isForeignAutoGateCommand(command) {
15000
+ if (typeof command !== "string")
15001
+ return false;
15002
+ if (command.includes(FOREIGN_AUTO_GATE_MARKER))
15003
+ return true;
15004
+ const first = command.trim().split(/\s+/)[0] ?? "";
15005
+ return /(^|[/\\])auto-gate$/.test(first);
15006
+ }
15007
+ function findForeignGateHandlers(entries) {
15008
+ const found = [];
15009
+ for (const entry of entries) {
15010
+ const hooks = entry?.hooks;
15011
+ if (!Array.isArray(hooks))
15012
+ continue;
15013
+ for (const h of hooks) {
15014
+ const cmd = h?.command;
15015
+ if (isForeignAutoGateCommand(cmd))
15016
+ found.push(cmd);
15017
+ }
15018
+ }
15019
+ return found;
15020
+ }
15021
+ function removeForeignGateHandlers(entries) {
15022
+ const removed = [];
15023
+ for (let i = entries.length - 1; i >= 0; i--) {
15024
+ const entry = entries[i];
15025
+ if (!Array.isArray(entry?.hooks))
15026
+ continue;
15027
+ entry.hooks = entry.hooks.filter((h) => {
15028
+ const cmd = h?.command;
15029
+ if (isForeignAutoGateCommand(cmd)) {
15030
+ removed.push(cmd);
15031
+ return false;
15032
+ }
15033
+ return true;
15034
+ });
15035
+ if (entry.hooks.length === 0)
15036
+ entries.splice(i, 1);
15037
+ }
15038
+ return removed;
15039
+ }
14895
15040
  function installClaudeHooks(opts = {}) {
14896
15041
  const settingsPath = opts.settingsPath ?? join25(homedir13(), ".claude", "settings.json");
14897
15042
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
@@ -14929,6 +15074,23 @@ function installClaudeHooks(opts = {}) {
14929
15074
  changed = true;
14930
15075
  migrated.push(`${eventName}/${sub}`);
14931
15076
  }
15077
+ const foreign = findForeignGateHandlers(entries);
15078
+ if (foreign.length > 0) {
15079
+ if (opts.gateMode === "on") {
15080
+ const removed = removeForeignGateHandlers(entries);
15081
+ if (removed.length > 0)
15082
+ changed = true;
15083
+ for (const command2 of removed) {
15084
+ log(`native-hook: removed foreign auto-gate PermissionRequest handler in ${settingsPath} [#P6 \xA715#5]: ${command2}`);
15085
+ opts.recordGateAction?.({ action: "removed-foreign-gate", command: command2 });
15086
+ }
15087
+ } else {
15088
+ for (const command2 of foreign) {
15089
+ 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}`);
15090
+ opts.recordGateAction?.({ action: "left-foreign-gate", command: command2 });
15091
+ }
15092
+ }
15093
+ }
14932
15094
  }
14933
15095
  const alreadyPresent = entries.some((m) => Array.isArray(m.hooks) && m.hooks.some((h) => typeof h.command === "string" && h.command === command));
14934
15096
  if (!alreadyPresent) {
@@ -14959,11 +15121,55 @@ function installClaudeHooks(opts = {}) {
14959
15121
  changed = true;
14960
15122
  }
14961
15123
  }
15124
+ if (typeof settings.env === "object" && settings.env !== null && !Array.isArray(settings.env)) {
15125
+ if (reconcileSubagentModels(settings.env, log))
15126
+ changed = true;
15127
+ }
14962
15128
  if (changed) {
14963
15129
  writeFile6(settingsPath, JSON.stringify(settings, null, 2));
14964
15130
  }
14965
15131
  return settingsPath;
14966
15132
  }
15133
+ function isCustomUpstream(baseUrl) {
15134
+ if (typeof baseUrl !== "string" || baseUrl.length === 0)
15135
+ return false;
15136
+ try {
15137
+ return new URL(baseUrl).hostname.toLowerCase() !== "api.anthropic.com";
15138
+ } catch {
15139
+ return false;
15140
+ }
15141
+ }
15142
+ function isAnthropicModelId(value) {
15143
+ if (typeof value !== "string")
15144
+ return false;
15145
+ const v2 = value.trim().toLowerCase();
15146
+ return v2 === "sonnet" || v2 === "opus" || v2 === "haiku" || v2.startsWith("claude-");
15147
+ }
15148
+ function reconcileSubagentModels(env, log) {
15149
+ if (!isCustomUpstream(env.ANTHROPIC_BASE_URL))
15150
+ return false;
15151
+ const model = env.ANTHROPIC_MODEL;
15152
+ if (typeof model !== "string" || model.length === 0)
15153
+ return false;
15154
+ let changed = false;
15155
+ for (const key of [SUBAGENT_MODEL_KEY, SMALL_FAST_MODEL_KEY]) {
15156
+ const current = env[key];
15157
+ if (current === void 0) {
15158
+ env[key] = model;
15159
+ changed = true;
15160
+ log(`native-hook: set ${key}='${model}' to match ANTHROPIC_MODEL on a custom upstream`);
15161
+ } else if (isAnthropicModelId(current)) {
15162
+ if (current !== model) {
15163
+ env[key] = model;
15164
+ changed = true;
15165
+ log(`native-hook: aligned ${key} '${String(current)}' \u2192 '${model}' for a custom upstream`);
15166
+ }
15167
+ } else {
15168
+ log(`native-hook: ${key} '${String(current)}' is a custom model id \u2014 left untouched`);
15169
+ }
15170
+ }
15171
+ return changed;
15172
+ }
14967
15173
  function mapSubToLifecycle(sub) {
14968
15174
  switch (sub) {
14969
15175
  case "session-start":
@@ -15013,7 +15219,7 @@ function defaultWriteFile(path39, content) {
15013
15219
  mkdirSync8(path39.replace(/\/[^/]+$/, ""), { recursive: true });
15014
15220
  writeFileSync11(path39, content, "utf-8");
15015
15221
  }
15016
- var CLAUDE_HOOK_EVENTS, DEFAULT_HOOK_CMD, GATE_HOOK_CMD, NativeHookSource;
15222
+ var CLAUDE_HOOK_EVENTS, DEFAULT_HOOK_CMD, GATE_HOOK_CMD, FOREIGN_AUTO_GATE_MARKER, SUBAGENT_MODEL_KEY, SMALL_FAST_MODEL_KEY, NativeHookSource;
15017
15223
  var init_native_hook_source = __esm({
15018
15224
  "packages/workspaces/dist/native-hooks/native-hook-source.js"() {
15019
15225
  CLAUDE_HOOK_EVENTS = [
@@ -15033,6 +15239,9 @@ var init_native_hook_source = __esm({
15033
15239
  ];
15034
15240
  DEFAULT_HOOK_CMD = "squadrant hooks";
15035
15241
  GATE_HOOK_CMD = "squadrant gate";
15242
+ FOREIGN_AUTO_GATE_MARKER = "auto-gate-managed";
15243
+ SUBAGENT_MODEL_KEY = "CLAUDE_CODE_SUBAGENT_MODEL";
15244
+ SMALL_FAST_MODEL_KEY = "ANTHROPIC_SMALL_FAST_MODEL";
15036
15245
  NativeHookSource = class {
15037
15246
  name = "native-hook";
15038
15247
  hookInstall;
@@ -15166,7 +15375,7 @@ async function paneHasOpenModal(runtime, pane) {
15166
15375
  }
15167
15376
  async function readModalOptions(runtime, pane) {
15168
15377
  const screen = await runtime.readPaneScreen(pane) ?? "";
15169
- return parseModalOptions(screen);
15378
+ return parseModal(screen);
15170
15379
  }
15171
15380
  async function confirmedSendToPane(runtime, pane, message) {
15172
15381
  const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
@@ -16217,27 +16426,6 @@ function promptLine(question) {
16217
16426
  });
16218
16427
  });
16219
16428
  }
16220
- async function resolveRouter(config, opts, isTTY) {
16221
- if (config.defaults?.router) return config.defaults.router;
16222
- let kind = opts.routerKind;
16223
- let baseUrl = opts.routerBaseUrl;
16224
- let apiKeyEnv = opts.routerApiKeyEnv;
16225
- if (isTTY && (!kind || !baseUrl)) {
16226
- const k = await promptLine(chalk4.cyan(` Router kind [${DEFAULT_ROUTER_KIND}]: `));
16227
- kind = kind ?? (k || DEFAULT_ROUTER_KIND);
16228
- const b = await promptLine(chalk4.cyan(` Router base URL [${DEFAULT_ROUTER_BASE_URL}]: `));
16229
- baseUrl = baseUrl ?? (b || DEFAULT_ROUTER_BASE_URL);
16230
- const e = await promptLine(chalk4.cyan(" Env var holding the API key (optional): "));
16231
- apiKeyEnv = apiKeyEnv ?? (e || void 0);
16232
- }
16233
- const resolvedKind = kind && isRouterKind(kind) ? kind : DEFAULT_ROUTER_KIND;
16234
- const resolvedBase = baseUrl || DEFAULT_ROUTER_BASE_URL;
16235
- if (!isTTY && !opts.routerBaseUrl) {
16236
- console.log(chalk4.dim(` - no router flags given; defaulting to ${resolvedKind} (${resolvedBase})`));
16237
- console.log(chalk4.dim(" set a credential: squadrant config set defaults.router.apiKeyEnv <VAR>"));
16238
- }
16239
- return { kind: resolvedKind, baseUrl: resolvedBase, ...apiKeyEnv ? { apiKeyEnv } : {} };
16240
- }
16241
16429
  async function askProvider(current, auth) {
16242
16430
  console.log(chalk4.bold("\n Choose your provider:"));
16243
16431
  for (const p of PROVIDER_PRESETS) {
@@ -16246,10 +16434,10 @@ async function askProvider(current, auth) {
16246
16434
  console.log(chalk4.dim(` ${p.summary}`));
16247
16435
  }
16248
16436
  if (!auth.authenticated) {
16249
- console.log(chalk4.yellow("\n \u26A0 No Anthropic credential detected \u2014 preset a will not work; consider b or c."));
16437
+ console.log(chalk4.yellow("\n \u26A0 No Anthropic credential detected \u2014 preset a will not work; consider b."));
16250
16438
  }
16251
16439
  const answer = (await promptLine(chalk4.cyan(`
16252
- Provider [a/b/c/d] (default ${current}): `))).toLowerCase();
16440
+ Provider [a/b/c] (default ${current}): `))).toLowerCase();
16253
16441
  if (!answer) return current;
16254
16442
  if (isProviderPresetId(answer)) return answer;
16255
16443
  console.log(chalk4.yellow(` \u26A0 Unknown choice '${answer}' \u2014 keeping '${current}'.`));
@@ -16258,9 +16446,9 @@ async function askProvider(current, auth) {
16258
16446
  function warnIfNoAnthropic(preset, auth) {
16259
16447
  if (preset !== "a" || auth.authenticated) return;
16260
16448
  console.log(chalk4.yellow(" \u26A0 No Anthropic credential detected (`claude auth status`)."));
16261
- console.log(chalk4.dim(" Preset A (Claude Pro/Max or an API key) may not work \u2014 consider preset b or c."));
16449
+ console.log(chalk4.dim(" Preset A (Claude Pro/Max or an API key) may not work \u2014 consider preset b."));
16262
16450
  }
16263
- var initCommand = new Command2("init").description("Guided first-time setup: provider, hub vault, agents, plugins, projects (re-run-safe)").option("--hub <path>", "Hub vault path", "~/squadrant-hub").option("--preset <id>", "provider preset: a|b|c|d (non-interactive)").option("--router-kind <kind>", "router kind for --preset c (opencode-go|openrouter|ccr|litellm|custom)").option("--router-base-url <url>", "router base URL for --preset c").option("--router-api-key-env <env>", "env var holding the router credential for --preset c").action(async (opts) => {
16451
+ var initCommand = new Command2("init").description("Guided first-time setup: provider, hub vault, agents, plugins, projects (re-run-safe)").option("--hub <path>", "Hub vault path", "~/squadrant-hub").option("--preset <id>", "provider preset: a|b|c (non-interactive; d accepted as a deprecated alias for c)").action(async (opts) => {
16264
16452
  const hubPath = resolveHome(opts.hub);
16265
16453
  const pkgRoot = findPackageRoot();
16266
16454
  const configDir = path23.join(os13.homedir(), ".config", "squadrant");
@@ -16268,9 +16456,9 @@ var initCommand = new Command2("init").description("Guided first-time setup: pro
16268
16456
  console.log(chalk4.bold("\nSquadrant Init\n"));
16269
16457
  let explicitPreset;
16270
16458
  if (opts.preset !== void 0) {
16271
- const candidate = opts.preset.toLowerCase();
16272
- if (!isProviderPresetId(candidate)) {
16273
- console.error(chalk4.red(` \u2718 Unknown --preset '${opts.preset}'. Valid values: a, b, c, d`));
16459
+ const candidate = normalizePresetId(opts.preset.toLowerCase());
16460
+ if (!candidate) {
16461
+ console.error(chalk4.red(` \u2718 Unknown --preset '${opts.preset}'. Valid values: a, b, c`));
16274
16462
  process.exitCode = 1;
16275
16463
  return;
16276
16464
  }
@@ -16280,7 +16468,7 @@ var initCommand = new Command2("init").description("Guided first-time setup: pro
16280
16468
  console.log(" Run these steps to get started:\n");
16281
16469
  console.log(chalk4.bold(" 1/5 Hub vault + provider"));
16282
16470
  console.log(chalk4.cyan(` squadrant init --hub ${opts.hub}`));
16283
- console.log(chalk4.dim(" Choose a provider: squadrant init --preset a|b|c|d"));
16471
+ console.log(chalk4.dim(" Choose a provider: squadrant init --preset a|b|c"));
16284
16472
  console.log(chalk4.bold("\n 2/5 Agent + projection setup"));
16285
16473
  console.log(" (handled automatically by: " + chalk4.cyan("squadrant init") + ")");
16286
16474
  console.log(chalk4.bold("\n 3/5 Plugins \u2014 open Claude Code and run:"));
@@ -16326,9 +16514,8 @@ var initCommand = new Command2("init").description("Guided first-time setup: pro
16326
16514
  let appliedChanges = [];
16327
16515
  if (chosenPreset) {
16328
16516
  const label = PROVIDER_PRESETS.find((p) => p.id === chosenPreset)?.label ?? chosenPreset;
16329
- const router = chosenPreset === "c" ? await resolveRouter(config, opts, isTTY) : void 0;
16330
16517
  const overwrite = !configExists || explicitPreset !== void 0;
16331
- const result = applyProviderPreset(config, chosenPreset, { router, overwrite });
16518
+ const result = applyProviderPreset(config, chosenPreset, { overwrite });
16332
16519
  appliedChanges = result.changes;
16333
16520
  config = result.config;
16334
16521
  console.log(chalk4.bold(`
@@ -19938,7 +20125,7 @@ var launchCommand = new Command14("launch").description(
19938
20125
  sessionId: priorRecord?.sessionId
19939
20126
  });
19940
20127
  if (!live) return;
19941
- const sessionId = live.sessionId ?? priorRecord?.sessionId;
20128
+ const sessionId = live.sessionId ?? (live.port === priorRecord?.port ? priorRecord?.sessionId : void 0);
19942
20129
  if (priorRecord && priorRecord.port === live.port && priorRecord.sessionId === sessionId) return;
19943
20130
  writeCaptainAddress(stateRoot, projectName, {
19944
20131
  agent: "opencode",
@@ -19952,13 +20139,20 @@ var launchCommand = new Command14("launch").description(
19952
20139
  console.log(chalk15.green(` \u2714 Workspace '${name}' created`));
19953
20140
  if (!projectName) return;
19954
20141
  if (isOpencodeCaptain && captainPort) {
20142
+ writeCaptainAddress(stateRoot, projectName, {
20143
+ agent: "opencode",
20144
+ port: captainPort,
20145
+ directory: realpathOrSelf(cwd),
20146
+ launchedAt
20147
+ });
19955
20148
  void resolveAndPersistOpencodeCaptain({
19956
20149
  stateRoot,
19957
20150
  project: projectName,
19958
20151
  port: captainPort,
19959
20152
  directory: realpathOrSelf(cwd),
19960
20153
  launchedAt,
19961
- sessionId: opencodeResumeSessionId
20154
+ sessionId: opencodeResumeSessionId,
20155
+ log: (m) => console.error(chalk15.yellow(` \u26A0 ${m}`))
19962
20156
  }).catch(() => {
19963
20157
  });
19964
20158
  } else if (role === "captain") {
@@ -22348,7 +22542,32 @@ init_dist2();
22348
22542
  init_dist();
22349
22543
  init_dist3();
22350
22544
  import { Command as Command32 } from "commander";
22545
+ import chalk33 from "chalk";
22351
22546
  var SOCK4 = DAEMON_SOCK_PATH;
22547
+ async function runAutoGateClaudeHook(o) {
22548
+ const make = o.deps.createAutoGate ?? createSquadrantAutoGate;
22549
+ const autoGate = make({
22550
+ config: o.config,
22551
+ env: o.env,
22552
+ ...o.project ? { project: o.project } : {},
22553
+ ...o.deps.sendEvent ? { sendBlocked: o.deps.sendEvent } : {},
22554
+ log: o.log
22555
+ });
22556
+ const out = await autoGate.decideClaudeHookPayload(o.deps.rawPayload ?? "");
22557
+ if (out === void 0) {
22558
+ o.log("gate auto-gate: ask/yield \u2014 normal dialog");
22559
+ return { decision: "ask", tier: 2, reason: "auto-gate: ask/yield" };
22560
+ }
22561
+ o.stdout(out);
22562
+ let decision = "allow";
22563
+ try {
22564
+ const parsed = JSON.parse(out);
22565
+ if (parsed?.hookSpecificOutput?.decision?.behavior === "deny") decision = "deny";
22566
+ } catch {
22567
+ }
22568
+ o.log(`gate auto-gate ${decision}`);
22569
+ return { decision, tier: 2, reason: "auto-gate" };
22570
+ }
22352
22571
  async function runGatePermissionRequest(deps) {
22353
22572
  const env = deps.env ?? process.env;
22354
22573
  const payload = deps.payload ?? {};
@@ -22363,6 +22582,10 @@ async function runGatePermissionRequest(deps) {
22363
22582
  const log = deps.log ?? ((m) => process.stderr.write(`[squadrant] ${m}
22364
22583
  `));
22365
22584
  const config = deps.config ?? (deps.loadConfigFn ?? loadConfig)();
22585
+ const gate = config.defaults.gate;
22586
+ if (resolveGateMode(env, gate) === "on" && resolveGateEngine(env, gate) === "auto-gate") {
22587
+ return runAutoGateClaudeHook({ deps, env, config, project, stdout, log });
22588
+ }
22366
22589
  const cwd = deps.cwd ?? (typeof p.cwd === "string" && p.cwd ? p.cwd : env.SQUADRANT_CREW_CWD ?? process.cwd());
22367
22590
  const transcriptPath = typeof p.transcript_path === "string" && p.transcript_path ? p.transcript_path : deriveTranscriptPath(
22368
22591
  typeof p.session_id === "string" ? p.session_id : "",
@@ -22404,10 +22627,60 @@ async function runGatePermissionRequest(deps) {
22404
22627
  return evaluation;
22405
22628
  }
22406
22629
  var DEFAULT_EVENT = "permission-request";
22630
+ function runGateModeGet(configPath = DEFAULT_CONFIG_PATH, env = process.env) {
22631
+ const config = loadConfig(configPath);
22632
+ const mode = resolveGateMode(env, config.defaults.gate);
22633
+ const source = env.SQUADRANT_GATE !== void 0 && isGateMode(env.SQUADRANT_GATE) ? "env" : config.defaults.gate?.mode && isGateMode(config.defaults.gate.mode) ? "config" : "default";
22634
+ return { mode, source };
22635
+ }
22636
+ function runGateModeSet(value, configPath = DEFAULT_CONFIG_PATH) {
22637
+ if (!isGateMode(value)) {
22638
+ throw new Error(`Invalid gate mode '${value}'. Valid values: ${GATE_MODES.join(" | ")}`);
22639
+ }
22640
+ const config = loadConfig(configPath);
22641
+ const old = config.defaults.gate?.mode && isGateMode(config.defaults.gate.mode) ? config.defaults.gate.mode : "auto";
22642
+ config.defaults.gate = { ...config.defaults.gate, mode: value };
22643
+ saveConfig(config, configPath);
22644
+ return { old, next: value };
22645
+ }
22646
+ function runGateStatus(configPath = DEFAULT_CONFIG_PATH, env = process.env) {
22647
+ const config = loadConfig(configPath);
22648
+ const { mode, source } = runGateModeGet(configPath, env);
22649
+ const engine = resolveGateEngine(env, config.defaults.gate);
22650
+ const model = resolveClassifierModel(env, config);
22651
+ const credentialPresent = engine === "auto-gate" ? hasAutoGateCredential(env) : Boolean(config.defaults.router?.apiKey ?? (config.defaults.router?.apiKeyEnv ? env[config.defaults.router.apiKeyEnv] : void 0));
22652
+ return { mode, source, engine, model, credentialPresent };
22653
+ }
22407
22654
  function gateCommand() {
22408
22655
  const gate = new Command32("gate").description(
22409
- "(internal) hook-based permission gate for router-backed crew/side sessions (#782)"
22656
+ "Permission gate (#782/#854): get/set defaults.gate.mode; 'claude <event>' is an internal hook handler"
22410
22657
  );
22658
+ gate.command("mode [value]").description("Get or set the permission gate mode (on | off | auto)").action((value) => {
22659
+ if (value === void 0) {
22660
+ const result2 = runGateModeGet();
22661
+ console.log(chalk33.bold("Gate mode:"), chalk33.cyan(result2.mode), chalk33.dim(`(${result2.source})`));
22662
+ return;
22663
+ }
22664
+ let result;
22665
+ try {
22666
+ result = runGateModeSet(value);
22667
+ } catch (e) {
22668
+ console.error(chalk33.red(e.message));
22669
+ process.exit(1);
22670
+ }
22671
+ console.log(chalk33.green(`\u2714 gate mode: ${result.old} \u2192 ${result.next}`));
22672
+ console.log(chalk33.dim("No daemon bounce needed \u2014 defaults.gate.mode is read live."));
22673
+ if (isGateMode(process.env.SQUADRANT_GATE ?? "") && process.env.SQUADRANT_GATE !== result.next) {
22674
+ console.log(chalk33.yellow(`Note: SQUADRANT_GATE=${process.env.SQUADRANT_GATE} in this session still overrides the config value.`));
22675
+ }
22676
+ });
22677
+ gate.command("status").description("Show the effective gate mode, engine, classifier model, and credential presence").action(() => {
22678
+ const result = runGateStatus();
22679
+ console.log(chalk33.bold("Gate mode:"), chalk33.cyan(result.mode), chalk33.dim(`(${result.source})`));
22680
+ console.log(chalk33.bold("Engine:"), result.engine);
22681
+ console.log(chalk33.bold("Classifier model:"), result.model ?? chalk33.dim("(unset)"));
22682
+ console.log(chalk33.bold("Credential:"), result.credentialPresent ? chalk33.green("present") : chalk33.yellow("absent"));
22683
+ });
22411
22684
  gate.command("claude <event>", { hidden: true }).description("internal: classify a claude PermissionRequest with the configured router model").action(async (event) => {
22412
22685
  let stdin = "";
22413
22686
  try {
@@ -22426,6 +22699,7 @@ function gateCommand() {
22426
22699
  }
22427
22700
  await runGatePermissionRequest({
22428
22701
  payload,
22702
+ rawPayload: stdin,
22429
22703
  env: process.env,
22430
22704
  stdout: (s) => process.stdout.write(s),
22431
22705
  sendEvent: async (project, ev) => {
@@ -22508,6 +22782,7 @@ function hooksCommand() {
22508
22782
  if (sub === "permission-request") {
22509
22783
  await runGatePermissionRequest({
22510
22784
  payload,
22785
+ rawPayload: stdin,
22511
22786
  env: process.env,
22512
22787
  stdout: (s) => process.stdout.write(s),
22513
22788
  sendEvent: async (project2, ev2) => {
@@ -22559,7 +22834,7 @@ init_dist();
22559
22834
  init_dist2();
22560
22835
  import path34 from "path";
22561
22836
  import { Command as Command34 } from "commander";
22562
- import chalk33 from "chalk";
22837
+ import chalk34 from "chalk";
22563
22838
  function detectCurrentProject(config, cwd = process.cwd()) {
22564
22839
  for (const [name, proj] of Object.entries(config.projects)) {
22565
22840
  const projPath = resolveHome(proj.path);
@@ -22599,21 +22874,21 @@ function visibleItems(items, includeDone) {
22599
22874
  function stateColor(state) {
22600
22875
  switch (state) {
22601
22876
  case "done":
22602
- return chalk33.green;
22877
+ return chalk34.green;
22603
22878
  case "cancelled":
22604
- return chalk33.dim;
22879
+ return chalk34.dim;
22605
22880
  case "blocked":
22606
- return chalk33.red;
22881
+ return chalk34.red;
22607
22882
  case "paused":
22608
- return chalk33.yellow;
22883
+ return chalk34.yellow;
22609
22884
  default:
22610
- return chalk33.cyan;
22885
+ return chalk34.cyan;
22611
22886
  }
22612
22887
  }
22613
22888
  function printItem(item, indent) {
22614
22889
  const color = stateColor(item.state);
22615
- const line = " ".repeat(indent) + `${chalk33.dim(item.id)} ${item.title} ${color(`[${item.state}]`)}` + (indent === 0 ? chalk33.dim(` (${item.project})`) : "");
22616
- console.log(TERMINAL_WORK_STATES.has(item.state) ? chalk33.dim(line) : line);
22890
+ const line = " ".repeat(indent) + `${chalk34.dim(item.id)} ${item.title} ${color(`[${item.state}]`)}` + (indent === 0 ? chalk34.dim(` (${item.project})`) : "");
22891
+ console.log(TERMINAL_WORK_STATES.has(item.state) ? chalk34.dim(line) : line);
22617
22892
  }
22618
22893
  function printTree(items) {
22619
22894
  const { roots, childrenOf } = groupByParent(items);
@@ -22632,15 +22907,15 @@ var startCmd = new Command34("start").description("Start a new work item").argum
22632
22907
  purgeExpiredWorkItems(store);
22633
22908
  const project = opts.project ?? detectCurrentProject(config);
22634
22909
  if (!project) {
22635
- console.error(chalk33.red("No --project given and cwd is not inside a registered project."));
22910
+ console.error(chalk34.red("No --project given and cwd is not inside a registered project."));
22636
22911
  process.exit(1);
22637
22912
  }
22638
22913
  if (opts.parent && !findWorkItemById(store, opts.parent)) {
22639
- console.error(chalk33.red(`Parent work item '${opts.parent}' not found.`));
22914
+ console.error(chalk34.red(`Parent work item '${opts.parent}' not found.`));
22640
22915
  process.exit(1);
22641
22916
  }
22642
22917
  const item = createWorkItem(store, { project, title, parent: opts.parent ?? null, tags: opts.tag });
22643
- console.log(chalk33.green(`\u2713 ${item.id}`) + ` ${item.title}` + chalk33.dim(` (${item.project})`));
22918
+ console.log(chalk34.green(`\u2713 ${item.id}`) + ` ${item.title}` + chalk34.dim(` (${item.project})`));
22644
22919
  });
22645
22920
  var listCmd2 = new Command34("list").description("List work items").option("--project <name>", "scope to one project").option("--all", "list across every project").option("--tree", "render parent/child nesting").option("--include-done", "include done/cancelled items").action((opts) => {
22646
22921
  const config = loadConfig();
@@ -22654,14 +22929,14 @@ var listCmd2 = new Command34("list").description("List work items").option("--pr
22654
22929
  } else {
22655
22930
  const project = detectCurrentProject(config);
22656
22931
  if (!project) {
22657
- console.error(chalk33.red("cwd is not inside a registered project \u2014 pass --project or --all."));
22932
+ console.error(chalk34.red("cwd is not inside a registered project \u2014 pass --project or --all."));
22658
22933
  process.exit(1);
22659
22934
  }
22660
22935
  items = store.list(project);
22661
22936
  }
22662
22937
  items = visibleItems(items, opts.includeDone ?? false);
22663
22938
  if (items.length === 0) {
22664
- console.log(chalk33.dim("\nNo work items.\n"));
22939
+ console.log(chalk34.dim("\nNo work items.\n"));
22665
22940
  return;
22666
22941
  }
22667
22942
  console.log();
@@ -22675,15 +22950,15 @@ function closeCommand(name, state, flag, key, desc) {
22675
22950
  purgeExpiredWorkItems(store);
22676
22951
  const item = closeWorkItem(store, id, state, { note: opts[key] });
22677
22952
  if (!item) {
22678
- console.error(chalk33.red(`Work item '${id}' not found.`));
22953
+ console.error(chalk34.red(`Work item '${id}' not found.`));
22679
22954
  process.exit(1);
22680
22955
  }
22681
- console.log(chalk33.green(`\u2713 ${item.id}`) + ` ${item.title} ${chalk33.dim(`[${item.state}]`)}`);
22956
+ console.log(chalk34.green(`\u2713 ${item.id}`) + ` ${item.title} ${chalk34.dim(`[${item.state}]`)}`);
22682
22957
  if (state === "done") {
22683
22958
  const openChildren = findOpenChildren(store, item.id);
22684
22959
  if (openChildren.length > 0) {
22685
22960
  const names = openChildren.map((c) => `${c.id} [${c.state}]`).join(", ");
22686
- console.log(chalk33.yellow(`\u26A0 still has ${openChildren.length} unfinished child item(s): ${names}`));
22961
+ console.log(chalk34.yellow(`\u26A0 still has ${openChildren.length} unfinished child item(s): ${names}`));
22687
22962
  }
22688
22963
  }
22689
22964
  });
@@ -23166,7 +23441,7 @@ handoffCommand.command("facts <project>").description(
23166
23441
  init_dist();
23167
23442
  init_dist3();
23168
23443
  import { Command as Command36 } from "commander";
23169
- import chalk34 from "chalk";
23444
+ import chalk35 from "chalk";
23170
23445
  var KNOWN_AGENTS = ["claude", "opencode", "codex", "gemini"];
23171
23446
  function normalized(p) {
23172
23447
  return p.replace(/\/+$/, "");
@@ -23219,7 +23494,7 @@ async function collectSessions(drivers) {
23219
23494
  }
23220
23495
  var sessionsCommand = new Command36("sessions").description("List live agent sessions squadrant can introspect (read-only; claude + opencode)").option("--json", "emit machine-readable JSON").option("--agent <name>", `filter to one agent (${KNOWN_AGENTS.join("|")})`).option("--project <name>", "filter to one registered project").option("--live-only", "drop sessions whose process is dead (status: stale)").action(async (opts) => {
23221
23496
  if (opts.agent && !KNOWN_AGENTS.includes(opts.agent)) {
23222
- console.error(chalk34.red(`
23497
+ console.error(chalk35.red(`
23223
23498
  \u2718 Unknown agent '${opts.agent}'. Known agents: ${KNOWN_AGENTS.join(", ")}
23224
23499
  `));
23225
23500
  process.exit(1);
@@ -23227,7 +23502,7 @@ var sessionsCommand = new Command36("sessions").description("List live agent ses
23227
23502
  const config = loadConfig();
23228
23503
  if (opts.project && !(opts.project in config.projects)) {
23229
23504
  const known = Object.keys(config.projects).sort().join(", ") || "(none registered)";
23230
- console.error(chalk34.red(`
23505
+ console.error(chalk35.red(`
23231
23506
  \u2718 Unknown project '${opts.project}'. Known projects: ${known}
23232
23507
  `));
23233
23508
  process.exit(1);
@@ -23243,7 +23518,7 @@ var sessionsCommand = new Command36("sessions").description("List live agent ses
23243
23518
  };
23244
23519
  const { rows, unsupported } = await collectSessions(drivers);
23245
23520
  if (opts.agent && unsupported.includes(opts.agent)) {
23246
- console.error(chalk34.red(`
23521
+ console.error(chalk35.red(`
23247
23522
  \u2718 Agent '${opts.agent}' does not support session introspection.
23248
23523
  `));
23249
23524
  process.exit(1);
@@ -23259,7 +23534,7 @@ var sessionsCommand = new Command36("sessions").description("List live agent ses
23259
23534
  } else {
23260
23535
  console.log(formatSessionTable(filtered));
23261
23536
  if (!opts.agent && unsupported.length > 0) {
23262
- console.error(chalk34.dim(`(unsupported for session introspection: ${unsupported.join(", ")})`));
23537
+ console.error(chalk35.dim(`(unsupported for session introspection: ${unsupported.join(", ")})`));
23263
23538
  }
23264
23539
  }
23265
23540
  });
@@ -23271,7 +23546,7 @@ init_dist3();
23271
23546
  import os20 from "os";
23272
23547
  import path38 from "path";
23273
23548
  import { Command as Command37 } from "commander";
23274
- import chalk35 from "chalk";
23549
+ import chalk36 from "chalk";
23275
23550
 
23276
23551
  // packages/cli/src/lib/whoami.ts
23277
23552
  function projectForCwd(cwd, projects) {
@@ -23430,7 +23705,7 @@ var whoamiCommand = new Command37("whoami").description("Show the agent session
23430
23705
  } else {
23431
23706
  console.log(formatWhoami(result));
23432
23707
  if (!result.ok) {
23433
- console.error(chalk35.yellow("\n Could not identify a squadrant agent session for this invocation."));
23708
+ console.error(chalk36.yellow("\n Could not identify a squadrant agent session for this invocation."));
23434
23709
  }
23435
23710
  }
23436
23711
  if (!result.ok) process.exitCode = 1;