squadrant 0.16.5 → 0.17.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
@@ -205,6 +205,14 @@ var init_control = __esm({
205
205
  }
206
206
  });
207
207
 
208
+ // packages/shared/dist/types/work.js
209
+ var TERMINAL_WORK_STATES;
210
+ var init_work = __esm({
211
+ "packages/shared/dist/types/work.js"() {
212
+ TERMINAL_WORK_STATES = /* @__PURE__ */ new Set(["done", "cancelled"]);
213
+ }
214
+ });
215
+
208
216
  // packages/shared/dist/types/projection.js
209
217
  var init_projection = __esm({
210
218
  "packages/shared/dist/types/projection.js"() {
@@ -226,22 +234,22 @@ function defaultCmuxConfigPath() {
226
234
  return join(homedir(), ".config", "cmux", "cmux.json");
227
235
  }
228
236
  function ensureSocketAutomation(opts = {}) {
229
- const path30 = opts.path ?? defaultCmuxConfigPath();
230
- if (!existsSync(path30)) {
231
- mkdirSync(dirname(path30), { recursive: true });
232
- writeFileSync(path30, MINIMAL_TEMPLATE);
233
- return { path: path30, changed: true, alreadySet: false };
237
+ const path34 = opts.path ?? defaultCmuxConfigPath();
238
+ if (!existsSync(path34)) {
239
+ mkdirSync(dirname(path34), { recursive: true });
240
+ writeFileSync(path34, MINIMAL_TEMPLATE);
241
+ return { path: path34, changed: true, alreadySet: false };
234
242
  }
235
- const text = readFileSync(path30, "utf-8");
243
+ const text = readFileSync(path34, "utf-8");
236
244
  const current = parse(text)?.automation?.socketControlMode;
237
245
  if (current === AUTOMATION_MODE) {
238
- return { path: path30, changed: false, alreadySet: true };
246
+ return { path: path34, changed: false, alreadySet: true };
239
247
  }
240
248
  const edits = modify(text, [...SOCKET_CONTROL_MODE_PATH], AUTOMATION_MODE, {
241
249
  formattingOptions: { insertSpaces: true, tabSize: 2 }
242
250
  });
243
- writeFileSync(path30, applyEdits(text, edits));
244
- return { path: path30, changed: true, alreadySet: false };
251
+ writeFileSync(path34, applyEdits(text, edits));
252
+ return { path: path34, changed: true, alreadySet: false };
245
253
  }
246
254
  var SOCKET_CONTROL_MODE_PATH, AUTOMATION_MODE, MINIMAL_TEMPLATE;
247
255
  var init_cmux_config = __esm({
@@ -394,9 +402,9 @@ import { dirname as dirname2, join as join4 } from "path";
394
402
  function defaultStatePath() {
395
403
  return join4(homedir3(), ".config", "squadrant", "state", "cmux-autoconfig.json");
396
404
  }
397
- function readState(path30) {
405
+ function readState(path34) {
398
406
  try {
399
- return JSON.parse(readFileSync4(path30, "utf-8"));
407
+ return JSON.parse(readFileSync4(path34, "utf-8"));
400
408
  } catch {
401
409
  return {};
402
410
  }
@@ -644,8 +652,8 @@ function formatUpdateNotice(latest, current) {
644
652
  return `\u2B06 squadrant ${latest} available (you have ${current}) \u2014 npm i -g squadrant@latest`;
645
653
  }
646
654
  async function fetchLatestVersion(requestFn = requestJson, timeoutMs = FETCH_TIMEOUT_MS) {
647
- const timeout = new Promise((resolve3) => {
648
- const timer = setTimeout(() => resolve3(null), timeoutMs);
655
+ const timeout = new Promise((resolve4) => {
656
+ const timer = setTimeout(() => resolve4(null), timeoutMs);
649
657
  timer.unref?.();
650
658
  });
651
659
  const request = (async () => {
@@ -717,11 +725,11 @@ var init_update_check = __esm({
717
725
  CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
718
726
  FAILURE_RETRY_MS = 60 * 60 * 1e3;
719
727
  FETCH_TIMEOUT_MS = 1500;
720
- requestJson = (url, timeoutMs) => new Promise((resolve3) => {
728
+ requestJson = (url, timeoutMs) => new Promise((resolve4) => {
721
729
  const req = https.get(url, { headers: { "user-agent": "squadrant-update-check" } }, (res) => {
722
730
  if (res.statusCode !== 200) {
723
731
  res.resume();
724
- resolve3(null);
732
+ resolve4(null);
725
733
  return;
726
734
  }
727
735
  let body = "";
@@ -729,15 +737,15 @@ var init_update_check = __esm({
729
737
  res.on("data", (chunk) => body += chunk);
730
738
  res.on("end", () => {
731
739
  try {
732
- resolve3(JSON.parse(body));
740
+ resolve4(JSON.parse(body));
733
741
  } catch {
734
- resolve3(null);
742
+ resolve4(null);
735
743
  }
736
744
  });
737
745
  });
738
746
  req.on("socket", (socket) => socket.unref());
739
747
  req.setTimeout(timeoutMs, () => req.destroy());
740
- req.on("error", () => resolve3(null));
748
+ req.on("error", () => resolve4(null));
741
749
  });
742
750
  }
743
751
  });
@@ -929,6 +937,22 @@ function mirrorFlat(src, dest, match, chmod) {
929
937
  }
930
938
  }
931
939
  }
940
+ function mirrorPluginSubset(src, dest, skills) {
941
+ fs6.mkdirSync(dest, { recursive: true });
942
+ mirrorDir(path5.join(src, ".claude-plugin"), path5.join(dest, ".claude-plugin"));
943
+ const skillsDest = path5.join(dest, "skills");
944
+ fs6.mkdirSync(skillsDest, { recursive: true });
945
+ for (const name of skills) {
946
+ const skillSrc = path5.join(src, "skills", name);
947
+ if (fs6.existsSync(skillSrc))
948
+ mirrorDir(skillSrc, path5.join(skillsDest, name));
949
+ }
950
+ for (const entry of fs6.readdirSync(skillsDest, { withFileTypes: true })) {
951
+ if (!skills.includes(entry.name)) {
952
+ fs6.rmSync(path5.join(skillsDest, entry.name), { recursive: true, force: true });
953
+ }
954
+ }
955
+ }
932
956
  function ensureRuntimeSynced(opts) {
933
957
  const targets = opts.targets ?? MANAGED_TARGETS;
934
958
  for (const t of targets) {
@@ -939,8 +963,10 @@ function ensureRuntimeSynced(opts) {
939
963
  const destDir = path5.join(opts.runtimeRoot, t.name);
940
964
  if (t.mode === "tree") {
941
965
  mirrorDir(srcDir, destDir);
942
- } else {
966
+ } else if (t.mode === "flat") {
943
967
  mirrorFlat(srcDir, destDir, t.match, t.chmod);
968
+ } else {
969
+ mirrorPluginSubset(srcDir, destDir, t.skills);
944
970
  }
945
971
  } catch (err) {
946
972
  process.stderr.write(`squadrant: runtime sync skipped for ${t.name}: ${err.message}
@@ -948,11 +974,13 @@ function ensureRuntimeSynced(opts) {
948
974
  }
949
975
  }
950
976
  }
951
- var MANAGED_TARGETS;
977
+ var CREW_SKILLS, MANAGED_TARGETS;
952
978
  var init_runtime_sync = __esm({
953
979
  "packages/shared/dist/lib/runtime-sync.js"() {
980
+ CREW_SKILLS = ["karpathy-principles"];
954
981
  MANAGED_TARGETS = [
955
982
  { name: "plugin", srcRel: "plugin", mode: "tree" },
983
+ { name: "plugin-crew", srcRel: "plugin", mode: "subset", skills: CREW_SKILLS },
956
984
  { name: "scripts", srcRel: "scripts", mode: "flat", match: /\.sh$/, chmod: 493 },
957
985
  {
958
986
  name: "templates",
@@ -1209,6 +1237,7 @@ var init_daemon_keys = __esm({
1209
1237
  var dist_exports = {};
1210
1238
  __export(dist_exports, {
1211
1239
  AUTOMATION_MODE: () => AUTOMATION_MODE,
1240
+ CREW_SKILLS: () => CREW_SKILLS,
1212
1241
  DEFAULT_CONFIG_PATH: () => DEFAULT_CONFIG_PATH,
1213
1242
  DEFAULT_NOTIFY: () => DEFAULT_NOTIFY,
1214
1243
  MANAGED_TARGETS: () => MANAGED_TARGETS,
@@ -1216,6 +1245,7 @@ __export(dist_exports, {
1216
1245
  SOCKET_CONTROL_MODE_PATH: () => SOCKET_CONTROL_MODE_PATH,
1217
1246
  SPOKE_SUBDIRS: () => SPOKE_SUBDIRS,
1218
1247
  TERMINAL_STATES: () => TERMINAL_STATES,
1248
+ TERMINAL_WORK_STATES: () => TERMINAL_WORK_STATES,
1219
1249
  UPDATE_CHECK_STATE_PATH: () => UPDATE_CHECK_STATE_PATH,
1220
1250
  addWorktree: () => addWorktree,
1221
1251
  applySafeFixes: () => applySafeFixes,
@@ -1283,6 +1313,7 @@ var init_dist = __esm({
1283
1313
  init_runtime();
1284
1314
  init_liveness();
1285
1315
  init_control();
1316
+ init_work();
1286
1317
  init_projection();
1287
1318
  init_workspaces();
1288
1319
  init_cmux_autoconfig();
@@ -1323,6 +1354,11 @@ function nextPendingTool(current, ev, now) {
1323
1354
  return void 0;
1324
1355
  return current;
1325
1356
  }
1357
+ function nextPendingMonitor(current, ev, now) {
1358
+ if (ev.note === "agent.hook.PreToolUse" && ev.tool === "Monitor")
1359
+ return { since: now };
1360
+ return current;
1361
+ }
1326
1362
  function reduce(rec, ev, now) {
1327
1363
  if (ev.type === "task.reopened") {
1328
1364
  return { ...rec, state: "working", question: void 0, error: void 0, lastHeartbeat: now, lastEvent: ev.type };
@@ -1339,14 +1375,17 @@ function reduce(rec, ev, now) {
1339
1375
  sessionId: ev.sessionId ?? rec.sessionId,
1340
1376
  question: void 0,
1341
1377
  // resuming after a blocked→reply clears the question
1342
- pendingTool: void 0
1378
+ pendingTool: void 0,
1343
1379
  // #354: a new turn closes any prior tool window
1380
+ pendingMonitor: void 0
1381
+ // #594a: same reset — a new turn moots any prior watch
1344
1382
  };
1345
1383
  case "task.progress": {
1346
1384
  const pendingTool = nextPendingTool(rec.pendingTool, ev, now);
1385
+ const pendingMonitor = nextPendingMonitor(rec.pendingMonitor, ev, now);
1347
1386
  if (isStickyAttention(rec.state))
1348
- return { ...rec, lastHeartbeat: now, lastEvent: ev.type, pendingTool };
1349
- const b = { ...base, pendingTool };
1387
+ return { ...rec, lastHeartbeat: now, lastEvent: ev.type, pendingTool, pendingMonitor };
1388
+ const b = { ...base, pendingTool, pendingMonitor };
1350
1389
  if (rec.state === "awaiting-input" || rec.state === "stalled")
1351
1390
  return { ...stampAttempt(b, {}, now), state: "working" };
1352
1391
  return stampAttempt(b, {}, now);
@@ -1360,9 +1399,9 @@ function reduce(rec, ev, now) {
1360
1399
  case "task.blocked":
1361
1400
  if (rec.state === "blocked")
1362
1401
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
1363
- return { ...base, state: "blocked", question: ev.question, pendingTool: void 0 };
1402
+ return { ...base, state: "blocked", question: ev.question, pendingTool: void 0, pendingMonitor: void 0 };
1364
1403
  case "task.review":
1365
- return { ...base, state: "review", reviewNote: ev.message, pendingTool: void 0 };
1404
+ return { ...base, state: "review", reviewNote: ev.message, pendingTool: void 0, pendingMonitor: void 0 };
1366
1405
  case "task.done":
1367
1406
  if (rec.state === "review" && ev.source !== "approve") {
1368
1407
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
@@ -1377,19 +1416,19 @@ function reduce(rec, ev, now) {
1377
1416
  case "task.session":
1378
1417
  return stampAttempt(base, { resumeRef: ev.resumeRef }, now);
1379
1418
  case "task.turn.started":
1380
- return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0 };
1419
+ return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0, pendingMonitor: void 0 };
1381
1420
  case "task.turn.completed":
1382
1421
  if (isStickyAttention(rec.state))
1383
1422
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
1384
- if (rec.pendingTool)
1423
+ if (rec.pendingTool || rec.pendingMonitor)
1385
1424
  return stampAttempt(base, {}, now);
1386
- return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0 };
1425
+ return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0, pendingMonitor: void 0 };
1387
1426
  case "task.delta":
1388
1427
  return stampAttempt(base, {}, now);
1389
1428
  // heartbeat-only
1390
1429
  case "task.input.requested":
1391
1430
  case "task.approval.requested":
1392
- return { ...stampAttempt(base, {}, now), state: "blocked", question: ev.question, pendingTool: void 0 };
1431
+ return { ...stampAttempt(base, {}, now), state: "blocked", question: ev.question, pendingTool: void 0, pendingMonitor: void 0 };
1393
1432
  case "task.reattached":
1394
1433
  return stampAttempt(base, {}, now);
1395
1434
  case "task.first-turn.confirmed":
@@ -1414,15 +1453,21 @@ var init_state_machine = __esm({
1414
1453
  });
1415
1454
 
1416
1455
  // packages/core/dist/watchdog.js
1417
- function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS) {
1456
+ function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS, monitorStallMs = MONITOR_STALL_BUDGET_MS) {
1418
1457
  if (rec.state !== "working")
1419
1458
  return null;
1420
1459
  if (rec.mode === "interactive") {
1421
- if (!rec.pendingTool)
1422
- return null;
1423
- if (now - rec.pendingTool.since <= toolStallMs)
1424
- return null;
1425
- return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
1460
+ if (rec.pendingTool) {
1461
+ if (now - rec.pendingTool.since <= toolStallMs)
1462
+ return null;
1463
+ return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
1464
+ }
1465
+ if (rec.pendingMonitor) {
1466
+ if (now - rec.pendingMonitor.since <= monitorStallMs)
1467
+ return null;
1468
+ return { ...rec, state: "stalled", lastEvent: "watchdog.monitor-stall" };
1469
+ }
1470
+ return null;
1426
1471
  }
1427
1472
  const liveness = rec.attempts.at(-1)?.lastHeartbeatAt ?? rec.lastHeartbeat;
1428
1473
  if (now - liveness <= rec.heartbeatBudgetMs)
@@ -1432,12 +1477,13 @@ function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS) {
1432
1477
  function recoverStall(rec, now) {
1433
1478
  if (rec.state !== "stalled")
1434
1479
  return null;
1435
- return { ...rec, state: "working", lastHeartbeat: now, lastEvent: "watchdog.recover", pendingTool: void 0 };
1480
+ return { ...rec, state: "working", lastHeartbeat: now, lastEvent: "watchdog.recover", pendingTool: void 0, pendingMonitor: void 0 };
1436
1481
  }
1437
- var TOOL_STALL_BUDGET_MS;
1482
+ var TOOL_STALL_BUDGET_MS, MONITOR_STALL_BUDGET_MS;
1438
1483
  var init_watchdog = __esm({
1439
1484
  "packages/core/dist/watchdog.js"() {
1440
1485
  TOOL_STALL_BUDGET_MS = 10 * 60 * 1e3;
1486
+ MONITOR_STALL_BUDGET_MS = 60 * 60 * 1e3;
1441
1487
  }
1442
1488
  });
1443
1489
 
@@ -1711,7 +1757,7 @@ function createDaemon(deps) {
1711
1757
  store.delete(r.project, r.id);
1712
1758
  continue;
1713
1759
  }
1714
- if (!TERMINAL_STATES.has(r.state)) {
1760
+ if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state)) {
1715
1761
  const ceiling = deps.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
1716
1762
  if (t - r.createdAt > ceiling) {
1717
1763
  const prevState = r.state;
@@ -1756,7 +1802,7 @@ function createDaemon(deps) {
1756
1802
  const idle = evaluateStall(r, t);
1757
1803
  if (idle) {
1758
1804
  store.put(idle);
1759
- const synthEvent = idle.pendingTool ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: idle.pendingTool.name, elapsedMs: t - idle.pendingTool.since } : { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs };
1805
+ const synthEvent = idle.pendingTool ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: idle.pendingTool.name, elapsedMs: t - idle.pendingTool.since } : idle.pendingMonitor ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: "Monitor", elapsedMs: t - idle.pendingMonitor.since } : { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs };
1760
1806
  firePush(deps, r.project, r.state, idle, synthEvent, lastCaptainTurnAt.get(r.id));
1761
1807
  continue;
1762
1808
  }
@@ -2251,9 +2297,9 @@ function startServer(sockPath, handlerOrCallbacks, onListenError = defaultListen
2251
2297
  return server;
2252
2298
  }
2253
2299
  function isDaemonSocketLive(sockPath, timeoutMs = 500) {
2254
- return new Promise((resolve3) => {
2300
+ return new Promise((resolve4) => {
2255
2301
  if (!existsSync5(sockPath)) {
2256
- resolve3(false);
2302
+ resolve4(false);
2257
2303
  return;
2258
2304
  }
2259
2305
  const conn = createConnection(sockPath);
@@ -2262,7 +2308,7 @@ function isDaemonSocketLive(sockPath, timeoutMs = 500) {
2262
2308
  conn.destroy();
2263
2309
  } catch {
2264
2310
  }
2265
- resolve3(v);
2311
+ resolve4(v);
2266
2312
  };
2267
2313
  const timer = setTimeout(() => finish(false), timeoutMs);
2268
2314
  conn.on("connect", () => {
@@ -2276,7 +2322,7 @@ function isDaemonSocketLive(sockPath, timeoutMs = 500) {
2276
2322
  });
2277
2323
  }
2278
2324
  function sendRequest(sockPath, msg, timeoutMs = 5e3) {
2279
- return new Promise((resolve3, reject) => {
2325
+ return new Promise((resolve4, reject) => {
2280
2326
  const conn = createConnection(sockPath);
2281
2327
  const dec = createDecoder();
2282
2328
  const timer = setTimeout(() => {
@@ -2292,7 +2338,7 @@ function sendRequest(sockPath, msg, timeoutMs = 5e3) {
2292
2338
  if (m._v !== void 0 && m._v !== PROTOCOL_VERSION) {
2293
2339
  reject(new Error(`squadrantd protocol v${m._v}, this client expects v${PROTOCOL_VERSION} \u2014 upgrade squadrantd or this CLI`));
2294
2340
  } else if (m.ok) {
2295
- resolve3(m.reply);
2341
+ resolve4(m.reply);
2296
2342
  } else {
2297
2343
  reject(new Error(m.error));
2298
2344
  }
@@ -2512,6 +2558,151 @@ var init_store = __esm({
2512
2558
  }
2513
2559
  });
2514
2560
 
2561
+ // packages/core/dist/work-store.js
2562
+ import { homedir as homedir4 } from "os";
2563
+ import { join as join7, resolve as resolve2, sep as sep2 } from "path";
2564
+ import { randomBytes } from "crypto";
2565
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, readdirSync as readdirSync2, renameSync as renameSync2, writeFileSync as writeFileSync5, existsSync as existsSync7, rmSync as rmSync4, statSync as statSync2 } from "fs";
2566
+ function defaultWorkRoot() {
2567
+ return join7(homedir4(), ".config", "squadrant", "work");
2568
+ }
2569
+ function safeSegment2(kind, s) {
2570
+ if (typeof s !== "string" || s.length === 0) {
2571
+ throw new Error(`invalid ${kind}: must be a non-empty string`);
2572
+ }
2573
+ if (s.includes("\0"))
2574
+ throw new Error(`invalid ${kind}: NUL byte not allowed`);
2575
+ if (s === "." || s === ".." || /[/\\]/.test(s)) {
2576
+ throw new Error(`invalid ${kind}: '${s}' \u2014 path separators/traversal not allowed`);
2577
+ }
2578
+ return s;
2579
+ }
2580
+ function createWorkStore(root = defaultWorkRoot()) {
2581
+ const rootResolved = resolve2(root);
2582
+ const assertUnderRoot = (target) => {
2583
+ const r = resolve2(target);
2584
+ if (r !== rootResolved && !r.startsWith(rootResolved + sep2)) {
2585
+ throw new Error(`path escapes state root: ${target}`);
2586
+ }
2587
+ return target;
2588
+ };
2589
+ const projDir = (p) => assertUnderRoot(join7(root, safeSegment2("project", p)));
2590
+ const itemFile = (p, id) => assertUnderRoot(join7(projDir(p), `${safeSegment2("id", id)}.json`));
2591
+ return {
2592
+ put(item) {
2593
+ mkdirSync4(projDir(item.project), { recursive: true });
2594
+ const dest = itemFile(item.project, item.id);
2595
+ const tmp = `${dest}.tmp`;
2596
+ writeFileSync5(tmp, JSON.stringify(item, null, 2));
2597
+ renameSync2(tmp, dest);
2598
+ },
2599
+ get(project, id) {
2600
+ const f = itemFile(project, id);
2601
+ if (!existsSync7(f))
2602
+ return void 0;
2603
+ try {
2604
+ return JSON.parse(readFileSync6(f, "utf-8"));
2605
+ } catch {
2606
+ return void 0;
2607
+ }
2608
+ },
2609
+ list(project) {
2610
+ const d = projDir(project);
2611
+ if (!existsSync7(d))
2612
+ return [];
2613
+ return readdirSync2(d).filter((n) => n.endsWith(".json") && !n.endsWith(".json.tmp")).map((n) => {
2614
+ try {
2615
+ return JSON.parse(readFileSync6(join7(d, n), "utf-8"));
2616
+ } catch {
2617
+ return void 0;
2618
+ }
2619
+ }).filter((r) => r !== void 0);
2620
+ },
2621
+ listAll() {
2622
+ if (!existsSync7(root))
2623
+ return [];
2624
+ return readdirSync2(root).filter((p) => {
2625
+ try {
2626
+ return statSync2(join7(root, p)).isDirectory();
2627
+ } catch {
2628
+ return false;
2629
+ }
2630
+ }).flatMap((p) => this.list(p));
2631
+ },
2632
+ delete(project, id) {
2633
+ const f = itemFile(project, id);
2634
+ if (existsSync7(f))
2635
+ rmSync4(f);
2636
+ }
2637
+ };
2638
+ }
2639
+ function purgeExpiredWorkItems(store, now = Date.now()) {
2640
+ let purged = 0;
2641
+ for (const item of store.listAll()) {
2642
+ if (item.closedAt !== null && now - item.closedAt > WORK_ITEM_TTL_MS) {
2643
+ store.delete(item.project, item.id);
2644
+ purged++;
2645
+ }
2646
+ }
2647
+ return purged;
2648
+ }
2649
+ function generateWorkId(store) {
2650
+ const existing = new Set(store.listAll().map((i) => i.id));
2651
+ for (let attempt = 0; attempt < 20; attempt++) {
2652
+ const id = `w_${randomBytes(2).toString("hex")}`;
2653
+ if (!existing.has(id))
2654
+ return id;
2655
+ }
2656
+ throw new Error("could not generate a unique work item id");
2657
+ }
2658
+ function createWorkItem(store, opts) {
2659
+ const now = opts.now ?? Date.now();
2660
+ const item = {
2661
+ id: generateWorkId(store),
2662
+ project: opts.project,
2663
+ title: opts.title,
2664
+ state: "working",
2665
+ parent: opts.parent ?? null,
2666
+ tags: opts.tags ?? [],
2667
+ note: "",
2668
+ crewTaskIds: [],
2669
+ issue: null,
2670
+ createdAt: now,
2671
+ updatedAt: now,
2672
+ closedAt: null
2673
+ };
2674
+ store.put(item);
2675
+ return item;
2676
+ }
2677
+ function findWorkItemById(store, id) {
2678
+ return store.listAll().find((i) => i.id === id);
2679
+ }
2680
+ function findOpenChildren(store, id) {
2681
+ return store.listAll().filter((i) => i.parent === id && !TERMINAL_WORK_STATES.has(i.state));
2682
+ }
2683
+ function closeWorkItem(store, id, state, opts = {}) {
2684
+ const item = findWorkItemById(store, id);
2685
+ if (!item)
2686
+ return void 0;
2687
+ const now = opts.now ?? Date.now();
2688
+ const updated = {
2689
+ ...item,
2690
+ state,
2691
+ note: opts.note ?? item.note,
2692
+ updatedAt: now,
2693
+ closedAt: now
2694
+ };
2695
+ store.put(updated);
2696
+ return updated;
2697
+ }
2698
+ var WORK_ITEM_TTL_MS;
2699
+ var init_work_store = __esm({
2700
+ "packages/core/dist/work-store.js"() {
2701
+ init_dist();
2702
+ WORK_ITEM_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
2703
+ }
2704
+ });
2705
+
2515
2706
  // packages/core/dist/snapshot.js
2516
2707
  var snapshot_exports = {};
2517
2708
  __export(snapshot_exports, {
@@ -2565,16 +2756,16 @@ var init_snapshot = __esm({
2565
2756
 
2566
2757
  // packages/core/dist/launchd.js
2567
2758
  import { execFileSync as execFileSync3 } from "child_process";
2568
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, readFileSync as readFileSync6, existsSync as existsSync7, openSync, writeSync, closeSync, unlinkSync as unlinkSync2, constants } from "fs";
2569
- import { homedir as homedir4 } from "os";
2570
- import { dirname as dirname3, join as join7 } from "path";
2759
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync6, readFileSync as readFileSync7, existsSync as existsSync8, openSync, writeSync, closeSync, unlinkSync as unlinkSync2, constants } from "fs";
2760
+ import { homedir as homedir5 } from "os";
2761
+ import { dirname as dirname3, join as join8 } from "path";
2571
2762
  import { fileURLToPath } from "url";
2572
2763
  function plistPath() {
2573
- return join7(homedir4(), "Library", "LaunchAgents", `${LABEL}.plist`);
2764
+ return join8(homedir5(), "Library", "LaunchAgents", `${LABEL}.plist`);
2574
2765
  }
2575
2766
  function daemonEntryPath() {
2576
- const p = join7(dirname3(fileURLToPath(import.meta.url)), "squadrantd.js");
2577
- if (!existsSync7(p)) {
2767
+ const p = join8(dirname3(fileURLToPath(import.meta.url)), "squadrantd.js");
2768
+ if (!existsSync8(p)) {
2578
2769
  throw new Error(`daemonEntryPath: compiled entry not found at '${p}'; run 'npm run build' \u2014 a src-tree or missing path in the launchd plist causes a MODULE_NOT_FOUND crash-loop (#259)`);
2579
2770
  }
2580
2771
  return p;
@@ -2582,10 +2773,10 @@ function daemonEntryPath() {
2582
2773
  function xmlEscape(s) {
2583
2774
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2584
2775
  }
2585
- function sanitizePathForPlist(path30) {
2776
+ function sanitizePathForPlist(path34) {
2586
2777
  const seen = /* @__PURE__ */ new Set();
2587
2778
  const stable = [];
2588
- for (const p of path30.split(":")) {
2779
+ for (const p of path34.split(":")) {
2589
2780
  if (!p)
2590
2781
  continue;
2591
2782
  if (p.includes("/.claude/plugins/"))
@@ -2631,7 +2822,7 @@ function buildDaemonPath(shellPath) {
2631
2822
  }).join(":");
2632
2823
  }
2633
2824
  function renderPlist(nodeBin, daemonEntry, pathEnv = "") {
2634
- const logPath2 = join7(homedir4(), ".config", "squadrant", "squadrantd.log");
2825
+ const logPath2 = join8(homedir5(), ".config", "squadrant", "squadrantd.log");
2635
2826
  return `<?xml version="1.0" encoding="UTF-8"?>
2636
2827
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2637
2828
  <plist version="1.0">
@@ -2660,13 +2851,13 @@ function _resetRestartInFlightForTest() {
2660
2851
  restartInFlight = false;
2661
2852
  }
2662
2853
  function daemonLockPath() {
2663
- return join7(homedir4(), ".config", "squadrant", "daemon.lock");
2854
+ return join8(homedir5(), ".config", "squadrant", "daemon.lock");
2664
2855
  }
2665
2856
  function tryAcquireDaemonLock() {
2666
2857
  const lp = daemonLockPath();
2667
- if (existsSync7(lp)) {
2858
+ if (existsSync8(lp)) {
2668
2859
  try {
2669
- const pid = parseInt(readFileSync6(lp, "utf-8").trim(), 10);
2860
+ const pid = parseInt(readFileSync7(lp, "utf-8").trim(), 10);
2670
2861
  if (!Number.isFinite(pid) || pid <= 0) {
2671
2862
  unlinkSync2(lp);
2672
2863
  } else {
@@ -2699,37 +2890,57 @@ function releaseDaemonLock() {
2699
2890
  } catch {
2700
2891
  }
2701
2892
  }
2702
- function ensureDaemon(nodeBin = process.execPath) {
2893
+ function computeDaemonDrift(nodeBin) {
2894
+ const p = plistPath();
2895
+ const entry = daemonEntryPath();
2896
+ const desired = renderPlist(nodeBin, entry, buildDaemonPath(process.env.PATH ?? ""));
2897
+ const current = existsSync8(p) ? readFileSync7(p, "utf-8") : null;
2898
+ const uid = process.getuid?.() ?? 0;
2899
+ const target = `gui/${uid}/${LABEL}`;
2900
+ const changed = current !== desired;
2901
+ const programChanged = current !== null && changed && !current.includes(programArgsBlock(nodeBin, entry));
2902
+ return { plistPath: p, target, desired, current, changed, programChanged };
2903
+ }
2904
+ function applyDaemonDrift(drift) {
2905
+ if (drift.changed) {
2906
+ mkdirSync5(dirname3(drift.plistPath), { recursive: true });
2907
+ writeFileSync6(drift.plistPath, drift.desired);
2908
+ }
2909
+ if (drift.programChanged) {
2910
+ try {
2911
+ execFileSync3("launchctl", ["bootout", drift.target], { stdio: "ignore" });
2912
+ } catch {
2913
+ }
2914
+ }
2915
+ const uid = process.getuid?.() ?? 0;
2916
+ try {
2917
+ execFileSync3("launchctl", ["bootstrap", `gui/${uid}`, drift.plistPath], { stdio: "ignore" });
2918
+ } catch {
2919
+ }
2920
+ execFileSync3("launchctl", ["kickstart", drift.target], { stdio: "ignore" });
2921
+ }
2922
+ function isOperatorInitiatedCommand(topLevelArg) {
2923
+ return topLevelArg !== void 0 && OPERATOR_INITIATED_COMMANDS.has(topLevelArg);
2924
+ }
2925
+ function ensureDaemon(nodeBin = process.execPath, opts = {}) {
2703
2926
  if (restartInFlight)
2704
2927
  return;
2705
2928
  restartInFlight = true;
2929
+ const authorized = process.env.SQUADRANT_ROLE === "captain" || opts.operatorInitiated === true;
2930
+ if (!authorized) {
2931
+ try {
2932
+ if (computeDaemonDrift(nodeBin).changed) {
2933
+ process.stderr.write("[squadrant] note: this machine's registered squadrant daemon config is out of date for the version/PATH running right now (common right after an `npm update -g squadrant`) \u2014 NOT applying it automatically because this command isn't the captain and isn't `launch`/`init`. This is usually harmless: the next captain command reconciles it on its own. If something looks stale or broken right now, run `squadrant heal daemon` to fix it immediately.\n");
2934
+ }
2935
+ } catch {
2936
+ }
2937
+ return;
2938
+ }
2706
2939
  if (!tryAcquireDaemonLock()) {
2707
2940
  return;
2708
2941
  }
2709
2942
  try {
2710
- const p = plistPath();
2711
- const entry = daemonEntryPath();
2712
- const desired = renderPlist(nodeBin, entry, buildDaemonPath(process.env.PATH ?? ""));
2713
- const current = existsSync7(p) ? readFileSync6(p, "utf-8") : null;
2714
- const uid = process.getuid?.() ?? 0;
2715
- const target = `gui/${uid}/${LABEL}`;
2716
- const changed = current !== desired;
2717
- const programChanged = current !== null && changed && !current.includes(programArgsBlock(nodeBin, entry));
2718
- if (changed) {
2719
- mkdirSync4(dirname3(p), { recursive: true });
2720
- writeFileSync5(p, desired);
2721
- }
2722
- if (programChanged) {
2723
- try {
2724
- execFileSync3("launchctl", ["bootout", target], { stdio: "ignore" });
2725
- } catch {
2726
- }
2727
- }
2728
- try {
2729
- execFileSync3("launchctl", ["bootstrap", `gui/${uid}`, p], { stdio: "ignore" });
2730
- } catch {
2731
- }
2732
- execFileSync3("launchctl", ["kickstart", target], { stdio: "ignore" });
2943
+ applyDaemonDrift(computeDaemonDrift(nodeBin));
2733
2944
  } catch (e) {
2734
2945
  process.stderr.write(`[squadrant] warn: ensureDaemon failed (${e instanceof Error ? e.message : e})
2735
2946
  `);
@@ -2737,12 +2948,22 @@ function ensureDaemon(nodeBin = process.execPath) {
2737
2948
  releaseDaemonLock();
2738
2949
  }
2739
2950
  }
2740
- var LABEL, AGENT_BINS, restartInFlight;
2951
+ function reregisterDaemon(nodeBin = process.execPath) {
2952
+ if (!tryAcquireDaemonLock())
2953
+ return;
2954
+ try {
2955
+ applyDaemonDrift(computeDaemonDrift(nodeBin));
2956
+ } finally {
2957
+ releaseDaemonLock();
2958
+ }
2959
+ }
2960
+ var LABEL, AGENT_BINS, restartInFlight, OPERATOR_INITIATED_COMMANDS;
2741
2961
  var init_launchd = __esm({
2742
2962
  "packages/core/dist/launchd.js"() {
2743
2963
  LABEL = "com.squadrant.daemon";
2744
2964
  AGENT_BINS = ["cmux", "claude", "opencode", "codex", "gemini", "node"];
2745
2965
  restartInFlight = false;
2966
+ OPERATOR_INITIATED_COMMANDS = /* @__PURE__ */ new Set(["launch", "init"]);
2746
2967
  }
2747
2968
  });
2748
2969
 
@@ -2890,7 +3111,7 @@ var init_gate = __esm({
2890
3111
  });
2891
3112
 
2892
3113
  // packages/core/dist/daemon/liveness-registry.js
2893
- import { writeFileSync as writeFileSync6, readFileSync as readFileSync7, renameSync as renameSync2 } from "fs";
3114
+ import { writeFileSync as writeFileSync7, readFileSync as readFileSync8, renameSync as renameSync3 } from "fs";
2894
3115
  var LivenessRegistry;
2895
3116
  var init_liveness_registry = __esm({
2896
3117
  "packages/core/dist/daemon/liveness-registry.js"() {
@@ -2904,14 +3125,14 @@ var init_liveness_registry = __esm({
2904
3125
  this.path = opts.path;
2905
3126
  this.readFile = opts.readFile ?? ((p) => {
2906
3127
  try {
2907
- return readFileSync7(p, "utf-8");
3128
+ return readFileSync8(p, "utf-8");
2908
3129
  } catch {
2909
3130
  return void 0;
2910
3131
  }
2911
3132
  });
2912
3133
  this.writeFile = opts.writeFile ?? ((p, c) => {
2913
- writeFileSync6(`${p}.tmp`, c);
2914
- renameSync2(`${p}.tmp`, p);
3134
+ writeFileSync7(`${p}.tmp`, c);
3135
+ renameSync3(`${p}.tmp`, p);
2915
3136
  });
2916
3137
  }
2917
3138
  load() {
@@ -2960,10 +3181,10 @@ var init_liveness_registry = __esm({
2960
3181
  });
2961
3182
 
2962
3183
  // packages/core/dist/daemon/context.js
2963
- import { homedir as homedir5 } from "os";
2964
- import { join as join8 } from "path";
3184
+ import { homedir as homedir6 } from "os";
3185
+ import { join as join9 } from "path";
2965
3186
  import { spawn as realSpawn } from "child_process";
2966
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
3187
+ import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync6 } from "fs";
2967
3188
  function defaultIsPidAlive(pid) {
2968
3189
  try {
2969
3190
  process.kill(pid, 0);
@@ -2973,18 +3194,18 @@ function defaultIsPidAlive(pid) {
2973
3194
  }
2974
3195
  }
2975
3196
  function buildContext(opts) {
2976
- const stateRoot = opts.stateRoot ?? join8(homedir5(), ".config", "squadrant", "state");
2977
- const sockPath = opts.sockPath ?? join8(homedir5(), ".config", "squadrant", "squadrant.sock");
3197
+ const stateRoot = opts.stateRoot ?? join9(homedir6(), ".config", "squadrant", "state");
3198
+ const sockPath = opts.sockPath ?? join9(homedir6(), ".config", "squadrant", "squadrant.sock");
2978
3199
  const store = createStore(stateRoot);
2979
3200
  const bootedAt = Date.now();
2980
3201
  const taskTimeoutMs = loadConfig().defaults.taskTimeoutMs;
2981
3202
  const isPidAlive = opts.isPidAlive ?? defaultIsPidAlive;
2982
3203
  const spawn2 = opts.spawn ?? realSpawn;
2983
- const resultsDir = join8(stateRoot, "_results");
2984
- mkdirSync5(resultsDir, { recursive: true });
3204
+ const resultsDir = join9(stateRoot, "_results");
3205
+ mkdirSync6(resultsDir, { recursive: true });
2985
3206
  const writeResult = (id, payload) => {
2986
- const p = join8(resultsDir, `${id}.txt`);
2987
- writeFileSync7(p, payload);
3207
+ const p = join9(resultsDir, `${id}.txt`);
3208
+ writeFileSync8(p, payload);
2988
3209
  return p;
2989
3210
  };
2990
3211
  const log = (m) => process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${m}
@@ -3006,7 +3227,7 @@ function buildContext(opts) {
3006
3227
  inFlightHeadlessIds: /* @__PURE__ */ new Set(),
3007
3228
  activeHeadlessKills: /* @__PURE__ */ new Set(),
3008
3229
  livenessRegistry: (() => {
3009
- const r = new LivenessRegistry({ path: join8(stateRoot, "liveness.json") });
3230
+ const r = new LivenessRegistry({ path: join9(stateRoot, "liveness.json") });
3010
3231
  r.load();
3011
3232
  return r;
3012
3233
  })(),
@@ -3319,9 +3540,11 @@ var init_defer_delivery = __esm({
3319
3540
  "packages/core/dist/delivery/defer-delivery.js"() {
3320
3541
  DeferDelivery = class extends Error {
3321
3542
  draft;
3322
- constructor(draft = null) {
3543
+ reason;
3544
+ constructor(draft = null, reason = "draft") {
3323
3545
  super("deferred: captain composing");
3324
3546
  this.draft = draft;
3547
+ this.reason = reason;
3325
3548
  this.name = "DeferDelivery";
3326
3549
  }
3327
3550
  };
@@ -3345,6 +3568,7 @@ var init_captain_delivery = __esm({
3345
3568
  deferCounts = /* @__PURE__ */ new Map();
3346
3569
  lastContent = /* @__PURE__ */ new Map();
3347
3570
  stableCounts = /* @__PURE__ */ new Map();
3571
+ lastReason = /* @__PURE__ */ new Map();
3348
3572
  constructor(opts) {
3349
3573
  this.opts = opts;
3350
3574
  }
@@ -3367,29 +3591,40 @@ var init_captain_delivery = __esm({
3367
3591
  this.deferCounts.delete(seq);
3368
3592
  this.stableCounts.delete(seq);
3369
3593
  this.lastContent.delete(seq);
3594
+ this.lastReason.delete(seq);
3370
3595
  return { delivered: true };
3371
3596
  } catch (e) {
3372
3597
  if (e instanceof DeferDelivery) {
3373
3598
  this.deferCounts.set(seq, deferCount + 1);
3374
3599
  const content = e.draft;
3600
+ let stableCount;
3375
3601
  if (content && content === this.lastContent.get(seq)) {
3376
- this.stableCounts.set(seq, (this.stableCounts.get(seq) ?? 0) + 1);
3602
+ stableCount = (this.stableCounts.get(seq) ?? 0) + 1;
3603
+ this.stableCounts.set(seq, stableCount);
3377
3604
  } else {
3605
+ stableCount = 0;
3378
3606
  this.stableCounts.set(seq, 0);
3379
3607
  }
3380
3608
  this.lastContent.set(seq, content);
3381
- return { deferred: true };
3609
+ const reason = e.reason !== "draft" ? e.reason : stableCount >= this.opts.stableProbePolls ? "stable" : "draft";
3610
+ this.lastReason.set(seq, reason);
3611
+ return { deferred: true, reason };
3382
3612
  }
3383
- return { deferred: true };
3613
+ this.lastReason.set(seq, "unknown");
3614
+ return { deferred: true, reason: "unknown" };
3384
3615
  }
3385
3616
  }
3386
3617
  /** Read-only. Never mutates — safe to poll from the snapshot assembler every tick. */
3387
3618
  stats() {
3388
3619
  let maxDeferCount = 0;
3389
- for (const c of this.deferCounts.values())
3390
- if (c > maxDeferCount)
3620
+ let reason;
3621
+ for (const [seq, c] of this.deferCounts) {
3622
+ if (c > maxDeferCount) {
3391
3623
  maxDeferCount = c;
3392
- return { maxDeferCount, stuck: maxDeferCount >= this.opts.maxDefers };
3624
+ reason = this.lastReason.get(seq);
3625
+ }
3626
+ }
3627
+ return { maxDeferCount, stuck: maxDeferCount >= this.opts.maxDefers, reason };
3393
3628
  }
3394
3629
  };
3395
3630
  }
@@ -3489,6 +3724,10 @@ function createDelivery(ctx, daemonCmux) {
3489
3724
  const notifyFault = ctx.notifyFault ?? (() => {
3490
3725
  });
3491
3726
  const defaultNotify = async (args) => {
3727
+ const fresh = store.get(args.project, args.record.id);
3728
+ if (fresh && TERMINAL_STATES.has(fresh.state) && fresh.state !== args.record.state) {
3729
+ return;
3730
+ }
3492
3731
  try {
3493
3732
  await appendToMailbox({
3494
3733
  stateRoot,
@@ -3578,16 +3817,19 @@ function createDelivery(ctx, daemonCmux) {
3578
3817
  log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
3579
3818
  await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
3580
3819
  } else {
3581
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred`);
3820
+ const { maxDeferCount } = d.stats();
3821
+ if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
3822
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
3823
+ }
3582
3824
  break;
3583
3825
  }
3584
3826
  }
3585
3827
  const stuck = d.stats().stuck;
3586
3828
  if (stuck && !stuckNotified.has(project)) {
3587
3829
  stuckNotified.add(project);
3588
- const { maxDeferCount } = d.stats();
3589
- log(`delivery stuck project=${project} deferCount=${maxDeferCount}`);
3590
- const text = `\u26A0\uFE0F DELIVERY STUCK: an in-progress draft (or ghost text) in your input box has blocked pending notification(s) for ${maxDeferCount}+ retries. Your input is never touched \u2014 this keeps retrying safely and will deliver automatically once you submit or clear it.`;
3830
+ const { maxDeferCount, reason } = d.stats();
3831
+ log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
3832
+ const text = reason === "modal" ? `\u26A0\uFE0F DELIVERY STUCK: a modal question is open in your captain pane and has blocked pending notification(s) for ${maxDeferCount}+ retries. This keeps retrying safely and will deliver automatically once you answer or dismiss it.` : `\u26A0\uFE0F DELIVERY STUCK: an in-progress draft (or ghost text) in your input box has blocked pending notification(s) for ${maxDeferCount}+ retries. Your input is never touched \u2014 this keeps retrying safely and will deliver automatically once you submit or clear it.`;
3591
3833
  appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`delivery stuck alert failed project=${project}: ${e.message}`));
3592
3834
  Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
3593
3835
  telegramBridge?.pushRaw(project, text);
@@ -3707,19 +3949,19 @@ var init_server = __esm({
3707
3949
 
3708
3950
  // packages/core/dist/daemon/snapshot-gather.js
3709
3951
  import { fileURLToPath as fileURLToPath2 } from "url";
3710
- import { join as join9 } from "path";
3711
- import { statSync as statSync2, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
3952
+ import { join as join10 } from "path";
3953
+ import { statSync as statSync3, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
3712
3954
  function distBuiltAt() {
3713
3955
  try {
3714
- return statSync2(SELF_PATH).mtimeMs;
3956
+ return statSync3(SELF_PATH).mtimeMs;
3715
3957
  } catch {
3716
3958
  return 0;
3717
3959
  }
3718
3960
  }
3719
- function gatherLogStats(path30, now, windowMs) {
3961
+ function gatherLogStats(path34, now, windowMs) {
3720
3962
  let sizeBytes = 0;
3721
3963
  try {
3722
- sizeBytes = statSync2(path30).size;
3964
+ sizeBytes = statSync3(path34).size;
3723
3965
  } catch {
3724
3966
  return { errorCount: 0, sizeBytes: 0, windowMs };
3725
3967
  }
@@ -3730,7 +3972,7 @@ function gatherLogStats(path30, now, windowMs) {
3730
3972
  const len = sizeBytes - start;
3731
3973
  let text = "";
3732
3974
  try {
3733
- const fd = openSync2(path30, "r");
3975
+ const fd = openSync2(path34, "r");
3734
3976
  try {
3735
3977
  const buf = Buffer.alloc(len);
3736
3978
  readSync(fd, buf, 0, len, start);
@@ -3761,9 +4003,9 @@ function gatherStoreStats(store, stateRoot, project) {
3761
4003
  for (const r of store.list(project))
3762
4004
  byState[r.state] = (byState[r.state] ?? 0) + 1;
3763
4005
  let corruptCount = 0;
3764
- const dir = join9(stateRoot, project);
4006
+ const dir = join10(stateRoot, project);
3765
4007
  try {
3766
- for (const n of readdirSync2(dir)) {
4008
+ for (const n of readdirSync3(dir)) {
3767
4009
  if (n.includes(".corrupt.")) {
3768
4010
  corruptCount++;
3769
4011
  continue;
@@ -3771,7 +4013,7 @@ function gatherStoreStats(store, stateRoot, project) {
3771
4013
  if (!n.endsWith(".json"))
3772
4014
  continue;
3773
4015
  try {
3774
- JSON.parse(readFileSync8(join9(dir, n), "utf-8"));
4016
+ JSON.parse(readFileSync9(join10(dir, n), "utf-8"));
3775
4017
  } catch {
3776
4018
  corruptCount++;
3777
4019
  }
@@ -3784,9 +4026,9 @@ function gatherResults(resultsDir) {
3784
4026
  let fileCount = 0;
3785
4027
  let totalBytes = 0;
3786
4028
  try {
3787
- for (const n of readdirSync2(resultsDir)) {
4029
+ for (const n of readdirSync3(resultsDir)) {
3788
4030
  try {
3789
- const s = statSync2(join9(resultsDir, n));
4031
+ const s = statSync3(join10(resultsDir, n));
3790
4032
  if (s.isFile()) {
3791
4033
  fileCount++;
3792
4034
  totalBytes += s.size;
@@ -3806,7 +4048,7 @@ var init_snapshot_gather = __esm({
3806
4048
  });
3807
4049
 
3808
4050
  // packages/core/dist/daemon/start.js
3809
- import { join as join10, dirname as dirname4 } from "path";
4051
+ import { join as join11, dirname as dirname4 } from "path";
3810
4052
  import { readdir } from "fs/promises";
3811
4053
  function startDaemon(ctx, opts, pkgVersion) {
3812
4054
  const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
@@ -3881,7 +4123,7 @@ function startDaemon(ctx, opts, pkgVersion) {
3881
4123
  return out;
3882
4124
  }
3883
4125
  async function gatherSnapshotInputs(now) {
3884
- const logPath2 = join10(dirname4(stateRoot), "squadrantd.log");
4126
+ const logPath2 = join11(dirname4(stateRoot), "squadrantd.log");
3885
4127
  const tier2Projects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
3886
4128
  const projects = await Promise.all(tier2Projects.map(async (project) => {
3887
4129
  const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER2 });
@@ -4015,7 +4257,7 @@ function startDaemon(ctx, opts, pkgVersion) {
4015
4257
  };
4016
4258
  let rotationTimer;
4017
4259
  if (rotationInterval > 0) {
4018
- const inboxPath = join10(stateRoot, "inbox");
4260
+ const inboxPath = join11(stateRoot, "inbox");
4019
4261
  rotationTimer = setInterval(async () => {
4020
4262
  try {
4021
4263
  let entries;
@@ -4058,9 +4300,9 @@ function startDaemon(ctx, opts, pkgVersion) {
4058
4300
  }
4059
4301
  for (const kill of ctx.activeHeadlessKills)
4060
4302
  kill();
4061
- return new Promise((resolve3) => server.close(() => {
4303
+ return new Promise((resolve4) => server.close(() => {
4062
4304
  log(`exit-complete pid=${process.pid}`);
4063
- resolve3();
4305
+ resolve4();
4064
4306
  }));
4065
4307
  },
4066
4308
  tickDelivery: deliveryTick,
@@ -4211,8 +4453,8 @@ import { exec as nodeExec } from "child_process";
4211
4453
  async function reapCrewChildren(taskId, graceMs = 2e3, execFn = nodeExec) {
4212
4454
  const marker = `SQUADRANT_CREW_TASK_ID=${taskId}`;
4213
4455
  try {
4214
- const stdout = await new Promise((resolve3, reject) => {
4215
- execFn("ps auxE", { maxBuffer: 64 * 1024 * 1024 }, (err, out) => err ? reject(err) : resolve3(out));
4456
+ const stdout = await new Promise((resolve4, reject) => {
4457
+ execFn("ps auxE", { maxBuffer: 64 * 1024 * 1024 }, (err, out) => err ? reject(err) : resolve4(out));
4216
4458
  });
4217
4459
  const pids = [];
4218
4460
  for (const line of stdout.split("\n").slice(1)) {
@@ -4428,7 +4670,7 @@ function createIsCaptainAlive(sock) {
4428
4670
  };
4429
4671
  }
4430
4672
  function createLaunch(cliBin, log) {
4431
- return (project) => new Promise((resolve3, reject) => {
4673
+ return (project) => new Promise((resolve4, reject) => {
4432
4674
  execFile(process.execPath, [cliBin, "launch", project, "--headless"], { timeout: 3e4 }, (err, stdout, stderr) => {
4433
4675
  const output = capOutput(stdout ?? "", stderr ?? "");
4434
4676
  if (err) {
@@ -4438,7 +4680,7 @@ function createLaunch(cliBin, log) {
4438
4680
  }
4439
4681
  if (output !== "(no output)")
4440
4682
  log?.(`launch ${project}: ${output}`);
4441
- resolve3();
4683
+ resolve4();
4442
4684
  });
4443
4685
  });
4444
4686
  }
@@ -4586,10 +4828,10 @@ function findProjectByThread(stateRoot, threadId) {
4586
4828
  for (const [key, id] of Object.entries(s.topics)) {
4587
4829
  if (id !== threadId)
4588
4830
  continue;
4589
- const sep2 = key.indexOf("::");
4590
- if (sep2 === -1)
4831
+ const sep3 = key.indexOf("::");
4832
+ if (sep3 === -1)
4591
4833
  continue;
4592
- return { project: key.slice(0, sep2), scope: key.slice(sep2 + 2) };
4834
+ return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2) };
4593
4835
  }
4594
4836
  return null;
4595
4837
  }
@@ -5140,11 +5382,11 @@ var init_bridge = __esm({
5140
5382
 
5141
5383
  // packages/core/dist/restart-daemon.js
5142
5384
  import { execFileSync as execFileSync4 } from "child_process";
5143
- import { existsSync as existsSync8 } from "fs";
5144
- import { homedir as homedir6 } from "os";
5145
- import { join as join11 } from "path";
5385
+ import { existsSync as existsSync9 } from "fs";
5386
+ import { homedir as homedir7 } from "os";
5387
+ import { join as join12 } from "path";
5146
5388
  function defaultIsRunning() {
5147
- return existsSync8(DEFAULT_SOCK_PATH);
5389
+ return existsSync9(DEFAULT_SOCK_PATH);
5148
5390
  }
5149
5391
  function defaultRunKickstart() {
5150
5392
  const uid = process.getuid?.() ?? 0;
@@ -5174,7 +5416,7 @@ var DEFAULT_SOCK_PATH;
5174
5416
  var init_restart_daemon = __esm({
5175
5417
  "packages/core/dist/restart-daemon.js"() {
5176
5418
  init_launchd();
5177
- DEFAULT_SOCK_PATH = join11(homedir6(), ".config", "squadrant", "squadrant.sock");
5419
+ DEFAULT_SOCK_PATH = join12(homedir7(), ".config", "squadrant", "squadrant.sock");
5178
5420
  }
5179
5421
  });
5180
5422
 
@@ -5274,8 +5516,8 @@ function runTelegramStatus(opts) {
5274
5516
  const env = opts.env ?? process.env;
5275
5517
  const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
5276
5518
  const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
5277
- const sep2 = key.indexOf("::");
5278
- return { project: key.slice(0, sep2), scope: key.slice(sep2 + 2), topicId };
5519
+ const sep3 = key.indexOf("::");
5520
+ return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2), topicId };
5279
5521
  });
5280
5522
  return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
5281
5523
  }
@@ -5299,8 +5541,8 @@ function runTelegramNotifyStatus(opts) {
5299
5541
  const s = loadState(opts.stateRoot);
5300
5542
  const projects = /* @__PURE__ */ new Set();
5301
5543
  for (const key of Object.keys(s.topics)) {
5302
- const sep2 = key.indexOf("::");
5303
- projects.add(sep2 === -1 ? key : key.slice(0, sep2));
5544
+ const sep3 = key.indexOf("::");
5545
+ projects.add(sep3 === -1 ? key : key.slice(0, sep3));
5304
5546
  }
5305
5547
  for (const p of Object.keys(s.notify))
5306
5548
  projects.add(p);
@@ -5398,8 +5640,8 @@ var init_crew_routing = __esm({
5398
5640
 
5399
5641
  // packages/core/dist/group-dispatch.js
5400
5642
  import { randomUUID as randomUUID3 } from "crypto";
5401
- import { homedir as homedir7 } from "os";
5402
- import { join as join12 } from "path";
5643
+ import { homedir as homedir8 } from "os";
5644
+ import { join as join13 } from "path";
5403
5645
  function resolveCurrentProject(config) {
5404
5646
  const cwd = process.cwd();
5405
5647
  for (const [name, proj] of Object.entries(config.projects)) {
@@ -5476,7 +5718,7 @@ var init_group_dispatch = __esm({
5476
5718
  "packages/core/dist/group-dispatch.js"() {
5477
5719
  init_dist();
5478
5720
  init_protocol();
5479
- DEFAULT_SOCK_PATH2 = join12(homedir7(), ".config", "squadrant", "squadrant.sock");
5721
+ DEFAULT_SOCK_PATH2 = join13(homedir8(), ".config", "squadrant", "squadrant.sock");
5480
5722
  GROUP_DISPATCH_WARMUP_TIMEOUT_MS = 12e4;
5481
5723
  GROUP_DISPATCH_WARMUP_POLL_MS = 1e3;
5482
5724
  }
@@ -5557,7 +5799,8 @@ async function launchOneWorkspace(opts) {
5557
5799
  forceFresh = true;
5558
5800
  }
5559
5801
  }
5560
- const agentCmd = opts.agentCmdFactory(forceFresh);
5802
+ const builtCmd = opts.agentCmdFactory(forceFresh);
5803
+ const agentCmd = opts.role === "captain" ? `SQUADRANT_ROLE=captain ${builtCmd}` : builtCmd;
5561
5804
  recordSession(opts.workspaceName, opts.role, {
5562
5805
  sessionsPath: opts.sessionsPath,
5563
5806
  templatesDir: opts.templatesDir
@@ -6079,12 +6322,15 @@ __export(dist_exports2, {
6079
6322
  GROUP_DISPATCH_WARMUP_TIMEOUT_MS: () => GROUP_DISPATCH_WARMUP_TIMEOUT_MS,
6080
6323
  IDLE_DEBOUNCE_MS: () => IDLE_DEBOUNCE_MS,
6081
6324
  LABEL: () => LABEL,
6325
+ MONITOR_STALL_BUDGET_MS: () => MONITOR_STALL_BUDGET_MS,
6326
+ OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
6082
6327
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
6083
6328
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
6084
6329
  STALE_THRESHOLD_MS: () => STALE_THRESHOLD_MS,
6085
6330
  TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
6086
6331
  TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
6087
6332
  TOOL_STALL_BUDGET_MS: () => TOOL_STALL_BUDGET_MS,
6333
+ WORK_ITEM_TTL_MS: () => WORK_ITEM_TTL_MS,
6088
6334
  WRITABLE_CONFIG_KEYS: () => WRITABLE_CONFIG_KEYS,
6089
6335
  _resetRestartInFlightForTest: () => _resetRestartInFlightForTest,
6090
6336
  ageText: () => ageText,
@@ -6100,6 +6346,7 @@ __export(dist_exports2, {
6100
6346
  capAllowed: () => capAllowed,
6101
6347
  capOutput: () => capOutput,
6102
6348
  classifyHealth: () => classifyHealth,
6349
+ closeWorkItem: () => closeWorkItem,
6103
6350
  computeTemplateHash: () => computeTemplateHash,
6104
6351
  createAttach: () => createAttach,
6105
6352
  createCrewPaneReader: () => createCrewPaneReader,
@@ -6117,6 +6364,8 @@ __export(dist_exports2, {
6117
6364
  createSurfaceLivenessProbe: () => createSurfaceLivenessProbe,
6118
6365
  createTelegramBridge: () => createTelegramBridge,
6119
6366
  createTelegramClient: () => createTelegramClient,
6367
+ createWorkItem: () => createWorkItem,
6368
+ createWorkStore: () => createWorkStore,
6120
6369
  crewPaneTitle: () => crewPaneTitle,
6121
6370
  crewTag: () => crewTag,
6122
6371
  daemonEntryPath: () => daemonEntryPath,
@@ -6124,6 +6373,7 @@ __export(dist_exports2, {
6124
6373
  decodeFrames: () => decodeFrames,
6125
6374
  defaultIsPidAlive: () => defaultIsPidAlive,
6126
6375
  defaultListenError: () => defaultListenError,
6376
+ defaultWorkRoot: () => defaultWorkRoot,
6127
6377
  deliverStartupPrompt: () => deliverStartupPrompt,
6128
6378
  deliverable: () => deliverable,
6129
6379
  deriveCaptainState: () => deriveCaptainState,
@@ -6135,7 +6385,9 @@ __export(dist_exports2, {
6135
6385
  encodeMsg: () => encodeMsg,
6136
6386
  ensureDaemon: () => ensureDaemon,
6137
6387
  evaluateStall: () => evaluateStall,
6388
+ findOpenChildren: () => findOpenChildren,
6138
6389
  findProjectByThread: () => findProjectByThread,
6390
+ findWorkItemById: () => findWorkItemById,
6139
6391
  formatInbound: () => formatInbound,
6140
6392
  formatLifecycle: () => formatLifecycle,
6141
6393
  healCmdFor: () => healCmdFor,
@@ -6147,7 +6399,9 @@ __export(dist_exports2, {
6147
6399
  isCrewTitle: () => isCrewTitle,
6148
6400
  isDaemonSocketLive: () => isDaemonSocketLive,
6149
6401
  isNotifyActive: () => isNotifyActive,
6402
+ isOperatorInitiatedCommand: () => isOperatorInitiatedCommand,
6150
6403
  isSideTitle: () => isSideTitle,
6404
+ isStickyAttention: () => isStickyAttention,
6151
6405
  isTurnAccepted: () => isTurnAccepted,
6152
6406
  kickstartArgv: () => kickstartArgv,
6153
6407
  launchOneWorkspace: () => launchOneWorkspace,
@@ -6165,6 +6419,7 @@ __export(dist_exports2, {
6165
6419
  plistPath: () => plistPath,
6166
6420
  programArgsBlock: () => programArgsBlock,
6167
6421
  projectHealth: () => projectHealth,
6422
+ purgeExpiredWorkItems: () => purgeExpiredWorkItems,
6168
6423
  readCursor: () => readCursor,
6169
6424
  readFromCursor: () => readFromCursor,
6170
6425
  reapCrewChildren: () => reapCrewChildren,
@@ -6176,6 +6431,7 @@ __export(dist_exports2, {
6176
6431
  reduceLifecycle: () => reduceLifecycle,
6177
6432
  releaseDaemonLock: () => releaseDaemonLock,
6178
6433
  renderPlist: () => renderPlist,
6434
+ reregisterDaemon: () => reregisterDaemon,
6179
6435
  resolveAgentBinDirs: () => resolveAgentBinDirs,
6180
6436
  resolveCrewRoute: () => resolveCrewRoute,
6181
6437
  resolveCurrentProject: () => resolveCurrentProject,
@@ -6240,6 +6496,7 @@ var init_dist2 = __esm({
6240
6496
  init_liveness2();
6241
6497
  init_watchdog();
6242
6498
  init_store();
6499
+ init_work_store();
6243
6500
  init_snapshot();
6244
6501
  init_launchd();
6245
6502
  init_crew_pane_reader();
@@ -6279,7 +6536,7 @@ function cmuxLocal(args) {
6279
6536
  }).trim();
6280
6537
  }
6281
6538
  function cmux(args) {
6282
- return new Promise((resolve3, reject) => {
6539
+ return new Promise((resolve4, reject) => {
6283
6540
  execFile2(
6284
6541
  resolveCmuxBin(),
6285
6542
  args,
@@ -6293,19 +6550,19 @@ function cmux(args) {
6293
6550
  reject(err.code === "ETIMEDOUT" ? new CmuxTimeoutError(args.join(" ")) : err);
6294
6551
  return;
6295
6552
  }
6296
- resolve3(stdout.trim());
6553
+ resolve4(stdout.trim());
6297
6554
  }
6298
6555
  );
6299
6556
  });
6300
6557
  }
6301
6558
  function cmuxStdin(args, input) {
6302
- return new Promise((resolve3, reject) => {
6559
+ return new Promise((resolve4, reject) => {
6303
6560
  const child = execFile2(resolveCmuxBin(), args, { encoding: "utf-8", timeout: CMUX_TIMEOUT, env: { ...process.env, CMUX_QUIET: "1" } }, (err, stdout) => {
6304
6561
  if (err) {
6305
6562
  reject(err.code === "ETIMEDOUT" ? new CmuxTimeoutError(args.join(" ")) : err);
6306
6563
  return;
6307
6564
  }
6308
- resolve3(stdout.trim());
6565
+ resolve4(stdout.trim());
6309
6566
  });
6310
6567
  child.stdin.end(input);
6311
6568
  });
@@ -6666,9 +6923,9 @@ function createCmuxDriver() {
6666
6923
  }
6667
6924
  const draft = parseDraftFromScreen(screen);
6668
6925
  if (draft === null)
6669
- throw new DeferDelivery(null);
6926
+ throw new DeferDelivery(null, "no-box");
6670
6927
  if (hasModalOptionList(screen))
6671
- throw new DeferDelivery(null);
6928
+ throw new DeferDelivery(null, "modal");
6672
6929
  if (draft === "") {
6673
6930
  await deliver();
6674
6931
  return;
@@ -6912,7 +7169,7 @@ var init_notifiers = __esm({
6912
7169
 
6913
7170
  // packages/workspaces/dist/workspaces/obsidian.js
6914
7171
  import fs15 from "fs/promises";
6915
- import { existsSync as existsSync9 } from "fs";
7172
+ import { existsSync as existsSync10 } from "fs";
6916
7173
  import path12 from "path";
6917
7174
  function resolveInRoot(root, relative) {
6918
7175
  const joined = path12.resolve(root, relative);
@@ -6932,7 +7189,7 @@ function createObsidianDriver(scope) {
6932
7189
  async probe() {
6933
7190
  return {
6934
7191
  installed: true,
6935
- rootExists: existsSync9(root)
7192
+ rootExists: existsSync10(root)
6936
7193
  };
6937
7194
  },
6938
7195
  async read(rel) {
@@ -7087,12 +7344,12 @@ var init_events_bridge = __esm({
7087
7344
  continue;
7088
7345
  }
7089
7346
  this.child = child;
7090
- await new Promise((resolve3) => {
7347
+ await new Promise((resolve4) => {
7091
7348
  let settled = false;
7092
7349
  const done = () => {
7093
7350
  if (!settled) {
7094
7351
  settled = true;
7095
- resolve3();
7352
+ resolve4();
7096
7353
  }
7097
7354
  };
7098
7355
  child.stdout?.on("data", (b) => this.onData(b));
@@ -7223,9 +7480,9 @@ var init_store_fingerprint = __esm({
7223
7480
  });
7224
7481
 
7225
7482
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
7226
- import { readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
7227
- import { join as join13 } from "path";
7228
- import { homedir as homedir8 } from "os";
7483
+ import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "fs";
7484
+ import { join as join14 } from "path";
7485
+ import { homedir as homedir9 } from "os";
7229
7486
  var DaemonCmux;
7230
7487
  var init_daemon_cmux = __esm({
7231
7488
  "packages/workspaces/dist/cmux-daemon/daemon-cmux.js"() {
@@ -7288,24 +7545,24 @@ var init_daemon_cmux = __esm({
7288
7545
  * file failed to read/parse — see the class doc above.
7289
7546
  */
7290
7547
  async liveness() {
7291
- const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join13(homedir8(), ".cmuxterm");
7548
+ const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join14(homedir9(), ".cmuxterm");
7292
7549
  const projects = loadConfig().projects;
7293
7550
  let files;
7294
7551
  try {
7295
- files = readdirSync3(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
7552
+ files = readdirSync4(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
7296
7553
  } catch (e) {
7297
7554
  throw new Error(`liveness: could not read cmux state dir ${dir}: ${e.message}`);
7298
7555
  }
7299
- return readLivenessSnapshot(files, (f) => readFileSync9(join13(dir, f), "utf-8"), projects);
7556
+ return readLivenessSnapshot(files, (f) => readFileSync10(join14(dir, f), "utf-8"), projects);
7300
7557
  }
7301
7558
  };
7302
7559
  }
7303
7560
  });
7304
7561
 
7305
7562
  // packages/workspaces/dist/cmux-daemon/cmux-store-source.js
7306
- import { join as join14 } from "path";
7307
- import { homedir as homedir9 } from "os";
7308
- import { watch, readdirSync as readdirSync4, readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
7563
+ import { join as join15 } from "path";
7564
+ import { homedir as homedir10 } from "os";
7565
+ import { watch, readdirSync as readdirSync5, readFileSync as readFileSync11, existsSync as existsSync11 } from "fs";
7309
7566
  function parseLifecycleState(s) {
7310
7567
  if (s === "running" || s === "idle" || s === "needsInput" || s === "unknown") {
7311
7568
  return s;
@@ -7322,14 +7579,14 @@ function defaultIsPidAlive2(pid) {
7322
7579
  }
7323
7580
  function defaultListFiles(dir) {
7324
7581
  try {
7325
- return readdirSync4(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
7582
+ return readdirSync5(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
7326
7583
  } catch {
7327
7584
  return [];
7328
7585
  }
7329
7586
  }
7330
- function defaultReadFile(path30) {
7587
+ function defaultReadFile(path34) {
7331
7588
  try {
7332
- return readFileSync10(path30, "utf-8");
7589
+ return readFileSync11(path34, "utf-8");
7333
7590
  } catch {
7334
7591
  return void 0;
7335
7592
  }
@@ -7365,12 +7622,12 @@ var init_cmux_store_source = __esm({
7365
7622
  active = false;
7366
7623
  lastError = null;
7367
7624
  constructor(opts = {}) {
7368
- this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join14(homedir9(), ".cmuxterm");
7625
+ this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir10(), ".cmuxterm");
7369
7626
  this.debounceMs = opts.debounceMs ?? 50;
7370
7627
  this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
7371
7628
  this.listFiles = opts.listFiles ?? defaultListFiles;
7372
7629
  this.readFile = opts.readFile ?? defaultReadFile;
7373
- this.fileExists = opts.fileExists ?? existsSync10;
7630
+ this.fileExists = opts.fileExists ?? existsSync11;
7374
7631
  this.watchDir = opts.watchDir ?? defaultWatchDir;
7375
7632
  this.scheduleTimer = opts.scheduleTimer ?? setTimeout;
7376
7633
  this.cancelTimer = opts.cancelTimer ?? clearTimeout;
@@ -7427,7 +7684,7 @@ var init_cmux_store_source = __esm({
7427
7684
  }
7428
7685
  scanFile(filename) {
7429
7686
  const deps = this.deps;
7430
- const filePath = join14(this.stateDir, filename);
7687
+ const filePath = join15(this.stateDir, filename);
7431
7688
  const lockPath = `${filePath}.lock`;
7432
7689
  if (this.fileExists(lockPath)) {
7433
7690
  this.log(`cmux-store: skipping ${filename} (locked)`);
@@ -7481,11 +7738,11 @@ var init_cmux_store_source = __esm({
7481
7738
  });
7482
7739
 
7483
7740
  // packages/workspaces/dist/native-hooks/native-hook-source.js
7484
- import { join as join15 } from "path";
7485
- import { homedir as homedir10 } from "os";
7486
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
7741
+ import { join as join16 } from "path";
7742
+ import { homedir as homedir11 } from "os";
7743
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
7487
7744
  function installClaudeHooks(opts = {}) {
7488
- const settingsPath = opts.settingsPath ?? join15(homedir10(), ".claude", "settings.json");
7745
+ const settingsPath = opts.settingsPath ?? join16(homedir11(), ".claude", "settings.json");
7489
7746
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
7490
7747
  const readFile6 = opts.readFile ?? defaultReadFile2;
7491
7748
  const writeFile5 = opts.writeFile ?? defaultWriteFile;
@@ -7493,6 +7750,7 @@ function installClaudeHooks(opts = {}) {
7493
7750
  });
7494
7751
  let settings = {};
7495
7752
  const raw = readFile6(settingsPath);
7753
+ const hadExistingSettings = raw !== void 0;
7496
7754
  if (raw) {
7497
7755
  try {
7498
7756
  settings = JSON.parse(raw);
@@ -7505,6 +7763,7 @@ function installClaudeHooks(opts = {}) {
7505
7763
  }
7506
7764
  const hooks = settings.hooks;
7507
7765
  let changed = false;
7766
+ const repaired = [];
7508
7767
  for (const [eventName, sub, matcher] of CLAUDE_HOOK_EVENTS) {
7509
7768
  if (!Array.isArray(hooks[eventName])) {
7510
7769
  hooks[eventName] = [];
@@ -7516,6 +7775,26 @@ function installClaudeHooks(opts = {}) {
7516
7775
  if (!alreadyPresent) {
7517
7776
  entries.push({ matcher: hookMatcher, hooks: [{ type: "command", command, timeout: 10 }] });
7518
7777
  changed = true;
7778
+ repaired.push(`${eventName}/${sub}`);
7779
+ }
7780
+ }
7781
+ if (repaired.length > 0 && hadExistingSettings) {
7782
+ log(`native-hook: repaired ${repaired.length} missing squadrant hook(s) in ${settingsPath} [${repaired.join(", ")}] \u2014 WARNING: blocked-signalling or lifecycle tracking may have been broken until this run`);
7783
+ }
7784
+ if (opts.claudeEnv && Object.keys(opts.claudeEnv).length > 0) {
7785
+ if (typeof settings.env !== "object" || settings.env === null || Array.isArray(settings.env)) {
7786
+ settings.env = {};
7787
+ }
7788
+ const env = settings.env;
7789
+ for (const [key, value] of Object.entries(opts.claudeEnv)) {
7790
+ if (key in env) {
7791
+ if (env[key] !== value) {
7792
+ log(`native-hook: claudeEnv key '${key}' already set to '${String(env[key])}' in ${settingsPath} \u2014 not overwriting with '${value}'`);
7793
+ }
7794
+ continue;
7795
+ }
7796
+ env[key] = value;
7797
+ changed = true;
7519
7798
  }
7520
7799
  }
7521
7800
  if (changed) {
@@ -7557,16 +7836,16 @@ function extractDetail(sub, payload) {
7557
7836
  }
7558
7837
  return void 0;
7559
7838
  }
7560
- function defaultReadFile2(path30) {
7839
+ function defaultReadFile2(path34) {
7561
7840
  try {
7562
- return readFileSync11(path30, "utf-8");
7841
+ return readFileSync12(path34, "utf-8");
7563
7842
  } catch {
7564
7843
  return void 0;
7565
7844
  }
7566
7845
  }
7567
- function defaultWriteFile(path30, content) {
7568
- mkdirSync6(path30.replace(/\/[^/]+$/, ""), { recursive: true });
7569
- writeFileSync8(path30, content, "utf-8");
7846
+ function defaultWriteFile(path34, content) {
7847
+ mkdirSync7(path34.replace(/\/[^/]+$/, ""), { recursive: true });
7848
+ writeFileSync9(path34, content, "utf-8");
7570
7849
  }
7571
7850
  var CLAUDE_HOOK_EVENTS, DEFAULT_HOOK_CMD, NativeHookSource;
7572
7851
  var init_native_hook_source = __esm({
@@ -7590,9 +7869,9 @@ var init_native_hook_source = __esm({
7590
7869
  cache = /* @__PURE__ */ new Map();
7591
7870
  active = false;
7592
7871
  constructor(opts = {}) {
7593
- this.hookInstall = opts.hookInstall ?? {};
7594
7872
  this.log = opts.log ?? (() => {
7595
7873
  });
7874
+ this.hookInstall = { log: this.log, ...opts.hookInstall };
7596
7875
  }
7597
7876
  start(deps) {
7598
7877
  this.deps = deps;
@@ -7676,13 +7955,13 @@ async function settleInputBox(runtime, pane) {
7676
7955
  return sawContent;
7677
7956
  }
7678
7957
  function getFreePort() {
7679
- return new Promise((resolve3, reject) => {
7958
+ return new Promise((resolve4, reject) => {
7680
7959
  const srv = net.createServer();
7681
7960
  srv.once("error", reject);
7682
7961
  srv.listen(0, "127.0.0.1", () => {
7683
7962
  const addr = srv.address();
7684
7963
  const port = typeof addr === "object" && addr ? addr.port : 0;
7685
- srv.close(() => port ? resolve3(port) : reject(new Error("no free port assigned")));
7964
+ srv.close(() => port ? resolve4(port) : reject(new Error("no free port assigned")));
7686
7965
  });
7687
7966
  });
7688
7967
  }
@@ -7925,7 +8204,8 @@ function createClaudeDriver() {
7925
8204
  if (opts.settingsPath) {
7926
8205
  cmd += ` --settings ${opts.settingsPath}`;
7927
8206
  }
7928
- const pluginDir = `${process.env.HOME}/.config/squadrant/plugin`;
8207
+ const pluginSubdir = opts.role === "crew" ? "plugin-crew" : "plugin";
8208
+ const pluginDir = `${process.env.HOME}/.config/squadrant/${pluginSubdir}`;
7929
8209
  cmd += ` --plugin-dir ${pluginDir}`;
7930
8210
  if (!opts.interactive) {
7931
8211
  cmd += ` -p "${opts.prompt.replace(/"/g, '\\"')}"`;
@@ -8347,8 +8627,8 @@ ${MARKER_END}
8347
8627
  const startIdx = existing.indexOf(MARKER_START);
8348
8628
  const endIdx = existing.indexOf(MARKER_END);
8349
8629
  if (startIdx === -1 && endIdx === -1) {
8350
- const sep2 = existing.endsWith("\n") ? "\n" : "\n\n";
8351
- return `${existing}${sep2}${block}`;
8630
+ const sep3 = existing.endsWith("\n") ? "\n" : "\n\n";
8631
+ return `${existing}${sep3}${block}`;
8352
8632
  }
8353
8633
  if (startIdx === -1 || endIdx === -1) {
8354
8634
  throw new Error(`Corrupted squadrant markers \u2014 found only ${startIdx === -1 ? "end" : "start"} marker. Remove the stray marker or delete the file and re-run projection emit.`);
@@ -8679,8 +8959,8 @@ var init_app_server_client = __esm({
8679
8959
  const info = this.opts.clientInfo ?? { name: "squadrant", version: "0" };
8680
8960
  const id = this.nextId++;
8681
8961
  const env = { jsonrpc: "2.0", id, method: "initialize", params: { clientInfo: info } };
8682
- const res = await new Promise((resolve3, reject) => {
8683
- this.pending.set(id, { resolve: resolve3, reject });
8962
+ const res = await new Promise((resolve4, reject) => {
8963
+ this.pending.set(id, { resolve: resolve4, reject });
8684
8964
  this.proc.stdin.write(JSON.stringify(env) + "\n");
8685
8965
  });
8686
8966
  this.proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "initialized" }) + "\n");
@@ -8713,13 +8993,13 @@ var init_app_server_client = __esm({
8713
8993
  const turnId = ack?.turn?.id;
8714
8994
  if (typeof turnId !== "string")
8715
8995
  throw new Error(`turn/start: unexpected ack shape (no turn.id): ${JSON.stringify(ack).slice(0, 200)}`);
8716
- return new Promise((resolve3, reject) => {
8996
+ return new Promise((resolve4, reject) => {
8717
8997
  const onNote = (n) => {
8718
8998
  if (n.params?.turn?.id !== turnId)
8719
8999
  return;
8720
9000
  if (n.method === "turn/completed") {
8721
9001
  cleanup();
8722
- resolve3({ turnId });
9002
+ resolve4({ turnId });
8723
9003
  }
8724
9004
  if (n.method === "turn/failed") {
8725
9005
  cleanup();
@@ -8770,8 +9050,8 @@ var init_app_server_client = __esm({
8770
9050
  throw new Error("AppServerClient not started");
8771
9051
  const id = this.nextId++;
8772
9052
  const env = { jsonrpc: "2.0", id, method, params: params ?? {} };
8773
- return new Promise((resolve3, reject) => {
8774
- this.pending.set(id, { resolve: resolve3, reject });
9053
+ return new Promise((resolve4, reject) => {
9054
+ this.pending.set(id, { resolve: resolve4, reject });
8775
9055
  this.proc.stdin.write(JSON.stringify(env) + "\n");
8776
9056
  });
8777
9057
  }
@@ -8903,11 +9183,11 @@ var init_codex_app_server_source = __esm({
8903
9183
 
8904
9184
  // packages/agents/dist/codex/config.js
8905
9185
  import { readFile as readFile5 } from "fs/promises";
8906
- import { homedir as homedir12 } from "os";
8907
- import { join as join17 } from "path";
9186
+ import { homedir as homedir13 } from "os";
9187
+ import { join as join18 } from "path";
8908
9188
  async function resolveCodexModel() {
8909
- const home = process.env["CODEX_HOME"] ?? join17(homedir12(), ".codex");
8910
- const configPath = join17(home, "config.toml");
9189
+ const home = process.env["CODEX_HOME"] ?? join18(homedir13(), ".codex");
9190
+ const configPath = join18(home, "config.toml");
8911
9191
  let text;
8912
9192
  try {
8913
9193
  text = await readFile5(configPath, "utf8");
@@ -9022,11 +9302,11 @@ function buildCodexDeveloperInstructions(rec) {
9022
9302
  ${directive}` : directive;
9023
9303
  }
9024
9304
  function withTimeout(p, ms, msg) {
9025
- return new Promise((resolve3, reject) => {
9305
+ return new Promise((resolve4, reject) => {
9026
9306
  const t = setTimeout(() => reject(new Error(msg)), ms);
9027
9307
  p.then((v) => {
9028
9308
  clearTimeout(t);
9029
- resolve3(v);
9309
+ resolve4(v);
9030
9310
  }, (e) => {
9031
9311
  clearTimeout(t);
9032
9312
  reject(e);
@@ -9428,9 +9708,9 @@ var init_sse_bridge = __esm({
9428
9708
 
9429
9709
  // packages/agents/dist/interactive/claude.js
9430
9710
  import { execSync as execSync7 } from "child_process";
9431
- import { readFileSync as readFileSync12 } from "fs";
9432
- import { homedir as homedir13 } from "os";
9433
- import { join as join18 } from "path";
9711
+ import { readFileSync as readFileSync13 } from "fs";
9712
+ import { homedir as homedir14 } from "os";
9713
+ import { join as join19 } from "path";
9434
9714
  function probeClaudeSettingsFlag() {
9435
9715
  try {
9436
9716
  const help = execSync7("claude --help", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
@@ -9488,11 +9768,11 @@ function deriveTranscriptPath(sessionId, cwd) {
9488
9768
  if (!sessionId || !cwd)
9489
9769
  return null;
9490
9770
  const escaped = cwd.replace(/[^a-zA-Z0-9]/g, "-");
9491
- return join18(homedir13(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
9771
+ return join19(homedir14(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
9492
9772
  }
9493
9773
  function readLastAssistantText(transcriptPath) {
9494
9774
  try {
9495
- const raw = readFileSync12(transcriptPath, "utf-8");
9775
+ const raw = readFileSync13(transcriptPath, "utf-8");
9496
9776
  const lines = raw.split(/\r?\n/);
9497
9777
  for (let i = lines.length - 1; i >= 0; i--) {
9498
9778
  const line = lines[i].trim();
@@ -9534,8 +9814,8 @@ function resolveLastAssistantText(payload) {
9534
9814
  const derived = deriveTranscriptPath(p?.session_id, cwd);
9535
9815
  if (derived)
9536
9816
  candidates.push(derived);
9537
- for (const path30 of candidates) {
9538
- const text = readLastAssistantText(path30);
9817
+ for (const path34 of candidates) {
9818
+ const text = readLastAssistantText(path34);
9539
9819
  if (text != null)
9540
9820
  return text;
9541
9821
  }
@@ -9885,14 +10165,14 @@ function runHeadless(opts) {
9885
10165
  if (err.length > ERR_CAP)
9886
10166
  err = err.slice(err.length - ERR_CAP);
9887
10167
  });
9888
- const result = new Promise((resolve3) => {
10168
+ const result = new Promise((resolve4) => {
9889
10169
  child.once("error", (e) => {
9890
10170
  if (debounceTimer) {
9891
10171
  clearTimeout(debounceTimer);
9892
10172
  debounceTimer = null;
9893
10173
  }
9894
10174
  opts.emit({ type: "task.failed", id: opts.id, error: `spawn error: ${e.message}`, exitCode: void 0 });
9895
- resolve3();
10175
+ resolve4();
9896
10176
  });
9897
10177
  child.on("close", (code) => {
9898
10178
  if (chunksSinceProgress > 0)
@@ -9909,7 +10189,7 @@ function runHeadless(opts) {
9909
10189
  const ref = opts.writeResult ? opts.writeResult(opts.id, res.payload ?? "") : "";
9910
10190
  opts.emit({ type: "task.done", id: opts.id, resultRef: ref, parseWarning: res.parseWarning });
9911
10191
  }
9912
- resolve3();
10192
+ resolve4();
9913
10193
  });
9914
10194
  });
9915
10195
  return { result, kill: () => child.kill("SIGTERM") };
@@ -9989,8 +10269,8 @@ var require_daemon_exports = {};
9989
10269
  __export(require_daemon_exports, {
9990
10270
  requireDaemon: () => requireDaemon
9991
10271
  });
9992
- import { join as join24 } from "path";
9993
- import { homedir as homedir19 } from "os";
10272
+ import { join as join25 } from "path";
10273
+ import { homedir as homedir20 } from "os";
9994
10274
  async function requireDaemon(sockPath = DEFAULT_SOCK_PATH3) {
9995
10275
  const isLive = await isDaemonSocketLive(sockPath);
9996
10276
  if (!isLive) {
@@ -10001,18 +10281,18 @@ var DEFAULT_SOCK_PATH3;
10001
10281
  var init_require_daemon = __esm({
10002
10282
  "packages/cli/src/lib/require-daemon.ts"() {
10003
10283
  init_dist2();
10004
- DEFAULT_SOCK_PATH3 = join24(homedir19(), ".config", "squadrant", "squadrant.sock");
10284
+ DEFAULT_SOCK_PATH3 = join25(homedir20(), ".config", "squadrant", "squadrant.sock");
10005
10285
  }
10006
10286
  });
10007
10287
 
10008
10288
  // packages/cli/src/index.ts
10009
10289
  init_dist();
10010
10290
  init_dist2();
10011
- import { Command as Command32 } from "commander";
10012
- import { readFileSync as readFileSync15, existsSync as existsSync12, writeFileSync as writeFileSync11 } from "fs";
10291
+ import { Command as Command35 } from "commander";
10292
+ import { readFileSync as readFileSync16, existsSync as existsSync13, writeFileSync as writeFileSync12 } from "fs";
10013
10293
  import { fileURLToPath as fileURLToPath6 } from "url";
10014
- import { dirname as dirname9, join as join29 } from "path";
10015
- import { homedir as homedir21 } from "os";
10294
+ import { dirname as dirname9, join as join30 } from "path";
10295
+ import { homedir as homedir22 } from "os";
10016
10296
 
10017
10297
  // packages/cli/src/commands/doctor.ts
10018
10298
  init_dist();
@@ -10029,10 +10309,10 @@ import chalk3 from "chalk";
10029
10309
  // packages/cli/src/commands/health-view.ts
10030
10310
  init_dist2();
10031
10311
  init_dist2();
10032
- import { homedir as homedir11 } from "os";
10033
- import { join as join16 } from "path";
10312
+ import { homedir as homedir12 } from "os";
10313
+ import { join as join17 } from "path";
10034
10314
  import chalk2 from "chalk";
10035
- var SOCK = join16(homedir11(), ".config", "squadrant", "squadrant.sock");
10315
+ var SOCK = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
10036
10316
  async function queryHealth(project) {
10037
10317
  try {
10038
10318
  const reply = await sendRequest(SOCK, { kind: "health", project });
@@ -10371,9 +10651,9 @@ import chalk4 from "chalk";
10371
10651
 
10372
10652
  // packages/cli/src/lib/per-crew-settings.ts
10373
10653
  init_dist4();
10374
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
10375
- import { dirname as dirname5, join as join19 } from "path";
10376
- import { homedir as homedir14 } from "os";
10654
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
10655
+ import { dirname as dirname5, join as join20 } from "path";
10656
+ import { homedir as homedir15 } from "os";
10377
10657
  var CREW_PERMISSION_ALLOWLIST = [
10378
10658
  // git — read + safe mutations (reset/clean/config intentionally excluded)
10379
10659
  "Bash(git status:*)",
@@ -10464,29 +10744,29 @@ function mergeCrewPermissions(settings) {
10464
10744
  return next;
10465
10745
  }
10466
10746
  function writePerCrewSettingsLocal(o) {
10467
- const dir = join19(o.projectCwd, ".claude");
10468
- mkdirSync7(dir, { recursive: true });
10469
- const file = join19(dir, "settings.local.json");
10747
+ const dir = join20(o.projectCwd, ".claude");
10748
+ mkdirSync8(dir, { recursive: true });
10749
+ const file = join20(dir, "settings.local.json");
10470
10750
  let existing = {};
10471
10751
  try {
10472
- const raw = healStaleCockpitRefs(readFileSync13(file, "utf-8"));
10752
+ const raw = healStaleCockpitRefs(readFileSync14(file, "utf-8"));
10473
10753
  existing = JSON.parse(raw);
10474
10754
  } catch {
10475
10755
  }
10476
10756
  const withHooks = mergeClaudeHooks(existing, o.hookCmd ?? "squadrant crew _hook");
10477
10757
  const merged = mergeCrewPermissions(withHooks);
10478
- writeFileSync9(file, JSON.stringify(merged, null, 2));
10758
+ writeFileSync10(file, JSON.stringify(merged, null, 2));
10479
10759
  return file;
10480
10760
  }
10481
- var DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH = join19(homedir14(), ".config", "opencode", "opencode.json");
10761
+ var DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH = join20(homedir15(), ".config", "opencode", "opencode.json");
10482
10762
  function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH) {
10483
- mkdirSync7(dirname5(configPath), { recursive: true });
10763
+ mkdirSync8(dirname5(configPath), { recursive: true });
10484
10764
  const defaultConfig = {
10485
10765
  $schema: "https://opencode.ai/config.json",
10486
10766
  model: "anthropic/claude-sonnet-4-5"
10487
10767
  };
10488
10768
  try {
10489
- writeFileSync9(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", { flag: "wx" });
10769
+ writeFileSync10(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", { flag: "wx" });
10490
10770
  return configPath;
10491
10771
  } catch (err) {
10492
10772
  if (err.code === "EEXIST") return null;
@@ -10494,9 +10774,9 @@ function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_
10494
10774
  }
10495
10775
  }
10496
10776
  function writePerCrewOpencodeConfig(o) {
10497
- const dir = join19(o.stateRoot, o.project, o.taskId);
10498
- mkdirSync7(dir, { recursive: true });
10499
- const file = join19(dir, "opencode.json");
10777
+ const dir = join20(o.stateRoot, o.project, o.taskId);
10778
+ mkdirSync8(dir, { recursive: true });
10779
+ const file = join20(dir, "opencode.json");
10500
10780
  const config = {
10501
10781
  permission: {
10502
10782
  read: "allow",
@@ -10511,7 +10791,7 @@ function writePerCrewOpencodeConfig(o) {
10511
10791
  external_directory: { "**": "allow" }
10512
10792
  }
10513
10793
  };
10514
- writeFileSync9(file, JSON.stringify(config, null, 2));
10794
+ writeFileSync10(file, JSON.stringify(config, null, 2));
10515
10795
  return file;
10516
10796
  }
10517
10797
 
@@ -10542,11 +10822,11 @@ function stepHeader(n, total, label) {
10542
10822
  ${n}/${total} ${label}`));
10543
10823
  }
10544
10824
  function promptLine(question) {
10545
- return new Promise((resolve3) => {
10825
+ return new Promise((resolve4) => {
10546
10826
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10547
10827
  rl.question(question, (answer) => {
10548
10828
  rl.close();
10549
- resolve3(answer.trim());
10829
+ resolve4(answer.trim());
10550
10830
  });
10551
10831
  });
10552
10832
  }
@@ -10874,48 +11154,22 @@ var projectsCommand = new Command3("projects").description("Manage registered pr
10874
11154
 
10875
11155
  // packages/cli/src/commands/status.ts
10876
11156
  init_dist();
10877
- init_dist3();
10878
11157
  import { Command as Command4 } from "commander";
10879
11158
  import chalk6 from "chalk";
10880
- import matter2 from "gray-matter";
10881
- function timeAgo(dateStr) {
10882
- if (!dateStr) return chalk6.dim("\u2014");
10883
- const date = new Date(dateStr);
10884
- if (isNaN(date.getTime())) return chalk6.dim("\u2014");
10885
- const diff = Date.now() - date.getTime();
10886
- const mins = Math.floor(diff / 6e4);
10887
- const hours = Math.floor(diff / 36e5);
10888
- const days = Math.floor(diff / 864e5);
10889
- if (mins < 1) return "just now";
10890
- if (mins < 60) return `${mins}m ago`;
10891
- if (hours < 24) return `${hours}h ago`;
10892
- return `${days}d ago`;
10893
- }
10894
- function progressBar(completed, total) {
10895
- if (total === 0) return chalk6.dim("no tasks");
10896
- const pct = Math.round(completed / total * 100);
10897
- const filled = Math.round(pct / 10);
10898
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
10899
- return `${bar} ${pct}%`;
10900
- }
10901
11159
  function captainIndicator(state) {
10902
11160
  if (state === "alive" || state === "stale") return chalk6.green("\u25CF");
10903
11161
  if (state === "stopped") return chalk6.magenta("\u23FB");
10904
11162
  if (state === void 0 || state === "unknown") return chalk6.dim("?");
10905
11163
  return chalk6.dim("\u25CB");
10906
11164
  }
10907
- function formatProjectRow(name, captainName, fm, statusMdState, captainState) {
11165
+ function formatProjectRow(name, captainName, captainState) {
10908
11166
  const sessionIndicator = captainIndicator(captainState);
10909
11167
  const captainDisplay = `${captainName.padEnd(11)} ${sessionIndicator}`;
10910
- const crew = statusMdState === "ok" ? String(fm.active_crew ?? 0).padEnd(6) : chalk6.dim("?").padEnd(6);
10911
- const progress = statusMdState === "ok" ? progressBar(fm.tasks_completed ?? 0, fm.tasks_total ?? 0).padEnd(25) : statusMdState === "unreadable" ? chalk6.red("status.md unreadable").padEnd(25) : chalk6.dim("no notes").padEnd(25);
10912
- const updated = statusMdState === "ok" ? timeAgo(fm.last_updated) : chalk6.dim("\u2014");
10913
- return ` ${name.padEnd(18)} ${captainDisplay} ${crew} ${progress} ${updated}`;
11168
+ return ` ${name.padEnd(18)} ${captainDisplay}`;
10914
11169
  }
10915
- var statusCommand = new Command4("status").description("Show status of all projects from spoke vault status files").option("--detailed", "also show live per-component service health from the daemon (#77)").action(async (opts) => {
11170
+ var statusCommand = new Command4("status").description("Show captain liveness for all projects (task/crew counts have no data source \u2014 #630)").option("--detailed", "also show live per-component service health from the daemon (#77)").action(async (opts) => {
10916
11171
  const config = loadConfig();
10917
11172
  const projects = Object.entries(config.projects);
10918
- const registry = new WorkspaceRegistry({ obsidian: createObsidianDriver });
10919
11173
  if (projects.length === 0) {
10920
11174
  console.log(chalk6.yellow("\nNo projects registered. Use: squadrant projects add <name> <path>\n"));
10921
11175
  return;
@@ -10928,30 +11182,12 @@ var statusCommand = new Command4("status").description("Show status of all proje
10928
11182
  }
10929
11183
  }
10930
11184
  console.log(chalk6.bold("\nProject Status\n"));
10931
- console.log(
10932
- chalk6.dim(
10933
- ` ${"PROJECT".padEnd(18)} ${"CAPTAIN".padEnd(12)} ${"CREW".padEnd(6)} ${"PROGRESS".padEnd(25)} LAST UPDATE`
10934
- )
10935
- );
10936
- console.log(chalk6.dim(" " + "\u2500".repeat(85)));
11185
+ console.log(chalk6.dim(` ${"PROJECT".padEnd(18)} CAPTAIN`));
11186
+ console.log(chalk6.dim(" " + "\u2500".repeat(35)));
10937
11187
  for (const [name, project] of projects) {
10938
- const workspace = registry.forProject(name, config);
10939
- let fm = {};
10940
- let statusMdState = "missing";
10941
- if (await workspace.exists("status.md")) {
10942
- try {
10943
- const raw = await workspace.read("status.md");
10944
- fm = matter2(raw).data;
10945
- statusMdState = "ok";
10946
- } catch {
10947
- statusMdState = "unreadable";
10948
- }
10949
- }
10950
- console.log(
10951
- formatProjectRow(name, project.captainName, fm, statusMdState, captainStateByProject.get(name))
10952
- );
11188
+ console.log(formatProjectRow(name, project.captainName, captainStateByProject.get(name)));
10953
11189
  }
10954
- console.log("");
11190
+ console.log(chalk6.dim("\n Task/crew counts: no data source yet \u2014 see squadrant/squadrant#630\n"));
10955
11191
  if (opts.detailed) {
10956
11192
  printServiceHealth(health);
10957
11193
  }
@@ -11040,9 +11276,9 @@ import { Command as Command8 } from "commander";
11040
11276
  import { createConnection as createConnection3 } from "net";
11041
11277
  import { randomUUID as randomUUID4 } from "crypto";
11042
11278
  import { execFileSync as execFileSync6 } from "child_process";
11043
- import { homedir as homedir16 } from "os";
11044
- import { join as join21 } from "path";
11045
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
11279
+ import { homedir as homedir17 } from "os";
11280
+ import { join as join22 } from "path";
11281
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
11046
11282
 
11047
11283
  // packages/cli/src/commands/crew-output.ts
11048
11284
  function tailLines(text, maxLines = 40, maxBytes = 4096) {
@@ -11100,11 +11336,11 @@ init_dist2();
11100
11336
  import { Command as Command6 } from "commander";
11101
11337
  import chalk8 from "chalk";
11102
11338
  import { createConnection as createConnection2 } from "net";
11103
- import { homedir as homedir15 } from "os";
11104
- import { join as join20 } from "path";
11339
+ import { homedir as homedir16 } from "os";
11340
+ import { join as join21 } from "path";
11105
11341
  import { createInterface } from "readline";
11106
11342
  function socketPath() {
11107
- return process.env.SQUADRANTD_SOCK ?? join20(homedir15(), ".config", "squadrant", "squadrant.sock");
11343
+ return process.env.SQUADRANTD_SOCK ?? join21(homedir16(), ".config", "squadrant", "squadrant.sock");
11108
11344
  }
11109
11345
  function rule(width, ch = "\u2500") {
11110
11346
  return ch.repeat(Math.max(0, width));
@@ -11340,11 +11576,11 @@ var crewChatCommand = new Command7("chat").description("[DEPRECATED] alias for `
11340
11576
  });
11341
11577
 
11342
11578
  // packages/cli/src/commands/crew-control.ts
11343
- var SOCK2 = join21(homedir16(), ".config", "squadrant", "squadrant.sock");
11579
+ var SOCK2 = join22(homedir17(), ".config", "squadrant", "squadrant.sock");
11344
11580
  var CODEX_FIRST_TURN_DELAY_MS = 1500;
11345
11581
  async function sendCodexFirstTurn(taskId, text) {
11346
11582
  await new Promise((r) => setTimeout(r, CODEX_FIRST_TURN_DELAY_MS));
11347
- await new Promise((resolve3, reject) => {
11583
+ await new Promise((resolve4, reject) => {
11348
11584
  const conn = createConnection3(SOCK2);
11349
11585
  conn.setEncoding("utf-8");
11350
11586
  conn.on("data", () => {
@@ -11360,7 +11596,7 @@ async function sendCodexFirstTurn(taskId, text) {
11360
11596
  }
11361
11597
  setTimeout(() => {
11362
11598
  conn.end();
11363
- resolve3();
11599
+ resolve4();
11364
11600
  }, 100);
11365
11601
  });
11366
11602
  });
@@ -11447,10 +11683,10 @@ function buildSignalRequest(signal, o) {
11447
11683
  return { kind: "event", project, event };
11448
11684
  }
11449
11685
  function defaultWriteResult(id, payload) {
11450
- const dir = join21(homedir16(), ".config", "squadrant", "state", "_results");
11451
- mkdirSync8(dir, { recursive: true });
11452
- const file = join21(dir, `${id}.txt`);
11453
- writeFileSync10(file, payload);
11686
+ const dir = join22(homedir17(), ".config", "squadrant", "state", "_results");
11687
+ mkdirSync9(dir, { recursive: true });
11688
+ const file = join22(dir, `${id}.txt`);
11689
+ writeFileSync11(file, payload);
11454
11690
  return file;
11455
11691
  }
11456
11692
  async function runCrewSignal(signal, o, deps) {
@@ -11484,6 +11720,16 @@ function defaultCreatePr(cwd, o) {
11484
11720
  { cwd, encoding: "utf-8" }
11485
11721
  ).trim();
11486
11722
  }
11723
+ function defaultGetCommitSubject(cwd) {
11724
+ try {
11725
+ return execFileSync6("git", ["-C", cwd, "log", "-1", "--format=%s"], {
11726
+ encoding: "utf-8",
11727
+ stdio: ["ignore", "pipe", "pipe"]
11728
+ }).trim();
11729
+ } catch {
11730
+ return "";
11731
+ }
11732
+ }
11487
11733
  async function runCrewApprove(project, crew, deps) {
11488
11734
  const config = loadConfig();
11489
11735
  const proj = config.projects[project];
@@ -11499,8 +11745,12 @@ async function runCrewApprove(project, crew, deps) {
11499
11745
  const cwd = task.cwd ?? proj.path;
11500
11746
  const base = resolveWorktreeBase(proj.path);
11501
11747
  const branch = crewBranch(crew);
11502
- const title = (task.task ?? branch).split(/\r?\n/)[0].trim().slice(0, 100);
11503
11748
  const body = (task.reviewNote ?? task.task ?? "").trim();
11749
+ const getCommitSubject = deps.getCommitSubject ?? defaultGetCommitSubject;
11750
+ const commitSubject = getCommitSubject(cwd).trim();
11751
+ const reviewNoteFirstLine = (task.reviewNote ?? "").split(/\r?\n/)[0].trim();
11752
+ const taskFirstLine = (task.task ?? branch).split(/\r?\n/)[0].trim();
11753
+ const title = (commitSubject || reviewNoteFirstLine || taskFirstLine).slice(0, 100);
11504
11754
  const pushBranch = deps.pushBranch ?? defaultPushBranch;
11505
11755
  const createPr = deps.createPr ?? defaultCreatePr;
11506
11756
  pushBranch(cwd, branch);
@@ -11838,11 +12088,11 @@ function parseCrewPick(raw, stats) {
11838
12088
  throw new Error(`Invalid selection '${raw}'. Enter a number 1-${stats.length} or a crew name.`);
11839
12089
  }
11840
12090
  function promptLine2(question) {
11841
- return new Promise((resolve3) => {
12091
+ return new Promise((resolve4) => {
11842
12092
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
11843
12093
  rl.question(question, (answer) => {
11844
12094
  rl.close();
11845
- resolve3(answer);
12095
+ resolve4(answer);
11846
12096
  });
11847
12097
  });
11848
12098
  }
@@ -12116,8 +12366,8 @@ init_dist();
12116
12366
  init_dist3();
12117
12367
  import { Command as Command12 } from "commander";
12118
12368
  import { execSync as execSync10 } from "child_process";
12119
- import { homedir as homedir18 } from "os";
12120
- import { join as join23 } from "path";
12369
+ import { homedir as homedir19 } from "os";
12370
+ import { join as join24 } from "path";
12121
12371
  import chalk13 from "chalk";
12122
12372
 
12123
12373
  // packages/web/dist/read-status.js
@@ -12330,9 +12580,9 @@ function mergeSnapshot(daemon, external, now) {
12330
12580
  // packages/web/dist/probes.js
12331
12581
  init_dist();
12332
12582
  init_dist();
12333
- import { join as join22 } from "path";
12334
- import { homedir as homedir17 } from "os";
12335
- import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
12583
+ import { join as join23 } from "path";
12584
+ import { homedir as homedir18 } from "os";
12585
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
12336
12586
  import { execFile as execFile4 } from "child_process";
12337
12587
  var DEFAULT_TIMEOUT_MS = 2e3;
12338
12588
  var AGENT_CLIS = ["claude", "codex", "gemini", "opencode"];
@@ -12368,7 +12618,7 @@ function vaultProbe(run, dir) {
12368
12618
  return { state: "unknown", detail: "no vault configured" };
12369
12619
  if (!run.pathExists(dir))
12370
12620
  return { state: "gone", detail: "vault directory missing" };
12371
- if (!run.pathExists(join22(dir, ".obsidian")))
12621
+ if (!run.pathExists(join23(dir, ".obsidian")))
12372
12622
  return { state: "gone", detail: "no .obsidian/ (not a vault)" };
12373
12623
  return { state: "alive" };
12374
12624
  } catch {
@@ -12436,27 +12686,27 @@ async function runExternalProbes(run, timeoutMs = DEFAULT_TIMEOUT_MS) {
12436
12686
  const sessions = probeSessions(run);
12437
12687
  return { cmux: cmux2, agentClis, vaults, config: { parseable, projectPaths, sessions } };
12438
12688
  }
12439
- var SESSIONS_PATH = join22(homedir17(), ".config", "squadrant", "sessions.json");
12689
+ var SESSIONS_PATH = join23(homedir18(), ".config", "squadrant", "sessions.json");
12440
12690
  function onPath(cli) {
12441
12691
  const dirs = (process.env.PATH ?? "").split(":").filter(Boolean);
12442
- return dirs.some((d) => existsSync11(join22(d, cli)));
12692
+ return dirs.some((d) => existsSync12(join23(d, cli)));
12443
12693
  }
12444
12694
  function readSessionsHashes() {
12445
- const raw = JSON.parse(readFileSync14(SESSIONS_PATH, "utf-8"));
12695
+ const raw = JSON.parse(readFileSync15(SESSIONS_PATH, "utf-8"));
12446
12696
  const hashes = Object.values(raw.workspaces ?? {}).map((w) => w.templateHash).filter((h) => typeof h === "string" && h.length > 0);
12447
12697
  return [...new Set(hashes)];
12448
12698
  }
12449
12699
  function defaultProbeRunners() {
12450
12700
  return {
12451
- probeCmuxBin: () => new Promise((resolve3) => {
12701
+ probeCmuxBin: () => new Promise((resolve4) => {
12452
12702
  try {
12453
- execFile4(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) => resolve3(!err));
12703
+ execFile4(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) => resolve4(!err));
12454
12704
  } catch {
12455
- resolve3(false);
12705
+ resolve4(false);
12456
12706
  }
12457
12707
  }),
12458
12708
  probeOnPath: async (cli) => onPath(cli),
12459
- pathExists: (p) => existsSync11(p),
12709
+ pathExists: (p) => existsSync12(p),
12460
12710
  loadConfig: () => loadConfig(),
12461
12711
  loadSessionsHashes: () => readSessionsHashes()
12462
12712
  };
@@ -13317,9 +13567,9 @@ async function startWebServer(opts) {
13317
13567
  }
13318
13568
  res.writeHead(404).end();
13319
13569
  });
13320
- await new Promise((resolve3, reject) => {
13570
+ await new Promise((resolve4, reject) => {
13321
13571
  server.once("error", reject);
13322
- server.listen(opts.port, host, resolve3);
13572
+ server.listen(opts.port, host, resolve4);
13323
13573
  });
13324
13574
  const addr = server.address();
13325
13575
  const boundPort = typeof addr === "object" && addr ? addr.port : opts.port;
@@ -13331,7 +13581,7 @@ async function startWebServer(opts) {
13331
13581
  timer.unref?.();
13332
13582
  return {
13333
13583
  port: boundPort,
13334
- close: () => new Promise((resolve3) => {
13584
+ close: () => new Promise((resolve4) => {
13335
13585
  clearInterval(timer);
13336
13586
  for (const res of clients) {
13337
13587
  try {
@@ -13340,14 +13590,14 @@ async function startWebServer(opts) {
13340
13590
  }
13341
13591
  }
13342
13592
  clients.clear();
13343
- server.close(() => resolve3());
13593
+ server.close(() => resolve4());
13344
13594
  })
13345
13595
  };
13346
13596
  }
13347
13597
 
13348
13598
  // packages/cli/src/commands/dashboard.ts
13349
13599
  init_dist();
13350
- var SOCK3 = join23(homedir18(), ".config", "squadrant", "squadrant.sock");
13600
+ var SOCK3 = join24(homedir19(), ".config", "squadrant", "squadrant.sock");
13351
13601
  function detectCurrentWorkspace2() {
13352
13602
  const out = execSync10(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
13353
13603
  const match = out.match(/workspace:\d+/);
@@ -13866,29 +14116,16 @@ init_dist();
13866
14116
  init_dist();
13867
14117
  init_dist3();
13868
14118
  import { Command as Command16 } from "commander";
13869
- import fs24 from "fs";
13870
- import path26 from "path";
13871
14119
  import chalk17 from "chalk";
13872
- import matter3 from "gray-matter";
13873
14120
  function getDateStr(yesterday) {
13874
14121
  return iso(daysAgo(yesterday ? 1 : 0));
13875
14122
  }
13876
14123
  async function getProjectStandup(name, project, dateStr, registry, config) {
13877
14124
  const workspace = registry.forProject(name, config);
13878
- const spokeVault = resolveHome(project.spokeVault);
13879
- const statusFile = path26.join(spokeVault, "status.md");
13880
- let status = {};
13881
- if (fs24.existsSync(statusFile)) {
13882
- try {
13883
- status = matter3(fs24.readFileSync(statusFile, "utf-8")).data;
13884
- } catch {
13885
- }
13886
- }
13887
14125
  const log = await readDailyLog(workspace, dateStr);
13888
14126
  const gitCommits = getGitCommits(project.path, dateStr);
13889
14127
  return {
13890
14128
  name,
13891
- status,
13892
14129
  dailyLog: log?.content ?? null,
13893
14130
  gitCommits,
13894
14131
  blockers: log?.blockers ?? []
@@ -13907,26 +14144,16 @@ ${header}
13907
14144
  }
13908
14145
  let hasBlockers = false;
13909
14146
  for (const s of standups) {
13910
- const tasksDone = s.status.tasks_completed ?? 0;
13911
- const tasksTotal = s.status.tasks_total ?? 0;
13912
- const tasksInProgress = s.status.tasks_in_progress ?? 0;
13913
14147
  if (!raw) {
13914
14148
  lines.push(chalk17.cyan.bold(`## ${s.name}`));
13915
14149
  } else {
13916
14150
  lines.push(`## ${s.name}`);
13917
14151
  }
13918
- if (s.gitCommits.length > 0 || tasksDone > 0) {
14152
+ if (s.gitCommits.length > 0) {
13919
14153
  lines.push(!raw ? chalk17.green("Done:") : "**Done:**");
13920
14154
  for (const commit of s.gitCommits) {
13921
14155
  lines.push(` - ${commit}`);
13922
14156
  }
13923
- if (tasksDone > 0 && s.gitCommits.length === 0) {
13924
- lines.push(` - ${tasksDone}/${tasksTotal} tasks completed`);
13925
- }
13926
- }
13927
- if (tasksInProgress > 0) {
13928
- lines.push(!raw ? chalk17.yellow("In Progress:") : "**In Progress:**");
13929
- lines.push(` - ${tasksInProgress} task(s) active`);
13930
14157
  }
13931
14158
  if (s.dailyLog) {
13932
14159
  const sections = ["Completed", "In Progress", "Tomorrow"];
@@ -13948,19 +14175,18 @@ ${header}
13948
14175
  lines.push(` - ${b}`);
13949
14176
  }
13950
14177
  }
13951
- if (s.gitCommits.length === 0 && tasksDone === 0 && !s.dailyLog) {
14178
+ if (s.gitCommits.length === 0 && !s.dailyLog) {
13952
14179
  lines.push(!raw ? chalk17.dim(" (no activity)") : " (no activity)");
13953
14180
  }
13954
14181
  lines.push("");
13955
14182
  }
13956
14183
  const totalCommits = standups.reduce((sum, s) => sum + s.gitCommits.length, 0);
13957
- const totalDone = standups.reduce((sum, s) => sum + (s.status.tasks_completed ?? 0), 0);
13958
14184
  if (!raw) {
13959
- lines.push(chalk17.dim(`--- ${totalCommits} commits, ${totalDone} tasks done${hasBlockers ? ", HAS BLOCKERS" : ""} ---
14185
+ lines.push(chalk17.dim(`--- ${totalCommits} commits${hasBlockers ? ", HAS BLOCKERS" : ""} (task tracking: no data source \u2014 #630) ---
13960
14186
  `));
13961
14187
  } else {
13962
14188
  lines.push(`---
13963
- *${totalCommits} commits, ${totalDone} tasks done${hasBlockers ? ", HAS BLOCKERS" : ""}*
14189
+ *${totalCommits} commits${hasBlockers ? ", HAS BLOCKERS" : ""} (task tracking: no data source \u2014 #630)*
13964
14190
  `);
13965
14191
  }
13966
14192
  return lines.join("\n");
@@ -13998,19 +14224,7 @@ init_dist();
13998
14224
  init_dist();
13999
14225
  init_dist3();
14000
14226
  import { Command as Command17 } from "commander";
14001
- import fs25 from "fs";
14002
- import path27 from "path";
14003
14227
  import chalk18 from "chalk";
14004
- import matter4 from "gray-matter";
14005
- function readStatus(spokeVault) {
14006
- const statusFile = path27.join(spokeVault, "status.md");
14007
- if (!fs25.existsSync(statusFile)) return {};
14008
- try {
14009
- return matter4(fs25.readFileSync(statusFile, "utf-8")).data;
14010
- } catch {
14011
- return {};
14012
- }
14013
- }
14014
14228
  function dedupe(items) {
14015
14229
  const seen = /* @__PURE__ */ new Set();
14016
14230
  const out = [];
@@ -14025,8 +14239,6 @@ function dedupe(items) {
14025
14239
  }
14026
14240
  async function getProjectRetro(name, project, fromStr, toStr, registry, config) {
14027
14241
  const workspace = registry.forProject(name, config);
14028
- const spokeVault = resolveHome(project.spokeVault);
14029
- const status = readStatus(spokeVault);
14030
14242
  const shipped = [];
14031
14243
  const inProgress = [];
14032
14244
  const blocked = [];
@@ -14056,9 +14268,7 @@ async function getProjectRetro(name, project, fromStr, toStr, registry, config)
14056
14268
  blocked: dedupe(blocked),
14057
14269
  decisions: dedupe(decisions),
14058
14270
  commits,
14059
- mergedPRs,
14060
- tasksCompletedNow: status.tasks_completed ?? 0,
14061
- tasksInProgressNow: status.tasks_in_progress ?? 0
14271
+ mergedPRs
14062
14272
  };
14063
14273
  }
14064
14274
  function renderList(lines, items, raw, label, color) {
@@ -14214,9 +14424,9 @@ async function runRuntimeSend(arg1, arg2, opts, confirmOpts) {
14214
14424
  const resolved = resolveTarget(registry, config, target, !!opts.command);
14215
14425
  await needRef(resolved);
14216
14426
  const finalProject = opts.command ? config.commandName : target;
14217
- const { join: join30, dirname: dirname10 } = await import("path");
14427
+ const { join: join31, dirname: dirname10 } = await import("path");
14218
14428
  const { DEFAULT_CONFIG_PATH: DEFAULT_CONFIG_PATH2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
14219
- const stateRoot = join30(dirname10(DEFAULT_CONFIG_PATH2), "state");
14429
+ const stateRoot = join31(dirname10(DEFAULT_CONFIG_PATH2), "state");
14220
14430
  const seq = await appendCaptainMessage2({
14221
14431
  stateRoot,
14222
14432
  project: finalProject,
@@ -14329,9 +14539,9 @@ workspaceCommand.command("read").description("Print the contents of a scope-rela
14329
14539
  const config = loadConfig();
14330
14540
  const registry = buildRegistry2();
14331
14541
  try {
14332
- const { projectTarget, path: path30 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14542
+ const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14333
14543
  const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
14334
- const content = await driver.read(path30);
14544
+ const content = await driver.read(path34);
14335
14545
  process.stdout.write(content);
14336
14546
  } catch (err) {
14337
14547
  console.error(chalk20.red(err.message));
@@ -14343,26 +14553,26 @@ workspaceCommand.command("write").description("Write content to a scope-relative
14343
14553
  const registry = buildRegistry2();
14344
14554
  try {
14345
14555
  let projectTarget;
14346
- let path30;
14556
+ let path34;
14347
14557
  let rawContent;
14348
14558
  if (opts.hub) {
14349
14559
  if (arg3 !== void 0) {
14350
14560
  throw new Error("With --hub, pass only the path and content");
14351
14561
  }
14352
14562
  projectTarget = void 0;
14353
- path30 = arg1;
14563
+ path34 = arg1;
14354
14564
  rawContent = arg2;
14355
14565
  } else {
14356
14566
  if (arg3 === void 0) {
14357
14567
  throw new Error("Missing content \u2014 usage: <project> <path> <content>");
14358
14568
  }
14359
14569
  projectTarget = arg1;
14360
- path30 = arg2;
14570
+ path34 = arg2;
14361
14571
  rawContent = arg3;
14362
14572
  }
14363
14573
  const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
14364
14574
  const payload = rawContent === "-" ? await readStdin() : rawContent;
14365
- await driver.write(path30, payload);
14575
+ await driver.write(path34, payload);
14366
14576
  } catch (err) {
14367
14577
  console.error(chalk20.red(err.message));
14368
14578
  process.exit(1);
@@ -14372,9 +14582,9 @@ workspaceCommand.command("list").description("List entries in a scope-relative d
14372
14582
  const config = loadConfig();
14373
14583
  const registry = buildRegistry2();
14374
14584
  try {
14375
- const { projectTarget, path: path30 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14585
+ const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14376
14586
  const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
14377
- const entries = await driver.list(path30);
14587
+ const entries = await driver.list(path34);
14378
14588
  for (const entry of entries) console.log(entry);
14379
14589
  } catch (err) {
14380
14590
  console.error(chalk20.red(err.message));
@@ -14385,9 +14595,9 @@ workspaceCommand.command("exists").description("Exit 0 if path exists, 1 if not"
14385
14595
  const config = loadConfig();
14386
14596
  const registry = buildRegistry2();
14387
14597
  try {
14388
- const { projectTarget, path: path30 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14598
+ const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14389
14599
  const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
14390
- const ok2 = await driver.exists(path30);
14600
+ const ok2 = await driver.exists(path34);
14391
14601
  process.exit(ok2 ? 0 : 1);
14392
14602
  } catch (err) {
14393
14603
  console.error(chalk20.red(err.message));
@@ -14398,9 +14608,9 @@ workspaceCommand.command("mkdir").description("Recursively create a scope-relati
14398
14608
  const config = loadConfig();
14399
14609
  const registry = buildRegistry2();
14400
14610
  try {
14401
- const { projectTarget, path: path30 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14611
+ const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14402
14612
  const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
14403
- await driver.mkdir(path30);
14613
+ await driver.mkdir(path34);
14404
14614
  } catch (err) {
14405
14615
  console.error(chalk20.red(err.message));
14406
14616
  process.exit(1);
@@ -14439,8 +14649,8 @@ init_dist3();
14439
14649
  init_dist();
14440
14650
  import { Command as Command21 } from "commander";
14441
14651
  import chalk22 from "chalk";
14442
- import fs26 from "fs";
14443
- import path28 from "path";
14652
+ import fs24 from "fs";
14653
+ import path26 from "path";
14444
14654
  import { fileURLToPath as fileURLToPath4 } from "url";
14445
14655
  function parseScope(v) {
14446
14656
  if (v !== "user" && v !== "project") {
@@ -14449,10 +14659,10 @@ function parseScope(v) {
14449
14659
  return v;
14450
14660
  }
14451
14661
  function findPackageRoot3() {
14452
- let dir = path28.dirname(fileURLToPath4(import.meta.url));
14662
+ let dir = path26.dirname(fileURLToPath4(import.meta.url));
14453
14663
  while (dir !== "/" && dir !== "") {
14454
- if (fs26.existsSync(path28.join(dir, "package.json"))) return dir;
14455
- dir = path28.dirname(dir);
14664
+ if (fs24.existsSync(path26.join(dir, "package.json"))) return dir;
14665
+ dir = path26.dirname(dir);
14456
14666
  }
14457
14667
  return process.cwd();
14458
14668
  }
@@ -14576,7 +14786,7 @@ projectionCommand.command("list").description("List registered projection target
14576
14786
  // packages/cli/src/commands/codex-chat-smoke.ts
14577
14787
  init_dist4();
14578
14788
  import { Command as Command22 } from "commander";
14579
- import { resolve as resolve2 } from "path";
14789
+ import { resolve as resolve3 } from "path";
14580
14790
  var codexChatSmokeCommand = new Command22("codex-chat-smoke").description("Phase 1 gate: prove the codex app-server JSON-RPC path works end-to-end.").option("--cwd <dir>", "working dir for the codex thread", process.cwd()).option("--model <m>", "model id (optional)").option(
14581
14791
  "--approval",
14582
14792
  "include the approval round-trip (Phase 1 PASS requires this)",
@@ -14593,7 +14803,7 @@ var codexChatSmokeCommand = new Command22("codex-chat-smoke").description("Phase
14593
14803
  c.start();
14594
14804
  await c.initialize();
14595
14805
  const { threadId } = await c.startThread({
14596
- cwd: resolve2(opts.cwd),
14806
+ cwd: resolve3(opts.cwd),
14597
14807
  model: opts.model,
14598
14808
  sandbox: "workspace-write",
14599
14809
  // With --approval, force untrusted policy so codex requests approval
@@ -14614,7 +14824,7 @@ var codexChatSmokeCommand = new Command22("codex-chat-smoke").description("Phase
14614
14824
  pendingApprovals.push({ id: r.id, method: r.method });
14615
14825
  c.respondToServerRequest(r.id, { decision: "approve" });
14616
14826
  });
14617
- await c.sendTurn(threadId, `Write the text "approval-ok" to a file at ${resolve2(opts.cwd)}/.squadrant-smoke.txt`);
14827
+ await c.sendTurn(threadId, `Write the text "approval-ok" to a file at ${resolve3(opts.cwd)}/.squadrant-smoke.txt`);
14618
14828
  if (pendingApprovals.length === 0) {
14619
14829
  throw new Error("approval gate: expected at least one server-request (approval/input) during the turn");
14620
14830
  }
@@ -14647,12 +14857,12 @@ init_dist();
14647
14857
  init_dist();
14648
14858
  init_dist2();
14649
14859
  import { Command as Command23 } from "commander";
14650
- import fs27 from "fs";
14860
+ import fs25 from "fs";
14651
14861
  import { fileURLToPath as fileURLToPath5 } from "url";
14652
- import { dirname as dirname6, join as join25 } from "path";
14862
+ import { dirname as dirname6, join as join26 } from "path";
14653
14863
  import chalk23 from "chalk";
14654
14864
  function runConfigCheck(opts) {
14655
- const raw = JSON.parse(fs27.readFileSync(opts.configPath, "utf-8"));
14865
+ const raw = JSON.parse(fs25.readFileSync(opts.configPath, "utf-8"));
14656
14866
  const def = getDefaultConfig();
14657
14867
  const items = detectDrift(raw, def);
14658
14868
  let working = raw;
@@ -14669,7 +14879,7 @@ function runConfigCheck(opts) {
14669
14879
  stamped = true;
14670
14880
  }
14671
14881
  if (opts.fix || opts.accept || stamped) {
14672
- fs27.writeFileSync(opts.configPath, JSON.stringify(working, null, 2) + "\n");
14882
+ fs25.writeFileSync(opts.configPath, JSON.stringify(working, null, 2) + "\n");
14673
14883
  }
14674
14884
  return { items, applied, remaining, stamped };
14675
14885
  }
@@ -14740,7 +14950,7 @@ function printItems(items) {
14740
14950
  var configCommand = new Command23("config").description("Inspect and reconcile squadrant config");
14741
14951
  configCommand.command("check").description("Detect config drift vs the current default schema").option("--fix", "Apply the safe tier (add missing, remove deprecated)", false).option("--accept", "Stamp the current version without changing config (dismiss advisories)", false).option("--json", "Output drift items as JSON", false).action((opts) => {
14742
14952
  const pkgVersion = readPkgVersion2();
14743
- if (!fs27.existsSync(DEFAULT_CONFIG_PATH)) {
14953
+ if (!fs25.existsSync(DEFAULT_CONFIG_PATH)) {
14744
14954
  console.log(chalk23.yellow("No config found \u2014 run `squadrant init` first."));
14745
14955
  return;
14746
14956
  }
@@ -14785,8 +14995,8 @@ configCommand.command("set").description("Write a config value by dotted key (e.
14785
14995
  }
14786
14996
  });
14787
14997
  function readPkgVersion2() {
14788
- const pkgPath = join25(dirname6(fileURLToPath5(import.meta.url)), "..", "package.json");
14789
- return JSON.parse(fs27.readFileSync(pkgPath, "utf-8")).version;
14998
+ const pkgPath = join26(dirname6(fileURLToPath5(import.meta.url)), "..", "package.json");
14999
+ return JSON.parse(fs25.readFileSync(pkgPath, "utf-8")).version;
14790
15000
  }
14791
15001
 
14792
15002
  // packages/cli/src/commands/heal.ts
@@ -14871,9 +15081,9 @@ var healCommand = new Command24("heal").description("Targeted, idempotent remedi
14871
15081
  process.exit(code);
14872
15082
  })
14873
15083
  ).addCommand(
14874
- new Command24("daemon").description("Restart squadrantd via the idempotent launchd kickstart path").action(async () => {
15084
+ new Command24("daemon").description("Explicitly reconcile + restart squadrantd (#636 operator opt-in \u2014 reads current PATH/entry drift and applies it, regardless of role)").action(async () => {
14875
15085
  const code = await runHealDaemon({
14876
- ensureDaemon: () => restartDaemonIfRunning({ reason: "heal", isRunning: () => true }),
15086
+ ensureDaemon: () => reregisterDaemon(),
14877
15087
  stdout: process.stdout,
14878
15088
  stderr: process.stderr
14879
15089
  });
@@ -14939,7 +15149,7 @@ var groupCommand = new Command26("group").description("Cross-project intra-group
14939
15149
  // packages/cli/src/commands/ping.ts
14940
15150
  init_dist();
14941
15151
  init_dist2();
14942
- import { join as join26, dirname as dirname7 } from "path";
15152
+ import { join as join27, dirname as dirname7 } from "path";
14943
15153
  import { Command as Command27 } from "commander";
14944
15154
  import chalk27 from "chalk";
14945
15155
  init_require_daemon();
@@ -14949,7 +15159,7 @@ async function runPing(project, message) {
14949
15159
  const resolved = resolveTarget(registry, config, project, false);
14950
15160
  await requireDaemon();
14951
15161
  await needRef(resolved);
14952
- const stateRoot = join26(dirname7(DEFAULT_CONFIG_PATH), "state");
15162
+ const stateRoot = join27(dirname7(DEFAULT_CONFIG_PATH), "state");
14953
15163
  await appendCaptainMessage({
14954
15164
  stateRoot,
14955
15165
  project,
@@ -15026,8 +15236,8 @@ var cmuxCommand = new Command28("cmux").description("cmux integration helpers").
15026
15236
  // packages/cli/src/commands/effort.ts
15027
15237
  init_dist();
15028
15238
  init_dist2();
15029
- import fs28 from "fs";
15030
- import path29 from "path";
15239
+ import fs26 from "fs";
15240
+ import path27 from "path";
15031
15241
  import { Command as Command29 } from "commander";
15032
15242
  import chalk29 from "chalk";
15033
15243
  var VALID_EFFORTS = ["max", "balance", "low"];
@@ -15070,9 +15280,9 @@ function effortScopeLabel(projectName) {
15070
15280
  }
15071
15281
  function canonical(p) {
15072
15282
  try {
15073
- return fs28.realpathSync(p);
15283
+ return fs26.realpathSync(p);
15074
15284
  } catch {
15075
- return path29.resolve(p);
15285
+ return path27.resolve(p);
15076
15286
  }
15077
15287
  }
15078
15288
  async function notifyCaptainsOfEffort(effort, config, driver, cwd = process.cwd(), append, scopeProject, projectConfigRoot) {
@@ -15122,7 +15332,7 @@ var effortCommand = new Command29("effort").description("Get or set the crew tok
15122
15332
  const config = loadConfig();
15123
15333
  const registry = new RuntimeRegistry2({ cmux: createCmuxDriver2() });
15124
15334
  const driver = registry.global(config);
15125
- const stateRoot = path29.join(path29.dirname(DEFAULT_CONFIG_PATH), "state");
15335
+ const stateRoot = path27.join(path27.dirname(DEFAULT_CONFIG_PATH), "state");
15126
15336
  const append = (project, text) => appendCaptainMessage({ stateRoot, project, text, source: "daemon" });
15127
15337
  await notifyCaptainsOfEffort(effort, config, driver, process.cwd(), append, options.project);
15128
15338
  } catch {
@@ -15130,18 +15340,308 @@ var effortCommand = new Command29("effort").description("Get or set the crew tok
15130
15340
  }
15131
15341
  });
15132
15342
 
15343
+ // packages/cli/src/commands/tokens.ts
15344
+ init_dist();
15345
+ import fs27 from "fs";
15346
+ import path28 from "path";
15347
+ import os15 from "os";
15348
+ import readline3 from "readline";
15349
+ import { Command as Command30 } from "commander";
15350
+ import chalk30 from "chalk";
15351
+ var CLAUDE_PROJECTS_DIR = path28.join(os15.homedir(), ".claude", "projects");
15352
+ function parseTranscriptLine(rawLine) {
15353
+ const line = rawLine.trim();
15354
+ if (!line) return { timestamp: null, usage: null };
15355
+ let obj;
15356
+ try {
15357
+ obj = JSON.parse(line);
15358
+ } catch {
15359
+ return { timestamp: null, usage: null };
15360
+ }
15361
+ const entry = obj;
15362
+ const timestamp = typeof entry?.timestamp === "string" ? entry.timestamp : null;
15363
+ if (entry?.type !== "assistant" || entry.message?.role !== "assistant") return { timestamp, usage: null };
15364
+ const usage2 = entry.message?.usage;
15365
+ if (!usage2) return { timestamp, usage: null };
15366
+ return {
15367
+ timestamp,
15368
+ usage: {
15369
+ input: usage2.input_tokens ?? 0,
15370
+ output: usage2.output_tokens ?? 0,
15371
+ cacheRead: usage2.cache_read_input_tokens ?? 0,
15372
+ cacheWrite: usage2.cache_creation_input_tokens ?? 0
15373
+ }
15374
+ };
15375
+ }
15376
+ function emptySessionAggregate() {
15377
+ return { calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: [], earliest: null, latest: null };
15378
+ }
15379
+ function extendRange(range, timestamp) {
15380
+ if (!timestamp) return;
15381
+ if (range.earliest === null || timestamp < range.earliest) range.earliest = timestamp;
15382
+ if (range.latest === null || timestamp > range.latest) range.latest = timestamp;
15383
+ }
15384
+ function foldTranscriptLine(agg, rawLine, state) {
15385
+ const { timestamp, usage: usage2 } = parseTranscriptLine(rawLine);
15386
+ extendRange(agg, timestamp);
15387
+ if (!usage2) return;
15388
+ agg.calls++;
15389
+ agg.input += usage2.input;
15390
+ agg.output += usage2.output;
15391
+ agg.cacheRead += usage2.cacheRead;
15392
+ agg.cacheWrite += usage2.cacheWrite;
15393
+ if (usage2.cacheRead !== state.lastCacheRead) {
15394
+ state.lastCacheRead = usage2.cacheRead;
15395
+ agg.turns.push({ total: usage2.input + usage2.cacheWrite + usage2.cacheRead, cacheRead: usage2.cacheRead });
15396
+ }
15397
+ }
15398
+ async function aggregateTranscriptFile(filePath) {
15399
+ const agg = emptySessionAggregate();
15400
+ const state = { lastCacheRead: null };
15401
+ const rl = readline3.createInterface({ input: fs27.createReadStream(filePath), crlfDelay: Infinity });
15402
+ for await (const line of rl) {
15403
+ foldTranscriptLine(agg, line, state);
15404
+ }
15405
+ return agg;
15406
+ }
15407
+ function escapeClaudeProjectPath(cwd) {
15408
+ return cwd.replace(/[^a-zA-Z0-9]/g, "-");
15409
+ }
15410
+ function isCrewDirName(dirName, captainSlug) {
15411
+ return dirName !== captainSlug && dirName.startsWith(`${captainSlug}-`);
15412
+ }
15413
+ function mergeRanges(ranges) {
15414
+ const merged = { earliest: null, latest: null };
15415
+ for (const r of ranges) {
15416
+ extendRange(merged, r.earliest);
15417
+ extendRange(merged, r.latest);
15418
+ }
15419
+ return merged;
15420
+ }
15421
+ function buildRoleReport(role, sessions) {
15422
+ let calls = 0;
15423
+ let input = 0;
15424
+ let output = 0;
15425
+ let cacheRead = 0;
15426
+ let cacheWrite = 0;
15427
+ const boots = [];
15428
+ let bootConfirmedSessions = 0;
15429
+ for (const s of sessions) {
15430
+ calls += s.calls;
15431
+ input += s.input;
15432
+ output += s.output;
15433
+ cacheRead += s.cacheRead;
15434
+ cacheWrite += s.cacheWrite;
15435
+ const [first, second] = s.turns;
15436
+ if (first) {
15437
+ boots.push(first.total);
15438
+ if (second && first.total > 0) {
15439
+ const rel = Math.abs(second.cacheRead - first.total) / first.total;
15440
+ if (rel < 0.02) bootConfirmedSessions++;
15441
+ }
15442
+ }
15443
+ }
15444
+ const meanCacheReadPerCall = calls > 0 ? cacheRead / calls : null;
15445
+ const meanBoot = boots.length > 0 ? boots.reduce((a, b) => a + b, 0) / boots.length : null;
15446
+ const accumulated = meanCacheReadPerCall !== null && meanBoot !== null ? meanCacheReadPerCall - meanBoot : null;
15447
+ const accumulatedPct = accumulated !== null && meanCacheReadPerCall ? accumulated / meanCacheReadPerCall : null;
15448
+ const { earliest, latest } = mergeRanges(sessions);
15449
+ return {
15450
+ role,
15451
+ sessionFiles: sessions.length,
15452
+ calls,
15453
+ input,
15454
+ output,
15455
+ cacheRead,
15456
+ cacheWrite,
15457
+ meanCacheReadPerCall,
15458
+ meanBoot,
15459
+ accumulated,
15460
+ accumulatedPct,
15461
+ bootConfirmedSessions,
15462
+ bootSampledSessions: boots.length,
15463
+ earliest,
15464
+ latest
15465
+ };
15466
+ }
15467
+ async function readdirSafe(dir) {
15468
+ try {
15469
+ return await fs27.promises.readdir(dir);
15470
+ } catch {
15471
+ return [];
15472
+ }
15473
+ }
15474
+ async function listJsonlFiles(dir) {
15475
+ const entries = await readdirSafe(dir);
15476
+ return entries.filter((e) => e.endsWith(".jsonl")).map((e) => path28.join(dir, e));
15477
+ }
15478
+ async function findTranscriptDirs(claudeProjectsDir, captainSlug) {
15479
+ const entries = await readdirSafe(claudeProjectsDir);
15480
+ const captainDirs = [];
15481
+ const crewDirs = [];
15482
+ for (const entry of entries) {
15483
+ if (entry === captainSlug) captainDirs.push(path28.join(claudeProjectsDir, entry));
15484
+ else if (isCrewDirName(entry, captainSlug)) crewDirs.push(path28.join(claudeProjectsDir, entry));
15485
+ }
15486
+ return { captainDirs, crewDirs };
15487
+ }
15488
+ async function aggregateFiles(files) {
15489
+ const sessions = [];
15490
+ for (const file of files) {
15491
+ sessions.push(await aggregateTranscriptFile(file));
15492
+ }
15493
+ return sessions;
15494
+ }
15495
+ async function collectProjectTokenReport(name, projectPath, claudeProjectsDir = CLAUDE_PROJECTS_DIR) {
15496
+ const captainSlug = escapeClaudeProjectPath(projectPath);
15497
+ const { captainDirs, crewDirs } = await findTranscriptDirs(claudeProjectsDir, captainSlug);
15498
+ const captainFiles = (await Promise.all(captainDirs.map(listJsonlFiles))).flat();
15499
+ const crewFiles = (await Promise.all(crewDirs.map(listJsonlFiles))).flat();
15500
+ const [captainSessions, crewSessions] = await Promise.all([
15501
+ aggregateFiles(captainFiles),
15502
+ aggregateFiles(crewFiles)
15503
+ ]);
15504
+ return {
15505
+ project: name,
15506
+ path: projectPath,
15507
+ captain: buildRoleReport("captain", captainSessions),
15508
+ crews: buildRoleReport("crews", crewSessions)
15509
+ };
15510
+ }
15511
+ function formatTokens(n) {
15512
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
15513
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
15514
+ return String(Math.round(n));
15515
+ }
15516
+ function formatPct(n) {
15517
+ return n === null ? "n/a" : `${Math.round(n * 100)}%`;
15518
+ }
15519
+ function printRoleRow(label, r) {
15520
+ const totalVolume = r.input + r.output + r.cacheRead + r.cacheWrite;
15521
+ console.log(
15522
+ ` ${label.padEnd(10)} ${String(r.sessionFiles).padStart(6)} ${String(r.calls).padStart(8)} ${formatTokens(r.input).padStart(8)} ${formatTokens(r.output).padStart(8)} ${formatTokens(r.cacheRead).padStart(10)} ${formatTokens(r.cacheWrite).padStart(10)} ${formatTokens(totalVolume).padStart(10)} ` + chalk30.dim(formatRange(r))
15523
+ );
15524
+ }
15525
+ function printBootLine(label, r) {
15526
+ if (r.meanBoot === null || r.meanCacheReadPerCall === null) {
15527
+ console.log(chalk30.dim(` ${label.padEnd(10)} no sessions with turn data`));
15528
+ return;
15529
+ }
15530
+ const bootPct = r.meanCacheReadPerCall > 0 ? r.meanBoot / r.meanCacheReadPerCall : null;
15531
+ console.log(
15532
+ ` ${label.padEnd(10)} mean ctx/call ${formatTokens(r.meanCacheReadPerCall).padStart(8)} boot ${formatTokens(r.meanBoot).padStart(8)} (${formatPct(bootPct)}) accumulated ${formatTokens(r.accumulated ?? 0).padStart(8)} (${formatPct(r.accumulatedPct)})` + chalk30.dim(` [boot confirmed ${r.bootConfirmedSessions}/${r.bootSampledSessions} sessions]`)
15533
+ );
15534
+ }
15535
+ function sumRoleReports(role, reports) {
15536
+ const calls = reports.reduce((a, r) => a + r.calls, 0);
15537
+ const cacheRead = reports.reduce((a, r) => a + r.cacheRead, 0);
15538
+ const bootWeighted = reports.reduce(
15539
+ (a, r) => a + (r.meanBoot !== null ? r.meanBoot * r.bootSampledSessions : 0),
15540
+ 0
15541
+ );
15542
+ const bootSampledSessions = reports.reduce((a, r) => a + r.bootSampledSessions, 0);
15543
+ const meanBoot = bootSampledSessions > 0 ? bootWeighted / bootSampledSessions : null;
15544
+ const meanCacheReadPerCall = calls > 0 ? cacheRead / calls : null;
15545
+ const accumulated = meanCacheReadPerCall !== null && meanBoot !== null ? meanCacheReadPerCall - meanBoot : null;
15546
+ const accumulatedPct = accumulated !== null && meanCacheReadPerCall ? accumulated / meanCacheReadPerCall : null;
15547
+ const { earliest, latest } = mergeRanges(reports);
15548
+ return {
15549
+ role,
15550
+ sessionFiles: reports.reduce((a, r) => a + r.sessionFiles, 0),
15551
+ calls,
15552
+ input: reports.reduce((a, r) => a + r.input, 0),
15553
+ output: reports.reduce((a, r) => a + r.output, 0),
15554
+ cacheRead,
15555
+ cacheWrite: reports.reduce((a, r) => a + r.cacheWrite, 0),
15556
+ meanCacheReadPerCall,
15557
+ meanBoot,
15558
+ accumulated,
15559
+ accumulatedPct,
15560
+ bootConfirmedSessions: reports.reduce((a, r) => a + r.bootConfirmedSessions, 0),
15561
+ bootSampledSessions,
15562
+ earliest,
15563
+ latest
15564
+ };
15565
+ }
15566
+ var ROLLING_WINDOW_NOTE = "Rolling window, not all-time: Claude Code prunes transcripts older than `cleanupPeriodDays` (default 30 days). Totals shrink over time purely from retention as old sessions age out \u2014 that is NOT the same as spend going down.";
15567
+ function formatDate(iso2) {
15568
+ return iso2 ? iso2.slice(0, 10) : "?";
15569
+ }
15570
+ function formatRange(r) {
15571
+ if (!r.earliest && !r.latest) return "no dated turns";
15572
+ return `${formatDate(r.earliest)} \u2192 ${formatDate(r.latest)}`;
15573
+ }
15574
+ var tokensCommand = new Command30("tokens").description(
15575
+ "Attribute token spend across captain vs crews and boot prefix vs accumulated conversation (Claude Code transcripts only)"
15576
+ ).option("--project <name>", "scope to a single registered project").option("--json", "print machine-readable JSON instead of a table").action(async (opts) => {
15577
+ const config = loadConfig();
15578
+ let entries = Object.entries(config.projects);
15579
+ if (opts.project) {
15580
+ if (!(opts.project in config.projects)) {
15581
+ const known = Object.keys(config.projects).sort().join(", ") || "(no projects registered)";
15582
+ console.error(chalk30.red(`Unknown project '${opts.project}'. Known projects: ${known}`));
15583
+ process.exit(1);
15584
+ }
15585
+ entries = entries.filter(([name]) => name === opts.project);
15586
+ }
15587
+ const reports = [];
15588
+ for (const [name, project] of entries) {
15589
+ reports.push(await collectProjectTokenReport(name, resolveHome(project.path)));
15590
+ }
15591
+ const active = reports.filter((r) => r.captain.calls > 0 || r.crews.calls > 0);
15592
+ const skipped = reports.length - active.length;
15593
+ const dataWindow = mergeRanges(active.flatMap((r) => [r.captain, r.crews]));
15594
+ if (opts.json) {
15595
+ console.log(JSON.stringify({ dataWindow, rollingWindowNote: ROLLING_WINDOW_NOTE, projects: active }, null, 2));
15596
+ return;
15597
+ }
15598
+ if (active.length === 0) {
15599
+ console.log(chalk30.yellow("\nNo Claude Code transcripts found for any registered project.\n"));
15600
+ return;
15601
+ }
15602
+ console.log(chalk30.bold("\nToken spend by project (Claude Code transcripts only)\n"));
15603
+ console.log(chalk30.yellow(` Data window: ${formatRange(dataWindow)}`));
15604
+ console.log(chalk30.dim(` ${ROLLING_WINDOW_NOTE}
15605
+ `));
15606
+ console.log(chalk30.dim(` ${"PROJECT/ROLE".padEnd(10)} ${"FILES".padStart(6)} ${"CALLS".padStart(8)} ${"INPUT".padStart(8)} ${"OUTPUT".padStart(8)} ${"CACHE_READ".padStart(10)} ${"CACHE_WRITE".padStart(10)} ${"TOTAL".padStart(10)} WINDOW`));
15607
+ console.log(chalk30.dim(" " + "\u2500".repeat(78)));
15608
+ for (const r of active) {
15609
+ console.log(chalk30.bold(` ${r.project}`));
15610
+ if (r.captain.calls > 0) printRoleRow("captain", r.captain);
15611
+ if (r.crews.calls > 0) printRoleRow("crews", r.crews);
15612
+ }
15613
+ const totalCaptain = sumRoleReports("captain", active.map((r) => r.captain));
15614
+ const totalCrews = sumRoleReports("crews", active.map((r) => r.crews));
15615
+ console.log(chalk30.dim(" " + "\u2500".repeat(78)));
15616
+ console.log(chalk30.bold(" TOTAL"));
15617
+ printRoleRow("captain", totalCaptain);
15618
+ printRoleRow("crews", totalCrews);
15619
+ console.log(chalk30.bold("\nBoot prefix vs accumulated conversation\n"));
15620
+ printBootLine("captain", totalCaptain);
15621
+ printBootLine("crews", totalCrews);
15622
+ console.log(
15623
+ chalk30.dim(
15624
+ "\n cache_read is ~1/10 the price of fresh input \u2014 do not read the TOTAL column as spend.\n claude-only reader today; squadrant is multi-agent but no other driver writes an equivalent transcript yet.\n"
15625
+ )
15626
+ );
15627
+ if (skipped > 0) {
15628
+ console.log(chalk30.dim(` ${skipped} project(s) with no local Claude transcripts omitted.
15629
+ `));
15630
+ }
15631
+ });
15632
+
15133
15633
  // packages/cli/src/commands/telegram.ts
15134
15634
  init_dist();
15135
15635
  init_dist2();
15136
- import { join as join27, dirname as dirname8 } from "path";
15636
+ import { join as join28, dirname as dirname8 } from "path";
15137
15637
  import { emitKeypressEvents } from "readline";
15138
- import { Command as Command30 } from "commander";
15139
- import chalk30 from "chalk";
15638
+ import { Command as Command31 } from "commander";
15639
+ import chalk31 from "chalk";
15140
15640
  function defaultStateRoot() {
15141
- return join27(dirname8(DEFAULT_CONFIG_PATH), "state");
15641
+ return join28(dirname8(DEFAULT_CONFIG_PATH), "state");
15142
15642
  }
15143
15643
  async function questionMasked() {
15144
- return new Promise((resolve3) => {
15644
+ return new Promise((resolve4) => {
15145
15645
  emitKeypressEvents(process.stdin);
15146
15646
  process.stdin.setRawMode(true);
15147
15647
  process.stdin.resume();
@@ -15158,7 +15658,7 @@ async function questionMasked() {
15158
15658
  process.stdin.setRawMode(false);
15159
15659
  process.stdin.pause();
15160
15660
  process.stdout.write("\n");
15161
- resolve3(answer);
15661
+ resolve4(answer);
15162
15662
  } else if (key.name === "backspace") {
15163
15663
  if (answer.length > 0) {
15164
15664
  answer = answer.slice(0, -1);
@@ -15175,19 +15675,19 @@ async function questionMasked() {
15175
15675
  async function questionYesNo(prompt) {
15176
15676
  const { createInterface: createInterface2 } = await import("readline");
15177
15677
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
15178
- return new Promise((resolve3) => {
15678
+ return new Promise((resolve4) => {
15179
15679
  rl.question(prompt, (ans) => {
15180
15680
  rl.close();
15181
15681
  process.stdin.pause();
15182
- resolve3(/^y(es)?$/i.test(ans.trim()));
15682
+ resolve4(/^y(es)?$/i.test(ans.trim()));
15183
15683
  });
15184
15684
  });
15185
15685
  }
15186
- var telegramCommand = new Command30("telegram").description("Two-way Telegram integration: push crew events to a topic and reply into the captain pane");
15686
+ var telegramCommand = new Command31("telegram").description("Two-way Telegram integration: push crew events to a topic and reply into the captain pane");
15187
15687
  telegramCommand.command("status").description("Show Telegram config and linked projects").action(() => {
15188
15688
  const { tokenSet, supergroupId, links } = runTelegramStatus({ config: loadConfig(), stateRoot: defaultStateRoot() });
15189
- console.log(`token: ${tokenSet ? chalk30.green("set") : chalk30.yellow("unset")}`);
15190
- console.log(`supergroup: ${supergroupId ?? chalk30.yellow("unset")}`);
15689
+ console.log(`token: ${tokenSet ? chalk31.green("set") : chalk31.yellow("unset")}`);
15690
+ console.log(`supergroup: ${supergroupId ?? chalk31.yellow("unset")}`);
15191
15691
  if (links.length === 0) {
15192
15692
  console.log("no projects linked");
15193
15693
  return;
@@ -15197,32 +15697,32 @@ telegramCommand.command("status").description("Show Telegram config and linked p
15197
15697
  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) => {
15198
15698
  const cfg = loadConfig().telegram;
15199
15699
  if (!cfg) {
15200
- console.error(chalk30.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
15700
+ console.error(chalk31.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
15201
15701
  process.exit(1);
15202
15702
  }
15203
15703
  const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
15204
15704
  if (!token) {
15205
- console.error(chalk30.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
15705
+ console.error(chalk31.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
15206
15706
  process.exit(1);
15207
15707
  }
15208
15708
  const client = createTelegramClient({ token });
15209
15709
  const { topicId, created } = await runTelegramLink({ project, cfg, client, stateRoot: defaultStateRoot() });
15210
- console.log(chalk30.green(`${created ? "linked" : "already linked"}: ${project} \u2192 topic ${topicId}`));
15710
+ console.log(chalk31.green(`${created ? "linked" : "already linked"}: ${project} \u2192 topic ${topicId}`));
15211
15711
  });
15212
15712
  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) => {
15213
15713
  if (!process.stdin.isTTY) {
15214
- console.error(chalk30.red("setup requires a TTY \u2014 pipe input is not supported"));
15714
+ console.error(chalk31.red("setup requires a TTY \u2014 pipe input is not supported"));
15215
15715
  process.exit(1);
15216
15716
  }
15217
15717
  console.log();
15218
- console.log(chalk30.bold("Telegram setup") + " \u2014 connect squadrant to a Telegram bot for notifications + remote control");
15718
+ console.log(chalk31.bold("Telegram setup") + " \u2014 connect squadrant to a Telegram bot for notifications + remote control");
15219
15719
  console.log();
15220
15720
  console.log("Before you start you need:");
15221
15721
  console.log(" 1. A bot token from @BotFather (send /newbot)");
15222
15722
  console.log(" 2. A forum supergroup with the bot added as an admin (Topics enabled)");
15223
15723
  console.log(" 3. Bot privacy mode set to OFF (@BotFather \u2192 /setprivacy \u2192 Disable)");
15224
15724
  console.log();
15225
- console.log(chalk30.bold("Step 1/3 \u2014 Bot token"));
15725
+ console.log(chalk31.bold("Step 1/3 \u2014 Bot token"));
15226
15726
  const existingCfg = loadConfig().telegram;
15227
15727
  const existingToken = existingCfg?.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
15228
15728
  const decision = resolveSetupToken(existingToken, { resetToken: opts.resetToken ?? false });
@@ -15234,67 +15734,67 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
15234
15734
  try {
15235
15735
  botUser = await client.getMe();
15236
15736
  token = existingToken;
15237
- console.log(chalk30.green(`Using existing bot token (@${botUser.username})`));
15737
+ console.log(chalk31.green(`Using existing bot token (@${botUser.username})`));
15238
15738
  console.log();
15239
15739
  } catch {
15240
- console.log(chalk30.yellow("Existing token is invalid \u2014 please enter a new one."));
15740
+ console.log(chalk31.yellow("Existing token is invalid \u2014 please enter a new one."));
15241
15741
  console.log("Paste your bot token then press Enter (input is hidden):");
15242
15742
  token = await questionMasked();
15243
15743
  if (!token) {
15244
- console.error(chalk30.red("token required"));
15744
+ console.error(chalk31.red("token required"));
15245
15745
  process.exit(1);
15246
15746
  }
15247
15747
  client = createTelegramClient({ token });
15248
15748
  try {
15249
15749
  botUser = await client.getMe();
15250
15750
  } catch (e) {
15251
- console.error(chalk30.red(`token rejected: ${e.message}`));
15751
+ console.error(chalk31.red(`token rejected: ${e.message}`));
15252
15752
  process.exit(1);
15253
15753
  }
15254
- console.log(chalk30.green(`Connected as @${botUser.username}`));
15754
+ console.log(chalk31.green(`Connected as @${botUser.username}`));
15255
15755
  console.log();
15256
15756
  }
15257
15757
  } else {
15258
15758
  console.log("Paste your bot token then press Enter (input is hidden):");
15259
15759
  token = await questionMasked();
15260
15760
  if (!token) {
15261
- console.error(chalk30.red("token required"));
15761
+ console.error(chalk31.red("token required"));
15262
15762
  process.exit(1);
15263
15763
  }
15264
15764
  client = createTelegramClient({ token });
15265
15765
  try {
15266
15766
  botUser = await client.getMe();
15267
15767
  } catch (e) {
15268
- console.error(chalk30.red(`token rejected: ${e.message}`));
15768
+ console.error(chalk31.red(`token rejected: ${e.message}`));
15269
15769
  process.exit(1);
15270
15770
  }
15271
- console.log(chalk30.green(`Connected as @${botUser.username}`));
15771
+ console.log(chalk31.green(`Connected as @${botUser.username}`));
15272
15772
  console.log();
15273
15773
  }
15274
- console.log(chalk30.bold("Step 2/3 \u2014 Supergroup"));
15774
+ console.log(chalk31.bold("Step 2/3 \u2014 Supergroup"));
15275
15775
  const groupDecision = resolveSetupGroup(existingCfg?.supergroupId, { redetect: opts.redetect ?? false });
15276
15776
  let supergroupId;
15277
15777
  let detectedUserId;
15278
15778
  if (groupDecision === "reuse") {
15279
15779
  supergroupId = existingCfg.supergroupId;
15280
- console.log(chalk30.green(`Using existing group: ${supergroupId}`));
15780
+ console.log(chalk31.green(`Using existing group: ${supergroupId}`));
15281
15781
  console.log();
15282
15782
  } else {
15283
15783
  console.log("Add the bot to your forum supergroup, then send any message in it.");
15284
- console.log(chalk30.dim("Waiting for a message (up to 60s)\u2026"));
15784
+ console.log(chalk31.dim("Waiting for a message (up to 60s)\u2026"));
15285
15785
  try {
15286
15786
  ({ supergroupId, userId: detectedUserId } = await detectGroupAndUser(client, { timeoutMs: 6e4 }));
15287
15787
  } catch {
15288
- console.error(chalk30.red("Timed out \u2014 no supergroup message received within 60s."));
15289
- console.error(chalk30.yellow("Check: bot is an admin in the group \xB7 privacy mode is OFF \xB7 Topics enabled"));
15788
+ console.error(chalk31.red("Timed out \u2014 no supergroup message received within 60s."));
15789
+ console.error(chalk31.yellow("Check: bot is an admin in the group \xB7 privacy mode is OFF \xB7 Topics enabled"));
15290
15790
  process.exit(1);
15291
15791
  }
15292
- console.log(chalk30.green(`Found group: ${supergroupId}`));
15792
+ console.log(chalk31.green(`Found group: ${supergroupId}`));
15293
15793
  console.log();
15294
15794
  }
15295
- console.log(chalk30.bold("Step 3/3 \u2014 Remote control + Save"));
15296
- console.log(chalk30.dim("Remote control enables auto-launching captains and the General command channel"));
15297
- console.log(chalk30.dim("from your phone \u2014 gated to your Telegram user-id only (fail-closed)."));
15795
+ console.log(chalk31.bold("Step 3/3 \u2014 Remote control + Save"));
15796
+ console.log(chalk31.dim("Remote control enables auto-launching captains and the General command channel"));
15797
+ console.log(chalk31.dim("from your phone \u2014 gated to your Telegram user-id only (fail-closed)."));
15298
15798
  const finalUserId = resolveSetupUserId(opts.userId, detectedUserId, defaultStateRoot());
15299
15799
  let users;
15300
15800
  let remoteControl;
@@ -15308,32 +15808,32 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
15308
15808
  remoteControl = true;
15309
15809
  }
15310
15810
  } else if (groupDecision === "detect") {
15311
- console.log(chalk30.yellow("Could not read your user-id from that message \u2014 skipping remote control."));
15312
- console.log(chalk30.yellow("Re-run with --user-id <id> to enable, or edit telegram.users in config manually."));
15811
+ console.log(chalk31.yellow("Could not read your user-id from that message \u2014 skipping remote control."));
15812
+ console.log(chalk31.yellow("Re-run with --user-id <id> to enable, or edit telegram.users in config manually."));
15313
15813
  printedRemoteControlState = true;
15314
15814
  } else {
15315
15815
  const existingUsers = existingCfg?.users;
15316
15816
  if (existingUsers && existingUsers.length > 0) {
15317
- console.log(chalk30.dim(`Remote control: already configured (user-id ${existingUsers[0]}). Use --user-id to update.`));
15817
+ console.log(chalk31.dim(`Remote control: already configured (user-id ${existingUsers[0]}). Use --user-id to update.`));
15318
15818
  } else {
15319
- console.log(chalk30.dim("Remote control: off. Re-run with --user-id <id> to enable."));
15819
+ console.log(chalk31.dim("Remote control: off. Re-run with --user-id <id> to enable."));
15320
15820
  }
15321
15821
  printedRemoteControlState = true;
15322
15822
  }
15323
15823
  writeTelegramConfig(DEFAULT_CONFIG_PATH, { token, supergroupId, users, remoteControl });
15324
- console.log(chalk30.green(`Wrote telegram config \u2014 token: ${maskToken(token)} group: ${supergroupId}`));
15824
+ console.log(chalk31.green(`Wrote telegram config \u2014 token: ${maskToken(token)} group: ${supergroupId}`));
15325
15825
  if (!printedRemoteControlState) {
15326
15826
  if (remoteControl) {
15327
- console.log(chalk30.green(`Remote control: ON (allowlisted user-id ${users[0]})`));
15827
+ console.log(chalk31.green(`Remote control: ON (allowlisted user-id ${users[0]})`));
15328
15828
  } else {
15329
- console.log(chalk30.dim("Remote control: off (default). Re-run with --user-id <id> to enable."));
15829
+ console.log(chalk31.dim("Remote control: off (default). Re-run with --user-id <id> to enable."));
15330
15830
  }
15331
15831
  }
15332
15832
  try {
15333
15833
  await runRegisterCommands({ client });
15334
- console.log(chalk30.dim("Registered the /command menu."));
15834
+ console.log(chalk31.dim("Registered the /command menu."));
15335
15835
  } catch (e) {
15336
- console.log(chalk30.yellow(`command-menu registration skipped: ${e.message}`));
15836
+ console.log(chalk31.yellow(`command-menu registration skipped: ${e.message}`));
15337
15837
  }
15338
15838
  const topics = loadState(defaultStateRoot()).topics;
15339
15839
  const topicEntries = Object.entries(topics);
@@ -15342,28 +15842,28 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
15342
15842
  const project = key.slice(0, key.indexOf("::"));
15343
15843
  return `${project}\u2192${id}`;
15344
15844
  }).join(", ");
15345
- console.log(chalk30.dim(`Existing topics: ${summary} (already created \u2014 not recreated)`));
15845
+ console.log(chalk31.dim(`Existing topics: ${summary} (already created \u2014 not recreated)`));
15346
15846
  } else {
15347
- console.log(chalk30.dim("No project topics yet \u2014 they're created on first delivery or via: squadrant telegram link <project>"));
15847
+ console.log(chalk31.dim("No project topics yet \u2014 they're created on first delivery or via: squadrant telegram link <project>"));
15348
15848
  }
15349
15849
  runTelegramPostSetup({});
15350
15850
  console.log();
15351
- console.log(`Next: ${chalk30.cyan("squadrant telegram link <project>")}`);
15851
+ console.log(`Next: ${chalk31.cyan("squadrant telegram link <project>")}`);
15352
15852
  });
15353
15853
  telegramCommand.command("register-commands").description("Register (or re-register) the bot's / command menu with Telegram").action(async () => {
15354
15854
  const cfg = loadConfig().telegram;
15355
15855
  if (!cfg) {
15356
- console.error(chalk30.red("telegram config absent \u2014 run: squadrant telegram setup"));
15856
+ console.error(chalk31.red("telegram config absent \u2014 run: squadrant telegram setup"));
15357
15857
  process.exit(1);
15358
15858
  }
15359
15859
  const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
15360
15860
  if (!token) {
15361
- console.error(chalk30.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
15861
+ console.error(chalk31.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
15362
15862
  process.exit(1);
15363
15863
  }
15364
15864
  const client = createTelegramClient({ token });
15365
15865
  await runRegisterCommands({ client });
15366
- console.log(chalk30.green(`registered ${BOT_COMMANDS.length} bot commands`));
15866
+ console.log(chalk31.green(`registered ${BOT_COMMANDS.length} bot commands`));
15367
15867
  });
15368
15868
  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) => {
15369
15869
  const stateRoot = defaultStateRoot();
@@ -15374,7 +15874,7 @@ telegramCommand.command("notify").argument("[project]", "project to toggle").arg
15374
15874
  return;
15375
15875
  }
15376
15876
  for (const r of rows) {
15377
- console.log(` ${r.project}: ${r.active ? chalk30.green("on") : chalk30.dim("off (muted)")}`);
15877
+ console.log(` ${r.project}: ${r.active ? chalk31.green("on") : chalk31.dim("off (muted)")}`);
15378
15878
  }
15379
15879
  return;
15380
15880
  }
@@ -15383,53 +15883,53 @@ telegramCommand.command("notify").argument("[project]", "project to toggle").arg
15383
15883
  const token = tgCfg?.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
15384
15884
  if (state === "crew" || state === "cap") {
15385
15885
  if (value === void 0) {
15386
- console.error(chalk30.red(`usage: squadrant telegram notify <project> ${state} <value>`));
15886
+ console.error(chalk31.red(`usage: squadrant telegram notify <project> ${state} <value>`));
15387
15887
  process.exit(1);
15388
15888
  }
15389
15889
  const resolved2 = resolveNotify(globalNotify, loadProjectOverride(project));
15390
15890
  const before2 = { ...resolved2, active: isNotifyActive(stateRoot, project) };
15391
15891
  const res = runTelegramNotifyPref({ project, dimension: state, value });
15392
15892
  if (!res.ok) {
15393
- console.error(chalk30.red(res.message));
15893
+ console.error(chalk31.red(res.message));
15394
15894
  process.exit(1);
15395
15895
  }
15396
- console.log(chalk30.green(`${project} ${state} = ${value}`));
15896
+ console.log(chalk31.green(`${project} ${state} = ${value}`));
15397
15897
  const after2 = state === "crew" ? { ...before2, crew: value } : { ...before2, cap: value === "on" };
15398
15898
  if (tgCfg && token) {
15399
15899
  const client = createTelegramClient({ token });
15400
15900
  const sent = await runNotifyConfirmation({ project, before: before2, after: after2, cfg: tgCfg, client, stateRoot });
15401
- if (sent) console.log(chalk30.dim(`\u2192 notified ${project} topic`));
15901
+ if (sent) console.log(chalk31.dim(`\u2192 notified ${project} topic`));
15402
15902
  }
15403
15903
  return;
15404
15904
  }
15405
15905
  if (state !== "on" && state !== "off") {
15406
- console.error(chalk30.red("usage: squadrant telegram notify <project> <on|off|crew <tier>|cap <on|off>>"));
15906
+ console.error(chalk31.red("usage: squadrant telegram notify <project> <on|off|crew <tier>|cap <on|off>>"));
15407
15907
  process.exit(1);
15408
15908
  }
15409
15909
  const resolved = resolveNotify(globalNotify, loadProjectOverride(project));
15410
15910
  const before = { ...resolved, active: isNotifyActive(stateRoot, project) };
15411
15911
  const after = { ...before, active: state === "on" };
15412
15912
  runTelegramNotifySet({ project, active: state === "on", stateRoot });
15413
- console.log(chalk30.green(`${project} notifications ${state === "on" ? "ON" : "OFF"}`));
15913
+ console.log(chalk31.green(`${project} notifications ${state === "on" ? "ON" : "OFF"}`));
15414
15914
  if (tgCfg && token) {
15415
15915
  const client = createTelegramClient({ token });
15416
15916
  const sent = await runNotifyConfirmation({ project, before, after, cfg: tgCfg, client, stateRoot });
15417
- if (sent) console.log(chalk30.dim(`\u2192 notified ${project} topic`));
15917
+ if (sent) console.log(chalk31.dim(`\u2192 notified ${project} topic`));
15418
15918
  }
15419
15919
  });
15420
15920
  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) => {
15421
15921
  const cfg = loadConfig().telegram;
15422
15922
  if (!cfg) {
15423
- console.error(chalk30.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
15923
+ console.error(chalk31.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
15424
15924
  process.exit(1);
15425
15925
  }
15426
15926
  const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
15427
15927
  if (!token) {
15428
- console.error(chalk30.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
15928
+ console.error(chalk31.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
15429
15929
  process.exit(1);
15430
15930
  }
15431
15931
  if (!capAllowed(project, cfg.notify)) {
15432
- console.log(chalk30.dim(`${project}: captain messages muted (cap=off) \u2014 not sent`));
15932
+ console.log(chalk31.dim(`${project}: captain messages muted (cap=off) \u2014 not sent`));
15433
15933
  return;
15434
15934
  }
15435
15935
  let message;
@@ -15442,19 +15942,19 @@ telegramCommand.command("send").argument("<project>", "project whose topic recei
15442
15942
  for await (const line of rl) lines.push(line);
15443
15943
  message = lines.join("\n").trimEnd();
15444
15944
  if (!message) {
15445
- console.error(chalk30.red("no message provided (stdin was empty)"));
15945
+ console.error(chalk31.red("no message provided (stdin was empty)"));
15446
15946
  process.exit(1);
15447
15947
  }
15448
15948
  } else {
15449
- console.error(chalk30.red("message required \u2014 pass as argument or pipe via stdin"));
15949
+ console.error(chalk31.red("message required \u2014 pass as argument or pipe via stdin"));
15450
15950
  process.exit(1);
15451
15951
  }
15452
15952
  const client = createTelegramClient({ token });
15453
15953
  try {
15454
15954
  const { chatId, topicId } = await runTelegramSend({ project, message, cfg, client, stateRoot: defaultStateRoot() });
15455
- console.log(chalk30.green(`sent to group ${chatId} topic ${topicId}`));
15955
+ console.log(chalk31.green(`sent to group ${chatId} topic ${topicId}`));
15456
15956
  } catch (e) {
15457
- console.error(chalk30.red(e.message));
15957
+ console.error(chalk31.red(e.message));
15458
15958
  process.exit(1);
15459
15959
  }
15460
15960
  });
@@ -15462,10 +15962,92 @@ telegramCommand.command("send").argument("<project>", "project whose topic recei
15462
15962
  // packages/cli/src/commands/hooks.ts
15463
15963
  init_dist2();
15464
15964
  init_dist4();
15465
- import { Command as Command31 } from "commander";
15466
- import { join as join28 } from "path";
15467
- import { homedir as homedir20 } from "os";
15468
- var SOCK4 = join28(homedir20(), ".config", "squadrant", "squadrant.sock");
15965
+ init_dist();
15966
+ import { Command as Command32 } from "commander";
15967
+ import { join as join29 } from "path";
15968
+ import { homedir as homedir21 } from "os";
15969
+
15970
+ // packages/cli/src/lib/captain-session-registry.ts
15971
+ import fs28 from "fs";
15972
+ import path29 from "path";
15973
+
15974
+ // packages/cli/src/lib/handoff-facts.ts
15975
+ var STALE_FETCH_WARNING_MS = 24 * 60 * 60 * 1e3;
15976
+ var SESSION_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
15977
+ function staleWarning(live) {
15978
+ if (live.aheadOfBaseSource !== "local-git") return null;
15979
+ if (live.fetchAgeMs === null) {
15980
+ return "aheadOfBase came from local git with no known last-fetch time \u2014 treat as possibly stale";
15981
+ }
15982
+ if (live.fetchAgeMs > STALE_FETCH_WARNING_MS) {
15983
+ const hours = Math.round(live.fetchAgeMs / 36e5);
15984
+ return `aheadOfBase came from local git, last fetched ${hours}h ago \u2014 may be stale`;
15985
+ }
15986
+ return null;
15987
+ }
15988
+ function sourceAvailability(live, claudeMem, checkpoint, gapSessions) {
15989
+ const available = [];
15990
+ const missing = [];
15991
+ const liveHasData = live.openPRs.length > 0 || live.liveCrews.length > 0 || live.aheadOfBaseSource !== "unknown" || live.recentCommits.length > 0;
15992
+ (liveHasData ? available : missing).push("liveRepo");
15993
+ const claudeMemHasData = !!claudeMem && (claudeMem.latestSessionSummary !== null || claudeMem.recentDecisions.length > 0);
15994
+ (claudeMemHasData ? available : missing).push("claudeMem");
15995
+ (checkpoint ? available : missing).push("checkpoint");
15996
+ (gapSessions.length > 0 ? available : missing).push("gapSessions");
15997
+ return { available, missing };
15998
+ }
15999
+ function assembleHandoffFacts(live, claudeMem, gapSessions, checkpoint, now, extras = {}) {
16000
+ const sortedGap = [...gapSessions].sort((a, b) => Date.parse(b.session.startedAt) - Date.parse(a.session.startedAt));
16001
+ const { available, missing } = sourceAvailability(live, claudeMem, checkpoint, sortedGap);
16002
+ return {
16003
+ meta: {
16004
+ generatedAt: now,
16005
+ checkpointFilename: checkpoint?.filename ?? null,
16006
+ usedFallbackWindow: extras.usedFallbackWindow ?? false,
16007
+ fallbackWindowMs: extras.usedFallbackWindow ? extras.fallbackWindowMs ?? SESSION_WINDOW_MS : null,
16008
+ gapSessionIds: sortedGap.map((s) => s.session.sessionId),
16009
+ sourcesAvailable: available,
16010
+ sourcesMissing: missing,
16011
+ registryNote: extras.registryNote ?? null
16012
+ },
16013
+ liveRepo: { ...live, staleWarning: staleWarning(live) },
16014
+ claudeMem,
16015
+ checkpoint,
16016
+ gapSessions: sortedGap
16017
+ };
16018
+ }
16019
+
16020
+ // packages/cli/src/lib/captain-session-registry.ts
16021
+ var CAPTAIN_SESSION_REGISTRY_FILE = "captain-sessions.jsonl";
16022
+ function appendCaptainSession(spokeVault, record) {
16023
+ fs28.mkdirSync(spokeVault, { recursive: true });
16024
+ const file = path29.join(spokeVault, CAPTAIN_SESSION_REGISTRY_FILE);
16025
+ fs28.appendFileSync(file, JSON.stringify(record) + "\n");
16026
+ }
16027
+ function readCaptainSessionRegistry(spokeVault) {
16028
+ const file = path29.join(spokeVault, CAPTAIN_SESSION_REGISTRY_FILE);
16029
+ if (!fs28.existsSync(file)) return [];
16030
+ const records = [];
16031
+ for (const line of fs28.readFileSync(file, "utf-8").split("\n")) {
16032
+ if (!line.trim()) continue;
16033
+ try {
16034
+ records.push(JSON.parse(line));
16035
+ } catch {
16036
+ }
16037
+ }
16038
+ return records;
16039
+ }
16040
+ function selectGapSessions(records, currentSessionId, checkpoint, now, fallbackWindowMs = SESSION_WINDOW_MS) {
16041
+ const excludingCurrent = records.filter((r) => r.sessionId !== currentSessionId);
16042
+ const filtered = checkpoint ? excludingCurrent.filter((r) => Date.parse(r.startedAt) > now - checkpoint.ageMs) : excludingCurrent.filter((r) => now - Date.parse(r.startedAt) <= fallbackWindowMs);
16043
+ return {
16044
+ gapSessions: filtered.sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt)),
16045
+ usedFallbackWindow: !checkpoint
16046
+ };
16047
+ }
16048
+
16049
+ // packages/cli/src/commands/hooks.ts
16050
+ var SOCK4 = join29(homedir21(), ".config", "squadrant", "squadrant.sock");
15469
16051
  async function sendToSock(req) {
15470
16052
  await sendRequest(SOCK4, req);
15471
16053
  }
@@ -15488,14 +16070,31 @@ function mapHookSub(sub, payload, taskId) {
15488
16070
  return null;
15489
16071
  }
15490
16072
  }
16073
+ function buildCaptainSessionRecord(payload, project, fallbackCwd, now) {
16074
+ if (typeof payload !== "object" || payload === null) return null;
16075
+ const p = payload;
16076
+ const sessionId = typeof p.session_id === "string" && p.session_id ? p.session_id : null;
16077
+ if (!sessionId) return null;
16078
+ const cwd = typeof p.cwd === "string" && p.cwd ? p.cwd : fallbackCwd;
16079
+ const transcriptPath = (typeof p.transcript_path === "string" && p.transcript_path ? p.transcript_path : null) ?? deriveTranscriptPath(sessionId, cwd) ?? "";
16080
+ return { sessionId, project, agent: "claude", startedAt: now, cwd, transcriptPath };
16081
+ }
16082
+ function recordCaptainSessionStart(payload) {
16083
+ try {
16084
+ const config = loadConfig();
16085
+ const project = resolveCurrentProject(config);
16086
+ if (!project) return;
16087
+ const proj = config.projects[project];
16088
+ if (!proj) return;
16089
+ const record = buildCaptainSessionRecord(payload, project, process.cwd(), (/* @__PURE__ */ new Date()).toISOString());
16090
+ if (!record) return;
16091
+ appendCaptainSession(proj.spokeVault, record);
16092
+ } catch {
16093
+ }
16094
+ }
15491
16095
  function hooksCommand() {
15492
- const hooks = new Command31("hooks").description("(internal) receive lifecycle hook events from agent processes");
16096
+ const hooks = new Command32("hooks").description("(internal) receive lifecycle hook events from agent processes");
15493
16097
  hooks.command("claude <sub>", { hidden: true }).description("internal: bridge a NativeHookSource claude hook to squadrantd").action(async (sub) => {
15494
- const taskId = process.env.SQUADRANT_CREW_TASK_ID;
15495
- const project = process.env.SQUADRANT_CREW_PROJECT;
15496
- if (!taskId || !project) {
15497
- process.exit(0);
15498
- }
15499
16098
  let stdin = "";
15500
16099
  try {
15501
16100
  for await (const chunk of process.stdin) stdin += chunk;
@@ -15508,6 +16107,14 @@ function hooksCommand() {
15508
16107
  } catch {
15509
16108
  }
15510
16109
  }
16110
+ if (sub === "session-start" && process.env.SQUADRANT_ROLE === "captain") {
16111
+ recordCaptainSessionStart(payload);
16112
+ }
16113
+ const taskId = process.env.SQUADRANT_CREW_TASK_ID;
16114
+ const project = process.env.SQUADRANT_CREW_PROJECT;
16115
+ if (!taskId || !project) {
16116
+ process.exit(0);
16117
+ }
15511
16118
  const ev = mapHookSub(sub, payload, taskId);
15512
16119
  if (!ev) {
15513
16120
  process.exit(0);
@@ -15521,26 +16128,553 @@ function hooksCommand() {
15521
16128
  return hooks;
15522
16129
  }
15523
16130
 
16131
+ // packages/cli/src/commands/work.ts
16132
+ init_dist();
16133
+ init_dist2();
16134
+ import path30 from "path";
16135
+ import { Command as Command33 } from "commander";
16136
+ import chalk32 from "chalk";
16137
+ function detectCurrentProject(config, cwd = process.cwd()) {
16138
+ for (const [name, proj] of Object.entries(config.projects)) {
16139
+ const projPath = resolveHome(proj.path);
16140
+ if (cwd === projPath || cwd.startsWith(projPath + path30.sep)) return name;
16141
+ }
16142
+ return void 0;
16143
+ }
16144
+ function groupByParent(items) {
16145
+ const byId = new Map(items.map((i) => [i.id, i]));
16146
+ const childrenOf = /* @__PURE__ */ new Map();
16147
+ const roots = [];
16148
+ for (const item of items) {
16149
+ if (item.parent && byId.has(item.parent)) {
16150
+ const list = childrenOf.get(item.parent) ?? [];
16151
+ list.push(item);
16152
+ childrenOf.set(item.parent, list);
16153
+ } else {
16154
+ roots.push(item);
16155
+ }
16156
+ }
16157
+ return { roots, childrenOf };
16158
+ }
16159
+ function visibleItems(items, includeDone) {
16160
+ if (includeDone) return items;
16161
+ const { childrenOf } = groupByParent(items);
16162
+ const memo = /* @__PURE__ */ new Map();
16163
+ const keep = (item) => {
16164
+ const cached = memo.get(item.id);
16165
+ if (cached !== void 0) return cached;
16166
+ memo.set(item.id, false);
16167
+ const result = !TERMINAL_WORK_STATES.has(item.state) || (childrenOf.get(item.id) ?? []).some(keep);
16168
+ memo.set(item.id, result);
16169
+ return result;
16170
+ };
16171
+ return items.filter(keep);
16172
+ }
16173
+ function stateColor(state) {
16174
+ switch (state) {
16175
+ case "done":
16176
+ return chalk32.green;
16177
+ case "cancelled":
16178
+ return chalk32.dim;
16179
+ case "blocked":
16180
+ return chalk32.red;
16181
+ case "paused":
16182
+ return chalk32.yellow;
16183
+ default:
16184
+ return chalk32.cyan;
16185
+ }
16186
+ }
16187
+ function printItem(item, indent) {
16188
+ const color = stateColor(item.state);
16189
+ const line = " ".repeat(indent) + `${chalk32.dim(item.id)} ${item.title} ${color(`[${item.state}]`)}` + (indent === 0 ? chalk32.dim(` (${item.project})`) : "");
16190
+ console.log(TERMINAL_WORK_STATES.has(item.state) ? chalk32.dim(line) : line);
16191
+ }
16192
+ function printTree(items) {
16193
+ const { roots, childrenOf } = groupByParent(items);
16194
+ const walk = (item, depth) => {
16195
+ printItem(item, depth);
16196
+ for (const child of childrenOf.get(item.id) ?? []) walk(child, depth + 1);
16197
+ };
16198
+ for (const root of roots) walk(root, 0);
16199
+ }
16200
+ function printFlat(items) {
16201
+ for (const item of items) printItem(item, 0);
16202
+ }
16203
+ var startCmd = new Command33("start").description("Start a new work item").argument("<title>", "what you're doing").option("--project <name>", "project this work belongs to (defaults to the current registered project)").option("--parent <id>", "id of the wave/parent item this nests under").option("--tag <tag>", "attach a tag (repeatable)", (v, prev) => [...prev, v], []).action((title, opts) => {
16204
+ const config = loadConfig();
16205
+ const store = createWorkStore();
16206
+ purgeExpiredWorkItems(store);
16207
+ const project = opts.project ?? detectCurrentProject(config);
16208
+ if (!project) {
16209
+ console.error(chalk32.red("No --project given and cwd is not inside a registered project."));
16210
+ process.exit(1);
16211
+ }
16212
+ if (opts.parent && !findWorkItemById(store, opts.parent)) {
16213
+ console.error(chalk32.red(`Parent work item '${opts.parent}' not found.`));
16214
+ process.exit(1);
16215
+ }
16216
+ const item = createWorkItem(store, { project, title, parent: opts.parent ?? null, tags: opts.tag });
16217
+ console.log(chalk32.green(`\u2713 ${item.id}`) + ` ${item.title}` + chalk32.dim(` (${item.project})`));
16218
+ });
16219
+ var listCmd2 = new Command33("list").description("List work items").option("--project <name>", "scope to one project").option("--all", "list across every project").option("--tree", "render parent/child nesting").option("--include-done", "include done/cancelled items").action((opts) => {
16220
+ const config = loadConfig();
16221
+ const store = createWorkStore();
16222
+ purgeExpiredWorkItems(store);
16223
+ let items;
16224
+ if (opts.project) {
16225
+ items = store.list(opts.project);
16226
+ } else if (opts.all) {
16227
+ items = store.listAll();
16228
+ } else {
16229
+ const project = detectCurrentProject(config);
16230
+ if (!project) {
16231
+ console.error(chalk32.red("cwd is not inside a registered project \u2014 pass --project or --all."));
16232
+ process.exit(1);
16233
+ }
16234
+ items = store.list(project);
16235
+ }
16236
+ items = visibleItems(items, opts.includeDone ?? false);
16237
+ if (items.length === 0) {
16238
+ console.log(chalk32.dim("\nNo work items.\n"));
16239
+ return;
16240
+ }
16241
+ console.log();
16242
+ if (opts.tree) printTree(items);
16243
+ else printFlat(items);
16244
+ console.log();
16245
+ });
16246
+ function closeCommand(name, state, flag, key, desc) {
16247
+ return new Command33(name).description(`Mark a work item ${state}`).argument("<id>", "work item id").option(flag, desc).action((id, opts) => {
16248
+ const store = createWorkStore();
16249
+ purgeExpiredWorkItems(store);
16250
+ const item = closeWorkItem(store, id, state, { note: opts[key] });
16251
+ if (!item) {
16252
+ console.error(chalk32.red(`Work item '${id}' not found.`));
16253
+ process.exit(1);
16254
+ }
16255
+ console.log(chalk32.green(`\u2713 ${item.id}`) + ` ${item.title} ${chalk32.dim(`[${item.state}]`)}`);
16256
+ if (state === "done") {
16257
+ const openChildren = findOpenChildren(store, item.id);
16258
+ if (openChildren.length > 0) {
16259
+ const names = openChildren.map((c) => `${c.id} [${c.state}]`).join(", ");
16260
+ console.log(chalk32.yellow(`\u26A0 still has ${openChildren.length} unfinished child item(s): ${names}`));
16261
+ }
16262
+ }
16263
+ });
16264
+ }
16265
+ var doneCmd = closeCommand("done", "done", "--note <text>", "note", "closing note");
16266
+ var cancelCmd = closeCommand("cancel", "cancelled", "--why <text>", "why", "reason");
16267
+ var workCommand = new Command33("work").description("Track your own in-flight work \u2014 persisted, cross-project, cross-session").addCommand(startCmd).addCommand(listCmd2).addCommand(doneCmd).addCommand(cancelCmd);
16268
+
16269
+ // packages/cli/src/commands/handoff.ts
16270
+ init_dist();
16271
+ import { Command as Command34 } from "commander";
16272
+ import path33 from "path";
16273
+ import os16 from "os";
16274
+
16275
+ // packages/cli/src/lib/handoff-live-repo.ts
16276
+ init_dist();
16277
+ import { execFileSync as execFileSync8 } from "child_process";
16278
+ import fs29 from "fs";
16279
+ import path31 from "path";
16280
+
16281
+ // packages/cli/src/lib/handoff-branch-state.ts
16282
+ function tryRun(runner, cmd, args, cwd) {
16283
+ try {
16284
+ return runner.run(cmd, args, cwd);
16285
+ } catch {
16286
+ return null;
16287
+ }
16288
+ }
16289
+ function parseUpstreamTrack(raw) {
16290
+ if (raw === null) return { upstreamStatus: "unknown", aheadOfUpstream: null, behindUpstream: null };
16291
+ const [upstreamShort = "", track = ""] = raw.trim().split("|");
16292
+ if (!upstreamShort.trim()) return { upstreamStatus: "no-upstream", aheadOfUpstream: null, behindUpstream: null };
16293
+ if (track.includes("[gone]")) return { upstreamStatus: "upstream-gone", aheadOfUpstream: null, behindUpstream: null };
16294
+ const aheadMatch = track.match(/ahead (\d+)/);
16295
+ const behindMatch = track.match(/behind (\d+)/);
16296
+ const ahead = aheadMatch ? Number(aheadMatch[1]) : 0;
16297
+ const behind = behindMatch ? Number(behindMatch[1]) : 0;
16298
+ if (ahead > 0 && behind > 0) return { upstreamStatus: "diverged", aheadOfUpstream: ahead, behindUpstream: behind };
16299
+ if (ahead > 0) return { upstreamStatus: "ahead", aheadOfUpstream: ahead, behindUpstream: 0 };
16300
+ if (behind > 0) return { upstreamStatus: "behind", aheadOfUpstream: 0, behindUpstream: behind };
16301
+ return { upstreamStatus: "up-to-date", aheadOfUpstream: 0, behindUpstream: 0 };
16302
+ }
16303
+ function gatherMergedIntoBase(runner, projectPath, branch, baseBranch) {
16304
+ if (branch === baseBranch) return null;
16305
+ const originBase = `origin/${baseBranch}`;
16306
+ const originResolved = tryRun(runner, "git", ["-C", projectPath, "rev-parse", "--verify", originBase], projectPath) !== null;
16307
+ const target = originResolved ? originBase : baseBranch;
16308
+ const branchSha = tryRun(runner, "git", ["-C", projectPath, "rev-parse", branch], projectPath);
16309
+ const mergeBaseSha = tryRun(runner, "git", ["-C", projectPath, "merge-base", branch, target], projectPath);
16310
+ if (branchSha === null || mergeBaseSha === null) return null;
16311
+ return branchSha.trim() === mergeBaseSha.trim();
16312
+ }
16313
+ function gatherDirty(runner, projectPath) {
16314
+ const status = tryRun(runner, "git", ["-C", projectPath, "status", "--porcelain"], projectPath);
16315
+ if (status === null) return null;
16316
+ return status.trim().length > 0;
16317
+ }
16318
+ function gatherBranchState(runner, projectPath, branch, baseBranch, detached, fetch2) {
16319
+ const fetchPerformed = fetch2 && tryRun(runner, "git", ["-C", projectPath, "fetch", "origin"], projectPath) !== null;
16320
+ const dirtyWorkingTree = gatherDirty(runner, projectPath);
16321
+ if (detached) {
16322
+ return {
16323
+ upstreamStatus: "unknown",
16324
+ aheadOfUpstream: null,
16325
+ behindUpstream: null,
16326
+ mergedIntoBase: null,
16327
+ dirtyWorkingTree,
16328
+ onUnexpectedBranch: false,
16329
+ fetchPerformed
16330
+ };
16331
+ }
16332
+ const trackRaw = tryRun(
16333
+ runner,
16334
+ "git",
16335
+ ["-C", projectPath, "for-each-ref", "--format=%(upstream:short)|%(upstream:track)", `refs/heads/${branch}`],
16336
+ projectPath
16337
+ );
16338
+ const { upstreamStatus, aheadOfUpstream, behindUpstream } = parseUpstreamTrack(trackRaw);
16339
+ return {
16340
+ upstreamStatus,
16341
+ aheadOfUpstream,
16342
+ behindUpstream,
16343
+ mergedIntoBase: gatherMergedIntoBase(runner, projectPath, branch, baseBranch),
16344
+ dirtyWorkingTree,
16345
+ onUnexpectedBranch: branch.startsWith("crew/"),
16346
+ fetchPerformed
16347
+ };
16348
+ }
16349
+
16350
+ // packages/cli/src/lib/handoff-live-repo.ts
16351
+ var RECENT_COMMITS_LIMIT = 15;
16352
+ var OPEN_PR_LIMIT = 20;
16353
+ var RELEASE_BRANCH = "main";
16354
+ var defaultCommandRunner = {
16355
+ run(cmd, args, cwd) {
16356
+ return execFileSync8(cmd, args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
16357
+ }
16358
+ };
16359
+ function tryRun2(runner, cmd, args, cwd) {
16360
+ try {
16361
+ return runner.run(cmd, args, cwd);
16362
+ } catch {
16363
+ return null;
16364
+ }
16365
+ }
16366
+ function tryInt(raw) {
16367
+ if (raw === null) return null;
16368
+ const n = Number.parseInt(raw.trim(), 10);
16369
+ return Number.isFinite(n) ? n : null;
16370
+ }
16371
+ function gatherGhRepoInfo(runner, projectPath) {
16372
+ const out = tryRun2(runner, "gh", ["repo", "view", "--json", "nameWithOwner,defaultBranchRef"], projectPath);
16373
+ if (!out) return null;
16374
+ try {
16375
+ const parsed = JSON.parse(out);
16376
+ if (!parsed.defaultBranchRef) return null;
16377
+ return { nameWithOwner: parsed.nameWithOwner, defaultBranch: parsed.defaultBranchRef.name };
16378
+ } catch {
16379
+ return null;
16380
+ }
16381
+ }
16382
+ function gatherOpenPRs(runner, projectPath) {
16383
+ const out = tryRun2(
16384
+ runner,
16385
+ "gh",
16386
+ ["pr", "list", "--json", "number,title,headRefName", "--limit", String(OPEN_PR_LIMIT)],
16387
+ projectPath
16388
+ );
16389
+ if (!out) return [];
16390
+ try {
16391
+ const parsed = JSON.parse(out);
16392
+ return parsed.map((pr) => ({ number: pr.number, title: pr.title, headRefName: pr.headRefName }));
16393
+ } catch {
16394
+ return [];
16395
+ }
16396
+ }
16397
+ function gatherLiveCrews(tasks) {
16398
+ return tasks.filter((t) => !TERMINAL_STATES.has(t.state)).map((t) => ({ name: t.name ?? t.id, state: t.state, task: t.task, question: t.question }));
16399
+ }
16400
+ function ghAheadOfBase(runner, projectPath, nameWithOwner, base, branch) {
16401
+ return tryInt(
16402
+ tryRun2(runner, "gh", ["api", `repos/${nameWithOwner}/compare/${base}...${branch}`, "--jq", ".ahead_by"], projectPath)
16403
+ );
16404
+ }
16405
+ function ghBaseSha(runner, projectPath, nameWithOwner, base) {
16406
+ const out = tryRun2(runner, "gh", ["api", `repos/${nameWithOwner}/commits/${base}`, "--jq", ".sha"], projectPath);
16407
+ return out ? out.trim() : null;
16408
+ }
16409
+ function localBaseSha(runner, projectPath, base) {
16410
+ const out = tryRun2(runner, "git", ["-C", projectPath, "rev-parse", `origin/${base}`], projectPath);
16411
+ return out ? out.trim() : null;
16412
+ }
16413
+ function localAheadOfBase(runner, projectPath, base) {
16414
+ return tryInt(tryRun2(runner, "git", ["-C", projectPath, "rev-list", "--count", `origin/${base}..HEAD`], projectPath));
16415
+ }
16416
+ function readFetchAgeMs(projectPath, now) {
16417
+ try {
16418
+ const stat2 = fs29.statSync(path31.join(projectPath, ".git", "FETCH_HEAD"));
16419
+ return now - stat2.mtime.getTime();
16420
+ } catch {
16421
+ return null;
16422
+ }
16423
+ }
16424
+ function gatherLiveRepoState(projectPath, fallbackBaseBranch, tasks, runner = defaultCommandRunner, now = Date.now(), fetch2 = false) {
16425
+ const branch = (tryRun2(runner, "git", ["-C", projectPath, "rev-parse", "--abbrev-ref", "HEAD"], projectPath) ?? "").trim();
16426
+ const detached = branch === "HEAD";
16427
+ const log = tryRun2(runner, "git", ["-C", projectPath, "log", `-${RECENT_COMMITS_LIMIT}`, "--oneline"], projectPath) ?? "";
16428
+ const recentCommits = log.split("\n").map((l) => l.trim()).filter(Boolean);
16429
+ const ghInfo = gatherGhRepoInfo(runner, projectPath);
16430
+ const baseBranch = ghInfo?.defaultBranch ?? fallbackBaseBranch;
16431
+ const baseBranchSource = ghInfo ? "gh-api" : "local-fallback";
16432
+ const fetchAgeMs = readFetchAgeMs(projectPath, now);
16433
+ let aheadOfBase = 0;
16434
+ let aheadOfBaseSource = "unknown";
16435
+ if (branch === baseBranch) {
16436
+ aheadOfBase = null;
16437
+ aheadOfBaseSource = "n-a";
16438
+ } else {
16439
+ if (ghInfo && !detached) {
16440
+ const ghAhead = ghAheadOfBase(runner, projectPath, ghInfo.nameWithOwner, baseBranch, branch);
16441
+ if (ghAhead !== null) {
16442
+ aheadOfBase = ghAhead;
16443
+ aheadOfBaseSource = "gh-api";
16444
+ }
16445
+ }
16446
+ if (aheadOfBaseSource === "unknown") {
16447
+ const localAhead = localAheadOfBase(runner, projectPath, baseBranch);
16448
+ if (localAhead !== null) {
16449
+ aheadOfBase = localAhead;
16450
+ aheadOfBaseSource = "local-git";
16451
+ }
16452
+ }
16453
+ }
16454
+ const unreleasedAheadOfReleaseBranch = ghInfo && baseBranch !== RELEASE_BRANCH ? ghAheadOfBase(runner, projectPath, ghInfo.nameWithOwner, RELEASE_BRANCH, baseBranch) : null;
16455
+ const conflicts = [];
16456
+ if (ghInfo) {
16457
+ const ghSha = ghBaseSha(runner, projectPath, ghInfo.nameWithOwner, baseBranch);
16458
+ const localSha = localBaseSha(runner, projectPath, baseBranch);
16459
+ if (ghSha && localSha && ghSha !== localSha) {
16460
+ const ageNote = fetchAgeMs !== null ? `fetched ${Math.round(fetchAgeMs / 36e5)}h ago` : "fetch age unknown";
16461
+ conflicts.push({
16462
+ field: "baseBranch",
16463
+ claim: `local git's last-known ${baseBranch} is ${localSha} (${ageNote})`,
16464
+ fact: `GitHub's live ${baseBranch} is ${ghSha}`,
16465
+ resolution: "GitHub API wins \u2014 local git may be stale"
16466
+ });
16467
+ }
16468
+ }
16469
+ return {
16470
+ branch,
16471
+ detached,
16472
+ baseBranch,
16473
+ baseBranchSource,
16474
+ recentCommits,
16475
+ aheadOfBase,
16476
+ aheadOfBaseSource,
16477
+ fetchAgeMs,
16478
+ openPRs: gatherOpenPRs(runner, projectPath),
16479
+ liveCrews: gatherLiveCrews(tasks),
16480
+ conflicts,
16481
+ branchState: gatherBranchState(runner, projectPath, branch, baseBranch, detached, fetch2),
16482
+ unreleasedAheadOfReleaseBranch
16483
+ };
16484
+ }
16485
+
16486
+ // packages/cli/src/lib/handoff-claude-mem.ts
16487
+ import { createRequire } from "module";
16488
+ import fs30 from "fs";
16489
+ var { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
16490
+ var CLAUDE_MEM_RECENCY_LIMIT = 20;
16491
+ function decisionText(row) {
16492
+ if (row.facts) {
16493
+ try {
16494
+ const parsed = JSON.parse(row.facts);
16495
+ if (Array.isArray(parsed) && parsed.length > 0) return parsed.join("; ");
16496
+ } catch {
16497
+ }
16498
+ }
16499
+ return row.narrative ?? "";
16500
+ }
16501
+ function queryClaudeMem(dbPath, project) {
16502
+ if (!fs30.existsSync(dbPath)) return null;
16503
+ let db;
16504
+ try {
16505
+ db = new DatabaseSync(dbPath, { readOnly: true });
16506
+ } catch {
16507
+ return null;
16508
+ }
16509
+ try {
16510
+ const summaryRow = db.prepare(
16511
+ `SELECT request, completed, next_steps, created_at FROM session_summaries
16512
+ WHERE project = ? ORDER BY created_at_epoch DESC LIMIT 1`
16513
+ ).get(project);
16514
+ const decisionRows = db.prepare(
16515
+ `SELECT title, narrative, facts, created_at FROM observations
16516
+ WHERE project = ? AND type = 'decision' ORDER BY created_at_epoch DESC LIMIT ?`
16517
+ ).all(project, CLAUDE_MEM_RECENCY_LIMIT);
16518
+ const recentDecisions = decisionRows.map((r) => ({
16519
+ title: r.title,
16520
+ text: decisionText(r),
16521
+ createdAt: r.created_at
16522
+ }));
16523
+ const candidates = [summaryRow?.created_at, ...decisionRows.map((r) => r.created_at)].filter(
16524
+ (v) => !!v
16525
+ );
16526
+ const oldestCreatedAt = candidates.length > 0 ? candidates.reduce((a, b) => a < b ? a : b) : null;
16527
+ return {
16528
+ latestSessionSummary: summaryRow ? {
16529
+ request: summaryRow.request,
16530
+ completed: summaryRow.completed,
16531
+ nextSteps: summaryRow.next_steps,
16532
+ createdAt: summaryRow.created_at
16533
+ } : null,
16534
+ recentDecisions,
16535
+ oldestCreatedAt
16536
+ };
16537
+ } catch {
16538
+ return null;
16539
+ } finally {
16540
+ db.close();
16541
+ }
16542
+ }
16543
+
16544
+ // packages/cli/src/lib/handoff-transcript.ts
16545
+ import fs31 from "fs";
16546
+ var TRANSCRIPT_BYTE_CAP = 2e5;
16547
+ function tailOf(content, byteCap) {
16548
+ const buf = Buffer.from(content, "utf-8");
16549
+ if (buf.length <= byteCap) return content;
16550
+ const text = buf.subarray(buf.length - byteCap).toString("utf-8");
16551
+ return text.split("\n").slice(1).join("\n");
16552
+ }
16553
+ function extractMessages(tailText) {
16554
+ let lastUserMessage = null;
16555
+ let lastAssistantText = null;
16556
+ for (const line of tailText.split("\n")) {
16557
+ if (!line.trim()) continue;
16558
+ let obj;
16559
+ try {
16560
+ obj = JSON.parse(line);
16561
+ } catch {
16562
+ continue;
16563
+ }
16564
+ if (obj.type === "user" && typeof obj.message?.content === "string") {
16565
+ lastUserMessage = obj.message.content;
16566
+ } else if (obj.type === "assistant" && Array.isArray(obj.message?.content)) {
16567
+ const texts = obj.message.content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text);
16568
+ if (texts.length > 0) lastAssistantText = texts.join("\n");
16569
+ }
16570
+ }
16571
+ return { lastUserMessage, lastAssistantText };
16572
+ }
16573
+ function extractTranscriptTail(transcriptPath, byteCap = TRANSCRIPT_BYTE_CAP) {
16574
+ if (!fs31.existsSync(transcriptPath)) return null;
16575
+ const content = fs31.readFileSync(transcriptPath, "utf-8");
16576
+ const { lastUserMessage, lastAssistantText } = extractMessages(tailOf(content, byteCap));
16577
+ const mtimeIso = fs31.statSync(transcriptPath).mtime.toISOString();
16578
+ return { path: transcriptPath, mtimeIso, lastUserMessage, lastAssistantText };
16579
+ }
16580
+
16581
+ // packages/cli/src/lib/handoff-archive.ts
16582
+ import fs32 from "fs";
16583
+ import path32 from "path";
16584
+ function readNewestArchivedHandoff(spokeVault, now) {
16585
+ const dir = path32.join(spokeVault, "handoffs");
16586
+ if (!fs32.existsSync(dir)) return null;
16587
+ const candidates = fs32.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => {
16588
+ const full = path32.join(dir, e.name);
16589
+ return { name: e.name, full, mtime: fs32.statSync(full).mtime };
16590
+ }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
16591
+ for (const candidate of candidates) {
16592
+ let content;
16593
+ try {
16594
+ content = JSON.parse(fs32.readFileSync(candidate.full, "utf-8"));
16595
+ } catch {
16596
+ continue;
16597
+ }
16598
+ return { filename: candidate.name, path: candidate.full, ageMs: now - candidate.mtime.getTime(), content };
16599
+ }
16600
+ return null;
16601
+ }
16602
+
16603
+ // packages/cli/src/commands/handoff.ts
16604
+ var CLAUDE_MEM_DB_PATH = path33.join(os16.homedir(), ".claude-mem", "claude-mem.db");
16605
+ async function defaultFetchTasks(project) {
16606
+ return await squadrantdCall({ kind: "list", project });
16607
+ }
16608
+ async function runHandoffFacts(project, deps = {}) {
16609
+ const config = loadConfig();
16610
+ const proj = config.projects[project];
16611
+ if (!proj) {
16612
+ throw new Error(`Project '${project}' not found. Run 'squadrant projects list'.`);
16613
+ }
16614
+ const fallbackBaseBranch = resolveWorktreeBase(proj.path);
16615
+ let tasks;
16616
+ try {
16617
+ tasks = await (deps.fetchTasks ?? defaultFetchTasks)(project);
16618
+ } catch {
16619
+ tasks = [];
16620
+ }
16621
+ const nowIso = deps.now ?? (/* @__PURE__ */ new Date()).toISOString();
16622
+ const nowMs = Date.parse(nowIso);
16623
+ const fallbackWindowMs = deps.windowMs ?? SESSION_WINDOW_MS;
16624
+ const live = gatherLiveRepoState(proj.path, fallbackBaseBranch, tasks, deps.runner ?? defaultCommandRunner, nowMs, deps.fetch ?? false);
16625
+ const claudeMem = queryClaudeMem(deps.claudeMemDbPath ?? CLAUDE_MEM_DB_PATH, project);
16626
+ const checkpoint = readNewestArchivedHandoff(proj.spokeVault, nowMs);
16627
+ const currentSessionId = deps.currentSessionId !== void 0 ? deps.currentSessionId : process.env.CLAUDE_CODE_SESSION_ID ?? null;
16628
+ let registryNote = null;
16629
+ let gapSessions = [];
16630
+ let usedFallbackWindow = false;
16631
+ if (currentSessionId === null) {
16632
+ registryNote = "current session id unknown (CLAUDE_CODE_SESSION_ID unset) \u2014 cannot safely exclude the running session, so the gap was skipped";
16633
+ } else {
16634
+ const allRecords = readCaptainSessionRegistry(proj.spokeVault);
16635
+ if (allRecords.length === 0) {
16636
+ registryNote = "no session registry found yet for this project (#651's SessionStart hook may not have fired before now)";
16637
+ }
16638
+ const selection = selectGapSessions(allRecords, currentSessionId, checkpoint, nowMs, fallbackWindowMs);
16639
+ usedFallbackWindow = selection.usedFallbackWindow;
16640
+ gapSessions = selection.gapSessions.map((session) => ({ session, transcript: extractTranscriptTail(session.transcriptPath) }));
16641
+ }
16642
+ return assembleHandoffFacts(live, claudeMem, gapSessions, checkpoint, nowIso, {
16643
+ registryNote,
16644
+ usedFallbackWindow,
16645
+ fallbackWindowMs
16646
+ });
16647
+ }
16648
+ var handoffCommand = new Command34("handoff").description(
16649
+ "Handoff continuity \u2014 gather verified facts for the captain to synthesize a handoff from (#650/#651)"
16650
+ );
16651
+ handoffCommand.command("facts <project>").description(
16652
+ "Gather structured facts (gh API > local git > claude-mem > registry-attributed session window) \u2014 NOT a handoff. Read-only, pre-rendered JSON on stdout; the caller synthesizes."
16653
+ ).option("--fetch", "update remote-tracking refs (git fetch origin) before computing branch state \u2014 the only opt-in exception to this command's read-only contract", false).action(async (project, opts) => {
16654
+ const out = await runHandoffFacts(project, { fetch: opts.fetch });
16655
+ console.log(JSON.stringify(out, null, 2));
16656
+ });
16657
+
15524
16658
  // packages/cli/src/index.ts
15525
16659
  init_dist();
15526
16660
  init_dist();
15527
16661
  init_dist();
15528
16662
  init_dist();
15529
16663
  var __dirname = dirname9(fileURLToPath6(import.meta.url));
15530
- var pkg = JSON.parse(readFileSync15(join29(__dirname, "..", "package.json"), "utf-8"));
16664
+ var pkg = JSON.parse(readFileSync16(join30(__dirname, "..", "package.json"), "utf-8"));
15531
16665
  ensureRuntimeSynced({
15532
- sourceRoot: join29(__dirname, ".."),
15533
- runtimeRoot: join29(homedir21(), ".config", "squadrant")
16666
+ sourceRoot: join30(__dirname, ".."),
16667
+ runtimeRoot: join30(homedir22(), ".config", "squadrant")
15534
16668
  });
15535
16669
  if (process.argv[2] !== "config") {
15536
16670
  try {
15537
- const cfgPath = join29(homedir21(), ".config", "squadrant", "config.json");
15538
- if (existsSync12(cfgPath)) {
15539
- const cfg = JSON.parse(readFileSync15(cfgPath, "utf-8"));
16671
+ const cfgPath = join30(homedir22(), ".config", "squadrant", "config.json");
16672
+ if (existsSync13(cfgPath)) {
16673
+ const cfg = JSON.parse(readFileSync16(cfgPath, "utf-8"));
15540
16674
  if (needsCheck(cfg, pkg.version)) {
15541
16675
  const items = detectDrift(cfg, getDefaultConfig());
15542
16676
  if (items.length === 0) {
15543
- writeFileSync11(cfgPath, JSON.stringify(withStamp(cfg, pkg.version), null, 2) + "\n");
16677
+ writeFileSync12(cfgPath, JSON.stringify(withStamp(cfg, pkg.version), null, 2) + "\n");
15544
16678
  } else {
15545
16679
  const from = cfg._squadrantVersion ?? "an earlier version";
15546
16680
  process.stderr.write(
@@ -15558,9 +16692,9 @@ if (process.argv[2] !== "config") {
15558
16692
  }
15559
16693
  }
15560
16694
  if (!process.env.SQUADRANT_DAEMON_SKIP) {
15561
- ensureDaemon();
16695
+ ensureDaemon(void 0, { operatorInitiated: isOperatorInitiatedCommand(process.argv[2]) });
15562
16696
  }
15563
- var program = new Command32();
16697
+ var program = new Command35();
15564
16698
  program.name("squadrant").description("Multi-project orchestration for your coding agents (Claude, Codex, opencode, Gemini)").version(pkg.version);
15565
16699
  program.addCommand(doctorCommand);
15566
16700
  program.addCommand(initCommand);
@@ -15589,8 +16723,11 @@ program.addCommand(pingCommand);
15589
16723
  program.addCommand(dispatchCommand);
15590
16724
  program.addCommand(cmuxCommand);
15591
16725
  program.addCommand(effortCommand);
16726
+ program.addCommand(tokensCommand);
15592
16727
  program.addCommand(telegramCommand);
15593
16728
  program.addCommand(hooksCommand());
16729
+ program.addCommand(workCommand);
16730
+ program.addCommand(handoffCommand);
15594
16731
  program.parseAsync().catch((e) => {
15595
16732
  process.stderr.write(`error: ${e instanceof Error ? e.message : String(e)}
15596
16733
  `);