@open-code-review/cli 2.1.0 → 2.2.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.
Files changed (62) hide show
  1. package/dist/dashboard/client/assets/{_basePickBy-B3ALyupE.js → _basePickBy-BAlGnwHG.js} +1 -1
  2. package/dist/dashboard/client/assets/{_baseUniq-b2RALAWc.js → _baseUniq-CoauyOeL.js} +1 -1
  3. package/dist/dashboard/client/assets/{arc-DcSVvhUd.js → arc-DtS0aHfP.js} +1 -1
  4. package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-BNUlmSCS.js → architectureDiagram-VXUJARFQ-CnWmtRTh.js} +1 -1
  5. package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-BmhiQVwa.js → blockDiagram-VD42YOAC-DgPp4oGV.js} +1 -1
  6. package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-jyJ3WOv5.js → c4Diagram-YG6GDRKO--LV4qQaE.js} +1 -1
  7. package/dist/dashboard/client/assets/channel-BU2129fl.js +1 -0
  8. package/dist/dashboard/client/assets/{chunk-4BX2VUAB-x1dQU_s3.js → chunk-4BX2VUAB-BRglpc7Z.js} +1 -1
  9. package/dist/dashboard/client/assets/{chunk-55IACEB6-CwbsE2XQ.js → chunk-55IACEB6-Bgx06_CV.js} +1 -1
  10. package/dist/dashboard/client/assets/{chunk-B4BG7PRW-BaE7c-ti.js → chunk-B4BG7PRW-D6HN3Yiy.js} +1 -1
  11. package/dist/dashboard/client/assets/{chunk-DI55MBZ5-Bw5PUaMK.js → chunk-DI55MBZ5-NH9EgN9T.js} +1 -1
  12. package/dist/dashboard/client/assets/{chunk-FMBD7UC4-B7cF6P3s.js → chunk-FMBD7UC4-xriO6WNP.js} +1 -1
  13. package/dist/dashboard/client/assets/{chunk-QN33PNHL-OY4evNHd.js → chunk-QN33PNHL-CV1h6_Zl.js} +1 -1
  14. package/dist/dashboard/client/assets/{chunk-QZHKN3VN-BpjQwIWz.js → chunk-QZHKN3VN-CV4VzxNq.js} +1 -1
  15. package/dist/dashboard/client/assets/{chunk-TZMSLE5B-D8b_Oq9B.js → chunk-TZMSLE5B-isdklocW.js} +1 -1
  16. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-CVftFGiR.js +1 -0
  17. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-CVftFGiR.js +1 -0
  18. package/dist/dashboard/client/assets/clone-DC6LEEC5.js +1 -0
  19. package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-C-sfP8PN.js → cose-bilkent-S5V4N54A-CCzlFSJf.js} +1 -1
  20. package/dist/dashboard/client/assets/{dagre-6UL2VRFP-Cqfo0NRg.js → dagre-6UL2VRFP-DVN3PkjZ.js} +1 -1
  21. package/dist/dashboard/client/assets/{diagram-PSM6KHXK-BR3ppxqI.js → diagram-PSM6KHXK-SzJVoSsb.js} +1 -1
  22. package/dist/dashboard/client/assets/{diagram-QEK2KX5R-Dvcx6x3R.js → diagram-QEK2KX5R-CgGn7ts-.js} +1 -1
  23. package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DoyBLnVN.js → diagram-S2PKOQOG-Bz1ukSx8.js} +1 -1
  24. package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-hy77l1cL.js → erDiagram-Q2GNP2WA-CpstUTMZ.js} +1 -1
  25. package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-Bz0B1rKM.js → flowDiagram-NV44I4VS-aYVydGhp.js} +1 -1
  26. package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-CLgrZPoC.js → ganttDiagram-JELNMOA3-Cb2DUSRk.js} +1 -1
  27. package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js → gitGraphDiagram-V2S2FVAM-BUOnwA2w.js} +1 -1
  28. package/dist/dashboard/client/assets/{graph-DDBMM_t2.js → graph-4X5ddhLp.js} +1 -1
  29. package/dist/dashboard/client/assets/index-CKWqYAfu.js +581 -0
  30. package/dist/dashboard/client/assets/index-CzxeSSaQ.css +1 -0
  31. package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-Bhn1FmAk.js → infoDiagram-HS3SLOUP-BlMqcrwm.js} +1 -1
  32. package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CzGbjX1y.js → journeyDiagram-XKPGCS4Q-DF2ew7ju.js} +1 -1
  33. package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Da77-WYk.js → kanban-definition-3W4ZIXB7-BKQMx0-n.js} +1 -1
  34. package/dist/dashboard/client/assets/{layout-CVwSB-GS.js → layout-DNcn2g9w.js} +1 -1
  35. package/dist/dashboard/client/assets/{linear-CTRAc5Jn.js → linear-Bqy9gvqb.js} +1 -1
  36. package/dist/dashboard/client/assets/{mermaid-renderer-Bjo170ax.js → mermaid-renderer-dJ71wgld.js} +4 -4
  37. package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-B55C2odl.js → mindmap-definition-VGOIOE7T-BARc8sqJ.js} +1 -1
  38. package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-5lrQLrSz.js → pieDiagram-ADFJNKIX-CULlNZTd.js} +1 -1
  39. package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-Bg55gC30.js → quadrantDiagram-AYHSOK5B-BJEZPVe9.js} +1 -1
  40. package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-CyR4YFJY.js → requirementDiagram-UZGBJVZJ-BhMsmUIs.js} +1 -1
  41. package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BVWKr9_-.js → sankeyDiagram-TZEHDZUN-BYbNgogG.js} +1 -1
  42. package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-D0AJg_tE.js → sequenceDiagram-WL72ISMW-MoM_NwWk.js} +1 -1
  43. package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-BuHpTgim.js → stateDiagram-FKZM4ZOC-ditrlbM3.js} +1 -1
  44. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-SqoG2LCn.js +1 -0
  45. package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-LDhpAmDd.js → timeline-definition-IT6M3QCI-DOAJyjuz.js} +1 -1
  46. package/dist/dashboard/client/assets/{treemap-GDKQZRPO-Dd4gjvUl.js → treemap-GDKQZRPO-BBJkjnJl.js} +1 -1
  47. package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-B9RDod39.js → xychartDiagram-PRI3JC2R-CPW4s5vm.js} +1 -1
  48. package/dist/dashboard/client/index.html +2 -2
  49. package/dist/dashboard/server.js +1188 -579
  50. package/dist/index.js +1395 -335
  51. package/dist/lib/db/index.js +485 -24
  52. package/dist/lib/models.js +125 -50
  53. package/dist/lib/runtime-config.js +29 -13
  54. package/dist/lib/state/index.js +2196 -0
  55. package/package.json +8 -2
  56. package/dist/dashboard/client/assets/channel-D3J8-GF_.js +0 -1
  57. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-tkFUL-1Y.js +0 -1
  58. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-tkFUL-1Y.js +0 -1
  59. package/dist/dashboard/client/assets/clone-CkY5ajLr.js +0 -1
  60. package/dist/dashboard/client/assets/index-Cr9yEo_B.js +0 -576
  61. package/dist/dashboard/client/assets/index-Z1pPudAt.css +0 -1
  62. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +0 -1
package/dist/index.js CHANGED
@@ -16017,6 +16017,86 @@ var require_emoji_regex2 = __commonJS({
16017
16017
  }
16018
16018
  });
16019
16019
 
16020
+ // ../shared/platform/src/index.ts
16021
+ import { pathToFileURL } from "node:url";
16022
+ import {
16023
+ execFile,
16024
+ execFileSync,
16025
+ spawn as spawn2
16026
+ } from "node:child_process";
16027
+ import { promisify } from "node:util";
16028
+ async function importModule(absolutePath) {
16029
+ return import(pathToFileURL(absolutePath).href);
16030
+ }
16031
+ function execBinary(binary, args, opts) {
16032
+ return execFileSync(binary, args, {
16033
+ ...opts,
16034
+ shell: isWindows
16035
+ });
16036
+ }
16037
+ async function execBinaryAsync(binary, args, opts) {
16038
+ return execFilePromise(binary, args, {
16039
+ ...opts,
16040
+ shell: isWindows
16041
+ });
16042
+ }
16043
+ function isProcessAlive(pid) {
16044
+ try {
16045
+ process.kill(pid, 0);
16046
+ return true;
16047
+ } catch (err) {
16048
+ return !(err instanceof Error && "code" in err && err.code === "ESRCH");
16049
+ }
16050
+ }
16051
+ function defaultIconFor(id, tier) {
16052
+ return BUILTIN_ICON_MAP[id] ?? (tier === "persona" ? "brain" : "user");
16053
+ }
16054
+ function hostCapabilitiesFor(vendor) {
16055
+ return vendor && HOST_CAPABILITIES[vendor] || DEFAULT_HOST_CAPABILITIES;
16056
+ }
16057
+ var execFilePromise, isWindows, BUILTIN_ICON_MAP, DEFAULT_HOST_CAPABILITIES, HOST_CAPABILITIES;
16058
+ var init_src = __esm({
16059
+ "../shared/platform/src/index.ts"() {
16060
+ "use strict";
16061
+ execFilePromise = promisify(execFile);
16062
+ isWindows = process.platform === "win32";
16063
+ BUILTIN_ICON_MAP = {
16064
+ architect: "blocks",
16065
+ fullstack: "layers",
16066
+ reliability: "activity",
16067
+ "staff-engineer": "compass",
16068
+ principal: "crown",
16069
+ frontend: "layout",
16070
+ backend: "server",
16071
+ infrastructure: "cloud",
16072
+ performance: "gauge",
16073
+ accessibility: "accessibility",
16074
+ data: "database",
16075
+ devops: "rocket",
16076
+ dx: "terminal",
16077
+ mobile: "smartphone",
16078
+ security: "shield-alert",
16079
+ quality: "sparkles",
16080
+ testing: "test-tubes",
16081
+ ai: "bot",
16082
+ "docs-writer": "file-text"
16083
+ };
16084
+ DEFAULT_HOST_CAPABILITIES = {
16085
+ subagentSpawn: false,
16086
+ perTaskModel: false
16087
+ };
16088
+ HOST_CAPABILITIES = {
16089
+ // Claude Code: Task tool + per-subagent model frontmatter.
16090
+ claude: { subagentSpawn: true, perTaskModel: true },
16091
+ // OpenCode: `--agent` sub-agent primitive, but no per-task model override.
16092
+ opencode: { subagentSpawn: true, perTaskModel: false },
16093
+ // Gemini CLI / Codex: no in-agent Task primitive → sequential Phase 4.
16094
+ gemini: { subagentSpawn: false, perTaskModel: false },
16095
+ codex: { subagentSpawn: false, perTaskModel: false }
16096
+ };
16097
+ }
16098
+ });
16099
+
16020
16100
  // ../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/identity.js
