ai-whisper 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -633,6 +633,40 @@ function countActiveWorkflowsForCollab(db, collabId2) {
633
633
  const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId2);
634
634
  return row.n;
635
635
  }
636
+ var COUNTED_STATUSES = ["done", "halted"];
637
+ function getHandsOffStats(db) {
638
+ const placeholders = COUNTED_STATUSES.map(() => "?").join(",");
639
+ const rows = db.prepare(`SELECT status, created_at, updated_at
640
+ FROM workflows
641
+ WHERE status IN (${placeholders})`).all(...COUNTED_STATUSES);
642
+ const stats = {
643
+ totalMs: 0,
644
+ count: 0,
645
+ byStatus: {
646
+ done: { count: 0, totalMs: 0 },
647
+ halted: { count: 0, totalMs: 0 }
648
+ },
649
+ earliestKickoffAt: null,
650
+ skipped: 0
651
+ };
652
+ for (const row of rows) {
653
+ const start = Date.parse(row.created_at);
654
+ const end = Date.parse(row.updated_at);
655
+ if (Number.isNaN(start) || Number.isNaN(end)) {
656
+ stats.skipped += 1;
657
+ continue;
658
+ }
659
+ const elapsed = Math.max(0, end - start);
660
+ stats.totalMs += elapsed;
661
+ stats.count += 1;
662
+ stats.byStatus[row.status].count += 1;
663
+ stats.byStatus[row.status].totalMs += elapsed;
664
+ if (stats.earliestKickoffAt === null || row.created_at < stats.earliestKickoffAt) {
665
+ stats.earliestKickoffAt = row.created_at;
666
+ }
667
+ }
668
+ return stats;
669
+ }
636
670
 
637
671
  // ../broker/dist/runtime/workspace-snapshot.js
638
672
  import { execFileSync } from "node:child_process";
@@ -4352,6 +4386,9 @@ function createControlService(db, events) {
4352
4386
  ...now !== void 0 ? { now } : {}
4353
4387
  });
4354
4388
  },
4389
+ getHandsOffStats() {
4390
+ return getHandsOffStats(db);
4391
+ },
4355
4392
  listRunCostRows(collabId2, workflowId) {
4356
4393
  return listRunCostRows(db, { collabId: collabId2, workflowId });
4357
4394
  },
@@ -5310,19 +5347,20 @@ async function sweepStaleBrokerDaemons(input) {
5310
5347
  }
5311
5348
  return { deleted };
5312
5349
  }
5313
- function defaultIsAlive(pid) {
5350
+ function isPidAlive(pid) {
5314
5351
  try {
5315
5352
  process.kill(pid, 0);
5316
- return Promise.resolve({ alive: true, startTime: null });
5353
+ return true;
5317
5354
  } catch (err) {
5318
5355
  const code = err.code;
5319
- if (code === "ESRCH")
5320
- return Promise.resolve({ alive: false, startTime: null });
5321
5356
  if (code === "EPERM")
5322
- return Promise.resolve({ alive: true, startTime: null });
5323
- return Promise.resolve({ alive: false, startTime: null });
5357
+ return true;
5358
+ return false;
5324
5359
  }
5325
5360
  }
5361
+ function defaultIsAlive(pid) {
5362
+ return Promise.resolve({ alive: isPidAlive(pid), startTime: null });
5363
+ }
5326
5364
 
5327
5365
  // ../broker/dist/runtime/daemon-heartbeat.js
5328
5366
  function createDaemonHeartbeat(input) {
@@ -666,6 +666,40 @@ function countActiveWorkflowsForCollab(db, collabId) {
666
666
  const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId);
667
667
  return row.n;
668
668
  }
669
+ var COUNTED_STATUSES = ["done", "halted"];
670
+ function getHandsOffStats(db) {
671
+ const placeholders = COUNTED_STATUSES.map(() => "?").join(",");
672
+ const rows = db.prepare(`SELECT status, created_at, updated_at
673
+ FROM workflows
674
+ WHERE status IN (${placeholders})`).all(...COUNTED_STATUSES);
675
+ const stats = {
676
+ totalMs: 0,
677
+ count: 0,
678
+ byStatus: {
679
+ done: { count: 0, totalMs: 0 },
680
+ halted: { count: 0, totalMs: 0 }
681
+ },
682
+ earliestKickoffAt: null,
683
+ skipped: 0
684
+ };
685
+ for (const row of rows) {
686
+ const start = Date.parse(row.created_at);
687
+ const end = Date.parse(row.updated_at);
688
+ if (Number.isNaN(start) || Number.isNaN(end)) {
689
+ stats.skipped += 1;
690
+ continue;
691
+ }
692
+ const elapsed = Math.max(0, end - start);
693
+ stats.totalMs += elapsed;
694
+ stats.count += 1;
695
+ stats.byStatus[row.status].count += 1;
696
+ stats.byStatus[row.status].totalMs += elapsed;
697
+ if (stats.earliestKickoffAt === null || row.created_at < stats.earliestKickoffAt) {
698
+ stats.earliestKickoffAt = row.created_at;
699
+ }
700
+ }
701
+ return stats;
702
+ }
669
703
 
670
704
  // ../broker/dist/runtime/workspace-snapshot.js
671
705
  import { execFileSync } from "node:child_process";
@@ -4372,6 +4406,9 @@ function createControlService(db, events) {
4372
4406
  ...now !== void 0 ? { now } : {}
4373
4407
  });
4374
4408
  },
4409
+ getHandsOffStats() {
4410
+ return getHandsOffStats(db);
4411
+ },
4375
4412
  listRunCostRows(collabId, workflowId) {
4376
4413
  return listRunCostRows(db, { collabId, workflowId });
4377
4414
  },
@@ -5320,19 +5357,20 @@ async function sweepStaleBrokerDaemons(input) {
5320
5357
  }
5321
5358
  return { deleted };
5322
5359
  }
