squadrant 0.14.2 → 0.15.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
@@ -168,11 +168,16 @@ var init_project_config = __esm({
168
168
  });
169
169
 
170
170
  // packages/shared/dist/effort.js
171
- function resolveEffort(config) {
171
+ function resolveEffort(config, projectName, projectConfigRoot) {
172
+ if (projectName) {
173
+ const override = loadProjectOverride(projectName, projectConfigRoot);
174
+ return override.effort ?? config.defaults.effort ?? "balance";
175
+ }
172
176
  return config.defaults.effort ?? "balance";
173
177
  }
174
178
  var init_effort = __esm({
175
179
  "packages/shared/dist/effort.js"() {
180
+ init_project_config();
176
181
  }
177
182
  });
178
183
 
@@ -182,6 +187,12 @@ var init_runtime = __esm({
182
187
  }
183
188
  });
184
189
 
190
+ // packages/shared/dist/types/liveness.js
191
+ var init_liveness = __esm({
192
+ "packages/shared/dist/types/liveness.js"() {
193
+ }
194
+ });
195
+
185
196
  // packages/shared/dist/types/control.js
186
197
  var TERMINAL_STATES;
187
198
  var init_control = __esm({
@@ -1040,6 +1051,7 @@ var init_dist = __esm({
1040
1051
  init_project_config();
1041
1052
  init_effort();
1042
1053
  init_runtime();
1054
+ init_liveness();
1043
1055
  init_control();
1044
1056
  init_projection();
1045
1057
  init_workspaces();
@@ -1202,7 +1214,7 @@ function healCmdFor(c) {
1202
1214
  return null;
1203
1215
  }
1204
1216
  var CREW_STALE_MS, CREW_GONE_MS;
1205
- var init_liveness = __esm({
1217
+ var init_liveness2 = __esm({
1206
1218
  "packages/core/dist/liveness.js"() {
1207
1219
  CREW_STALE_MS = 5 * 6e4;
1208
1220
  CREW_GONE_MS = 30 * 6e4;
@@ -1422,15 +1434,24 @@ var init_gate = __esm({
1422
1434
  }
1423
1435
  });
1424
1436
 
1437
+ // packages/core/dist/daemon/liveness-registry.js
1438
+ import { writeFileSync as writeFileSync6, readFileSync as readFileSync7, renameSync as renameSync2 } from "fs";
1439
+ var init_liveness_registry = __esm({
1440
+ "packages/core/dist/daemon/liveness-registry.js"() {
1441
+ init_liveness2();
1442
+ }
1443
+ });
1444
+
1425
1445
  // packages/core/dist/daemon/context.js
1426
1446
  import { homedir as homedir5 } from "os";
1427
1447
  import { join as join8 } from "path";
1428
1448
  import { spawn as realSpawn } from "child_process";
1429
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5 } from "fs";
1449
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
1430
1450
  var init_context = __esm({
1431
1451
  "packages/core/dist/daemon/context.js"() {
1432
1452
  init_store();
1433
1453
  init_dist();
1454
+ init_liveness_registry();
1434
1455
  }
1435
1456
  });
1436
1457
 
@@ -1489,6 +1510,7 @@ var init_delivery_loop = __esm({
1489
1510
  init_captain_delivery();
1490
1511
  init_dist();
1491
1512
  init_interactive_probe();
1513
+ init_liveness2();
1492
1514
  }
1493
1515
  });
1494
1516
 
@@ -1508,7 +1530,7 @@ var init_server = __esm({
1508
1530
  // packages/core/dist/daemon/snapshot-gather.js
1509
1531
  import { fileURLToPath as fileURLToPath2 } from "url";
1510
1532
  import { join as join9 } from "path";
1511
- import { statSync as statSync2, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
1533
+ import { statSync as statSync2, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
1512
1534
  var SELF_PATH;
1513
1535
  var init_snapshot_gather = __esm({
1514
1536
  "packages/core/dist/daemon/snapshot-gather.js"() {
@@ -1528,7 +1550,7 @@ var init_start = __esm({
1528
1550
  init_gates();
1529
1551
  init_server();
1530
1552
  init_mailbox();
1531
- init_liveness();
1553
+ init_liveness2();
1532
1554
  init_dist();
1533
1555
  init_snapshot_gather();
1534
1556
  SNAPSHOT_LOG_WINDOW_MS = 60 * 60 * 1e3;
@@ -2165,7 +2187,7 @@ async function isCaptainAlive(project, sockPath = DEFAULT_SOCK_PATH2) {
2165
2187
  try {
2166
2188
  const health = await sendRequest(sockPath, { kind: "health", project }, 5e3);
2167
2189
  const captain = health?.find((h) => h.kind === "captain" && h.project === project);
2168
- return captain != null && captain.state !== "gone" && captain.state !== "unknown";
2190
+ return captain?.state === "alive";
2169
2191
  } catch {
2170
2192
  return false;
2171
2193
  }
@@ -2186,15 +2208,16 @@ async function dispatchToSibling(opts) {
2186
2208
  if (!toCfg) {
2187
2209
  throw new Error(`target project '${opts.toProject}' not found in config`);
2188
2210
  }
2189
- if (!fromCfg.group || !toCfg.group || fromCfg.group !== toCfg.group) {
2190
- throw new Error(`cannot dispatch: '${opts.toProject}' (group: ${toCfg.group ?? "none"}) is not in the same group as '${opts.fromProject}' (group: ${fromCfg.group ?? "none"})`);
2191
- }
2211
+ const sameGroup = !!fromCfg?.group && !!toCfg.group && fromCfg.group === toCfg.group;
2192
2212
  if (toCfg.acceptDelegations === false) {
2193
2213
  throw new Error(`cannot dispatch to '${opts.toProject}': project has acceptDelegations set to false`);
2194
2214
  }
2195
2215
  const sockPath = opts.sockPath ?? DEFAULT_SOCK_PATH2;
2196
2216
  const alive = await isCaptainAlive(opts.toProject, sockPath);
2197
2217
  if (!alive) {
2218
+ if (!sameGroup) {
2219
+ throw new Error(`cannot dispatch to '${opts.toProject}': captain is not running and cross-group dispatch does not auto-boot it. Use 'squadrant ping ${opts.toProject} "<msg>"' or start it manually with 'squadrant launch ${opts.toProject}', then retry.`);
2220
+ }
2198
2221
  if (opts.bootCaptain) {
2199
2222
  await opts.bootCaptain(opts.toProject);
2200
2223
  }
@@ -2786,7 +2809,7 @@ var init_dist2 = __esm({
2786
2809
  init_mailbox();
2787
2810
  init_protocol();
2788
2811
  init_state_machine();
2789
- init_liveness();
2812
+ init_liveness2();
2790
2813
  init_watchdog();
2791
2814
  init_store();
2792
2815
  init_snapshot();
@@ -3660,11 +3683,81 @@ var init_events_bridge = __esm({
3660
3683
  }
3661
3684
  });
3662
3685
 
3686
+ // packages/workspaces/dist/cmux-daemon/store-fingerprint.js
3687
+ function roleFromTemplate(args) {
3688
+ const i = args?.indexOf("--append-system-prompt-file") ?? -1;
3689
+ const tmpl = i >= 0 && args ? (args[i + 1] ?? "").split("/").pop() ?? "" : "";
3690
+ if (tmpl.startsWith("captain"))
3691
+ return "captain";
3692
+ if (tmpl.startsWith("crew"))
3693
+ return "crew";
3694
+ if (tmpl.startsWith("command"))
3695
+ return "command";
3696
+ return "unknown";
3697
+ }
3698
+ function projectFromCwd(cwd, projects) {
3699
+ for (const [name, p] of Object.entries(projects)) {
3700
+ const projPath = resolveHome(p.path);
3701
+ if (cwd === projPath || cwd.startsWith(`${projPath}/`))
3702
+ return name;
3703
+ }
3704
+ return void 0;
3705
+ }
3706
+ function parseStoreRecords(fileContent, projects) {
3707
+ let parsed;
3708
+ try {
3709
+ parsed = JSON.parse(fileContent);
3710
+ } catch (e) {
3711
+ throw new Error(`parseStoreRecords: invalid JSON: ${e.message}`);
3712
+ }
3713
+ const out = [];
3714
+ for (const s of Object.values(parsed.sessions ?? {})) {
3715
+ const cwd = s.cwd ?? s.launchCommand?.workingDirectory ?? "";
3716
+ const project = projectFromCwd(cwd, projects);
3717
+ if (!project || !s.sessionId)
3718
+ continue;
3719
+ out.push({
3720
+ role: roleFromTemplate(s.launchCommand?.arguments),
3721
+ project,
3722
+ pid: typeof s.pid === "number" ? s.pid : null,
3723
+ sessionId: s.sessionId,
3724
+ present: true,
3725
+ isRestorable: s.isRestorable
3726
+ });
3727
+ }
3728
+ return out;
3729
+ }
3730
+ function readLivenessSnapshot(files, readFile6, projects) {
3731
+ const out = [];
3732
+ let successes = 0;
3733
+ for (const f of files) {
3734
+ try {
3735
+ out.push(...parseStoreRecords(readFile6(f), projects));
3736
+ successes++;
3737
+ } catch {
3738
+ }
3739
+ }
3740
+ if (files.length > 0 && successes === 0) {
3741
+ throw new Error(`readLivenessSnapshot: all ${files.length} store file(s) unreadable/corrupt this tick`);
3742
+ }
3743
+ return out;
3744
+ }
3745
+ var init_store_fingerprint = __esm({
3746
+ "packages/workspaces/dist/cmux-daemon/store-fingerprint.js"() {
3747
+ init_dist();
3748
+ }
3749
+ });
3750
+
3663
3751
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
3752
+ import { readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
3753
+ import { join as join13 } from "path";
3754
+ import { homedir as homedir8 } from "os";
3664
3755
  var DaemonCmux;
3665
3756
  var init_daemon_cmux = __esm({
3666
3757
  "packages/workspaces/dist/cmux-daemon/daemon-cmux.js"() {
3667
3758
  init_dist2();
3759
+ init_dist();
3760
+ init_store_fingerprint();
3668
3761
  DaemonCmux = class {
3669
3762
  driver;
3670
3763
  constructor(driver) {
@@ -3715,14 +3808,30 @@ var init_daemon_cmux = __esm({
3715
3808
  return false;
3716
3809
  }
3717
3810
  }
3811
+ /**
3812
+ * Ground-truth liveness from cmux's own hook-sessions store (§5.4).
3813
+ * THROWS (does not return []) when the dir can't be listed, or every store
3814
+ * file failed to read/parse — see the class doc above.
3815
+ */
3816
+ async liveness() {
3817
+ const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join13(homedir8(), ".cmuxterm");
3818
+ const projects = loadConfig().projects;
3819
+ let files;
3820
+ try {
3821
+ files = readdirSync3(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
3822
+ } catch (e) {
3823
+ throw new Error(`liveness: could not read cmux state dir ${dir}: ${e.message}`);
3824
+ }
3825
+ return readLivenessSnapshot(files, (f) => readFileSync9(join13(dir, f), "utf-8"), projects);
3826
+ }
3718
3827
  };
3719
3828
  }
3720
3829
  });
3721
3830
 
3722
3831
  // packages/workspaces/dist/cmux-daemon/cmux-store-source.js
3723
- import { join as join13 } from "path";
3724
- import { homedir as homedir8 } from "os";
3725
- import { watch, readdirSync as readdirSync3, readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
3832
+ import { join as join14 } from "path";
3833
+ import { homedir as homedir9 } from "os";
3834
+ import { watch, readdirSync as readdirSync4, readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
3726
3835
  function parseLifecycleState(s) {
3727
3836
  if (s === "running" || s === "idle" || s === "needsInput" || s === "unknown") {
3728
3837
  return s;
@@ -3739,14 +3848,14 @@ function defaultIsPidAlive(pid) {
3739
3848
  }
3740
3849
  function defaultListFiles(dir) {
3741
3850
  try {
3742
- return readdirSync3(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
3851
+ return readdirSync4(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
3743
3852
  } catch {
3744
3853
  return [];
3745
3854
  }
3746
3855
  }
3747
3856
  function defaultReadFile(path29) {
3748
3857
  try {
3749
- return readFileSync8(path29, "utf-8");
3858
+ return readFileSync10(path29, "utf-8");
3750
3859
  } catch {
3751
3860
  return void 0;
3752
3861
  }
@@ -3782,7 +3891,7 @@ var init_cmux_store_source = __esm({
3782
3891
  active = false;
3783
3892
  lastError = null;
3784
3893
  constructor(opts = {}) {
3785
- this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join13(homedir8(), ".cmuxterm");
3894
+ this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join14(homedir9(), ".cmuxterm");
3786
3895
  this.debounceMs = opts.debounceMs ?? 50;
3787
3896
  this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive;
3788
3897
  this.listFiles = opts.listFiles ?? defaultListFiles;
@@ -3844,7 +3953,7 @@ var init_cmux_store_source = __esm({
3844
3953
  }
3845
3954
  scanFile(filename) {
3846
3955
  const deps = this.deps;
3847
- const filePath = join13(this.stateDir, filename);
3956
+ const filePath = join14(this.stateDir, filename);
3848
3957
  const lockPath = `${filePath}.lock`;
3849
3958
  if (this.fileExists(lockPath)) {
3850
3959
  this.log(`cmux-store: skipping ${filename} (locked)`);
@@ -3898,11 +4007,11 @@ var init_cmux_store_source = __esm({
3898
4007
  });
3899
4008
 
3900
4009
  // packages/workspaces/dist/native-hooks/native-hook-source.js
3901
- import { join as join14 } from "path";
3902
- import { homedir as homedir9 } from "os";
3903
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
4010
+ import { join as join15 } from "path";
4011
+ import { homedir as homedir10 } from "os";
4012
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
3904
4013
  function installClaudeHooks(opts = {}) {
3905
- const settingsPath = opts.settingsPath ?? join14(homedir9(), ".claude", "settings.json");
4014
+ const settingsPath = opts.settingsPath ?? join15(homedir10(), ".claude", "settings.json");
3906
4015
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
3907
4016
  const readFile6 = opts.readFile ?? defaultReadFile2;
3908
4017
  const writeFile5 = opts.writeFile ?? defaultWriteFile;
@@ -3976,14 +4085,14 @@ function extractDetail(sub, payload) {
3976
4085
  }
3977
4086
  function defaultReadFile2(path29) {
3978
4087
  try {
3979
- return readFileSync9(path29, "utf-8");
4088
+ return readFileSync11(path29, "utf-8");
3980
4089
  } catch {
3981
4090
  return void 0;
3982
4091
  }
3983
4092
  }
3984
4093
  function defaultWriteFile(path29, content) {
3985
4094
  mkdirSync6(path29.replace(/\/[^/]+$/, ""), { recursive: true });
3986
- writeFileSync7(path29, content, "utf-8");
4095
+ writeFileSync8(path29, content, "utf-8");
3987
4096
  }
3988
4097
  var CLAUDE_HOOK_EVENTS, DEFAULT_HOOK_CMD, NativeHookSource;
3989
4098
  var init_native_hook_source = __esm({
@@ -4152,6 +4261,21 @@ async function confirmedSendToPane(runtime, pane, message) {
4152
4261
  }
4153
4262
  return { delivered: false };
4154
4263
  }
4264
+ async function resendCrewFirstTurn(runtime, captainName, project, name, message) {
4265
+ const captain = await runtime.status(captainName);
4266
+ if (!captain)
4267
+ return { delivered: false };
4268
+ const surfaces = await runtime.listSurfaces(captain.id);
4269
+ const want = titleFor(project, name);
4270
+ const pane = surfaces.find((s) => s.title === want);
4271
+ if (!pane)
4272
+ return { delivered: false };
4273
+ const screen = await runtime.readPaneScreen(pane) ?? "";
4274
+ if (!hasCCInputBox(screen) || classifyStartupSurface(screen) !== "idle") {
4275
+ return { delivered: false };
4276
+ }
4277
+ return confirmedSendToPane(runtime, pane, message);
4278
+ }
4155
4279
  async function sendFirstTurnWhenReady(runtime, pane, task, preLaunchScreen, acceptanceConfig) {
4156
4280
  await new Promise((r) => setTimeout(r, SEND_FIRST_TURN_FLOOR_MS));
4157
4281
  const maxPolls = Math.floor((SEND_FIRST_TURN_TIMEOUT_MS - SEND_FIRST_TURN_FLOOR_MS) / POLL_INTERVAL_MS);
@@ -4260,6 +4384,7 @@ __export(dist_exports, {
4260
4384
  isInsideCmux: () => isInsideCmux,
4261
4385
  listProjectCrews: () => listProjectCrews,
4262
4386
  mapSubToLifecycle: () => mapSubToLifecycle,
4387
+ resendCrewFirstTurn: () => resendCrewFirstTurn,
4263
4388
  resolveCaptainWorkspace: () => resolveCaptainWorkspace,
4264
4389
  sendFirstTurnWhenReady: () => sendFirstTurnWhenReady
4265
4390
  });
@@ -5296,11 +5421,11 @@ var init_codex_app_server_source = __esm({
5296
5421
 
5297
5422
  // packages/agents/dist/codex/config.js
5298
5423
  import { readFile as readFile5 } from "fs/promises";
5299
- import { homedir as homedir11 } from "os";
5300
- import { join as join16 } from "path";
5424
+ import { homedir as homedir12 } from "os";
5425
+ import { join as join17 } from "path";
5301
5426
  async function resolveCodexModel() {
5302
- const home = process.env["CODEX_HOME"] ?? join16(homedir11(), ".codex");
5303
- const configPath = join16(home, "config.toml");
5427
+ const home = process.env["CODEX_HOME"] ?? join17(homedir12(), ".codex");
5428
+ const configPath = join17(home, "config.toml");
5304
5429
  let text;
5305
5430
  try {
5306
5431
  text = await readFile5(configPath, "utf8");
@@ -5722,7 +5847,7 @@ var init_sse_bridge = __esm({
5722
5847
  const fetchImpl = this.deps.fetchImpl ?? fetch;
5723
5848
  const sleep2 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5724
5849
  const reconnectMs = this.deps.reconnectMs ?? 500;
5725
- const maxBoot = this.deps.maxBootAttempts ?? 60;
5850
+ const maxBoot = this.deps.maxBootAttempts ?? 240;
5726
5851
  const url = `http://127.0.0.1:${port}/event`;
5727
5852
  let booted = false;
5728
5853
  let bootAttempts = 0;
@@ -5821,9 +5946,9 @@ var init_sse_bridge = __esm({
5821
5946
 
5822
5947
  // packages/agents/dist/interactive/claude.js
5823
5948
  import { execSync as execSync7 } from "child_process";
5824
- import { readFileSync as readFileSync10 } from "fs";
5825
- import { homedir as homedir12 } from "os";
5826
- import { join as join17 } from "path";
5949
+ import { readFileSync as readFileSync12 } from "fs";
5950
+ import { homedir as homedir13 } from "os";
5951
+ import { join as join18 } from "path";
5827
5952
  function probeClaudeSettingsFlag() {
5828
5953
  try {
5829
5954
  const help = execSync7("claude --help", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
@@ -5874,11 +5999,11 @@ function deriveTranscriptPath(sessionId, cwd) {
5874
5999
  if (!sessionId || !cwd)
5875
6000
  return null;
5876
6001
  const escaped = cwd.replace(/[^a-zA-Z0-9]/g, "-");
5877
- return join17(homedir12(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
6002
+ return join18(homedir13(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
5878
6003
  }
5879
6004
  function readLastAssistantText(transcriptPath) {
5880
6005
  try {
5881
- const raw = readFileSync10(transcriptPath, "utf-8");
6006
+ const raw = readFileSync12(transcriptPath, "utf-8");
5882
6007
  const lines = raw.split(/\r?\n/);
5883
6008
  for (let i = lines.length - 1; i >= 0; i--) {
5884
6009
  const line = lines[i].trim();
@@ -6344,11 +6469,11 @@ var init_dist4 = __esm({
6344
6469
  // packages/cli/src/index.ts
6345
6470
  init_dist();
6346
6471
  init_dist2();
6347
- import { Command as Command29 } from "commander";
6348
- import { readFileSync as readFileSync13, existsSync as existsSync12, writeFileSync as writeFileSync10 } from "fs";
6472
+ import { Command as Command31 } from "commander";
6473
+ import { readFileSync as readFileSync15, existsSync as existsSync12, writeFileSync as writeFileSync11 } from "fs";
6349
6474
  import { fileURLToPath as fileURLToPath6 } from "url";
6350
- import { dirname as dirname7, join as join26 } from "path";
6351
- import { homedir as homedir18 } from "os";
6475
+ import { dirname as dirname8, join as join27 } from "path";
6476
+ import { homedir as homedir20 } from "os";
6352
6477
 
6353
6478
  // packages/cli/src/commands/doctor.ts
6354
6479
  init_dist();
@@ -6365,10 +6490,10 @@ import chalk3 from "chalk";
6365
6490
  // packages/cli/src/commands/health-view.ts
6366
6491
  init_dist2();
6367
6492
  init_dist2();
6368
- import { homedir as homedir10 } from "os";
6369
- import { join as join15 } from "path";
6493
+ import { homedir as homedir11 } from "os";
6494
+ import { join as join16 } from "path";
6370
6495
  import chalk2 from "chalk";
6371
- var SOCK = join15(homedir10(), ".config", "squadrant", "squadrant.sock");
6496
+ var SOCK = join16(homedir11(), ".config", "squadrant", "squadrant.sock");
6372
6497
  async function queryHealth(project) {
6373
6498
  try {
6374
6499
  const reply = await sendRequest(SOCK, { kind: "health", project });
@@ -6698,13 +6823,161 @@ ${passed === total ? chalk3.green("All checks passed") : chalk3.yellow(`${passed
6698
6823
  init_dist();
6699
6824
  init_dist3();
6700
6825
  init_dist();
6701
- init_dist4();
6702
6826
  import { Command as Command2 } from "commander";
6703
6827
  import fs16 from "fs";
6704
6828
  import path18 from "path";
6705
6829
  import os9 from "os";
6706
6830
  import readline from "readline";
6707
6831
  import chalk4 from "chalk";
6832
+
6833
+ // packages/cli/src/lib/per-crew-settings.ts
6834
+ init_dist4();
6835
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
6836
+ import { dirname as dirname5, join as join19 } from "path";
6837
+ import { homedir as homedir14 } from "os";
6838
+ var CREW_PERMISSION_ALLOWLIST = [
6839
+ // git — read + safe mutations (reset/clean/config intentionally excluded)
6840
+ "Bash(git status:*)",
6841
+ "Bash(git add:*)",
6842
+ "Bash(git commit:*)",
6843
+ "Bash(git push:*)",
6844
+ "Bash(git pull:*)",
6845
+ "Bash(git fetch:*)",
6846
+ "Bash(git checkout:*)",
6847
+ "Bash(git branch:*)",
6848
+ "Bash(git diff:*)",
6849
+ "Bash(git log:*)",
6850
+ "Bash(git stash:*)",
6851
+ "Bash(git restore:*)",
6852
+ // git — read-only queries (no-mutation subcommands)
6853
+ "Bash(git show:*)",
6854
+ "Bash(git rev-parse:*)",
6855
+ "Bash(git remote -v:*)",
6856
+ "Bash(git stash list:*)",
6857
+ // npm / npx — installs + script/test/build runners
6858
+ "Bash(npm install:*)",
6859
+ "Bash(npm ci:*)",
6860
+ "Bash(npm run:*)",
6861
+ "Bash(npm test:*)",
6862
+ "Bash(npm ls:*)",
6863
+ "Bash(npm view:*)",
6864
+ "Bash(npx vitest:*)",
6865
+ "Bash(npx tsc:*)",
6866
+ // direct runners
6867
+ "Bash(node:*)",
6868
+ "Bash(node --version:*)",
6869
+ "Bash(vitest:*)",
6870
+ "Bash(tsc:*)",
6871
+ // pnpm
6872
+ "Bash(pnpm ls:*)",
6873
+ // process / system read — non-interactive, read-only
6874
+ "Bash(ps:*)",
6875
+ "Bash(pgrep:*)",
6876
+ "Bash(lsof:*)",
6877
+ "Bash(top -l:*)",
6878
+ "Bash(uptime:*)",
6879
+ "Bash(whoami:*)",
6880
+ "Bash(uname:*)",
6881
+ "Bash(date:*)",
6882
+ "Bash(env:*)",
6883
+ "Bash(sysctl:*)",
6884
+ // file read / navigation
6885
+ "Bash(ls:*)",
6886
+ "Bash(cat:*)",
6887
+ "Bash(head:*)",
6888
+ "Bash(tail:*)",
6889
+ "Bash(wc:*)",
6890
+ "Bash(grep:*)",
6891
+ "Bash(rg:*)",
6892
+ "Bash(find:*)",
6893
+ "Bash(pwd:*)",
6894
+ "Bash(stat:*)",
6895
+ "Bash(file:*)",
6896
+ "Bash(realpath:*)",
6897
+ "Bash(basename:*)",
6898
+ "Bash(dirname:*)",
6899
+ "Bash(du:*)",
6900
+ "Bash(df:*)",
6901
+ "Bash(tree:*)",
6902
+ // read-only text processing
6903
+ "Bash(sort:*)",
6904
+ "Bash(uniq:*)",
6905
+ "Bash(cut:*)",
6906
+ "Bash(tr:*)",
6907
+ "Bash(jq:*)",
6908
+ "Bash(diff:*)",
6909
+ "Bash(column:*)",
6910
+ // harmless utilities
6911
+ "Bash(echo:*)",
6912
+ "Bash(which:*)",
6913
+ "Bash(true:*)",
6914
+ "Bash(sleep:*)",
6915
+ "Bash(printf:*)"
6916
+ ];
6917
+ function healStaleCockpitRefs(raw) {
6918
+ return raw.replaceAll("cockpit crew _hook", "squadrant crew _hook").replaceAll(".config/cockpit", ".config/squadrant").replaceAll("cockpit-hub", "squadrant-hub");
6919
+ }
6920
+ function mergeCrewPermissions(settings) {
6921
+ const next = structuredClone(settings ?? {});
6922
+ const permissions = next.permissions ??= {};
6923
+ const existing = Array.isArray(permissions.allow) ? permissions.allow : [];
6924
+ permissions.allow = [.../* @__PURE__ */ new Set([...existing, ...CREW_PERMISSION_ALLOWLIST])];
6925
+ return next;
6926
+ }
6927
+ function writePerCrewSettingsLocal(o) {
6928
+ const dir = join19(o.projectCwd, ".claude");
6929
+ mkdirSync7(dir, { recursive: true });
6930
+ const file = join19(dir, "settings.local.json");
6931
+ let existing = {};
6932
+ try {
6933
+ const raw = healStaleCockpitRefs(readFileSync13(file, "utf-8"));
6934
+ existing = JSON.parse(raw);
6935
+ } catch {
6936
+ }
6937
+ const withHooks = mergeClaudeHooks(existing, o.hookCmd ?? "squadrant crew _hook");
6938
+ const merged = mergeCrewPermissions(withHooks);
6939
+ writeFileSync9(file, JSON.stringify(merged, null, 2));
6940
+ return file;
6941
+ }
6942
+ var DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH = join19(homedir14(), ".config", "opencode", "opencode.json");
6943
+ function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH) {
6944
+ mkdirSync7(dirname5(configPath), { recursive: true });
6945
+ const defaultConfig = {
6946
+ $schema: "https://opencode.ai/config.json",
6947
+ model: "anthropic/claude-sonnet-4-5"
6948
+ };
6949
+ try {
6950
+ writeFileSync9(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", { flag: "wx" });
6951
+ return configPath;
6952
+ } catch (err) {
6953
+ if (err.code === "EEXIST") return null;
6954
+ throw err;
6955
+ }
6956
+ }
6957
+ function writePerCrewOpencodeConfig(o) {
6958
+ const dir = join19(o.stateRoot, o.project, o.taskId);
6959
+ mkdirSync7(dir, { recursive: true });
6960
+ const file = join19(dir, "opencode.json");
6961
+ const config = {
6962
+ permission: {
6963
+ read: "allow",
6964
+ edit: "allow",
6965
+ glob: "allow",
6966
+ grep: "allow",
6967
+ bash: o.gateBash ? "ask" : "allow",
6968
+ webfetch: "allow",
6969
+ websearch: "allow",
6970
+ task: "allow",
6971
+ lsp: "allow",
6972
+ external_directory: { "**": "allow" }
6973
+ }
6974
+ };
6975
+ writeFileSync9(file, JSON.stringify(config, null, 2));
6976
+ return file;
6977
+ }
6978
+
6979
+ // packages/cli/src/commands/init.ts
6980
+ init_dist4();
6708
6981
  function findPackageRoot() {
6709
6982
  let dir = path18.dirname(new URL(import.meta.url).pathname);
6710
6983
  while (dir !== "/") {
@@ -6754,6 +7027,9 @@ var initCommand = new Command2("init").description("Guided first-time setup: hub
6754
7027
  console.log(chalk4.cyan(" /plugin marketplace add superpowers"));
6755
7028
  console.log(chalk4.cyan(" /plugin marketplace add thedotmack/claude-mem"));
6756
7029
  console.log(chalk4.cyan(" /plugin marketplace add context7"));
7030
+ console.log(chalk4.dim("\n Using opencode crews? Install the CLI:"));
7031
+ console.log(chalk4.cyan(" npm install -g opencode-ai"));
7032
+ console.log(chalk4.dim(` (squadrant provisions a default model in ${DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH} if it's missing)`));
6757
7033
  console.log(chalk4.bold("\n 4/5 Register first project"));
6758
7034
  console.log(chalk4.cyan(" squadrant projects add <name> <path>"));
6759
7035
  console.log(chalk4.bold("\n 5/5 Telegram (optional)"));
@@ -6849,6 +7125,15 @@ var initCommand = new Command2("init").description("Guided first-time setup: hub
6849
7125
  console.log(chalk4.cyan(" /plugin marketplace add thedotmack/claude-mem"));
6850
7126
  console.log(chalk4.cyan(" /plugin marketplace add context7\n"));
6851
7127
  console.log(chalk4.dim(" Squadrant never auto-installs plugins \u2014 open Claude Code and run the commands above."));
7128
+ console.log(chalk4.bold("\n Using opencode crews?"));
7129
+ console.log(chalk4.cyan(" npm install -g opencode-ai"));
7130
+ const opencodeConfigWritten = ensureGlobalOpencodeConfig();
7131
+ if (opencodeConfigWritten) {
7132
+ console.log(chalk4.green(` \u2714 Default model config created at ${opencodeConfigWritten}`));
7133
+ } else {
7134
+ console.log(chalk4.dim(` - ${DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH} already exists (unchanged)`));
7135
+ }
7136
+ console.log(chalk4.dim(" Per-crew config deep-merges with this file \u2014 edit it to change opencode's default model/plugins/mcp."));
6852
7137
  stepHeader(4, 5, "Register first project");
6853
7138
  const projectPath = await promptLine(
6854
7139
  chalk4.cyan(" Absolute path to your first project (Enter to skip): ")
@@ -7203,9 +7488,9 @@ init_dist4();
7203
7488
  import { Command as Command8 } from "commander";
7204
7489
  import { createConnection as createConnection3 } from "net";
7205
7490
  import { randomUUID as randomUUID4 } from "crypto";
7206
- import { homedir as homedir14 } from "os";
7207
- import { join as join19 } from "path";
7208
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
7491
+ import { homedir as homedir16 } from "os";
7492
+ import { join as join21 } from "path";
7493
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
7209
7494
 
7210
7495
  // packages/cli/src/commands/crew-output.ts
7211
7496
  function tailLines(text, maxLines = 40, maxBytes = 4096) {
@@ -7263,11 +7548,11 @@ init_dist2();
7263
7548
  import { Command as Command6 } from "commander";
7264
7549
  import chalk8 from "chalk";
7265
7550
  import { createConnection as createConnection2 } from "net";
7266
- import { homedir as homedir13 } from "os";
7267
- import { join as join18 } from "path";
7551
+ import { homedir as homedir15 } from "os";
7552
+ import { join as join20 } from "path";
7268
7553
  import { createInterface } from "readline";
7269
7554
  function socketPath() {
7270
- return process.env.SQUADRANTD_SOCK ?? join18(homedir13(), ".config", "squadrant", "squadrant.sock");
7555
+ return process.env.SQUADRANTD_SOCK ?? join20(homedir15(), ".config", "squadrant", "squadrant.sock");
7271
7556
  }
7272
7557
  function rule(width, ch = "\u2500") {
7273
7558
  return ch.repeat(Math.max(0, width));
@@ -7503,7 +7788,7 @@ var crewChatCommand = new Command7("chat").description("[DEPRECATED] alias for `
7503
7788
  });
7504
7789
 
7505
7790
  // packages/cli/src/commands/crew-control.ts
7506
- var SOCK2 = join19(homedir14(), ".config", "squadrant", "squadrant.sock");
7791
+ var SOCK2 = join21(homedir16(), ".config", "squadrant", "squadrant.sock");
7507
7792
  var CODEX_FIRST_TURN_DELAY_MS = 1500;
7508
7793
  async function sendCodexFirstTurn(taskId, text) {
7509
7794
  await new Promise((r) => setTimeout(r, CODEX_FIRST_TURN_DELAY_MS));
@@ -7608,10 +7893,10 @@ function buildSignalRequest(signal, o) {
7608
7893
  return { kind: "event", project, event };
7609
7894
  }
7610
7895
  function defaultWriteResult(id, payload) {
7611
- const dir = join19(homedir14(), ".config", "squadrant", "state", "_results");
7612
- mkdirSync7(dir, { recursive: true });
7613
- const file = join19(dir, `${id}.txt`);
7614
- writeFileSync8(file, payload);
7896
+ const dir = join21(homedir16(), ".config", "squadrant", "state", "_results");
7897
+ mkdirSync8(dir, { recursive: true });
7898
+ const file = join21(dir, `${id}.txt`);
7899
+ writeFileSync10(file, payload);
7615
7900
  return file;
7616
7901
  }
7617
7902
  function addControlPlaneCrewCommands(crew) {
@@ -7717,136 +8002,6 @@ function addControlPlaneCrewCommands(crew) {
7717
8002
  var crewControlCommand = new Command8("crew").description("Dispatch and track crew via the squadrant control plane");
7718
8003
  addControlPlaneCrewCommands(crewControlCommand);
7719
8004
 
7720
- // packages/cli/src/lib/per-crew-settings.ts
7721
- init_dist4();
7722
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
7723
- import { join as join20 } from "path";
7724
- var CREW_PERMISSION_ALLOWLIST = [
7725
- // git — read + safe mutations (reset/clean/config intentionally excluded)
7726
- "Bash(git status:*)",
7727
- "Bash(git add:*)",
7728
- "Bash(git commit:*)",
7729
- "Bash(git push:*)",
7730
- "Bash(git pull:*)",
7731
- "Bash(git fetch:*)",
7732
- "Bash(git checkout:*)",
7733
- "Bash(git branch:*)",
7734
- "Bash(git diff:*)",
7735
- "Bash(git log:*)",
7736
- "Bash(git stash:*)",
7737
- "Bash(git restore:*)",
7738
- // git — read-only queries (no-mutation subcommands)
7739
- "Bash(git show:*)",
7740
- "Bash(git rev-parse:*)",
7741
- "Bash(git remote -v:*)",
7742
- "Bash(git stash list:*)",
7743
- // npm / npx — installs + script/test/build runners
7744
- "Bash(npm install:*)",
7745
- "Bash(npm ci:*)",
7746
- "Bash(npm run:*)",
7747
- "Bash(npm test:*)",
7748
- "Bash(npm ls:*)",
7749
- "Bash(npm view:*)",
7750
- "Bash(npx vitest:*)",
7751
- "Bash(npx tsc:*)",
7752
- // direct runners
7753
- "Bash(node:*)",
7754
- "Bash(node --version:*)",
7755
- "Bash(vitest:*)",
7756
- "Bash(tsc:*)",
7757
- // pnpm
7758
- "Bash(pnpm ls:*)",
7759
- // process / system read — non-interactive, read-only
7760
- "Bash(ps:*)",
7761
- "Bash(pgrep:*)",
7762
- "Bash(lsof:*)",
7763
- "Bash(top -l:*)",
7764
- "Bash(uptime:*)",
7765
- "Bash(whoami:*)",
7766
- "Bash(uname:*)",
7767
- "Bash(date:*)",
7768
- "Bash(env:*)",
7769
- "Bash(sysctl:*)",
7770
- // file read / navigation
7771
- "Bash(ls:*)",
7772
- "Bash(cat:*)",
7773
- "Bash(head:*)",
7774
- "Bash(tail:*)",
7775
- "Bash(wc:*)",
7776
- "Bash(grep:*)",
7777
- "Bash(rg:*)",
7778
- "Bash(find:*)",
7779
- "Bash(pwd:*)",
7780
- "Bash(stat:*)",
7781
- "Bash(file:*)",
7782
- "Bash(realpath:*)",
7783
- "Bash(basename:*)",
7784
- "Bash(dirname:*)",
7785
- "Bash(du:*)",
7786
- "Bash(df:*)",
7787
- "Bash(tree:*)",
7788
- // read-only text processing
7789
- "Bash(sort:*)",
7790
- "Bash(uniq:*)",
7791
- "Bash(cut:*)",
7792
- "Bash(tr:*)",
7793
- "Bash(jq:*)",
7794
- "Bash(diff:*)",
7795
- "Bash(column:*)",
7796
- // harmless utilities
7797
- "Bash(echo:*)",
7798
- "Bash(which:*)",
7799
- "Bash(true:*)",
7800
- "Bash(sleep:*)",
7801
- "Bash(printf:*)"
7802
- ];
7803
- function healStaleCockpitRefs(raw) {
7804
- return raw.replaceAll("cockpit crew _hook", "squadrant crew _hook").replaceAll(".config/cockpit", ".config/squadrant").replaceAll("cockpit-hub", "squadrant-hub");
7805
- }
7806
- function mergeCrewPermissions(settings) {
7807
- const next = structuredClone(settings ?? {});
7808
- const permissions = next.permissions ??= {};
7809
- const existing = Array.isArray(permissions.allow) ? permissions.allow : [];
7810
- permissions.allow = [.../* @__PURE__ */ new Set([...existing, ...CREW_PERMISSION_ALLOWLIST])];
7811
- return next;
7812
- }
7813
- function writePerCrewSettingsLocal(o) {
7814
- const dir = join20(o.projectCwd, ".claude");
7815
- mkdirSync8(dir, { recursive: true });
7816
- const file = join20(dir, "settings.local.json");
7817
- let existing = {};
7818
- try {
7819
- const raw = healStaleCockpitRefs(readFileSync11(file, "utf-8"));
7820
- existing = JSON.parse(raw);
7821
- } catch {
7822
- }
7823
- const withHooks = mergeClaudeHooks(existing, o.hookCmd ?? "squadrant crew _hook");
7824
- const merged = mergeCrewPermissions(withHooks);
7825
- writeFileSync9(file, JSON.stringify(merged, null, 2));
7826
- return file;
7827
- }
7828
- function writePerCrewOpencodeConfig(o) {
7829
- const dir = join20(o.stateRoot, o.project, o.taskId);
7830
- mkdirSync8(dir, { recursive: true });
7831
- const file = join20(dir, "opencode.json");
7832
- const config = {
7833
- permission: {
7834
- read: "allow",
7835
- edit: "allow",
7836
- glob: "allow",
7837
- grep: "allow",
7838
- bash: o.gateBash ? "ask" : "allow",
7839
- webfetch: "allow",
7840
- websearch: "allow",
7841
- task: "allow",
7842
- lsp: "allow",
7843
- external_directory: { "**": "allow" }
7844
- }
7845
- };
7846
- writeFileSync9(file, JSON.stringify(config, null, 2));
7847
- return file;
7848
- }
7849
-
7850
8005
  // packages/cli/src/commands/crew.ts
7851
8006
  async function runCrewSpawn2(input) {
7852
8007
  const config = loadConfig();
@@ -8143,8 +8298,8 @@ init_dist();
8143
8298
  init_dist3();
8144
8299
  import { Command as Command11 } from "commander";
8145
8300
  import { execSync as execSync10 } from "child_process";
8146
- import { homedir as homedir16 } from "os";
8147
- import { join as join22 } from "path";
8301
+ import { homedir as homedir18 } from "os";
8302
+ import { join as join23 } from "path";
8148
8303
  import chalk12 from "chalk";
8149
8304
 
8150
8305
  // packages/web/dist/read-status.js
@@ -8157,6 +8312,11 @@ function deriveState(tasks) {
8157
8312
  return "busy";
8158
8313
  return "idle";
8159
8314
  }
8315
+ function deriveRowState(tasks, captainState) {
8316
+ if (captainState === "gone" || captainState === "stopped" || captainState === "unknown")
8317
+ return "offline";
8318
+ return deriveState(tasks);
8319
+ }
8160
8320
  function buildExcerpt(tasks) {
8161
8321
  const working = tasks.filter((t) => t.state === "working").length;
8162
8322
  const blocked = tasks.filter((t) => t.state === "blocked" || t.state === "awaiting-input").length;
@@ -8180,13 +8340,25 @@ async function readAllStatuses(deps) {
8180
8340
  const result = await deps.call({ kind: "list", project });
8181
8341
  return result;
8182
8342
  });
8343
+ const captainStateByProject = /* @__PURE__ */ new Map();
8344
+ if (deps.call) {
8345
+ try {
8346
+ const health = await deps.call({ kind: "health" });
8347
+ for (const h of health ?? []) {
8348
+ if (h.kind === "captain")
8349
+ captainStateByProject.set(h.project, h.state);
8350
+ }
8351
+ } catch {
8352
+ }
8353
+ }
8183
8354
  const rows = [];
8184
8355
  for (const [name, project] of Object.entries(deps.config.projects)) {
8185
8356
  try {
8186
8357
  const tasks = await listTasks(name);
8358
+ const captainState = captainStateByProject.get(name) ?? "unknown";
8187
8359
  rows.push({
8188
8360
  project: name,
8189
- state: deriveState(tasks),
8361
+ state: deriveRowState(tasks, captainState),
8190
8362
  lastChecked: (/* @__PURE__ */ new Date()).toISOString(),
8191
8363
  captainWorkspace: project.captainName,
8192
8364
  excerpt: buildExcerpt(tasks)
@@ -8340,9 +8512,9 @@ function mergeSnapshot(daemon, external, now) {
8340
8512
  // packages/web/dist/probes.js
8341
8513
  init_dist();
8342
8514
  init_dist();
8343
- import { join as join21 } from "path";
8344
- import { homedir as homedir15 } from "os";
8345
- import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
8515
+ import { join as join22 } from "path";
8516
+ import { homedir as homedir17 } from "os";
8517
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
8346
8518
  import { execFile as execFile3 } from "child_process";
8347
8519
  var DEFAULT_TIMEOUT_MS = 2e3;
8348
8520
  var AGENT_CLIS = ["claude", "codex", "gemini", "opencode"];
@@ -8378,7 +8550,7 @@ function vaultProbe(run, dir) {
8378
8550
  return { state: "unknown", detail: "no vault configured" };
8379
8551
  if (!run.pathExists(dir))
8380
8552
  return { state: "gone", detail: "vault directory missing" };
8381
- if (!run.pathExists(join21(dir, ".obsidian")))
8553
+ if (!run.pathExists(join22(dir, ".obsidian")))
8382
8554
  return { state: "gone", detail: "no .obsidian/ (not a vault)" };
8383
8555
  return { state: "alive" };
8384
8556
  } catch {
@@ -8446,13 +8618,13 @@ async function runExternalProbes(run, timeoutMs = DEFAULT_TIMEOUT_MS) {
8446
8618
  const sessions = probeSessions(run);
8447
8619
  return { cmux: cmux2, agentClis, vaults, config: { parseable, projectPaths, sessions } };
8448
8620
  }
8449
- var SESSIONS_PATH = join21(homedir15(), ".config", "squadrant", "sessions.json");
8621
+ var SESSIONS_PATH = join22(homedir17(), ".config", "squadrant", "sessions.json");
8450
8622
  function onPath(cli) {
8451
8623
  const dirs = (process.env.PATH ?? "").split(":").filter(Boolean);
8452
- return dirs.some((d) => existsSync11(join21(d, cli)));
8624
+ return dirs.some((d) => existsSync11(join22(d, cli)));
8453
8625
  }
8454
8626
  function readSessionsHashes() {
8455
- const raw = JSON.parse(readFileSync12(SESSIONS_PATH, "utf-8"));
8627
+ const raw = JSON.parse(readFileSync14(SESSIONS_PATH, "utf-8"));
8456
8628
  const hashes = Object.values(raw.workspaces ?? {}).map((w) => w.templateHash).filter((h) => typeof h === "string" && h.length > 0);
8457
8629
  return [...new Set(hashes)];
8458
8630
  }
@@ -8661,6 +8833,16 @@ function renderOverview(snap, col) {
8661
8833
  out.push(`</section>`);
8662
8834
  return out.join("");
8663
8835
  }
8836
+ function filterBar() {
8837
+ return [
8838
+ `<div class="filter-bar" role="group" aria-label="Filter projects by status">`,
8839
+ `<button class="filter-btn on" data-filter="all" aria-pressed="true">All</button>`,
8840
+ `<button class="filter-btn" data-filter="alive" aria-pressed="false">Nominal</button>`,
8841
+ `<button class="filter-btn" data-filter="caution" aria-pressed="false">Caution</button>`,
8842
+ `<button class="filter-btn" data-filter="offline" aria-pressed="false">Offline</button>`,
8843
+ `</div>`
8844
+ ].join("");
8845
+ }
8664
8846
  function componentRow(c, now) {
8665
8847
  const detail = c.detail ? esc(c.detail) : "";
8666
8848
  const row = `<tr><td>${statePill(c.state)}</td><td class="mono">${esc(c.kind)}</td><td class="mono">${esc(c.ref)}</td><td class="mono dim">${esc(ageText(c.lastSeenMs, now))}</td><td class="dim">${detail}</td></tr>`;
@@ -8685,6 +8867,7 @@ function renderProjects(snap, now) {
8685
8867
  out.push(`<div class="empty">Telemetry link lost \u2014 per-project data is served by the daemon. Start it to restore: <code>squadrant heal daemon</code></div></section>`);
8686
8868
  return out.join("");
8687
8869
  }
8870
+ out.push(filterBar());
8688
8871
  const d = snap.daemon;
8689
8872
  const projects = /* @__PURE__ */ new Set([...d.tier1.map((c) => c.project), ...d.tier2.projects.map((p) => p.project)]);
8690
8873
  if (projects.size === 0)
@@ -8701,8 +8884,10 @@ function renderProjects(snap, now) {
8701
8884
  deferralState
8702
8885
  ];
8703
8886
  const rollup = worst(rollupStates);
8887
+ const captainComp = comps.find((c) => c.kind === "captain");
8888
+ const captainState = captainComp?.state ?? "unknown";
8704
8889
  out.push(`<article class="card" data-rollup="${rollup}">`);
8705
- out.push(`<header class="card-head"><span class="card-title">${esc(project)}</span>${statePill(rollup)}</header>`);
8890
+ out.push(`<header class="card-head"><span class="card-title">${esc(project)}</span><div class="card-badges"><span class="captain-badge">captain ${statePill(captainState)}</span>${statePill(rollup)}</div></header>`);
8706
8891
  if (comps.length) {
8707
8892
  out.push(`<table class="grid"><thead><tr><th>status</th><th>kind</th><th>ref</th><th>last seen</th><th>detail</th></tr></thead><tbody>`, ...comps.map((c) => componentRow(c, now)), `</tbody></table>`);
8708
8893
  } else {
@@ -8783,7 +8968,131 @@ function renderEnv(ext) {
8783
8968
  out.push(`</section>`);
8784
8969
  return out.join("");
8785
8970
  }
8971
+ var ATTN_ORDER = { stale: 0, gone: 1, alive: 2, unknown: 3, stopped: 4 };
8972
+ var STATE_LABEL = { alive: "running", stale: "blocked", gone: "errored", unknown: "idle", stopped: "offline" };
8973
+ var STATE_CLS = { alive: "s-alive", stale: "s-stale", gone: "s-gone", unknown: "s-unknown", stopped: "s-stopped" };
8974
+ var STATE_ORDER = ["alive", "stale", "gone", "unknown", "stopped"];
8975
+ var TASK_ICON = { done: "\u2713", working: "\u25B6", blocked: "\u25CF", failed: "\u2715", cancelled: "\u2299" };
8976
+ function renderLiveGrid(snap, now) {
8977
+ const out = [`<section class="panel" data-panel="live" role="tabpanel" aria-label="Live">`];
8978
+ if (snap.daemon === "unreachable") {
8979
+ out.push(`<div class="empty">Telemetry link lost \u2014 per-project data is served by the daemon. Start it to restore: <code>squadrant heal daemon</code></div></section>`);
8980
+ return out.join("");
8981
+ }
8982
+ const d = snap.daemon;
8983
+ const projects = /* @__PURE__ */ new Set([...d.tier1.map((c) => c.project), ...d.tier2.projects.map((p) => p.project)]);
8984
+ if (projects.size === 0) {
8985
+ out.push(`<div class="empty">No projects registered yet.</div>`);
8986
+ } else {
8987
+ const rows = [];
8988
+ const stateCounts = {};
8989
+ for (const s of STATE_ORDER)
8990
+ stateCounts[s] = 0;
8991
+ for (const project of projects) {
8992
+ const comps = d.tier1.filter((c) => c.project === project);
8993
+ const dp = d.tier2.projects.find((p) => p.project === project);
8994
+ const captainStopped = comps.some((c) => c.kind === "captain" && c.state === "stopped");
8995
+ const deferralState = !captainStopped && dp && dp.deferral.stuck ? "gone" : !captainStopped && dp && dp.deferral.maxDeferCount > 0 ? "stale" : "alive";
8996
+ const rollupStates = [
8997
+ ...comps.map((c) => c.state),
8998
+ !captainStopped && dp && dp.delivery.behind > 0 ? "stale" : "alive",
8999
+ dp && dp.store.corruptCount > 0 ? "gone" : "alive",
9000
+ deferralState
9001
+ ];
9002
+ const rollup = worst(rollupStates);
9003
+ const captainComp = comps.find((c) => c.kind === "captain");
9004
+ const captainState = captainComp?.state ?? "unknown";
9005
+ const crews = comps.filter((c) => c.kind === "crew");
9006
+ let detail = "";
9007
+ if (crews.length > 0) {
9008
+ const alive = crews.filter((c) => c.state === "alive");
9009
+ const other = crews.filter((c) => c.state !== "alive");
9010
+ const parts = [];
9011
+ if (alive.length > 0)
9012
+ parts.push(`${alive.length} working \u2014 ${alive.map((c) => c.ref).join("; ")}`);
9013
+ for (const c of other)
9014
+ parts.push(`${c.state} \u2014 ${c.ref}`);
9015
+ detail = parts.join(", ");
9016
+ } else if (captainState === "alive") {
9017
+ detail = "captain idle";
9018
+ }
9019
+ rows.push({ name: project, rollup, captainState, detail, crews, tasks: dp?.store.byState ?? {} });
9020
+ stateCounts[rollup]++;
9021
+ }
9022
+ const headerLabels = STATE_ORDER.map((s) => `${STATE_LABEL[s]}`);
9023
+ out.push(`<div class="live-header" data-live-header="">`);
9024
+ out.push(headerLabels.map((l) => {
9025
+ const raw = Object.entries(STATE_LABEL).find(([, v]) => v === l)[0];
9026
+ const c = stateCounts[raw];
9027
+ const cls = c === 0 ? "zero" : "";
9028
+ return `<span class="live-stat ${cls}"><span class="pdot ${STATE_CLS[raw]}"></span>${c} ${l}</span>`;
9029
+ }).join(` <span class="live-sep">\xB7</span> `));
9030
+ out.push(`</div>`);
9031
+ rows.sort((a, b) => {
9032
+ const d2 = (ATTN_ORDER[a.rollup] ?? 99) - (ATTN_ORDER[b.rollup] ?? 99);
9033
+ return d2 !== 0 ? d2 : a.name.localeCompare(b.name);
9034
+ });
9035
+ out.push(`<div class="live-controls">`);
9036
+ out.push(`<input type="text" class="live-search" placeholder="Search projects or crews\u2026" data-live-search="">`);
9037
+ out.push(`<div class="filter-bar" role="group" aria-label="Filter live rows by status">`);
9038
+ for (const fb of [["all", "All"], ["alive", "Nominal"], ["caution", "Caution"], ["offline", "Offline"]]) {
9039
+ out.push(`<button class="filter-btn${fb[0] === "all" ? " on" : ""}" data-filter="${fb[0]}" data-live-filter="" aria-pressed="${fb[0] === "all" ? "true" : "false"}">${fb[1]}</button>`);
9040
+ }
9041
+ out.push(`</div></div>`);
9042
+ out.push(`<table class="live-grid" data-live-table=""><thead><tr class="lh-row">`);
9043
+ out.push(`<th class="lh-dot"></th><th class="lh-name" data-sort="name">Project</th><th class="lh-state" data-sort="attention">Status</th><th class="lh-detail">Activity</th><th class="lh-lastseen" data-sort="activity">Last Seen</th><th class="lh-tasks">Tasks</th>`);
9044
+ out.push(`</tr></thead><tbody>`);
9045
+ for (const row of rows) {
9046
+ let maxAge = -1;
9047
+ let maxSeenMs = null;
9048
+ for (const c of row.crews) {
9049
+ if (c.lastSeenMs != null) {
9050
+ const age = now - c.lastSeenMs;
9051
+ if (age > maxAge) {
9052
+ maxAge = age;
9053
+ maxSeenMs = c.lastSeenMs;
9054
+ }
9055
+ }
9056
+ }
9057
+ const crewAgeStr = maxSeenMs == null ? "\u2014" : ageText(maxSeenMs, now);
9058
+ const crewAgeQuiet = maxSeenMs != null && maxAge > CREW_STALE_MS;
9059
+ const tParts = [];
9060
+ for (const [st, n] of Object.entries(row.tasks)) {
9061
+ const icon = TASK_ICON[st] ?? st[0];
9062
+ tParts.push(`<span class="tk">${n}${esc(icon)}</span>`);
9063
+ }
9064
+ out.push(`<tr class="live-row" data-rollup="${row.rollup}" data-project="${esc(row.name)}" data-max-age="${maxAge}">`);
9065
+ out.push(`<td class="live-dot"><span class="pdot s-${row.rollup}"></span></td>`);
9066
+ out.push(`<td class="live-name">${esc(row.name)}</td>`);
9067
+ out.push(`<td class="live-state">${statePill(row.captainState)}</td>`);
9068
+ out.push(`<td class="live-detail dim">${esc(row.detail)}</td>`);
9069
+ out.push(`<td class="live-lastseen${crewAgeQuiet ? " quiet" : ""}">${crewAgeStr}</td>`);
9070
+ out.push(`<td class="live-tasks">${tParts.join("") || '<span class="dim">\u2014</span>'}</td>`);
9071
+ out.push(`</tr>`);
9072
+ out.push(`<tr class="live-expand" data-project="${esc(row.name)}" hidden>`);
9073
+ out.push(`<td colspan="6"><div class="live-crew-list">`);
9074
+ out.push(`<div class="live-crew-head"><span class="lc-name">Crew</span><span class="lc-state">Status</span><span class="lc-lastseen">Last Seen</span><span class="lc-detail">Detail</span></div>`);
9075
+ for (const c of row.crews) {
9076
+ const cs = ageText(c.lastSeenMs, now);
9077
+ out.push(`<div class="live-crew-row"><span class="lc-name">${esc(c.ref)}</span><span class="lc-state">${statePill(c.state)}</span><span class="lc-lastseen">${cs}</span><span class="lc-detail dim">${c.detail ? esc(c.detail) : ""}</span></div>`);
9078
+ }
9079
+ if (row.crews.length === 0) {
9080
+ out.push(`<div class="live-crew-row dim"><span class="lc-name" colspan="4">no live crews</span></div>`);
9081
+ }
9082
+ out.push(`</div><div class="live-crew-meta">`);
9083
+ out.push(`<span class="lc-meta-item">pid: \u2014 (incoming)</span>`);
9084
+ out.push(`<span class="lc-meta-item">uptime: \u2014 (incoming)</span>`);
9085
+ out.push(`<span class="lc-meta-item">agent: \u2014 (incoming)</span>`);
9086
+ out.push(`<span class="lc-meta-item">model: \u2014 (incoming)</span>`);
9087
+ out.push(`</div></td></tr>`);
9088
+ }
9089
+ out.push(`</tbody></table>`);
9090
+ }
9091
+ out.push(`</section>`);
9092
+ return out.join("");
9093
+ }
8786
9094
  var TABS = [
9095
+ ["live", "Live"],
8787
9096
  ["overview", "Overview"],
8788
9097
  ["projects", "Projects"],
8789
9098
  ["daemon", "Daemon"],
@@ -8831,6 +9140,7 @@ function renderContent(snap) {
8831
9140
  }
8832
9141
  out.push(tabNav());
8833
9142
  out.push(`<div class="panels">`);
9143
+ out.push(renderLiveGrid(snap, col.now));
8834
9144
  out.push(renderOverview(snap, col));
8835
9145
  out.push(renderProjects(snap, col.now));
8836
9146
  out.push(renderDaemon(snap));
@@ -8955,6 +9265,77 @@ body{margin:0;min-height:100vh;background:radial-gradient(1200px 760px at 50% -2
8955
9265
  .spark-area{fill:var(--hud);opacity:.1}
8956
9266
  .spark-dot{fill:var(--hud)}
8957
9267
 
9268
+ /* Live controls (search + filter bar) */
9269
+ .live-controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:4px 0 10px}
9270
+ .live-search{flex:1;min-width:140px;max-width:300px;padding:5px 10px;border:1px solid var(--bezel);border-radius:8px;background:var(--panel);color:var(--ink);font:12px/1.4 ui-monospace,monospace;outline:none;transition:border-color .15s ease}
9271
+ .live-search:focus{border-color:var(--hud);box-shadow:0 0 0 2px rgba(11,111,194,.15)}
9272
+ .live-search::placeholder{color:var(--ink-dim);opacity:.7}
9273
+
9274
+ /* Live grid (mission control) */
9275
+ .live-grid{width:100%;border-collapse:collapse;font-size:12px;table-layout:fixed}
9276
+ .live-grid td,.live-grid th{padding:5px 6px;vertical-align:middle;white-space:nowrap}
9277
+ .live-grid th{padding:6px 6px 8px;text-align:left;font-family:system-ui,sans-serif;text-transform:uppercase;letter-spacing:.1em;font-size:9px;color:var(--ink-dim);border-bottom:1px solid var(--bezel);font-weight:600;cursor:default}
9278
+ .live-grid th:hover{color:var(--ink)}
9279
+ .live-row td{border-bottom:1px solid var(--track)}
9280
+ .live-row:last-child td{border-bottom:0}
9281
+ .lh-dot{width:22px;text-align:center}
9282
+ .lh-name{width:auto;min-width:14%}
9283
+ .lh-state{width:88px;text-align:right;padding-right:12px!important}
9284
+ .lh-detail{width:auto}
9285
+ .lh-lastseen{width:105px}
9286
+ .lh-tasks{width:90px;text-align:right}
9287
+ .live-dot{width:22px;text-align:center;padding-right:0!important}
9288
+ .live-dot .pdot{display:inline-block;vertical-align:middle}
9289
+ .live-name{font-weight:600}
9290
+ .live-state{width:88px;text-align:right;padding-right:12px!important}
9291
+ .live-state .pill{font-size:10px;padding:1px 7px}
9292
+ .live-detail{overflow:hidden;text-overflow:ellipsis;color:var(--ink-dim)}
9293
+ .live-lastseen{width:105px;font-size:11px;color:var(--ink-dim)}
9294
+ .live-lastseen.quiet{color:var(--warn);font-weight:600}
9295
+ .live-tasks{width:90px;text-align:right;font-size:11px}
9296
+ .live-tasks .tk{padding:0 3px}
9297
+ .live-tasks .tk:first-child{padding-left:0}
9298
+ .live-row.expanded td{border-bottom-color:transparent}
9299
+ .live-expand td{padding:0 6px 10px;background:var(--panel-2);border-bottom:1px solid var(--track)}
9300
+ .live-expand td:first-child{border-radius:0 0 0 8px}
9301
+ .live-expand td:last-child{border-radius:0 0 8px 0}
9302
+ .live-crew-list{display:table;width:100%;border-collapse:collapse;font-size:12px}
9303
+ .live-crew-head,.live-crew-row{display:table-row}
9304
+ .live-crew-head>span,.live-crew-row>span{display:table-cell;padding:4px 8px;white-space:nowrap;vertical-align:middle}
9305
+ .live-crew-head>span{font-family:system-ui,sans-serif;text-transform:uppercase;letter-spacing:.1em;font-size:9px;color:var(--ink-dim);font-weight:600;border-bottom:1px solid var(--bezel)}
9306
+ .live-crew-row>span{border-bottom:1px solid var(--track)}
9307
+ .live-crew-row:last-child>span{border-bottom:0}
9308
+ .lc-name{font-weight:600}
9309
+ .lc-state{width:80px}
9310
+ .lc-state .pill{font-size:10px;padding:1px 7px}
9311
+ .lc-lastseen{width:90px;color:var(--ink-dim);font-size:11px}
9312
+ .lc-detail{color:var(--ink-dim);font-size:11px}
9313
+ .live-crew-meta{display:flex;gap:14px;padding:6px 8px 0;font-size:11px;color:var(--ink-dim);border-top:1px solid var(--track);margin-top:4px;padding-top:6px}
9314
+ .lc-meta-item{white-space:nowrap}
9315
+ .live-row.expanded+.live-expand td{background:var(--panel-2)}
9316
+ .live-row[data-rollup="gone"] td.live-dot .pdot{background:var(--crit);box-shadow:0 0 6px var(--crit)}
9317
+ .live-row[data-rollup="stale"] td.live-dot .pdot{background:var(--warn);box-shadow:0 0 6px var(--warn)}
9318
+ .live-row[data-rollup="stopped"] td.live-dot .pdot{background:var(--stopped)}
9319
+ .live-row[data-rollup="alive"] td.live-dot .pdot{background:var(--ok);box-shadow:0 0 6px var(--ok)}
9320
+ .live-row[data-rollup="unknown"] td.live-dot .pdot{background:var(--unk)}
9321
+ .live-row:hover{background:var(--panel-2)}
9322
+ .live-header{display:flex;gap:8px 16px;flex-wrap:wrap;padding:6px 0 10px;font-size:12px;cursor:default;user-select:none}
9323
+ .live-stat{display:flex;align-items:center;gap:5px;font-weight:600}
9324
+ .live-stat .pdot{width:6px;height:6px}
9325
+ .live-stat.zero{opacity:.35}
9326
+ .live-sep{color:var(--ink-dim);opacity:.4}
9327
+
9328
+ /* Filter bar */
9329
+ .filter-bar{display:flex;gap:6px;margin:0 0 14px;flex-wrap:wrap}
9330
+ .filter-btn{appearance:none;background:var(--panel-2);border:1px solid var(--bezel);border-radius:999px;
9331
+ padding:4px 13px;cursor:pointer;font-family:system-ui,sans-serif;text-transform:uppercase;letter-spacing:.1em;
9332
+ font-size:10px;font-weight:600;color:var(--ink-dim);transition:all .15s ease}
9333
+ .filter-btn:hover{background:var(--panel);border-color:var(--ink-dim);color:var(--ink)}
9334
+ .filter-btn.on{background:var(--hud);border-color:var(--hud);color:#fff;box-shadow:0 1px 3px rgba(11,111,194,.3)}
9335
+ .card-badges{display:flex;align-items:center;gap:8px;flex:none}
9336
+ .captain-badge{display:flex;align-items:center;gap:4px;font-size:10px;color:var(--ink-dim);letter-spacing:.04em}
9337
+ .captain-badge .pill{font-size:10px;padding:1px 7px}
9338
+
8958
9339
  /* Cards / tables */
8959
9340
  .card{border:1px solid var(--bezel);border-radius:12px;background:var(--panel);box-shadow:var(--shadow);padding:14px 16px;margin-bottom:14px;border-left:3px solid var(--bezel)}
8960
9341
  .card[data-rollup="gone"]{border-left-color:var(--crit)}
@@ -9017,7 +9398,7 @@ body{margin:0;min-height:100vh;background:radial-gradient(1200px 760px at 50% -2
9017
9398
  `.trim();
9018
9399
  var CLIENT_JS = `
9019
9400
  (function(){
9020
- var activeTab='overview';var hist={};var prev={};
9401
+ var activeTab='live';var hist={};var prev={};var currentFilter='all';var currentSort='attention';var sortAsc=true;var currentSearch='';var expandedProjects={};
9021
9402
  function pushHist(k,v,t){var a=hist[k]||(hist[k]=[]);if(a.length&&a[a.length-1].t===t)return;a.push({t:t,v:v});if(a.length>48)a.shift();}
9022
9403
  function ingest(){var el=document.getElementById('squadrant-metrics');if(!el)return;var m;try{m=JSON.parse(el.textContent);}catch(e){return;}pushHist('errors',m.errors,m.t);pushHist('behind',m.behind,m.t);pushHist('crewAge',Math.round(m.crewAgeMs/1000),m.t);pushHist('defers',m.maxDeferCount,m.t);}
9023
9404
  function sparkSVG(a){var W=100,H=28,p=2;if(!a.length)return'';var vs=a.map(function(x){return x.v;});var mx=Math.max.apply(null,vs),mn=Math.min.apply(null,vs);if(mx===mn)mx=mn+1;var n=a.length;var pts=a.map(function(x,i){var px=n>1?p+i/(n-1)*(W-2*p):W/2;var py=H-p-(x.v-mn)/(mx-mn)*(H-2*p);return[px,py];});var d=pts.map(function(pt,i){return(i?'L':'M')+pt[0].toFixed(1)+' '+pt[1].toFixed(1);}).join(' ');var last=pts[pts.length-1];var area=d+' L'+last[0].toFixed(1)+' '+H+' L'+pts[0][0].toFixed(1)+' '+H+' Z';return'<path class="spark-area" d="'+area+'"/><path class="spark-line" d="'+d+'"/><circle class="spark-dot" cx="'+last[0].toFixed(1)+'" cy="'+last[1].toFixed(1)+'" r="2"/>';}
@@ -9025,9 +9406,15 @@ var CLIENT_JS = `
9025
9406
  function applyTab(){var ts=document.querySelectorAll('[data-tab]');for(var i=0;i<ts.length;i++){var on=ts[i].getAttribute('data-tab')===activeTab;ts[i].setAttribute('aria-selected',on?'true':'false');ts[i].classList.toggle('on',on);}var ps=document.querySelectorAll('[data-panel]');for(var j=0;j<ps.length;j++){var on2=ps[j].getAttribute('data-panel')===activeTab;ps[j].hidden=!on2;}}
9026
9407
  function ease(k){return k<.5?2*k*k:1-Math.pow(-2*k+2,2)/2;}
9027
9408
  function countUps(){var ns=document.querySelectorAll('[data-countup]');for(var i=0;i<ns.length;i++){(function(el){var key=el.getAttribute('data-countup');var to=parseFloat(el.getAttribute('data-value'))||0;var from=prev[key]!=null?prev[key]:0;prev[key]=to;if(from===to){el.textContent=to;return;}var s=null;function step(ts){if(s===null)s=ts;var k=Math.min(1,(ts-s)/600);el.textContent=Math.round(from+(to-from)*ease(k));if(k<1)requestAnimationFrame(step);}requestAnimationFrame(step);})(ns[i]);}}
9028
- function refresh(){ingest();applyTab();drawSparks();countUps();}
9029
- document.addEventListener('click',function(e){var t=e.target&&e.target.closest?e.target.closest('[data-tab]'):null;if(t){activeTab=t.getAttribute('data-tab');applyTab();var c=document.getElementById('content');if(c){c.classList.remove('swap');void c.offsetWidth;c.classList.add('swap');}}});
9030
- document.addEventListener('keydown',function(e){var ae=document.activeElement;if((e.key==='ArrowRight'||e.key==='ArrowLeft')&&ae&&ae.getAttribute&&ae.getAttribute('data-tab')){var o=['overview','projects','daemon','environment'];var i=o.indexOf(activeTab);i=(i+(e.key==='ArrowRight'?1:o.length-1))%o.length;activeTab=o[i];applyTab();var nt=document.querySelector('[data-tab="'+activeTab+'"]');if(nt)nt.focus();e.preventDefault();}});
9409
+ function hideByFilter(r,filter){if(filter==='all')return false;if(filter==='alive')return r!=='alive';if(filter==='caution')return r!=='stale';if(filter==='offline')return r!=='gone'&&r!=='stopped'&&r!=='unknown';return false;}
9410
+ function applyFilter(){document.querySelectorAll('[data-panel="projects"] .card').forEach(function(c){c.hidden=hideByFilter(c.getAttribute('data-rollup'),currentFilter);});document.querySelectorAll('[data-panel="live"] .live-row').forEach(function(c){c.hidden=hideByFilter(c.getAttribute('data-rollup'),currentFilter);});document.querySelectorAll('.filter-btn').forEach(function(x){var on=x.getAttribute('data-filter')===currentFilter;x.classList.toggle('on',on);x.setAttribute('aria-pressed',on?'true':'false');});}
9411
+ function applySearch(){var q=currentSearch.toLowerCase();document.querySelectorAll('[data-panel="live"] .live-row').forEach(function(r){if(!q){if(!hideByFilter(r.getAttribute('data-rollup'),currentFilter))r.hidden=false;return;}var pn=(r.getAttribute('data-project')||'').toLowerCase();var dd=((r.querySelector('.live-detail')||{}).textContent||'').toLowerCase();var rl=r.getAttribute('data-rollup');if(hideByFilter(rl,currentFilter)){r.hidden=true;return;}r.hidden=pn.indexOf(q)===-1&&dd.indexOf(q)===-1;});}
9412
+ function applySort(){var table=document.querySelector('[data-panel="live"] [data-live-table]');if(!table)return;var tbody=table.querySelector('tbody');if(!tbody)return;var rows=Array.prototype.slice.call(tbody.querySelectorAll('.live-row'));var dir=sortAsc?1:-1;rows.sort(function(a,b){var va,vb;if(currentSort==='name'){va=(a.getAttribute('data-project')||'').toLowerCase();vb=(b.getAttribute('data-project')||'').toLowerCase();}else if(currentSort==='attention'){var order={gone:0,stale:1,alive:2,unknown:3,stopped:4};va=order[a.getAttribute('data-rollup')]??99;vb=order[b.getAttribute('data-rollup')]??99;if(va===vb){va=(a.getAttribute('data-project')||'').toLowerCase();vb=(b.getAttribute('data-project')||'').toLowerCase();return va<vb?-1:va>vb?1:0;}}else if(currentSort==='activity'){va=parseInt(a.getAttribute('data-max-age'))||0;vb=parseInt(b.getAttribute('data-max-age'))||0;}return va<vb?-dir:va>vb?dir:0;});rows.forEach(function(r){tbody.appendChild(r);});}
9413
+ function applyExpand(){document.querySelectorAll('[data-panel="live"] .live-row').forEach(function(r){var pn=r.getAttribute('data-project');var er=document.querySelector('[data-panel="live"] .live-expand[data-project="'+pn+'"]');if(er){var show=!!expandedProjects[pn];er.hidden=!show;r.classList.toggle('expanded',show);}});}
9414
+ function refresh(){ingest();applyTab();drawSparks();countUps();applyFilter();applySearch();applySort();applyExpand();}
9415
+ document.addEventListener('click',function(e){var t=e.target&&e.target.closest?e.target.closest('[data-tab]'):null;if(t){activeTab=t.getAttribute('data-tab');applyTab();var c=document.getElementById('content');if(c){c.classList.remove('swap');void c.offsetWidth;c.classList.add('swap');}}var fb=e.target&&e.target.closest?e.target.closest('[data-filter]'):null;if(fb){currentFilter=fb.getAttribute('data-filter');applyFilter();applySearch();}var sh=e.target&&e.target.closest?e.target.closest('[data-sort]'):null;if(sh){var key=sh.getAttribute('data-sort');if(key===currentSort)sortAsc=!sortAsc;else{currentSort=key;sortAsc=true;}applySort();}var lr=e.target&&e.target.closest?e.target.closest('[data-panel="live"] .live-row'):null;if(lr&&!e.target.closest('[data-sort]')){var pn=lr.getAttribute('data-project');var er=document.querySelector('[data-panel="live"] .live-expand[data-project="'+pn+'"]');if(er){var show=er.hidden;er.hidden=!show;lr.classList.toggle('expanded',!!show);expandedProjects[pn]=!!show;}}});
9416
+ document.addEventListener('keydown',function(e){var ae=document.activeElement;if((e.key==='ArrowRight'||e.key==='ArrowLeft')&&ae&&ae.getAttribute&&ae.getAttribute('data-tab')){var o=['live','overview','projects','daemon','environment'];var i=o.indexOf(activeTab);i=(i+(e.key==='ArrowRight'?1:o.length-1))%o.length;activeTab=o[i];applyTab();var nt=document.querySelector('[data-tab="'+activeTab+'"]');if(nt)nt.focus();e.preventDefault();}});
9417
+ document.addEventListener('input',function(e){var sb=e.target&&e.target.closest?e.target.closest('[data-live-search]'):null;if(sb){currentSearch=sb.value;applySearch();}});
9031
9418
  var led=document.getElementById('led');var conn=document.getElementById('conn');var updated=document.getElementById('updated');
9032
9419
  function tickAge(){if(!generatedAt)return;var s=Math.max(0,Math.round((Date.now()-generatedAt)/1000));updated.textContent='updated '+s+'s ago';}
9033
9420
  setInterval(tickAge,1000);
@@ -9142,7 +9529,7 @@ async function startWebServer(opts) {
9142
9529
 
9143
9530
  // packages/cli/src/commands/dashboard.ts
9144
9531
  init_dist();
9145
- var SOCK3 = join22(homedir16(), ".config", "squadrant", "squadrant.sock");
9532
+ var SOCK3 = join23(homedir18(), ".config", "squadrant", "squadrant.sock");
9146
9533
  function detectCurrentWorkspace2() {
9147
9534
  const out = execSync10(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
9148
9535
  const match = out.match(/workspace:\d+/);
@@ -9310,8 +9697,8 @@ init_dist2();
9310
9697
  var CMUX_APP = "/Applications/cmux.app";
9311
9698
  var TEMPLATES_DIR4 = path23.join(os12.homedir(), ".config", "squadrant", "templates");
9312
9699
  var SESSIONS_PATH2 = path23.join(os12.homedir(), ".config", "squadrant", "sessions.json");
9313
- function ensureCmuxReady() {
9314
- if (isInsideCmux()) return;
9700
+ function ensureCmuxReady(headless) {
9701
+ if (headless || isInsideCmux()) return;
9315
9702
  console.log(chalk13.yellow("\n Not running inside cmux. Opening cmux app...\n"));
9316
9703
  execSync11(`open "${CMUX_APP}"`, { stdio: "inherit" });
9317
9704
  console.log(chalk13.bold(" Run `squadrant launch` from inside a cmux workspace.\n"));
@@ -9319,8 +9706,9 @@ function ensureCmuxReady() {
9319
9706
  }
9320
9707
  var launchCommand = new Command12("launch").description(
9321
9708
  "Launch a project captain (with project arg) or all captains (--all). Use `squadrant command` for one-shot Command tasks."
9322
- ).argument("[project]", "Project name to launch captain for").option("--fresh", "Start a new session instead of resuming the last one").option("--all", "Launch all captain workspaces").action(async (project, opts) => {
9709
+ ).argument("[project]", "Project name to launch captain for").option("--fresh", "Start a new session instead of resuming the last one").option("--all", "Launch all captain workspaces").option("--headless", "Skip the interactive cmux-app requirement (used by the daemon to boot captains without a terminal)").action(async (project, opts) => {
9323
9710
  const config = loadConfig();
9711
+ let hadFailure = false;
9324
9712
  const drivers = {
9325
9713
  claude: createClaudeDriver(),
9326
9714
  codex: createCodexDriver(),
@@ -9330,7 +9718,7 @@ var launchCommand = new Command12("launch").description(
9330
9718
  const registry = new CapabilityRegistry(drivers);
9331
9719
  const runtimes = new RuntimeRegistry({ cmux: createCmuxDriver() });
9332
9720
  async function launchOne(workspaceName, role, cwd, permissionMode, navigate, pinToTop = false, projectName) {
9333
- ensureCmuxReady();
9721
+ ensureCmuxReady(!!opts.headless);
9334
9722
  const roleConfig = config.defaults.roles?.[role];
9335
9723
  const agentName = roleConfig?.agent || "claude";
9336
9724
  const model = roleConfig?.model || config.defaults.models?.[role];
@@ -9370,6 +9758,7 @@ var launchCommand = new Command12("launch").description(
9370
9758
  });
9371
9759
  } catch (err) {
9372
9760
  console.error(chalk13.red(` \u2718 Failed: ${err.message}`));
9761
+ hadFailure = true;
9373
9762
  }
9374
9763
  }
9375
9764
  if (opts.all) {
@@ -9398,7 +9787,7 @@ var launchCommand = new Command12("launch").description(
9398
9787
  );
9399
9788
  process.exit(1);
9400
9789
  }
9401
- ensureCmuxReady();
9790
+ ensureCmuxReady(!!opts.headless);
9402
9791
  const sessions = loadSessions(SESSIONS_PATH2);
9403
9792
  const entries = Object.entries(config.projects).map(([name, proj]) => ({
9404
9793
  projectName: name,
@@ -9455,6 +9844,7 @@ Launching captain workspace for '${project}' (${proj.captainName})
9455
9844
  );
9456
9845
  await launchOne(proj.captainName, "captain", projPath, config.defaults.permissions?.captain || "auto", false, true, project);
9457
9846
  }
9847
+ if (hadFailure) process.exitCode = 1;
9458
9848
  });
9459
9849
 
9460
9850
  // packages/cli/src/commands/shutdown.ts
@@ -10423,7 +10813,7 @@ init_dist2();
10423
10813
  import { Command as Command22 } from "commander";
10424
10814
  import fs25 from "fs";
10425
10815
  import { fileURLToPath as fileURLToPath5 } from "url";
10426
- import { dirname as dirname5, join as join23 } from "path";
10816
+ import { dirname as dirname6, join as join24 } from "path";
10427
10817
  import chalk22 from "chalk";
10428
10818
  function runConfigCheck(opts) {
10429
10819
  const raw = JSON.parse(fs25.readFileSync(opts.configPath, "utf-8"));
@@ -10559,7 +10949,7 @@ configCommand.command("set").description("Write a config value by dotted key (e.
10559
10949
  }
10560
10950
  });
10561
10951
  function readPkgVersion2() {
10562
- const pkgPath = join23(dirname5(fileURLToPath5(import.meta.url)), "..", "package.json");
10952
+ const pkgPath = join24(dirname6(fileURLToPath5(import.meta.url)), "..", "package.json");
10563
10953
  return JSON.parse(fs25.readFileSync(pkgPath, "utf-8")).version;
10564
10954
  }
10565
10955
 
@@ -10656,49 +11046,85 @@ var healCommand = new Command23("heal").description("Targeted, idempotent remedi
10656
11046
  );
10657
11047
 
10658
11048
  // packages/cli/src/commands/group.ts
11049
+ import { Command as Command25 } from "commander";
11050
+ import chalk25 from "chalk";
11051
+
11052
+ // packages/cli/src/commands/dispatch.ts
10659
11053
  init_dist();
10660
11054
  init_dist2();
10661
- init_dist2();
10662
11055
  import { Command as Command24 } from "commander";
10663
11056
  import { execSync as execSync13 } from "child_process";
10664
11057
  import chalk24 from "chalk";
10665
- var groupCommand = new Command24("group").description("Cross-project intra-group operations (Phase 1: dispatch)").addCommand(
10666
- new Command24("dispatch").description("Dispatch a task to a sibling project in the same group").argument("<to-project>", "Target project name (must be in the same group)").argument("<task>", "Task description to dispatch").option("--provider <p>", "claude|opencode|codex", "claude").option("--mode <m>", "headless|interactive", "headless").option("--warmup-timeout <s>", "seconds to wait for target captain relay to boot (default: 120)", (v) => parseInt(v, 10) * 1e3).action(async (toProject, task, opts) => {
10667
- const fromProject = resolveCurrentProject(loadConfig());
10668
- if (!fromProject) {
10669
- console.error(chalk24.red("Could not determine current project from cwd. Run from inside a registered project directory."));
10670
- process.exit(1);
10671
- }
10672
- try {
10673
- const result = await dispatchToSibling({
10674
- fromProject,
10675
- toProject,
10676
- task,
10677
- provider: opts.provider,
10678
- mode: opts.mode,
10679
- warmupTimeoutMs: opts.warmupTimeout,
10680
- bootCaptain: async (project) => {
10681
- try {
10682
- execSync13(`squadrant launch ${project}`, { stdio: "ignore", timeout: 15e3 });
10683
- } catch {
10684
- throw new Error(`failed to launch captain for '${project}' \u2014 is squadrant installed?`);
10685
- }
10686
- }
10687
- });
10688
- console.log(chalk24.green(`\u2714 Dispatched to '${toProject}' (task ${result.id.slice(0, 8)})`));
10689
- console.log(chalk24.dim(` originProject: ${result.originProject ?? "none"}`));
10690
- console.log(chalk24.dim(" You will be notified when the task settles (done/blocked/failed)."));
10691
- } catch (e) {
10692
- console.error(chalk24.red(`\u2718 ${e.message}`));
10693
- process.exit(1);
11058
+ async function runDispatch(toProject, task, opts) {
11059
+ const fromProject = resolveCurrentProject(loadConfig());
11060
+ if (!fromProject) {
11061
+ throw new Error("Could not determine current project from cwd. Run from inside a registered project directory.");
11062
+ }
11063
+ return dispatchToSibling({
11064
+ fromProject,
11065
+ toProject,
11066
+ task,
11067
+ provider: opts.provider,
11068
+ mode: opts.mode,
11069
+ warmupTimeoutMs: opts.warmupTimeout,
11070
+ bootCaptain: async (project) => {
11071
+ try {
11072
+ execSync13(`squadrant launch ${project}`, { stdio: "ignore", timeout: 15e3 });
11073
+ } catch {
11074
+ throw new Error(`failed to launch captain for '${project}' \u2014 is squadrant installed?`);
11075
+ }
10694
11076
  }
11077
+ });
11078
+ }
11079
+ async function dispatchAction(toProject, task, opts) {
11080
+ try {
11081
+ const result = await runDispatch(toProject, task, opts);
11082
+ console.log(chalk24.green(`\u2714 Dispatched to '${toProject}' (task ${result.id.slice(0, 8)})`));
11083
+ console.log(chalk24.dim(` originProject: ${result.originProject ?? "none"}`));
11084
+ console.log(chalk24.dim(" You will be notified when the task settles (done/blocked/failed)."));
11085
+ } catch (e) {
11086
+ console.error(chalk24.red(`\u2718 ${e.message}`));
11087
+ process.exit(1);
11088
+ }
11089
+ }
11090
+ var dispatchCommand = new Command24("dispatch").description("Dispatch a task to any registered project (tracked, reports back on settle)").argument("<project>", "Target project name").argument("<task>", "Task description to dispatch").option("--provider <p>", "claude|opencode|codex", "claude").option("--mode <m>", "headless|interactive", "headless").option("--warmup-timeout <s>", "seconds to wait for target captain to boot (same-group only; default: 120)", (v) => parseInt(v, 10) * 1e3).action(dispatchAction);
11091
+
11092
+ // packages/cli/src/commands/group.ts
11093
+ init_dist2();
11094
+ var groupCommand = new Command25("group").description("Cross-project intra-group operations (Phase 1: dispatch)").addCommand(
11095
+ new Command25("dispatch").description("[DEPRECATED \u2014 use 'squadrant dispatch'] Dispatch a task to a sibling project in the same group").argument("<to-project>", "Target project name").argument("<task>", "Task description to dispatch").option("--provider <p>", "claude|opencode|codex", "claude").option("--mode <m>", "headless|interactive", "headless").option("--warmup-timeout <s>", "seconds to wait for target captain to boot (default: 120)", (v) => parseInt(v, 10) * 1e3).action(async (toProject, task, opts) => {
11096
+ console.error(chalk25.yellow(
11097
+ `\u26A0 'squadrant group dispatch' is deprecated \u2014 use 'squadrant dispatch <project> "<task>"' instead.`
11098
+ ));
11099
+ await dispatchAction(toProject, task, opts);
10695
11100
  })
10696
11101
  );
10697
11102
 
11103
+ // packages/cli/src/commands/ping.ts
11104
+ init_dist();
11105
+ import { Command as Command26 } from "commander";
11106
+ import chalk26 from "chalk";
11107
+ async function runPing(project, message) {
11108
+ const config = loadConfig();
11109
+ const registry = buildRegistry();
11110
+ const resolved = resolveTarget(registry, config, project, false);
11111
+ const ref = await needRef(resolved);
11112
+ await resolved.driver.send(ref, message);
11113
+ }
11114
+ var pingCommand = new Command26("ping").description("Fire-and-forget: deliver a message into a registered project's captain pane (no tracked task, no report-back)").argument("<project>", "Target project name (must be registered)").argument("<message>", "Message to deliver").action(async (project, message) => {
11115
+ try {
11116
+ await runPing(project, message);
11117
+ console.log(chalk26.green(`\u2714 Pinged '${project}'`));
11118
+ } catch (err) {
11119
+ console.error(chalk26.red(err.message));
11120
+ process.exit(1);
11121
+ }
11122
+ });
11123
+
10698
11124
  // packages/cli/src/commands/cmux.ts
10699
11125
  init_dist();
10700
- import { Command as Command25 } from "commander";
10701
- import chalk25 from "chalk";
11126
+ import { Command as Command27 } from "commander";
11127
+ import chalk27 from "chalk";
10702
11128
  async function runCmuxAutoconfig(opts) {
10703
11129
  const { json, stdout, stderr } = opts;
10704
11130
  let r;
@@ -10717,19 +11143,19 @@ async function runCmuxAutoconfig(opts) {
10717
11143
  stdout.write(`wrote cmux automation config \u2192 ${r.configPath}
10718
11144
  `);
10719
11145
  } else {
10720
- stdout.write(chalk25.dim(`cmux automation config already in place (${r.configPath})
11146
+ stdout.write(chalk27.dim(`cmux automation config already in place (${r.configPath})
10721
11147
  `));
10722
11148
  }
10723
11149
  if (r.verdict === "reachable") {
10724
- stdout.write(chalk25.green("\u2714 daemon-direct delivery is reachable \u2014 cmux control socket accepts the daemon\n"));
11150
+ stdout.write(chalk27.green("\u2714 daemon-direct delivery is reachable \u2014 cmux control socket accepts the daemon\n"));
10725
11151
  return 0;
10726
11152
  }
10727
11153
  if (r.needsRestart) {
10728
11154
  stdout.write(
10729
- chalk25.yellow("\u26A0 cmux is still on the old socket mode \u2014 restart cmux to enable daemon-direct delivery.\n")
11155
+ chalk27.yellow("\u26A0 cmux is still on the old socket mode \u2014 restart cmux to enable daemon-direct delivery.\n")
10730
11156
  );
10731
11157
  if (r.promptedThisRun) {
10732
- stdout.write(chalk25.dim(" (one-time prompt \u2014 you won't be nagged again)\n"));
11158
+ stdout.write(chalk27.dim(" (one-time prompt \u2014 you won't be nagged again)\n"));
10733
11159
  }
10734
11160
  return 2;
10735
11161
  }
@@ -10738,8 +11164,8 @@ async function runCmuxAutoconfig(opts) {
10738
11164
  );
10739
11165
  return 1;
10740
11166
  }
10741
- var cmuxCommand = new Command25("cmux").description("cmux integration helpers").addCommand(
10742
- new Command25("autoconfig").description(
11167
+ var cmuxCommand = new Command27("cmux").description("cmux integration helpers").addCommand(
11168
+ new Command27("autoconfig").description(
10743
11169
  "Write the cmux automation socket config and probe whether daemon-direct\ndelivery is reachable. Idempotent; prompts once if a cmux restart is needed."
10744
11170
  ).option("--json", "output machine-readable JSON (exit 0=reachable, 1=unknown, 2=restart-needed)").action(async (opts) => {
10745
11171
  const code = await runCmuxAutoconfig({
@@ -10755,17 +11181,17 @@ var cmuxCommand = new Command25("cmux").description("cmux integration helpers").
10755
11181
  init_dist();
10756
11182
  import fs26 from "fs";
10757
11183
  import path28 from "path";
10758
- import { Command as Command26 } from "commander";
10759
- import chalk26 from "chalk";
11184
+ import { Command as Command28 } from "commander";
11185
+ import chalk28 from "chalk";
10760
11186
  var VALID_EFFORTS = ["max", "balance", "low"];
10761
11187
  var EFFORT_MEANING = {
10762
11188
  max: "tokens are plentiful \u2014 bias crew spawns toward claude/opus",
10763
11189
  balance: "normal routing \u2014 use default crew routing rules unchanged",
10764
11190
  low: "conserve tokens \u2014 bias crew spawns toward opencode/sonnet; reserve opus for work that genuinely needs it"
10765
11191
  };
10766
- function runEffortGet(configPath = DEFAULT_CONFIG_PATH) {
11192
+ function runEffortGet(configPath = DEFAULT_CONFIG_PATH, projectName) {
10767
11193
  const config = loadConfig(configPath);
10768
- const effort = resolveEffort(config);
11194
+ const effort = resolveEffort(config, projectName);
10769
11195
  const description = `${effort}: ${EFFORT_MEANING[effort]}`;
10770
11196
  return { effort, description };
10771
11197
  }
@@ -10800,22 +11226,23 @@ async function notifyCaptainsOfEffort(effort, config, driver, cwd = process.cwd(
10800
11226
  }
10801
11227
  }
10802
11228
  }
10803
- var effortCommand = new Command26("effort").description("Get or set the global crew tokenomics dial (max | balance | low)").argument("[value]", "effort level to set: max | balance | low").action(async (value) => {
11229
+ var effortCommand = new Command28("effort").description("Get or set the global crew tokenomics dial (max | balance | low)").argument("[value]", "effort level to set: max | balance | low").option("--project <name>", "show resolved effort for a specific project").action(async (value, options) => {
10804
11230
  if (value === void 0) {
10805
- const { effort: effort2, description } = runEffortGet();
10806
- console.log(chalk26.bold("Current effort:"), chalk26.cyan(effort2));
10807
- console.log(chalk26.dim(EFFORT_MEANING[effort2]));
11231
+ const { effort: effort2, description } = runEffortGet(void 0, options.project);
11232
+ const label = options.project ? `${options.project} project` : "global";
11233
+ console.log(chalk28.bold(`Current effort (${label}):`), chalk28.cyan(effort2));
11234
+ console.log(chalk28.dim(EFFORT_MEANING[effort2]));
10808
11235
  return;
10809
11236
  }
10810
11237
  try {
10811
11238
  runEffortSet(value);
10812
11239
  } catch (err) {
10813
- console.error(chalk26.red(err.message));
11240
+ console.error(chalk28.red(err.message));
10814
11241
  process.exit(1);
10815
11242
  }
10816
11243
  const effort = value;
10817
- console.log(chalk26.green(`\u2714 effort \u2192 ${effort}`));
10818
- console.log(chalk26.dim(EFFORT_MEANING[effort]));
11244
+ console.log(chalk28.green(`\u2714 effort \u2192 ${effort}`));
11245
+ console.log(chalk28.dim(EFFORT_MEANING[effort]));
10819
11246
  try {
10820
11247
  const { createCmuxDriver: createCmuxDriver2, RuntimeRegistry: RuntimeRegistry2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports));
10821
11248
  const config = loadConfig();
@@ -10823,19 +11250,19 @@ var effortCommand = new Command26("effort").description("Get or set the global c
10823
11250
  const driver = registry.global(config);
10824
11251
  await notifyCaptainsOfEffort(effort, config, driver);
10825
11252
  } catch {
10826
- console.log(chalk26.dim("(no running captain detected \u2014 change applies on next launch)"));
11253
+ console.log(chalk28.dim("(no running captain detected \u2014 change applies on next launch)"));
10827
11254
  }
10828
11255
  });
10829
11256
 
10830
11257
  // packages/cli/src/commands/telegram.ts
10831
11258
  init_dist();
10832
11259
  init_dist2();
10833
- import { join as join24, dirname as dirname6 } from "path";
11260
+ import { join as join25, dirname as dirname7 } from "path";
10834
11261
  import { emitKeypressEvents } from "readline";
10835
- import { Command as Command27 } from "commander";
10836
- import chalk27 from "chalk";
11262
+ import { Command as Command29 } from "commander";
11263
+ import chalk29 from "chalk";
10837
11264
  function defaultStateRoot() {
10838
- return join24(dirname6(DEFAULT_CONFIG_PATH), "state");
11265
+ return join25(dirname7(DEFAULT_CONFIG_PATH), "state");
10839
11266
  }
10840
11267
  async function questionMasked() {
10841
11268
  return new Promise((resolve3) => {
@@ -10880,11 +11307,11 @@ async function questionYesNo(prompt) {
10880
11307
  });
10881
11308
  });
10882
11309
  }
10883
- var telegramCommand = new Command27("telegram").description("Two-way Telegram integration: push crew events to a topic and reply into the captain pane");
11310
+ var telegramCommand = new Command29("telegram").description("Two-way Telegram integration: push crew events to a topic and reply into the captain pane");
10884
11311
  telegramCommand.command("status").description("Show Telegram config and linked projects").action(() => {
10885
11312
  const { tokenSet, supergroupId, links } = runTelegramStatus({ config: loadConfig(), stateRoot: defaultStateRoot() });
10886
- console.log(`token: ${tokenSet ? chalk27.green("set") : chalk27.yellow("unset")}`);
10887
- console.log(`supergroup: ${supergroupId ?? chalk27.yellow("unset")}`);
11313
+ console.log(`token: ${tokenSet ? chalk29.green("set") : chalk29.yellow("unset")}`);
11314
+ console.log(`supergroup: ${supergroupId ?? chalk29.yellow("unset")}`);
10888
11315
  if (links.length === 0) {
10889
11316
  console.log("no projects linked");
10890
11317
  return;
@@ -10894,32 +11321,32 @@ telegramCommand.command("status").description("Show Telegram config and linked p
10894
11321
  telegramCommand.command("link").argument("<project>", "project to bind to a Telegram topic").description("Create (or reuse) a forum topic for a project and bind it").action(async (project) => {
10895
11322
  const cfg = loadConfig().telegram;
10896
11323
  if (!cfg) {
10897
- console.error(chalk27.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
11324
+ console.error(chalk29.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
10898
11325
  process.exit(1);
10899
11326
  }
10900
11327
  const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
10901
11328
  if (!token) {
10902
- console.error(chalk27.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
11329
+ console.error(chalk29.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
10903
11330
  process.exit(1);
10904
11331
  }
10905
11332
  const client = createTelegramClient({ token });
10906
11333
  const { topicId, created } = await runTelegramLink({ project, cfg, client, stateRoot: defaultStateRoot() });
10907
- console.log(chalk27.green(`${created ? "linked" : "already linked"}: ${project} \u2192 topic ${topicId}`));
11334
+ console.log(chalk29.green(`${created ? "linked" : "already linked"}: ${project} \u2192 topic ${topicId}`));
10908
11335
  });
10909
11336
  telegramCommand.command("setup").description("Interactive wizard \u2014 bot token, validate, auto-detect supergroup, write config").option("--reset-token", "force re-entry of the bot token even if one already exists").option("--redetect", "force group re-detection even when a supergroup is already configured").option("--user-id <id>", "allowlist user-id \u2014 enables remote control on a re-run without getUpdates detection", (v) => parseInt(v, 10)).action(async (opts) => {
10910
11337
  if (!process.stdin.isTTY) {
10911
- console.error(chalk27.red("setup requires a TTY \u2014 pipe input is not supported"));
11338
+ console.error(chalk29.red("setup requires a TTY \u2014 pipe input is not supported"));
10912
11339
  process.exit(1);
10913
11340
  }
10914
11341
  console.log();
10915
- console.log(chalk27.bold("Telegram setup") + " \u2014 connect squadrant to a Telegram bot for notifications + remote control");
11342
+ console.log(chalk29.bold("Telegram setup") + " \u2014 connect squadrant to a Telegram bot for notifications + remote control");
10916
11343
  console.log();
10917
11344
  console.log("Before you start you need:");
10918
11345
  console.log(" 1. A bot token from @BotFather (send /newbot)");
10919
11346
  console.log(" 2. A forum supergroup with the bot added as an admin (Topics enabled)");
10920
11347
  console.log(" 3. Bot privacy mode set to OFF (@BotFather \u2192 /setprivacy \u2192 Disable)");
10921
11348
  console.log();
10922
- console.log(chalk27.bold("Step 1/3 \u2014 Bot token"));
11349
+ console.log(chalk29.bold("Step 1/3 \u2014 Bot token"));
10923
11350
  const existingCfg = loadConfig().telegram;
10924
11351
  const existingToken = existingCfg?.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
10925
11352
  const decision = resolveSetupToken(existingToken, { resetToken: opts.resetToken ?? false });
@@ -10931,67 +11358,67 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
10931
11358
  try {
10932
11359
  botUser = await client.getMe();
10933
11360
  token = existingToken;
10934
- console.log(chalk27.green(`Using existing bot token (@${botUser.username})`));
11361
+ console.log(chalk29.green(`Using existing bot token (@${botUser.username})`));
10935
11362
  console.log();
10936
11363
  } catch {
10937
- console.log(chalk27.yellow("Existing token is invalid \u2014 please enter a new one."));
11364
+ console.log(chalk29.yellow("Existing token is invalid \u2014 please enter a new one."));
10938
11365
  console.log("Paste your bot token then press Enter (input is hidden):");
10939
11366
  token = await questionMasked();
10940
11367
  if (!token) {
10941
- console.error(chalk27.red("token required"));
11368
+ console.error(chalk29.red("token required"));
10942
11369
  process.exit(1);
10943
11370
  }
10944
11371
  client = createTelegramClient({ token });
10945
11372
  try {
10946
11373
  botUser = await client.getMe();
10947
11374
  } catch (e) {
10948
- console.error(chalk27.red(`token rejected: ${e.message}`));
11375
+ console.error(chalk29.red(`token rejected: ${e.message}`));
10949
11376
  process.exit(1);
10950
11377
  }
10951
- console.log(chalk27.green(`Connected as @${botUser.username}`));
11378
+ console.log(chalk29.green(`Connected as @${botUser.username}`));
10952
11379
  console.log();
10953
11380
  }
10954
11381
  } else {
10955
11382
  console.log("Paste your bot token then press Enter (input is hidden):");
10956
11383
  token = await questionMasked();
10957
11384
  if (!token) {
10958
- console.error(chalk27.red("token required"));
11385
+ console.error(chalk29.red("token required"));
10959
11386
  process.exit(1);
10960
11387
  }
10961
11388
  client = createTelegramClient({ token });
10962
11389
  try {
10963
11390
  botUser = await client.getMe();
10964
11391
  } catch (e) {
10965
- console.error(chalk27.red(`token rejected: ${e.message}`));
11392
+ console.error(chalk29.red(`token rejected: ${e.message}`));
10966
11393
  process.exit(1);
10967
11394
  }
10968
- console.log(chalk27.green(`Connected as @${botUser.username}`));
11395
+ console.log(chalk29.green(`Connected as @${botUser.username}`));
10969
11396
  console.log();
10970
11397
  }
10971
- console.log(chalk27.bold("Step 2/3 \u2014 Supergroup"));
11398
+ console.log(chalk29.bold("Step 2/3 \u2014 Supergroup"));
10972
11399
  const groupDecision = resolveSetupGroup(existingCfg?.supergroupId, { redetect: opts.redetect ?? false });
10973
11400
  let supergroupId;
10974
11401
  let detectedUserId;
10975
11402
  if (groupDecision === "reuse") {
10976
11403
  supergroupId = existingCfg.supergroupId;
10977
- console.log(chalk27.green(`Using existing group: ${supergroupId}`));
11404
+ console.log(chalk29.green(`Using existing group: ${supergroupId}`));
10978
11405
  console.log();
10979
11406
  } else {
10980
11407
  console.log("Add the bot to your forum supergroup, then send any message in it.");
10981
- console.log(chalk27.dim("Waiting for a message (up to 60s)\u2026"));
11408
+ console.log(chalk29.dim("Waiting for a message (up to 60s)\u2026"));
10982
11409
  try {
10983
11410
  ({ supergroupId, userId: detectedUserId } = await detectGroupAndUser(client, { timeoutMs: 6e4 }));
10984
11411
  } catch {
10985
- console.error(chalk27.red("Timed out \u2014 no supergroup message received within 60s."));
10986
- console.error(chalk27.yellow("Check: bot is an admin in the group \xB7 privacy mode is OFF \xB7 Topics enabled"));
11412
+ console.error(chalk29.red("Timed out \u2014 no supergroup message received within 60s."));
11413
+ console.error(chalk29.yellow("Check: bot is an admin in the group \xB7 privacy mode is OFF \xB7 Topics enabled"));
10987
11414
  process.exit(1);
10988
11415
  }
10989
- console.log(chalk27.green(`Found group: ${supergroupId}`));
11416
+ console.log(chalk29.green(`Found group: ${supergroupId}`));
10990
11417
  console.log();
10991
11418
  }
10992
- console.log(chalk27.bold("Step 3/3 \u2014 Remote control + Save"));
10993
- console.log(chalk27.dim("Remote control enables auto-launching captains and the General command channel"));
10994
- console.log(chalk27.dim("from your phone \u2014 gated to your Telegram user-id only (fail-closed)."));
11419
+ console.log(chalk29.bold("Step 3/3 \u2014 Remote control + Save"));
11420
+ console.log(chalk29.dim("Remote control enables auto-launching captains and the General command channel"));
11421
+ console.log(chalk29.dim("from your phone \u2014 gated to your Telegram user-id only (fail-closed)."));
10995
11422
  const finalUserId = resolveSetupUserId(opts.userId, detectedUserId, defaultStateRoot());
10996
11423
  let users;
10997
11424
  let remoteControl;
@@ -11005,32 +11432,32 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
11005
11432
  remoteControl = true;
11006
11433
  }
11007
11434
  } else if (groupDecision === "detect") {
11008
- console.log(chalk27.yellow("Could not read your user-id from that message \u2014 skipping remote control."));
11009
- console.log(chalk27.yellow("Re-run with --user-id <id> to enable, or edit telegram.users in config manually."));
11435
+ console.log(chalk29.yellow("Could not read your user-id from that message \u2014 skipping remote control."));
11436
+ console.log(chalk29.yellow("Re-run with --user-id <id> to enable, or edit telegram.users in config manually."));
11010
11437
  printedRemoteControlState = true;
11011
11438
  } else {
11012
11439
  const existingUsers = existingCfg?.users;
11013
11440
  if (existingUsers && existingUsers.length > 0) {
11014
- console.log(chalk27.dim(`Remote control: already configured (user-id ${existingUsers[0]}). Use --user-id to update.`));
11441
+ console.log(chalk29.dim(`Remote control: already configured (user-id ${existingUsers[0]}). Use --user-id to update.`));
11015
11442
  } else {
11016
- console.log(chalk27.dim("Remote control: off. Re-run with --user-id <id> to enable."));
11443
+ console.log(chalk29.dim("Remote control: off. Re-run with --user-id <id> to enable."));
11017
11444
  }
11018
11445
  printedRemoteControlState = true;
11019
11446
  }
11020
11447
  writeTelegramConfig(DEFAULT_CONFIG_PATH, { token, supergroupId, users, remoteControl });
11021
- console.log(chalk27.green(`Wrote telegram config \u2014 token: ${maskToken(token)} group: ${supergroupId}`));
11448
+ console.log(chalk29.green(`Wrote telegram config \u2014 token: ${maskToken(token)} group: ${supergroupId}`));
11022
11449
  if (!printedRemoteControlState) {
11023
11450
  if (remoteControl) {
11024
- console.log(chalk27.green(`Remote control: ON (allowlisted user-id ${users[0]})`));
11451
+ console.log(chalk29.green(`Remote control: ON (allowlisted user-id ${users[0]})`));
11025
11452
  } else {
11026
- console.log(chalk27.dim("Remote control: off (default). Re-run with --user-id <id> to enable."));
11453
+ console.log(chalk29.dim("Remote control: off (default). Re-run with --user-id <id> to enable."));
11027
11454
  }
11028
11455
  }
11029
11456
  try {
11030
11457
  await runRegisterCommands({ client });
11031
- console.log(chalk27.dim("Registered the /command menu."));
11458
+ console.log(chalk29.dim("Registered the /command menu."));
11032
11459
  } catch (e) {
11033
- console.log(chalk27.yellow(`command-menu registration skipped: ${e.message}`));
11460
+ console.log(chalk29.yellow(`command-menu registration skipped: ${e.message}`));
11034
11461
  }
11035
11462
  const topics = loadState(defaultStateRoot()).topics;
11036
11463
  const topicEntries = Object.entries(topics);
@@ -11039,28 +11466,28 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
11039
11466
  const project = key.slice(0, key.indexOf("::"));
11040
11467
  return `${project}\u2192${id}`;
11041
11468
  }).join(", ");
11042
- console.log(chalk27.dim(`Existing topics: ${summary} (already created \u2014 not recreated)`));
11469
+ console.log(chalk29.dim(`Existing topics: ${summary} (already created \u2014 not recreated)`));
11043
11470
  } else {
11044
- console.log(chalk27.dim("No project topics yet \u2014 they're created on first delivery or via: squadrant telegram link <project>"));
11471
+ console.log(chalk29.dim("No project topics yet \u2014 they're created on first delivery or via: squadrant telegram link <project>"));
11045
11472
  }
11046
11473
  runTelegramPostSetup({});
11047
11474
  console.log();
11048
- console.log(`Next: ${chalk27.cyan("squadrant telegram link <project>")}`);
11475
+ console.log(`Next: ${chalk29.cyan("squadrant telegram link <project>")}`);
11049
11476
  });
11050
11477
  telegramCommand.command("register-commands").description("Register (or re-register) the bot's / command menu with Telegram").action(async () => {
11051
11478
  const cfg = loadConfig().telegram;
11052
11479
  if (!cfg) {
11053
- console.error(chalk27.red("telegram config absent \u2014 run: squadrant telegram setup"));
11480
+ console.error(chalk29.red("telegram config absent \u2014 run: squadrant telegram setup"));
11054
11481
  process.exit(1);
11055
11482
  }
11056
11483
  const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
11057
11484
  if (!token) {
11058
- console.error(chalk27.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
11485
+ console.error(chalk29.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
11059
11486
  process.exit(1);
11060
11487
  }
11061
11488
  const client = createTelegramClient({ token });
11062
11489
  await runRegisterCommands({ client });
11063
- console.log(chalk27.green(`registered ${BOT_COMMANDS.length} bot commands`));
11490
+ console.log(chalk29.green(`registered ${BOT_COMMANDS.length} bot commands`));
11064
11491
  });
11065
11492
  telegramCommand.command("notify").argument("[project]", "project to toggle").argument("[state]", "on | off | crew | cap").argument("[value]", "tier for crew (all|alert_only|done_only|none) or on|off for cap").option("--status", "list notification state for all projects").description("Live on|off (state), or crew <tier> / cap <on|off> preference (per-project config)").action(async (project, state, value, opts) => {
11066
11493
  const stateRoot = defaultStateRoot();
@@ -11071,7 +11498,7 @@ telegramCommand.command("notify").argument("[project]", "project to toggle").arg
11071
11498
  return;
11072
11499
  }
11073
11500
  for (const r of rows) {
11074
- console.log(` ${r.project}: ${r.active ? chalk27.green("on") : chalk27.dim("off (muted)")}`);
11501
+ console.log(` ${r.project}: ${r.active ? chalk29.green("on") : chalk29.dim("off (muted)")}`);
11075
11502
  }
11076
11503
  return;
11077
11504
  }
@@ -11080,53 +11507,53 @@ telegramCommand.command("notify").argument("[project]", "project to toggle").arg
11080
11507
  const token = tgCfg?.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
11081
11508
  if (state === "crew" || state === "cap") {
11082
11509
  if (value === void 0) {
11083
- console.error(chalk27.red(`usage: squadrant telegram notify <project> ${state} <value>`));
11510
+ console.error(chalk29.red(`usage: squadrant telegram notify <project> ${state} <value>`));
11084
11511
  process.exit(1);
11085
11512
  }
11086
11513
  const resolved2 = resolveNotify(globalNotify, loadProjectOverride(project));
11087
11514
  const before2 = { ...resolved2, active: isNotifyActive(stateRoot, project) };
11088
11515
  const res = runTelegramNotifyPref({ project, dimension: state, value });
11089
11516
  if (!res.ok) {
11090
- console.error(chalk27.red(res.message));
11517
+ console.error(chalk29.red(res.message));
11091
11518
  process.exit(1);
11092
11519
  }
11093
- console.log(chalk27.green(`${project} ${state} = ${value}`));
11520
+ console.log(chalk29.green(`${project} ${state} = ${value}`));
11094
11521
  const after2 = state === "crew" ? { ...before2, crew: value } : { ...before2, cap: value === "on" };
11095
11522
  if (tgCfg && token) {
11096
11523
  const client = createTelegramClient({ token });
11097
11524
  const sent = await runNotifyConfirmation({ project, before: before2, after: after2, cfg: tgCfg, client, stateRoot });
11098
- if (sent) console.log(chalk27.dim(`\u2192 notified ${project} topic`));
11525
+ if (sent) console.log(chalk29.dim(`\u2192 notified ${project} topic`));
11099
11526
  }
11100
11527
  return;
11101
11528
  }
11102
11529
  if (state !== "on" && state !== "off") {
11103
- console.error(chalk27.red("usage: squadrant telegram notify <project> <on|off|crew <tier>|cap <on|off>>"));
11530
+ console.error(chalk29.red("usage: squadrant telegram notify <project> <on|off|crew <tier>|cap <on|off>>"));
11104
11531
  process.exit(1);
11105
11532
  }
11106
11533
  const resolved = resolveNotify(globalNotify, loadProjectOverride(project));
11107
11534
  const before = { ...resolved, active: isNotifyActive(stateRoot, project) };
11108
11535
  const after = { ...before, active: state === "on" };
11109
11536
  runTelegramNotifySet({ project, active: state === "on", stateRoot });
11110
- console.log(chalk27.green(`${project} notifications ${state === "on" ? "ON" : "OFF"}`));
11537
+ console.log(chalk29.green(`${project} notifications ${state === "on" ? "ON" : "OFF"}`));
11111
11538
  if (tgCfg && token) {
11112
11539
  const client = createTelegramClient({ token });
11113
11540
  const sent = await runNotifyConfirmation({ project, before, after, cfg: tgCfg, client, stateRoot });
11114
- if (sent) console.log(chalk27.dim(`\u2192 notified ${project} topic`));
11541
+ if (sent) console.log(chalk29.dim(`\u2192 notified ${project} topic`));
11115
11542
  }
11116
11543
  });
11117
11544
  telegramCommand.command("send").argument("<project>", "project whose topic receives the message").argument("[message...]", "message text (omit to read from stdin)").description("Send a message to a project's linked Telegram topic").action(async (project, messageParts) => {
11118
11545
  const cfg = loadConfig().telegram;
11119
11546
  if (!cfg) {
11120
- console.error(chalk27.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
11547
+ console.error(chalk29.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
11121
11548
  process.exit(1);
11122
11549
  }
11123
11550
  const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
11124
11551
  if (!token) {
11125
- console.error(chalk27.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
11552
+ console.error(chalk29.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
11126
11553
  process.exit(1);
11127
11554
  }
11128
11555
  if (!capAllowed(project, cfg.notify)) {
11129
- console.log(chalk27.dim(`${project}: captain messages muted (cap=off) \u2014 not sent`));
11556
+ console.log(chalk29.dim(`${project}: captain messages muted (cap=off) \u2014 not sent`));
11130
11557
  return;
11131
11558
  }
11132
11559
  let message;
@@ -11139,19 +11566,19 @@ telegramCommand.command("send").argument("<project>", "project whose topic recei
11139
11566
  for await (const line of rl) lines.push(line);
11140
11567
  message = lines.join("\n").trimEnd();
11141
11568
  if (!message) {
11142
- console.error(chalk27.red("no message provided (stdin was empty)"));
11569
+ console.error(chalk29.red("no message provided (stdin was empty)"));
11143
11570
  process.exit(1);
11144
11571
  }
11145
11572
  } else {
11146
- console.error(chalk27.red("message required \u2014 pass as argument or pipe via stdin"));
11573
+ console.error(chalk29.red("message required \u2014 pass as argument or pipe via stdin"));
11147
11574
  process.exit(1);
11148
11575
  }
11149
11576
  const client = createTelegramClient({ token });
11150
11577
  try {
11151
11578
  const { chatId, topicId } = await runTelegramSend({ project, message, cfg, client, stateRoot: defaultStateRoot() });
11152
- console.log(chalk27.green(`sent to group ${chatId} topic ${topicId}`));
11579
+ console.log(chalk29.green(`sent to group ${chatId} topic ${topicId}`));
11153
11580
  } catch (e) {
11154
- console.error(chalk27.red(e.message));
11581
+ console.error(chalk29.red(e.message));
11155
11582
  process.exit(1);
11156
11583
  }
11157
11584
  });
@@ -11159,10 +11586,10 @@ telegramCommand.command("send").argument("<project>", "project whose topic recei
11159
11586
  // packages/cli/src/commands/hooks.ts
11160
11587
  init_dist2();
11161
11588
  init_dist4();
11162
- import { Command as Command28 } from "commander";
11163
- import { join as join25 } from "path";
11164
- import { homedir as homedir17 } from "os";
11165
- var SOCK4 = join25(homedir17(), ".config", "squadrant", "squadrant.sock");
11589
+ import { Command as Command30 } from "commander";
11590
+ import { join as join26 } from "path";
11591
+ import { homedir as homedir19 } from "os";
11592
+ var SOCK4 = join26(homedir19(), ".config", "squadrant", "squadrant.sock");
11166
11593
  async function sendToSock(req) {
11167
11594
  await sendRequest(SOCK4, req);
11168
11595
  }
@@ -11188,7 +11615,7 @@ function mapHookSub(sub, payload, taskId) {
11188
11615
  }
11189
11616
  }
11190
11617
  function hooksCommand() {
11191
- const hooks = new Command28("hooks").description("(internal) receive lifecycle hook events from agent processes");
11618
+ const hooks = new Command30("hooks").description("(internal) receive lifecycle hook events from agent processes");
11192
11619
  hooks.command("claude <sub>", { hidden: true }).description("internal: bridge a NativeHookSource claude hook to squadrantd").action(async (sub) => {
11193
11620
  const taskId = process.env.SQUADRANT_CREW_TASK_ID;
11194
11621
  const project = process.env.SQUADRANT_CREW_PROJECT;
@@ -11224,21 +11651,21 @@ function hooksCommand() {
11224
11651
  init_dist();
11225
11652
  init_dist();
11226
11653
  init_dist();
11227
- var __dirname = dirname7(fileURLToPath6(import.meta.url));
11228
- var pkg = JSON.parse(readFileSync13(join26(__dirname, "..", "package.json"), "utf-8"));
11654
+ var __dirname = dirname8(fileURLToPath6(import.meta.url));
11655
+ var pkg = JSON.parse(readFileSync15(join27(__dirname, "..", "package.json"), "utf-8"));
11229
11656
  ensureRuntimeSynced({
11230
- sourceRoot: join26(__dirname, ".."),
11231
- runtimeRoot: join26(homedir18(), ".config", "squadrant")
11657
+ sourceRoot: join27(__dirname, ".."),
11658
+ runtimeRoot: join27(homedir20(), ".config", "squadrant")
11232
11659
  });
11233
11660
  if (process.argv[2] !== "config") {
11234
11661
  try {
11235
- const cfgPath = join26(homedir18(), ".config", "squadrant", "config.json");
11662
+ const cfgPath = join27(homedir20(), ".config", "squadrant", "config.json");
11236
11663
  if (existsSync12(cfgPath)) {
11237
- const cfg = JSON.parse(readFileSync13(cfgPath, "utf-8"));
11664
+ const cfg = JSON.parse(readFileSync15(cfgPath, "utf-8"));
11238
11665
  if (needsCheck(cfg, pkg.version)) {
11239
11666
  const items = detectDrift(cfg, getDefaultConfig());
11240
11667
  if (items.length === 0) {
11241
- writeFileSync10(cfgPath, JSON.stringify(withStamp(cfg, pkg.version), null, 2) + "\n");
11668
+ writeFileSync11(cfgPath, JSON.stringify(withStamp(cfg, pkg.version), null, 2) + "\n");
11242
11669
  } else {
11243
11670
  const from = cfg._squadrantVersion ?? "an earlier version";
11244
11671
  process.stderr.write(
@@ -11257,7 +11684,7 @@ if (process.argv[2] !== "config") {
11257
11684
  if (!process.env.SQUADRANT_DAEMON_SKIP) {
11258
11685
  ensureDaemon();
11259
11686
  }
11260
- var program = new Command29();
11687
+ var program = new Command31();
11261
11688
  program.name("squadrant").description("Multi-project orchestration for your coding agents (Claude, Codex, opencode, Gemini)").version(pkg.version);
11262
11689
  program.addCommand(doctorCommand);
11263
11690
  program.addCommand(initCommand);
@@ -11281,6 +11708,8 @@ program.addCommand(codexChatSmokeCommand);
11281
11708
  program.addCommand(configCommand);
11282
11709
  program.addCommand(healCommand);
11283
11710
  program.addCommand(groupCommand);
11711
+ program.addCommand(pingCommand);
11712
+ program.addCommand(dispatchCommand);
11284
11713
  program.addCommand(cmuxCommand);
11285
11714
  program.addCommand(effortCommand);
11286
11715
  program.addCommand(telegramCommand);