squadrant 0.16.6 → 0.17.1

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();
@@ -2266,9 +2297,9 @@ function startServer(sockPath, handlerOrCallbacks, onListenError = defaultListen
2266
2297
  return server;
2267
2298
  }
2268
2299
  function isDaemonSocketLive(sockPath, timeoutMs = 500) {
2269
- return new Promise((resolve3) => {
2300
+ return new Promise((resolve4) => {
2270
2301
  if (!existsSync5(sockPath)) {
2271
- resolve3(false);
2302
+ resolve4(false);
2272
2303
  return;
2273
2304
  }
2274
2305
  const conn = createConnection(sockPath);
@@ -2277,7 +2308,7 @@ function isDaemonSocketLive(sockPath, timeoutMs = 500) {
2277
2308
  conn.destroy();
2278
2309
  } catch {
2279
2310
  }
2280
- resolve3(v);
2311
+ resolve4(v);
2281
2312
  };
2282
2313
  const timer = setTimeout(() => finish(false), timeoutMs);
2283
2314
  conn.on("connect", () => {
@@ -2291,7 +2322,7 @@ function isDaemonSocketLive(sockPath, timeoutMs = 500) {
2291
2322
  });
2292
2323
  }
2293
2324
  function sendRequest(sockPath, msg, timeoutMs = 5e3) {
2294
- return new Promise((resolve3, reject) => {
2325
+ return new Promise((resolve4, reject) => {
2295
2326
  const conn = createConnection(sockPath);
2296
2327
  const dec = createDecoder();
2297
2328
  const timer = setTimeout(() => {
@@ -2307,7 +2338,7 @@ function sendRequest(sockPath, msg, timeoutMs = 5e3) {
2307
2338
  if (m._v !== void 0 && m._v !== PROTOCOL_VERSION) {
2308
2339
  reject(new Error(`squadrantd protocol v${m._v}, this client expects v${PROTOCOL_VERSION} \u2014 upgrade squadrantd or this CLI`));
2309
2340
  } else if (m.ok) {
2310
- resolve3(m.reply);
2341
+ resolve4(m.reply);
2311
2342
  } else {
2312
2343
  reject(new Error(m.error));
2313
2344
  }
@@ -2527,6 +2558,151 @@ var init_store = __esm({
2527
2558
  }
2528
2559
  });
2529
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
+
2530
2706
  // packages/core/dist/snapshot.js
2531
2707
  var snapshot_exports = {};
2532
2708
  __export(snapshot_exports, {
@@ -2580,16 +2756,16 @@ var init_snapshot = __esm({
2580
2756
 
2581
2757
  // packages/core/dist/launchd.js
2582
2758
  import { execFileSync as execFileSync3 } from "child_process";
2583
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, readFileSync as readFileSync6, existsSync as existsSync7, openSync, writeSync, closeSync, unlinkSync as unlinkSync2, constants } from "fs";
2584
- import { homedir as homedir4 } from "os";
2585
- 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";
2586
2762
  import { fileURLToPath } from "url";
2587
2763
  function plistPath() {
2588
- return join7(homedir4(), "Library", "LaunchAgents", `${LABEL}.plist`);
2764
+ return join8(homedir5(), "Library", "LaunchAgents", `${LABEL}.plist`);
2589
2765
  }
2590
2766
  function daemonEntryPath() {
2591
- const p = join7(dirname3(fileURLToPath(import.meta.url)), "squadrantd.js");
2592
- if (!existsSync7(p)) {
2767
+ const p = join8(dirname3(fileURLToPath(import.meta.url)), "squadrantd.js");
2768
+ if (!existsSync8(p)) {
2593
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)`);
2594
2770
  }
2595
2771
  return p;
@@ -2597,10 +2773,10 @@ function daemonEntryPath() {
2597
2773
  function xmlEscape(s) {
2598
2774
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2599
2775
  }
2600
- function sanitizePathForPlist(path30) {
2776
+ function sanitizePathForPlist(path34) {
2601
2777
  const seen = /* @__PURE__ */ new Set();
2602
2778
  const stable = [];
2603
- for (const p of path30.split(":")) {
2779
+ for (const p of path34.split(":")) {
2604
2780
  if (!p)
2605
2781
  continue;
2606
2782
  if (p.includes("/.claude/plugins/"))
@@ -2646,7 +2822,7 @@ function buildDaemonPath(shellPath) {
2646
2822
  }).join(":");
2647
2823
  }
2648
2824
  function renderPlist(nodeBin, daemonEntry, pathEnv = "") {
2649
- const logPath2 = join7(homedir4(), ".config", "squadrant", "squadrantd.log");
2825
+ const logPath2 = join8(homedir5(), ".config", "squadrant", "squadrantd.log");
2650
2826
  return `<?xml version="1.0" encoding="UTF-8"?>
2651
2827
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2652
2828
  <plist version="1.0">
@@ -2675,13 +2851,13 @@ function _resetRestartInFlightForTest() {
2675
2851
  restartInFlight = false;
2676
2852
  }
2677
2853
  function daemonLockPath() {
2678
- return join7(homedir4(), ".config", "squadrant", "daemon.lock");
2854
+ return join8(homedir5(), ".config", "squadrant", "daemon.lock");
2679
2855
  }
2680
2856
  function tryAcquireDaemonLock() {
2681
2857
  const lp = daemonLockPath();
2682
- if (existsSync7(lp)) {
2858
+ if (existsSync8(lp)) {
2683
2859
  try {
2684
- const pid = parseInt(readFileSync6(lp, "utf-8").trim(), 10);
2860
+ const pid = parseInt(readFileSync7(lp, "utf-8").trim(), 10);
2685
2861
  if (!Number.isFinite(pid) || pid <= 0) {
2686
2862
  unlinkSync2(lp);
2687
2863
  } else {
@@ -2714,37 +2890,57 @@ function releaseDaemonLock() {
2714
2890
  } catch {
2715
2891
  }
2716
2892
  }
2717
- 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 = {}) {
2718
2926
  if (restartInFlight)
2719
2927
  return;
2720
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
+ }
2721
2939
  if (!tryAcquireDaemonLock()) {
2722
2940
  return;
2723
2941
  }
2724
2942
  try {
2725
- const p = plistPath();
2726
- const entry = daemonEntryPath();
2727
- const desired = renderPlist(nodeBin, entry, buildDaemonPath(process.env.PATH ?? ""));
2728
- const current = existsSync7(p) ? readFileSync6(p, "utf-8") : null;
2729
- const uid = process.getuid?.() ?? 0;
2730
- const target = `gui/${uid}/${LABEL}`;
2731
- const changed = current !== desired;
2732
- const programChanged = current !== null && changed && !current.includes(programArgsBlock(nodeBin, entry));
2733
- if (changed) {
2734
- mkdirSync4(dirname3(p), { recursive: true });
2735
- writeFileSync5(p, desired);
2736
- }
2737
- if (programChanged) {
2738
- try {
2739
- execFileSync3("launchctl", ["bootout", target], { stdio: "ignore" });
2740
- } catch {
2741
- }
2742
- }
2743
- try {
2744
- execFileSync3("launchctl", ["bootstrap", `gui/${uid}`, p], { stdio: "ignore" });
2745
- } catch {
2746
- }
2747
- execFileSync3("launchctl", ["kickstart", target], { stdio: "ignore" });
2943
+ applyDaemonDrift(computeDaemonDrift(nodeBin));
2748
2944
  } catch (e) {
2749
2945
  process.stderr.write(`[squadrant] warn: ensureDaemon failed (${e instanceof Error ? e.message : e})
2750
2946
  `);
@@ -2752,12 +2948,22 @@ function ensureDaemon(nodeBin = process.execPath) {
2752
2948
  releaseDaemonLock();
2753
2949
  }
2754
2950
  }
2755
- 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;
2756
2961
  var init_launchd = __esm({
2757
2962
  "packages/core/dist/launchd.js"() {
2758
2963
  LABEL = "com.squadrant.daemon";
2759
2964
  AGENT_BINS = ["cmux", "claude", "opencode", "codex", "gemini", "node"];
2760
2965
  restartInFlight = false;
2966
+ OPERATOR_INITIATED_COMMANDS = /* @__PURE__ */ new Set(["launch", "init"]);
2761
2967
  }
2762
2968
  });
2763
2969
 
@@ -2905,7 +3111,7 @@ var init_gate = __esm({
2905
3111
  });
2906
3112
 
2907
3113
  // packages/core/dist/daemon/liveness-registry.js
2908
- 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";
2909
3115
  var LivenessRegistry;
2910
3116
  var init_liveness_registry = __esm({
2911
3117
  "packages/core/dist/daemon/liveness-registry.js"() {
@@ -2919,14 +3125,14 @@ var init_liveness_registry = __esm({
2919
3125
  this.path = opts.path;
2920
3126
  this.readFile = opts.readFile ?? ((p) => {
2921
3127
  try {
2922
- return readFileSync7(p, "utf-8");
3128
+ return readFileSync8(p, "utf-8");
2923
3129
  } catch {
2924
3130
  return void 0;
2925
3131
  }
2926
3132
  });
2927
3133
  this.writeFile = opts.writeFile ?? ((p, c) => {
2928
- writeFileSync6(`${p}.tmp`, c);
2929
- renameSync2(`${p}.tmp`, p);
3134
+ writeFileSync7(`${p}.tmp`, c);
3135
+ renameSync3(`${p}.tmp`, p);
2930
3136
  });
2931
3137
  }
2932
3138
  load() {
@@ -2975,10 +3181,10 @@ var init_liveness_registry = __esm({
2975
3181
  });
2976
3182
 
2977
3183
  // packages/core/dist/daemon/context.js
2978
- import { homedir as homedir5 } from "os";
2979
- import { join as join8 } from "path";
3184
+ import { homedir as homedir6 } from "os";
3185
+ import { join as join9 } from "path";
2980
3186
  import { spawn as realSpawn } from "child_process";
2981
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
3187
+ import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync6 } from "fs";
2982
3188
  function defaultIsPidAlive(pid) {
2983
3189
  try {
2984
3190
  process.kill(pid, 0);
@@ -2988,18 +3194,18 @@ function defaultIsPidAlive(pid) {
2988
3194
  }
2989
3195
  }
2990
3196
  function buildContext(opts) {
2991
- const stateRoot = opts.stateRoot ?? join8(homedir5(), ".config", "squadrant", "state");
2992
- 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");
2993
3199
  const store = createStore(stateRoot);
2994
3200
  const bootedAt = Date.now();
2995
3201
  const taskTimeoutMs = loadConfig().defaults.taskTimeoutMs;
2996
3202
  const isPidAlive = opts.isPidAlive ?? defaultIsPidAlive;
2997
3203
  const spawn2 = opts.spawn ?? realSpawn;
2998
- const resultsDir = join8(stateRoot, "_results");
2999
- mkdirSync5(resultsDir, { recursive: true });
3204
+ const resultsDir = join9(stateRoot, "_results");
3205
+ mkdirSync6(resultsDir, { recursive: true });
3000
3206
  const writeResult = (id, payload) => {
3001
- const p = join8(resultsDir, `${id}.txt`);
3002
- writeFileSync7(p, payload);
3207
+ const p = join9(resultsDir, `${id}.txt`);
3208
+ writeFileSync8(p, payload);
3003
3209
  return p;
3004
3210
  };
3005
3211
  const log = (m) => process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${m}
@@ -3021,7 +3227,7 @@ function buildContext(opts) {
3021
3227
  inFlightHeadlessIds: /* @__PURE__ */ new Set(),
3022
3228
  activeHeadlessKills: /* @__PURE__ */ new Set(),
3023
3229
  livenessRegistry: (() => {
3024
- const r = new LivenessRegistry({ path: join8(stateRoot, "liveness.json") });
3230
+ const r = new LivenessRegistry({ path: join9(stateRoot, "liveness.json") });
3025
3231
  r.load();
3026
3232
  return r;
3027
3233
  })(),
@@ -3743,19 +3949,19 @@ var init_server = __esm({
3743
3949
 
3744
3950
  // packages/core/dist/daemon/snapshot-gather.js
3745
3951
  import { fileURLToPath as fileURLToPath2 } from "url";
3746
- import { join as join9 } from "path";
3747
- 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";
3748
3954
  function distBuiltAt() {
3749
3955
  try {
3750
- return statSync2(SELF_PATH).mtimeMs;
3956
+ return statSync3(SELF_PATH).mtimeMs;
3751
3957
  } catch {
3752
3958
  return 0;
3753
3959
  }
3754
3960
  }
3755
- function gatherLogStats(path30, now, windowMs) {
3961
+ function gatherLogStats(path34, now, windowMs) {
3756
3962
  let sizeBytes = 0;
3757
3963
  try {
3758
- sizeBytes = statSync2(path30).size;
3964
+ sizeBytes = statSync3(path34).size;
3759
3965
  } catch {
3760
3966
  return { errorCount: 0, sizeBytes: 0, windowMs };
3761
3967
  }
@@ -3766,7 +3972,7 @@ function gatherLogStats(path30, now, windowMs) {
3766
3972
  const len = sizeBytes - start;
3767
3973
  let text = "";
3768
3974
  try {
3769
- const fd = openSync2(path30, "r");
3975
+ const fd = openSync2(path34, "r");
3770
3976
  try {
3771
3977
  const buf = Buffer.alloc(len);
3772
3978
  readSync(fd, buf, 0, len, start);
@@ -3797,9 +4003,9 @@ function gatherStoreStats(store, stateRoot, project) {
3797
4003
  for (const r of store.list(project))
3798
4004
  byState[r.state] = (byState[r.state] ?? 0) + 1;
3799
4005
  let corruptCount = 0;
3800
- const dir = join9(stateRoot, project);
4006
+ const dir = join10(stateRoot, project);
3801
4007
  try {
3802
- for (const n of readdirSync2(dir)) {
4008
+ for (const n of readdirSync3(dir)) {
3803
4009
  if (n.includes(".corrupt.")) {
3804
4010
  corruptCount++;
3805
4011
  continue;
@@ -3807,7 +4013,7 @@ function gatherStoreStats(store, stateRoot, project) {
3807
4013
  if (!n.endsWith(".json"))
3808
4014
  continue;
3809
4015
  try {
3810
- JSON.parse(readFileSync8(join9(dir, n), "utf-8"));
4016
+ JSON.parse(readFileSync9(join10(dir, n), "utf-8"));
3811
4017
  } catch {
3812
4018
  corruptCount++;
3813
4019
  }
@@ -3820,9 +4026,9 @@ function gatherResults(resultsDir) {
3820
4026
  let fileCount = 0;
3821
4027
  let totalBytes = 0;
3822
4028
  try {
3823
- for (const n of readdirSync2(resultsDir)) {
4029
+ for (const n of readdirSync3(resultsDir)) {
3824
4030
  try {
3825
- const s = statSync2(join9(resultsDir, n));
4031
+ const s = statSync3(join10(resultsDir, n));
3826
4032
  if (s.isFile()) {
3827
4033
  fileCount++;
3828
4034
  totalBytes += s.size;
@@ -3842,7 +4048,7 @@ var init_snapshot_gather = __esm({
3842
4048
  });
3843
4049
 
3844
4050
  // packages/core/dist/daemon/start.js
3845
- import { join as join10, dirname as dirname4 } from "path";
4051
+ import { join as join11, dirname as dirname4 } from "path";
3846
4052
  import { readdir } from "fs/promises";
3847
4053
  function startDaemon(ctx, opts, pkgVersion) {
3848
4054
  const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
@@ -3917,7 +4123,7 @@ function startDaemon(ctx, opts, pkgVersion) {
3917
4123
  return out;
3918
4124
  }
3919
4125
  async function gatherSnapshotInputs(now) {
3920
- const logPath2 = join10(dirname4(stateRoot), "squadrantd.log");
4126
+ const logPath2 = join11(dirname4(stateRoot), "squadrantd.log");
3921
4127
  const tier2Projects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
3922
4128
  const projects = await Promise.all(tier2Projects.map(async (project) => {
3923
4129
  const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER2 });
@@ -4051,7 +4257,7 @@ function startDaemon(ctx, opts, pkgVersion) {
4051
4257
  };
4052
4258
  let rotationTimer;
4053
4259
  if (rotationInterval > 0) {
4054
- const inboxPath = join10(stateRoot, "inbox");
4260
+ const inboxPath = join11(stateRoot, "inbox");
4055
4261
  rotationTimer = setInterval(async () => {
4056
4262
  try {
4057
4263
  let entries;
@@ -4094,9 +4300,9 @@ function startDaemon(ctx, opts, pkgVersion) {
4094
4300
  }
4095
4301
  for (const kill of ctx.activeHeadlessKills)
4096
4302
  kill();
4097
- return new Promise((resolve3) => server.close(() => {
4303
+ return new Promise((resolve4) => server.close(() => {
4098
4304
  log(`exit-complete pid=${process.pid}`);
4099
- resolve3();
4305
+ resolve4();
4100
4306
  }));
4101
4307
  },
4102
4308
  tickDelivery: deliveryTick,
@@ -4247,8 +4453,8 @@ import { exec as nodeExec } from "child_process";
4247
4453
  async function reapCrewChildren(taskId, graceMs = 2e3, execFn = nodeExec) {
4248
4454
  const marker = `SQUADRANT_CREW_TASK_ID=${taskId}`;
4249
4455
  try {
4250
- const stdout = await new Promise((resolve3, reject) => {
4251
- 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));
4252
4458
  });
4253
4459
  const pids = [];
4254
4460
  for (const line of stdout.split("\n").slice(1)) {
@@ -4464,7 +4670,7 @@ function createIsCaptainAlive(sock) {
4464
4670
  };
4465
4671
  }
4466
4672
  function createLaunch(cliBin, log) {
4467
- return (project) => new Promise((resolve3, reject) => {
4673
+ return (project) => new Promise((resolve4, reject) => {
4468
4674
  execFile(process.execPath, [cliBin, "launch", project, "--headless"], { timeout: 3e4 }, (err, stdout, stderr) => {
4469
4675
  const output = capOutput(stdout ?? "", stderr ?? "");
4470
4676
  if (err) {
@@ -4474,7 +4680,7 @@ function createLaunch(cliBin, log) {
4474
4680
  }
4475
4681
  if (output !== "(no output)")
4476
4682
  log?.(`launch ${project}: ${output}`);
4477
- resolve3();
4683
+ resolve4();
4478
4684
  });
4479
4685
  });
4480
4686
  }
@@ -4622,10 +4828,10 @@ function findProjectByThread(stateRoot, threadId) {
4622
4828
  for (const [key, id] of Object.entries(s.topics)) {
4623
4829
  if (id !== threadId)
4624
4830
  continue;
4625
- const sep2 = key.indexOf("::");
4626
- if (sep2 === -1)
4831
+ const sep3 = key.indexOf("::");
4832
+ if (sep3 === -1)
4627
4833
  continue;
4628
- return { project: key.slice(0, sep2), scope: key.slice(sep2 + 2) };
4834
+ return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2) };
4629
4835
  }
4630
4836
  return null;
4631
4837
  }
@@ -5176,11 +5382,11 @@ var init_bridge = __esm({
5176
5382
 
5177
5383
  // packages/core/dist/restart-daemon.js
5178
5384
  import { execFileSync as execFileSync4 } from "child_process";
5179
- import { existsSync as existsSync8 } from "fs";
5180
- import { homedir as homedir6 } from "os";
5181
- 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";
5182
5388
  function defaultIsRunning() {
5183
- return existsSync8(DEFAULT_SOCK_PATH);
5389
+ return existsSync9(DEFAULT_SOCK_PATH);
5184
5390
  }
5185
5391
  function defaultRunKickstart() {
5186
5392
  const uid = process.getuid?.() ?? 0;
@@ -5210,7 +5416,7 @@ var DEFAULT_SOCK_PATH;
5210
5416
  var init_restart_daemon = __esm({
5211
5417
  "packages/core/dist/restart-daemon.js"() {
5212
5418
  init_launchd();
5213
- DEFAULT_SOCK_PATH = join11(homedir6(), ".config", "squadrant", "squadrant.sock");
5419
+ DEFAULT_SOCK_PATH = join12(homedir7(), ".config", "squadrant", "squadrant.sock");
5214
5420
  }
5215
5421
  });
5216
5422
 
@@ -5310,8 +5516,8 @@ function runTelegramStatus(opts) {
5310
5516
  const env = opts.env ?? process.env;
5311
5517
  const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
5312
5518
  const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
5313
- const sep2 = key.indexOf("::");
5314
- 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 };
5315
5521
  });
5316
5522
  return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
5317
5523
  }
@@ -5335,8 +5541,8 @@ function runTelegramNotifyStatus(opts) {
5335
5541
  const s = loadState(opts.stateRoot);
5336
5542
  const projects = /* @__PURE__ */ new Set();
5337
5543
  for (const key of Object.keys(s.topics)) {
5338
- const sep2 = key.indexOf("::");
5339
- projects.add(sep2 === -1 ? key : key.slice(0, sep2));
5544
+ const sep3 = key.indexOf("::");
5545
+ projects.add(sep3 === -1 ? key : key.slice(0, sep3));
5340
5546
  }
5341
5547
  for (const p of Object.keys(s.notify))
5342
5548
  projects.add(p);
@@ -5434,8 +5640,8 @@ var init_crew_routing = __esm({
5434
5640
 
5435
5641
  // packages/core/dist/group-dispatch.js
5436
5642
  import { randomUUID as randomUUID3 } from "crypto";
5437
- import { homedir as homedir7 } from "os";
5438
- import { join as join12 } from "path";
5643
+ import { homedir as homedir8 } from "os";
5644
+ import { join as join13 } from "path";
5439
5645
  function resolveCurrentProject(config) {
5440
5646
  const cwd = process.cwd();
5441
5647
  for (const [name, proj] of Object.entries(config.projects)) {
@@ -5512,7 +5718,7 @@ var init_group_dispatch = __esm({
5512
5718
  "packages/core/dist/group-dispatch.js"() {
5513
5719
  init_dist();
5514
5720
  init_protocol();
5515
- DEFAULT_SOCK_PATH2 = join12(homedir7(), ".config", "squadrant", "squadrant.sock");
5721
+ DEFAULT_SOCK_PATH2 = join13(homedir8(), ".config", "squadrant", "squadrant.sock");
5516
5722
  GROUP_DISPATCH_WARMUP_TIMEOUT_MS = 12e4;
5517
5723
  GROUP_DISPATCH_WARMUP_POLL_MS = 1e3;
5518
5724
  }
@@ -5593,7 +5799,8 @@ async function launchOneWorkspace(opts) {
5593
5799
  forceFresh = true;
5594
5800
  }
5595
5801
  }
5596
- const agentCmd = opts.agentCmdFactory(forceFresh);
5802
+ const builtCmd = opts.agentCmdFactory(forceFresh);
5803
+ const agentCmd = opts.role === "captain" ? `SQUADRANT_ROLE=captain ${builtCmd}` : builtCmd;
5597
5804
  recordSession(opts.workspaceName, opts.role, {
5598
5805
  sessionsPath: opts.sessionsPath,
5599
5806
  templatesDir: opts.templatesDir
@@ -6116,12 +6323,14 @@ __export(dist_exports2, {
6116
6323
  IDLE_DEBOUNCE_MS: () => IDLE_DEBOUNCE_MS,
6117
6324
  LABEL: () => LABEL,
6118
6325
  MONITOR_STALL_BUDGET_MS: () => MONITOR_STALL_BUDGET_MS,
6326
+ OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
6119
6327
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
6120
6328
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
6121
6329
  STALE_THRESHOLD_MS: () => STALE_THRESHOLD_MS,
6122
6330
  TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
6123
6331
  TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
6124
6332
  TOOL_STALL_BUDGET_MS: () => TOOL_STALL_BUDGET_MS,
6333
+ WORK_ITEM_TTL_MS: () => WORK_ITEM_TTL_MS,
6125
6334
  WRITABLE_CONFIG_KEYS: () => WRITABLE_CONFIG_KEYS,
6126
6335
  _resetRestartInFlightForTest: () => _resetRestartInFlightForTest,
6127
6336
  ageText: () => ageText,
@@ -6137,6 +6346,7 @@ __export(dist_exports2, {
6137
6346
  capAllowed: () => capAllowed,
6138
6347
  capOutput: () => capOutput,
6139
6348
  classifyHealth: () => classifyHealth,
6349
+ closeWorkItem: () => closeWorkItem,
6140
6350
  computeTemplateHash: () => computeTemplateHash,
6141
6351
  createAttach: () => createAttach,
6142
6352
  createCrewPaneReader: () => createCrewPaneReader,
@@ -6154,6 +6364,8 @@ __export(dist_exports2, {
6154
6364
  createSurfaceLivenessProbe: () => createSurfaceLivenessProbe,
6155
6365
  createTelegramBridge: () => createTelegramBridge,
6156
6366
  createTelegramClient: () => createTelegramClient,
6367
+ createWorkItem: () => createWorkItem,
6368
+ createWorkStore: () => createWorkStore,
6157
6369
  crewPaneTitle: () => crewPaneTitle,
6158
6370
  crewTag: () => crewTag,
6159
6371
  daemonEntryPath: () => daemonEntryPath,
@@ -6161,6 +6373,7 @@ __export(dist_exports2, {
6161
6373
  decodeFrames: () => decodeFrames,
6162
6374
  defaultIsPidAlive: () => defaultIsPidAlive,
6163
6375
  defaultListenError: () => defaultListenError,
6376
+ defaultWorkRoot: () => defaultWorkRoot,
6164
6377
  deliverStartupPrompt: () => deliverStartupPrompt,
6165
6378
  deliverable: () => deliverable,
6166
6379
  deriveCaptainState: () => deriveCaptainState,
@@ -6172,7 +6385,9 @@ __export(dist_exports2, {
6172
6385
  encodeMsg: () => encodeMsg,
6173
6386
  ensureDaemon: () => ensureDaemon,
6174
6387
  evaluateStall: () => evaluateStall,
6388
+ findOpenChildren: () => findOpenChildren,
6175
6389
  findProjectByThread: () => findProjectByThread,
6390
+ findWorkItemById: () => findWorkItemById,
6176
6391
  formatInbound: () => formatInbound,
6177
6392
  formatLifecycle: () => formatLifecycle,
6178
6393
  healCmdFor: () => healCmdFor,
@@ -6184,6 +6399,7 @@ __export(dist_exports2, {
6184
6399
  isCrewTitle: () => isCrewTitle,
6185
6400
  isDaemonSocketLive: () => isDaemonSocketLive,
6186
6401
  isNotifyActive: () => isNotifyActive,
6402
+ isOperatorInitiatedCommand: () => isOperatorInitiatedCommand,
6187
6403
  isSideTitle: () => isSideTitle,
6188
6404
  isStickyAttention: () => isStickyAttention,
6189
6405
  isTurnAccepted: () => isTurnAccepted,
@@ -6203,6 +6419,7 @@ __export(dist_exports2, {
6203
6419
  plistPath: () => plistPath,
6204
6420
  programArgsBlock: () => programArgsBlock,
6205
6421
  projectHealth: () => projectHealth,
6422
+ purgeExpiredWorkItems: () => purgeExpiredWorkItems,
6206
6423
  readCursor: () => readCursor,
6207
6424
  readFromCursor: () => readFromCursor,
6208
6425
  reapCrewChildren: () => reapCrewChildren,
@@ -6214,6 +6431,7 @@ __export(dist_exports2, {
6214
6431
  reduceLifecycle: () => reduceLifecycle,
6215
6432
  releaseDaemonLock: () => releaseDaemonLock,
6216
6433
  renderPlist: () => renderPlist,
6434
+ reregisterDaemon: () => reregisterDaemon,
6217
6435
  resolveAgentBinDirs: () => resolveAgentBinDirs,
6218
6436
  resolveCrewRoute: () => resolveCrewRoute,
6219
6437
  resolveCurrentProject: () => resolveCurrentProject,
@@ -6278,6 +6496,7 @@ var init_dist2 = __esm({
6278
6496
  init_liveness2();
6279
6497
  init_watchdog();
6280
6498
  init_store();
6499
+ init_work_store();
6281
6500
  init_snapshot();
6282
6501
  init_launchd();
6283
6502
  init_crew_pane_reader();
@@ -6317,7 +6536,7 @@ function cmuxLocal(args) {
6317
6536
  }).trim();
6318
6537
  }
6319
6538
  function cmux(args) {
6320
- return new Promise((resolve3, reject) => {
6539
+ return new Promise((resolve4, reject) => {
6321
6540
  execFile2(
6322
6541
  resolveCmuxBin(),
6323
6542
  args,
@@ -6331,19 +6550,19 @@ function cmux(args) {
6331
6550
  reject(err.code === "ETIMEDOUT" ? new CmuxTimeoutError(args.join(" ")) : err);
6332
6551
  return;
6333
6552
  }
6334
- resolve3(stdout.trim());
6553
+ resolve4(stdout.trim());
6335
6554
  }
6336
6555
  );
6337
6556
  });
6338
6557
  }
6339
6558
  function cmuxStdin(args, input) {
6340
- return new Promise((resolve3, reject) => {
6559
+ return new Promise((resolve4, reject) => {
6341
6560
  const child = execFile2(resolveCmuxBin(), args, { encoding: "utf-8", timeout: CMUX_TIMEOUT, env: { ...process.env, CMUX_QUIET: "1" } }, (err, stdout) => {
6342
6561
  if (err) {
6343
6562
  reject(err.code === "ETIMEDOUT" ? new CmuxTimeoutError(args.join(" ")) : err);
6344
6563
  return;
6345
6564
  }
6346
- resolve3(stdout.trim());
6565
+ resolve4(stdout.trim());
6347
6566
  });
6348
6567
  child.stdin.end(input);
6349
6568
  });
@@ -6950,7 +7169,7 @@ var init_notifiers = __esm({
6950
7169
 
6951
7170
  // packages/workspaces/dist/workspaces/obsidian.js
6952
7171
  import fs15 from "fs/promises";
6953
- import { existsSync as existsSync9 } from "fs";
7172
+ import { existsSync as existsSync10 } from "fs";
6954
7173
  import path12 from "path";
6955
7174
  function resolveInRoot(root, relative) {
6956
7175
  const joined = path12.resolve(root, relative);
@@ -6970,7 +7189,7 @@ function createObsidianDriver(scope) {
6970
7189
  async probe() {
6971
7190
  return {
6972
7191
  installed: true,
6973
- rootExists: existsSync9(root)
7192
+ rootExists: existsSync10(root)
6974
7193
  };
6975
7194
  },
6976
7195
  async read(rel) {
@@ -7125,12 +7344,12 @@ var init_events_bridge = __esm({
7125
7344
  continue;
7126
7345
  }
7127
7346
  this.child = child;
7128
- await new Promise((resolve3) => {
7347
+ await new Promise((resolve4) => {
7129
7348
  let settled = false;
7130
7349
  const done = () => {
7131
7350
  if (!settled) {
7132
7351
  settled = true;
7133
- resolve3();
7352
+ resolve4();
7134
7353
  }
7135
7354
  };
7136
7355
  child.stdout?.on("data", (b) => this.onData(b));
@@ -7261,9 +7480,9 @@ var init_store_fingerprint = __esm({
7261
7480
  });
7262
7481
 
7263
7482
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
7264
- import { readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
7265
- import { join as join13 } from "path";
7266
- 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";
7267
7486
  var DaemonCmux;
7268
7487
  var init_daemon_cmux = __esm({
7269
7488
  "packages/workspaces/dist/cmux-daemon/daemon-cmux.js"() {
@@ -7326,24 +7545,24 @@ var init_daemon_cmux = __esm({
7326
7545
  * file failed to read/parse — see the class doc above.
7327
7546
  */
7328
7547
  async liveness() {
7329
- 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");
7330
7549
  const projects = loadConfig().projects;
7331
7550
  let files;
7332
7551
  try {
7333
- 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"));
7334
7553
  } catch (e) {
7335
7554
  throw new Error(`liveness: could not read cmux state dir ${dir}: ${e.message}`);
7336
7555
  }
7337
- return readLivenessSnapshot(files, (f) => readFileSync9(join13(dir, f), "utf-8"), projects);
7556
+ return readLivenessSnapshot(files, (f) => readFileSync10(join14(dir, f), "utf-8"), projects);
7338
7557
  }
7339
7558
  };
7340
7559
  }
7341
7560
  });
7342
7561
 
7343
7562
  // packages/workspaces/dist/cmux-daemon/cmux-store-source.js
7344
- import { join as join14 } from "path";
7345
- import { homedir as homedir9 } from "os";
7346
- 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";
7347
7566
  function parseLifecycleState(s) {
7348
7567
  if (s === "running" || s === "idle" || s === "needsInput" || s === "unknown") {
7349
7568
  return s;
@@ -7360,14 +7579,14 @@ function defaultIsPidAlive2(pid) {
7360
7579
  }
7361
7580
  function defaultListFiles(dir) {
7362
7581
  try {
7363
- 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"));
7364
7583
  } catch {
7365
7584
  return [];
7366
7585
  }
7367
7586
  }
7368
- function defaultReadFile(path30) {
7587
+ function defaultReadFile(path34) {
7369
7588
  try {
7370
- return readFileSync10(path30, "utf-8");
7589
+ return readFileSync11(path34, "utf-8");
7371
7590
  } catch {
7372
7591
  return void 0;
7373
7592
  }
@@ -7403,12 +7622,12 @@ var init_cmux_store_source = __esm({
7403
7622
  active = false;
7404
7623
  lastError = null;
7405
7624
  constructor(opts = {}) {
7406
- 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");
7407
7626
  this.debounceMs = opts.debounceMs ?? 50;
7408
7627
  this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
7409
7628
  this.listFiles = opts.listFiles ?? defaultListFiles;
7410
7629
  this.readFile = opts.readFile ?? defaultReadFile;
7411
- this.fileExists = opts.fileExists ?? existsSync10;
7630
+ this.fileExists = opts.fileExists ?? existsSync11;
7412
7631
  this.watchDir = opts.watchDir ?? defaultWatchDir;
7413
7632
  this.scheduleTimer = opts.scheduleTimer ?? setTimeout;
7414
7633
  this.cancelTimer = opts.cancelTimer ?? clearTimeout;
@@ -7465,7 +7684,7 @@ var init_cmux_store_source = __esm({
7465
7684
  }
7466
7685
  scanFile(filename) {
7467
7686
  const deps = this.deps;
7468
- const filePath = join14(this.stateDir, filename);
7687
+ const filePath = join15(this.stateDir, filename);
7469
7688
  const lockPath = `${filePath}.lock`;
7470
7689
  if (this.fileExists(lockPath)) {
7471
7690
  this.log(`cmux-store: skipping ${filename} (locked)`);
@@ -7519,11 +7738,11 @@ var init_cmux_store_source = __esm({
7519
7738
  });
7520
7739
 
7521
7740
  // packages/workspaces/dist/native-hooks/native-hook-source.js
7522
- import { join as join15 } from "path";
7523
- import { homedir as homedir10 } from "os";
7524
- 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";
7525
7744
  function installClaudeHooks(opts = {}) {
7526
- const settingsPath = opts.settingsPath ?? join15(homedir10(), ".claude", "settings.json");
7745
+ const settingsPath = opts.settingsPath ?? join16(homedir11(), ".claude", "settings.json");
7527
7746
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
7528
7747
  const readFile6 = opts.readFile ?? defaultReadFile2;
7529
7748
  const writeFile5 = opts.writeFile ?? defaultWriteFile;
@@ -7617,16 +7836,16 @@ function extractDetail(sub, payload) {
7617
7836
  }
7618
7837
  return void 0;
7619
7838
  }
7620
- function defaultReadFile2(path30) {
7839
+ function defaultReadFile2(path34) {
7621
7840
  try {
7622
- return readFileSync11(path30, "utf-8");
7841
+ return readFileSync12(path34, "utf-8");
7623
7842
  } catch {
7624
7843
  return void 0;
7625
7844
  }
7626
7845
  }
7627
- function defaultWriteFile(path30, content) {
7628
- mkdirSync6(path30.replace(/\/[^/]+$/, ""), { recursive: true });
7629
- writeFileSync8(path30, content, "utf-8");
7846
+ function defaultWriteFile(path34, content) {
7847
+ mkdirSync7(path34.replace(/\/[^/]+$/, ""), { recursive: true });
7848
+ writeFileSync9(path34, content, "utf-8");
7630
7849
  }
7631
7850
  var CLAUDE_HOOK_EVENTS, DEFAULT_HOOK_CMD, NativeHookSource;
7632
7851
  var init_native_hook_source = __esm({
@@ -7736,13 +7955,13 @@ async function settleInputBox(runtime, pane) {
7736
7955
  return sawContent;
7737
7956
  }
7738
7957
  function getFreePort() {
7739
- return new Promise((resolve3, reject) => {
7958
+ return new Promise((resolve4, reject) => {
7740
7959
  const srv = net.createServer();
7741
7960
  srv.once("error", reject);
7742
7961
  srv.listen(0, "127.0.0.1", () => {
7743
7962
  const addr = srv.address();
7744
7963
  const port = typeof addr === "object" && addr ? addr.port : 0;
7745
- 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")));
7746
7965
  });
7747
7966
  });
7748
7967
  }
@@ -7985,7 +8204,8 @@ function createClaudeDriver() {
7985
8204
  if (opts.settingsPath) {
7986
8205
  cmd += ` --settings ${opts.settingsPath}`;
7987
8206
  }
7988
- 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}`;
7989
8209
  cmd += ` --plugin-dir ${pluginDir}`;
7990
8210
  if (!opts.interactive) {
7991
8211
  cmd += ` -p "${opts.prompt.replace(/"/g, '\\"')}"`;
@@ -8407,8 +8627,8 @@ ${MARKER_END}
8407
8627
  const startIdx = existing.indexOf(MARKER_START);
8408
8628
  const endIdx = existing.indexOf(MARKER_END);
8409
8629
  if (startIdx === -1 && endIdx === -1) {
8410
- const sep2 = existing.endsWith("\n") ? "\n" : "\n\n";
8411
- return `${existing}${sep2}${block}`;
8630
+ const sep3 = existing.endsWith("\n") ? "\n" : "\n\n";
8631
+ return `${existing}${sep3}${block}`;
8412
8632
  }
8413
8633
  if (startIdx === -1 || endIdx === -1) {
8414
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.`);
@@ -8739,8 +8959,8 @@ var init_app_server_client = __esm({
8739
8959
  const info = this.opts.clientInfo ?? { name: "squadrant", version: "0" };
8740
8960
  const id = this.nextId++;
8741
8961
  const env = { jsonrpc: "2.0", id, method: "initialize", params: { clientInfo: info } };
8742
- const res = await new Promise((resolve3, reject) => {
8743
- this.pending.set(id, { resolve: resolve3, reject });
8962
+ const res = await new Promise((resolve4, reject) => {
8963
+ this.pending.set(id, { resolve: resolve4, reject });
8744
8964
  this.proc.stdin.write(JSON.stringify(env) + "\n");
8745
8965
  });
8746
8966
  this.proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "initialized" }) + "\n");
@@ -8773,13 +8993,13 @@ var init_app_server_client = __esm({
8773
8993
  const turnId = ack?.turn?.id;
8774
8994
  if (typeof turnId !== "string")
8775
8995
  throw new Error(`turn/start: unexpected ack shape (no turn.id): ${JSON.stringify(ack).slice(0, 200)}`);
8776
- return new Promise((resolve3, reject) => {
8996
+ return new Promise((resolve4, reject) => {
8777
8997
  const onNote = (n) => {
8778
8998
  if (n.params?.turn?.id !== turnId)
8779
8999
  return;
8780
9000
  if (n.method === "turn/completed") {
8781
9001
  cleanup();
8782
- resolve3({ turnId });
9002
+ resolve4({ turnId });
8783
9003
  }
8784
9004
  if (n.method === "turn/failed") {
8785
9005
  cleanup();
@@ -8830,8 +9050,8 @@ var init_app_server_client = __esm({
8830
9050
  throw new Error("AppServerClient not started");
8831
9051
  const id = this.nextId++;
8832
9052
  const env = { jsonrpc: "2.0", id, method, params: params ?? {} };
8833
- return new Promise((resolve3, reject) => {
8834
- this.pending.set(id, { resolve: resolve3, reject });
9053
+ return new Promise((resolve4, reject) => {
9054
+ this.pending.set(id, { resolve: resolve4, reject });
8835
9055
  this.proc.stdin.write(JSON.stringify(env) + "\n");
8836
9056
  });
8837
9057
  }
@@ -8963,11 +9183,11 @@ var init_codex_app_server_source = __esm({
8963
9183
 
8964
9184
  // packages/agents/dist/codex/config.js
8965
9185
  import { readFile as readFile5 } from "fs/promises";
8966
- import { homedir as homedir12 } from "os";
8967
- import { join as join17 } from "path";
9186
+ import { homedir as homedir13 } from "os";
9187
+ import { join as join18 } from "path";
8968
9188
  async function resolveCodexModel() {
8969
- const home = process.env["CODEX_HOME"] ?? join17(homedir12(), ".codex");
8970
- const configPath = join17(home, "config.toml");
9189
+ const home = process.env["CODEX_HOME"] ?? join18(homedir13(), ".codex");
9190
+ const configPath = join18(home, "config.toml");
8971
9191
  let text;
8972
9192
  try {
8973
9193
  text = await readFile5(configPath, "utf8");
@@ -9082,11 +9302,11 @@ function buildCodexDeveloperInstructions(rec) {
9082
9302
  ${directive}` : directive;
9083
9303
  }
9084
9304
  function withTimeout(p, ms, msg) {
9085
- return new Promise((resolve3, reject) => {
9305
+ return new Promise((resolve4, reject) => {
9086
9306
  const t = setTimeout(() => reject(new Error(msg)), ms);
9087
9307
  p.then((v) => {
9088
9308
  clearTimeout(t);
9089
- resolve3(v);
9309
+ resolve4(v);
9090
9310
  }, (e) => {
9091
9311
  clearTimeout(t);
9092
9312
  reject(e);
@@ -9488,9 +9708,9 @@ var init_sse_bridge = __esm({
9488
9708
 
9489
9709
  // packages/agents/dist/interactive/claude.js
9490
9710
  import { execSync as execSync7 } from "child_process";
9491
- import { readFileSync as readFileSync12 } from "fs";
9492
- import { homedir as homedir13 } from "os";
9493
- 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";
9494
9714
  function probeClaudeSettingsFlag() {
9495
9715
  try {
9496
9716
  const help = execSync7("claude --help", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
@@ -9548,11 +9768,11 @@ function deriveTranscriptPath(sessionId, cwd) {
9548
9768
  if (!sessionId || !cwd)
9549
9769
  return null;
9550
9770
  const escaped = cwd.replace(/[^a-zA-Z0-9]/g, "-");
9551
- return join18(homedir13(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
9771
+ return join19(homedir14(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
9552
9772
  }
9553
9773
  function readLastAssistantText(transcriptPath) {
9554
9774
  try {
9555
- const raw = readFileSync12(transcriptPath, "utf-8");
9775
+ const raw = readFileSync13(transcriptPath, "utf-8");
9556
9776
  const lines = raw.split(/\r?\n/);
9557
9777
  for (let i = lines.length - 1; i >= 0; i--) {
9558
9778
  const line = lines[i].trim();
@@ -9594,8 +9814,8 @@ function resolveLastAssistantText(payload) {
9594
9814
  const derived = deriveTranscriptPath(p?.session_id, cwd);
9595
9815
  if (derived)
9596
9816
  candidates.push(derived);
9597
- for (const path30 of candidates) {
9598
- const text = readLastAssistantText(path30);
9817
+ for (const path34 of candidates) {
9818
+ const text = readLastAssistantText(path34);
9599
9819
  if (text != null)
9600
9820
  return text;
9601
9821
  }
@@ -9945,14 +10165,14 @@ function runHeadless(opts) {
9945
10165
  if (err.length > ERR_CAP)
9946
10166
  err = err.slice(err.length - ERR_CAP);
9947
10167
  });
9948
- const result = new Promise((resolve3) => {
10168
+ const result = new Promise((resolve4) => {
9949
10169
  child.once("error", (e) => {
9950
10170
  if (debounceTimer) {
9951
10171
  clearTimeout(debounceTimer);
9952
10172
  debounceTimer = null;
9953
10173
  }
9954
10174
  opts.emit({ type: "task.failed", id: opts.id, error: `spawn error: ${e.message}`, exitCode: void 0 });
9955
- resolve3();
10175
+ resolve4();
9956
10176
  });
9957
10177
  child.on("close", (code) => {
9958
10178
  if (chunksSinceProgress > 0)
@@ -9969,7 +10189,7 @@ function runHeadless(opts) {
9969
10189
  const ref = opts.writeResult ? opts.writeResult(opts.id, res.payload ?? "") : "";
9970
10190
  opts.emit({ type: "task.done", id: opts.id, resultRef: ref, parseWarning: res.parseWarning });
9971
10191
  }
9972
- resolve3();
10192
+ resolve4();
9973
10193
  });
9974
10194
  });
9975
10195
  return { result, kill: () => child.kill("SIGTERM") };
@@ -10049,8 +10269,8 @@ var require_daemon_exports = {};
10049
10269
  __export(require_daemon_exports, {
10050
10270
  requireDaemon: () => requireDaemon
10051
10271
  });
10052
- import { join as join24 } from "path";
10053
- import { homedir as homedir19 } from "os";
10272
+ import { join as join25 } from "path";
10273
+ import { homedir as homedir20 } from "os";
10054
10274
  async function requireDaemon(sockPath = DEFAULT_SOCK_PATH3) {
10055
10275
  const isLive = await isDaemonSocketLive(sockPath);
10056
10276
  if (!isLive) {
@@ -10061,18 +10281,18 @@ var DEFAULT_SOCK_PATH3;
10061
10281
  var init_require_daemon = __esm({
10062
10282
  "packages/cli/src/lib/require-daemon.ts"() {
10063
10283
  init_dist2();
10064
- DEFAULT_SOCK_PATH3 = join24(homedir19(), ".config", "squadrant", "squadrant.sock");
10284
+ DEFAULT_SOCK_PATH3 = join25(homedir20(), ".config", "squadrant", "squadrant.sock");
10065
10285
  }
10066
10286
  });
10067
10287
 
10068
10288
  // packages/cli/src/index.ts
10069
10289
  init_dist();
10070
10290
  init_dist2();
10071
- import { Command as Command32 } from "commander";
10072
- 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";
10073
10293
  import { fileURLToPath as fileURLToPath6 } from "url";
10074
- import { dirname as dirname9, join as join29 } from "path";
10075
- import { homedir as homedir21 } from "os";
10294
+ import { dirname as dirname9, join as join30 } from "path";
10295
+ import { homedir as homedir22 } from "os";
10076
10296
 
10077
10297
  // packages/cli/src/commands/doctor.ts
10078
10298
  init_dist();
@@ -10089,10 +10309,10 @@ import chalk3 from "chalk";
10089
10309
  // packages/cli/src/commands/health-view.ts
10090
10310
  init_dist2();
10091
10311
  init_dist2();
10092
- import { homedir as homedir11 } from "os";
10093
- import { join as join16 } from "path";
10312
+ import { homedir as homedir12 } from "os";
10313
+ import { join as join17 } from "path";
10094
10314
  import chalk2 from "chalk";
10095
- var SOCK = join16(homedir11(), ".config", "squadrant", "squadrant.sock");
10315
+ var SOCK = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
10096
10316
  async function queryHealth(project) {
10097
10317
  try {
10098
10318
  const reply = await sendRequest(SOCK, { kind: "health", project });
@@ -10431,9 +10651,9 @@ import chalk4 from "chalk";
10431
10651
 
10432
10652
  // packages/cli/src/lib/per-crew-settings.ts
10433
10653
  init_dist4();
10434
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
10435
- import { dirname as dirname5, join as join19 } from "path";
10436
- 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";
10437
10657
  var CREW_PERMISSION_ALLOWLIST = [
10438
10658
  // git — read + safe mutations (reset/clean/config intentionally excluded)
10439
10659
  "Bash(git status:*)",
@@ -10524,29 +10744,29 @@ function mergeCrewPermissions(settings) {
10524
10744
  return next;
10525
10745
  }
10526
10746
  function writePerCrewSettingsLocal(o) {
10527
- const dir = join19(o.projectCwd, ".claude");
10528
- mkdirSync7(dir, { recursive: true });
10529
- 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");
10530
10750
  let existing = {};
10531
10751
  try {
10532
- const raw = healStaleCockpitRefs(readFileSync13(file, "utf-8"));
10752
+ const raw = healStaleCockpitRefs(readFileSync14(file, "utf-8"));
10533
10753
  existing = JSON.parse(raw);
10534
10754
  } catch {
10535
10755
  }
10536
10756
  const withHooks = mergeClaudeHooks(existing, o.hookCmd ?? "squadrant crew _hook");
10537
10757
  const merged = mergeCrewPermissions(withHooks);
10538
- writeFileSync9(file, JSON.stringify(merged, null, 2));
10758
+ writeFileSync10(file, JSON.stringify(merged, null, 2));
10539
10759
  return file;
10540
10760
  }
10541
- 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");
10542
10762
  function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH) {
10543
- mkdirSync7(dirname5(configPath), { recursive: true });
10763
+ mkdirSync8(dirname5(configPath), { recursive: true });
10544
10764
  const defaultConfig = {
10545
10765
  $schema: "https://opencode.ai/config.json",
10546
10766
  model: "anthropic/claude-sonnet-4-5"
10547
10767
  };
10548
10768
  try {
10549
- writeFileSync9(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", { flag: "wx" });
10769
+ writeFileSync10(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", { flag: "wx" });
10550
10770
  return configPath;
10551
10771
  } catch (err) {
10552
10772
  if (err.code === "EEXIST") return null;
@@ -10554,9 +10774,9 @@ function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_
10554
10774
  }
10555
10775
  }
10556
10776
  function writePerCrewOpencodeConfig(o) {
10557
- const dir = join19(o.stateRoot, o.project, o.taskId);
10558
- mkdirSync7(dir, { recursive: true });
10559
- 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");
10560
10780
  const config = {
10561
10781
  permission: {
10562
10782
  read: "allow",
@@ -10571,7 +10791,7 @@ function writePerCrewOpencodeConfig(o) {
10571
10791
  external_directory: { "**": "allow" }
10572
10792
  }
10573
10793
  };
10574
- writeFileSync9(file, JSON.stringify(config, null, 2));
10794
+ writeFileSync10(file, JSON.stringify(config, null, 2));
10575
10795
  return file;
10576
10796
  }
10577
10797
 
@@ -10602,11 +10822,11 @@ function stepHeader(n, total, label) {
10602
10822
  ${n}/${total} ${label}`));
10603
10823
  }
10604
10824
  function promptLine(question) {
10605
- return new Promise((resolve3) => {
10825
+ return new Promise((resolve4) => {
10606
10826
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10607
10827
  rl.question(question, (answer) => {
10608
10828
  rl.close();
10609
- resolve3(answer.trim());
10829
+ resolve4(answer.trim());
10610
10830
  });
10611
10831
  });
10612
10832
  }
@@ -10934,48 +11154,22 @@ var projectsCommand = new Command3("projects").description("Manage registered pr
10934
11154
 
10935
11155
  // packages/cli/src/commands/status.ts
10936
11156
  init_dist();
10937
- init_dist3();
10938
11157
  import { Command as Command4 } from "commander";
10939
11158
  import chalk6 from "chalk";
10940
- import matter2 from "gray-matter";
10941
- function timeAgo(dateStr) {
10942
- if (!dateStr) return chalk6.dim("\u2014");
10943
- const date = new Date(dateStr);
10944
- if (isNaN(date.getTime())) return chalk6.dim("\u2014");
10945
- const diff = Date.now() - date.getTime();
10946
- const mins = Math.floor(diff / 6e4);
10947
- const hours = Math.floor(diff / 36e5);
10948
- const days = Math.floor(diff / 864e5);
10949
- if (mins < 1) return "just now";
10950
- if (mins < 60) return `${mins}m ago`;
10951
- if (hours < 24) return `${hours}h ago`;
10952
- return `${days}d ago`;
10953
- }
10954
- function progressBar(completed, total) {
10955
- if (total === 0) return chalk6.dim("no tasks");
10956
- const pct = Math.round(completed / total * 100);
10957
- const filled = Math.round(pct / 10);
10958
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
10959
- return `${bar} ${pct}%`;
10960
- }
10961
11159
  function captainIndicator(state) {
10962
11160
  if (state === "alive" || state === "stale") return chalk6.green("\u25CF");
10963
11161
  if (state === "stopped") return chalk6.magenta("\u23FB");
10964
11162
  if (state === void 0 || state === "unknown") return chalk6.dim("?");
10965
11163
  return chalk6.dim("\u25CB");
10966
11164
  }
10967
- function formatProjectRow(name, captainName, fm, statusMdState, captainState) {
11165
+ function formatProjectRow(name, captainName, captainState) {
10968
11166
  const sessionIndicator = captainIndicator(captainState);
10969
11167
  const captainDisplay = `${captainName.padEnd(11)} ${sessionIndicator}`;
10970
- const crew = statusMdState === "ok" ? String(fm.active_crew ?? 0).padEnd(6) : chalk6.dim("?").padEnd(6);
10971
- 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);
10972
- const updated = statusMdState === "ok" ? timeAgo(fm.last_updated) : chalk6.dim("\u2014");
10973
- return ` ${name.padEnd(18)} ${captainDisplay} ${crew} ${progress} ${updated}`;
11168
+ return ` ${name.padEnd(18)} ${captainDisplay}`;
10974
11169
  }
10975
- 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) => {
10976
11171
  const config = loadConfig();
10977
11172
  const projects = Object.entries(config.projects);
10978
- const registry = new WorkspaceRegistry({ obsidian: createObsidianDriver });
10979
11173
  if (projects.length === 0) {
10980
11174
  console.log(chalk6.yellow("\nNo projects registered. Use: squadrant projects add <name> <path>\n"));
10981
11175
  return;
@@ -10988,30 +11182,12 @@ var statusCommand = new Command4("status").description("Show status of all proje
10988
11182
  }
10989
11183
  }
10990
11184
  console.log(chalk6.bold("\nProject Status\n"));
10991
- console.log(
10992
- chalk6.dim(
10993
- ` ${"PROJECT".padEnd(18)} ${"CAPTAIN".padEnd(12)} ${"CREW".padEnd(6)} ${"PROGRESS".padEnd(25)} LAST UPDATE`
10994
- )
10995
- );
10996
- console.log(chalk6.dim(" " + "\u2500".repeat(85)));
11185
+ console.log(chalk6.dim(` ${"PROJECT".padEnd(18)} CAPTAIN`));
11186
+ console.log(chalk6.dim(" " + "\u2500".repeat(35)));
10997
11187
  for (const [name, project] of projects) {
10998
- const workspace = registry.forProject(name, config);
10999
- let fm = {};
11000
- let statusMdState = "missing";
11001
- if (await workspace.exists("status.md")) {
11002
- try {
11003
- const raw = await workspace.read("status.md");
11004
- fm = matter2(raw).data;
11005
- statusMdState = "ok";
11006
- } catch {
11007
- statusMdState = "unreadable";
11008
- }
11009
- }
11010
- console.log(
11011
- formatProjectRow(name, project.captainName, fm, statusMdState, captainStateByProject.get(name))
11012
- );
11188
+ console.log(formatProjectRow(name, project.captainName, captainStateByProject.get(name)));
11013
11189
  }
11014
- console.log("");
11190
+ console.log(chalk6.dim("\n Task/crew counts: no data source yet \u2014 see squadrant/squadrant#630\n"));
11015
11191
  if (opts.detailed) {
11016
11192
  printServiceHealth(health);
11017
11193
  }
@@ -11100,9 +11276,9 @@ import { Command as Command8 } from "commander";
11100
11276
  import { createConnection as createConnection3 } from "net";
11101
11277
  import { randomUUID as randomUUID4 } from "crypto";
11102
11278
  import { execFileSync as execFileSync6 } from "child_process";
11103
- import { homedir as homedir16 } from "os";
11104
- import { join as join21 } from "path";
11105
- 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";
11106
11282
 
11107
11283
  // packages/cli/src/commands/crew-output.ts
11108
11284
  function tailLines(text, maxLines = 40, maxBytes = 4096) {
@@ -11160,11 +11336,11 @@ init_dist2();
11160
11336
  import { Command as Command6 } from "commander";
11161
11337
  import chalk8 from "chalk";
11162
11338
  import { createConnection as createConnection2 } from "net";
11163
- import { homedir as homedir15 } from "os";
11164
- import { join as join20 } from "path";
11339
+ import { homedir as homedir16 } from "os";
11340
+ import { join as join21 } from "path";
11165
11341
  import { createInterface } from "readline";
11166
11342
  function socketPath() {
11167
- return process.env.SQUADRANTD_SOCK ?? join20(homedir15(), ".config", "squadrant", "squadrant.sock");
11343
+ return process.env.SQUADRANTD_SOCK ?? join21(homedir16(), ".config", "squadrant", "squadrant.sock");
11168
11344
  }
11169
11345
  function rule(width, ch = "\u2500") {
11170
11346
  return ch.repeat(Math.max(0, width));
@@ -11400,11 +11576,11 @@ var crewChatCommand = new Command7("chat").description("[DEPRECATED] alias for `
11400
11576
  });
11401
11577
 
11402
11578
  // packages/cli/src/commands/crew-control.ts
11403
- var SOCK2 = join21(homedir16(), ".config", "squadrant", "squadrant.sock");
11579
+ var SOCK2 = join22(homedir17(), ".config", "squadrant", "squadrant.sock");
11404
11580
  var CODEX_FIRST_TURN_DELAY_MS = 1500;
11405
11581
  async function sendCodexFirstTurn(taskId, text) {
11406
11582
  await new Promise((r) => setTimeout(r, CODEX_FIRST_TURN_DELAY_MS));
11407
- await new Promise((resolve3, reject) => {
11583
+ await new Promise((resolve4, reject) => {
11408
11584
  const conn = createConnection3(SOCK2);
11409
11585
  conn.setEncoding("utf-8");
11410
11586
  conn.on("data", () => {
@@ -11420,7 +11596,7 @@ async function sendCodexFirstTurn(taskId, text) {
11420
11596
  }
11421
11597
  setTimeout(() => {
11422
11598
  conn.end();
11423
- resolve3();
11599
+ resolve4();
11424
11600
  }, 100);
11425
11601
  });
11426
11602
  });
@@ -11507,10 +11683,10 @@ function buildSignalRequest(signal, o) {
11507
11683
  return { kind: "event", project, event };
11508
11684
  }
11509
11685
  function defaultWriteResult(id, payload) {
11510
- const dir = join21(homedir16(), ".config", "squadrant", "state", "_results");
11511
- mkdirSync8(dir, { recursive: true });
11512
- const file = join21(dir, `${id}.txt`);
11513
- 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);
11514
11690
  return file;
11515
11691
  }
11516
11692
  async function runCrewSignal(signal, o, deps) {
@@ -11912,11 +12088,11 @@ function parseCrewPick(raw, stats) {
11912
12088
  throw new Error(`Invalid selection '${raw}'. Enter a number 1-${stats.length} or a crew name.`);
11913
12089
  }
11914
12090
  function promptLine2(question) {
11915
- return new Promise((resolve3) => {
12091
+ return new Promise((resolve4) => {
11916
12092
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
11917
12093
  rl.question(question, (answer) => {
11918
12094
  rl.close();
11919
- resolve3(answer);
12095
+ resolve4(answer);
11920
12096
  });
11921
12097
  });
11922
12098
  }
@@ -12190,8 +12366,8 @@ init_dist();
12190
12366
  init_dist3();
12191
12367
  import { Command as Command12 } from "commander";
12192
12368
  import { execSync as execSync10 } from "child_process";
12193
- import { homedir as homedir18 } from "os";
12194
- import { join as join23 } from "path";
12369
+ import { homedir as homedir19 } from "os";
12370
+ import { join as join24 } from "path";
12195
12371
  import chalk13 from "chalk";
12196
12372
 
12197
12373
  // packages/web/dist/read-status.js
@@ -12404,9 +12580,9 @@ function mergeSnapshot(daemon, external, now) {
12404
12580
  // packages/web/dist/probes.js
12405
12581
  init_dist();
12406
12582
  init_dist();
12407
- import { join as join22 } from "path";
12408
- import { homedir as homedir17 } from "os";
12409
- 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";
12410
12586
  import { execFile as execFile4 } from "child_process";
12411
12587
  var DEFAULT_TIMEOUT_MS = 2e3;
12412
12588
  var AGENT_CLIS = ["claude", "codex", "gemini", "opencode"];
@@ -12442,7 +12618,7 @@ function vaultProbe(run, dir) {
12442
12618
  return { state: "unknown", detail: "no vault configured" };
12443
12619
  if (!run.pathExists(dir))
12444
12620
  return { state: "gone", detail: "vault directory missing" };
12445
- if (!run.pathExists(join22(dir, ".obsidian")))
12621
+ if (!run.pathExists(join23(dir, ".obsidian")))
12446
12622
  return { state: "gone", detail: "no .obsidian/ (not a vault)" };
12447
12623
  return { state: "alive" };
12448
12624
  } catch {
@@ -12510,27 +12686,27 @@ async function runExternalProbes(run, timeoutMs = DEFAULT_TIMEOUT_MS) {
12510
12686
  const sessions = probeSessions(run);
12511
12687
  return { cmux: cmux2, agentClis, vaults, config: { parseable, projectPaths, sessions } };
12512
12688
  }
12513
- var SESSIONS_PATH = join22(homedir17(), ".config", "squadrant", "sessions.json");
12689
+ var SESSIONS_PATH = join23(homedir18(), ".config", "squadrant", "sessions.json");
12514
12690
  function onPath(cli) {
12515
12691
  const dirs = (process.env.PATH ?? "").split(":").filter(Boolean);
12516
- return dirs.some((d) => existsSync11(join22(d, cli)));
12692
+ return dirs.some((d) => existsSync12(join23(d, cli)));
12517
12693
  }
12518
12694
  function readSessionsHashes() {
12519
- const raw = JSON.parse(readFileSync14(SESSIONS_PATH, "utf-8"));
12695
+ const raw = JSON.parse(readFileSync15(SESSIONS_PATH, "utf-8"));
12520
12696
  const hashes = Object.values(raw.workspaces ?? {}).map((w) => w.templateHash).filter((h) => typeof h === "string" && h.length > 0);
12521
12697
  return [...new Set(hashes)];
12522
12698
  }
12523
12699
  function defaultProbeRunners() {
12524
12700
  return {
12525
- probeCmuxBin: () => new Promise((resolve3) => {
12701
+ probeCmuxBin: () => new Promise((resolve4) => {
12526
12702
  try {
12527
- execFile4(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) => resolve3(!err));
12703
+ execFile4(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) => resolve4(!err));
12528
12704
  } catch {
12529
- resolve3(false);
12705
+ resolve4(false);
12530
12706
  }
12531
12707
  }),
12532
12708
  probeOnPath: async (cli) => onPath(cli),
12533
- pathExists: (p) => existsSync11(p),
12709
+ pathExists: (p) => existsSync12(p),
12534
12710
  loadConfig: () => loadConfig(),
12535
12711
  loadSessionsHashes: () => readSessionsHashes()
12536
12712
  };
@@ -13391,9 +13567,9 @@ async function startWebServer(opts) {
13391
13567
  }
13392
13568
  res.writeHead(404).end();
13393
13569
  });
13394
- await new Promise((resolve3, reject) => {
13570
+ await new Promise((resolve4, reject) => {
13395
13571
  server.once("error", reject);
13396
- server.listen(opts.port, host, resolve3);
13572
+ server.listen(opts.port, host, resolve4);
13397
13573
  });
13398
13574
  const addr = server.address();
13399
13575
  const boundPort = typeof addr === "object" && addr ? addr.port : opts.port;
@@ -13405,7 +13581,7 @@ async function startWebServer(opts) {
13405
13581
  timer.unref?.();
13406
13582
  return {
13407
13583
  port: boundPort,
13408
- close: () => new Promise((resolve3) => {
13584
+ close: () => new Promise((resolve4) => {
13409
13585
  clearInterval(timer);
13410
13586
  for (const res of clients) {
13411
13587
  try {
@@ -13414,14 +13590,14 @@ async function startWebServer(opts) {
13414
13590
  }
13415
13591
  }
13416
13592
  clients.clear();
13417
- server.close(() => resolve3());
13593
+ server.close(() => resolve4());
13418
13594
  })
13419
13595
  };
13420
13596
  }
13421
13597
 
13422
13598
  // packages/cli/src/commands/dashboard.ts
13423
13599
  init_dist();
13424
- var SOCK3 = join23(homedir18(), ".config", "squadrant", "squadrant.sock");
13600
+ var SOCK3 = join24(homedir19(), ".config", "squadrant", "squadrant.sock");
13425
13601
  function detectCurrentWorkspace2() {
13426
13602
  const out = execSync10(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
13427
13603
  const match = out.match(/workspace:\d+/);
@@ -13940,29 +14116,16 @@ init_dist();
13940
14116
  init_dist();
13941
14117
  init_dist3();
13942
14118
  import { Command as Command16 } from "commander";
13943
- import fs24 from "fs";
13944
- import path26 from "path";
13945
14119
  import chalk17 from "chalk";
13946
- import matter3 from "gray-matter";
13947
14120
  function getDateStr(yesterday) {
13948
14121
  return iso(daysAgo(yesterday ? 1 : 0));
13949
14122
  }
13950
14123
  async function getProjectStandup(name, project, dateStr, registry, config) {
13951
14124
  const workspace = registry.forProject(name, config);
13952
- const spokeVault = resolveHome(project.spokeVault);
13953
- const statusFile = path26.join(spokeVault, "status.md");
13954
- let status = {};
13955
- if (fs24.existsSync(statusFile)) {
13956
- try {
13957
- status = matter3(fs24.readFileSync(statusFile, "utf-8")).data;
13958
- } catch {
13959
- }
13960
- }
13961
14125
  const log = await readDailyLog(workspace, dateStr);
13962
14126
  const gitCommits = getGitCommits(project.path, dateStr);
13963
14127
  return {
13964
14128
  name,
13965
- status,
13966
14129
  dailyLog: log?.content ?? null,
13967
14130
  gitCommits,
13968
14131
  blockers: log?.blockers ?? []
@@ -13981,26 +14144,16 @@ ${header}
13981
14144
  }
13982
14145
  let hasBlockers = false;
13983
14146
  for (const s of standups) {
13984
- const tasksDone = s.status.tasks_completed ?? 0;
13985
- const tasksTotal = s.status.tasks_total ?? 0;
13986
- const tasksInProgress = s.status.tasks_in_progress ?? 0;
13987
14147
  if (!raw) {
13988
14148
  lines.push(chalk17.cyan.bold(`## ${s.name}`));
13989
14149
  } else {
13990
14150
  lines.push(`## ${s.name}`);
13991
14151
  }
13992
- if (s.gitCommits.length > 0 || tasksDone > 0) {
14152
+ if (s.gitCommits.length > 0) {
13993
14153
  lines.push(!raw ? chalk17.green("Done:") : "**Done:**");
13994
14154
  for (const commit of s.gitCommits) {
13995
14155
  lines.push(` - ${commit}`);
13996
14156
  }
13997
- if (tasksDone > 0 && s.gitCommits.length === 0) {
13998
- lines.push(` - ${tasksDone}/${tasksTotal} tasks completed`);
13999
- }
14000
- }
14001
- if (tasksInProgress > 0) {
14002
- lines.push(!raw ? chalk17.yellow("In Progress:") : "**In Progress:**");
14003
- lines.push(` - ${tasksInProgress} task(s) active`);
14004
14157
  }
14005
14158
  if (s.dailyLog) {
14006
14159
  const sections = ["Completed", "In Progress", "Tomorrow"];
@@ -14022,19 +14175,18 @@ ${header}
14022
14175
  lines.push(` - ${b}`);
14023
14176
  }
14024
14177
  }
14025
- if (s.gitCommits.length === 0 && tasksDone === 0 && !s.dailyLog) {
14178
+ if (s.gitCommits.length === 0 && !s.dailyLog) {
14026
14179
  lines.push(!raw ? chalk17.dim(" (no activity)") : " (no activity)");
14027
14180
  }
14028
14181
  lines.push("");
14029
14182
  }
14030
14183
  const totalCommits = standups.reduce((sum, s) => sum + s.gitCommits.length, 0);
14031
- const totalDone = standups.reduce((sum, s) => sum + (s.status.tasks_completed ?? 0), 0);
14032
14184
  if (!raw) {
14033
- 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) ---
14034
14186
  `));
14035
14187
  } else {
14036
14188
  lines.push(`---
14037
- *${totalCommits} commits, ${totalDone} tasks done${hasBlockers ? ", HAS BLOCKERS" : ""}*
14189
+ *${totalCommits} commits${hasBlockers ? ", HAS BLOCKERS" : ""} (task tracking: no data source \u2014 #630)*
14038
14190
  `);
14039
14191
  }
14040
14192
  return lines.join("\n");
@@ -14072,19 +14224,7 @@ init_dist();
14072
14224
  init_dist();
14073
14225
  init_dist3();
14074
14226
  import { Command as Command17 } from "commander";
14075
- import fs25 from "fs";
14076
- import path27 from "path";
14077
14227
  import chalk18 from "chalk";
14078
- import matter4 from "gray-matter";
14079
- function readStatus(spokeVault) {
14080
- const statusFile = path27.join(spokeVault, "status.md");
14081
- if (!fs25.existsSync(statusFile)) return {};
14082
- try {
14083
- return matter4(fs25.readFileSync(statusFile, "utf-8")).data;
14084
- } catch {
14085
- return {};
14086
- }
14087
- }
14088
14228
  function dedupe(items) {
14089
14229
  const seen = /* @__PURE__ */ new Set();
14090
14230
  const out = [];
@@ -14099,8 +14239,6 @@ function dedupe(items) {
14099
14239
  }
14100
14240
  async function getProjectRetro(name, project, fromStr, toStr, registry, config) {
14101
14241
  const workspace = registry.forProject(name, config);
14102
- const spokeVault = resolveHome(project.spokeVault);
14103
- const status = readStatus(spokeVault);
14104
14242
  const shipped = [];
14105
14243
  const inProgress = [];
14106
14244
  const blocked = [];
@@ -14130,9 +14268,7 @@ async function getProjectRetro(name, project, fromStr, toStr, registry, config)
14130
14268
  blocked: dedupe(blocked),
14131
14269
  decisions: dedupe(decisions),
14132
14270
  commits,
14133
- mergedPRs,
14134
- tasksCompletedNow: status.tasks_completed ?? 0,
14135
- tasksInProgressNow: status.tasks_in_progress ?? 0
14271
+ mergedPRs
14136
14272
  };
14137
14273
  }
14138
14274
  function renderList(lines, items, raw, label, color) {
@@ -14288,9 +14424,9 @@ async function runRuntimeSend(arg1, arg2, opts, confirmOpts) {
14288
14424
  const resolved = resolveTarget(registry, config, target, !!opts.command);
14289
14425
  await needRef(resolved);
14290
14426
  const finalProject = opts.command ? config.commandName : target;
14291
- const { join: join30, dirname: dirname10 } = await import("path");
14427
+ const { join: join31, dirname: dirname10 } = await import("path");
14292
14428
  const { DEFAULT_CONFIG_PATH: DEFAULT_CONFIG_PATH2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
14293
- const stateRoot = join30(dirname10(DEFAULT_CONFIG_PATH2), "state");
14429
+ const stateRoot = join31(dirname10(DEFAULT_CONFIG_PATH2), "state");
14294
14430
  const seq = await appendCaptainMessage2({
14295
14431
  stateRoot,
14296
14432
  project: finalProject,
@@ -14403,9 +14539,9 @@ workspaceCommand.command("read").description("Print the contents of a scope-rela
14403
14539
  const config = loadConfig();
14404
14540
  const registry = buildRegistry2();
14405
14541
  try {
14406
- const { projectTarget, path: path30 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14542
+ const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14407
14543
  const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
14408
- const content = await driver.read(path30);
14544
+ const content = await driver.read(path34);
14409
14545
  process.stdout.write(content);
14410
14546
  } catch (err) {
14411
14547
  console.error(chalk20.red(err.message));
@@ -14417,26 +14553,26 @@ workspaceCommand.command("write").description("Write content to a scope-relative
14417
14553
  const registry = buildRegistry2();
14418
14554
  try {
14419
14555
  let projectTarget;
14420
- let path30;
14556
+ let path34;
14421
14557
  let rawContent;
14422
14558
  if (opts.hub) {
14423
14559
  if (arg3 !== void 0) {
14424
14560
  throw new Error("With --hub, pass only the path and content");
14425
14561
  }
14426
14562
  projectTarget = void 0;
14427
- path30 = arg1;
14563
+ path34 = arg1;
14428
14564
  rawContent = arg2;
14429
14565
  } else {
14430
14566
  if (arg3 === void 0) {
14431
14567
  throw new Error("Missing content \u2014 usage: <project> <path> <content>");
14432
14568
  }
14433
14569
  projectTarget = arg1;
14434
- path30 = arg2;
14570
+ path34 = arg2;
14435
14571
  rawContent = arg3;
14436
14572
  }
14437
14573
  const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
14438
14574
  const payload = rawContent === "-" ? await readStdin() : rawContent;
14439
- await driver.write(path30, payload);
14575
+ await driver.write(path34, payload);
14440
14576
  } catch (err) {
14441
14577
  console.error(chalk20.red(err.message));
14442
14578
  process.exit(1);
@@ -14446,9 +14582,9 @@ workspaceCommand.command("list").description("List entries in a scope-relative d
14446
14582
  const config = loadConfig();
14447
14583
  const registry = buildRegistry2();
14448
14584
  try {
14449
- const { projectTarget, path: path30 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14585
+ const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14450
14586
  const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
14451
- const entries = await driver.list(path30);
14587
+ const entries = await driver.list(path34);
14452
14588
  for (const entry of entries) console.log(entry);
14453
14589
  } catch (err) {
14454
14590
  console.error(chalk20.red(err.message));
@@ -14459,9 +14595,9 @@ workspaceCommand.command("exists").description("Exit 0 if path exists, 1 if not"
14459
14595
  const config = loadConfig();
14460
14596
  const registry = buildRegistry2();
14461
14597
  try {
14462
- const { projectTarget, path: path30 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14598
+ const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14463
14599
  const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
14464
- const ok2 = await driver.exists(path30);
14600
+ const ok2 = await driver.exists(path34);
14465
14601
  process.exit(ok2 ? 0 : 1);
14466
14602
  } catch (err) {
14467
14603
  console.error(chalk20.red(err.message));
@@ -14472,9 +14608,9 @@ workspaceCommand.command("mkdir").description("Recursively create a scope-relati
14472
14608
  const config = loadConfig();
14473
14609
  const registry = buildRegistry2();
14474
14610
  try {
14475
- const { projectTarget, path: path30 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14611
+ const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
14476
14612
  const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
14477
- await driver.mkdir(path30);
14613
+ await driver.mkdir(path34);
14478
14614
  } catch (err) {
14479
14615
  console.error(chalk20.red(err.message));
14480
14616
  process.exit(1);
@@ -14513,8 +14649,8 @@ init_dist3();
14513
14649
  init_dist();
14514
14650
  import { Command as Command21 } from "commander";
14515
14651
  import chalk22 from "chalk";
14516
- import fs26 from "fs";
14517
- import path28 from "path";
14652
+ import fs24 from "fs";
14653
+ import path26 from "path";
14518
14654
  import { fileURLToPath as fileURLToPath4 } from "url";
14519
14655
  function parseScope(v) {
14520
14656
  if (v !== "user" && v !== "project") {
@@ -14523,10 +14659,10 @@ function parseScope(v) {
14523
14659
  return v;
14524
14660
  }
14525
14661
  function findPackageRoot3() {
14526
- let dir = path28.dirname(fileURLToPath4(import.meta.url));
14662
+ let dir = path26.dirname(fileURLToPath4(import.meta.url));
14527
14663
  while (dir !== "/" && dir !== "") {
14528
- if (fs26.existsSync(path28.join(dir, "package.json"))) return dir;
14529
- dir = path28.dirname(dir);
14664
+ if (fs24.existsSync(path26.join(dir, "package.json"))) return dir;
14665
+ dir = path26.dirname(dir);
14530
14666
  }
14531
14667
  return process.cwd();
14532
14668
  }
@@ -14650,7 +14786,7 @@ projectionCommand.command("list").description("List registered projection target
14650
14786
  // packages/cli/src/commands/codex-chat-smoke.ts
14651
14787
  init_dist4();
14652
14788
  import { Command as Command22 } from "commander";
14653
- import { resolve as resolve2 } from "path";
14789
+ import { resolve as resolve3 } from "path";
14654
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(
14655
14791
  "--approval",
14656
14792
  "include the approval round-trip (Phase 1 PASS requires this)",
@@ -14667,7 +14803,7 @@ var codexChatSmokeCommand = new Command22("codex-chat-smoke").description("Phase
14667
14803
  c.start();
14668
14804
  await c.initialize();
14669
14805
  const { threadId } = await c.startThread({
14670
- cwd: resolve2(opts.cwd),
14806
+ cwd: resolve3(opts.cwd),
14671
14807
  model: opts.model,
14672
14808
  sandbox: "workspace-write",
14673
14809
  // With --approval, force untrusted policy so codex requests approval
@@ -14688,7 +14824,7 @@ var codexChatSmokeCommand = new Command22("codex-chat-smoke").description("Phase
14688
14824
  pendingApprovals.push({ id: r.id, method: r.method });
14689
14825
  c.respondToServerRequest(r.id, { decision: "approve" });
14690
14826
  });
14691
- 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`);
14692
14828
  if (pendingApprovals.length === 0) {
14693
14829
  throw new Error("approval gate: expected at least one server-request (approval/input) during the turn");
14694
14830
  }
@@ -14721,12 +14857,12 @@ init_dist();
14721
14857
  init_dist();
14722
14858
  init_dist2();
14723
14859
  import { Command as Command23 } from "commander";
14724
- import fs27 from "fs";
14860
+ import fs25 from "fs";
14725
14861
  import { fileURLToPath as fileURLToPath5 } from "url";
14726
- import { dirname as dirname6, join as join25 } from "path";
14862
+ import { dirname as dirname6, join as join26 } from "path";
14727
14863
  import chalk23 from "chalk";
14728
14864
  function runConfigCheck(opts) {
14729
- const raw = JSON.parse(fs27.readFileSync(opts.configPath, "utf-8"));
14865
+ const raw = JSON.parse(fs25.readFileSync(opts.configPath, "utf-8"));
14730
14866
  const def = getDefaultConfig();
14731
14867
  const items = detectDrift(raw, def);
14732
14868
  let working = raw;
@@ -14743,7 +14879,7 @@ function runConfigCheck(opts) {
14743
14879
  stamped = true;
14744
14880
  }
14745
14881
  if (opts.fix || opts.accept || stamped) {
14746
- fs27.writeFileSync(opts.configPath, JSON.stringify(working, null, 2) + "\n");
14882
+ fs25.writeFileSync(opts.configPath, JSON.stringify(working, null, 2) + "\n");
14747
14883
  }
14748
14884
  return { items, applied, remaining, stamped };
14749
14885
  }
@@ -14814,7 +14950,7 @@ function printItems(items) {
14814
14950
  var configCommand = new Command23("config").description("Inspect and reconcile squadrant config");
14815
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) => {
14816
14952
  const pkgVersion = readPkgVersion2();
14817
- if (!fs27.existsSync(DEFAULT_CONFIG_PATH)) {
14953
+ if (!fs25.existsSync(DEFAULT_CONFIG_PATH)) {
14818
14954
  console.log(chalk23.yellow("No config found \u2014 run `squadrant init` first."));
14819
14955
  return;
14820
14956
  }
@@ -14859,8 +14995,8 @@ configCommand.command("set").description("Write a config value by dotted key (e.
14859
14995
  }
14860
14996
  });
14861
14997
  function readPkgVersion2() {
14862
- const pkgPath = join25(dirname6(fileURLToPath5(import.meta.url)), "..", "package.json");
14863
- 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;
14864
15000
  }
14865
15001
 
14866
15002
  // packages/cli/src/commands/heal.ts
@@ -14945,9 +15081,9 @@ var healCommand = new Command24("heal").description("Targeted, idempotent remedi
14945
15081
  process.exit(code);
14946
15082
  })
14947
15083
  ).addCommand(
14948
- 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 () => {
14949
15085
  const code = await runHealDaemon({
14950
- ensureDaemon: () => restartDaemonIfRunning({ reason: "heal", isRunning: () => true }),
15086
+ ensureDaemon: () => reregisterDaemon(),
14951
15087
  stdout: process.stdout,
14952
15088
  stderr: process.stderr
14953
15089
  });
@@ -15013,7 +15149,7 @@ var groupCommand = new Command26("group").description("Cross-project intra-group
15013
15149
  // packages/cli/src/commands/ping.ts
15014
15150
  init_dist();
15015
15151
  init_dist2();
15016
- import { join as join26, dirname as dirname7 } from "path";
15152
+ import { join as join27, dirname as dirname7 } from "path";
15017
15153
  import { Command as Command27 } from "commander";
15018
15154
  import chalk27 from "chalk";
15019
15155
  init_require_daemon();
@@ -15023,7 +15159,7 @@ async function runPing(project, message) {
15023
15159
  const resolved = resolveTarget(registry, config, project, false);
15024
15160
  await requireDaemon();
15025
15161
  await needRef(resolved);
15026
- const stateRoot = join26(dirname7(DEFAULT_CONFIG_PATH), "state");
15162
+ const stateRoot = join27(dirname7(DEFAULT_CONFIG_PATH), "state");
15027
15163
  await appendCaptainMessage({
15028
15164
  stateRoot,
15029
15165
  project,
@@ -15100,8 +15236,8 @@ var cmuxCommand = new Command28("cmux").description("cmux integration helpers").
15100
15236
  // packages/cli/src/commands/effort.ts
15101
15237
  init_dist();
15102
15238
  init_dist2();
15103
- import fs28 from "fs";
15104
- import path29 from "path";
15239
+ import fs26 from "fs";
15240
+ import path27 from "path";
15105
15241
  import { Command as Command29 } from "commander";
15106
15242
  import chalk29 from "chalk";
15107
15243
  var VALID_EFFORTS = ["max", "balance", "low"];
@@ -15144,9 +15280,9 @@ function effortScopeLabel(projectName) {
15144
15280
  }
15145
15281
  function canonical(p) {
15146
15282
  try {
15147
- return fs28.realpathSync(p);
15283
+ return fs26.realpathSync(p);
15148
15284
  } catch {
15149
- return path29.resolve(p);
15285
+ return path27.resolve(p);
15150
15286
  }
15151
15287
  }
15152
15288
  async function notifyCaptainsOfEffort(effort, config, driver, cwd = process.cwd(), append, scopeProject, projectConfigRoot) {
@@ -15196,7 +15332,7 @@ var effortCommand = new Command29("effort").description("Get or set the crew tok
15196
15332
  const config = loadConfig();
15197
15333
  const registry = new RuntimeRegistry2({ cmux: createCmuxDriver2() });
15198
15334
  const driver = registry.global(config);
15199
- const stateRoot = path29.join(path29.dirname(DEFAULT_CONFIG_PATH), "state");
15335
+ const stateRoot = path27.join(path27.dirname(DEFAULT_CONFIG_PATH), "state");
15200
15336
  const append = (project, text) => appendCaptainMessage({ stateRoot, project, text, source: "daemon" });
15201
15337
  await notifyCaptainsOfEffort(effort, config, driver, process.cwd(), append, options.project);
15202
15338
  } catch {
@@ -15204,18 +15340,308 @@ var effortCommand = new Command29("effort").description("Get or set the crew tok
15204
15340
  }
15205
15341
  });
15206
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
+
15207
15633
  // packages/cli/src/commands/telegram.ts
15208
15634
  init_dist();
15209
15635
  init_dist2();
15210
- import { join as join27, dirname as dirname8 } from "path";
15636
+ import { join as join28, dirname as dirname8 } from "path";
15211
15637
  import { emitKeypressEvents } from "readline";
15212
- import { Command as Command30 } from "commander";
15213
- import chalk30 from "chalk";
15638
+ import { Command as Command31 } from "commander";
15639
+ import chalk31 from "chalk";
15214
15640
  function defaultStateRoot() {
15215
- return join27(dirname8(DEFAULT_CONFIG_PATH), "state");
15641
+ return join28(dirname8(DEFAULT_CONFIG_PATH), "state");
15216
15642
  }
15217
15643
  async function questionMasked() {
15218
- return new Promise((resolve3) => {
15644
+ return new Promise((resolve4) => {
15219
15645
  emitKeypressEvents(process.stdin);
15220
15646
  process.stdin.setRawMode(true);
15221
15647
  process.stdin.resume();
@@ -15232,7 +15658,7 @@ async function questionMasked() {
15232
15658
  process.stdin.setRawMode(false);
15233
15659
  process.stdin.pause();
15234
15660
  process.stdout.write("\n");
15235
- resolve3(answer);
15661
+ resolve4(answer);
15236
15662
  } else if (key.name === "backspace") {
15237
15663
  if (answer.length > 0) {
15238
15664
  answer = answer.slice(0, -1);
@@ -15249,19 +15675,19 @@ async function questionMasked() {
15249
15675
  async function questionYesNo(prompt) {
15250
15676
  const { createInterface: createInterface2 } = await import("readline");
15251
15677
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
15252
- return new Promise((resolve3) => {
15678
+ return new Promise((resolve4) => {
15253
15679
  rl.question(prompt, (ans) => {
15254
15680
  rl.close();
15255
15681
  process.stdin.pause();
15256
- resolve3(/^y(es)?$/i.test(ans.trim()));
15682
+ resolve4(/^y(es)?$/i.test(ans.trim()));
15257
15683
  });
15258
15684
  });
15259
15685
  }
15260
- 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");
15261
15687
  telegramCommand.command("status").description("Show Telegram config and linked projects").action(() => {
15262
15688
  const { tokenSet, supergroupId, links } = runTelegramStatus({ config: loadConfig(), stateRoot: defaultStateRoot() });
15263
- console.log(`token: ${tokenSet ? chalk30.green("set") : chalk30.yellow("unset")}`);
15264
- 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")}`);
15265
15691
  if (links.length === 0) {
15266
15692
  console.log("no projects linked");
15267
15693
  return;
@@ -15271,32 +15697,32 @@ telegramCommand.command("status").description("Show Telegram config and linked p
15271
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) => {
15272
15698
  const cfg = loadConfig().telegram;
15273
15699
  if (!cfg) {
15274
- 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"));
15275
15701
  process.exit(1);
15276
15702
  }
15277
15703
  const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
15278
15704
  if (!token) {
15279
- 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"));
15280
15706
  process.exit(1);
15281
15707
  }
15282
15708
  const client = createTelegramClient({ token });
15283
15709
  const { topicId, created } = await runTelegramLink({ project, cfg, client, stateRoot: defaultStateRoot() });
15284
- 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}`));
15285
15711
  });
15286
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) => {
15287
15713
  if (!process.stdin.isTTY) {
15288
- 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"));
15289
15715
  process.exit(1);
15290
15716
  }
15291
15717
  console.log();
15292
- 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");
15293
15719
  console.log();
15294
15720
  console.log("Before you start you need:");
15295
15721
  console.log(" 1. A bot token from @BotFather (send /newbot)");
15296
15722
  console.log(" 2. A forum supergroup with the bot added as an admin (Topics enabled)");
15297
15723
  console.log(" 3. Bot privacy mode set to OFF (@BotFather \u2192 /setprivacy \u2192 Disable)");
15298
15724
  console.log();
15299
- console.log(chalk30.bold("Step 1/3 \u2014 Bot token"));
15725
+ console.log(chalk31.bold("Step 1/3 \u2014 Bot token"));
15300
15726
  const existingCfg = loadConfig().telegram;
15301
15727
  const existingToken = existingCfg?.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
15302
15728
  const decision = resolveSetupToken(existingToken, { resetToken: opts.resetToken ?? false });
@@ -15308,67 +15734,67 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
15308
15734
  try {
15309
15735
  botUser = await client.getMe();
15310
15736
  token = existingToken;
15311
- console.log(chalk30.green(`Using existing bot token (@${botUser.username})`));
15737
+ console.log(chalk31.green(`Using existing bot token (@${botUser.username})`));
15312
15738
  console.log();
15313
15739
  } catch {
15314
- 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."));
15315
15741
  console.log("Paste your bot token then press Enter (input is hidden):");
15316
15742
  token = await questionMasked();
15317
15743
  if (!token) {
15318
- console.error(chalk30.red("token required"));
15744
+ console.error(chalk31.red("token required"));
15319
15745
  process.exit(1);
15320
15746
  }
15321
15747
  client = createTelegramClient({ token });
15322
15748
  try {
15323
15749
  botUser = await client.getMe();
15324
15750
  } catch (e) {
15325
- console.error(chalk30.red(`token rejected: ${e.message}`));
15751
+ console.error(chalk31.red(`token rejected: ${e.message}`));
15326
15752
  process.exit(1);
15327
15753
  }
15328
- console.log(chalk30.green(`Connected as @${botUser.username}`));
15754
+ console.log(chalk31.green(`Connected as @${botUser.username}`));
15329
15755
  console.log();
15330
15756
  }
15331
15757
  } else {
15332
15758
  console.log("Paste your bot token then press Enter (input is hidden):");
15333
15759
  token = await questionMasked();
15334
15760
  if (!token) {
15335
- console.error(chalk30.red("token required"));
15761
+ console.error(chalk31.red("token required"));
15336
15762
  process.exit(1);
15337
15763
  }
15338
15764
  client = createTelegramClient({ token });
15339
15765
  try {
15340
15766
  botUser = await client.getMe();
15341
15767
  } catch (e) {
15342
- console.error(chalk30.red(`token rejected: ${e.message}`));
15768
+ console.error(chalk31.red(`token rejected: ${e.message}`));
15343
15769
  process.exit(1);
15344
15770
  }
15345
- console.log(chalk30.green(`Connected as @${botUser.username}`));
15771
+ console.log(chalk31.green(`Connected as @${botUser.username}`));
15346
15772
  console.log();
15347
15773
  }
15348
- console.log(chalk30.bold("Step 2/3 \u2014 Supergroup"));
15774
+ console.log(chalk31.bold("Step 2/3 \u2014 Supergroup"));
15349
15775
  const groupDecision = resolveSetupGroup(existingCfg?.supergroupId, { redetect: opts.redetect ?? false });
15350
15776
  let supergroupId;
15351
15777
  let detectedUserId;
15352
15778
  if (groupDecision === "reuse") {
15353
15779
  supergroupId = existingCfg.supergroupId;
15354
- console.log(chalk30.green(`Using existing group: ${supergroupId}`));
15780
+ console.log(chalk31.green(`Using existing group: ${supergroupId}`));
15355
15781
  console.log();
15356
15782
  } else {
15357
15783
  console.log("Add the bot to your forum supergroup, then send any message in it.");
15358
- 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"));
15359
15785
  try {
15360
15786
  ({ supergroupId, userId: detectedUserId } = await detectGroupAndUser(client, { timeoutMs: 6e4 }));
15361
15787
  } catch {
15362
- console.error(chalk30.red("Timed out \u2014 no supergroup message received within 60s."));
15363
- 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"));
15364
15790
  process.exit(1);
15365
15791
  }
15366
- console.log(chalk30.green(`Found group: ${supergroupId}`));
15792
+ console.log(chalk31.green(`Found group: ${supergroupId}`));
15367
15793
  console.log();
15368
15794
  }
15369
- console.log(chalk30.bold("Step 3/3 \u2014 Remote control + Save"));
15370
- console.log(chalk30.dim("Remote control enables auto-launching captains and the General command channel"));
15371
- 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)."));
15372
15798
  const finalUserId = resolveSetupUserId(opts.userId, detectedUserId, defaultStateRoot());
15373
15799
  let users;
15374
15800
  let remoteControl;
@@ -15382,32 +15808,32 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
15382
15808
  remoteControl = true;
15383
15809
  }
15384
15810
  } else if (groupDecision === "detect") {
15385
- console.log(chalk30.yellow("Could not read your user-id from that message \u2014 skipping remote control."));
15386
- 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."));
15387
15813
  printedRemoteControlState = true;
15388
15814
  } else {
15389
15815
  const existingUsers = existingCfg?.users;
15390
15816
  if (existingUsers && existingUsers.length > 0) {
15391
- 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.`));
15392
15818
  } else {
15393
- 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."));
15394
15820
  }
15395
15821
  printedRemoteControlState = true;
15396
15822
  }
15397
15823
  writeTelegramConfig(DEFAULT_CONFIG_PATH, { token, supergroupId, users, remoteControl });
15398
- 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}`));
15399
15825
  if (!printedRemoteControlState) {
15400
15826
  if (remoteControl) {
15401
- 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]})`));
15402
15828
  } else {
15403
- 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."));
15404
15830
  }
15405
15831
  }
15406
15832
  try {
15407
15833
  await runRegisterCommands({ client });
15408
- console.log(chalk30.dim("Registered the /command menu."));
15834
+ console.log(chalk31.dim("Registered the /command menu."));
15409
15835
  } catch (e) {
15410
- console.log(chalk30.yellow(`command-menu registration skipped: ${e.message}`));
15836
+ console.log(chalk31.yellow(`command-menu registration skipped: ${e.message}`));
15411
15837
  }
15412
15838
  const topics = loadState(defaultStateRoot()).topics;
15413
15839
  const topicEntries = Object.entries(topics);
@@ -15416,28 +15842,28 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
15416
15842
  const project = key.slice(0, key.indexOf("::"));
15417
15843
  return `${project}\u2192${id}`;
15418
15844
  }).join(", ");
15419
- 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)`));
15420
15846
  } else {
15421
- 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>"));
15422
15848
  }
15423
15849
  runTelegramPostSetup({});
15424
15850
  console.log();
15425
- console.log(`Next: ${chalk30.cyan("squadrant telegram link <project>")}`);
15851
+ console.log(`Next: ${chalk31.cyan("squadrant telegram link <project>")}`);
15426
15852
  });
15427
15853
  telegramCommand.command("register-commands").description("Register (or re-register) the bot's / command menu with Telegram").action(async () => {
15428
15854
  const cfg = loadConfig().telegram;
15429
15855
  if (!cfg) {
15430
- 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"));
15431
15857
  process.exit(1);
15432
15858
  }
15433
15859
  const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
15434
15860
  if (!token) {
15435
- 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"));
15436
15862
  process.exit(1);
15437
15863
  }
15438
15864
  const client = createTelegramClient({ token });
15439
15865
  await runRegisterCommands({ client });
15440
- console.log(chalk30.green(`registered ${BOT_COMMANDS.length} bot commands`));
15866
+ console.log(chalk31.green(`registered ${BOT_COMMANDS.length} bot commands`));
15441
15867
  });
15442
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) => {
15443
15869
  const stateRoot = defaultStateRoot();
@@ -15448,7 +15874,7 @@ telegramCommand.command("notify").argument("[project]", "project to toggle").arg
15448
15874
  return;
15449
15875
  }
15450
15876
  for (const r of rows) {
15451
- 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)")}`);
15452
15878
  }
15453
15879
  return;
15454
15880
  }
@@ -15457,53 +15883,53 @@ telegramCommand.command("notify").argument("[project]", "project to toggle").arg
15457
15883
  const token = tgCfg?.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
15458
15884
  if (state === "crew" || state === "cap") {
15459
15885
  if (value === void 0) {
15460
- console.error(chalk30.red(`usage: squadrant telegram notify <project> ${state} <value>`));
15886
+ console.error(chalk31.red(`usage: squadrant telegram notify <project> ${state} <value>`));
15461
15887
  process.exit(1);
15462
15888
  }
15463
15889
  const resolved2 = resolveNotify(globalNotify, loadProjectOverride(project));
15464
15890
  const before2 = { ...resolved2, active: isNotifyActive(stateRoot, project) };
15465
15891
  const res = runTelegramNotifyPref({ project, dimension: state, value });
15466
15892
  if (!res.ok) {
15467
- console.error(chalk30.red(res.message));
15893
+ console.error(chalk31.red(res.message));
15468
15894
  process.exit(1);
15469
15895
  }
15470
- console.log(chalk30.green(`${project} ${state} = ${value}`));
15896
+ console.log(chalk31.green(`${project} ${state} = ${value}`));
15471
15897
  const after2 = state === "crew" ? { ...before2, crew: value } : { ...before2, cap: value === "on" };
15472
15898
  if (tgCfg && token) {
15473
15899
  const client = createTelegramClient({ token });
15474
15900
  const sent = await runNotifyConfirmation({ project, before: before2, after: after2, cfg: tgCfg, client, stateRoot });
15475
- if (sent) console.log(chalk30.dim(`\u2192 notified ${project} topic`));
15901
+ if (sent) console.log(chalk31.dim(`\u2192 notified ${project} topic`));
15476
15902
  }
15477
15903
  return;
15478
15904
  }
15479
15905
  if (state !== "on" && state !== "off") {
15480
- 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>>"));
15481
15907
  process.exit(1);
15482
15908
  }
15483
15909
  const resolved = resolveNotify(globalNotify, loadProjectOverride(project));
15484
15910
  const before = { ...resolved, active: isNotifyActive(stateRoot, project) };
15485
15911
  const after = { ...before, active: state === "on" };
15486
15912
  runTelegramNotifySet({ project, active: state === "on", stateRoot });
15487
- console.log(chalk30.green(`${project} notifications ${state === "on" ? "ON" : "OFF"}`));
15913
+ console.log(chalk31.green(`${project} notifications ${state === "on" ? "ON" : "OFF"}`));
15488
15914
  if (tgCfg && token) {
15489
15915
  const client = createTelegramClient({ token });
15490
15916
  const sent = await runNotifyConfirmation({ project, before, after, cfg: tgCfg, client, stateRoot });
15491
- if (sent) console.log(chalk30.dim(`\u2192 notified ${project} topic`));
15917
+ if (sent) console.log(chalk31.dim(`\u2192 notified ${project} topic`));
15492
15918
  }
15493
15919
  });
15494
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) => {
15495
15921
  const cfg = loadConfig().telegram;
15496
15922
  if (!cfg) {
15497
- 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"));
15498
15924
  process.exit(1);
15499
15925
  }
15500
15926
  const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
15501
15927
  if (!token) {
15502
- 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"));
15503
15929
  process.exit(1);
15504
15930
  }
15505
15931
  if (!capAllowed(project, cfg.notify)) {
15506
- 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`));
15507
15933
  return;
15508
15934
  }
15509
15935
  let message;
@@ -15516,19 +15942,19 @@ telegramCommand.command("send").argument("<project>", "project whose topic recei
15516
15942
  for await (const line of rl) lines.push(line);
15517
15943
  message = lines.join("\n").trimEnd();
15518
15944
  if (!message) {
15519
- console.error(chalk30.red("no message provided (stdin was empty)"));
15945
+ console.error(chalk31.red("no message provided (stdin was empty)"));
15520
15946
  process.exit(1);
15521
15947
  }
15522
15948
  } else {
15523
- 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"));
15524
15950
  process.exit(1);
15525
15951
  }
15526
15952
  const client = createTelegramClient({ token });
15527
15953
  try {
15528
15954
  const { chatId, topicId } = await runTelegramSend({ project, message, cfg, client, stateRoot: defaultStateRoot() });
15529
- console.log(chalk30.green(`sent to group ${chatId} topic ${topicId}`));
15955
+ console.log(chalk31.green(`sent to group ${chatId} topic ${topicId}`));
15530
15956
  } catch (e) {
15531
- console.error(chalk30.red(e.message));
15957
+ console.error(chalk31.red(e.message));
15532
15958
  process.exit(1);
15533
15959
  }
15534
15960
  });
@@ -15536,10 +15962,92 @@ telegramCommand.command("send").argument("<project>", "project whose topic recei
15536
15962
  // packages/cli/src/commands/hooks.ts
15537
15963
  init_dist2();
15538
15964
  init_dist4();
15539
- import { Command as Command31 } from "commander";
15540
- import { join as join28 } from "path";
15541
- import { homedir as homedir20 } from "os";
15542
- 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");
15543
16051
  async function sendToSock(req) {
15544
16052
  await sendRequest(SOCK4, req);
15545
16053
  }
@@ -15562,14 +16070,31 @@ function mapHookSub(sub, payload, taskId) {
15562
16070
  return null;
15563
16071
  }
15564
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
+ }
15565
16095
  function hooksCommand() {
15566
- 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");
15567
16097
  hooks.command("claude <sub>", { hidden: true }).description("internal: bridge a NativeHookSource claude hook to squadrantd").action(async (sub) => {
15568
- const taskId = process.env.SQUADRANT_CREW_TASK_ID;
15569
- const project = process.env.SQUADRANT_CREW_PROJECT;
15570
- if (!taskId || !project) {
15571
- process.exit(0);
15572
- }
15573
16098
  let stdin = "";
15574
16099
  try {
15575
16100
  for await (const chunk of process.stdin) stdin += chunk;
@@ -15582,6 +16107,14 @@ function hooksCommand() {
15582
16107
  } catch {
15583
16108
  }
15584
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
+ }
15585
16118
  const ev = mapHookSub(sub, payload, taskId);
15586
16119
  if (!ev) {
15587
16120
  process.exit(0);
@@ -15595,26 +16128,554 @@ function hooksCommand() {
15595
16128
  return hooks;
15596
16129
  }
15597
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 Math.max(0, 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 branchState = gatherBranchState(runner, projectPath, branch, baseBranch, detached, fetch2);
16433
+ const fetchAgeMs = readFetchAgeMs(projectPath, now);
16434
+ let aheadOfBase = 0;
16435
+ let aheadOfBaseSource = "unknown";
16436
+ if (branch === baseBranch) {
16437
+ aheadOfBase = null;
16438
+ aheadOfBaseSource = "n-a";
16439
+ } else {
16440
+ if (ghInfo && !detached) {
16441
+ const ghAhead = ghAheadOfBase(runner, projectPath, ghInfo.nameWithOwner, baseBranch, branch);
16442
+ if (ghAhead !== null) {
16443
+ aheadOfBase = ghAhead;
16444
+ aheadOfBaseSource = "gh-api";
16445
+ }
16446
+ }
16447
+ if (aheadOfBaseSource === "unknown") {
16448
+ const localAhead = localAheadOfBase(runner, projectPath, baseBranch);
16449
+ if (localAhead !== null) {
16450
+ aheadOfBase = localAhead;
16451
+ aheadOfBaseSource = "local-git";
16452
+ }
16453
+ }
16454
+ }
16455
+ const unreleasedAheadOfReleaseBranch = ghInfo && baseBranch !== RELEASE_BRANCH ? ghAheadOfBase(runner, projectPath, ghInfo.nameWithOwner, RELEASE_BRANCH, baseBranch) : null;
16456
+ const conflicts = [];
16457
+ if (ghInfo) {
16458
+ const ghSha = ghBaseSha(runner, projectPath, ghInfo.nameWithOwner, baseBranch);
16459
+ const localSha = localBaseSha(runner, projectPath, baseBranch);
16460
+ if (ghSha && localSha && ghSha !== localSha) {
16461
+ const ageNote = fetchAgeMs !== null ? `fetched ${Math.round(fetchAgeMs / 36e5)}h ago` : "fetch age unknown";
16462
+ conflicts.push({
16463
+ field: "baseBranch",
16464
+ claim: `local git's last-known ${baseBranch} is ${localSha} (${ageNote})`,
16465
+ fact: `GitHub's live ${baseBranch} is ${ghSha}`,
16466
+ resolution: "GitHub API wins \u2014 local git may be stale"
16467
+ });
16468
+ }
16469
+ }
16470
+ return {
16471
+ branch,
16472
+ detached,
16473
+ baseBranch,
16474
+ baseBranchSource,
16475
+ recentCommits,
16476
+ aheadOfBase,
16477
+ aheadOfBaseSource,
16478
+ fetchAgeMs,
16479
+ openPRs: gatherOpenPRs(runner, projectPath),
16480
+ liveCrews: gatherLiveCrews(tasks),
16481
+ conflicts,
16482
+ branchState,
16483
+ unreleasedAheadOfReleaseBranch
16484
+ };
16485
+ }
16486
+
16487
+ // packages/cli/src/lib/handoff-claude-mem.ts
16488
+ import { createRequire } from "module";
16489
+ import fs30 from "fs";
16490
+ var { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
16491
+ var CLAUDE_MEM_RECENCY_LIMIT = 20;
16492
+ function decisionText(row) {
16493
+ if (row.facts) {
16494
+ try {
16495
+ const parsed = JSON.parse(row.facts);
16496
+ if (Array.isArray(parsed) && parsed.length > 0) return parsed.join("; ");
16497
+ } catch {
16498
+ }
16499
+ }
16500
+ return row.narrative ?? "";
16501
+ }
16502
+ function queryClaudeMem(dbPath, project) {
16503
+ if (!fs30.existsSync(dbPath)) return null;
16504
+ let db;
16505
+ try {
16506
+ db = new DatabaseSync(dbPath, { readOnly: true });
16507
+ } catch {
16508
+ return null;
16509
+ }
16510
+ try {
16511
+ const summaryRow = db.prepare(
16512
+ `SELECT request, completed, next_steps, created_at FROM session_summaries
16513
+ WHERE project = ? ORDER BY created_at_epoch DESC LIMIT 1`
16514
+ ).get(project);
16515
+ const decisionRows = db.prepare(
16516
+ `SELECT title, narrative, facts, created_at FROM observations
16517
+ WHERE project = ? AND type = 'decision' ORDER BY created_at_epoch DESC LIMIT ?`
16518
+ ).all(project, CLAUDE_MEM_RECENCY_LIMIT);
16519
+ const recentDecisions = decisionRows.map((r) => ({
16520
+ title: r.title,
16521
+ text: decisionText(r),
16522
+ createdAt: r.created_at
16523
+ }));
16524
+ const candidates = [summaryRow?.created_at, ...decisionRows.map((r) => r.created_at)].filter(
16525
+ (v) => !!v
16526
+ );
16527
+ const oldestCreatedAt = candidates.length > 0 ? candidates.reduce((a, b) => a < b ? a : b) : null;
16528
+ return {
16529
+ latestSessionSummary: summaryRow ? {
16530
+ request: summaryRow.request,
16531
+ completed: summaryRow.completed,
16532
+ nextSteps: summaryRow.next_steps,
16533
+ createdAt: summaryRow.created_at
16534
+ } : null,
16535
+ recentDecisions,
16536
+ oldestCreatedAt
16537
+ };
16538
+ } catch {
16539
+ return null;
16540
+ } finally {
16541
+ db.close();
16542
+ }
16543
+ }
16544
+
16545
+ // packages/cli/src/lib/handoff-transcript.ts
16546
+ import fs31 from "fs";
16547
+ var TRANSCRIPT_BYTE_CAP = 2e5;
16548
+ function tailOf(content, byteCap) {
16549
+ const buf = Buffer.from(content, "utf-8");
16550
+ if (buf.length <= byteCap) return content;
16551
+ const text = buf.subarray(buf.length - byteCap).toString("utf-8");
16552
+ return text.split("\n").slice(1).join("\n");
16553
+ }
16554
+ function extractMessages(tailText) {
16555
+ let lastUserMessage = null;
16556
+ let lastAssistantText = null;
16557
+ for (const line of tailText.split("\n")) {
16558
+ if (!line.trim()) continue;
16559
+ let obj;
16560
+ try {
16561
+ obj = JSON.parse(line);
16562
+ } catch {
16563
+ continue;
16564
+ }
16565
+ if (obj.type === "user" && typeof obj.message?.content === "string") {
16566
+ lastUserMessage = obj.message.content;
16567
+ } else if (obj.type === "assistant" && Array.isArray(obj.message?.content)) {
16568
+ const texts = obj.message.content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text);
16569
+ if (texts.length > 0) lastAssistantText = texts.join("\n");
16570
+ }
16571
+ }
16572
+ return { lastUserMessage, lastAssistantText };
16573
+ }
16574
+ function extractTranscriptTail(transcriptPath, byteCap = TRANSCRIPT_BYTE_CAP) {
16575
+ if (!fs31.existsSync(transcriptPath)) return null;
16576
+ const content = fs31.readFileSync(transcriptPath, "utf-8");
16577
+ const { lastUserMessage, lastAssistantText } = extractMessages(tailOf(content, byteCap));
16578
+ const mtimeIso = fs31.statSync(transcriptPath).mtime.toISOString();
16579
+ return { path: transcriptPath, mtimeIso, lastUserMessage, lastAssistantText };
16580
+ }
16581
+
16582
+ // packages/cli/src/lib/handoff-archive.ts
16583
+ import fs32 from "fs";
16584
+ import path32 from "path";
16585
+ function readNewestArchivedHandoff(spokeVault, now) {
16586
+ const dir = path32.join(spokeVault, "handoffs");
16587
+ if (!fs32.existsSync(dir)) return null;
16588
+ const candidates = fs32.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => {
16589
+ const full = path32.join(dir, e.name);
16590
+ return { name: e.name, full, mtime: fs32.statSync(full).mtime };
16591
+ }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
16592
+ for (const candidate of candidates) {
16593
+ let content;
16594
+ try {
16595
+ content = JSON.parse(fs32.readFileSync(candidate.full, "utf-8"));
16596
+ } catch {
16597
+ continue;
16598
+ }
16599
+ return { filename: candidate.name, path: candidate.full, ageMs: now - candidate.mtime.getTime(), content };
16600
+ }
16601
+ return null;
16602
+ }
16603
+
16604
+ // packages/cli/src/commands/handoff.ts
16605
+ var CLAUDE_MEM_DB_PATH = path33.join(os16.homedir(), ".claude-mem", "claude-mem.db");
16606
+ async function defaultFetchTasks(project) {
16607
+ return await squadrantdCall({ kind: "list", project });
16608
+ }
16609
+ async function runHandoffFacts(project, deps = {}) {
16610
+ const config = loadConfig();
16611
+ const proj = config.projects[project];
16612
+ if (!proj) {
16613
+ throw new Error(`Project '${project}' not found. Run 'squadrant projects list'.`);
16614
+ }
16615
+ const fallbackBaseBranch = resolveWorktreeBase(proj.path);
16616
+ let tasks;
16617
+ try {
16618
+ tasks = await (deps.fetchTasks ?? defaultFetchTasks)(project);
16619
+ } catch {
16620
+ tasks = [];
16621
+ }
16622
+ const nowIso = deps.now ?? (/* @__PURE__ */ new Date()).toISOString();
16623
+ const nowMs = Date.parse(nowIso);
16624
+ const fallbackWindowMs = deps.windowMs ?? SESSION_WINDOW_MS;
16625
+ const live = gatherLiveRepoState(proj.path, fallbackBaseBranch, tasks, deps.runner ?? defaultCommandRunner, nowMs, deps.fetch ?? false);
16626
+ const claudeMem = queryClaudeMem(deps.claudeMemDbPath ?? CLAUDE_MEM_DB_PATH, project);
16627
+ const checkpoint = readNewestArchivedHandoff(proj.spokeVault, nowMs);
16628
+ const currentSessionId = deps.currentSessionId !== void 0 ? deps.currentSessionId : process.env.CLAUDE_CODE_SESSION_ID ?? null;
16629
+ let registryNote = null;
16630
+ let gapSessions = [];
16631
+ let usedFallbackWindow = false;
16632
+ if (currentSessionId === null) {
16633
+ registryNote = "current session id unknown (CLAUDE_CODE_SESSION_ID unset) \u2014 cannot safely exclude the running session, so the gap was skipped";
16634
+ } else {
16635
+ const allRecords = readCaptainSessionRegistry(proj.spokeVault);
16636
+ if (allRecords.length === 0) {
16637
+ registryNote = "no session registry found yet for this project (#651's SessionStart hook may not have fired before now)";
16638
+ }
16639
+ const selection = selectGapSessions(allRecords, currentSessionId, checkpoint, nowMs, fallbackWindowMs);
16640
+ usedFallbackWindow = selection.usedFallbackWindow;
16641
+ gapSessions = selection.gapSessions.map((session) => ({ session, transcript: extractTranscriptTail(session.transcriptPath) }));
16642
+ }
16643
+ return assembleHandoffFacts(live, claudeMem, gapSessions, checkpoint, nowIso, {
16644
+ registryNote,
16645
+ usedFallbackWindow,
16646
+ fallbackWindowMs
16647
+ });
16648
+ }
16649
+ var handoffCommand = new Command34("handoff").description(
16650
+ "Handoff continuity \u2014 gather verified facts for the captain to synthesize a handoff from (#650/#651)"
16651
+ );
16652
+ handoffCommand.command("facts <project>").description(
16653
+ "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."
16654
+ ).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) => {
16655
+ const out = await runHandoffFacts(project, { fetch: opts.fetch });
16656
+ console.log(JSON.stringify(out, null, 2));
16657
+ });
16658
+
15598
16659
  // packages/cli/src/index.ts
15599
16660
  init_dist();
15600
16661
  init_dist();
15601
16662
  init_dist();
15602
16663
  init_dist();
15603
16664
  var __dirname = dirname9(fileURLToPath6(import.meta.url));
15604
- var pkg = JSON.parse(readFileSync15(join29(__dirname, "..", "package.json"), "utf-8"));
16665
+ var pkg = JSON.parse(readFileSync16(join30(__dirname, "..", "package.json"), "utf-8"));
15605
16666
  ensureRuntimeSynced({
15606
- sourceRoot: join29(__dirname, ".."),
15607
- runtimeRoot: join29(homedir21(), ".config", "squadrant")
16667
+ sourceRoot: join30(__dirname, ".."),
16668
+ runtimeRoot: join30(homedir22(), ".config", "squadrant")
15608
16669
  });
15609
16670
  if (process.argv[2] !== "config") {
15610
16671
  try {
15611
- const cfgPath = join29(homedir21(), ".config", "squadrant", "config.json");
15612
- if (existsSync12(cfgPath)) {
15613
- const cfg = JSON.parse(readFileSync15(cfgPath, "utf-8"));
16672
+ const cfgPath = join30(homedir22(), ".config", "squadrant", "config.json");
16673
+ if (existsSync13(cfgPath)) {
16674
+ const cfg = JSON.parse(readFileSync16(cfgPath, "utf-8"));
15614
16675
  if (needsCheck(cfg, pkg.version)) {
15615
16676
  const items = detectDrift(cfg, getDefaultConfig());
15616
16677
  if (items.length === 0) {
15617
- writeFileSync11(cfgPath, JSON.stringify(withStamp(cfg, pkg.version), null, 2) + "\n");
16678
+ writeFileSync12(cfgPath, JSON.stringify(withStamp(cfg, pkg.version), null, 2) + "\n");
15618
16679
  } else {
15619
16680
  const from = cfg._squadrantVersion ?? "an earlier version";
15620
16681
  process.stderr.write(
@@ -15632,9 +16693,9 @@ if (process.argv[2] !== "config") {
15632
16693
  }
15633
16694
  }
15634
16695
  if (!process.env.SQUADRANT_DAEMON_SKIP) {
15635
- ensureDaemon();
16696
+ ensureDaemon(void 0, { operatorInitiated: isOperatorInitiatedCommand(process.argv[2]) });
15636
16697
  }
15637
- var program = new Command32();
16698
+ var program = new Command35();
15638
16699
  program.name("squadrant").description("Multi-project orchestration for your coding agents (Claude, Codex, opencode, Gemini)").version(pkg.version);
15639
16700
  program.addCommand(doctorCommand);
15640
16701
  program.addCommand(initCommand);
@@ -15663,8 +16724,11 @@ program.addCommand(pingCommand);
15663
16724
  program.addCommand(dispatchCommand);
15664
16725
  program.addCommand(cmuxCommand);
15665
16726
  program.addCommand(effortCommand);
16727
+ program.addCommand(tokensCommand);
15666
16728
  program.addCommand(telegramCommand);
15667
16729
  program.addCommand(hooksCommand());
16730
+ program.addCommand(workCommand);
16731
+ program.addCommand(handoffCommand);
15668
16732
  program.parseAsync().catch((e) => {
15669
16733
  process.stderr.write(`error: ${e instanceof Error ? e.message : String(e)}
15670
16734
  `);