5323
- function defaultIsAlive(pid) {
5360
+ function isPidAlive(pid) {
5324
5361
  try {
5325
5362
  process.kill(pid, 0);
5326
- return Promise.resolve({ alive: true, startTime: null });
5363
+ return true;
5327
5364
  } catch (err) {
5328
5365
  const code = err.code;
5329
- if (code === "ESRCH")
5330
- return Promise.resolve({ alive: false, startTime: null });
5331
5366
  if (code === "EPERM")
5332
- return Promise.resolve({ alive: true, startTime: null });
5333
- return Promise.resolve({ alive: false, startTime: null });
5367
+ return true;
5368
+ return false;
5334
5369
  }
5335
5370
  }
5371
+ function defaultIsAlive(pid) {
5372
+ return Promise.resolve({ alive: isPidAlive(pid), startTime: null });
5373
+ }
5336
5374
 
5337
5375
  // ../broker/dist/runtime/daemon-heartbeat.js
5338
5376
  function createDaemonHeartbeat(input) {
@@ -636,6 +636,40 @@ function countActiveWorkflowsForCollab(db, collabId2) {
636
636
  const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId2);
637
637
  return row.n;
638
638
  }
639
+ var COUNTED_STATUSES = ["done", "halted"];
640
+ function getHandsOffStats(db) {
641
+ const placeholders = COUNTED_STATUSES.map(() => "?").join(",");
642
+ const rows = db.prepare(`SELECT status, created_at, updated_at
643
+ FROM workflows
644
+ WHERE status IN (${placeholders})`).all(...COUNTED_STATUSES);
645
+ const stats = {
646
+ totalMs: 0,
647
+ count: 0,
648
+ byStatus: {
649
+ done: { count: 0, totalMs: 0 },
650
+ halted: { count: 0, totalMs: 0 }
651
+ },
652
+ earliestKickoffAt: null,
653
+ skipped: 0
654
+ };
655
+ for (const row of rows) {
656
+ const start = Date.parse(row.created_at);
657
+ const end = Date.parse(row.updated_at);
658
+ if (Number.isNaN(start) || Number.isNaN(end)) {
659
+ stats.skipped += 1;
660
+ continue;
661
+ }
662
+ const elapsed = Math.max(0, end - start);
663
+ stats.totalMs += elapsed;
664
+ stats.count += 1;
665
+ stats.byStatus[row.status].count += 1;
666
+ stats.byStatus[row.status].totalMs += elapsed;
667
+ if (stats.earliestKickoffAt === null || row.created_at < stats.earliestKickoffAt) {
668
+ stats.earliestKickoffAt = row.created_at;
669
+ }
670
+ }
671
+ return stats;
672
+ }
639
673
 
640
674
  // ../broker/dist/runtime/workspace-snapshot.js
641
675
  import { execFileSync } from "node:child_process";
@@ -4342,6 +4376,9 @@ function createControlService(db, events) {
4342
4376
  ...now !== void 0 ? { now } : {}
4343
4377
  });
4344
4378
  },
4379
+ getHandsOffStats() {
4380
+ return getHandsOffStats(db);
4381
+ },
4345
4382
  listRunCostRows(collabId2, workflowId) {
4346
4383
  return listRunCostRows(db, { collabId: collabId2, workflowId });
4347
4384
  },
@@ -5294,19 +5331,20 @@ async function sweepStaleBrokerDaemons(input) {
5294
5331
  }
5295
5332
  return { deleted };
5296
5333
  }