16021
16101
  var require_identity = __commonJS({
16022
16102
  "../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/identity.js"(exports) {
@@ -24044,6 +24124,35 @@ var init_migrations = __esm({
24044
24124
  db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
24045
24125
  db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
24046
24126
  }
24127
+ },
24128
+ {
24129
+ version: 14,
24130
+ description: "Self-heal markdown_artifacts duplication: collapse NULL-round duplicate rows and add a NULL-safe unique index so the dedup bug cannot recur",
24131
+ // The table's `UNIQUE(session_id, artifact_type, round_number, file_path)`
24132
+ // never deduped session-level artifacts because SQLite treats NULL ≠ NULL,
24133
+ // and the writer used `INSERT OR REPLACE` — so every re-parse of a
24134
+ // NULL-round artifact (context.md, map.md, …) appended a duplicate (one
24135
+ // context.md reached 775 identical rows, ~177 MB). The writer is now an
24136
+ // explicit UPDATE-or-INSERT; this migration heals existing DBs and adds a
24137
+ // NULL-collapsing unique index as a DB-level backstop.
24138
+ //
24139
+ // Orphan-row sweep (FK-dangling children from the pre-FK-enforcement era)
24140
+ // is intentionally NOT done here — it needs `PRAGMA foreign_keys = OFF`,
24141
+ // which is a no-op inside the migration transaction. `ocr db doctor --fix`
24142
+ // performs it outside a transaction.
24143
+ run: (db) => {
24144
+ db.run(`
24145
+ DELETE FROM markdown_artifacts
24146
+ WHERE rowid NOT IN (
24147
+ SELECT MAX(rowid) FROM markdown_artifacts
24148
+ GROUP BY session_id, artifact_type, IFNULL(round_number, -1), file_path
24149
+ )
24150
+ `);
24151
+ db.run(`
24152
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_markdown_artifacts_logical
24153
+ ON markdown_artifacts(session_id, artifact_type, IFNULL(round_number, -1), file_path)
24154
+ `);
24155
+ }
24047
24156
  }
24048
24157
  ];
24049
24158
  }
@@ -24172,7 +24281,7 @@ var init_queries = __esm({
24172
24281
 
24173
24282
  // src/lib/db/reconcile.ts
24174
24283
  import { existsSync as existsSync10 } from "node:fs";
24175
- import { isAbsolute as isAbsolute2, join as join12, dirname as dirname4 } from "node:path";
24284
+ import { isAbsolute as isAbsolute2, join as join12, dirname as dirname5 } from "node:path";
24176
24285
  function hasTerminalArtifactEvent(db, sessionId, workflowType, currentRound, currentMapRun) {
24177
24286
  const eventType = workflowType === "map" ? "map_completed" : "round_completed";
24178
24287
  const round = workflowType === "map" ? currentMapRun : currentRound;
@@ -24213,7 +24322,7 @@ function hasInFlightDependents(db, sessionId) {
24213
24322
  function resolveSessionDir(ocrDir, sessionDir) {
24214
24323
  if (!sessionDir) return null;
24215
24324
  if (isAbsolute2(sessionDir)) return sessionDir;
24216
- return join12(dirname4(ocrDir), sessionDir);
24325
+ return join12(dirname5(ocrDir), sessionDir);
24217
24326
  }
24218
24327
  function reconcileLegacyState(db, ocrDir, opts = {}) {
24219
24328
  const dryRun = opts.dryRun ?? false;
@@ -24331,7 +24440,7 @@ var init_liveness = __esm({
24331
24440
  });
24332
24441
 
24333
24442
  // src/lib/state/exit-codes.ts
24334
- var STATE_EXIT, StateError, CANCELLED_EXIT_CODE, ORPHAN_EXIT_CODE, CASCADE_CLOSE_EXIT_CODE;
24443
+ var STATE_EXIT, StateError, CANCELLED_EXIT_CODE, ORPHAN_EXIT_CODE, CASCADE_CLOSE_EXIT_CODE, WATCHDOG_DEADLINE_EXIT_CODE;
24335
24444
  var init_exit_codes = __esm({
24336
24445
  "src/lib/state/exit-codes.ts"() {
24337
24446
  "use strict";
@@ -24356,6 +24465,7 @@ var init_exit_codes = __esm({
24356
24465
  CANCELLED_EXIT_CODE = -2;
24357
24466
  ORPHAN_EXIT_CODE = -3;
24358
24467
  CASCADE_CLOSE_EXIT_CODE = -4;
24468
+ WATCHDOG_DEADLINE_EXIT_CODE = -5;
24359
24469
  }
24360
24470
  });
24361
24471
 
@@ -24738,24 +24848,431 @@ var init_agent_sessions = __esm({
24738
24848
  }
24739
24849
  });
24740
24850
 
24851
+ // src/lib/db/maintenance.ts
24852
+ import {
24853
+ existsSync as existsSync11,
24854
+ readdirSync as readdirSync5,
24855
+ statSync,
24856
+ unlinkSync as unlinkSync3,
24857
+ copyFileSync
24858
+ } from "node:fs";
24859
+ import { dirname as dirname6, join as join13, basename as basename7 } from "node:path";
24860
+ function withForeignKeysDisabled(db, fn) {
24861
+ db.pragma("foreign_keys = OFF");
24862
+ try {
24863
+ return fn();
24864
+ } finally {
24865
+ db.pragma("foreign_keys = ON");
24866
+ }
24867
+ }
24868
+ function scalarInt(db, sql) {
24869
+ const r = db.exec(sql);
24870
+ const v = r[0]?.values[0]?.[0];
24871
+ return typeof v === "number" ? v : Number(v ?? 0);
24872
+ }
24873
+ function foreignKeyViolationGroups(db) {
24874
+ const r = db.exec("PRAGMA foreign_key_check");
24875
+ const rows = r[0]?.values ?? [];
24876
+ const counts = /* @__PURE__ */ new Map();
24877
+ for (const row of rows) {
24878
+ const table = String(row[0]);
24879
+ counts.set(table, (counts.get(table) ?? 0) + 1);
24880
+ }
24881
+ return [...counts.entries()].map(([table, count]) => ({ table, count })).sort((a, b) => b.count - a.count);
24882
+ }
24883
+ function scanOrphanTempFiles(dataDir) {
24884
+ let entries;
24885
+ try {
24886
+ entries = readdirSync5(dataDir);
24887
+ } catch {
24888
+ return [];
24889
+ }
24890
+ const out = [];
24891
+ for (const name of entries) {
24892
+ const m = name.match(/^ocr\.db\.(\d+)\.tmp$/);
24893
+ if (!m) continue;
24894
+ const pid = Number(m[1]);
24895
+ let ageMs = 0;
24896
+ try {
24897
+ ageMs = Date.now() - statSync(join13(dataDir, name)).mtimeMs;
24898
+ } catch {
24899
+ continue;
24900
+ }
24901
+ const alive = isProcessAlive(pid);
24902
+ out.push({
24903
+ name,
24904
+ pid,
24905
+ ageMs,
24906
+ // Reapable only when the writer PID is dead AND the file is old enough
24907
+ // that no live mid-write could plausibly own it.
24908
+ reapable: !alive && ageMs > ONE_HOUR_MS
24909
+ });
24910
+ }
24911
+ return out;
24912
+ }
24913
+ function scanBackupFiles(dataDir, dbBase) {
24914
+ let entries;
24915
+ try {
24916
+ entries = readdirSync5(dataDir);
24917
+ } catch {
24918
+ return [];
24919
+ }
24920
+ const out = [];
24921
+ for (const name of entries) {
24922
+ if (!name.startsWith(`${dbBase}.bak`)) continue;
24923
+ try {
24924
+ out.push({ name, sizeBytes: statSync(join13(dataDir, name)).size });
24925
+ } catch {
24926
+ }
24927
+ }
24928
+ return out.sort((a, b) => b.sizeBytes - a.sizeBytes);
24929
+ }
24930
+ function collectDbHealth(db, dbPath) {
24931
+ const dataDir = dirname6(dbPath);
24932
+ const dbBase = basename7(dbPath);
24933
+ const pageSize = scalarInt(db, "PRAGMA page_size");
24934
+ const pageCount = scalarInt(db, "PRAGMA page_count");
24935
+ const freelistCount = scalarInt(db, "PRAGMA freelist_count");
24936
+ const integ = db.exec("PRAGMA integrity_check");
24937
+ const integRows = (integ[0]?.values ?? []).map((v) => String(v[0]));
24938
+ const integrityOk = integRows.length === 1 && integRows[0] === "ok";
24939
+ const allGroups = foreignKeyViolationGroups(db);
24940
+ const fkViolations = allGroups.filter((g) => !PROTECTED_TABLES.has(g.table));
24941
+ const protectedFkViolations = allGroups.filter(
24942
+ (g) => PROTECTED_TABLES.has(g.table)
24943
+ );
24944
+ const fileSizeBytes = existsSync11(dbPath) ? statSync(dbPath).size : 0;
24945
+ return {
24946
+ dbPath,
24947
+ fileSizeBytes,
24948
+ pageSize,
24949
+ pageCount,
24950
+ freelistCount,
24951
+ reclaimableBytes: freelistCount * pageSize,
24952
+ integrityOk,
24953
+ integrityErrors: integrityOk ? [] : integRows,
24954
+ fkViolations,
24955
+ protectedFkViolations,
24956
+ totalFkViolations: allGroups.reduce((n, g) => n + g.count, 0),
24957
+ markdownDuplicateRows: scalarInt(
24958
+ db,
24959
+ `SELECT COALESCE(SUM(cnt - 1), 0) FROM (
24960
+ SELECT COUNT(*) AS cnt FROM markdown_artifacts
24961
+ GROUP BY session_id, artifact_type, IFNULL(round_number, -1), file_path
24962
+ HAVING cnt > 1)`
24963
+ ),
24964
+ orphanTempFiles: scanOrphanTempFiles(dataDir),
24965
+ backupFiles: scanBackupFiles(dataDir, dbBase),
24966
+ eventCount: scalarInt(db, "SELECT COUNT(*) FROM orchestration_events"),
24967
+ sessionCount: scalarInt(db, "SELECT COUNT(*) FROM sessions")
24968
+ };
24969
+ }
24970
+ function snapshotDb(db, dbPath, label = "doctor") {
24971
+ try {
24972
+ if (!existsSync11(dbPath) || statSync(dbPath).size === 0) return null;
24973
+ db.pragma("wal_checkpoint(TRUNCATE)");
24974
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
24975
+ const bakPath = `${dbPath}.bak.${label}.${ts}`;
24976
+ copyFileSync(dbPath, bakPath);
24977
+ return bakPath;
24978
+ } catch {
24979
+ return null;
24980
+ }
24981
+ }
24982
+ function reapOrphanDbFiles(dataDir) {
24983
+ const reaped = [];
24984
+ for (const f of scanOrphanTempFiles(dataDir)) {
24985
+ if (!f.reapable) continue;
24986
+ try {
24987
+ unlinkSync3(join13(dataDir, f.name));
24988
+ reaped.push(f.name);
24989
+ } catch {
24990
+ }
24991
+ }
24992
+ return reaped;
24993
+ }
24994
+ function reapStaleExecLogs(execLogsDir, maxAgeMs = SEVEN_DAYS_MS) {
24995
+ let entries;
24996
+ try {
24997
+ entries = readdirSync5(execLogsDir);
24998
+ } catch {
24999
+ return [];
25000
+ }
25001
+ const cutoff = Date.now() - maxAgeMs;
25002
+ const reaped = [];
25003
+ for (const name of entries) {
25004
+ if (!name.endsWith(".log")) continue;
25005
+ const full = join13(execLogsDir, name);
25006
+ try {
25007
+ if (statSync(full).mtimeMs > cutoff) continue;
25008
+ unlinkSync3(full);
25009
+ reaped.push(name);
25010
+ } catch {
25011
+ }
25012
+ }
25013
+ return reaped;
25014
+ }
25015
+ function pruneBackups(dataDir, dbPath, opts = {}) {
25016
+ const keep = opts.keep ?? 1;
25017
+ if (!Number.isInteger(keep) || keep < 0) {
25018
+ throw new Error(
25019
+ `pruneBackups: keep must be a non-negative integer (got ${String(keep)})`
25020
+ );
25021
+ }
25022
+ const dryRun = opts.dryRun ?? false;
25023
+ const dbBase = basename7(dbPath);
25024
+ const withMtime = [];
25025
+ for (const file of scanBackupFiles(dataDir, dbBase)) {
25026
+ try {
25027
+ withMtime.push({ file, mtimeMs: statSync(join13(dataDir, file.name)).mtimeMs });
25028
+ } catch {
25029
+ }
25030
+ }
25031
+ withMtime.sort((a, b) => b.mtimeMs - a.mtimeMs);
25032
+ const kept = withMtime.slice(0, keep).map((x) => x.file);
25033
+ const toDelete = withMtime.slice(keep).map((x) => x.file);
25034
+ const deleted = [];
25035
+ if (!dryRun) {
25036
+ for (const b of toDelete) {
25037
+ try {
25038
+ unlinkSync3(join13(dataDir, b.name));
25039
+ deleted.push(b);
25040
+ } catch {
25041
+ }
25042
+ }
25043
+ }
25044
+ const reported = dryRun ? toDelete : deleted;
25045
+ return {
25046
+ dryRun,
25047
+ deleted: reported,
25048
+ kept,
25049
+ reclaimedBytes: reported.reduce((n, b) => n + b.sizeBytes, 0)
25050
+ };
25051
+ }
25052
+ function fixDb(db, dbPath, opts = {}) {
25053
+ const dataDir = dirname6(dbPath);
25054
+ const sizeBeforeBytes = existsSync11(dbPath) ? statSync(dbPath).size : 0;
25055
+ const snapshotPath = opts.snapshot === false ? null : snapshotDb(db, dbPath, "doctor");
25056
+ const fkOrphansDeleted = [];
25057
+ withForeignKeysDisabled(db, () => {
25058
+ db.transaction(() => {
25059
+ for (const sweep of ORPHAN_SWEEPS) {
25060
+ const info = db.prepare(sweep.sql).run();
25061
+ const count = Number(info.changes);
25062
+ if (count > 0) fkOrphansDeleted.push({ table: sweep.table, count });
25063
+ }
25064
+ });
25065
+ });
25066
+ let markdownDupsDeleted = 0;
25067
+ db.transaction(() => {
25068
+ const info = db.prepare(MARKDOWN_DEDUP_SQL).run();
25069
+ markdownDupsDeleted = Number(info.changes);
25070
+ });
25071
+ const tempsReaped = opts.reapTemps === false ? [] : reapOrphanDbFiles(dataDir);
25072
+ let vacuumed = false;
25073
+ if (opts.vacuum !== false) {
25074
+ try {
25075
+ db.pragma("wal_checkpoint(TRUNCATE)");
25076
+ db.run("VACUUM");
25077
+ vacuumed = true;
25078
+ } catch {
25079
+ vacuumed = false;
25080
+ }
25081
+ }
25082
+ const post = collectDbHealth(db, dbPath);
25083
+ return {
25084
+ snapshotPath,
25085
+ fkOrphansDeleted,
25086
+ totalFkOrphansDeleted: fkOrphansDeleted.reduce((n, g) => n + g.count, 0),
25087
+ protectedViolationsRemaining: post.protectedFkViolations,
25088
+ markdownDupsDeleted,
25089
+ tempsReaped,
25090
+ vacuumed,
25091
+ sizeBeforeBytes,
25092
+ sizeAfterBytes: post.fileSizeBytes,
25093
+ integrityOkAfter: post.integrityOk,
25094
+ fkViolationsAfter: post.totalFkViolations
25095
+ };
25096
+ }
25097
+ function vacuumDb(db, dbPath, opts = {}) {
25098
+ const sizeBeforeBytes = existsSync11(dbPath) ? statSync(dbPath).size : 0;
25099
+ const snapshotPath = opts.snapshot === false ? null : snapshotDb(db, dbPath, "vacuum");
25100
+ db.pragma("wal_checkpoint(TRUNCATE)");
25101
+ db.run("VACUUM");
25102
+ db.pragma("wal_checkpoint(TRUNCATE)");
25103
+ const sizeAfterBytes = existsSync11(dbPath) ? statSync(dbPath).size : 0;
25104
+ return {
25105
+ snapshotPath,
25106
+ sizeBeforeBytes,
25107
+ sizeAfterBytes,
25108
+ reclaimedBytes: Math.max(0, sizeBeforeBytes - sizeAfterBytes)
25109
+ };
25110
+ }
25111
+ function countSessionArtifacts(db, sessionId) {
25112
+ const r = db.exec(
25113
+ `SELECT
25114
+ (SELECT COUNT(*) FROM markdown_artifacts WHERE session_id = ?) +
25115
+ (SELECT COUNT(*) FROM review_rounds WHERE session_id = ?) +
25116
+ (SELECT COUNT(*) FROM reviewer_outputs ro JOIN review_rounds rr ON ro.round_id = rr.id WHERE rr.session_id = ?) +
25117
+ (SELECT COUNT(*) FROM review_findings rf JOIN reviewer_outputs ro ON rf.reviewer_output_id = ro.id JOIN review_rounds rr ON ro.round_id = rr.id WHERE rr.session_id = ?) +
25118
+ (SELECT COUNT(*) FROM map_runs WHERE session_id = ?) +
25119
+ (SELECT COUNT(*) FROM chat_conversations WHERE session_id = ?)`,
25120
+ Array(6).fill(sessionId)
25121
+ );
25122
+ const v = r[0]?.values[0]?.[0];
25123
+ return typeof v === "number" ? v : Number(v ?? 0);
25124
+ }
25125
+ function pruneDb(db, dbPath, opts = {}) {
25126
+ const dryRun = opts.dryRun ?? false;
25127
+ const hasBound = opts.olderThanDays !== void 0 || opts.keepSessions !== void 0;
25128
+ if (!hasBound) {
25129
+ return { dryRun, snapshotPath: null, prunedSessions: [], totalArtifactRows: 0 };
25130
+ }
25131
+ const rows = db.exec(
25132
+ `SELECT s.id,
25133
+ (SELECT (julianday('now') - julianday(MAX(e.created_at))) * 86400
25134
+ FROM orchestration_events e WHERE e.session_id = s.id) AS quiet_seconds
25135
+ FROM sessions s
25136
+ WHERE s.status = 'closed'
25137
+ ORDER BY quiet_seconds ASC`
25138
+ );
25139
+ const closed = (rows[0]?.values ?? []).map((v) => ({
25140
+ id: String(v[0]),
25141
+ quietSeconds: typeof v[1] === "number" ? v[1] : Number(v[1] ?? 0)
25142
+ }));
25143
+ const keepN = opts.keepSessions ?? 0;
25144
+ const olderThanSeconds = opts.olderThanDays !== void 0 ? opts.olderThanDays * 86400 : null;
25145
+ const targets = closed.filter((s, idx) => {
25146
+ if (idx < keepN) return false;
25147
+ if (olderThanSeconds !== null && s.quietSeconds < olderThanSeconds)
25148
+ return false;
25149
+ return true;
25150
+ });
25151
+ const prunedSessions = [];
25152
+ for (const t of targets) {
25153
+ const artifactRows = countSessionArtifacts(db, t.id);
25154
+ if (artifactRows === 0) continue;
25155
+ prunedSessions.push({ sessionId: t.id, artifactRows });
25156
+ }
25157
+ if (dryRun || prunedSessions.length === 0) {
25158
+ return {
25159
+ dryRun,
25160
+ snapshotPath: null,
25161
+ prunedSessions,
25162
+ totalArtifactRows: prunedSessions.reduce((n, p) => n + p.artifactRows, 0)
25163
+ };
25164
+ }
25165
+ const snapshotPath = snapshotDb(db, dbPath, "prune");
25166
+ db.transaction(() => {
25167
+ for (const p of prunedSessions) {
25168
+ db.run("DELETE FROM review_rounds WHERE session_id = ?", [p.sessionId]);
25169
+ db.run("DELETE FROM map_runs WHERE session_id = ?", [p.sessionId]);
25170
+ db.run("DELETE FROM markdown_artifacts WHERE session_id = ?", [p.sessionId]);
25171
+ db.run("DELETE FROM chat_conversations WHERE session_id = ?", [p.sessionId]);
25172
+ }
25173
+ });
25174
+ return {
25175
+ dryRun,
25176
+ snapshotPath,
25177
+ prunedSessions,
25178
+ totalArtifactRows: prunedSessions.reduce((n, p) => n + p.artifactRows, 0)
25179
+ };
25180
+ }
25181
+ var PROTECTED_TABLES, ORPHAN_SWEEPS, MARKDOWN_DEDUP_SQL, ONE_HOUR_MS, SEVEN_DAYS_MS;
25182
+ var init_maintenance = __esm({
25183
+ "src/lib/db/maintenance.ts"() {
25184
+ "use strict";
25185
+ init_src();
25186
+ PROTECTED_TABLES = /* @__PURE__ */ new Set([
25187
+ "sessions",
25188
+ "orchestration_events",
25189
+ "agent_sessions",
25190
+ "command_executions",
25191
+ "schema_version"
25192
+ ]);
25193
+ ORPHAN_SWEEPS = [
25194
+ // session-rooted parents first
25195
+ {
25196
+ table: "review_rounds",
25197
+ sql: "DELETE FROM review_rounds WHERE session_id NOT IN (SELECT id FROM sessions)"
25198
+ },
25199
+ {
25200
+ table: "map_runs",
25201
+ sql: "DELETE FROM map_runs WHERE session_id NOT IN (SELECT id FROM sessions)"
25202
+ },
25203
+ {
25204
+ table: "markdown_artifacts",
25205
+ sql: "DELETE FROM markdown_artifacts WHERE session_id NOT IN (SELECT id FROM sessions)"
25206
+ },
25207
+ {
25208
+ table: "chat_conversations",
25209
+ sql: "DELETE FROM chat_conversations WHERE session_id NOT IN (SELECT id FROM sessions)"
25210
+ },
25211
+ // second level (pick up parents deleted above)
25212
+ {
25213
+ table: "reviewer_outputs",
25214
+ sql: "DELETE FROM reviewer_outputs WHERE round_id NOT IN (SELECT id FROM review_rounds)"
25215
+ },
25216
+ {
25217
+ table: "map_sections",
25218
+ sql: "DELETE FROM map_sections WHERE map_run_id NOT IN (SELECT id FROM map_runs)"
25219
+ },
25220
+ {
25221
+ table: "chat_messages",
25222
+ sql: "DELETE FROM chat_messages WHERE conversation_id NOT IN (SELECT id FROM chat_conversations)"
25223
+ },
25224
+ {
25225
+ table: "user_round_progress",
25226
+ sql: "DELETE FROM user_round_progress WHERE round_id NOT IN (SELECT id FROM review_rounds)"
25227
+ },
25228
+ // third level
25229
+ {
25230
+ table: "review_findings",
25231
+ sql: "DELETE FROM review_findings WHERE reviewer_output_id NOT IN (SELECT id FROM reviewer_outputs)"
25232
+ },
25233
+ {
25234
+ table: "map_files",
25235
+ sql: "DELETE FROM map_files WHERE section_id NOT IN (SELECT id FROM map_sections)"
25236
+ },
25237
+ // leaves
25238
+ {
25239
+ table: "user_finding_progress",
25240
+ sql: "DELETE FROM user_finding_progress WHERE finding_id NOT IN (SELECT id FROM review_findings)"
25241
+ },
25242
+ {
25243
+ table: "user_file_progress",
25244
+ sql: "DELETE FROM user_file_progress WHERE map_file_id NOT IN (SELECT id FROM map_files)"
25245
+ }
25246
+ ];
25247
+ MARKDOWN_DEDUP_SQL = `
25248
+ DELETE FROM markdown_artifacts
25249
+ WHERE rowid NOT IN (
25250
+ SELECT MAX(rowid) FROM markdown_artifacts
25251
+ GROUP BY session_id, artifact_type, IFNULL(round_number, -1), file_path
25252
+ )`;
25253
+ ONE_HOUR_MS = 60 * 60 * 1e3;
25254
+ SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
25255
+ }
25256
+ });
25257
+
24741
25258
  // src/lib/db/command-log.ts
24742
- import { appendFileSync, existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync6 } from "node:fs";
24743
- import { dirname as dirname5, join as join13 } from "node:path";
25259
+ import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync6 } from "node:fs";
25260
+ import { dirname as dirname7, join as join14 } from "node:path";
24744
25261
  import { randomUUID as randomUUID2 } from "node:crypto";
24745
25262
  function generateCommandUid() {
24746
25263
  return randomUUID2();
24747
25264
  }
24748
25265
  function cacheDir(ocrDir) {
24749
- return join13(ocrDir, "data", CACHE_DIR);
25266
+ return join14(ocrDir, "data", CACHE_DIR);
24750
25267
  }
24751
25268
  function commandLogPath(ocrDir) {
24752
- return join13(cacheDir(ocrDir), FILENAME);
25269
+ return join14(cacheDir(ocrDir), FILENAME);
24753
25270
  }
24754
25271
  function appendCommandLog(ocrDir, entry) {
24755
25272
  try {
24756
25273
  const filePath = commandLogPath(ocrDir);
24757
- const dir = dirname5(filePath);
24758
- if (!existsSync11(dir)) mkdirSync3(dir, { recursive: true });
25274
+ const dir = dirname7(filePath);
25275
+ if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
24759
25276
  const line = JSON.stringify(entry) + "\n";
24760
25277
  appendFileSync(filePath, line, { encoding: "utf-8" });
24761
25278
  if (approxLineCount >= 0) approxLineCount++;
@@ -24765,7 +25282,7 @@ function appendCommandLog(ocrDir, entry) {
24765
25282
  }
24766
25283
  function readCommandLog(ocrDir) {
24767
25284
  const filePath = commandLogPath(ocrDir);
24768
- if (!existsSync11(filePath)) return [];
25285
+ if (!existsSync12(filePath)) return [];
24769
25286
  const content = readFileSync9(filePath, "utf-8");
24770
25287
  const entries = [];
24771
25288
  for (const line of content.split("\n")) {
@@ -24851,6 +25368,7 @@ __export(db_exports, {
24851
25368
  PID_REUSE_GUARD_MS: () => PID_REUSE_GUARD_MS,
24852
25369
  STATE_EXIT: () => STATE_EXIT,
24853
25370
  StateError: () => StateError,
25371
+ WATCHDOG_DEADLINE_EXIT_CODE: () => WATCHDOG_DEADLINE_EXIT_CODE,
24854
25372
  appendCommandLog: () => appendCommandLog,
24855
25373
  bindVendorSessionIdOpportunistically: () => bindVendorSessionIdOpportunistically,
24856
25374
  bumpAgentSessionHeartbeat: () => bumpAgentSessionHeartbeat,
@@ -24858,10 +25376,12 @@ __export(db_exports, {
24858
25376
  cascadeTerminateExecutions: () => cascadeTerminateExecutions,
24859
25377
  closeAllDatabases: () => closeAllDatabases,
24860
25378
  closeDatabase: () => closeDatabase,
25379
+ collectDbHealth: () => collectDbHealth,
24861
25380
  commandLogPath: () => commandLogPath,
24862
25381
  commitReasonClose: () => commitReasonClose,
24863
25382
  defaultIsAlive: () => defaultIsAlive,
24864
25383
  ensureDatabase: () => ensureDatabase,
25384
+ fixDb: () => fixDb,
24865
25385
  formatUpgradeNotice: () => formatUpgradeNotice,
24866
25386
  generateCommandUid: () => generateCommandUid,
24867
25387
  getAgentSession: () => getAgentSession,
@@ -24873,6 +25393,7 @@ __export(db_exports, {
24873
25393
  getLatestEventId: () => getLatestEventId,
24874
25394
  getSchemaVersion: () => getSchemaVersion,
24875
25395
  getSession: () => getSession,
25396
+ hasInFlightDependents: () => hasInFlightDependents,
24876
25397
  insertAgentSession: () => insertAgentSession,
24877
25398
  insertEvent: () => insertEvent,
24878
25399
  insertSession: () => insertSession,
@@ -24882,7 +25403,11 @@ __export(db_exports, {
24882
25403
  openDatabase: () => openDatabase,
24883
25404
  probeEngine: () => probeEngine,
24884
25405
  probeWrite: () => probeWrite,
25406
+ pruneBackups: () => pruneBackups,
25407
+ pruneDb: () => pruneDb,
24885
25408
  readCommandLog: () => readCommandLog,
25409
+ reapOrphanDbFiles: () => reapOrphanDbFiles,
25410
+ reapStaleExecLogs: () => reapStaleExecLogs,
24886
25411
  reconcileLegacyState: () => reconcileLegacyState,
24887
25412
  recordVendorSessionIdForExecution: () => recordVendorSessionIdForExecution,
24888
25413
  replayCommandLog: () => replayCommandLog,
@@ -24892,31 +25417,34 @@ __export(db_exports, {
24892
25417
  runMigrations: () => runMigrations,
24893
25418
  setAgentSessionStatus: () => setAgentSessionStatus,
24894
25419
  setAgentSessionVendorId: () => setAgentSessionVendorId,
25420
+ snapshotDb: () => snapshotDb,
24895
25421
  sqliteUtcMs: () => sqliteUtcMs,
24896
25422
  sweepStaleAgentSessions: () => sweepStaleAgentSessions,
24897
25423
  sweepStaleSessions: () => sweepStaleSessions,
24898
25424
  updateAgentSession: () => updateAgentSession,
24899
25425
  updateSession: () => updateSession,
24900
- walCheckpointTruncate: () => walCheckpointTruncate
25426
+ vacuumDb: () => vacuumDb,
25427
+ walCheckpointTruncate: () => walCheckpointTruncate,
25428
+ withForeignKeysDisabled: () => withForeignKeysDisabled
24901
25429
  });
24902
25430
  import {
24903
- existsSync as existsSync12,
24904
- mkdirSync as mkdirSync4,
24905
- copyFileSync,
24906
- statSync,
25431
+ existsSync as existsSync13,
25432
+ mkdirSync as mkdirSync5,
25433
+ copyFileSync as copyFileSync2,
25434
+ statSync as statSync2,
24907
25435
  mkdtempSync,
24908
25436
  rmSync
24909
25437
  } from "node:fs";
24910
25438
  import { tmpdir } from "node:os";
24911
- import { dirname as dirname6, join as join14 } from "node:path";
25439
+ import { dirname as dirname8, join as join15 } from "node:path";
24912
25440
  function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
24913
25441
  if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
24914
25442
  const bakPath = `${dbPath}.bak.v${fromVersion}`;
24915
- if (existsSync12(bakPath)) return bakPath;
25443
+ if (existsSync13(bakPath)) return bakPath;
24916
25444
  try {
24917
- if (!existsSync12(dbPath) || statSync(dbPath).size === 0) return null;
25445
+ if (!existsSync13(dbPath) || statSync2(dbPath).size === 0) return null;
24918
25446
  db.pragma("wal_checkpoint(TRUNCATE)");
24919
- copyFileSync(dbPath, bakPath);
25447
+ copyFileSync2(dbPath, bakPath);
24920
25448
  return bakPath;
24921
25449
  } catch {
24922
25450
  return null;
@@ -24949,24 +25477,24 @@ async function openDatabase(dbPath) {
24949
25477
  if (cached) {
24950
25478
  return cached;
24951
25479
  }
24952
- const dir = dirname6(dbPath);
24953
- if (!existsSync12(dir)) {
24954
- mkdirSync4(dir, { recursive: true });
25480
+ const dir = dirname8(dbPath);
25481
+ if (!existsSync13(dir)) {
25482
+ mkdirSync5(dir, { recursive: true });
24955
25483
  }
24956
25484
  const db = openEngine(dbPath);
24957
25485
  connections.set(dbPath, db);
24958
25486
  return db;
24959
25487
  }
24960
25488
  async function getDb(ocrDir) {
24961
- const dbPath = join14(ocrDir, "data", "ocr.db");
25489
+ const dbPath = join15(ocrDir, "data", "ocr.db");
24962
25490
  return openDatabase(dbPath);
24963
25491
  }
24964
25492
  async function ensureDatabase(ocrDir) {
24965
- const dataDir = join14(ocrDir, "data");
24966
- if (!existsSync12(dataDir)) {
24967
- mkdirSync4(dataDir, { recursive: true });
25493
+ const dataDir = join15(ocrDir, "data");
25494
+ if (!existsSync13(dataDir)) {
25495
+ mkdirSync5(dataDir, { recursive: true });
24968
25496
  }
24969
- const dbPath = join14(dataDir, "ocr.db");
25497
+ const dbPath = join15(dataDir, "ocr.db");
24970
25498
  const db = await openDatabase(dbPath);
24971
25499
  let before = 0;
24972
25500
  try {
@@ -24994,7 +25522,7 @@ async function ensureDatabase(ocrDir) {
24994
25522
  return db;
24995
25523
  }
24996
25524
  function walCheckpointTruncate(dbPath) {
24997
- if (!existsSync12(dbPath)) {
25525
+ if (!existsSync13(dbPath)) {
24998
25526
  return "skipped";
24999
25527
  }
25000
25528
  const cached = connections.get(dbPath);
@@ -25036,8 +25564,8 @@ function closeAllDatabases() {
25036
25564
  function probeWrite() {
25037
25565
  let dir;
25038
25566
  try {
25039
- dir = mkdtempSync(join14(tmpdir(), "ocr-probe-"));
25040
- const db = openEngine(join14(dir, "probe.db"));
25567
+ dir = mkdtempSync(join15(tmpdir(), "ocr-probe-"));
25568
+ const db = openEngine(join15(dir, "probe.db"));
25041
25569
  try {
25042
25570
  db.run("CREATE TABLE _probe_write (id INTEGER PRIMARY KEY, v TEXT)");
25043
25571
  db.transaction(() => {
@@ -25083,6 +25611,7 @@ var init_db = __esm({
25083
25611
  init_result_mapper();
25084
25612
  init_engine();
25085
25613
  init_reconcile();
25614
+ init_maintenance();
25086
25615
  init_migrations();
25087
25616
  init_command_log();
25088
25617
  V2_SCHEMA_VERSION = 12;
@@ -28613,6 +29142,8 @@ function ora(options) {
28613
29142
  }
28614
29143
 
28615
29144
  // src/lib/config.ts
29145
+ init_src();
29146
+ init_src();
28616
29147
  var AI_TOOLS = [
28617
29148
  {
28618
29149
  id: "amazon-q",
@@ -28636,7 +29167,9 @@ var AI_TOOLS = [
28636
29167
  configDir: ".claude",
28637
29168
  commandsDir: ".claude/commands",
28638
29169
  skillsDir: ".claude/skills",
28639
- commandStrategy: "subdirectory"
29170
+ commandStrategy: "subdirectory",
29171
+ instructionFiles: [{ path: "CLAUDE.md", format: "markdown" }],
29172
+ vendorBinary: "claude"
28640
29173
  },
28641
29174
  {
28642
29175
  id: "cline",
@@ -28652,7 +29185,9 @@ var AI_TOOLS = [
28652
29185
  configDir: ".codex",
28653
29186
  commandsDir: ".codex/commands",
28654
29187
  skillsDir: ".codex/skills",
28655
- commandStrategy: "subdirectory"
29188
+ commandStrategy: "subdirectory",
29189
+ // Codex reads AGENTS.md natively — no extra instruction file.
29190
+ vendorBinary: "codex"
28656
29191
  },
28657
29192
  {
28658
29193
  id: "continue",
@@ -28676,7 +29211,9 @@ var AI_TOOLS = [
28676
29211
  configDir: ".gemini",
28677
29212
  commandsDir: ".gemini/commands",
28678
29213
  skillsDir: ".gemini/skills",
28679
- commandStrategy: "subdirectory"
29214
+ commandStrategy: "subdirectory",
29215
+ instructionFiles: [{ path: "GEMINI.md", format: "markdown" }],
29216
+ vendorBinary: "gemini"
28680
29217
  },
28681
29218
  {
28682
29219
  id: "github-copilot",
@@ -28684,7 +29221,10 @@ var AI_TOOLS = [
28684
29221
  configDir: ".github",
28685
29222
  commandsDir: ".github/commands",
28686
29223
  skillsDir: ".github/skills",
28687
- commandStrategy: "subdirectory"
29224
+ commandStrategy: "subdirectory",
29225
+ instructionFiles: [
29226
+ { path: ".github/copilot-instructions.md", format: "markdown" }
29227
+ ]
28688
29228
  },
28689
29229
  {
28690
29230
  id: "kilo-code",
@@ -28700,7 +29240,9 @@ var AI_TOOLS = [
28700
29240
  configDir: ".opencode",
28701
29241
  commandsDir: ".opencode/commands",
28702
29242
  skillsDir: ".opencode/skills",
28703
- commandStrategy: "subdirectory"
29243
+ commandStrategy: "subdirectory",
29244
+ // OpenCode reads AGENTS.md natively — no extra instruction file.
29245
+ vendorBinary: "opencode"
28704
29246
  },
28705
29247
  {
28706
29248
  id: "qoder",
@@ -28724,9 +29266,16 @@ var AI_TOOLS = [
28724
29266
  configDir: ".windsurf",
28725
29267
  commandsDir: ".windsurf/workflows",
28726
29268
  skillsDir: ".windsurf/skills",
28727
- commandStrategy: "flat-prefixed"
29269
+ commandStrategy: "flat-prefixed",
29270
+ instructionFiles: [{ path: ".windsurfrules", format: "plaintext" }]
28728
29271
  }
28729
29272
  ];
29273
+ function getToolById(id) {
29274
+ return AI_TOOLS.find((tool) => tool.id === id);
29275
+ }
29276
+ function getHostCapabilities(id) {
29277
+ return hostCapabilitiesFor(id);
29278
+ }
28730
29279
  function getToolIds() {
28731
29280
  return AI_TOOLS.map((tool) => tool.id);
28732
29281
  }
@@ -28787,11 +29336,11 @@ function ensureGitignore(ocrDir) {
28787
29336
  const gitignorePath = join(ocrDir, ".gitignore");
28788
29337
  const block = buildManagedBlock();
28789
29338
  let content = existsSync(gitignorePath) ? readFileSync2(gitignorePath, "utf-8") : "";
28790
- const blockRegex = new RegExp(
29339
+ const blockRegex2 = new RegExp(
28791
29340
  `${escapeRegex(START_MARKER)}[\\s\\S]*?${escapeRegex(END_MARKER)}\\n?`,
28792
29341
  "g"
28793
29342
  );
28794
- if (blockRegex.test(content)) {
29343
+ if (blockRegex2.test(content)) {
28795
29344
  content = content.replace(
28796
29345
  new RegExp(
28797
29346
  `${escapeRegex(START_MARKER)}[\\s\\S]*?${escapeRegex(END_MARKER)}\\n?`,
@@ -28981,6 +29530,7 @@ function resolveTeamComposition(team, override) {
28981
29530
  }
28982
29531
 
28983
29532
  // src/lib/installer.ts
29533
+ init_src();
28984
29534
  var require2 = createRequire(import.meta.url);
28985
29535
  function ensureDir(dir) {
28986
29536
  if (!existsSync3(dir)) {
@@ -29089,27 +29639,6 @@ function installCommandsForTool(tool, commandsSource, targetDir) {
29089
29639
  return false;
29090
29640
  }
29091
29641
  }
29092
- var BUILTIN_ICON_MAP = {
29093
- architect: "blocks",
29094
- fullstack: "layers",
29095
- reliability: "activity",
29096
- "staff-engineer": "compass",
29097
- principal: "crown",
29098
- frontend: "layout",
29099
- backend: "server",
29100
- infrastructure: "cloud",
29101
- performance: "gauge",
29102
- accessibility: "accessibility",
29103
- data: "database",
29104
- devops: "rocket",
29105
- dx: "terminal",
29106
- mobile: "smartphone",
29107
- security: "shield-alert",
29108
- quality: "sparkles",
29109
- testing: "test-tubes",
29110
- ai: "bot",
29111
- "docs-writer": "file-text"
29112
- };
29113
29642
  var HOLISTIC_IDS = /* @__PURE__ */ new Set(["architect", "fullstack", "reliability", "staff-engineer", "principal"]);
29114
29643
  var SPECIALIST_IDS = /* @__PURE__ */ new Set([
29115
29644
  "frontend",
@@ -29212,7 +29741,7 @@ function generateReviewersMeta(reviewersDir, configPath) {
29212
29741
  id,
29213
29742
  name: extractReviewerName(content),
29214
29743
  tier,
29215
- icon: BUILTIN_ICON_MAP[id] ?? (tier === "persona" ? "brain" : "user"),
29744
+ icon: defaultIconFor(id, tier),
29216
29745
  description: extractReviewerDescription(content),
29217
29746
  focus_areas: extractFocusAreas(content),
29218
29747
  is_default: defaultTeamIds.has(id),
@@ -29313,7 +29842,9 @@ function installForTool(tool, targetDir) {
29313
29842
  if (meta) {
29314
29843
  writeFileSync3(metaPath, JSON.stringify(meta, null, 2) + "\n");
29315
29844
  }
29316
- } catch {
29845
+ } catch (err) {
29846
+ const msg = err instanceof Error ? err.message : "unknown error";
29847
+ warnings.push(`Could not generate reviewers-meta.json: ${msg}`);
29317
29848
  }
29318
29849
  const commandsOk = installCommandsForTool(tool, commandsSource, targetDir);
29319
29850
  if (!commandsOk) {
@@ -29347,12 +29878,14 @@ function detectInstalledTools(targetDir, tools) {
29347
29878
  }
29348
29879
 
29349
29880
  // src/lib/injector.ts
29350
- import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
29351
- import { join as join4 } from "node:path";
29352
- var START_MARKER2 = "<!-- OCR:START -->";
29353
- var END_MARKER2 = "<!-- OCR:END -->";
29354
- var OCR_INSTRUCTION_BLOCK = `${START_MARKER2}
29355
- ## Open Code Review Instructions
29881
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
29882
+ import { dirname as dirname2, join as join4 } from "node:path";
29883
+ var AGENTS_MD = { path: "AGENTS.md", format: "markdown" };
29884
+ var MARKERS = {
29885
+ markdown: { start: "<!-- OCR:START -->", end: "<!-- OCR:END -->" },
29886
+ plaintext: { start: "# OCR:START", end: "# OCR:END" }
29887
+ };
29888
+ var OCR_INSTRUCTION_BODY = `## Open Code Review Instructions
29356
29889
 
29357
29890
  These instructions are for AI assistants handling code review in this project.
29358
29891
 
@@ -29368,37 +29901,95 @@ Use \`.ocr/skills/SKILL.md\` to learn:
29368
29901
  - Available reviewer personas and their focus areas
29369
29902
  - Session management and output format
29370
29903
 
29371
- Keep this managed block so \`ocr init\` can refresh the instructions.
29372
-
29373
- ${END_MARKER2}`;
29374
- function injectOcrInstructions(filePath) {
29904
+ Keep this managed block so \`ocr init\` can refresh the instructions.`;
29905
+ function buildBlock(format) {
29906
+ const { start, end } = MARKERS[format];
29907
+ return `${start}
29908
+ ${OCR_INSTRUCTION_BODY}
29909
+ ${end}`;
29910
+ }
29911
+ function escapeRegex2(str) {
29912
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29913
+ }
29914
+ function blockRegex(format) {
29915
+ const { start, end } = MARKERS[format];
29916
+ return new RegExp(
29917
+ `${escapeRegex2(start)}[\\s\\S]*?${escapeRegex2(end)}\\n?`,
29918
+ "g"
29919
+ );
29920
+ }
29921
+ function injectOcrInstructions(filePath, format = "markdown") {
29375
29922
  try {
29923
+ mkdirSync2(dirname2(filePath), { recursive: true });
29376
29924
  let content = existsSync4(filePath) ? readFileSync5(filePath, "utf-8") : "";
29377
- const regex2 = new RegExp(
29378
- `${escapeRegex2(START_MARKER2)}[\\s\\S]*?${escapeRegex2(END_MARKER2)}\\n?`,
29379
- "g"
29380
- );
29381
- content = content.replace(regex2, "");
29925
+ content = content.replace(blockRegex(format), "");
29382
29926
  content = content.trim();
29383
29927
  if (content.length > 0) {
29384
29928
  content += "\n\n";
29385
29929
  }
29386
- content += OCR_INSTRUCTION_BLOCK + "\n";
29930
+ content += buildBlock(format) + "\n";
29387
29931
  writeFileSync4(filePath, content);
29388
29932
  return true;
29389
29933
  } catch {
29390
29934
  return false;
29391
29935
  }
29392
29936
  }
29393
- function escapeRegex2(str) {
29394
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29937
+ function resolveTargets(selectedTools) {
29938
+ const targets = /* @__PURE__ */ new Map();
29939
+ targets.set(AGENTS_MD.path, AGENTS_MD);
29940
+ for (const tool of selectedTools) {
29941
+ for (const file of tool.instructionFiles ?? []) {
29942
+ targets.set(file.path, file);
29943
+ }
29944
+ }
29945
+ return [...targets.values()];
29946
+ }
29947
+ function plannedInstructionFiles(selectedTools) {
29948
+ return resolveTargets(selectedTools).map((t) => t.path);
29949
+ }
29950
+ function injectIntoProjectFiles(targetDir, selectedTools) {
29951
+ const written = [];
29952
+ const failed = [];
29953
+ for (const target of resolveTargets(selectedTools)) {
29954
+ const ok = injectOcrInstructions(join4(targetDir, target.path), target.format);
29955
+ (ok ? written : failed).push(target.path);
29956
+ }
29957
+ return { written, failed };
29395
29958
  }
29396
- function injectIntoProjectFiles(targetDir) {
29397
- const agentsMdPath = join4(targetDir, "AGENTS.md");
29398
- const claudeMdPath = join4(targetDir, "CLAUDE.md");
29399
- const agentsMd = injectOcrInstructions(agentsMdPath);
29400
- const claudeMd = injectOcrInstructions(claudeMdPath);
29401
- return { agentsMd, claudeMd };
29959
+ function hasOcrInstructions(filePath) {
29960
+ if (!existsSync4(filePath)) {
29961
+ return false;
29962
+ }
29963
+ const content = readFileSync5(filePath, "utf-8");
29964
+ return Object.values(MARKERS).some(
29965
+ (m) => content.includes(m.start) && content.includes(m.end)
29966
+ );
29967
+ }
29968
+ function findStaleInstructionFiles(targetDir, writtenPaths) {
29969
+ const written = new Set(writtenPaths);
29970
+ const candidates = /* @__PURE__ */ new Set();
29971
+ for (const tool of AI_TOOLS) {
29972
+ for (const file of tool.instructionFiles ?? []) {
29973
+ candidates.add(file.path);
29974
+ }
29975
+ }
29976
+ const stale = [];
29977
+ for (const path2 of candidates) {
29978
+ if (written.has(path2)) continue;
29979
+ if (hasOcrInstructions(join4(targetDir, path2))) {
29980
+ stale.push(path2);
29981
+ }
29982
+ }
29983
+ return stale;
29984
+ }
29985
+ function formatStaleWarnings(stale, mode) {
29986
+ if (mode === "dry-run") {
29987
+ return stale.map((path2) => `${path2} (stale OCR block \u2014 left untouched)`);
29988
+ }
29989
+ const owner = mode === "init" ? "installed" : "configured";
29990
+ return stale.map(
29991
+ (path2) => `${path2} still has an OCR block but no ${owner} tool uses it \u2014 remove it manually if unneeded.`
29992
+ );
29402
29993
  }
29403
29994
 
29404
29995
  // src/lib/banner.ts
@@ -29501,29 +30092,10 @@ ${hint}
29501
30092
  }
29502
30093
 
29503
30094
  // src/lib/version.ts
29504
- var CLI_VERSION = true ? "2.1.0" : createRequire(import.meta.url)("../../package.json").version;
29505
-
29506
- // ../shared/platform/src/index.ts
29507
- import { pathToFileURL } from "node:url";
29508
- import {
29509
- execFile,
29510
- execFileSync,
29511
- spawn as spawn2
29512
- } from "node:child_process";
29513
- import { promisify } from "node:util";
29514
- var execFilePromise = promisify(execFile);
29515
- var isWindows = process.platform === "win32";
29516
- async function importModule(absolutePath) {
29517
- return import(pathToFileURL(absolutePath).href);
29518
- }
29519
- function execBinary(binary, args, opts) {
29520
- return execFileSync(binary, args, {
29521
- ...opts,
29522
- shell: isWindows
29523
- });
29524
- }
30095
+ var CLI_VERSION = true ? "2.2.1" : createRequire(import.meta.url)("../../package.json").version;
29525
30096
 
29526
30097
  // src/lib/deps.ts
30098
+ init_src();
29527
30099
  var CATEGORY_ORDER = ["core", "ai-cli", "github"];
29528
30100
  var CATEGORY_INFO = {
29529
30101
  core: { label: "Core", hint: "" },
@@ -29695,7 +30267,7 @@ function printCapabilities(result) {
29695
30267
  }
29696
30268
 
29697
30269
  // src/commands/init.ts
29698
- var initCommand = new Command("init").description("Set up OCR for AI coding environments").option("-t, --tools <tools>", 'Comma-separated tool IDs or "all"').option("--no-inject", "Skip injecting instructions into AGENTS.md/CLAUDE.md").action(async (options) => {
30270
+ var initCommand = new Command("init").description("Set up OCR for AI coding environments").option("-t, --tools <tools>", 'Comma-separated tool IDs or "all"').option("--no-inject", "Skip injecting instructions into project instruction files (AGENTS.md + each tool's native file)").action(async (options) => {
29699
30271
  printBanner();
29700
30272
  const depResult = checkDependencies();
29701
30273
  printDepChecks(depResult);
@@ -29786,17 +30358,19 @@ var initCommand = new Command("init").description("Set up OCR for AI coding envi
29786
30358
  const injectSpinner = ora(
29787
30359
  "Injecting OCR instructions into project files..."
29788
30360
  ).start();
29789
- const injectResults = injectIntoProjectFiles(targetDir);
30361
+ const installedTools = successful.map((r) => r.tool);
30362
+ const injectResults = injectIntoProjectFiles(targetDir, installedTools);
29790
30363
  injectSpinner.stop();
29791
- if (injectResults.agentsMd || injectResults.claudeMd) {
30364
+ if (injectResults.written.length > 0) {
29792
30365
  console.log(source_default.green("\u2713 OCR instructions injected"));
29793
- if (injectResults.agentsMd) {
29794
- console.log(` ${source_default.green("\u2713")} AGENTS.md`);
29795
- }
29796
- if (injectResults.claudeMd) {
29797
- console.log(` ${source_default.green("\u2713")} CLAUDE.md`);
30366
+ for (const path2 of injectResults.written) {
30367
+ console.log(` ${source_default.green("\u2713")} ${path2}`);
29798
30368
  }
29799
30369
  }
30370
+ const stale = findStaleInstructionFiles(targetDir, injectResults.written);
30371
+ for (const warning of formatStaleWarnings(stale, "init")) {
30372
+ console.log(source_default.yellow(` \u26A0 ${warning}`));
30373
+ }
29800
30374
  }
29801
30375
  console.log();
29802
30376
  console.log(source_default.bold("Next steps:"));
@@ -29984,10 +30558,10 @@ var ReaddirpStream = class extends Readable {
29984
30558
  }
29985
30559
  async _formatEntry(dirent, path2) {
29986
30560
  let entry;
29987
- const basename8 = this._isDirent ? dirent.name : dirent;
30561
+ const basename9 = this._isDirent ? dirent.name : dirent;
29988
30562
  try {
29989
- const fullPath = presolve(pjoin(path2, basename8));
29990
- entry = { path: prelative(this._root, fullPath), fullPath, basename: basename8 };
30563
+ const fullPath = presolve(pjoin(path2, basename9));
30564
+ entry = { path: prelative(this._root, fullPath), fullPath, basename: basename9 };
29991
30565
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
29992
30566
  } catch (err) {
29993
30567
  this._onError(err);
@@ -30526,9 +31100,9 @@ var NodeFsHandler = class {
30526
31100
  _watchWithNodeFs(path2, listener) {
30527
31101
  const opts = this.fsw.options;
30528
31102
  const directory = sysPath.dirname(path2);
30529
- const basename8 = sysPath.basename(path2);
31103
+ const basename9 = sysPath.basename(path2);
30530
31104
  const parent = this.fsw._getWatchedDir(directory);
30531
- parent.add(basename8);
31105
+ parent.add(basename9);
30532
31106
  const absolutePath = sysPath.resolve(path2);
30533
31107
  const options = {
30534
31108
  persistent: opts.persistent
@@ -30538,7 +31112,7 @@ var NodeFsHandler = class {
30538
31112
  let closer;
30539
31113
  if (opts.usePolling) {
30540
31114
  const enableBin = opts.interval !== opts.binaryInterval;
30541
- options.interval = enableBin && isBinaryPath(basename8) ? opts.binaryInterval : opts.interval;
31115
+ options.interval = enableBin && isBinaryPath(basename9) ? opts.binaryInterval : opts.interval;
30542
31116
  closer = setFsWatchFileListener(path2, absolutePath, options, {
30543
31117
  listener,
30544
31118
  rawEmitter: this.fsw._emitRaw
@@ -30560,11 +31134,11 @@ var NodeFsHandler = class {
30560
31134
  if (this.fsw.closed) {
30561
31135
  return;
30562
31136
  }
30563
- const dirname8 = sysPath.dirname(file);
30564
- const basename8 = sysPath.basename(file);
30565
- const parent = this.fsw._getWatchedDir(dirname8);
31137
+ const dirname10 = sysPath.dirname(file);
31138
+ const basename9 = sysPath.basename(file);
31139
+ const parent = this.fsw._getWatchedDir(dirname10);
30566
31140
  let prevStats = stats;
30567
- if (parent.has(basename8))
31141
+ if (parent.has(basename9))
30568
31142
  return;
30569
31143
  const listener = async (path2, newStats) => {
30570
31144
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
@@ -30589,9 +31163,9 @@ var NodeFsHandler = class {
30589
31163
  prevStats = newStats2;
30590
31164
  }
30591
31165
  } catch (error) {
30592
- this.fsw._remove(dirname8, basename8);
31166
+ this.fsw._remove(dirname10, basename9);
30593
31167
  }
30594
- } else if (parent.has(basename8)) {
31168
+ } else if (parent.has(basename9)) {
30595
31169
  const at = newStats.atimeMs;
30596
31170
  const mt = newStats.mtimeMs;
30597
31171
  if (!at || at <= mt || mt !== prevStats.mtimeMs) {
@@ -31522,8 +32096,8 @@ function watch(paths, options = {}) {
31522
32096
  }
31523
32097
 
31524
32098
  // src/commands/progress.ts
31525
- import { existsSync as existsSync13, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
31526
- import { join as join15, basename as basename7 } from "node:path";
32099
+ import { existsSync as existsSync14, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
32100
+ import { join as join16, basename as basename8 } from "node:path";
31527
32101
 
31528
32102
  // ../../node_modules/.pnpm/log-update@7.0.2/node_modules/log-update/index.js
31529
32103
  import process12 from "node:process";
@@ -32391,7 +32965,7 @@ var log_update_default = logUpdate;
32391
32965
  var logUpdateStderr = createLogUpdate(process12.stderr);
32392
32966
 
32393
32967
  // src/lib/guards.ts
32394
- import { existsSync as existsSync6, mkdirSync as mkdirSync2 } from "node:fs";
32968
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "node:fs";
32395
32969
  import { join as join8 } from "node:path";
32396
32970
  function checkOcrSetup(targetDir) {
32397
32971
  const ocrDir = join8(targetDir, ".ocr");
@@ -32437,7 +33011,7 @@ function requireOcrSetup(targetDir) {
32437
33011
  function ensureSessionsDir(targetDir) {
32438
33012
  const sessionsDir = join8(targetDir, ".ocr", "sessions");
32439
33013
  if (!existsSync6(sessionsDir)) {
32440
- mkdirSync2(sessionsDir, { recursive: true });
33014
+ mkdirSync3(sessionsDir, { recursive: true });
32441
33015
  }
32442
33016
  return sessionsDir;
32443
33017
  }
@@ -33159,15 +33733,15 @@ function debounce(fn, delay) {
33159
33733
  };
33160
33734
  }
33161
33735
  function findLatestActiveSession(sessionsDir) {
33162
- if (!existsSync13(sessionsDir)) {
33736
+ if (!existsSync14(sessionsDir)) {
33163
33737
  return null;
33164
33738
  }
33165
- const sessions = readdirSync5(sessionsDir).filter((name) => {
33166
- const sessionPath = join15(sessionsDir, name);
33167
- return statSync2(sessionPath).isDirectory();
33739
+ const sessions = readdirSync6(sessionsDir).filter((name) => {
33740
+ const sessionPath = join16(sessionsDir, name);
33741
+ return statSync3(sessionPath).isDirectory();
33168
33742
  }).sort().reverse();
33169
33743
  for (const session of sessions) {
33170
- const sessionPath = join15(sessionsDir, session);
33744
+ const sessionPath = join16(sessionsDir, session);
33171
33745
  if (isSessionActive(sessionPath)) {
33172
33746
  return session;
33173
33747
  }
@@ -33182,8 +33756,8 @@ function getStrategyForSession(sessionPath, explicitWorkflow) {
33182
33756
  return getStrategy(workflowType) ?? null;
33183
33757
  }
33184
33758
  async function initProgressDb(ocrDir) {
33185
- const dbPath = join15(ocrDir, "data", "ocr.db");
33186
- if (!existsSync13(dbPath)) {
33759
+ const dbPath = join16(ocrDir, "data", "ocr.db");
33760
+ if (!existsSync14(dbPath)) {
33187
33761
  return;
33188
33762
  }
33189
33763
  try {
@@ -33208,11 +33782,11 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33208
33782
  const targetDir = process.cwd();
33209
33783
  requireOcrSetup(targetDir);
33210
33784
  const sessionsDir = ensureSessionsDir(targetDir);
33211
- const ocrDir = join15(targetDir, ".ocr");
33785
+ const ocrDir = join16(targetDir, ".ocr");
33212
33786
  await initProgressDb(ocrDir);
33213
33787
  if (options.session) {
33214
- const sessionPath = join15(sessionsDir, options.session);
33215
- if (!existsSync13(sessionPath)) {
33788
+ const sessionPath = join16(sessionsDir, options.session);
33789
+ if (!existsSync14(sessionPath)) {
33216
33790
  console.error(source_default.red(`Session not found: ${options.session}`));
33217
33791
  process.exit(1);
33218
33792
  }
@@ -33273,7 +33847,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33273
33847
  return;
33274
33848
  }
33275
33849
  let currentSession = findLatestActiveSession(sessionsDir);
33276
- let currentSessionPath = currentSession ? join15(sessionsDir, currentSession) : null;
33850
+ let currentSessionPath = currentSession ? join16(sessionsDir, currentSession) : null;
33277
33851
  let sessionWatcher = null;
33278
33852
  const preservedStartTimes = {
33279
33853
  review: void 0,
@@ -33281,11 +33855,11 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33281
33855
  };
33282
33856
  let currentStrategy = null;
33283
33857
  const updateDisplayImpl = () => {
33284
- if (!currentSessionPath || !existsSync13(currentSessionPath) || !isSessionActive(currentSessionPath)) {
33858
+ if (!currentSessionPath || !existsSync14(currentSessionPath) || !isSessionActive(currentSessionPath)) {
33285
33859
  const latestActive = findLatestActiveSession(sessionsDir);
33286
33860
  if (latestActive && latestActive !== currentSession) {
33287
33861
  currentSession = latestActive;
33288
- currentSessionPath = join15(sessionsDir, latestActive);
33862
+ currentSessionPath = join16(sessionsDir, latestActive);
33289
33863
  preservedStartTimes.review = void 0;
33290
33864
  preservedStartTimes.map = void 0;
33291
33865
  currentStrategy = null;
@@ -33298,7 +33872,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33298
33872
  currentStrategy = null;
33299
33873
  }
33300
33874
  }
33301
- if (currentSessionPath && existsSync13(currentSessionPath)) {
33875
+ if (currentSessionPath && existsSync14(currentSessionPath)) {
33302
33876
  if (!options.workflow) {
33303
33877
  const activeWorkflows = detectActiveWorkflows(currentSessionPath);
33304
33878
  if (activeWorkflows.length > 1) {
@@ -33352,17 +33926,17 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33352
33926
  watchSession(currentSessionPath);
33353
33927
  }
33354
33928
  const timerInterval = setInterval(updateDisplay, 1e3);
33355
- const watchDir = existsSync13(ocrDir) ? ocrDir : targetDir;
33929
+ const watchDir = existsSync14(ocrDir) ? ocrDir : targetDir;
33356
33930
  const dirWatcher = watch(watchDir, {
33357
33931
  persistent: true,
33358
33932
  ignoreInitial: true,
33359
33933
  depth: 3
33360
33934
  });
33361
33935
  dirWatcher.on("addDir", (dirPath) => {
33362
- const parentDir = join15(dirPath, "..");
33363
- const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(join15(".ocr", "sessions"));
33936
+ const parentDir = join16(dirPath, "..");
33937
+ const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(join16(".ocr", "sessions"));
33364
33938
  if (isDirectChild && !dirPath.endsWith("sessions")) {
33365
- const newSession = basename7(dirPath);
33939
+ const newSession = basename8(dirPath);
33366
33940
  currentSession = newSession;
33367
33941
  currentSessionPath = dirPath;
33368
33942
  preservedStartTimes.review = void 0;
@@ -33401,7 +33975,7 @@ function renderGenericWaiting() {
33401
33975
  }
33402
33976
  function renderCombinedProgress(sessionPath, preservedStartTimes, ocrDir) {
33403
33977
  const lines = [];
33404
- const session = basename7(sessionPath);
33978
+ const session = basename8(sessionPath);
33405
33979
  lines.push("");
33406
33980
  lines.push(
33407
33981
  source_default.bold.white(" Open Code Review") + source_default.yellow(" \xB7 Parallel Workflows")
@@ -33462,21 +34036,21 @@ function renderCombinedProgress(sessionPath, preservedStartTimes, ocrDir) {
33462
34036
  }
33463
34037
 
33464
34038
  // src/commands/state.ts
33465
- import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync11 } from "node:fs";
33466
- import { join as join17 } from "node:path";
34039
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11 } from "node:fs";
34040
+ import { join as join18 } from "node:path";
33467
34041
 
33468
34042
  // src/lib/state/index.ts
33469
34043
  init_db();
33470
34044
  init_exit_codes();
33471
34045
  import {
33472
- existsSync as existsSync14,
33473
- mkdirSync as mkdirSync5,
33474
- readdirSync as readdirSync6,
34046
+ existsSync as existsSync15,
34047
+ mkdirSync as mkdirSync6,
34048
+ readdirSync as readdirSync7,
33475
34049
  readFileSync as readFileSync10,
33476
- statSync as statSync3,
34050
+ statSync as statSync4,
33477
34051
  writeFileSync as writeFileSync7
33478
34052
  } from "node:fs";
33479
- import { join as join16 } from "node:path";
34053
+ import { join as join17 } from "node:path";
33480
34054
 
33481
34055
  // src/lib/state/phase-graph.ts
33482
34056
  init_exit_codes();
@@ -33784,9 +34358,9 @@ function deriveNextRound(db, sessionId, fallbackRound) {
33784
34358
  }
33785
34359
  function hasArtifacts(dir) {
33786
34360
  try {
33787
- for (const entry of readdirSync6(dir, { withFileTypes: true })) {
34361
+ for (const entry of readdirSync7(dir, { withFileTypes: true })) {
33788
34362
  if (entry.isDirectory()) {
33789
- if (hasArtifacts(join16(dir, entry.name))) return true;
34363
+ if (hasArtifacts(join17(dir, entry.name))) return true;
33790
34364
  } else if (/\.(md|json)$/.test(entry.name)) {
33791
34365
  return true;
33792
34366
  }
@@ -33797,7 +34371,7 @@ function hasArtifacts(dir) {
33797
34371
  }
33798
34372
  function readJsonFromSource(params) {
33799
34373
  if (params.source === "file") {
33800
- if (!existsSync14(params.filePath)) {
34374
+ if (!existsSync15(params.filePath)) {
33801
34375
  throw new StateError(STATE_EXIT.NOT_FOUND, `File not found: ${params.filePath}`);
33802
34376
  }
33803
34377
  return readFileSync10(params.filePath, "utf-8");
@@ -34124,7 +34698,7 @@ async function stateCompleteRound(params) {
34124
34698
  }
34125
34699
  const resolved = resolveSession(db, params.sessionId);
34126
34700
  const roundNumber = params.round ?? resolved.current_round;
34127
- const roundMetaPath = join16(
34701
+ const roundMetaPath = join17(
34128
34702
  resolved.session_dir,
34129
34703
  "rounds",
34130
34704
  `round-${roundNumber}`,
@@ -34145,8 +34719,8 @@ async function stateCompleteRound(params) {
34145
34719
  );
34146
34720
  }
34147
34721
  if (params.requireFinal) {
34148
- const finalPath = join16(resolved.session_dir, "rounds", `round-${roundNumber}`, "final.md");
34149
- if (!existsSync14(finalPath)) {
34722
+ const finalPath = join17(resolved.session_dir, "rounds", `round-${roundNumber}`, "final.md");
34723
+ if (!existsSync15(finalPath)) {
34150
34724
  throw new StateError(
34151
34725
  STATE_EXIT.INVARIANT_UNMET,
34152
34726
  `Cannot complete round: --require-final set but ${finalPath} is missing.`
@@ -34155,8 +34729,8 @@ async function stateCompleteRound(params) {
34155
34729
  }
34156
34730
  let metaPath;
34157
34731
  if (params.source === "stdin") {
34158
- const roundDir = join16(resolved.session_dir, "rounds", `round-${roundNumber}`);
34159
- mkdirSync5(roundDir, { recursive: true });
34732
+ const roundDir = join17(resolved.session_dir, "rounds", `round-${roundNumber}`);
34733
+ mkdirSync6(roundDir, { recursive: true });
34160
34734
  metaPath = roundMetaPath;
34161
34735
  writeFileSync7(metaPath, JSON.stringify(meta, null, 2));
34162
34736
  }
@@ -34210,7 +34784,7 @@ async function stateCompleteMap(params) {
34210
34784
  }
34211
34785
  const resolved = resolveSession(db, params.sessionId);
34212
34786
  const mapRunNumber = params.mapRun ?? resolved.current_map_run;
34213
- const mapMetaPath = join16(
34787
+ const mapMetaPath = join17(
34214
34788
  resolved.session_dir,
34215
34789
  "map",
34216
34790
  "runs",
@@ -34233,8 +34807,8 @@ async function stateCompleteMap(params) {
34233
34807
  }
34234
34808
  let metaPath;
34235
34809
  if (params.source === "stdin") {
34236
- const runDir = join16(resolved.session_dir, "map", "runs", `run-${mapRunNumber}`);
34237
- mkdirSync5(runDir, { recursive: true });
34810
+ const runDir = join17(resolved.session_dir, "map", "runs", `run-${mapRunNumber}`);
34811
+ mkdirSync6(runDir, { recursive: true });
34238
34812
  metaPath = mapMetaPath;
34239
34813
  writeFileSync7(metaPath, JSON.stringify(meta, null, 2));
34240
34814
  }
@@ -34319,17 +34893,17 @@ async function stateStatus(ocrDir, sessionId) {
34319
34893
  }
34320
34894
  async function stateSync(ocrDir) {
34321
34895
  const db = await ensureDatabase(ocrDir);
34322
- const sessionsRoot = join16(ocrDir, "sessions");
34323
- if (!existsSync14(sessionsRoot)) {
34896
+ const sessionsRoot = join17(ocrDir, "sessions");
34897
+ if (!existsSync15(sessionsRoot)) {
34324
34898
  return 0;
34325
34899
  }
34326
- const entries = readdirSync6(sessionsRoot).filter((name) => {
34327
- const fullPath = join16(sessionsRoot, name);
34328
- return statSync3(fullPath).isDirectory();
34900
+ const entries = readdirSync7(sessionsRoot).filter((name) => {
34901
+ const fullPath = join17(sessionsRoot, name);
34902
+ return statSync4(fullPath).isDirectory();
34329
34903
  });
34330
34904
  let synced = 0;
34331
34905
  for (const dirName of entries) {
34332
- const dirPath = join16(sessionsRoot, dirName);
34906
+ const dirPath = join17(sessionsRoot, dirName);
34333
34907
  const existing = getSession(db, dirName);
34334
34908
  if (existing) {
34335
34909
  continue;
@@ -34337,8 +34911,8 @@ async function stateSync(ocrDir) {
34337
34911
  if (!hasArtifacts(dirPath)) {
34338
34912
  continue;
34339
34913
  }
34340
- const hasRoundsDir = existsSync14(join16(dirPath, "rounds"));
34341
- const hasMapDir = existsSync14(join16(dirPath, "map"));
34914
+ const hasRoundsDir = existsSync15(join17(dirPath, "rounds"));
34915
+ const hasMapDir = existsSync15(join17(dirPath, "map"));
34342
34916
  const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
34343
34917
  const branchMatch = dirName.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
34344
34918
  const branch = branchMatch?.[1] ?? dirName;
@@ -34347,14 +34921,14 @@ async function stateSync(ocrDir) {
34347
34921
  let inferredRound = 1;
34348
34922
  let inferredMapRun = 1;
34349
34923
  if (workflowType === "review") {
34350
- const roundsDir = join16(dirPath, "rounds");
34351
- if (existsSync14(roundsDir)) {
34352
- const roundDirs = readdirSync6(roundsDir).filter((d) => /^round-\d+$/.test(d)).map((d) => parseInt(d.replace("round-", ""), 10)).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
34924
+ const roundsDir = join17(dirPath, "rounds");
34925
+ if (existsSync15(roundsDir)) {
34926
+ const roundDirs = readdirSync7(roundsDir).filter((d) => /^round-\d+$/.test(d)).map((d) => parseInt(d.replace("round-", ""), 10)).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
34353
34927
  const latestRoundNum = roundDirs[roundDirs.length - 1];
34354
34928
  if (latestRoundNum !== void 0) {
34355
34929
  inferredRound = latestRoundNum;
34356
- if (existsSync14(
34357
- join16(roundsDir, `round-${latestRoundNum}`, "final.md")
34930
+ if (existsSync15(
34931
+ join17(roundsDir, `round-${latestRoundNum}`, "final.md")
34358
34932
  )) {
34359
34933
  inferredPhase = "complete";
34360
34934
  inferredPhaseNumber = 8;
@@ -34362,13 +34936,13 @@ async function stateSync(ocrDir) {
34362
34936
  }
34363
34937
  }
34364
34938
  } else if (workflowType === "map") {
34365
- const runsDir = join16(dirPath, "map", "runs");
34366
- if (existsSync14(runsDir)) {
34367
- const runDirs = readdirSync6(runsDir).filter((d) => /^run-\d+$/.test(d)).map((d) => parseInt(d.replace("run-", ""), 10)).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
34939
+ const runsDir = join17(dirPath, "map", "runs");
34940
+ if (existsSync15(runsDir)) {
34941
+ const runDirs = readdirSync7(runsDir).filter((d) => /^run-\d+$/.test(d)).map((d) => parseInt(d.replace("run-", ""), 10)).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
34368
34942
  const latestRunNum = runDirs[runDirs.length - 1];
34369
34943
  if (latestRunNum !== void 0) {
34370
34944
  inferredMapRun = latestRunNum;
34371
- if (existsSync14(join16(runsDir, `run-${latestRunNum}`, "map.md"))) {
34945
+ if (existsSync15(join17(runsDir, `run-${latestRunNum}`, "map.md"))) {
34372
34946
  inferredPhase = "complete";
34373
34947
  inferredPhaseNumber = 6;
34374
34948
  }
@@ -34406,7 +34980,7 @@ init_command_log();
34406
34980
  init_db();
34407
34981
  init_db();
34408
34982
  function readDashboardSpawnMarker(ocrDir) {
34409
- const path2 = join17(ocrDir, "data", "dashboard-active-spawn.json");
34983
+ const path2 = join18(ocrDir, "data", "dashboard-active-spawn.json");
34410
34984
  let raw;
34411
34985
  try {
34412
34986
  raw = readFileSync11(path2, "utf-8");
@@ -34471,7 +35045,7 @@ async function linkDashboardInvocation(ocrDir, sessionId, explicitUid, label) {
34471
35045
  var showSubcommand = new Command("show").description("Show current session state").option("--session-id <id>", "Session ID (defaults to latest active)").option("--json", "Output as JSON").action(async (options) => {
34472
35046
  const targetDir = process.cwd();
34473
35047
  requireOcrSetup(targetDir);
34474
- const ocrDir = join17(targetDir, ".ocr");
35048
+ const ocrDir = join18(targetDir, ".ocr");
34475
35049
  try {
34476
35050
  const result = await stateShow(ocrDir, options.sessionId);
34477
35051
  if (!result) {
@@ -34540,7 +35114,7 @@ var showSubcommand = new Command("show").description("Show current session state
34540
35114
  var syncSubcommand = new Command("sync").description("Rebuild session state from filesystem artifacts").action(async () => {
34541
35115
  const targetDir = process.cwd();
34542
35116
  requireOcrSetup(targetDir);
34543
- const ocrDir = join17(targetDir, ".ocr");
35117
+ const ocrDir = join18(targetDir, ".ocr");
34544
35118
  try {
34545
35119
  const synced = await stateSync(ocrDir);
34546
35120
  console.log(`Synced ${synced} session${synced !== 1 ? "s" : ""} from filesystem.`);
@@ -34567,7 +35141,7 @@ var reconcileSubcommand = new Command("reconcile").description(
34567
35141
  ).option("--dry-run", "Print the repair plan without writing anything").option("--json", "Output the result as JSON").action(async (options) => {
34568
35142
  const targetDir = process.cwd();
34569
35143
  requireOcrSetup(targetDir);
34570
- const ocrDir = join17(targetDir, ".ocr");
35144
+ const ocrDir = join18(targetDir, ".ocr");
34571
35145
  try {
34572
35146
  const db = await ensureDatabase(ocrDir);
34573
35147
  const result = reconcileLegacyState(db, ocrDir, { dryRun: options.dryRun });
@@ -34626,9 +35200,9 @@ var beginSubcommand = new Command("begin").description("Start or resume a workfl
34626
35200
  async (options) => {
34627
35201
  const targetDir = process.cwd();
34628
35202
  requireOcrSetup(targetDir);
34629
- const ocrDir = join17(targetDir, ".ocr");
34630
- const sessionDir = options.sessionDir ?? join17(ocrDir, "sessions", options.sessionId);
34631
- if (!existsSync15(sessionDir)) mkdirSync6(sessionDir, { recursive: true });
35203
+ const ocrDir = join18(targetDir, ".ocr");
35204
+ const sessionDir = options.sessionDir ?? join18(ocrDir, "sessions", options.sessionId);
35205
+ if (!existsSync16(sessionDir)) mkdirSync7(sessionDir, { recursive: true });
34632
35206
  try {
34633
35207
  const result = await stateBegin({
34634
35208
  sessionId: options.sessionId,
@@ -34650,7 +35224,7 @@ var advanceSubcommand = new Command("advance").description("Advance the workflow
34650
35224
  async (options) => {
34651
35225
  const targetDir = process.cwd();
34652
35226
  requireOcrSetup(targetDir);
34653
- const ocrDir = join17(targetDir, ".ocr");
35227
+ const ocrDir = join18(targetDir, ".ocr");
34654
35228
  try {
34655
35229
  const { id: sessionId } = await resolveActiveSession(ocrDir, options.sessionId);
34656
35230
  await stateAdvance({
@@ -34670,7 +35244,7 @@ var completeRoundSubcommand = new Command("complete-round").description("Atomica
34670
35244
  async (options) => {
34671
35245
  const targetDir = process.cwd();
34672
35246
  requireOcrSetup(targetDir);
34673
- const ocrDir = join17(targetDir, ".ocr");
35247
+ const ocrDir = join18(targetDir, ".ocr");
34674
35248
  try {
34675
35249
  const base = options.stdin ? { source: "stdin", data: await readStdin() } : options.file ? { source: "file", filePath: options.file } : (() => {
34676
35250
  throw new StateError(STATE_EXIT.USAGE, "Provide --stdin or --file with round metadata");
@@ -34694,7 +35268,7 @@ var completeMapSubcommand = new Command("complete-map").description("Atomically
34694
35268
  async (options) => {
34695
35269
  const targetDir = process.cwd();
34696
35270
  requireOcrSetup(targetDir);
34697
- const ocrDir = join17(targetDir, ".ocr");
35271
+ const ocrDir = join18(targetDir, ".ocr");
34698
35272
  try {
34699
35273
  const base = options.stdin ? { source: "stdin", data: await readStdin() } : options.file ? { source: "file", filePath: options.file } : (() => {
34700
35274
  throw new StateError(STATE_EXIT.USAGE, "Provide --stdin or --file with map metadata");
@@ -34716,7 +35290,7 @@ var completeMapSubcommand = new Command("complete-map").description("Atomically
34716
35290
  var finishSubcommand = new Command("finish").description("Close a workflow (refuses unless the current round/run is complete)").option("--session-id <id>", "Session ID (auto-detects active if omitted)").option("--abort", "Abandon the session \u2014 records a distinct, non-success terminal").action(async (options) => {
34717
35291
  const targetDir = process.cwd();
34718
35292
  requireOcrSetup(targetDir);
34719
- const ocrDir = join17(targetDir, ".ocr");
35293
+ const ocrDir = join18(targetDir, ".ocr");
34720
35294
  try {
34721
35295
  const { id: sessionId } = await resolveActiveSession(ocrDir, options.sessionId);
34722
35296
  await stateClose({ sessionId, ocrDir, abort: options.abort });
@@ -34728,7 +35302,7 @@ var finishSubcommand = new Command("finish").description("Close a workflow (refu
34728
35302
  var statusSubcommand = new Command("status").description("Report whether a session is complete and, if not, what's missing").option("--session-id <id>", "Session ID (auto-detects active if omitted)").option("--json", "Output the result as JSON").action(async (options) => {
34729
35303
  const targetDir = process.cwd();
34730
35304
  requireOcrSetup(targetDir);
34731
- const ocrDir = join17(targetDir, ".ocr");
35305
+ const ocrDir = join18(targetDir, ".ocr");
34732
35306
  try {
34733
35307
  const result = await stateStatus(ocrDir, options.sessionId);
34734
35308
  if (options.json) {
@@ -34757,44 +35331,50 @@ var stateCommand = new Command("state").description("Manage OCR session state").
34757
35331
 
34758
35332
  // src/commands/session.ts
34759
35333
  import { randomUUID as randomUUID3 } from "node:crypto";
34760
- import { join as join19 } from "node:path";
35334
+ import { join as join20 } from "node:path";
34761
35335
  init_db();
34762
35336
 
34763
35337
  // src/lib/runtime-config.ts
34764
- import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
34765
- import { join as join18 } from "node:path";
35338
+ import { existsSync as existsSync17, readFileSync as readFileSync12 } from "node:fs";
35339
+ import { join as join19 } from "node:path";
34766
35340
  var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
34767
- function getAgentHeartbeatSeconds(ocrDir) {
34768
- const configPath = join18(ocrDir, "config.yaml");
34769
- if (!existsSync16(configPath)) {
34770
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
34771
- }
35341
+ function readRuntimePositiveInt(ocrDir, key, defaultValue) {
35342
+ const configPath = join19(ocrDir, "config.yaml");
35343
+ if (!existsSync17(configPath)) return defaultValue;
34772
35344
  let content;
34773
35345
  try {
34774
35346
  content = readFileSync12(configPath, "utf-8");
34775
35347
  } catch {
34776
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
35348
+ return defaultValue;
34777
35349
  }
34778
35350
  const blockMatch = content.match(
34779
- /^runtime:\s*\n(?:\s+[^\n]*\n)*?\s+agent_heartbeat_seconds:\s*([^\s#\n]+)/m
35351
+ new RegExp(
35352
+ String.raw`^runtime:\s*\n(?:\s+[^\n]*\n)*?\s+${key}:\s*([^\s#\n]+)`,
35353
+ "m"
35354
+ )
34780
35355
  );
34781
35356
  const inlineMatch = content.match(
34782
- /^runtime:\s*\{[^}]*\bagent_heartbeat_seconds:\s*([^\s,}]+)/m
35357
+ new RegExp(String.raw`^runtime:\s*\{[^}]*\b${key}:\s*([^\s,}]+)`, "m")
34783
35358
  );
34784
35359
  const raw = blockMatch?.[1] ?? inlineMatch?.[1];
34785
- if (!raw) {
34786
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
34787
- }
35360
+ if (!raw) return defaultValue;
34788
35361
  const parsed = Number(raw);
34789
35362
  if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
34790
35363
  process.stderr.write(
34791
- `[ocr] runtime.agent_heartbeat_seconds is not a positive integer (got "${raw}"); falling back to ${DEFAULT_AGENT_HEARTBEAT_SECONDS}s.
35364
+ `[ocr] runtime.${key} is not a positive integer (got "${raw}"); falling back to ${defaultValue}.
34792
35365
  `
34793
35366
  );
34794
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
35367
+ return defaultValue;
34795
35368
  }
34796
35369
  return parsed;
34797
35370
  }
35371
+ function getAgentHeartbeatSeconds(ocrDir) {
35372
+ return readRuntimePositiveInt(
35373
+ ocrDir,
35374
+ "agent_heartbeat_seconds",
35375
+ DEFAULT_AGENT_HEARTBEAT_SECONDS
35376
+ );
35377
+ }
34798
35378
 
34799
35379
  // src/commands/session.ts
34800
35380
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
@@ -34810,7 +35390,7 @@ function fail(message) {
34810
35390
  async function setup() {
34811
35391
  const targetDir = process.cwd();
34812
35392
  requireOcrSetup(targetDir);
34813
- const ocrDir = join19(targetDir, ".ocr");
35393
+ const ocrDir = join20(targetDir, ".ocr");
34814
35394
  return { ocrDir };
34815
35395
  }
34816
35396
  var startInstanceSubcommand = new Command("start-instance").description("Journal a new agent-CLI process spawned for the active review").option("--workflow <id>", "Workflow session id (auto-detects active if omitted)").option("--persona <name>", "Reviewer persona, e.g. 'principal'").option("--instance <number>", "Instance index within (workflow, persona)", parseInt).option("--name <name>", "Human-friendly name (default: '{persona}-{instance}')").requiredOption("--vendor <vendor>", "Underlying CLI vendor (e.g. 'claude', 'opencode')").option("--model <id>", "Resolved model id passed to the CLI's --model flag").option("--phase <phase>", "Workflow phase this instance is doing").option("--pid <pid>", "Process id of the spawned process", parseInt).option("--note <text>", "Free-form note to attach").action(
@@ -34945,23 +35525,63 @@ var listSubcommand = new Command("list").description("List agent sessions for a
34945
35525
  var sessionCommand = new Command("session").description("Manage agent-CLI session lifecycle journal").addCommand(startInstanceSubcommand).addCommand(bindVendorIdSubcommand).addCommand(beatSubcommand).addCommand(endInstanceSubcommand).addCommand(listSubcommand);
34946
35526
 
34947
35527
  // src/lib/models.ts
34948
- var BUNDLED_CLAUDE_MODELS = [
34949
- { id: "claude-opus-4-7", displayName: "Claude Opus 4.7" },
34950
- { id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" },
34951
- { id: "claude-haiku-4-5-20251001", displayName: "Claude Haiku 4.5" }
34952
- ];
34953
- var BUNDLED_OPENCODE_MODELS = [
34954
- { id: "anthropic/claude-opus-4-7", provider: "anthropic" },
34955
- { id: "anthropic/claude-sonnet-4-6", provider: "anthropic" },
34956
- { id: "anthropic/claude-haiku-4-5-20251001", provider: "anthropic" }
34957
- ];
34958
- function detectActiveVendor() {
34959
- for (const vendor of ["claude", "opencode"]) {
35528
+ init_src();
35529
+ function parseOpenCodeModelList(stdout) {
35530
+ const models = [];
35531
+ for (const rawLine of stdout.split(/\r?\n/)) {
35532
+ const line = rawLine.trim();
35533
+ if (!/^[^\s:]+\/\S+$/.test(line)) continue;
35534
+ const provider = line.slice(0, line.indexOf("/"));
35535
+ models.push({ id: line, provider });
35536
+ }
35537
+ return models.length > 0 ? models : null;
35538
+ }
35539
+ var VENDOR_MODEL_STRATEGIES = {
35540
+ claude: {
35541
+ displayName: "Claude Code",
35542
+ native: {
35543
+ // Verified against Claude Code 2.1.x: the CLI has no model-listing
35544
+ // subcommand (`claude models --json` → "unknown option"). Revisit if
35545
+ // a future release adds one.
35546
+ unavailableReason: "Claude Code does not provide a model-listing command; showing its documented model aliases instead"
35547
+ },
35548
+ // Vendor-documented aliases that always track the latest generation —
35549
+ // dated ids here would go stale by construction (the exact bug class of
35550
+ // issue #39). Pinned dated ids remain available via free-text entry.
35551
+ bundled: [
35552
+ { id: "opus", displayName: "Claude Opus (latest)" },
35553
+ { id: "sonnet", displayName: "Claude Sonnet (latest)" },
35554
+ { id: "haiku", displayName: "Claude Haiku (latest)" }
35555
+ ]
35556
+ },
35557
+ opencode: {
35558
+ displayName: "OpenCode",
35559
+ native: {
35560
+ // Plain `opencode models` — newline-delimited ids. (`--json` is not a
35561
+ // real flag, and `--verbose` interleaves JSON metadata blocks that
35562
+ // defeat line parsing.)
35563
+ args: ["models"],
35564
+ parse: parseOpenCodeModelList
35565
+ },
35566
+ bundled: [
35567
+ { id: "anthropic/claude-opus-4-8", provider: "anthropic" },
35568
+ { id: "anthropic/claude-sonnet-4-6", provider: "anthropic" },
35569
+ { id: "anthropic/claude-haiku-4-5", provider: "anthropic" }
35570
+ ]
35571
+ }
35572
+ };
35573
+ var SUPPORTED_VENDORS = Object.keys(
35574
+ VENDOR_MODEL_STRATEGIES
35575
+ );
35576
+ function isModelVendor(value) {
35577
+ return Object.hasOwn(VENDOR_MODEL_STRATEGIES, value);
35578
+ }
35579
+ async function detectActiveVendor() {
35580
+ for (const vendor of SUPPORTED_VENDORS) {
34960
35581
  try {
34961
- execBinary(vendor, ["--version"], {
35582
+ await execBinaryAsync(vendor, ["--version"], {
34962
35583
  encoding: "utf-8",
34963
- timeout: 3e3,
34964
- stdio: ["ignore", "pipe", "ignore"]
35584
+ timeout: 3e3
34965
35585
  });
34966
35586
  return vendor;
34967
35587
  } catch {
@@ -34969,68 +35589,97 @@ function detectActiveVendor() {
34969
35589
  }
34970
35590
  return null;
34971
35591
  }
34972
- function tryNativeEnumeration(vendor) {
35592
+ function describeProbeFailure(vendor, args, err) {
35593
+ const command = `${vendor} ${args.join(" ")}`;
35594
+ const e = err;
35595
+ if (e.code === "ENOENT") {
35596
+ return `\`${vendor}\` is not installed or not on PATH`;
35597
+ }
35598
+ if (e.killed) {
35599
+ return `\`${command}\` timed out or exceeded output limits`;
35600
+ }
35601
+ const stderr = typeof e.stderr === "string" ? e.stderr.trim() : "";
35602
+ const firstLine = (stderr.split(/\r?\n/)[0] ?? "").replace(/\u001b\[[0-9;]*[A-Za-z]/g, "").replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
35603
+ const detail = firstLine ? `: ${firstLine}` : "";
35604
+ const exit = typeof e.code === "number" ? ` with exit code ${e.code}` : "";
35605
+ return `\`${command}\` failed${exit}${detail}`;
35606
+ }
35607
+ async function tryNativeEnumeration(vendor, probe) {
35608
+ let stdout;
34973
35609
  try {
34974
- const output = execBinary(vendor, ["models", "--json"], {
35610
+ const result = await execBinaryAsync(vendor, probe.args, {
34975
35611
  encoding: "utf-8",
34976
- timeout: 5e3,
34977
- stdio: ["ignore", "pipe", "ignore"]
35612
+ timeout: 5e3
34978
35613
  });
34979
- const parsed = JSON.parse(output);
34980
- if (!Array.isArray(parsed)) return null;
34981
- const models = [];
34982
- for (const item of parsed) {
34983
- if (typeof item === "string") {
34984
- models.push({ id: item });
34985
- } else if (typeof item === "object" && item !== null && "id" in item && typeof item.id === "string") {
34986
- const obj = item;
34987
- const desc = { id: obj.id };
34988
- if (typeof obj.displayName === "string") desc.displayName = obj.displayName;
34989
- if (typeof obj.provider === "string") desc.provider = obj.provider;
34990
- if (Array.isArray(obj.tags)) {
34991
- desc.tags = obj.tags.filter((t) => typeof t === "string");
34992
- }
34993
- models.push(desc);
34994
- }
34995
- }
34996
- return models.length > 0 ? models : null;
34997
- } catch {
34998
- return null;
35614
+ stdout = result.stdout;
35615
+ } catch (err) {
35616
+ return { models: null, reason: describeProbeFailure(vendor, probe.args, err) };
34999
35617
  }
35618
+ const models = probe.parse(stdout);
35619
+ if (!models) {
35620
+ return {
35621
+ models: null,
35622
+ reason: `\`${vendor} ${probe.args.join(" ")}\` output did not contain any model identifiers`
35623
+ };
35624
+ }
35625
+ return { models };
35000
35626
  }
35001
- function bundledForVendor(vendor) {
35002
- if (vendor === "claude") return BUNDLED_CLAUDE_MODELS;
35003
- return BUNDLED_OPENCODE_MODELS;
35004
- }
35005
- function listModelsForVendor(vendor) {
35006
- const native = tryNativeEnumeration(vendor);
35007
- if (native) {
35008
- return { vendor, source: "native", models: native };
35627
+ var SUCCESS_TTL_MS = 6e4;
35628
+ var FAILURE_TTL_MS = 1e4;
35629
+ var cache = /* @__PURE__ */ new Map();
35630
+ async function listModelsForVendor(vendor) {
35631
+ const cached = cache.get(vendor);
35632
+ if (cached && cached.expiresAt > Date.now()) {
35633
+ return cached.result;
35634
+ }
35635
+ const strategy = VENDOR_MODEL_STRATEGIES[vendor];
35636
+ if (!strategy) {
35637
+ throw new Error(`Unknown vendor: ${vendor}`);
35638
+ }
35639
+ let result;
35640
+ if ("unavailableReason" in strategy.native) {
35641
+ result = {
35642
+ vendor,
35643
+ source: "bundled",
35644
+ models: strategy.bundled,
35645
+ nativeUnavailableReason: strategy.native.unavailableReason
35646
+ };
35647
+ } else {
35648
+ const native = await tryNativeEnumeration(vendor, strategy.native);
35649
+ result = native.models ? { vendor, source: "native", models: native.models } : {
35650
+ vendor,
35651
+ source: "bundled",
35652
+ models: strategy.bundled,
35653
+ nativeUnavailableReason: native.reason
35654
+ };
35009
35655
  }
35010
- return { vendor, source: "bundled", models: bundledForVendor(vendor) };
35656
+ const ttl = result.source === "native" ? SUCCESS_TTL_MS : FAILURE_TTL_MS;
35657
+ cache.set(vendor, { result, expiresAt: Date.now() + ttl });
35658
+ return result;
35011
35659
  }
35012
35660
 
35013
35661
  // src/commands/models.ts
35014
- var listSubcommand2 = new Command("list").description("List models the active AI CLI is willing to accept").option(
35015
- "--vendor <vendor>",
35016
- "Override autodetection (claude | opencode)"
35017
- ).option("--json", "Emit JSON for programmatic consumption").action(async (options) => {
35662
+ var vendorList = SUPPORTED_VENDORS.join(" | ");
35663
+ var listSubcommand2 = new Command("list").description("List models the active AI CLI is willing to accept").option("--vendor <vendor>", `Override autodetection (${vendorList})`).option("--json", "Emit JSON for programmatic consumption").action(async (options) => {
35018
35664
  let vendor;
35019
35665
  if (options.vendor) {
35020
- if (options.vendor !== "claude" && options.vendor !== "opencode") {
35666
+ const requested = options.vendor.toLowerCase();
35667
+ if (!isModelVendor(requested)) {
35021
35668
  console.error(
35022
35669
  source_default.red(
35023
- `Invalid --vendor: "${options.vendor}". Must be "claude" or "opencode".`
35670
+ `Invalid --vendor: "${options.vendor}". Must be one of: ${vendorList}.`
35024
35671
  )
35025
35672
  );
35026
35673
  process.exit(1);
35027
35674
  }
35028
- vendor = options.vendor;
35675
+ vendor = requested;
35029
35676
  } else {
35030
- vendor = detectActiveVendor();
35677
+ vendor = await detectActiveVendor();
35031
35678
  if (!vendor) {
35032
35679
  if (options.json) {
35033
- console.log("[]");
35680
+ console.log(
35681
+ JSON.stringify({ vendor: null, source: null, models: [] }, null, 2)
35682
+ );
35034
35683
  return;
35035
35684
  }
35036
35685
  console.error(
@@ -35041,16 +35690,18 @@ var listSubcommand2 = new Command("list").description("List models the active AI
35041
35690
  process.exit(1);
35042
35691
  }
35043
35692
  }
35044
- const { source, models } = listModelsForVendor(vendor);
35693
+ const result = await listModelsForVendor(vendor);
35045
35694
  if (options.json) {
35046
- console.log(JSON.stringify(models, null, 2));
35695
+ console.log(JSON.stringify(result, null, 2));
35047
35696
  return;
35048
35697
  }
35698
+ const { source, models, nativeUnavailableReason } = result;
35049
35699
  console.log(source_default.bold(`Models for ${vendor} (${source})`));
35050
35700
  if (source === "bundled") {
35701
+ const reason = nativeUnavailableReason ? ` \u2014 ${nativeUnavailableReason}` : "";
35051
35702
  console.log(
35052
35703
  source_default.dim(
35053
- " Note: bundled fallback list \u2014 may be stale. Free-text input is always accepted."
35704
+ ` Note: bundled fallback list${reason}. Free-text input is always accepted.`
35054
35705
  )
35055
35706
  );
35056
35707
  }
@@ -35065,8 +35716,8 @@ var modelsCommand = new Command("models").description("Inspect models available
35065
35716
 
35066
35717
  // src/commands/team.ts
35067
35718
  var import_yaml2 = __toESM(require_dist(), 1);
35068
- import { existsSync as existsSync17, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
35069
- import { join as join20 } from "node:path";
35719
+ import { existsSync as existsSync18, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
35720
+ import { join as join21 } from "node:path";
35070
35721
  async function readStdin2() {
35071
35722
  const chunks = [];
35072
35723
  for await (const chunk of process.stdin) {
@@ -35125,7 +35776,7 @@ var resolveSubcommand = new Command("resolve").description("Resolve and print th
35125
35776
  async (options) => {
35126
35777
  const targetDir = process.cwd();
35127
35778
  requireOcrSetup(targetDir);
35128
- const ocrDir = join20(targetDir, ".ocr");
35779
+ const ocrDir = join21(targetDir, ".ocr");
35129
35780
  try {
35130
35781
  const { team } = loadTeamConfig(ocrDir);
35131
35782
  let override;
@@ -35164,8 +35815,8 @@ var setSubcommand = new Command("set").description("Persist a new default_team c
35164
35815
  }
35165
35816
  const targetDir = process.cwd();
35166
35817
  requireOcrSetup(targetDir);
35167
- const ocrDir = join20(targetDir, ".ocr");
35168
- const configPath = join20(ocrDir, "config.yaml");
35818
+ const ocrDir = join21(targetDir, ".ocr");
35819
+ const configPath = join21(ocrDir, "config.yaml");
35169
35820
  try {
35170
35821
  const raw = await readStdin2();
35171
35822
  const team = parseSessionOverride(raw);
@@ -35175,12 +35826,12 @@ var setSubcommand = new Command("set").description("Persist a new default_team c
35175
35826
  list.push(inst);
35176
35827
  byPersona.set(inst.persona, list);
35177
35828
  }
35178
- const doc = existsSync17(configPath) ? (0, import_yaml2.parseDocument)(readFileSync13(configPath, "utf-8")) : new import_yaml2.Document({});
35829
+ const doc = existsSync18(configPath) ? (0, import_yaml2.parseDocument)(readFileSync13(configPath, "utf-8")) : new import_yaml2.Document({});
35179
35830
  applyDefaultTeamSurgically(doc, byPersona);
35180
35831
  const yamlOutput = doc.toString({ lineWidth: 0 });
35181
35832
  writeFileSync8(configPath, yamlOutput, "utf-8");
35182
- const reviewersDir = join20(ocrDir, "skills", "references", "reviewers");
35183
- const metaPath = join20(ocrDir, "reviewers-meta.json");
35833
+ const reviewersDir = join21(ocrDir, "skills", "references", "reviewers");
35834
+ const metaPath = join21(ocrDir, "reviewers-meta.json");
35184
35835
  let metaWritten = false;
35185
35836
  try {
35186
35837
  const meta = generateReviewersMeta(reviewersDir, configPath);
@@ -35276,7 +35927,7 @@ var teamCommand = new Command("team").description("Resolve and persist team comp
35276
35927
 
35277
35928
  // src/commands/review.ts
35278
35929
  import { spawn as spawn3 } from "node:child_process";
35279
- import { join as join21 } from "node:path";
35930
+ import { join as join22 } from "node:path";
35280
35931
  init_db();
35281
35932
 
35282
35933
  // src/lib/vendor-resume.ts
@@ -35315,7 +35966,7 @@ var reviewCommand = new Command("review").description("Run or resume an OCR revi
35315
35966
  }
35316
35967
  const targetDir = process.cwd();
35317
35968
  requireOcrSetup(targetDir);
35318
- const ocrDir = join21(targetDir, ".ocr");
35969
+ const ocrDir = join22(targetDir, ".ocr");
35319
35970
  const db = await ensureDatabase(ocrDir);
35320
35971
  const session = getSession(db, options.resume);
35321
35972
  if (!session) {
@@ -35357,23 +36008,23 @@ var reviewCommand = new Command("review").description("Run or resume an OCR revi
35357
36008
  });
35358
36009
 
35359
36010
  // src/commands/update.ts
35360
- import { existsSync as existsSync18 } from "node:fs";
35361
- import { join as join22 } from "node:path";
36011
+ import { existsSync as existsSync19 } from "node:fs";
36012
+ import { join as join23 } from "node:path";
35362
36013
  function detectConfiguredTools(targetDir) {
35363
36014
  return AI_TOOLS.filter((tool) => {
35364
36015
  if (tool.commandStrategy === "subdirectory") {
35365
- const ocrDir = join22(targetDir, tool.commandsDir, "ocr");
35366
- return existsSync18(ocrDir);
36016
+ const ocrDir = join23(targetDir, tool.commandsDir, "ocr");
36017
+ return existsSync19(ocrDir);
35367
36018
  } else {
35368
- const reviewCmd = join22(targetDir, tool.commandsDir, "ocr-review.md");
35369
- return existsSync18(reviewCmd);
36019
+ const reviewCmd = join23(targetDir, tool.commandsDir, "ocr-review.md");
36020
+ return existsSync19(reviewCmd);
35370
36021
  }
35371
36022
  });
35372
36023
  }
35373
36024
  var updateCommand = new Command("update").description("Update OCR assets after package upgrade").option("--commands", "Update only commands/workflows").option(
35374
36025
  "--skills",
35375
36026
  "Update only skills (includes templates, references, assets)"
35376
- ).option("--inject", "Update only AGENTS.md/CLAUDE.md injection").option("--dry-run", "Preview changes without modifying files").action(async (options) => {
36027
+ ).option("--inject", "Update only instruction-file injection (AGENTS.md + each tool's native file)").option("--dry-run", "Preview changes without modifying files").action(async (options) => {
35377
36028
  const targetDir = process.cwd();
35378
36029
  requireOcrSetup(targetDir);
35379
36030
  console.log();
@@ -35440,7 +36091,7 @@ var updateCommand = new Command("update").description("Update OCR assets after p
35440
36091
  const result = installForTool(tool, targetDir);
35441
36092
  results.push(result);
35442
36093
  }
35443
- ensureGitignore(join22(targetDir, ".ocr"));
36094
+ ensureGitignore(join23(targetDir, ".ocr"));
35444
36095
  spinner.stop();
35445
36096
  const successful = results.filter((r) => r.success);
35446
36097
  const failed = results.filter((r) => !r.success);
@@ -35474,30 +36125,34 @@ var updateCommand = new Command("update").description("Update OCR assets after p
35474
36125
  }
35475
36126
  }
35476
36127
  if (updateInject) {
36128
+ const planned = plannedInstructionFiles(toolsToUpdate);
35477
36129
  if (options.dryRun) {
35478
36130
  console.log(source_default.dim(" Would update:"));
35479
- if (existsSync18(join22(targetDir, "AGENTS.md"))) {
35480
- console.log(source_default.dim(" \u2022 AGENTS.md (OCR managed block)"));
36131
+ for (const path2 of planned) {
36132
+ const verb = existsSync19(join23(targetDir, path2)) ? "update" : "create";
36133
+ console.log(source_default.dim(` \u2022 ${path2} (${verb} OCR managed block)`));
35481
36134
  }
35482
- if (existsSync18(join22(targetDir, "CLAUDE.md"))) {
35483
- console.log(source_default.dim(" \u2022 CLAUDE.md (OCR managed block)"));
36135
+ const staleDry = findStaleInstructionFiles(targetDir, planned);
36136
+ for (const warning of formatStaleWarnings(staleDry, "dry-run")) {
36137
+ console.log(source_default.dim(` \u2022 ${warning}`));
35484
36138
  }
35485
36139
  console.log();
35486
36140
  } else {
35487
- const spinner = ora("Updating AGENTS.md/CLAUDE.md...").start();
35488
- const injectResults = injectIntoProjectFiles(targetDir);
36141
+ const spinner = ora("Updating instruction files...").start();
36142
+ const injectResults = injectIntoProjectFiles(targetDir, toolsToUpdate);
35489
36143
  spinner.stop();
35490
- if (injectResults.agentsMd || injectResults.claudeMd) {
36144
+ if (injectResults.written.length > 0) {
35491
36145
  console.log(source_default.green(" \u2713 Instructions updated"));
35492
- if (injectResults.agentsMd) {
35493
- console.log(` ${source_default.green("\u2713")} AGENTS.md`);
35494
- }
35495
- if (injectResults.claudeMd) {
35496
- console.log(` ${source_default.green("\u2713")} CLAUDE.md`);
36146
+ for (const path2 of injectResults.written) {
36147
+ console.log(` ${source_default.green("\u2713")} ${path2}`);
35497
36148
  }
35498
36149
  } else {
35499
36150
  console.log(source_default.dim(" No instruction files to update"));
35500
36151
  }
36152
+ const stale = findStaleInstructionFiles(targetDir, injectResults.written);
36153
+ for (const warning of formatStaleWarnings(stale, "update")) {
36154
+ console.log(source_default.yellow(` \u26A0 ${warning}`));
36155
+ }
35501
36156
  console.log();
35502
36157
  }
35503
36158
  }
@@ -35511,14 +36166,15 @@ var updateCommand = new Command("update").description("Update OCR assets after p
35511
36166
  });
35512
36167
 
35513
36168
  // src/commands/dashboard.ts
35514
- import { existsSync as existsSync19 } from "node:fs";
35515
- import { join as join23, dirname as dirname7 } from "node:path";
36169
+ import { existsSync as existsSync20 } from "node:fs";
36170
+ import { join as join24, dirname as dirname9 } from "node:path";
35516
36171
  import { fileURLToPath } from "node:url";
36172
+ init_src();
35517
36173
  init_db();
35518
36174
  var __filename = fileURLToPath(import.meta.url);
35519
- var __dirname = dirname7(__filename);
36175
+ var __dirname = dirname9(__filename);
35520
36176
  function resolveServerPath() {
35521
- return join23(__dirname, "dashboard", "server.js");
36177
+ return join24(__dirname, "dashboard", "server.js");
35522
36178
  }
35523
36179
  var dashboardCommand = new Command("dashboard").description("Start the OCR dashboard web interface").option("-p, --port <port>", "Port to run the server on", "4173").option("--no-open", "Don't open the browser automatically").action(
35524
36180
  async (options) => {
@@ -35529,7 +36185,7 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
35529
36185
  console.error(source_default.red(`Error: Invalid port "${options.port}". Must be 1-65535.`));
35530
36186
  process.exit(1);
35531
36187
  }
35532
- const ocrDir = join23(targetDir, ".ocr");
36188
+ const ocrDir = join24(targetDir, ".ocr");
35533
36189
  try {
35534
36190
  await ensureDatabase(ocrDir);
35535
36191
  closeAllDatabases();
@@ -35543,7 +36199,7 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
35543
36199
  process.exit(1);
35544
36200
  }
35545
36201
  const serverPath = resolveServerPath();
35546
- if (!existsSync19(serverPath)) {
36202
+ if (!existsSync20(serverPath)) {
35547
36203
  console.error(source_default.red("Error: Dashboard server bundle not found."));
35548
36204
  console.error(
35549
36205
  source_default.dim(` Expected at: ${serverPath}`)
@@ -35577,8 +36233,8 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
35577
36233
  );
35578
36234
 
35579
36235
  // src/commands/doctor.ts
35580
- import { existsSync as existsSync20 } from "node:fs";
35581
- import { join as join24 } from "node:path";
36236
+ import { existsSync as existsSync21 } from "node:fs";
36237
+ import { join as join25 } from "node:path";
35582
36238
  init_db();
35583
36239
  function printStorageEngine(probeWriteEnabled) {
35584
36240
  console.log();
@@ -35635,10 +36291,10 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
35635
36291
  console.log(source_default.bold(" OCR Installation"));
35636
36292
  console.log();
35637
36293
  const ocrStatus = checkOcrSetup(targetDir);
35638
- const configPath = join24(targetDir, ".ocr", "config.yaml");
35639
- const dbPath = join24(targetDir, ".ocr", "data", "ocr.db");
35640
- const hasConfig = existsSync20(configPath);
35641
- const hasDb = existsSync20(dbPath);
36294
+ const configPath = join25(targetDir, ".ocr", "config.yaml");
36295
+ const dbPath = join25(targetDir, ".ocr", "data", "ocr.db");
36296
+ const hasConfig = existsSync21(configPath);
36297
+ const hasDb = existsSync21(dbPath);
35642
36298
  const ocrChecks = [
35643
36299
  { label: ".ocr/skills/", ok: ocrStatus.hasSkills },
35644
36300
  { label: ".ocr/sessions/", ok: ocrStatus.hasSessions },
@@ -35714,9 +36370,331 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
35714
36370
  console.log();
35715
36371
  });
35716
36372
 
36373
+ // src/commands/db.ts
36374
+ import { existsSync as existsSync22, readFileSync as readFileSync14 } from "node:fs";
36375
+ import { join as join26 } from "node:path";
36376
+ init_src();
36377
+ init_db();
36378
+ function fail4(message) {
36379
+ console.error(source_default.red(`Error: ${message}`));
36380
+ process.exit(1);
36381
+ }
36382
+ function resolveOcrDir() {
36383
+ const targetDir = process.cwd();
36384
+ requireOcrSetup(targetDir);
36385
+ return join26(targetDir, ".ocr");
36386
+ }
36387
+ function dbPathFor(ocrDir) {
36388
+ return join26(ocrDir, "data", "ocr.db");
36389
+ }
36390
+ function formatBytes(n) {
36391
+ if (n < 1024) return `${n} B`;
36392
+ const units = ["KB", "MB", "GB", "TB"];
36393
+ let v = n / 1024;
36394
+ let i = 0;
36395
+ while (v >= 1024 && i < units.length - 1) {
36396
+ v /= 1024;
36397
+ i++;
36398
+ }
36399
+ return `${v.toFixed(v >= 100 ? 0 : 1)} ${units[i]}`;
36400
+ }
36401
+ function liveDashboardPid(ocrDir) {
36402
+ const pidFile = join26(ocrDir, "data", "dashboard.pid");
36403
+ if (!existsSync22(pidFile)) return null;
36404
+ try {
36405
+ const pid = parseInt(readFileSync14(pidFile, "utf-8").trim(), 10);
36406
+ if (!Number.isNaN(pid) && isProcessAlive(pid)) return pid;
36407
+ } catch {
36408
+ }
36409
+ return null;
36410
+ }
36411
+ function guardExclusive(ocrDir, force, op) {
36412
+ const pid = liveDashboardPid(ocrDir);
36413
+ if (pid !== null && !force) {
36414
+ fail4(
36415
+ `a dashboard appears to be running (PID ${pid}); ${op} needs exclusive access to the database.
36416
+ Stop it first, or pass --force to proceed anyway.`
36417
+ );
36418
+ }
36419
+ }
36420
+ function printHealth(report) {
36421
+ console.log();
36422
+ console.log(source_default.bold(" Database Health"));
36423
+ console.log();
36424
+ console.log(` File: ${report.dbPath}`);
36425
+ console.log(` Size: ${formatBytes(report.fileSizeBytes)}`);
36426
+ if (report.reclaimableBytes > 0) {
36427
+ console.log(
36428
+ ` Reclaimable: ${source_default.yellow(formatBytes(report.reclaimableBytes))} ` + source_default.dim(`(${report.freelistCount} free pages \u2014 run \`ocr db vacuum\`)`)
36429
+ );
36430
+ }
36431
+ console.log(
36432
+ ` Records: ${report.sessionCount} session(s), ${report.eventCount} event(s)`
36433
+ );
36434
+ console.log();
36435
+ const ok = (s) => ` ${source_default.green("\u2713")} ${s}`;
36436
+ const bad = (s) => ` ${source_default.red("\u2717")} ${s}`;
36437
+ console.log(
36438
+ report.integrityOk ? ok("integrity_check: ok") : bad(`integrity_check: ${report.integrityErrors.length} error(s)`)
36439
+ );
36440
+ if (!report.integrityOk) {
36441
+ for (const e of report.integrityErrors.slice(0, 5)) {
36442
+ console.log(` ${source_default.dim(e)}`);
36443
+ }
36444
+ }
36445
+ const fkTotal = report.fkViolations.reduce((n, g) => n + g.count, 0) + report.protectedFkViolations.reduce((n, g) => n + g.count, 0);
36446
+ if (fkTotal === 0) {
36447
+ console.log(ok("foreign_key_check: 0 violations"));
36448
+ } else {
36449
+ console.log(bad(`foreign_key_check: ${fkTotal} violation(s)`));
36450
+ for (const g of report.fkViolations) {
36451
+ console.log(` ${source_default.dim(`${g.table}: ${g.count} orphan(s)`)}`);
36452
+ }
36453
+ for (const g of report.protectedFkViolations) {
36454
+ console.log(
36455
+ ` ${source_default.yellow(`${g.table}: ${g.count} (protected \u2014 manual review)`)}`
36456
+ );
36457
+ }
36458
+ }
36459
+ if (report.markdownDuplicateRows === 0) {
36460
+ console.log(ok("markdown_artifacts: no duplicates"));
36461
+ } else {
36462
+ console.log(
36463
+ bad(`markdown_artifacts: ${report.markdownDuplicateRows} duplicate row(s)`)
36464
+ );
36465
+ }
36466
+ const reapable = report.orphanTempFiles.filter((f) => f.reapable);
36467
+ if (report.orphanTempFiles.length > 0) {
36468
+ console.log(
36469
+ ` ${reapable.length > 0 ? source_default.yellow("\u26A0") : source_default.dim("\xB7")} orphan temp files: ${report.orphanTempFiles.length} (${reapable.length} reapable)`
36470
+ );
36471
+ }
36472
+ if (report.backupFiles.length > 0) {
36473
+ const total = report.backupFiles.reduce((n, b) => n + b.sizeBytes, 0);
36474
+ console.log(
36475
+ ` ${source_default.dim("\xB7")} backups: ${report.backupFiles.length} (${formatBytes(total)})`
36476
+ );
36477
+ }
36478
+ console.log();
36479
+ }
36480
+ function needsFix(report) {
36481
+ return !report.integrityOk || report.fkViolations.length > 0 || report.markdownDuplicateRows > 0 || report.orphanTempFiles.some((f) => f.reapable) || report.reclaimableBytes > 0;
36482
+ }
36483
+ var doctorSubcommand = new Command("doctor").description("Report database health; --fix repairs orphans/dupes and VACUUMs").option("--fix", "apply repairs: FK-orphan sweep, dedup, temp reap, VACUUM").option("--no-snapshot", "skip the pre-fix snapshot (with --fix)").option("--force", "proceed even if a live dashboard owns the database").option("--json", "emit the health report as JSON (implies no --fix)").action(
36484
+ async (options) => {
36485
+ const ocrDir = resolveOcrDir();
36486
+ const dbPath = dbPathFor(ocrDir);
36487
+ const db = await ensureDatabase(ocrDir);
36488
+ if (options.json) {
36489
+ console.log(JSON.stringify(collectDbHealth(db, dbPath), null, 2));
36490
+ return;
36491
+ }
36492
+ const before = collectDbHealth(db, dbPath);
36493
+ printHealth(before);
36494
+ if (!options.fix) {
36495
+ if (needsFix(before)) {
36496
+ console.log(
36497
+ source_default.dim(" Run `ocr db doctor --fix` to repair the issues above.")
36498
+ );
36499
+ console.log();
36500
+ } else {
36501
+ console.log(source_default.green(" \u2713 Database is healthy"));
36502
+ console.log();
36503
+ }
36504
+ return;
36505
+ }
36506
+ guardExclusive(ocrDir, options.force ?? false, "doctor --fix");
36507
+ const result = fixDb(db, dbPath, { snapshot: options.snapshot !== false });
36508
+ console.log(source_default.bold(" Repairs applied"));
36509
+ console.log();
36510
+ if (result.snapshotPath) {
36511
+ console.log(` ${source_default.dim("snapshot:")} ${result.snapshotPath}`);
36512
+ }
36513
+ if (result.totalFkOrphansDeleted > 0) {
36514
+ console.log(
36515
+ ` ${source_default.green("\u2713")} swept ${result.totalFkOrphansDeleted} FK-orphan row(s)`
36516
+ );
36517
+ for (const g of result.fkOrphansDeleted) {
36518
+ console.log(` ${source_default.dim(`${g.table}: ${g.count}`)}`);
36519
+ }
36520
+ }
36521
+ if (result.markdownDupsDeleted > 0) {
36522
+ console.log(
36523
+ ` ${source_default.green("\u2713")} removed ${result.markdownDupsDeleted} duplicate markdown row(s)`
36524
+ );
36525
+ }
36526
+ if (result.tempsReaped.length > 0) {
36527
+ console.log(
36528
+ ` ${source_default.green("\u2713")} reaped ${result.tempsReaped.length} orphan temp file(s)`
36529
+ );
36530
+ }
36531
+ if (result.vacuumed) {
36532
+ const saved = result.sizeBeforeBytes - result.sizeAfterBytes;
36533
+ console.log(
36534
+ ` ${source_default.green("\u2713")} VACUUM: ${formatBytes(result.sizeBeforeBytes)} \u2192 ${formatBytes(result.sizeAfterBytes)} ` + source_default.dim(`(reclaimed ${formatBytes(Math.max(0, saved))})`)
36535
+ );
36536
+ }
36537
+ console.log();
36538
+ if (result.protectedViolationsRemaining.length > 0) {
36539
+ console.log(
36540
+ source_default.yellow(
36541
+ " \u26A0 Violations remain in protected (system-of-record) tables:"
36542
+ )
36543
+ );
36544
+ for (const g of result.protectedViolationsRemaining) {
36545
+ console.log(` ${source_default.yellow(`${g.table}: ${g.count}`)}`);
36546
+ }
36547
+ console.log();
36548
+ }
36549
+ if (result.integrityOkAfter && result.fkViolationsAfter === 0) {
36550
+ console.log(source_default.green(" \u2713 Database repaired and healthy"));
36551
+ } else {
36552
+ console.log(
36553
+ source_default.red(
36554
+ ` \u2717 Post-fix check: integrity ${result.integrityOkAfter ? "ok" : "FAILED"}, ${result.fkViolationsAfter} FK violation(s) remaining`
36555
+ )
36556
+ );
36557
+ process.exitCode = 1;
36558
+ }
36559
+ console.log();
36560
+ }
36561
+ );
36562
+ var vacuumSubcommand = new Command("vacuum").description("Checkpoint the WAL and VACUUM the database (snapshot-first)").option("--no-snapshot", "skip the pre-vacuum snapshot").option("--force", "proceed even if a live dashboard owns the database").action(async (options) => {
36563
+ const ocrDir = resolveOcrDir();
36564
+ const dbPath = dbPathFor(ocrDir);
36565
+ guardExclusive(ocrDir, options.force ?? false, "vacuum");
36566
+ const db = await ensureDatabase(ocrDir);
36567
+ const result = vacuumDb(db, dbPath, { snapshot: options.snapshot !== false });
36568
+ console.log();
36569
+ if (result.snapshotPath) {
36570
+ console.log(` ${source_default.dim("snapshot:")} ${result.snapshotPath}`);
36571
+ }
36572
+ console.log(
36573
+ ` ${source_default.green("\u2713")} VACUUM: ${formatBytes(result.sizeBeforeBytes)} \u2192 ${formatBytes(result.sizeAfterBytes)} ` + source_default.dim(`(reclaimed ${formatBytes(result.reclaimedBytes)})`)
36574
+ );
36575
+ console.log();
36576
+ });
36577
+ var pruneSubcommand = new Command("prune").description(
36578
+ "Drop derived artifacts of old CLOSED sessions (events + sessions kept)"
36579
+ ).option(
36580
+ "--keep-sessions <n>",
36581
+ "protect the N most-recently-active closed sessions",
36582
+ (v) => parseInt(v, 10)
36583
+ ).option(
36584
+ "--older-than <days>",
36585
+ "only prune closed sessions quiet for more than D days",
36586
+ (v) => parseInt(v, 10)
36587
+ ).option("--dry-run", "show what would be pruned without deleting").option("--force", "proceed even if a live dashboard owns the database").action(
36588
+ async (options) => {
36589
+ const ocrDir = resolveOcrDir();
36590
+ const dbPath = dbPathFor(ocrDir);
36591
+ if (options.keepSessions === void 0 && options.olderThan === void 0) {
36592
+ fail4(
36593
+ "prune needs a bound: pass --older-than <days> and/or --keep-sessions <n>."
36594
+ );
36595
+ }
36596
+ if (!options.dryRun) {
36597
+ guardExclusive(ocrDir, options.force ?? false, "prune");
36598
+ }
36599
+ const db = await ensureDatabase(ocrDir);
36600
+ const result = pruneDb(db, dbPath, {
36601
+ keepSessions: options.keepSessions,
36602
+ olderThanDays: options.olderThan,
36603
+ dryRun: options.dryRun ?? false
36604
+ });
36605
+ console.log();
36606
+ if (result.prunedSessions.length === 0) {
36607
+ console.log(source_default.green(" \u2713 Nothing to prune"));
36608
+ console.log();
36609
+ return;
36610
+ }
36611
+ const verb = result.dryRun ? "Would prune" : "Pruned";
36612
+ console.log(
36613
+ source_default.bold(
36614
+ ` ${verb} ${result.totalArtifactRows} artifact row(s) across ${result.prunedSessions.length} session(s)`
36615
+ )
36616
+ );
36617
+ console.log();
36618
+ for (const p of result.prunedSessions.slice(0, 20)) {
36619
+ console.log(
36620
+ ` ${source_default.dim("\xB7")} ${p.sessionId} ${source_default.dim(`(${p.artifactRows} rows)`)}`
36621
+ );
36622
+ }
36623
+ if (result.prunedSessions.length > 20) {
36624
+ console.log(
36625
+ ` ${source_default.dim(`\u2026 and ${result.prunedSessions.length - 20} more`)}`
36626
+ );
36627
+ }
36628
+ console.log();
36629
+ if (result.snapshotPath) {
36630
+ console.log(` ${source_default.dim("snapshot:")} ${result.snapshotPath}`);
36631
+ }
36632
+ console.log(
36633
+ source_default.dim(
36634
+ result.dryRun ? " Re-run without --dry-run to apply. Events + session rows are always kept." : " Events + session rows were kept; sessions remain fully auditable."
36635
+ )
36636
+ );
36637
+ console.log();
36638
+ }
36639
+ );
36640
+ function validatePruneBackupsOptions(options) {
36641
+ if (!Number.isInteger(options.keep) || options.keep < 0) {
36642
+ return `--keep must be a non-negative integer (got "${String(options.keep)}").`;
36643
+ }
36644
+ if (options.keep === 0 && !options.force && !options.dryRun) {
36645
+ return "--keep 0 removes every backup (including any just-written snapshot). Re-run with --dry-run to preview, or --force to confirm.";
36646
+ }
36647
+ return null;
36648
+ }
36649
+ var pruneBackupsSubcommand = new Command("prune-backups").description("Delete old ocr.db.bak.* snapshots, keeping the most recent few").option(
36650
+ "--keep <n>",
36651
+ "retain the N most-recent backups (default 1; 0 removes all, requires --force)",
36652
+ // Raw conversion only — `Number('oops')` is NaN and flows into
36653
+ // validatePruneBackupsOptions, the single validation home. (parseInt would
36654
+ // also silently accept "3abc" → 3; Number rejects it as NaN.)
36655
+ (v) => Number(v),
36656
+ 1
36657
+ ).option("--force", "permit --keep 0 (removing the last backup / safety net)").option("--dry-run", "show what would be deleted without deleting").action(async (options) => {
36658
+ const ocrDir = resolveOcrDir();
36659
+ const dataDir = join26(ocrDir, "data");
36660
+ const invalid = validatePruneBackupsOptions(options);
36661
+ if (invalid !== null) {
36662
+ fail4(invalid);
36663
+ }
36664
+ const result = pruneBackups(dataDir, dbPathFor(ocrDir), {
36665
+ keep: options.keep,
36666
+ dryRun: options.dryRun ?? false
36667
+ });
36668
+ console.log();
36669
+ if (result.deleted.length === 0) {
36670
+ console.log(source_default.green(" \u2713 No backups to remove"));
36671
+ console.log();
36672
+ return;
36673
+ }
36674
+ const verb = result.dryRun ? "Would delete" : "Deleted";
36675
+ console.log(
36676
+ source_default.bold(
36677
+ ` ${verb} ${result.deleted.length} backup(s) \u2014 ${formatBytes(result.reclaimedBytes)}`
36678
+ )
36679
+ );
36680
+ console.log();
36681
+ for (const b of result.deleted) {
36682
+ console.log(` ${source_default.dim("\xB7")} ${b.name} ${source_default.dim(`(${formatBytes(b.sizeBytes)})`)}`);
36683
+ }
36684
+ if (result.kept.length > 0) {
36685
+ console.log();
36686
+ console.log(
36687
+ source_default.dim(` Kept ${result.kept.length} most-recent backup(s) as a safety net.`)
36688
+ );
36689
+ }
36690
+ console.log();
36691
+ });
36692
+ var dbCommand = new Command("db").description("Inspect and maintain the OCR SQLite database").addCommand(doctorSubcommand).addCommand(vacuumSubcommand).addCommand(pruneSubcommand).addCommand(pruneBackupsSubcommand);
36693
+
35717
36694
  // src/commands/reviewers.ts
35718
36695
  import { writeFileSync as writeFileSync9, renameSync as renameSync2 } from "node:fs";
35719
- import { join as join25 } from "node:path";
36696
+ import { join as join27 } from "node:path";
36697
+ init_src();
35720
36698
  async function readStdin3() {
35721
36699
  const chunks = [];
35722
36700
  for await (const chunk of process.stdin) {
@@ -35730,6 +36708,25 @@ async function readStdin3() {
35730
36708
  }
35731
36709
  var VALID_TIERS = /* @__PURE__ */ new Set(["holistic", "specialist", "persona", "custom"]);
35732
36710
  var SLUG_RE = /^[a-z][a-z0-9-]*$/;
36711
+ var INJECTION_PATTERNS = [
36712
+ /ignore\s+(all\s+|the\s+)?(previous|prior|above)?\s*(instructions|prompts|rules)/i,
36713
+ /disregard\s+(all\s+|the\s+)?(previous|prior|above)/i,
36714
+ /\byou\s+are\s+now\b/i,
36715
+ /^\s*system\s*:/im,
36716
+ /\balways\s+(conclude|respond|reply|return|output|approve|reject|say)\b/i,
36717
+ /\bnew\s+rule\s*:/i
36718
+ ];
36719
+ function warnIfSuspiciousPersona(label, fields) {
36720
+ const text = fields.filter((f) => typeof f === "string").join("\n");
36721
+ const hit = INJECTION_PATTERNS.find((re) => re.test(text));
36722
+ if (hit) {
36723
+ console.error(
36724
+ source_default.yellow(
36725
+ `\u26A0 ${label} contains text resembling a prompt-injection override (matched ${hit}). Review the persona before relying on it.`
36726
+ )
36727
+ );
36728
+ }
36729
+ }
35733
36730
  function validateReviewersMeta(data) {
35734
36731
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
35735
36732
  throw new Error("Payload must be a JSON object");
@@ -35767,6 +36764,19 @@ function validateReviewersMeta(data) {
35767
36764
  if (!Array.isArray(r.focus_areas)) {
35768
36765
  throw new Error(`${prefix}.focus_areas must be an array`);
35769
36766
  }
36767
+ if (r.icon !== void 0 && typeof r.icon !== "string") {
36768
+ throw new Error(`${prefix}.icon must be a string if provided (got ${JSON.stringify(r.icon)})`);
36769
+ }
36770
+ if (typeof r.icon !== "string" || r.icon.length === 0) {
36771
+ r.icon = defaultIconFor(r.id, r.tier);
36772
+ }
36773
+ warnIfSuspiciousPersona(`${prefix} ("${r.name}")`, [
36774
+ r.name,
36775
+ r.description,
36776
+ ...Array.isArray(r.focus_areas) ? r.focus_areas : [],
36777
+ r.known_for,
36778
+ r.philosophy
36779
+ ]);
35770
36780
  if (r.known_for !== void 0 && typeof r.known_for !== "string") {
35771
36781
  throw new Error(`${prefix}.known_for must be a string if provided`);
35772
36782
  }
@@ -35779,17 +36789,17 @@ function validateReviewersMeta(data) {
35779
36789
  var syncSubcommand2 = new Command("sync").description("Sync reviewers-meta.json from reviewer markdown files or structured JSON").option("--stdin", "Read reviewers JSON from stdin (for AI-invoked sync)").action(async (options) => {
35780
36790
  const targetDir = process.cwd();
35781
36791
  requireOcrSetup(targetDir);
35782
- const ocrDir = join25(targetDir, ".ocr");
36792
+ const ocrDir = join27(targetDir, ".ocr");
35783
36793
  if (!options.stdin) {
35784
36794
  try {
35785
- const reviewersDir = join25(ocrDir, "skills", "references", "reviewers");
35786
- const configPath = join25(ocrDir, "config.yaml");
36795
+ const reviewersDir = join27(ocrDir, "skills", "references", "reviewers");
36796
+ const configPath = join27(ocrDir, "config.yaml");
35787
36797
  const meta = generateReviewersMeta(reviewersDir, configPath);
35788
36798
  if (!meta || meta.reviewers.length === 0) {
35789
36799
  console.error(source_default.yellow("No reviewer files found in .ocr/skills/references/reviewers/"));
35790
36800
  process.exit(1);
35791
36801
  }
35792
- const metaPath = join25(ocrDir, "reviewers-meta.json");
36802
+ const metaPath = join27(ocrDir, "reviewers-meta.json");
35793
36803
  const tmpPath = metaPath + ".tmp";
35794
36804
  writeFileSync9(tmpPath, JSON.stringify(meta, null, 2) + "\n");
35795
36805
  renameSync2(tmpPath, metaPath);
@@ -35819,7 +36829,7 @@ var syncSubcommand2 = new Command("sync").description("Sync reviewers-meta.json
35819
36829
  throw new Error("Invalid JSON on stdin");
35820
36830
  }
35821
36831
  const meta = validateReviewersMeta(parsed);
35822
- const metaPath = join25(ocrDir, "reviewers-meta.json");
36832
+ const metaPath = join27(ocrDir, "reviewers-meta.json");
35823
36833
  const tmpPath = metaPath + ".tmp";
35824
36834
  writeFileSync9(tmpPath, JSON.stringify(meta, null, 2) + "\n");
35825
36835
  renameSync2(tmpPath, metaPath);
@@ -35845,27 +36855,75 @@ var syncSubcommand2 = new Command("sync").description("Sync reviewers-meta.json
35845
36855
  });
35846
36856
  var reviewersCommand = new Command("reviewers").description("Manage OCR reviewer metadata").addCommand(syncSubcommand2);
35847
36857
 
36858
+ // src/commands/host.ts
36859
+ function describeRow(id) {
36860
+ const tool = getToolById(id);
36861
+ const caps = getHostCapabilities(id);
36862
+ return {
36863
+ id,
36864
+ name: tool?.name ?? id,
36865
+ subagentSpawn: caps.subagentSpawn,
36866
+ perTaskModel: caps.perTaskModel,
36867
+ phase4: caps.subagentSpawn ? "parallel-subagents" : "sequential"
36868
+ };
36869
+ }
36870
+ var capabilitiesSubcommand = new Command("capabilities").description("Print host (AI CLI) Phase-4 capabilities").option("--tool <id>", "Show capabilities for a single tool id").option("--json", "Output JSON").action((options) => {
36871
+ if (options.tool) {
36872
+ const id = options.tool.trim().toLowerCase();
36873
+ if (!getToolIds().includes(id)) {
36874
+ console.error(
36875
+ source_default.red(
36876
+ `Error: unknown tool id "${options.tool}". Valid ids: ${getToolIds().join(", ")}`
36877
+ )
36878
+ );
36879
+ process.exit(1);
36880
+ }
36881
+ const row = describeRow(id);
36882
+ if (options.json) {
36883
+ console.log(JSON.stringify(row, null, 2));
36884
+ } else {
36885
+ printRows([row]);
36886
+ }
36887
+ return;
36888
+ }
36889
+ const rows = AI_TOOLS.map((t) => describeRow(t.id));
36890
+ if (options.json) {
36891
+ console.log(JSON.stringify(rows, null, 2));
36892
+ } else {
36893
+ printRows(rows);
36894
+ }
36895
+ });
36896
+ function printRows(rows) {
36897
+ const yn = (v) => v ? source_default.green("yes") : source_default.dim("no");
36898
+ for (const row of rows) {
36899
+ console.log(
36900
+ `${source_default.bold(row.name.padEnd(20))} subagentSpawn=${yn(row.subagentSpawn)} perTaskModel=${yn(row.perTaskModel)} \u2192 ${source_default.cyan(row.phase4)}`
36901
+ );
36902
+ }
36903
+ }
36904
+ var hostCommand = new Command("host").description("Inspect host (AI CLI) capabilities").addCommand(capabilitiesSubcommand);
36905
+
35848
36906
  // src/lib/update-check.ts
35849
36907
  import { homedir } from "node:os";
35850
- import { join as join26 } from "node:path";
35851
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, mkdirSync as mkdirSync7 } from "node:fs";
36908
+ import { join as join28 } from "node:path";
36909
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync10, mkdirSync as mkdirSync8 } from "node:fs";
35852
36910
  var PACKAGE_NAME = "@open-code-review/cli";
35853
36911
  var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
35854
- var CACHE_DIR2 = join26(homedir(), ".ocr");
35855
- var CACHE_FILE = join26(CACHE_DIR2, "update-check.json");
36912
+ var CACHE_DIR2 = join28(homedir(), ".ocr");
36913
+ var CACHE_FILE = join28(CACHE_DIR2, "update-check.json");
35856
36914
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
35857
36915
  var FETCH_TIMEOUT_MS = 3e3;
35858
36916
  function readCache(cacheFile) {
35859
36917
  try {
35860
- return JSON.parse(readFileSync14(cacheFile, "utf-8"));
36918
+ return JSON.parse(readFileSync15(cacheFile, "utf-8"));
35861
36919
  } catch {
35862
36920
  return null;
35863
36921
  }
35864
36922
  }
35865
- function writeCache(cacheFile, cache) {
36923
+ function writeCache(cacheFile, cache2) {
35866
36924
  try {
35867
- mkdirSync7(join26(cacheFile, ".."), { recursive: true });
35868
- writeFileSync10(cacheFile, JSON.stringify(cache));
36925
+ mkdirSync8(join28(cacheFile, ".."), { recursive: true });
36926
+ writeFileSync10(cacheFile, JSON.stringify(cache2));
35869
36927
  } catch {
35870
36928
  }
35871
36929
  }
@@ -35885,16 +36943,16 @@ async function checkForUpdate(currentVersion, options) {
35885
36943
  if (process.env.CI || process.env.OCR_NO_UPDATE_CHECK) {
35886
36944
  return null;
35887
36945
  }
35888
- const cacheFile = join26(options?.cacheDir ?? CACHE_DIR2, "update-check.json");
36946
+ const cacheFile = join28(options?.cacheDir ?? CACHE_DIR2, "update-check.json");
35889
36947
  try {
35890
- const cache = readCache(cacheFile);
35891
- if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {
35892
- if (!cache.latestVersion) return null;
35893
- if (!isNewer(cache.latestVersion, currentVersion)) return null;
36948
+ const cache2 = readCache(cacheFile);
36949
+ if (cache2 && Date.now() - cache2.lastCheck < CHECK_INTERVAL_MS) {
36950
+ if (!cache2.latestVersion) return null;
36951
+ if (!isNewer(cache2.latestVersion, currentVersion)) return null;
35894
36952
  return {
35895
36953
  updateAvailable: true,
35896
36954
  currentVersion,
35897
- latestVersion: cache.latestVersion,
36955
+ latestVersion: cache2.latestVersion,
35898
36956
  updateCommand: detectUpdateCommand()
35899
36957
  };
35900
36958
  }
@@ -35944,7 +37002,9 @@ program2.addCommand(reviewCommand);
35944
37002
  program2.addCommand(updateCommand);
35945
37003
  program2.addCommand(dashboardCommand);
35946
37004
  program2.addCommand(doctorCommand);
37005
+ program2.addCommand(dbCommand);
35947
37006
  program2.addCommand(reviewersCommand);
37007
+ program2.addCommand(hostCommand);
35948
37008
  await program2.parseAsync();
35949
37009
  if (subcommand && HUMAN_COMMANDS.has(subcommand)) {
35950
37010
  const drift = checkLocalArtifactVersion(process.cwd(), CLI_VERSION);