5297
- function defaultIsAlive(pid) {
5334
+ function isPidAlive(pid) {
5298
5335
  try {
5299
5336
  process.kill(pid, 0);
5300
- return Promise.resolve({ alive: true, startTime: null });
5337
+ return true;
5301
5338
  } catch (err) {
5302
5339
  const code = err.code;
5303
- if (code === "ESRCH")
5304
- return Promise.resolve({ alive: false, startTime: null });
5305
5340
  if (code === "EPERM")
5306
- return Promise.resolve({ alive: true, startTime: null });
5307
- return Promise.resolve({ alive: false, startTime: null });
5341
+ return true;
5342
+ return false;
5308
5343
  }
5309
5344
  }
5345
+ function defaultIsAlive(pid) {
5346
+ return Promise.resolve({ alive: isPidAlive(pid), startTime: null });
5347
+ }
5310
5348
 
5311
5349
  // ../broker/dist/runtime/daemon-heartbeat.js
5312
5350
  function createDaemonHeartbeat(input) {
@@ -821,6 +821,46 @@ function insertCollab(db, collab) {
821
821
  orchestrator_max_rounds
822
822
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(collab.collabId, collab.workspaceRoot, collab.displayName, collab.status, collab.createdAt, collab.updatedAt, collab.orchestratorEnabled ? 1 : 0, collab.orchestratorMaxRounds);
823
823
  }
824
+ function listAllCollabs(db) {
825
+ const rows = db.prepare(`SELECT collab_id, workspace_root, workspace_id, status, tmux_session
826
+ FROM collab
827
+ ORDER BY created_at`).all();
828
+ return rows.map((r) => ({
829
+ collabId: r.collab_id,
830
+ workspaceRoot: r.workspace_root,
831
+ workspaceId: r.workspace_id,
832
+ status: r.status,
833
+ tmuxSession: r.tmux_session
834
+ }));
835
+ }
836
+ function tableHasCollabIdColumn(db, table) {
837
+ const cols = db.prepare(`PRAGMA table_info("${table}")`).all();
838
+ return cols.some((c) => c.name === "collab_id");
839
+ }
840
+ function listCollabIdTables(db) {
841
+ const names = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all().map((r) => r.name);
842
+ return names.filter((name) => name !== "collab" && tableHasCollabIdColumn(db, name));
843
+ }
844
+ function countCollabRows(db, collabId) {
845
+ let total = 0;
846
+ for (const table of listCollabIdTables(db)) {
847
+ total += db.prepare(`SELECT COUNT(*) AS n FROM "${table}" WHERE collab_id = ?`).get(collabId).n;
848
+ }
849
+ total += db.prepare("SELECT COUNT(*) AS n FROM workflow_phases WHERE workflow_id IN (SELECT workflow_id FROM workflows WHERE collab_id = ?)").get(collabId).n;
850
+ total += db.prepare("SELECT COUNT(*) AS n FROM work_item_cancellation WHERE work_item_id IN (SELECT work_item_id FROM work_item WHERE collab_id = ?)").get(collabId).n;
851
+ total += db.prepare("SELECT COUNT(*) AS n FROM clipboard_capture_lease WHERE holder_collab_id = ?").get(collabId).n;
852
+ total += db.prepare("SELECT COUNT(*) AS n FROM collab WHERE collab_id = ?").get(collabId).n;
853
+ return total;
854
+ }
855
+ function deleteCollabCascade(db, collabId) {
856
+ db.prepare("DELETE FROM workflow_phases WHERE workflow_id IN (SELECT workflow_id FROM workflows WHERE collab_id = ?)").run(collabId);
857
+ db.prepare("DELETE FROM work_item_cancellation WHERE work_item_id IN (SELECT work_item_id FROM work_item WHERE collab_id = ?)").run(collabId);
858
+ for (const table of listCollabIdTables(db)) {
859
+ db.prepare(`DELETE FROM "${table}" WHERE collab_id = ?`).run(collabId);
860
+ }
861
+ db.prepare("DELETE FROM clipboard_capture_lease WHERE holder_collab_id = ?").run(collabId);
862
+ db.prepare("DELETE FROM collab WHERE collab_id = ?").run(collabId);
863
+ }
824
864
  function getCollab(db, collabId) {
825
865
  const row = db.prepare(`SELECT collab_id, workspace_root, display_name, status, created_at, updated_at,
826
866
  orchestrator_enabled, orchestrator_max_rounds
@@ -912,9 +952,52 @@ function countActiveWorkflowsForCollab(db, collabId) {
912
952
  const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId);
913
953
  return row.n;
914
954
  }
955
+ function findNonTerminalWorkflow(db, collabId) {
956
+ const row = db.prepare(`SELECT workflow_id, status
957
+ FROM workflows
958
+ WHERE collab_id = ? AND status NOT IN ('done', 'canceled')
959
+ ORDER BY created_at DESC
960
+ LIMIT 1`).get(collabId);
961
+ return row ? { workflowId: row.workflow_id, status: row.status } : null;
962
+ }
963
+ function getHandsOffStats(db) {
964
+ const placeholders = COUNTED_STATUSES.map(() => "?").join(",");
965
+ const rows = db.prepare(`SELECT status, created_at, updated_at
966
+ FROM workflows
967
+ WHERE status IN (${placeholders})`).all(...COUNTED_STATUSES);
968
+ const stats = {
969
+ totalMs: 0,
970
+ count: 0,
971
+ byStatus: {
972
+ done: { count: 0, totalMs: 0 },
973
+ halted: { count: 0, totalMs: 0 }
974
+ },
975
+ earliestKickoffAt: null,
976
+ skipped: 0
977
+ };
978
+ for (const row of rows) {
979
+ const start = Date.parse(row.created_at);
980
+ const end = Date.parse(row.updated_at);
981
+ if (Number.isNaN(start) || Number.isNaN(end)) {
982
+ stats.skipped += 1;
983
+ continue;
984
+ }
985
+ const elapsed = Math.max(0, end - start);
986
+ stats.totalMs += elapsed;
987
+ stats.count += 1;
988
+ stats.byStatus[row.status].count += 1;
989
+ stats.byStatus[row.status].totalMs += elapsed;
990
+ if (stats.earliestKickoffAt === null || row.created_at < stats.earliestKickoffAt) {
991
+ stats.earliestKickoffAt = row.created_at;
992
+ }
993
+ }
994
+ return stats;
995
+ }
996
+ var COUNTED_STATUSES;
915
997
  var init_workflow_repository = __esm({
916
998
  "../broker/dist/storage/repositories/workflow-repository.js"() {
917
999
  "use strict";
1000
+ COUNTED_STATUSES = ["done", "halted"];
918
1001
  }
919
1002
  });
920
1003
 
@@ -4773,6 +4856,9 @@ function createControlService(db, events) {
4773
4856
  ...now !== void 0 ? { now } : {}
4774
4857
  });
4775
4858
  },
4859
+ getHandsOffStats() {
4860
+ return getHandsOffStats(db);
4861
+ },
4776
4862
  listRunCostRows(collabId, workflowId) {
4777
4863
  return listRunCostRows(db, { collabId, workflowId });
4778
4864
  },
@@ -5836,19 +5922,20 @@ async function sweepStaleBrokerDaemons(input) {
5836
5922
  }
5837
5923
  return { deleted };
5838
5924
  }
5839
- function defaultIsAlive(pid) {
5925
+ function isPidAlive(pid) {
5840
5926
  try {
5841
5927
  process.kill(pid, 0);
5842
- return Promise.resolve({ alive: true, startTime: null });
5928
+ return true;
5843
5929
  } catch (err) {
5844
5930
  const code = err.code;
5845
- if (code === "ESRCH")
5846
- return Promise.resolve({ alive: false, startTime: null });
5847
5931
  if (code === "EPERM")
5848
- return Promise.resolve({ alive: true, startTime: null });
5849
- return Promise.resolve({ alive: false, startTime: null });
5932
+ return true;
5933
+ return false;
5850
5934
  }
5851
5935
  }
5936
+ function defaultIsAlive(pid) {
5937
+ return Promise.resolve({ alive: isPidAlive(pid), startTime: null });
5938
+ }
5852
5939
  var init_broker_daemon_sweep = __esm({
5853
5940
  "../broker/dist/runtime/broker-daemon-sweep.js"() {
5854
5941
  "use strict";
@@ -6490,6 +6577,8 @@ var init_dist2 = __esm({
6490
6577
  init_session_attachment_repository();
6491
6578
  init_broker_daemon_repository();
6492
6579
  init_recovery_state_repository();
6580
+ init_collab_repository();
6581
+ init_workflow_repository();
6493
6582
  }
6494
6583
  });
6495
6584
 
@@ -6582,6 +6671,15 @@ function fmtDur(ms) {
6582
6671
  const m = Math.floor(s / 60);
6583
6672
  return m > 0 ? `${m}m${String(s % 60).padStart(2, "0")}s` : `${s}s`;
6584
6673
  }
6674
+ function fmtDurCoarse(ms) {
6675
+ const totalMin = Math.floor(Math.max(0, ms) / 6e4);
6676
+ const d = Math.floor(totalMin / 1440);
6677
+ const h = Math.floor(totalMin % 1440 / 60);
6678
+ const m = totalMin % 60;
6679
+ if (d > 0) return `${d}d ${h}h`;
6680
+ if (h > 0) return `${h}h ${m}m`;
6681
+ return `${m}m`;
6682
+ }
6585
6683
  function isOkOutcome(outcome) {
6586
6684
  if (!outcome) return false;
6587
6685
  return !/escalat|halt|fail|cancel/i.test(outcome);
@@ -7088,10 +7186,13 @@ function Wall(props) {
7088
7186
  const paneWidth = Math.floor(props.cols / colsCount);
7089
7187
  let globalIdx = 0;
7090
7188
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: props.cols, children: [
7091
- props.counts ? /* @__PURE__ */ jsx3(Text2, { wrap: "truncate", children: SUMMARY_SEGMENTS.map((seg, i) => {
7092
- const n = props.counts[seg.key];
7093
- return /* @__PURE__ */ jsx3(Text2, { color: n > 0 ? seg.color : THEME.muted, children: (i > 0 ? " " : "") + `${seg.glyph} ${n} ${seg.label}` }, seg.key);
7094
- }) }) : null,
7189
+ props.counts ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7190
+ SUMMARY_SEGMENTS.map((seg, i) => {
7191
+ const n = props.counts[seg.key];
7192
+ return /* @__PURE__ */ jsx3(Text2, { color: n > 0 ? seg.color : THEME.muted, children: (i > 0 ? " " : "") + `${seg.glyph} ${n} ${seg.label}` }, seg.key);
7193
+ }),
7194
+ /* @__PURE__ */ jsx3(Text2, { color: THEME.muted, children: ` \u2502 hands-off saved ${fmtDurCoarse(state.handsOff.totalMs)} (or ${Math.floor(state.handsOff.totalMs / 36e5)}h) (${state.handsOff.count} wf runs)` })
7195
+ ] }) : null,
7095
7196
  state.sections.map((sec) => {
7096
7197
  const rows = [];
7097
7198
  for (let i = 0; i < sec.panes.length; i += colsCount) {
@@ -7693,7 +7794,8 @@ function buildWallState(input) {
7693
7794
  page: alloc.page,
7694
7795
  pageCount: alloc.pageCount,
7695
7796
  totalRuns: alloc.totalRuns,
7696
- selected
7797
+ selected,
7798
+ handsOff: input.handsOff ?? { totalMs: 0, count: 0 }
7697
7799
  };
7698
7800
  }
7699
7801
  var MIN_PANE_COLS2, CARD_HEIGHT, HEADER_ROWS, GROUP_ORDER, GROUP_LABEL;
@@ -8035,6 +8137,7 @@ function createDashboardRuntime(input) {
8035
8137
  mountAlive: mountAliveOf(sess.agentType)
8036
8138
  }));
8037
8139
  }
8140
+ const handsOff = c.getHandsOffStats();
8038
8141
  const wallState = buildWallState({
8039
8142
  summaries,
8040
8143
  now: isoNow,
@@ -8043,7 +8146,8 @@ function createDashboardRuntime(input) {
8043
8146
  rows,
8044
8147
  page: wallPage,
8045
8148
  selected: wallSelected,
8046
- snapshots
8149
+ snapshots,
8150
+ handsOff: { totalMs: handsOff.totalMs, count: handsOff.count }
8047
8151
  });
8048
8152
  wallPage = wallState.page;
8049
8153
  wallSelected = wallState.selected;
@@ -8238,7 +8342,8 @@ function loadDotEnv() {
8238
8342
  }
8239
8343
 
8240
8344
  // src/create-cli.ts
8241
- import { execSync as execSync4 } from "node:child_process";
8345
+ init_dist2();
8346
+ import { execSync as execSync5 } from "node:child_process";
8242
8347
  import { Command, Option } from "commander";
8243
8348
 
8244
8349
  // src/runtime/wait-for-broker-ready.ts
@@ -13934,8 +14039,8 @@ function compareSemver(a, b) {
13934
14039
  var EZIO_PROVENANCE = {
13935
14040
  ezioCliVersion: "0.3.0",
13936
14041
  ezioGitSha: "ffd95dc",
13937
- builtAt: "2026-06-30T04:51:15.900Z",
13938
- whisperVersion: "0.10.0"
14042
+ builtAt: "2026-07-01T04:07:32.417Z",
14043
+ whisperVersion: "0.11.0"
13939
14044
  };
13940
14045
 
13941
14046
  // src/ezio-provenance-types.ts
@@ -17862,6 +17967,250 @@ function runCollabStop(input) {
17862
17967
  }
17863
17968
  }
17864
17969
 
17970
+ // src/commands/collab/purge.ts
17971
+ init_dist2();
17972
+ init_dist2();
17973
+ import { execSync as execSync3 } from "node:child_process";
17974
+ import { createInterface } from "node:readline/promises";
17975
+ function isCollabLive(db, collabId, isAlive) {
17976
+ const daemon = getBrokerDaemonByCollab(db, collabId);
17977
+ if (daemon?.pid != null && isAlive(daemon.pid)) return true;
17978
+ return listSessionAttachmentsByCollab(db, collabId).some(
17979
+ (a) => a.pid != null && isAlive(a.pid)
17980
+ );
17981
+ }
17982
+ function classifyCollab(db, collab, isAlive) {
17983
+ const base = {
17984
+ collabId: collab.collabId,
17985
+ workspaceRoot: collab.workspaceRoot,
17986
+ workspaceId: collab.workspaceId,
17987
+ status: collab.status,
17988
+ tmuxSession: collab.tmuxSession,
17989
+ rowCount: countCollabRows(db, collab.collabId)
17990
+ };
17991
+ const daemon = getBrokerDaemonByCollab(db, collab.collabId);
17992
+ const daemonAlive = daemon?.pid != null && isAlive(daemon.pid);
17993
+ const mountAlive = listSessionAttachmentsByCollab(db, collab.collabId).some(
17994
+ (a) => a.pid != null && isAlive(a.pid)
17995
+ );
17996
+ if (daemonAlive || mountAlive) {
17997
+ return {
17998
+ ...base,
17999
+ bucket: "live",
18000
+ reason: daemonAlive ? "live-daemon" : "live-mount"
18001
+ };
18002
+ }
18003
+ const wf = findNonTerminalWorkflow(db, collab.collabId);
18004
+ if (wf) {
18005
+ return {
18006
+ ...base,
18007
+ bucket: "protected",
18008
+ reason: `workflow ${wf.status}`,
18009
+ workflowId: wf.workflowId,
18010
+ workflowStatus: wf.status
18011
+ };
18012
+ }
18013
+ const reason = collab.status === "stopped" ? "stopped" : daemon ? "dead-daemon" : "dead-mounts";
18014
+ return { ...base, bucket: "stale", reason };
18015
+ }
18016
+ function classifyAllCollabs(db, collabs, isAlive) {
18017
+ return collabs.map((c) => classifyCollab(db, c, isAlive));
18018
+ }
18019
+ function defaultConfirm(promptText) {
18020
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
18021
+ return rl.question(`${promptText} [y/N] `).then((answer) => /^y(es)?$/i.test(answer.trim())).finally(() => rl.close());
18022
+ }
18023
+ function defaultKillTmuxSession(session) {
18024
+ try {
18025
+ const escaped = session.replace(/'/g, "'\\''");
18026
+ execSync3(`tmux kill-session -t '${escaped}'`, { stdio: "ignore" });
18027
+ } catch {
18028
+ }
18029
+ }
18030
+ function buildJsonPayload(classifications, purged, skippedWentLive, skippedError, protectedSkipped, aborted) {
18031
+ return {
18032
+ classifications,
18033
+ purged,
18034
+ skipped: { wentLive: skippedWentLive, error: skippedError },
18035
+ protected: protectedSkipped,
18036
+ aborted
18037
+ };
18038
+ }
18039
+ function renderTable(classifications, log) {
18040
+ if (classifications.length === 0) {
18041
+ log("No collabs found.");
18042
+ return;
18043
+ }
18044
+ log(
18045
+ "COLLAB BUCKET REASON ROWS WORKSPACE"
18046
+ );
18047
+ for (const c of classifications) {
18048
+ const wf = c.workflowId ? ` [${c.workflowId} ${c.workflowStatus}]` : "";
18049
+ log(
18050
+ `${c.collabId.padEnd(31)} ${c.bucket.padEnd(10)} ${c.reason.padEnd(16)} ${String(c.rowCount).padStart(4)} ${c.workspaceRoot}${wf}`
18051
+ );
18052
+ }
18053
+ }
18054
+ async function runCollabPurge(opts) {
18055
+ if (opts.collabId && opts.workspace) {
18056
+ throw new Error("--collab and --workspace are mutually exclusive");
18057
+ }
18058
+ const isAlive = opts.isAlive ?? isPidAlive;
18059
+ const log = opts.log ?? ((line) => console.log(line));
18060
+ const isTTY = opts.isTTY ?? Boolean(process.stdin.isTTY);
18061
+ const confirm = opts.confirm ?? defaultConfirm;
18062
+ const killTmux = opts.killTmuxSession ?? defaultKillTmuxSession;
18063
+ const empty = (exitCode, classifications = [], protectedSkipped = [], aborted = false) => ({
18064
+ classifications,
18065
+ purged: [],
18066
+ skippedWentLive: [],
18067
+ skippedError: [],
18068
+ protectedSkipped,
18069
+ aborted,
18070
+ exitCode
18071
+ });
18072
+ let workspaceFilter = null;
18073
+ if (opts.workspace) {
18074
+ try {
18075
+ workspaceFilter = workspaceIdFromPath(opts.workspace);
18076
+ } catch {
18077
+ log(`Cannot resolve --workspace path: ${opts.workspace}`);
18078
+ return empty(1);
18079
+ }
18080
+ }
18081
+ const db = openDatabase(getSharedSqlitePath());
18082
+ applyMigrations(db);
18083
+ try {
18084
+ let collabs = listAllCollabs(db);
18085
+ if (opts.collabId)
18086
+ collabs = collabs.filter((c) => c.collabId === opts.collabId);
18087
+ if (workspaceFilter)
18088
+ collabs = collabs.filter((c) => c.workspaceId === workspaceFilter);
18089
+ const classifications = classifyAllCollabs(db, collabs, isAlive);
18090
+ const protectedSkipped = classifications.filter((c) => c.bucket === "protected").map((c) => c.collabId);
18091
+ const candidates = classifications.filter(
18092
+ (c) => c.bucket === "stale" || opts.force === true && c.bucket === "protected"
18093
+ );
18094
+ if (!opts.json) {
18095
+ renderTable(classifications, log);
18096
+ }
18097
+ if (candidates.length === 0) {
18098
+ if (opts.json) {
18099
+ log(
18100
+ JSON.stringify(
18101
+ buildJsonPayload(
18102
+ classifications,
18103
+ [],
18104
+ [],
18105
+ [],
18106
+ protectedSkipped,
18107
+ false
18108
+ ),
18109
+ null,
18110
+ 2
18111
+ )
18112
+ );
18113
+ } else {
18114
+ log("Nothing to purge.");
18115
+ }
18116
+ return empty(0, classifications, protectedSkipped);
18117
+ }
18118
+ const previewOnly = opts.dryRun === true || opts.json === true && opts.yes !== true;
18119
+ if (previewOnly) {
18120
+ if (opts.json) {
18121
+ log(
18122
+ JSON.stringify(
18123
+ buildJsonPayload(
18124
+ classifications,
18125
+ [],
18126
+ [],
18127
+ [],
18128
+ protectedSkipped,
18129
+ false
18130
+ ),
18131
+ null,
18132
+ 2
18133
+ )
18134
+ );
18135
+ } else {
18136
+ log(
18137
+ `Dry run \u2014 ${candidates.length} collab(s) would be purged. Nothing deleted.`
18138
+ );
18139
+ }
18140
+ return empty(0, classifications, protectedSkipped);
18141
+ }
18142
+ if (opts.yes !== true) {
18143
+ if (!isTTY) {
18144
+ log(
18145
+ `Refusing to delete ${candidates.length} collab(s) without confirmation. Re-run with --yes.`
18146
+ );
18147
+ return empty(1, classifications, protectedSkipped, true);
18148
+ }
18149
+ const ok = await confirm(`Delete these ${candidates.length} collab(s)?`);
18150
+ if (!ok) {
18151
+ log("Aborted. Nothing deleted.");
18152
+ return empty(0, classifications, protectedSkipped, true);
18153
+ }
18154
+ }
18155
+ const purged = [];
18156
+ const skippedWentLive = [];
18157
+ const skippedError = [];
18158
+ const cascade = opts.deleteCascade ?? deleteCollabCascade;
18159
+ const purgeOne = db.transaction((collabId) => {
18160
+ if (isCollabLive(db, collabId, isAlive)) return false;
18161
+ cascade(db, collabId);
18162
+ return true;
18163
+ });
18164
+ for (const cand of candidates) {
18165
+ try {
18166
+ const deleted = purgeOne.immediate(cand.collabId);
18167
+ if (deleted) {
18168
+ purged.push(cand.collabId);
18169
+ if (cand.tmuxSession) killTmux(cand.tmuxSession);
18170
+ } else {
18171
+ skippedWentLive.push(cand.collabId);
18172
+ }
18173
+ } catch (err) {
18174
+ skippedError.push({
18175
+ collabId: cand.collabId,
18176
+ error: err.message
18177
+ });
18178
+ }
18179
+ }
18180
+ if (opts.json) {
18181
+ log(
18182
+ JSON.stringify(
18183
+ buildJsonPayload(
18184
+ classifications,
18185
+ purged,
18186
+ skippedWentLive,
18187
+ skippedError,
18188
+ protectedSkipped,
18189
+ false
18190
+ ),
18191
+ null,
18192
+ 2
18193
+ )
18194
+ );
18195
+ } else {
18196
+ log(
18197
+ `Purged ${purged.length}, skipped (went live) ${skippedWentLive.length}, errors ${skippedError.length}, protected ${protectedSkipped.length}.`
18198
+ );
18199
+ }
18200
+ return {
18201
+ classifications,
18202
+ purged,
18203
+ skippedWentLive,
18204
+ skippedError,
18205
+ protectedSkipped,
18206
+ aborted: false,
18207
+ exitCode: skippedError.length > 0 ? 1 : 0
18208
+ };
18209
+ } finally {
18210
+ db.close();
18211
+ }
18212
+ }
18213
+
17865
18214
  // src/commands/collab/tell.ts
17866
18215
  init_dist2();
17867
18216
  init_dist();
@@ -18049,7 +18398,7 @@ async function runCollabTell(input) {
18049
18398
  }
18050
18399
 
18051
18400
  // src/runtime/launcher.ts
18052
- import { execSync as execSync3, spawn as nodeSpawn2 } from "node:child_process";
18401
+ import { execSync as execSync4, spawn as nodeSpawn2 } from "node:child_process";
18053
18402
  import { dirname as dirname8, resolve as resolve4 } from "node:path";
18054
18403
  import { fileURLToPath as fileURLToPath5 } from "node:url";
18055
18404
  var __dirname = dirname8(fileURLToPath5(import.meta.url));
@@ -18060,7 +18409,7 @@ function shellQuote(value) {
18060
18409
  }
18061
18410
  function detectTmux() {
18062
18411
  try {
18063
- execSync3("which tmux", { stdio: "ignore" });
18412
+ execSync4("which tmux", { stdio: "ignore" });
18064
18413
  return true;
18065
18414
  } catch {
18066
18415
  return false;
@@ -18103,7 +18452,7 @@ function defaultSpawn(command) {
18103
18452
  return child.pid;
18104
18453
  }
18105
18454
  function defaultExec(command) {
18106
- execSync3(command, { stdio: "inherit" });
18455
+ execSync4(command, { stdio: "inherit" });
18107
18456
  }
18108
18457
  function wrapForTerminalWindow(agentCommand, label) {
18109
18458
  if (process.platform === "darwin") {
@@ -18323,6 +18672,53 @@ async function runWorkflowTypes() {
18323
18672
  return listWorkflowTypes();
18324
18673
  }
18325
18674
 
18675
+ // src/commands/workflow/stats.ts
18676
+ init_relay_view_state();
18677
+ function runWorkflowStats(deps) {
18678
+ const stats = deps.broker.control.getHandsOffStats();
18679
+ const out = deps.stdout ?? process.stdout;
18680
+ if (deps.json) {
18681
+ const payload = {
18682
+ totalMs: stats.totalMs,
18683
+ totalHuman: fmtDurCoarse(stats.totalMs),
18684
+ count: stats.count,
18685
+ since: stats.earliestKickoffAt,
18686
+ byStatus: {
18687
+ done: {
18688
+ count: stats.byStatus.done.count,
18689
+ totalMs: stats.byStatus.done.totalMs,
18690
+ human: fmtDurCoarse(stats.byStatus.done.totalMs)
18691
+ },
18692
+ halted: {
18693
+ count: stats.byStatus.halted.count,
18694
+ totalMs: stats.byStatus.halted.totalMs,
18695
+ human: fmtDurCoarse(stats.byStatus.halted.totalMs)
18696
+ }
18697
+ },
18698
+ skipped: stats.skipped
18699
+ };
18700
+ out.write(`${JSON.stringify(payload, null, 2)}
18701
+ `);
18702
+ return;
18703
+ }
18704
+ if (stats.count === 0) {
18705
+ out.write("Hands-off time saved: 0m (no completed workflows yet)\n");
18706
+ return;
18707
+ }
18708
+ const since = (stats.earliestKickoffAt ?? "").slice(0, 10);
18709
+ out.write(
18710
+ `Hands-off time saved: ${fmtDurCoarse(stats.totalMs)} (${stats.count} workflows, since ${since})
18711
+ `
18712
+ );
18713
+ for (const status of ["done", "halted"]) {
18714
+ const b = stats.byStatus[status];
18715
+ out.write(
18716
+ ` ${status.padEnd(9)}${b.count} runs \xB7 ${fmtDurCoarse(b.totalMs)}
18717
+ `
18718
+ );
18719
+ }
18720
+ }
18721
+
18326
18722
  // src/commands/skill/install.ts
18327
18723
  import { cp, mkdir, readdir, stat } from "node:fs/promises";
18328
18724
  import path3 from "node:path";
@@ -18548,9 +18944,7 @@ function createCli() {
18548
18944
  launch
18549
18945
  });
18550
18946
  }
18551
- console.log(
18552
- `Collab started: ${r.collabId} (launch: ${launchMode})`
18553
- );
18947
+ console.log(`Collab started: ${r.collabId} (launch: ${launchMode})`);
18554
18948
  if (launchMode === "none") {
18555
18949
  console.log("Collab started (no-launch mode).");
18556
18950
  }
@@ -18563,7 +18957,10 @@ Not attached (stdin/stdout is not a TTY). To view the tmux session, run:
18563
18957
  );
18564
18958
  }
18565
18959
  });
18566
- collab.command("status").description("Show current collaboration status").option("--collab <id>", "Inspect a specific collab id (defaults to the active collab for cwd)").option("--json", "Emit machine-readable JSON instead of text").action((opts) => {
18960
+ collab.command("status").description("Show current collaboration status").option(
18961
+ "--collab <id>",
18962
+ "Inspect a specific collab id (defaults to the active collab for cwd)"
18963
+ ).option("--json", "Emit machine-readable JSON instead of text").action((opts) => {
18567
18964
  const output = runCollabStatus({
18568
18965
  cwd: process.cwd(),
18569
18966
  ...opts.collab ? { collabIdOverride: opts.collab } : {},
@@ -18571,7 +18968,13 @@ Not attached (stdin/stdout is not a TTY). To view the tmux session, run:
18571
18968
  });
18572
18969
  console.log(output);
18573
18970
  });
18574
- collab.command("tell").description("Send an instruction to an agent").requiredOption("--target <agent>", "Target agent: codex, claude, ezio, or agy").option("--collab <id>", "Send to a specific collab id (defaults to the active collab for cwd)").option("--action <action>", "Explicit requested action").option(
18971
+ collab.command("tell").description("Send an instruction to an agent").requiredOption(
18972
+ "--target <agent>",
18973
+ "Target agent: codex, claude, ezio, or agy"
18974
+ ).option(
18975
+ "--collab <id>",
18976
+ "Send to a specific collab id (defaults to the active collab for cwd)"
18977
+ ).option("--action <action>", "Explicit requested action").option(
18575
18978
  "--artifact <path>",
18576
18979
  "Artifact file path. Repeat for multiple files.",
18577
18980
  collectArtifact,
@@ -18600,7 +19003,10 @@ Not attached (stdin/stdout is not a TTY). To view the tmux session, run:
18600
19003
  console.log("No work items to process.");
18601
19004
  }
18602
19005
  });
18603
- collab.command("recover").description("Recover the current workspace collab after broker loss").option("--collab <id>", "Recover a specific collab id (defaults to the active collab for cwd)").option(
19006
+ collab.command("recover").description("Recover the current workspace collab after broker loss").option(
19007
+ "--collab <id>",
19008
+ "Recover a specific collab id (defaults to the active collab for cwd)"
19009
+ ).option(
18604
19010
  "--port <port>",
18605
19011
  "Explicit port to bind for the recovered daemon",
18606
19012
  (v) => Number.parseInt(v, 10)
@@ -18624,18 +19030,30 @@ Not attached (stdin/stdout is not a TTY). To view the tmux session, run:
18624
19030
  `Collab recovered: ${result.collabId} (pid ${result.pid}, port ${result.port})`
18625
19031
  );
18626
19032
  });
18627
- collab.command("reconnect").description("Reconnect a remembered role after broker recovery (mount mode)").argument("<agent>", "Target agent: codex, claude, ezio, or agy").option("--workspace <path>", "Workspace root", process.cwd()).option("--collab <id>", "Target a specific collab id (defaults to the active collab for cwd)").action(async (target, opts) => {
18628
- await runCollabReconnect({
18629
- workspaceRoot: opts.workspace,
18630
- ...opts.collab ? { collabIdOverride: opts.collab } : {},
18631
- target,
18632
- now: (/* @__PURE__ */ new Date()).toISOString()
18633
- });
18634
- });
18635
- collab.command("mount").description("Mount the current terminal as the managed session surface for a role").argument("<agent>", "Target agent: codex, claude, ezio, or agy").argument(
19033
+ collab.command("reconnect").description(
19034
+ "Reconnect a remembered role after broker recovery (mount mode)"
19035
+ ).argument("<agent>", "Target agent: codex, claude, ezio, or agy").option("--workspace <path>", "Workspace root", process.cwd()).option(
19036
+ "--collab <id>",
19037
+ "Target a specific collab id (defaults to the active collab for cwd)"
19038
+ ).action(
19039
+ async (target, opts) => {
19040
+ await runCollabReconnect({
19041
+ workspaceRoot: opts.workspace,
19042
+ ...opts.collab ? { collabIdOverride: opts.collab } : {},
19043
+ target,
19044
+ now: (/* @__PURE__ */ new Date()).toISOString()
19045
+ });
19046
+ }
19047
+ );
19048
+ collab.command("mount").description(
19049
+ "Mount the current terminal as the managed session surface for a role"
19050
+ ).argument("<agent>", "Target agent: codex, claude, ezio, or agy").argument(
18636
19051
  "[passthroughArgs...]",
18637
19052
  "Args forwarded after `--` to the agent binary spawn (e.g. `mount codex -- --full-auto`)"
18638
- ).option("--workspace <path>", "Workspace root", process.cwd()).option("--collab <id>", "Target a specific collab id (defaults to the active collab for cwd)").option(
19053
+ ).option("--workspace <path>", "Workspace root", process.cwd()).option(
19054
+ "--collab <id>",
19055
+ "Target a specific collab id (defaults to the active collab for cwd)"
19056
+ ).option(
18639
19057
  "--turn-events <providers>",
18640
19058
  "Scope the push turn-completion event path to the given providers (comma-separated providers with turn-end hooks: claude,codex,agy), or disable it with `off`/`none` to revert to pure clipboard capture. Overrides AI_WHISPER_TURN_EVENTS. Default: on (claude,codex,agy)."
18641
19059
  ).action(
@@ -18650,7 +19068,10 @@ Not attached (stdin/stdout is not a TTY). To view the tmux session, run:
18650
19068
  });
18651
19069
  }
18652
19070
  );
18653
- collab.command("inspect").description("Inspect the active collab thread").option("--collab <id>", "Inspect a specific collab id (defaults to the active collab for cwd)").option("--watch", "Continuously redraw the active-thread operator view").option(
19071
+ collab.command("inspect").description("Inspect the active collab thread").option(
19072
+ "--collab <id>",
19073
+ "Inspect a specific collab id (defaults to the active collab for cwd)"
19074
+ ).option("--watch", "Continuously redraw the active-thread operator view").option(
18654
19075
  "--captures [chainId]",
18655
19076
  "Show recent capture-diagnostics rows. Pass a chain id to filter, or 'all' for the full history."
18656
19077
  ).option(
@@ -18673,13 +19094,20 @@ Not attached (stdin/stdout is not a TTY). To view the tmux session, run:
18673
19094
  }
18674
19095
  }
18675
19096
  );
18676
- collab.command("relay-monitor").description("Run the relay monitor in the current terminal (renders the relay conversation stream)").option("--collab <id>", "Monitor a specific collab id (defaults to the active collab for cwd)").action(async (opts) => {
19097
+ collab.command("relay-monitor").description(
19098
+ "Run the relay monitor in the current terminal (renders the relay conversation stream)"
19099
+ ).option(
19100
+ "--collab <id>",
19101
+ "Monitor a specific collab id (defaults to the active collab for cwd)"
19102
+ ).action(async (opts) => {
18677
19103
  await runCollabRelayMonitor({
18678
19104
  cwd: process.cwd(),
18679
19105
  ...opts.collab ? { collabIdOverride: opts.collab } : {}
18680
19106
  });
18681
19107
  });
18682
- collab.command("dashboard").description("Full-screen dashboard: live wall of recently-active runs + per-run inspector").option(
19108
+ collab.command("dashboard").description(
19109
+ "Full-screen dashboard: live wall of recently-active runs + per-run inspector"
19110
+ ).option(
18683
19111
  "--window <duration>",
18684
19112
  "Eligible-collab activity window. Accepts ms or Ns/Nm/Nh/Nd (e.g. 45s, 30m, 2h, 1d), or 'all' for no limit. Default: 30m."
18685
19113
  ).option(
@@ -18716,7 +19144,7 @@ Not attached (stdin/stdout is not a TTY). To view the tmux session, run:
18716
19144
  },
18717
19145
  execCommand: (cmd) => {
18718
19146
  try {
18719
- execSync4(cmd, { stdio: "ignore" });
19147
+ execSync5(cmd, { stdio: "ignore" });
18720
19148
  } catch {
18721
19149
  }
18722
19150
  }
@@ -18736,10 +19164,43 @@ Not attached (stdin/stdout is not a TTY). To view the tmux session, run:
18736
19164
  throw err;
18737
19165
  }
18738
19166
  });
19167
+ collab.command("purge").description("Remove stale collabs (no live process) across all workspaces").option("--dry-run", "Classify and print only; never prompt or delete").option("-y, --yes", "Skip the confirmation prompt and delete").option(
19168
+ "--force",
19169
+ "Also purge collabs with a non-terminal (resumable) workflow"
19170
+ ).option("--collab <id>", "Limit the sweep to a single collab id").option("--workspace <path>", "Limit the sweep to a single workspace root").option("--json", "Emit machine-readable JSON instead of the table").action(
19171
+ async (opts) => {
19172
+ if (opts.collab && opts.workspace) {
19173
+ console.error("--collab and --workspace are mutually exclusive.");
19174
+ process.exitCode = 1;
19175
+ return;
19176
+ }
19177
+ const result = await runCollabPurge({
19178
+ cwd: process.cwd(),
19179
+ ...opts.dryRun ? { dryRun: opts.dryRun } : {},
19180
+ ...opts.yes ? { yes: opts.yes } : {},
19181
+ ...opts.force ? { force: opts.force } : {},
19182
+ ...opts.json ? { json: opts.json } : {},
19183
+ ...opts.collab ? { collabId: opts.collab } : {},
19184
+ ...opts.workspace ? { workspace: opts.workspace } : {}
19185
+ });
19186
+ process.exitCode = result.exitCode;
19187
+ }
19188
+ );
18739
19189
  const workflow = cli.command("workflow").description("Manage AI agent workflows");
18740
- workflow.command("start").description("Start a new workflow").requiredOption("--type <type>", "Workflow type (e.g. spec-driven-development)").requiredOption("--spec <path>", "Spec file path").option("--implementer <agent>", "Implementer agent: claude, codex, ezio, or agy (defaults to the workflow type's defaultImplementer)").option("--reviewer <agent>", "Reviewer agent: claude, codex, ezio, or agy (defaults to the workflow type's defaultReviewer)").option("--name <name>", "Optional workflow display name").option("--workspace <path>", "Workspace root", process.cwd()).action(
19190
+ workflow.command("start").description("Start a new workflow").requiredOption(
19191
+ "--type <type>",
19192
+ "Workflow type (e.g. spec-driven-development)"
19193
+ ).requiredOption("--spec <path>", "Spec file path").option(
19194
+ "--implementer <agent>",
19195
+ "Implementer agent: claude, codex, ezio, or agy (defaults to the workflow type's defaultImplementer)"
19196
+ ).option(
19197
+ "--reviewer <agent>",
19198
+ "Reviewer agent: claude, codex, ezio, or agy (defaults to the workflow type's defaultReviewer)"
19199
+ ).option("--name <name>", "Optional workflow display name").option("--workspace <path>", "Workspace root", process.cwd()).action(
18741
19200
  async (opts) => {
18742
- const { broker, collabId } = await connectToWorkspaceBroker({ cwd: opts.workspace });
19201
+ const { broker, collabId } = await connectToWorkspaceBroker({
19202
+ cwd: opts.workspace
19203
+ });
18743
19204
  try {
18744
19205
  const result = await runWorkflowStart({
18745
19206
  broker,
@@ -18760,7 +19221,9 @@ Not attached (stdin/stdout is not a TTY). To view the tmux session, run:
18760
19221
  }
18761
19222
  );
18762
19223
  workflow.command("list").description("List workflows for the active collab").option("--workspace <path>", "Workspace root", process.cwd()).action(async (opts) => {
18763
- const { broker, collabId } = await connectToWorkspaceBroker({ cwd: opts.workspace });
19224
+ const { broker, collabId } = await connectToWorkspaceBroker({
19225
+ cwd: opts.workspace
19226
+ });
18764
19227
  try {
18765
19228
  const list = runWorkflowList({ broker, collabId });
18766
19229
  if (list.length === 0) {
@@ -18775,7 +19238,9 @@ Not attached (stdin/stdout is not a TTY). To view the tmux session, run:
18775
19238
  }
18776
19239
  });
18777
19240
  workflow.command("inspect").description("Inspect a workflow and its phase runs").argument("<workflowId>", "Workflow ID").option("--workspace <path>", "Workspace root", process.cwd()).action(async (workflowId, opts) => {
18778
- const { broker } = await connectToWorkspaceBroker({ cwd: opts.workspace });
19241
+ const { broker } = await connectToWorkspaceBroker({
19242
+ cwd: opts.workspace
19243
+ });
18779
19244
  try {
18780
19245
  const result = await runWorkflowInspect({ broker, workflowId });
18781
19246
  console.log(JSON.stringify(result, null, 2));
@@ -18784,17 +19249,28 @@ Not attached (stdin/stdout is not a TTY). To view the tmux session, run:
18784
19249
  }
18785
19250
  });
18786
19251
  workflow.command("pause").description("Pause a running workflow").argument("<workflowId>", "Workflow ID").option("--workspace <path>", "Workspace root", process.cwd()).action(async (workflowId, opts) => {
18787
- const { broker } = await connectToWorkspaceBroker({ cwd: opts.workspace });
19252
+ const { broker } = await connectToWorkspaceBroker({
19253
+ cwd: opts.workspace
19254
+ });
18788
19255
  try {
18789
- await runWorkflowPause({ broker, workflowId, now: (/* @__PURE__ */ new Date()).toISOString() });
19256
+ await runWorkflowPause({
19257
+ broker,
19258
+ workflowId,
19259
+ now: (/* @__PURE__ */ new Date()).toISOString()
19260
+ });
18790
19261
  console.log(`Workflow paused: ${workflowId}`);
18791
19262
  } finally {
18792
19263
  await broker.stop();
18793
19264
  }
18794
19265
  });
18795
- workflow.command("resume").description("Resume a paused or halted workflow").argument("<workflowId>", "Workflow ID").option("--workspace <path>", "Workspace root", process.cwd()).option("--message <note>", "Operator note delivered to the agents on resume").action(
19266
+ workflow.command("resume").description("Resume a paused or halted workflow").argument("<workflowId>", "Workflow ID").option("--workspace <path>", "Workspace root", process.cwd()).option(
19267
+ "--message <note>",
19268
+ "Operator note delivered to the agents on resume"
19269
+ ).action(
18796
19270
  async (workflowId, opts) => {
18797
- const { broker } = await connectToWorkspaceBroker({ cwd: opts.workspace });
19271
+ const { broker } = await connectToWorkspaceBroker({
19272
+ cwd: opts.workspace
19273
+ });
18798
19274
  try {
18799
19275
  await runWorkflowResume({
18800
19276
  broker,
@@ -18809,9 +19285,15 @@ Not attached (stdin/stdout is not a TTY). To view the tmux session, run:
18809
19285
  }
18810
19286
  );
18811
19287
  workflow.command("cancel").description("Cancel a running or halted workflow").argument("<workflowId>", "Workflow ID").option("--workspace <path>", "Workspace root", process.cwd()).action(async (workflowId, opts) => {
18812
- const { broker } = await connectToWorkspaceBroker({ cwd: opts.workspace });
19288
+ const { broker } = await connectToWorkspaceBroker({
19289
+ cwd: opts.workspace
19290
+ });
18813
19291
  try {
18814
- await runWorkflowCancel({ broker, workflowId, now: (/* @__PURE__ */ new Date()).toISOString() });
19292
+ await runWorkflowCancel({
19293
+ broker,
19294
+ workflowId,
19295
+ now: (/* @__PURE__ */ new Date()).toISOString()
19296
+ });
18815
19297
  console.log(`Workflow canceled: ${workflowId}`);
18816
19298
  } finally {
18817
19299
  await broker.stop();
@@ -18823,22 +19305,37 @@ Not attached (stdin/stdout is not a TTY). To view the tmux session, run:
18823
19305
  console.log(t);
18824
19306
  }
18825
19307
  });
19308
+ workflow.command("stats").description(
19309
+ "Show accumulated hands-off time saved across all workflows (global, all-time)"
19310
+ ).option("--json", "Output raw JSON instead of the human summary").action(async (opts) => {
19311
+ const sqlitePath = getSharedSqlitePath();
19312
+ const broker = createBrokerRuntime({
19313
+ sqlitePath,
19314
+ runWorkflowDriver: false,
19315
+ runDiagnosticsSweep: false,
19316
+ runDaemonHeartbeat: false,
19317
+ runBrokerDaemonSweep: false
19318
+ });
19319
+ try {
19320
+ runWorkflowStats({ broker, json: opts.json ?? false });
19321
+ } finally {
19322
+ await broker.stop();
19323
+ }
19324
+ });
18826
19325
  const skill = cli.command("skill").description("Manage bundled agent skills");
18827
19326
  skill.command("install").description(
18828
19327
  "Install the bundled ai-whisper skills into your agent skill directories"
18829
19328
  ).addOption(
18830
19329
  new Option("--target <target>", "Agent install target").choices(["claude", "codex", "ezio", "agy", "all"]).default("all")
18831
- ).option("--force", "Overwrite existing skill destinations").action(
18832
- async (opts) => {
18833
- const result = await runSkillInstall({
18834
- target: opts.target,
18835
- ...opts.force ? { force: true } : {}
18836
- });
18837
- for (const p of result.installedAt) {
18838
- console.log(`Installed: ${p}`);
18839
- }
19330
+ ).option("--force", "Overwrite existing skill destinations").action(async (opts) => {
19331
+ const result = await runSkillInstall({
19332
+ target: opts.target,
19333
+ ...opts.force ? { force: true } : {}
19334
+ });
19335
+ for (const p of result.installedAt) {
19336
+ console.log(`Installed: ${p}`);
18840
19337
  }
18841
- );
19338
+ });
18842
19339
  cli.command("env").description(
18843
19340
  "Print machine-readable engine/integration facts for external supervisors"
18844
19341
  ).option(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-whisper",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Terminal-first relay for paired AI coding agents (Claude + Codex), driven by structured workflows.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -52,13 +52,13 @@
52
52
  "@types/better-sqlite3": "^7.6.12",
53
53
  "@types/react": "^19.2.14",
54
54
  "ink-testing-library": "^4.0.0",
55
+ "@ai-whisper/adapter-ai-ezio": "0.0.0",
56
+ "@ai-whisper/adapter-claude": "0.0.0",
55
57
  "@ai-whisper/adapter-antigravity": "0.0.0",
56
58
  "@ai-whisper/adapter-codex": "0.0.0",
57
- "@ai-whisper/adapter-claude": "0.0.0",
58
- "@ai-whisper/adapter-ai-ezio": "0.0.0",
59
59
  "@ai-whisper/broker": "0.0.0",
60
- "@ai-whisper/companion-core": "0.0.0",
61
- "@ai-whisper/shared": "0.0.0"
60
+ "@ai-whisper/shared": "0.0.0",
61
+ "@ai-whisper/companion-core": "0.0.0"
62
62
  },
63
63
  "files": [
64
64
  "dist",