@open-code-review/cli 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +2 -0
  2. package/dist/dashboard/client/assets/{_basePickBy-B3ALyupE.js → _basePickBy-BBPb8BJA.js} +1 -1
  3. package/dist/dashboard/client/assets/{_baseUniq-b2RALAWc.js → _baseUniq-CFHdos6T.js} +1 -1
  4. package/dist/dashboard/client/assets/{arc-DcSVvhUd.js → arc-BKGGWA2F.js} +1 -1
  5. package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-BNUlmSCS.js → architectureDiagram-VXUJARFQ-B_ovNjX1.js} +1 -1
  6. package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-BmhiQVwa.js → blockDiagram-VD42YOAC-C2M-avVp.js} +1 -1
  7. package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-jyJ3WOv5.js → c4Diagram-YG6GDRKO-BtOBpAzH.js} +1 -1
  8. package/dist/dashboard/client/assets/channel-rgw7C1e7.js +1 -0
  9. package/dist/dashboard/client/assets/{chunk-4BX2VUAB-x1dQU_s3.js → chunk-4BX2VUAB-Cz2EbHPl.js} +1 -1
  10. package/dist/dashboard/client/assets/{chunk-55IACEB6-CwbsE2XQ.js → chunk-55IACEB6-C8xpXw9G.js} +1 -1
  11. package/dist/dashboard/client/assets/{chunk-B4BG7PRW-BaE7c-ti.js → chunk-B4BG7PRW-BSRfOovX.js} +1 -1
  12. package/dist/dashboard/client/assets/{chunk-DI55MBZ5-Bw5PUaMK.js → chunk-DI55MBZ5-CEUbYQWn.js} +1 -1
  13. package/dist/dashboard/client/assets/{chunk-FMBD7UC4-B7cF6P3s.js → chunk-FMBD7UC4-5xWP6GRj.js} +1 -1
  14. package/dist/dashboard/client/assets/{chunk-QN33PNHL-OY4evNHd.js → chunk-QN33PNHL-DfNCVcy8.js} +1 -1
  15. package/dist/dashboard/client/assets/{chunk-QZHKN3VN-BpjQwIWz.js → chunk-QZHKN3VN--OdToKKu.js} +1 -1
  16. package/dist/dashboard/client/assets/{chunk-TZMSLE5B-D8b_Oq9B.js → chunk-TZMSLE5B-B_0K0Qso.js} +1 -1
  17. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-DTGi7d9X.js +1 -0
  18. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-DTGi7d9X.js +1 -0
  19. package/dist/dashboard/client/assets/clone-Cz7hswqi.js +1 -0
  20. package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-C-sfP8PN.js → cose-bilkent-S5V4N54A-Cc_Dmnxz.js} +1 -1
  21. package/dist/dashboard/client/assets/{dagre-6UL2VRFP-Cqfo0NRg.js → dagre-6UL2VRFP-DaAfvUXU.js} +1 -1
  22. package/dist/dashboard/client/assets/{diagram-PSM6KHXK-BR3ppxqI.js → diagram-PSM6KHXK-7idwN0rC.js} +1 -1
  23. package/dist/dashboard/client/assets/{diagram-QEK2KX5R-Dvcx6x3R.js → diagram-QEK2KX5R-D9j9H13n.js} +1 -1
  24. package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DoyBLnVN.js → diagram-S2PKOQOG-SMF5SB0K.js} +1 -1
  25. package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-hy77l1cL.js → erDiagram-Q2GNP2WA-EVJ4Qa2F.js} +1 -1
  26. package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-Bz0B1rKM.js → flowDiagram-NV44I4VS-tZ7SFE77.js} +1 -1
  27. package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-CLgrZPoC.js → ganttDiagram-JELNMOA3-DFSqguY7.js} +1 -1
  28. package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js → gitGraphDiagram-V2S2FVAM-CqHdP3HE.js} +1 -1
  29. package/dist/dashboard/client/assets/{graph-DDBMM_t2.js → graph-C0XnkNkk.js} +1 -1
  30. package/dist/dashboard/client/assets/{index-Cr9yEo_B.js → index-C3NEq704.js} +133 -138
  31. package/dist/dashboard/client/assets/index-CzxeSSaQ.css +1 -0
  32. package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-Bhn1FmAk.js → infoDiagram-HS3SLOUP-DlXZo9U2.js} +1 -1
  33. package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CzGbjX1y.js → journeyDiagram-XKPGCS4Q-CgC8_7eN.js} +1 -1
  34. package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Da77-WYk.js → kanban-definition-3W4ZIXB7-BMAw_jNp.js} +1 -1
  35. package/dist/dashboard/client/assets/{layout-CVwSB-GS.js → layout-XjM3Q-ka.js} +1 -1
  36. package/dist/dashboard/client/assets/{linear-CTRAc5Jn.js → linear-CMUrrr1X.js} +1 -1
  37. package/dist/dashboard/client/assets/{mermaid-renderer-Bjo170ax.js → mermaid-renderer-D2jYNs7K.js} +4 -4
  38. package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-B55C2odl.js → mindmap-definition-VGOIOE7T-CL4hv-vg.js} +1 -1
  39. package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-5lrQLrSz.js → pieDiagram-ADFJNKIX-DTqv-1h1.js} +1 -1
  40. package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-Bg55gC30.js → quadrantDiagram-AYHSOK5B-BpFlSW9N.js} +1 -1
  41. package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-CyR4YFJY.js → requirementDiagram-UZGBJVZJ-BqYqqXL4.js} +1 -1
  42. package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BVWKr9_-.js → sankeyDiagram-TZEHDZUN-kEI9kntR.js} +1 -1
  43. package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-D0AJg_tE.js → sequenceDiagram-WL72ISMW-Cnu_1j-N.js} +1 -1
  44. package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-BuHpTgim.js → stateDiagram-FKZM4ZOC-BoC-rqoG.js} +1 -1
  45. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-COR3QD3v.js +1 -0
  46. package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-LDhpAmDd.js → timeline-definition-IT6M3QCI-CXMWuzDL.js} +1 -1
  47. package/dist/dashboard/client/assets/{treemap-GDKQZRPO-Dd4gjvUl.js → treemap-GDKQZRPO-o9ZFgpbJ.js} +1 -1
  48. package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-B9RDod39.js → xychartDiagram-PRI3JC2R-CfIuUpeA.js} +1 -1
  49. package/dist/dashboard/client/index.html +2 -2
  50. package/dist/dashboard/server.js +1175 -450
  51. package/dist/index.js +1489 -312
  52. package/dist/lib/db/index.js +666 -48
  53. package/dist/lib/runtime-config.js +29 -13
  54. package/dist/lib/state/index.js +2196 -0
  55. package/package.json +9 -5
  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-Z1pPudAt.css +0 -1
  61. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +0 -1
package/dist/index.js CHANGED
@@ -39,6 +39,30 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
39
39
  mod
40
40
  ));
41
41
 
42
+ // src/lib/runtime-checks.ts
43
+ function isSupportedNode(version) {
44
+ const [major = 0, minor = 0] = version.split(".").map((n) => Number.parseInt(n, 10) || 0);
45
+ return major > NODE_FLOOR.major || major === NODE_FLOOR.major && minor >= NODE_FLOOR.minor;
46
+ }
47
+ function nodeVersionGuardMessage(version) {
48
+ return `
49
+ Open Code Review requires Node.js >= ${NODE_FLOOR.major}.${NODE_FLOOR.minor} (it uses Node's built-in SQLite, \`node:sqlite\`).
50
+ You have Node ${version}. Upgrade Node (e.g. \`nvm install 22 && nvm use 22\`) and re-run.
51
+
52
+ `;
53
+ }
54
+ function isSuppressibleSqliteWarning(warning) {
55
+ const message = typeof warning === "string" ? warning : warning?.message;
56
+ return typeof message === "string" && message.includes("SQLite is an experimental feature");
57
+ }
58
+ var NODE_FLOOR;
59
+ var init_runtime_checks = __esm({
60
+ "src/lib/runtime-checks.ts"() {
61
+ "use strict";
62
+ NODE_FLOOR = { major: 22, minor: 5 };
63
+ }
64
+ });
65
+
42
66
  // ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/error.js
43
67
  var require_error = __commonJS({
44
68
  "../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/error.js"(exports) {
@@ -15993,6 +16017,80 @@ var require_emoji_regex2 = __commonJS({
15993
16017
  }
15994
16018
  });
15995
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
+ function isProcessAlive(pid) {
16038
+ try {
16039
+ process.kill(pid, 0);
16040
+ return true;
16041
+ } catch (err) {
16042
+ return !(err instanceof Error && "code" in err && err.code === "ESRCH");
16043
+ }
16044
+ }
16045
+ function defaultIconFor(id, tier) {
16046
+ return BUILTIN_ICON_MAP[id] ?? (tier === "persona" ? "brain" : "user");
16047
+ }
16048
+ function hostCapabilitiesFor(vendor) {
16049
+ return vendor && HOST_CAPABILITIES[vendor] || DEFAULT_HOST_CAPABILITIES;
16050
+ }
16051
+ var execFilePromise, isWindows, BUILTIN_ICON_MAP, DEFAULT_HOST_CAPABILITIES, HOST_CAPABILITIES;
16052
+ var init_src = __esm({
16053
+ "../shared/platform/src/index.ts"() {
16054
+ "use strict";
16055
+ execFilePromise = promisify(execFile);
16056
+ isWindows = process.platform === "win32";
16057
+ BUILTIN_ICON_MAP = {
16058
+ architect: "blocks",
16059
+ fullstack: "layers",
16060
+ reliability: "activity",
16061
+ "staff-engineer": "compass",
16062
+ principal: "crown",
16063
+ frontend: "layout",
16064
+ backend: "server",
16065
+ infrastructure: "cloud",
16066
+ performance: "gauge",
16067
+ accessibility: "accessibility",
16068
+ data: "database",
16069
+ devops: "rocket",
16070
+ dx: "terminal",
16071
+ mobile: "smartphone",
16072
+ security: "shield-alert",
16073
+ quality: "sparkles",
16074
+ testing: "test-tubes",
16075
+ ai: "bot",
16076
+ "docs-writer": "file-text"
16077
+ };
16078
+ DEFAULT_HOST_CAPABILITIES = {
16079
+ subagentSpawn: false,
16080
+ perTaskModel: false
16081
+ };
16082
+ HOST_CAPABILITIES = {
16083
+ // Claude Code: Task tool + per-subagent model frontmatter.
16084
+ claude: { subagentSpawn: true, perTaskModel: true },
16085
+ // OpenCode: `--agent` sub-agent primitive, but no per-task model override.
16086
+ opencode: { subagentSpawn: true, perTaskModel: false },
16087
+ // Gemini CLI / Codex: no in-agent Task primitive → sequential Phase 4.
16088
+ gemini: { subagentSpawn: false, perTaskModel: false },
16089
+ codex: { subagentSpawn: false, perTaskModel: false }
16090
+ };
16091
+ }
16092
+ });
16093
+
15996
16094
  // ../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/identity.js
15997
16095
  var require_identity = __commonJS({
15998
16096
  "../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/identity.js"(exports) {
@@ -23320,21 +23418,41 @@ var init_result_mapper = __esm({
23320
23418
  });
23321
23419
 
23322
23420
  // src/lib/db/engine.ts
23323
- import BetterSqlite3 from "better-sqlite3";
23324
- function isBusyError(e) {
23325
- if (e instanceof BetterSqlite3.SqliteError) {
23326
- return e.code === "SQLITE_BUSY" || e.code === "SQLITE_BUSY_SNAPSHOT";
23421
+ import { createRequire as createRequire2 } from "node:module";
23422
+ function applyEnginePreconditions() {
23423
+ if (_preconditionsApplied) return;
23424
+ _preconditionsApplied = true;
23425
+ const originalEmitWarning = process.emitWarning.bind(process);
23426
+ process.emitWarning = (warning, ...args) => {
23427
+ if (isSuppressibleSqliteWarning(warning)) return;
23428
+ originalEmitWarning(warning, ...args);
23429
+ };
23430
+ }
23431
+ function newDatabase(path2) {
23432
+ if (!_DatabaseSyncCtor) {
23433
+ applyEnginePreconditions();
23434
+ try {
23435
+ _DatabaseSyncCtor = nodeRequire("node:sqlite").DatabaseSync;
23436
+ } catch (e) {
23437
+ if (!isSupportedNode(process.versions.node)) {
23438
+ throw new Error(nodeVersionGuardMessage(process.versions.node).trim());
23439
+ }
23440
+ throw e;
23441
+ }
23327
23442
  }
23328
- const code = e?.code;
23329
- return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_SNAPSHOT";
23443
+ return new _DatabaseSyncCtor(path2);
23444
+ }
23445
+ function isBusyError(e) {
23446
+ const errcode = e?.errcode;
23447
+ return errcode === SQLITE_BUSY || errcode === SQLITE_BUSY_SNAPSHOT;
23330
23448
  }
23331
23449
  function sleepSync(ms) {
23332
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
23450
+ Atomics.wait(SLEEP_BUF, 0, 0, ms);
23333
23451
  }
23334
23452
  function probeEngine() {
23335
23453
  try {
23336
- const db = new BetterSqlite3(":memory:");
23337
- db.pragma("journal_mode = WAL");
23454
+ const db = newDatabase(":memory:");
23455
+ db.exec("PRAGMA journal_mode = WAL");
23338
23456
  db.exec("CREATE TABLE _probe(x); INSERT INTO _probe VALUES (1);");
23339
23457
  const row = db.prepare("SELECT sqlite_version() AS v").get();
23340
23458
  db.close();
@@ -23344,33 +23462,47 @@ function probeEngine() {
23344
23462
  }
23345
23463
  }
23346
23464
  function openEngine(dbPath) {
23347
- const native = new BetterSqlite3(dbPath);
23348
- native.pragma("journal_mode = WAL");
23349
- native.pragma("foreign_keys = ON");
23350
- native.pragma("busy_timeout = 5000");
23351
- native.pragma("synchronous = NORMAL");
23352
- return new BetterSqliteAdapter(native);
23353
- }
23354
- var BUSY_RETRY_ATTEMPTS, BUSY_RETRY_BACKOFF_MS, BetterSqliteAdapter;
23465
+ const native = newDatabase(dbPath);
23466
+ native.exec("PRAGMA journal_mode = WAL");
23467
+ native.exec("PRAGMA foreign_keys = ON");
23468
+ native.exec("PRAGMA busy_timeout = 5000");
23469
+ native.exec("PRAGMA synchronous = NORMAL");
23470
+ return new NodeSqliteAdapter(native);
23471
+ }
23472
+ var SQLITE_BUSY, SQLITE_BUSY_SNAPSHOT, BUSY_RETRY_ATTEMPTS, BUSY_RETRY_BACKOFF_MS, savepointName, nodeRequire, _preconditionsApplied, _DatabaseSyncCtor, SLEEP_BUF, NodeSqliteAdapter;
23355
23473
  var init_engine = __esm({
23356
23474
  "src/lib/db/engine.ts"() {
23357
23475
  "use strict";
23476
+ init_runtime_checks();
23477
+ SQLITE_BUSY = 5;
23478
+ SQLITE_BUSY_SNAPSHOT = 261;
23358
23479
  BUSY_RETRY_ATTEMPTS = 5;
23359
23480
  BUSY_RETRY_BACKOFF_MS = 50;
23360
- BetterSqliteAdapter = class {
23481
+ savepointName = (depth) => `ocr_sp_${depth}`;
23482
+ nodeRequire = createRequire2(import.meta.url);
23483
+ _preconditionsApplied = false;
23484
+ SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
23485
+ NodeSqliteAdapter = class {
23361
23486
  raw;
23487
+ /**
23488
+ * Transaction nesting depth. `node:sqlite` has no transaction helper, so we
23489
+ * drive `BEGIN IMMEDIATE` ourselves and use SAVEPOINTs for nested calls
23490
+ * (better-sqlite3 did this automatically). 0 = no transaction open.
23491
+ */
23492
+ txnDepth = 0;
23362
23493
  constructor(db) {
23363
23494
  this.raw = db;
23364
23495
  }
23365
23496
  exec(sql, params) {
23366
23497
  const stmt = this.raw.prepare(sql);
23367
- if (!stmt.reader) {
23498
+ const cols = stmt.columns();
23499
+ if (cols.length === 0) {
23368
23500
  stmt.run(...params ?? []);
23369
23501
  return [];
23370
23502
  }
23371
- const columns = stmt.columns().map((c) => c.name);
23372
- const values = stmt.raw().all(...params ?? []);
23373
- return values.length > 0 ? [{ columns, values }] : [];
23503
+ stmt.setReturnArrays(true);
23504
+ const values = stmt.all(...params ?? []);
23505
+ return values.length > 0 ? [{ columns: cols.map((c) => c.name), values }] : [];
23374
23506
  }
23375
23507
  run(sql, params) {
23376
23508
  if (params !== void 0) {
@@ -23383,25 +23515,84 @@ var init_engine = __esm({
23383
23515
  return this.raw.prepare(sql);
23384
23516
  }
23385
23517
  transaction(fn) {
23386
- const tx = this.raw.transaction(fn);
23387
- for (let attempt = 0; ; attempt++) {
23518
+ return this.txnDepth > 0 ? this.runNested(fn) : this.runOuter(fn);
23519
+ }
23520
+ /**
23521
+ * Nested call: a SAVEPOINT within the outer transaction's write lock. No
23522
+ * busy-retry — the outer transaction already holds the lock. The savepoint
23523
+ * lets the inner block roll back independently while the outer continues.
23524
+ */
23525
+ runNested(fn) {
23526
+ const name = savepointName(this.txnDepth);
23527
+ this.raw.exec(`SAVEPOINT ${name}`);
23528
+ this.txnDepth++;
23529
+ try {
23530
+ const result = fn();
23531
+ this.raw.exec(`RELEASE ${name}`);
23532
+ return result;
23533
+ } catch (e) {
23388
23534
  try {
23389
- return tx.immediate();
23535
+ this.raw.exec(`ROLLBACK TO ${name}`);
23536
+ this.raw.exec(`RELEASE ${name}`);
23537
+ } catch {
23538
+ }
23539
+ throw e;
23540
+ } finally {
23541
+ this.txnDepth--;
23542
+ }
23543
+ }
23544
+ /**
23545
+ * Outer transaction: `BEGIN IMMEDIATE` acquires the write lock up front so
23546
+ * cross-process writers serialize cleanly under WAL instead of failing late
23547
+ * on upgrade. `busy_timeout` covers most contention; a bounded synchronous
23548
+ * retry absorbs the residual SQLITE_BUSY (another connection holds the lock
23549
+ * past the timeout, or BUSY_SNAPSHOT). Non-busy errors and the final attempt
23550
+ * re-throw so genuine failures propagate.
23551
+ */
23552
+ runOuter(fn) {
23553
+ for (let attempt = 0; attempt < BUSY_RETRY_ATTEMPTS; attempt++) {
23554
+ try {
23555
+ return this.runOnce(fn);
23390
23556
  } catch (e) {
23391
- if (!isBusyError(e) || attempt >= BUSY_RETRY_ATTEMPTS - 1) throw e;
23557
+ if (!isBusyError(e) || attempt === BUSY_RETRY_ATTEMPTS - 1) throw e;
23392
23558
  sleepSync(BUSY_RETRY_BACKOFF_MS);
23393
23559
  }
23394
23560
  }
23561
+ throw new Error("transaction retry budget exhausted");
23562
+ }
23563
+ /** One `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK` lifecycle. */
23564
+ runOnce(fn) {
23565
+ this.raw.exec("BEGIN IMMEDIATE");
23566
+ this.txnDepth = 1;
23567
+ try {
23568
+ const result = fn();
23569
+ this.raw.exec("COMMIT");
23570
+ return result;
23571
+ } catch (e) {
23572
+ try {
23573
+ this.raw.exec("ROLLBACK");
23574
+ } catch {
23575
+ }
23576
+ throw e;
23577
+ } finally {
23578
+ this.txnDepth = 0;
23579
+ }
23395
23580
  }
23396
23581
  pragma(source) {
23397
- return this.raw.pragma(source);
23582
+ this.raw.exec(`PRAGMA ${source}`);
23583
+ return void 0;
23398
23584
  }
23399
23585
  close() {
23400
23586
  try {
23401
- this.raw.pragma("wal_checkpoint(TRUNCATE)");
23587
+ this.raw.exec("PRAGMA wal_checkpoint(TRUNCATE)");
23402
23588
  } catch {
23403
23589
  }
23404
- this.raw.close();
23590
+ try {
23591
+ this.raw.close();
23592
+ } catch (e) {
23593
+ const message = e?.message ?? "";
23594
+ if (!/database is not open/i.test(message)) throw e;
23595
+ }
23405
23596
  }
23406
23597
  };
23407
23598
  }
@@ -23927,6 +24118,35 @@ var init_migrations = __esm({
23927
24118
  db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
23928
24119
  db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
23929
24120
  }
24121
+ },
24122
+ {
24123
+ version: 14,
24124
+ description: "Self-heal markdown_artifacts duplication: collapse NULL-round duplicate rows and add a NULL-safe unique index so the dedup bug cannot recur",
24125
+ // The table's `UNIQUE(session_id, artifact_type, round_number, file_path)`
24126
+ // never deduped session-level artifacts because SQLite treats NULL ≠ NULL,
24127
+ // and the writer used `INSERT OR REPLACE` — so every re-parse of a
24128
+ // NULL-round artifact (context.md, map.md, …) appended a duplicate (one
24129
+ // context.md reached 775 identical rows, ~177 MB). The writer is now an
24130
+ // explicit UPDATE-or-INSERT; this migration heals existing DBs and adds a
24131
+ // NULL-collapsing unique index as a DB-level backstop.
24132
+ //
24133
+ // Orphan-row sweep (FK-dangling children from the pre-FK-enforcement era)
24134
+ // is intentionally NOT done here — it needs `PRAGMA foreign_keys = OFF`,
24135
+ // which is a no-op inside the migration transaction. `ocr db doctor --fix`
24136
+ // performs it outside a transaction.
24137
+ run: (db) => {
24138
+ db.run(`
24139
+ DELETE FROM markdown_artifacts
24140
+ WHERE rowid NOT IN (
24141
+ SELECT MAX(rowid) FROM markdown_artifacts
24142
+ GROUP BY session_id, artifact_type, IFNULL(round_number, -1), file_path
24143
+ )
24144
+ `);
24145
+ db.run(`
24146
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_markdown_artifacts_logical
24147
+ ON markdown_artifacts(session_id, artifact_type, IFNULL(round_number, -1), file_path)
24148
+ `);
24149
+ }
23930
24150
  }
23931
24151
  ];
23932
24152
  }
@@ -24055,7 +24275,7 @@ var init_queries = __esm({
24055
24275
 
24056
24276
  // src/lib/db/reconcile.ts
24057
24277
  import { existsSync as existsSync10 } from "node:fs";
24058
- import { isAbsolute as isAbsolute2, join as join12, dirname as dirname4 } from "node:path";
24278
+ import { isAbsolute as isAbsolute2, join as join12, dirname as dirname5 } from "node:path";
24059
24279
  function hasTerminalArtifactEvent(db, sessionId, workflowType, currentRound, currentMapRun) {
24060
24280
  const eventType = workflowType === "map" ? "map_completed" : "round_completed";
24061
24281
  const round = workflowType === "map" ? currentMapRun : currentRound;
@@ -24096,7 +24316,7 @@ function hasInFlightDependents(db, sessionId) {
24096
24316
  function resolveSessionDir(ocrDir, sessionDir) {
24097
24317
  if (!sessionDir) return null;
24098
24318
  if (isAbsolute2(sessionDir)) return sessionDir;
24099
- return join12(dirname4(ocrDir), sessionDir);
24319
+ return join12(dirname5(ocrDir), sessionDir);
24100
24320
  }
24101
24321
  function reconcileLegacyState(db, ocrDir, opts = {}) {
24102
24322
  const dryRun = opts.dryRun ?? false;
@@ -24214,7 +24434,7 @@ var init_liveness = __esm({
24214
24434
  });
24215
24435
 
24216
24436
  // src/lib/state/exit-codes.ts
24217
- var STATE_EXIT, StateError, CANCELLED_EXIT_CODE, ORPHAN_EXIT_CODE, CASCADE_CLOSE_EXIT_CODE;
24437
+ var STATE_EXIT, StateError, CANCELLED_EXIT_CODE, ORPHAN_EXIT_CODE, CASCADE_CLOSE_EXIT_CODE, WATCHDOG_DEADLINE_EXIT_CODE;
24218
24438
  var init_exit_codes = __esm({
24219
24439
  "src/lib/state/exit-codes.ts"() {
24220
24440
  "use strict";
@@ -24239,6 +24459,7 @@ var init_exit_codes = __esm({
24239
24459
  CANCELLED_EXIT_CODE = -2;
24240
24460
  ORPHAN_EXIT_CODE = -3;
24241
24461
  CASCADE_CLOSE_EXIT_CODE = -4;
24462
+ WATCHDOG_DEADLINE_EXIT_CODE = -5;
24242
24463
  }
24243
24464
  });
24244
24465
 
@@ -24621,24 +24842,431 @@ var init_agent_sessions = __esm({
24621
24842
  }
24622
24843
  });
24623
24844
 
24845
+ // src/lib/db/maintenance.ts
24846
+ import {
24847
+ existsSync as existsSync11,
24848
+ readdirSync as readdirSync5,
24849
+ statSync,
24850
+ unlinkSync as unlinkSync3,
24851
+ copyFileSync
24852
+ } from "node:fs";
24853
+ import { dirname as dirname6, join as join13, basename as basename7 } from "node:path";
24854
+ function withForeignKeysDisabled(db, fn) {
24855
+ db.pragma("foreign_keys = OFF");
24856
+ try {
24857
+ return fn();
24858
+ } finally {
24859
+ db.pragma("foreign_keys = ON");
24860
+ }
24861
+ }
24862
+ function scalarInt(db, sql) {
24863
+ const r = db.exec(sql);
24864
+ const v = r[0]?.values[0]?.[0];
24865
+ return typeof v === "number" ? v : Number(v ?? 0);
24866
+ }
24867
+ function foreignKeyViolationGroups(db) {
24868
+ const r = db.exec("PRAGMA foreign_key_check");
24869
+ const rows = r[0]?.values ?? [];
24870
+ const counts = /* @__PURE__ */ new Map();
24871
+ for (const row of rows) {
24872
+ const table = String(row[0]);
24873
+ counts.set(table, (counts.get(table) ?? 0) + 1);
24874
+ }
24875
+ return [...counts.entries()].map(([table, count]) => ({ table, count })).sort((a, b) => b.count - a.count);
24876
+ }
24877
+ function scanOrphanTempFiles(dataDir) {
24878
+ let entries;
24879
+ try {
24880
+ entries = readdirSync5(dataDir);
24881
+ } catch {
24882
+ return [];
24883
+ }
24884
+ const out = [];
24885
+ for (const name of entries) {
24886
+ const m = name.match(/^ocr\.db\.(\d+)\.tmp$/);
24887
+ if (!m) continue;
24888
+ const pid = Number(m[1]);
24889
+ let ageMs = 0;
24890
+ try {
24891
+ ageMs = Date.now() - statSync(join13(dataDir, name)).mtimeMs;
24892
+ } catch {
24893
+ continue;
24894
+ }
24895
+ const alive = isProcessAlive(pid);
24896
+ out.push({
24897
+ name,
24898
+ pid,
24899
+ ageMs,
24900
+ // Reapable only when the writer PID is dead AND the file is old enough
24901
+ // that no live mid-write could plausibly own it.
24902
+ reapable: !alive && ageMs > ONE_HOUR_MS
24903
+ });
24904
+ }
24905
+ return out;
24906
+ }
24907
+ function scanBackupFiles(dataDir, dbBase) {
24908
+ let entries;
24909
+ try {
24910
+ entries = readdirSync5(dataDir);
24911
+ } catch {
24912
+ return [];
24913
+ }
24914
+ const out = [];
24915
+ for (const name of entries) {
24916
+ if (!name.startsWith(`${dbBase}.bak`)) continue;
24917
+ try {
24918
+ out.push({ name, sizeBytes: statSync(join13(dataDir, name)).size });
24919
+ } catch {
24920
+ }
24921
+ }
24922
+ return out.sort((a, b) => b.sizeBytes - a.sizeBytes);
24923
+ }
24924
+ function collectDbHealth(db, dbPath) {
24925
+ const dataDir = dirname6(dbPath);
24926
+ const dbBase = basename7(dbPath);
24927
+ const pageSize = scalarInt(db, "PRAGMA page_size");
24928
+ const pageCount = scalarInt(db, "PRAGMA page_count");
24929
+ const freelistCount = scalarInt(db, "PRAGMA freelist_count");
24930
+ const integ = db.exec("PRAGMA integrity_check");
24931
+ const integRows = (integ[0]?.values ?? []).map((v) => String(v[0]));
24932
+ const integrityOk = integRows.length === 1 && integRows[0] === "ok";
24933
+ const allGroups = foreignKeyViolationGroups(db);
24934
+ const fkViolations = allGroups.filter((g) => !PROTECTED_TABLES.has(g.table));
24935
+ const protectedFkViolations = allGroups.filter(
24936
+ (g) => PROTECTED_TABLES.has(g.table)
24937
+ );
24938
+ const fileSizeBytes = existsSync11(dbPath) ? statSync(dbPath).size : 0;
24939
+ return {
24940
+ dbPath,
24941
+ fileSizeBytes,
24942
+ pageSize,
24943
+ pageCount,
24944
+ freelistCount,
24945
+ reclaimableBytes: freelistCount * pageSize,
24946
+ integrityOk,
24947
+ integrityErrors: integrityOk ? [] : integRows,
24948
+ fkViolations,
24949
+ protectedFkViolations,
24950
+ totalFkViolations: allGroups.reduce((n, g) => n + g.count, 0),
24951
+ markdownDuplicateRows: scalarInt(
24952
+ db,
24953
+ `SELECT COALESCE(SUM(cnt - 1), 0) FROM (
24954
+ SELECT COUNT(*) AS cnt FROM markdown_artifacts
24955
+ GROUP BY session_id, artifact_type, IFNULL(round_number, -1), file_path
24956
+ HAVING cnt > 1)`
24957
+ ),
24958
+ orphanTempFiles: scanOrphanTempFiles(dataDir),
24959
+ backupFiles: scanBackupFiles(dataDir, dbBase),
24960
+ eventCount: scalarInt(db, "SELECT COUNT(*) FROM orchestration_events"),
24961
+ sessionCount: scalarInt(db, "SELECT COUNT(*) FROM sessions")
24962
+ };
24963
+ }
24964
+ function snapshotDb(db, dbPath, label = "doctor") {
24965
+ try {
24966
+ if (!existsSync11(dbPath) || statSync(dbPath).size === 0) return null;
24967
+ db.pragma("wal_checkpoint(TRUNCATE)");
24968
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
24969
+ const bakPath = `${dbPath}.bak.${label}.${ts}`;
24970
+ copyFileSync(dbPath, bakPath);
24971
+ return bakPath;
24972
+ } catch {
24973
+ return null;
24974
+ }
24975
+ }
24976
+ function reapOrphanDbFiles(dataDir) {
24977
+ const reaped = [];
24978
+ for (const f of scanOrphanTempFiles(dataDir)) {
24979
+ if (!f.reapable) continue;
24980
+ try {
24981
+ unlinkSync3(join13(dataDir, f.name));
24982
+ reaped.push(f.name);
24983
+ } catch {
24984
+ }
24985
+ }
24986
+ return reaped;
24987
+ }
24988
+ function reapStaleExecLogs(execLogsDir, maxAgeMs = SEVEN_DAYS_MS) {
24989
+ let entries;
24990
+ try {
24991
+ entries = readdirSync5(execLogsDir);
24992
+ } catch {
24993
+ return [];
24994
+ }
24995
+ const cutoff = Date.now() - maxAgeMs;
24996
+ const reaped = [];
24997
+ for (const name of entries) {
24998
+ if (!name.endsWith(".log")) continue;
24999
+ const full = join13(execLogsDir, name);
25000
+ try {
25001
+ if (statSync(full).mtimeMs > cutoff) continue;
25002
+ unlinkSync3(full);
25003
+ reaped.push(name);
25004
+ } catch {
25005
+ }
25006
+ }
25007
+ return reaped;
25008
+ }
25009
+ function pruneBackups(dataDir, dbPath, opts = {}) {
25010
+ const keep = opts.keep ?? 1;
25011
+ if (!Number.isInteger(keep) || keep < 0) {
25012
+ throw new Error(
25013
+ `pruneBackups: keep must be a non-negative integer (got ${String(keep)})`
25014
+ );
25015
+ }
25016
+ const dryRun = opts.dryRun ?? false;
25017
+ const dbBase = basename7(dbPath);
25018
+ const withMtime = [];
25019
+ for (const file of scanBackupFiles(dataDir, dbBase)) {
25020
+ try {
25021
+ withMtime.push({ file, mtimeMs: statSync(join13(dataDir, file.name)).mtimeMs });
25022
+ } catch {
25023
+ }
25024
+ }
25025
+ withMtime.sort((a, b) => b.mtimeMs - a.mtimeMs);
25026
+ const kept = withMtime.slice(0, keep).map((x) => x.file);
25027
+ const toDelete = withMtime.slice(keep).map((x) => x.file);
25028
+ const deleted = [];
25029
+ if (!dryRun) {
25030
+ for (const b of toDelete) {
25031
+ try {
25032
+ unlinkSync3(join13(dataDir, b.name));
25033
+ deleted.push(b);
25034
+ } catch {
25035
+ }
25036
+ }
25037
+ }
25038
+ const reported = dryRun ? toDelete : deleted;
25039
+ return {
25040
+ dryRun,
25041
+ deleted: reported,
25042
+ kept,
25043
+ reclaimedBytes: reported.reduce((n, b) => n + b.sizeBytes, 0)
25044
+ };
25045
+ }
25046
+ function fixDb(db, dbPath, opts = {}) {
25047
+ const dataDir = dirname6(dbPath);
25048
+ const sizeBeforeBytes = existsSync11(dbPath) ? statSync(dbPath).size : 0;
25049
+ const snapshotPath = opts.snapshot === false ? null : snapshotDb(db, dbPath, "doctor");
25050
+ const fkOrphansDeleted = [];
25051
+ withForeignKeysDisabled(db, () => {
25052
+ db.transaction(() => {
25053
+ for (const sweep of ORPHAN_SWEEPS) {
25054
+ const info = db.prepare(sweep.sql).run();
25055
+ const count = Number(info.changes);
25056
+ if (count > 0) fkOrphansDeleted.push({ table: sweep.table, count });
25057
+ }
25058
+ });
25059
+ });
25060
+ let markdownDupsDeleted = 0;
25061
+ db.transaction(() => {
25062
+ const info = db.prepare(MARKDOWN_DEDUP_SQL).run();
25063
+ markdownDupsDeleted = Number(info.changes);
25064
+ });
25065
+ const tempsReaped = opts.reapTemps === false ? [] : reapOrphanDbFiles(dataDir);
25066
+ let vacuumed = false;
25067
+ if (opts.vacuum !== false) {
25068
+ try {
25069
+ db.pragma("wal_checkpoint(TRUNCATE)");
25070
+ db.run("VACUUM");
25071
+ vacuumed = true;
25072
+ } catch {
25073
+ vacuumed = false;
25074
+ }
25075
+ }
25076
+ const post = collectDbHealth(db, dbPath);
25077
+ return {
25078
+ snapshotPath,
25079
+ fkOrphansDeleted,
25080
+ totalFkOrphansDeleted: fkOrphansDeleted.reduce((n, g) => n + g.count, 0),
25081
+ protectedViolationsRemaining: post.protectedFkViolations,
25082
+ markdownDupsDeleted,
25083
+ tempsReaped,
25084
+ vacuumed,
25085
+ sizeBeforeBytes,
25086
+ sizeAfterBytes: post.fileSizeBytes,
25087
+ integrityOkAfter: post.integrityOk,
25088
+ fkViolationsAfter: post.totalFkViolations
25089
+ };
25090
+ }
25091
+ function vacuumDb(db, dbPath, opts = {}) {
25092
+ const sizeBeforeBytes = existsSync11(dbPath) ? statSync(dbPath).size : 0;
25093
+ const snapshotPath = opts.snapshot === false ? null : snapshotDb(db, dbPath, "vacuum");
25094
+ db.pragma("wal_checkpoint(TRUNCATE)");
25095
+ db.run("VACUUM");
25096
+ db.pragma("wal_checkpoint(TRUNCATE)");
25097
+ const sizeAfterBytes = existsSync11(dbPath) ? statSync(dbPath).size : 0;
25098
+ return {
25099
+ snapshotPath,
25100
+ sizeBeforeBytes,
25101
+ sizeAfterBytes,
25102
+ reclaimedBytes: Math.max(0, sizeBeforeBytes - sizeAfterBytes)
25103
+ };
25104
+ }
25105
+ function countSessionArtifacts(db, sessionId) {
25106
+ const r = db.exec(
25107
+ `SELECT
25108
+ (SELECT COUNT(*) FROM markdown_artifacts WHERE session_id = ?) +
25109
+ (SELECT COUNT(*) FROM review_rounds WHERE session_id = ?) +
25110
+ (SELECT COUNT(*) FROM reviewer_outputs ro JOIN review_rounds rr ON ro.round_id = rr.id WHERE rr.session_id = ?) +
25111
+ (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 = ?) +
25112
+ (SELECT COUNT(*) FROM map_runs WHERE session_id = ?) +
25113
+ (SELECT COUNT(*) FROM chat_conversations WHERE session_id = ?)`,
25114
+ Array(6).fill(sessionId)
25115
+ );
25116
+ const v = r[0]?.values[0]?.[0];
25117
+ return typeof v === "number" ? v : Number(v ?? 0);
25118
+ }
25119
+ function pruneDb(db, dbPath, opts = {}) {
25120
+ const dryRun = opts.dryRun ?? false;
25121
+ const hasBound = opts.olderThanDays !== void 0 || opts.keepSessions !== void 0;
25122
+ if (!hasBound) {
25123
+ return { dryRun, snapshotPath: null, prunedSessions: [], totalArtifactRows: 0 };
25124
+ }
25125
+ const rows = db.exec(
25126
+ `SELECT s.id,
25127
+ (SELECT (julianday('now') - julianday(MAX(e.created_at))) * 86400
25128
+ FROM orchestration_events e WHERE e.session_id = s.id) AS quiet_seconds
25129
+ FROM sessions s
25130
+ WHERE s.status = 'closed'
25131
+ ORDER BY quiet_seconds ASC`
25132
+ );
25133
+ const closed = (rows[0]?.values ?? []).map((v) => ({
25134
+ id: String(v[0]),
25135
+ quietSeconds: typeof v[1] === "number" ? v[1] : Number(v[1] ?? 0)
25136
+ }));
25137
+ const keepN = opts.keepSessions ?? 0;
25138
+ const olderThanSeconds = opts.olderThanDays !== void 0 ? opts.olderThanDays * 86400 : null;
25139
+ const targets = closed.filter((s, idx) => {
25140
+ if (idx < keepN) return false;
25141
+ if (olderThanSeconds !== null && s.quietSeconds < olderThanSeconds)
25142
+ return false;
25143
+ return true;
25144
+ });
25145
+ const prunedSessions = [];
25146
+ for (const t of targets) {
25147
+ const artifactRows = countSessionArtifacts(db, t.id);
25148
+ if (artifactRows === 0) continue;
25149
+ prunedSessions.push({ sessionId: t.id, artifactRows });
25150
+ }
25151
+ if (dryRun || prunedSessions.length === 0) {
25152
+ return {
25153
+ dryRun,
25154
+ snapshotPath: null,
25155
+ prunedSessions,
25156
+ totalArtifactRows: prunedSessions.reduce((n, p) => n + p.artifactRows, 0)
25157
+ };
25158
+ }
25159
+ const snapshotPath = snapshotDb(db, dbPath, "prune");
25160
+ db.transaction(() => {
25161
+ for (const p of prunedSessions) {
25162
+ db.run("DELETE FROM review_rounds WHERE session_id = ?", [p.sessionId]);
25163
+ db.run("DELETE FROM map_runs WHERE session_id = ?", [p.sessionId]);
25164
+ db.run("DELETE FROM markdown_artifacts WHERE session_id = ?", [p.sessionId]);
25165
+ db.run("DELETE FROM chat_conversations WHERE session_id = ?", [p.sessionId]);
25166
+ }
25167
+ });
25168
+ return {
25169
+ dryRun,
25170
+ snapshotPath,
25171
+ prunedSessions,
25172
+ totalArtifactRows: prunedSessions.reduce((n, p) => n + p.artifactRows, 0)
25173
+ };
25174
+ }
25175
+ var PROTECTED_TABLES, ORPHAN_SWEEPS, MARKDOWN_DEDUP_SQL, ONE_HOUR_MS, SEVEN_DAYS_MS;
25176
+ var init_maintenance = __esm({
25177
+ "src/lib/db/maintenance.ts"() {
25178
+ "use strict";
25179
+ init_src();
25180
+ PROTECTED_TABLES = /* @__PURE__ */ new Set([
25181
+ "sessions",
25182
+ "orchestration_events",
25183
+ "agent_sessions",
25184
+ "command_executions",
25185
+ "schema_version"
25186
+ ]);
25187
+ ORPHAN_SWEEPS = [
25188
+ // session-rooted parents first
25189
+ {
25190
+ table: "review_rounds",
25191
+ sql: "DELETE FROM review_rounds WHERE session_id NOT IN (SELECT id FROM sessions)"
25192
+ },
25193
+ {
25194
+ table: "map_runs",
25195
+ sql: "DELETE FROM map_runs WHERE session_id NOT IN (SELECT id FROM sessions)"
25196
+ },
25197
+ {
25198
+ table: "markdown_artifacts",
25199
+ sql: "DELETE FROM markdown_artifacts WHERE session_id NOT IN (SELECT id FROM sessions)"
25200
+ },
25201
+ {
25202
+ table: "chat_conversations",
25203
+ sql: "DELETE FROM chat_conversations WHERE session_id NOT IN (SELECT id FROM sessions)"
25204
+ },
25205
+ // second level (pick up parents deleted above)
25206
+ {
25207
+ table: "reviewer_outputs",
25208
+ sql: "DELETE FROM reviewer_outputs WHERE round_id NOT IN (SELECT id FROM review_rounds)"
25209
+ },
25210
+ {
25211
+ table: "map_sections",
25212
+ sql: "DELETE FROM map_sections WHERE map_run_id NOT IN (SELECT id FROM map_runs)"
25213
+ },
25214
+ {
25215
+ table: "chat_messages",
25216
+ sql: "DELETE FROM chat_messages WHERE conversation_id NOT IN (SELECT id FROM chat_conversations)"
25217
+ },
25218
+ {
25219
+ table: "user_round_progress",
25220
+ sql: "DELETE FROM user_round_progress WHERE round_id NOT IN (SELECT id FROM review_rounds)"
25221
+ },
25222
+ // third level
25223
+ {
25224
+ table: "review_findings",
25225
+ sql: "DELETE FROM review_findings WHERE reviewer_output_id NOT IN (SELECT id FROM reviewer_outputs)"
25226
+ },
25227
+ {
25228
+ table: "map_files",
25229
+ sql: "DELETE FROM map_files WHERE section_id NOT IN (SELECT id FROM map_sections)"
25230
+ },
25231
+ // leaves
25232
+ {
25233
+ table: "user_finding_progress",
25234
+ sql: "DELETE FROM user_finding_progress WHERE finding_id NOT IN (SELECT id FROM review_findings)"
25235
+ },
25236
+ {
25237
+ table: "user_file_progress",
25238
+ sql: "DELETE FROM user_file_progress WHERE map_file_id NOT IN (SELECT id FROM map_files)"
25239
+ }
25240
+ ];
25241
+ MARKDOWN_DEDUP_SQL = `
25242
+ DELETE FROM markdown_artifacts
25243
+ WHERE rowid NOT IN (
25244
+ SELECT MAX(rowid) FROM markdown_artifacts
25245
+ GROUP BY session_id, artifact_type, IFNULL(round_number, -1), file_path
25246
+ )`;
25247
+ ONE_HOUR_MS = 60 * 60 * 1e3;
25248
+ SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
25249
+ }
25250
+ });
25251
+
24624
25252
  // src/lib/db/command-log.ts
24625
- import { appendFileSync, existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync6 } from "node:fs";
24626
- import { dirname as dirname5, join as join13 } from "node:path";
25253
+ import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync6 } from "node:fs";
25254
+ import { dirname as dirname7, join as join14 } from "node:path";
24627
25255
  import { randomUUID as randomUUID2 } from "node:crypto";
24628
25256
  function generateCommandUid() {
24629
25257
  return randomUUID2();
24630
25258
  }
24631
25259
  function cacheDir(ocrDir) {
24632
- return join13(ocrDir, "data", CACHE_DIR);
25260
+ return join14(ocrDir, "data", CACHE_DIR);
24633
25261
  }
24634
25262
  function commandLogPath(ocrDir) {
24635
- return join13(cacheDir(ocrDir), FILENAME);
25263
+ return join14(cacheDir(ocrDir), FILENAME);
24636
25264
  }
24637
25265
  function appendCommandLog(ocrDir, entry) {
24638
25266
  try {
24639
25267
  const filePath = commandLogPath(ocrDir);
24640
- const dir = dirname5(filePath);
24641
- if (!existsSync11(dir)) mkdirSync3(dir, { recursive: true });
25268
+ const dir = dirname7(filePath);
25269
+ if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
24642
25270
  const line = JSON.stringify(entry) + "\n";
24643
25271
  appendFileSync(filePath, line, { encoding: "utf-8" });
24644
25272
  if (approxLineCount >= 0) approxLineCount++;
@@ -24648,7 +25276,7 @@ function appendCommandLog(ocrDir, entry) {
24648
25276
  }
24649
25277
  function readCommandLog(ocrDir) {
24650
25278
  const filePath = commandLogPath(ocrDir);
24651
- if (!existsSync11(filePath)) return [];
25279
+ if (!existsSync12(filePath)) return [];
24652
25280
  const content = readFileSync9(filePath, "utf-8");
24653
25281
  const entries = [];
24654
25282
  for (const line of content.split("\n")) {
@@ -24734,6 +25362,7 @@ __export(db_exports, {
24734
25362
  PID_REUSE_GUARD_MS: () => PID_REUSE_GUARD_MS,
24735
25363
  STATE_EXIT: () => STATE_EXIT,
24736
25364
  StateError: () => StateError,
25365
+ WATCHDOG_DEADLINE_EXIT_CODE: () => WATCHDOG_DEADLINE_EXIT_CODE,
24737
25366
  appendCommandLog: () => appendCommandLog,
24738
25367
  bindVendorSessionIdOpportunistically: () => bindVendorSessionIdOpportunistically,
24739
25368
  bumpAgentSessionHeartbeat: () => bumpAgentSessionHeartbeat,
@@ -24741,10 +25370,12 @@ __export(db_exports, {
24741
25370
  cascadeTerminateExecutions: () => cascadeTerminateExecutions,
24742
25371
  closeAllDatabases: () => closeAllDatabases,
24743
25372
  closeDatabase: () => closeDatabase,
25373
+ collectDbHealth: () => collectDbHealth,
24744
25374
  commandLogPath: () => commandLogPath,
24745
25375
  commitReasonClose: () => commitReasonClose,
24746
25376
  defaultIsAlive: () => defaultIsAlive,
24747
25377
  ensureDatabase: () => ensureDatabase,
25378
+ fixDb: () => fixDb,
24748
25379
  formatUpgradeNotice: () => formatUpgradeNotice,
24749
25380
  generateCommandUid: () => generateCommandUid,
24750
25381
  getAgentSession: () => getAgentSession,
@@ -24756,6 +25387,7 @@ __export(db_exports, {
24756
25387
  getLatestEventId: () => getLatestEventId,
24757
25388
  getSchemaVersion: () => getSchemaVersion,
24758
25389
  getSession: () => getSession,
25390
+ hasInFlightDependents: () => hasInFlightDependents,
24759
25391
  insertAgentSession: () => insertAgentSession,
24760
25392
  insertEvent: () => insertEvent,
24761
25393
  insertSession: () => insertSession,
@@ -24764,7 +25396,12 @@ __export(db_exports, {
24764
25396
  listAgentSessionsForWorkflow: () => listAgentSessionsForWorkflow,
24765
25397
  openDatabase: () => openDatabase,
24766
25398
  probeEngine: () => probeEngine,
25399
+ probeWrite: () => probeWrite,
25400
+ pruneBackups: () => pruneBackups,
25401
+ pruneDb: () => pruneDb,
24767
25402
  readCommandLog: () => readCommandLog,
25403
+ reapOrphanDbFiles: () => reapOrphanDbFiles,
25404
+ reapStaleExecLogs: () => reapStaleExecLogs,
24768
25405
  reconcileLegacyState: () => reconcileLegacyState,
24769
25406
  recordVendorSessionIdForExecution: () => recordVendorSessionIdForExecution,
24770
25407
  replayCommandLog: () => replayCommandLog,
@@ -24774,23 +25411,34 @@ __export(db_exports, {
24774
25411
  runMigrations: () => runMigrations,
24775
25412
  setAgentSessionStatus: () => setAgentSessionStatus,
24776
25413
  setAgentSessionVendorId: () => setAgentSessionVendorId,
25414
+ snapshotDb: () => snapshotDb,
24777
25415
  sqliteUtcMs: () => sqliteUtcMs,
24778
25416
  sweepStaleAgentSessions: () => sweepStaleAgentSessions,
24779
25417
  sweepStaleSessions: () => sweepStaleSessions,
24780
25418
  updateAgentSession: () => updateAgentSession,
24781
25419
  updateSession: () => updateSession,
24782
- walCheckpointTruncate: () => walCheckpointTruncate
25420
+ vacuumDb: () => vacuumDb,
25421
+ walCheckpointTruncate: () => walCheckpointTruncate,
25422
+ withForeignKeysDisabled: () => withForeignKeysDisabled
24783
25423
  });
24784
- import { existsSync as existsSync12, mkdirSync as mkdirSync4, copyFileSync, statSync } from "node:fs";
24785
- import { dirname as dirname6, join as join14 } from "node:path";
25424
+ import {
25425
+ existsSync as existsSync13,
25426
+ mkdirSync as mkdirSync5,
25427
+ copyFileSync as copyFileSync2,
25428
+ statSync as statSync2,
25429
+ mkdtempSync,
25430
+ rmSync
25431
+ } from "node:fs";
25432
+ import { tmpdir } from "node:os";
25433
+ import { dirname as dirname8, join as join15 } from "node:path";
24786
25434
  function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
24787
25435
  if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
24788
25436
  const bakPath = `${dbPath}.bak.v${fromVersion}`;
24789
- if (existsSync12(bakPath)) return bakPath;
25437
+ if (existsSync13(bakPath)) return bakPath;
24790
25438
  try {
24791
- if (!existsSync12(dbPath) || statSync(dbPath).size === 0) return null;
25439
+ if (!existsSync13(dbPath) || statSync2(dbPath).size === 0) return null;
24792
25440
  db.pragma("wal_checkpoint(TRUNCATE)");
24793
- copyFileSync(dbPath, bakPath);
25441
+ copyFileSync2(dbPath, bakPath);
24794
25442
  return bakPath;
24795
25443
  } catch {
24796
25444
  return null;
@@ -24823,24 +25471,24 @@ async function openDatabase(dbPath) {
24823
25471
  if (cached) {
24824
25472
  return cached;
24825
25473
  }
24826
- const dir = dirname6(dbPath);
24827
- if (!existsSync12(dir)) {
24828
- mkdirSync4(dir, { recursive: true });
25474
+ const dir = dirname8(dbPath);
25475
+ if (!existsSync13(dir)) {
25476
+ mkdirSync5(dir, { recursive: true });
24829
25477
  }
24830
25478
  const db = openEngine(dbPath);
24831
25479
  connections.set(dbPath, db);
24832
25480
  return db;
24833
25481
  }
24834
25482
  async function getDb(ocrDir) {
24835
- const dbPath = join14(ocrDir, "data", "ocr.db");
25483
+ const dbPath = join15(ocrDir, "data", "ocr.db");
24836
25484
  return openDatabase(dbPath);
24837
25485
  }
24838
25486
  async function ensureDatabase(ocrDir) {
24839
- const dataDir = join14(ocrDir, "data");
24840
- if (!existsSync12(dataDir)) {
24841
- mkdirSync4(dataDir, { recursive: true });
25487
+ const dataDir = join15(ocrDir, "data");
25488
+ if (!existsSync13(dataDir)) {
25489
+ mkdirSync5(dataDir, { recursive: true });
24842
25490
  }
24843
- const dbPath = join14(dataDir, "ocr.db");
25491
+ const dbPath = join15(dataDir, "ocr.db");
24844
25492
  const db = await openDatabase(dbPath);
24845
25493
  let before = 0;
24846
25494
  try {
@@ -24868,7 +25516,7 @@ async function ensureDatabase(ocrDir) {
24868
25516
  return db;
24869
25517
  }
24870
25518
  function walCheckpointTruncate(dbPath) {
24871
- if (!existsSync12(dbPath)) {
25519
+ if (!existsSync13(dbPath)) {
24872
25520
  return "skipped";
24873
25521
  }
24874
25522
  const cached = connections.get(dbPath);
@@ -24889,7 +25537,7 @@ function walCheckpointTruncate(dbPath) {
24889
25537
  return "failed";
24890
25538
  } finally {
24891
25539
  try {
24892
- transient?.raw.close();
25540
+ transient?.close();
24893
25541
  } catch {
24894
25542
  }
24895
25543
  }
@@ -24907,6 +25555,41 @@ function closeAllDatabases() {
24907
25555
  connections.delete(path2);
24908
25556
  }
24909
25557
  }
25558
+ function probeWrite() {
25559
+ let dir;
25560
+ try {
25561
+ dir = mkdtempSync(join15(tmpdir(), "ocr-probe-"));
25562
+ const db = openEngine(join15(dir, "probe.db"));
25563
+ try {
25564
+ db.run("CREATE TABLE _probe_write (id INTEGER PRIMARY KEY, v TEXT)");
25565
+ db.transaction(() => {
25566
+ db.run("INSERT INTO _probe_write (v) VALUES (?)", ["written-in-txn"]);
25567
+ });
25568
+ const value = db.exec("SELECT v FROM _probe_write")[0]?.values[0]?.[0];
25569
+ if (value !== "written-in-txn") {
25570
+ return { ok: false, error: `unexpected probe value: ${String(value)}` };
25571
+ }
25572
+ return { ok: true };
25573
+ } finally {
25574
+ db.close();
25575
+ }
25576
+ } catch (e) {
25577
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
25578
+ } finally {
25579
+ if (dir) rmDirBestEffort(dir);
25580
+ }
25581
+ }
25582
+ function rmDirBestEffort(dir) {
25583
+ for (let attempt = 0; attempt < 3; attempt++) {
25584
+ try {
25585
+ rmSync(dir, { recursive: true, force: true });
25586
+ return;
25587
+ } catch {
25588
+ if (attempt === 2) return;
25589
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
25590
+ }
25591
+ }
25592
+ }
24910
25593
  var V2_SCHEMA_VERSION, connections;
24911
25594
  var init_db = __esm({
24912
25595
  "src/lib/db/index.ts"() {
@@ -24922,6 +25605,7 @@ var init_db = __esm({
24922
25605
  init_result_mapper();
24923
25606
  init_engine();
24924
25607
  init_reconcile();
25608
+ init_maintenance();
24925
25609
  init_migrations();
24926
25610
  init_command_log();
24927
25611
  V2_SCHEMA_VERSION = 12;
@@ -24929,6 +25613,13 @@ var init_db = __esm({
24929
25613
  }
24930
25614
  });
24931
25615
 
25616
+ // src/lib/runtime-guard.ts
25617
+ init_runtime_checks();
25618
+ if (!isSupportedNode(process.versions.node)) {
25619
+ process.stderr.write(nodeVersionGuardMessage(process.versions.node));
25620
+ process.exit(1);
25621
+ }
25622
+
24932
25623
  // ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/esm.mjs
24933
25624
  var import_index = __toESM(require_commander(), 1);
24934
25625
  var {
@@ -28445,6 +29136,8 @@ function ora(options) {
28445
29136
  }
28446
29137
 
28447
29138
  // src/lib/config.ts
29139
+ init_src();
29140
+ init_src();
28448
29141
  var AI_TOOLS = [
28449
29142
  {
28450
29143
  id: "amazon-q",
@@ -28468,7 +29161,9 @@ var AI_TOOLS = [
28468
29161
  configDir: ".claude",
28469
29162
  commandsDir: ".claude/commands",
28470
29163
  skillsDir: ".claude/skills",
28471
- commandStrategy: "subdirectory"
29164
+ commandStrategy: "subdirectory",
29165
+ instructionFiles: [{ path: "CLAUDE.md", format: "markdown" }],
29166
+ vendorBinary: "claude"
28472
29167
  },
28473
29168
  {
28474
29169
  id: "cline",
@@ -28484,7 +29179,9 @@ var AI_TOOLS = [
28484
29179
  configDir: ".codex",
28485
29180
  commandsDir: ".codex/commands",
28486
29181
  skillsDir: ".codex/skills",
28487
- commandStrategy: "subdirectory"
29182
+ commandStrategy: "subdirectory",
29183
+ // Codex reads AGENTS.md natively — no extra instruction file.
29184
+ vendorBinary: "codex"
28488
29185
  },
28489
29186
  {
28490
29187
  id: "continue",
@@ -28508,7 +29205,9 @@ var AI_TOOLS = [
28508
29205
  configDir: ".gemini",
28509
29206
  commandsDir: ".gemini/commands",
28510
29207
  skillsDir: ".gemini/skills",
28511
- commandStrategy: "subdirectory"
29208
+ commandStrategy: "subdirectory",
29209
+ instructionFiles: [{ path: "GEMINI.md", format: "markdown" }],
29210
+ vendorBinary: "gemini"
28512
29211
  },
28513
29212
  {
28514
29213
  id: "github-copilot",
@@ -28516,7 +29215,10 @@ var AI_TOOLS = [
28516
29215
  configDir: ".github",
28517
29216
  commandsDir: ".github/commands",
28518
29217
  skillsDir: ".github/skills",
28519
- commandStrategy: "subdirectory"
29218
+ commandStrategy: "subdirectory",
29219
+ instructionFiles: [
29220
+ { path: ".github/copilot-instructions.md", format: "markdown" }
29221
+ ]
28520
29222
  },
28521
29223
  {
28522
29224
  id: "kilo-code",
@@ -28532,7 +29234,9 @@ var AI_TOOLS = [
28532
29234
  configDir: ".opencode",
28533
29235
  commandsDir: ".opencode/commands",
28534
29236
  skillsDir: ".opencode/skills",
28535
- commandStrategy: "subdirectory"
29237
+ commandStrategy: "subdirectory",
29238
+ // OpenCode reads AGENTS.md natively — no extra instruction file.
29239
+ vendorBinary: "opencode"
28536
29240
  },
28537
29241
  {
28538
29242
  id: "qoder",
@@ -28556,9 +29260,16 @@ var AI_TOOLS = [
28556
29260
  configDir: ".windsurf",
28557
29261
  commandsDir: ".windsurf/workflows",
28558
29262
  skillsDir: ".windsurf/skills",
28559
- commandStrategy: "flat-prefixed"
29263
+ commandStrategy: "flat-prefixed",
29264
+ instructionFiles: [{ path: ".windsurfrules", format: "plaintext" }]
28560
29265
  }
28561
29266
  ];
29267
+ function getToolById(id) {
29268
+ return AI_TOOLS.find((tool) => tool.id === id);
29269
+ }
29270
+ function getHostCapabilities(id) {
29271
+ return hostCapabilitiesFor(id);
29272
+ }
28562
29273
  function getToolIds() {
28563
29274
  return AI_TOOLS.map((tool) => tool.id);
28564
29275
  }
@@ -28619,11 +29330,11 @@ function ensureGitignore(ocrDir) {
28619
29330
  const gitignorePath = join(ocrDir, ".gitignore");
28620
29331
  const block = buildManagedBlock();
28621
29332
  let content = existsSync(gitignorePath) ? readFileSync2(gitignorePath, "utf-8") : "";
28622
- const blockRegex = new RegExp(
29333
+ const blockRegex2 = new RegExp(
28623
29334
  `${escapeRegex(START_MARKER)}[\\s\\S]*?${escapeRegex(END_MARKER)}\\n?`,
28624
29335
  "g"
28625
29336
  );
28626
- if (blockRegex.test(content)) {
29337
+ if (blockRegex2.test(content)) {
28627
29338
  content = content.replace(
28628
29339
  new RegExp(
28629
29340
  `${escapeRegex(START_MARKER)}[\\s\\S]*?${escapeRegex(END_MARKER)}\\n?`,
@@ -28813,6 +29524,7 @@ function resolveTeamComposition(team, override) {
28813
29524
  }
28814
29525
 
28815
29526
  // src/lib/installer.ts
29527
+ init_src();
28816
29528
  var require2 = createRequire(import.meta.url);
28817
29529
  function ensureDir(dir) {
28818
29530
  if (!existsSync3(dir)) {
@@ -28921,27 +29633,6 @@ function installCommandsForTool(tool, commandsSource, targetDir) {
28921
29633
  return false;
28922
29634
  }
28923
29635
  }
28924
- var BUILTIN_ICON_MAP = {
28925
- architect: "blocks",
28926
- fullstack: "layers",
28927
- reliability: "activity",
28928
- "staff-engineer": "compass",
28929
- principal: "crown",
28930
- frontend: "layout",
28931
- backend: "server",
28932
- infrastructure: "cloud",
28933
- performance: "gauge",
28934
- accessibility: "accessibility",
28935
- data: "database",
28936
- devops: "rocket",
28937
- dx: "terminal",
28938
- mobile: "smartphone",
28939
- security: "shield-alert",
28940
- quality: "sparkles",
28941
- testing: "test-tubes",
28942
- ai: "bot",
28943
- "docs-writer": "file-text"
28944
- };
28945
29636
  var HOLISTIC_IDS = /* @__PURE__ */ new Set(["architect", "fullstack", "reliability", "staff-engineer", "principal"]);
28946
29637
  var SPECIALIST_IDS = /* @__PURE__ */ new Set([
28947
29638
  "frontend",
@@ -29044,7 +29735,7 @@ function generateReviewersMeta(reviewersDir, configPath) {
29044
29735
  id,
29045
29736
  name: extractReviewerName(content),
29046
29737
  tier,
29047
- icon: BUILTIN_ICON_MAP[id] ?? (tier === "persona" ? "brain" : "user"),
29738
+ icon: defaultIconFor(id, tier),
29048
29739
  description: extractReviewerDescription(content),
29049
29740
  focus_areas: extractFocusAreas(content),
29050
29741
  is_default: defaultTeamIds.has(id),
@@ -29145,7 +29836,9 @@ function installForTool(tool, targetDir) {
29145
29836
  if (meta) {
29146
29837
  writeFileSync3(metaPath, JSON.stringify(meta, null, 2) + "\n");
29147
29838
  }
29148
- } catch {
29839
+ } catch (err) {
29840
+ const msg = err instanceof Error ? err.message : "unknown error";
29841
+ warnings.push(`Could not generate reviewers-meta.json: ${msg}`);
29149
29842
  }
29150
29843
  const commandsOk = installCommandsForTool(tool, commandsSource, targetDir);
29151
29844
  if (!commandsOk) {
@@ -29179,12 +29872,14 @@ function detectInstalledTools(targetDir, tools) {
29179
29872
  }
29180
29873
 
29181
29874
  // src/lib/injector.ts
29182
- import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
29183
- import { join as join4 } from "node:path";
29184
- var START_MARKER2 = "<!-- OCR:START -->";
29185
- var END_MARKER2 = "<!-- OCR:END -->";
29186
- var OCR_INSTRUCTION_BLOCK = `${START_MARKER2}
29187
- ## Open Code Review Instructions
29875
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
29876
+ import { dirname as dirname2, join as join4 } from "node:path";
29877
+ var AGENTS_MD = { path: "AGENTS.md", format: "markdown" };
29878
+ var MARKERS = {
29879
+ markdown: { start: "<!-- OCR:START -->", end: "<!-- OCR:END -->" },
29880
+ plaintext: { start: "# OCR:START", end: "# OCR:END" }
29881
+ };
29882
+ var OCR_INSTRUCTION_BODY = `## Open Code Review Instructions
29188
29883
 
29189
29884
  These instructions are for AI assistants handling code review in this project.
29190
29885
 
@@ -29200,37 +29895,95 @@ Use \`.ocr/skills/SKILL.md\` to learn:
29200
29895
  - Available reviewer personas and their focus areas
29201
29896
  - Session management and output format
29202
29897
 
29203
- Keep this managed block so \`ocr init\` can refresh the instructions.
29204
-
29205
- ${END_MARKER2}`;
29206
- function injectOcrInstructions(filePath) {
29898
+ Keep this managed block so \`ocr init\` can refresh the instructions.`;
29899
+ function buildBlock(format) {
29900
+ const { start, end } = MARKERS[format];
29901
+ return `${start}
29902
+ ${OCR_INSTRUCTION_BODY}
29903
+ ${end}`;
29904
+ }
29905
+ function escapeRegex2(str) {
29906
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29907
+ }
29908
+ function blockRegex(format) {
29909
+ const { start, end } = MARKERS[format];
29910
+ return new RegExp(
29911
+ `${escapeRegex2(start)}[\\s\\S]*?${escapeRegex2(end)}\\n?`,
29912
+ "g"
29913
+ );
29914
+ }
29915
+ function injectOcrInstructions(filePath, format = "markdown") {
29207
29916
  try {
29917
+ mkdirSync2(dirname2(filePath), { recursive: true });
29208
29918
  let content = existsSync4(filePath) ? readFileSync5(filePath, "utf-8") : "";
29209
- const regex2 = new RegExp(
29210
- `${escapeRegex2(START_MARKER2)}[\\s\\S]*?${escapeRegex2(END_MARKER2)}\\n?`,
29211
- "g"
29212
- );
29213
- content = content.replace(regex2, "");
29919
+ content = content.replace(blockRegex(format), "");
29214
29920
  content = content.trim();
29215
29921
  if (content.length > 0) {
29216
29922
  content += "\n\n";
29217
29923
  }
29218
- content += OCR_INSTRUCTION_BLOCK + "\n";
29924
+ content += buildBlock(format) + "\n";
29219
29925
  writeFileSync4(filePath, content);
29220
29926
  return true;
29221
29927
  } catch {
29222
29928
  return false;
29223
29929
  }
29224
29930
  }
29225
- function escapeRegex2(str) {
29226
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29931
+ function resolveTargets(selectedTools) {
29932
+ const targets = /* @__PURE__ */ new Map();
29933
+ targets.set(AGENTS_MD.path, AGENTS_MD);
29934
+ for (const tool of selectedTools) {
29935
+ for (const file of tool.instructionFiles ?? []) {
29936
+ targets.set(file.path, file);
29937
+ }
29938
+ }
29939
+ return [...targets.values()];
29227
29940
  }
29228
- function injectIntoProjectFiles(targetDir) {
29229
- const agentsMdPath = join4(targetDir, "AGENTS.md");
29230
- const claudeMdPath = join4(targetDir, "CLAUDE.md");
29231
- const agentsMd = injectOcrInstructions(agentsMdPath);
29232
- const claudeMd = injectOcrInstructions(claudeMdPath);
29233
- return { agentsMd, claudeMd };
29941
+ function plannedInstructionFiles(selectedTools) {
29942
+ return resolveTargets(selectedTools).map((t) => t.path);
29943
+ }
29944
+ function injectIntoProjectFiles(targetDir, selectedTools) {
29945
+ const written = [];
29946
+ const failed = [];
29947
+ for (const target of resolveTargets(selectedTools)) {
29948
+ const ok = injectOcrInstructions(join4(targetDir, target.path), target.format);
29949
+ (ok ? written : failed).push(target.path);
29950
+ }
29951
+ return { written, failed };
29952
+ }
29953
+ function hasOcrInstructions(filePath) {
29954
+ if (!existsSync4(filePath)) {
29955
+ return false;
29956
+ }
29957
+ const content = readFileSync5(filePath, "utf-8");
29958
+ return Object.values(MARKERS).some(
29959
+ (m) => content.includes(m.start) && content.includes(m.end)
29960
+ );
29961
+ }
29962
+ function findStaleInstructionFiles(targetDir, writtenPaths) {
29963
+ const written = new Set(writtenPaths);
29964
+ const candidates = /* @__PURE__ */ new Set();
29965
+ for (const tool of AI_TOOLS) {
29966
+ for (const file of tool.instructionFiles ?? []) {
29967
+ candidates.add(file.path);
29968
+ }
29969
+ }
29970
+ const stale = [];
29971
+ for (const path2 of candidates) {
29972
+ if (written.has(path2)) continue;
29973
+ if (hasOcrInstructions(join4(targetDir, path2))) {
29974
+ stale.push(path2);
29975
+ }
29976
+ }
29977
+ return stale;
29978
+ }
29979
+ function formatStaleWarnings(stale, mode) {
29980
+ if (mode === "dry-run") {
29981
+ return stale.map((path2) => `${path2} (stale OCR block \u2014 left untouched)`);
29982
+ }
29983
+ const owner = mode === "init" ? "installed" : "configured";
29984
+ return stale.map(
29985
+ (path2) => `${path2} still has an OCR block but no ${owner} tool uses it \u2014 remove it manually if unneeded.`
29986
+ );
29234
29987
  }
29235
29988
 
29236
29989
  // src/lib/banner.ts
@@ -29333,29 +30086,10 @@ ${hint}
29333
30086
  }
29334
30087
 
29335
30088
  // src/lib/version.ts
29336
- var CLI_VERSION = true ? "2.0.0" : createRequire(import.meta.url)("../../package.json").version;
29337
-
29338
- // ../shared/platform/src/index.ts
29339
- import { pathToFileURL } from "node:url";
29340
- import {
29341
- execFile,
29342
- execFileSync,
29343
- spawn as spawn2
29344
- } from "node:child_process";
29345
- import { promisify } from "node:util";
29346
- var execFilePromise = promisify(execFile);
29347
- var isWindows = process.platform === "win32";
29348
- async function importModule(absolutePath) {
29349
- return import(pathToFileURL(absolutePath).href);
29350
- }
29351
- function execBinary(binary, args, opts) {
29352
- return execFileSync(binary, args, {
29353
- ...opts,
29354
- shell: isWindows
29355
- });
29356
- }
30089
+ var CLI_VERSION = true ? "2.2.0" : createRequire(import.meta.url)("../../package.json").version;
29357
30090
 
29358
30091
  // src/lib/deps.ts
30092
+ init_src();
29359
30093
  var CATEGORY_ORDER = ["core", "ai-cli", "github"];
29360
30094
  var CATEGORY_INFO = {
29361
30095
  core: { label: "Core", hint: "" },
@@ -29527,7 +30261,7 @@ function printCapabilities(result) {
29527
30261
  }
29528
30262
 
29529
30263
  // src/commands/init.ts
29530
- 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) => {
30264
+ 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) => {
29531
30265
  printBanner();
29532
30266
  const depResult = checkDependencies();
29533
30267
  printDepChecks(depResult);
@@ -29618,17 +30352,19 @@ var initCommand = new Command("init").description("Set up OCR for AI coding envi
29618
30352
  const injectSpinner = ora(
29619
30353
  "Injecting OCR instructions into project files..."
29620
30354
  ).start();
29621
- const injectResults = injectIntoProjectFiles(targetDir);
30355
+ const installedTools = successful.map((r) => r.tool);
30356
+ const injectResults = injectIntoProjectFiles(targetDir, installedTools);
29622
30357
  injectSpinner.stop();
29623
- if (injectResults.agentsMd || injectResults.claudeMd) {
30358
+ if (injectResults.written.length > 0) {
29624
30359
  console.log(source_default.green("\u2713 OCR instructions injected"));
29625
- if (injectResults.agentsMd) {
29626
- console.log(` ${source_default.green("\u2713")} AGENTS.md`);
29627
- }
29628
- if (injectResults.claudeMd) {
29629
- console.log(` ${source_default.green("\u2713")} CLAUDE.md`);
30360
+ for (const path2 of injectResults.written) {
30361
+ console.log(` ${source_default.green("\u2713")} ${path2}`);
29630
30362
  }
29631
30363
  }
30364
+ const stale = findStaleInstructionFiles(targetDir, injectResults.written);
30365
+ for (const warning of formatStaleWarnings(stale, "init")) {
30366
+ console.log(source_default.yellow(` \u26A0 ${warning}`));
30367
+ }
29632
30368
  }
29633
30369
  console.log();
29634
30370
  console.log(source_default.bold("Next steps:"));
@@ -29816,10 +30552,10 @@ var ReaddirpStream = class extends Readable {
29816
30552
  }
29817
30553
  async _formatEntry(dirent, path2) {
29818
30554
  let entry;
29819
- const basename8 = this._isDirent ? dirent.name : dirent;
30555
+ const basename9 = this._isDirent ? dirent.name : dirent;
29820
30556
  try {
29821
- const fullPath = presolve(pjoin(path2, basename8));
29822
- entry = { path: prelative(this._root, fullPath), fullPath, basename: basename8 };
30557
+ const fullPath = presolve(pjoin(path2, basename9));
30558
+ entry = { path: prelative(this._root, fullPath), fullPath, basename: basename9 };
29823
30559
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
29824
30560
  } catch (err) {
29825
30561
  this._onError(err);
@@ -30358,9 +31094,9 @@ var NodeFsHandler = class {
30358
31094
  _watchWithNodeFs(path2, listener) {
30359
31095
  const opts = this.fsw.options;
30360
31096
  const directory = sysPath.dirname(path2);
30361
- const basename8 = sysPath.basename(path2);
31097
+ const basename9 = sysPath.basename(path2);
30362
31098
  const parent = this.fsw._getWatchedDir(directory);
30363
- parent.add(basename8);
31099
+ parent.add(basename9);
30364
31100
  const absolutePath = sysPath.resolve(path2);
30365
31101
  const options = {
30366
31102
  persistent: opts.persistent
@@ -30370,7 +31106,7 @@ var NodeFsHandler = class {
30370
31106
  let closer;
30371
31107
  if (opts.usePolling) {
30372
31108
  const enableBin = opts.interval !== opts.binaryInterval;
30373
- options.interval = enableBin && isBinaryPath(basename8) ? opts.binaryInterval : opts.interval;
31109
+ options.interval = enableBin && isBinaryPath(basename9) ? opts.binaryInterval : opts.interval;
30374
31110
  closer = setFsWatchFileListener(path2, absolutePath, options, {
30375
31111
  listener,
30376
31112
  rawEmitter: this.fsw._emitRaw
@@ -30392,11 +31128,11 @@ var NodeFsHandler = class {
30392
31128
  if (this.fsw.closed) {
30393
31129
  return;
30394
31130
  }
30395
- const dirname8 = sysPath.dirname(file);
30396
- const basename8 = sysPath.basename(file);
30397
- const parent = this.fsw._getWatchedDir(dirname8);
31131
+ const dirname10 = sysPath.dirname(file);
31132
+ const basename9 = sysPath.basename(file);
31133
+ const parent = this.fsw._getWatchedDir(dirname10);
30398
31134
  let prevStats = stats;
30399
- if (parent.has(basename8))
31135
+ if (parent.has(basename9))
30400
31136
  return;
30401
31137
  const listener = async (path2, newStats) => {
30402
31138
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
@@ -30421,9 +31157,9 @@ var NodeFsHandler = class {
30421
31157
  prevStats = newStats2;
30422
31158
  }
30423
31159
  } catch (error) {
30424
- this.fsw._remove(dirname8, basename8);
31160
+ this.fsw._remove(dirname10, basename9);
30425
31161
  }
30426
- } else if (parent.has(basename8)) {
31162
+ } else if (parent.has(basename9)) {
30427
31163
  const at = newStats.atimeMs;
30428
31164
  const mt = newStats.mtimeMs;
30429
31165
  if (!at || at <= mt || mt !== prevStats.mtimeMs) {
@@ -31354,8 +32090,8 @@ function watch(paths, options = {}) {
31354
32090
  }
31355
32091
 
31356
32092
  // src/commands/progress.ts
31357
- import { existsSync as existsSync13, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
31358
- import { join as join15, basename as basename7 } from "node:path";
32093
+ import { existsSync as existsSync14, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
32094
+ import { join as join16, basename as basename8 } from "node:path";
31359
32095
 
31360
32096
  // ../../node_modules/.pnpm/log-update@7.0.2/node_modules/log-update/index.js
31361
32097
  import process12 from "node:process";
@@ -32223,7 +32959,7 @@ var log_update_default = logUpdate;
32223
32959
  var logUpdateStderr = createLogUpdate(process12.stderr);
32224
32960
 
32225
32961
  // src/lib/guards.ts
32226
- import { existsSync as existsSync6, mkdirSync as mkdirSync2 } from "node:fs";
32962
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "node:fs";
32227
32963
  import { join as join8 } from "node:path";
32228
32964
  function checkOcrSetup(targetDir) {
32229
32965
  const ocrDir = join8(targetDir, ".ocr");
@@ -32269,7 +33005,7 @@ function requireOcrSetup(targetDir) {
32269
33005
  function ensureSessionsDir(targetDir) {
32270
33006
  const sessionsDir = join8(targetDir, ".ocr", "sessions");
32271
33007
  if (!existsSync6(sessionsDir)) {
32272
- mkdirSync2(sessionsDir, { recursive: true });
33008
+ mkdirSync3(sessionsDir, { recursive: true });
32273
33009
  }
32274
33010
  return sessionsDir;
32275
33011
  }
@@ -32991,15 +33727,15 @@ function debounce(fn, delay) {
32991
33727
  };
32992
33728
  }
32993
33729
  function findLatestActiveSession(sessionsDir) {
32994
- if (!existsSync13(sessionsDir)) {
33730
+ if (!existsSync14(sessionsDir)) {
32995
33731
  return null;
32996
33732
  }
32997
- const sessions = readdirSync5(sessionsDir).filter((name) => {
32998
- const sessionPath = join15(sessionsDir, name);
32999
- return statSync2(sessionPath).isDirectory();
33733
+ const sessions = readdirSync6(sessionsDir).filter((name) => {
33734
+ const sessionPath = join16(sessionsDir, name);
33735
+ return statSync3(sessionPath).isDirectory();
33000
33736
  }).sort().reverse();
33001
33737
  for (const session of sessions) {
33002
- const sessionPath = join15(sessionsDir, session);
33738
+ const sessionPath = join16(sessionsDir, session);
33003
33739
  if (isSessionActive(sessionPath)) {
33004
33740
  return session;
33005
33741
  }
@@ -33014,8 +33750,8 @@ function getStrategyForSession(sessionPath, explicitWorkflow) {
33014
33750
  return getStrategy(workflowType) ?? null;
33015
33751
  }
33016
33752
  async function initProgressDb(ocrDir) {
33017
- const dbPath = join15(ocrDir, "data", "ocr.db");
33018
- if (!existsSync13(dbPath)) {
33753
+ const dbPath = join16(ocrDir, "data", "ocr.db");
33754
+ if (!existsSync14(dbPath)) {
33019
33755
  return;
33020
33756
  }
33021
33757
  try {
@@ -33040,11 +33776,11 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33040
33776
  const targetDir = process.cwd();
33041
33777
  requireOcrSetup(targetDir);
33042
33778
  const sessionsDir = ensureSessionsDir(targetDir);
33043
- const ocrDir = join15(targetDir, ".ocr");
33779
+ const ocrDir = join16(targetDir, ".ocr");
33044
33780
  await initProgressDb(ocrDir);
33045
33781
  if (options.session) {
33046
- const sessionPath = join15(sessionsDir, options.session);
33047
- if (!existsSync13(sessionPath)) {
33782
+ const sessionPath = join16(sessionsDir, options.session);
33783
+ if (!existsSync14(sessionPath)) {
33048
33784
  console.error(source_default.red(`Session not found: ${options.session}`));
33049
33785
  process.exit(1);
33050
33786
  }
@@ -33105,7 +33841,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33105
33841
  return;
33106
33842
  }
33107
33843
  let currentSession = findLatestActiveSession(sessionsDir);
33108
- let currentSessionPath = currentSession ? join15(sessionsDir, currentSession) : null;
33844
+ let currentSessionPath = currentSession ? join16(sessionsDir, currentSession) : null;
33109
33845
  let sessionWatcher = null;
33110
33846
  const preservedStartTimes = {
33111
33847
  review: void 0,
@@ -33113,11 +33849,11 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33113
33849
  };
33114
33850
  let currentStrategy = null;
33115
33851
  const updateDisplayImpl = () => {
33116
- if (!currentSessionPath || !existsSync13(currentSessionPath) || !isSessionActive(currentSessionPath)) {
33852
+ if (!currentSessionPath || !existsSync14(currentSessionPath) || !isSessionActive(currentSessionPath)) {
33117
33853
  const latestActive = findLatestActiveSession(sessionsDir);
33118
33854
  if (latestActive && latestActive !== currentSession) {
33119
33855
  currentSession = latestActive;
33120
- currentSessionPath = join15(sessionsDir, latestActive);
33856
+ currentSessionPath = join16(sessionsDir, latestActive);
33121
33857
  preservedStartTimes.review = void 0;
33122
33858
  preservedStartTimes.map = void 0;
33123
33859
  currentStrategy = null;
@@ -33130,7 +33866,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33130
33866
  currentStrategy = null;
33131
33867
  }
33132
33868
  }
33133
- if (currentSessionPath && existsSync13(currentSessionPath)) {
33869
+ if (currentSessionPath && existsSync14(currentSessionPath)) {
33134
33870
  if (!options.workflow) {
33135
33871
  const activeWorkflows = detectActiveWorkflows(currentSessionPath);
33136
33872
  if (activeWorkflows.length > 1) {
@@ -33184,17 +33920,17 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33184
33920
  watchSession(currentSessionPath);
33185
33921
  }
33186
33922
  const timerInterval = setInterval(updateDisplay, 1e3);
33187
- const watchDir = existsSync13(ocrDir) ? ocrDir : targetDir;
33923
+ const watchDir = existsSync14(ocrDir) ? ocrDir : targetDir;
33188
33924
  const dirWatcher = watch(watchDir, {
33189
33925
  persistent: true,
33190
33926
  ignoreInitial: true,
33191
33927
  depth: 3
33192
33928
  });
33193
33929
  dirWatcher.on("addDir", (dirPath) => {
33194
- const parentDir = join15(dirPath, "..");
33195
- const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(join15(".ocr", "sessions"));
33930
+ const parentDir = join16(dirPath, "..");
33931
+ const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(join16(".ocr", "sessions"));
33196
33932
  if (isDirectChild && !dirPath.endsWith("sessions")) {
33197
- const newSession = basename7(dirPath);
33933
+ const newSession = basename8(dirPath);
33198
33934
  currentSession = newSession;
33199
33935
  currentSessionPath = dirPath;
33200
33936
  preservedStartTimes.review = void 0;
@@ -33233,7 +33969,7 @@ function renderGenericWaiting() {
33233
33969
  }
33234
33970
  function renderCombinedProgress(sessionPath, preservedStartTimes, ocrDir) {
33235
33971
  const lines = [];
33236
- const session = basename7(sessionPath);
33972
+ const session = basename8(sessionPath);
33237
33973
  lines.push("");
33238
33974
  lines.push(
33239
33975
  source_default.bold.white(" Open Code Review") + source_default.yellow(" \xB7 Parallel Workflows")
@@ -33294,21 +34030,21 @@ function renderCombinedProgress(sessionPath, preservedStartTimes, ocrDir) {
33294
34030
  }
33295
34031
 
33296
34032
  // src/commands/state.ts
33297
- import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync11 } from "node:fs";
33298
- import { join as join17 } from "node:path";
34033
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11 } from "node:fs";
34034
+ import { join as join18 } from "node:path";
33299
34035
 
33300
34036
  // src/lib/state/index.ts
33301
34037
  init_db();
33302
34038
  init_exit_codes();
33303
34039
  import {
33304
- existsSync as existsSync14,
33305
- mkdirSync as mkdirSync5,
33306
- readdirSync as readdirSync6,
34040
+ existsSync as existsSync15,
34041
+ mkdirSync as mkdirSync6,
34042
+ readdirSync as readdirSync7,
33307
34043
  readFileSync as readFileSync10,
33308
- statSync as statSync3,
34044
+ statSync as statSync4,
33309
34045
  writeFileSync as writeFileSync7
33310
34046
  } from "node:fs";
33311
- import { join as join16 } from "node:path";
34047
+ import { join as join17 } from "node:path";
33312
34048
 
33313
34049
  // src/lib/state/phase-graph.ts
33314
34050
  init_exit_codes();
@@ -33616,9 +34352,9 @@ function deriveNextRound(db, sessionId, fallbackRound) {
33616
34352
  }
33617
34353
  function hasArtifacts(dir) {
33618
34354
  try {
33619
- for (const entry of readdirSync6(dir, { withFileTypes: true })) {
34355
+ for (const entry of readdirSync7(dir, { withFileTypes: true })) {
33620
34356
  if (entry.isDirectory()) {
33621
- if (hasArtifacts(join16(dir, entry.name))) return true;
34357
+ if (hasArtifacts(join17(dir, entry.name))) return true;
33622
34358
  } else if (/\.(md|json)$/.test(entry.name)) {
33623
34359
  return true;
33624
34360
  }
@@ -33629,7 +34365,7 @@ function hasArtifacts(dir) {
33629
34365
  }
33630
34366
  function readJsonFromSource(params) {
33631
34367
  if (params.source === "file") {
33632
- if (!existsSync14(params.filePath)) {
34368
+ if (!existsSync15(params.filePath)) {
33633
34369
  throw new StateError(STATE_EXIT.NOT_FOUND, `File not found: ${params.filePath}`);
33634
34370
  }
33635
34371
  return readFileSync10(params.filePath, "utf-8");
@@ -33956,7 +34692,7 @@ async function stateCompleteRound(params) {
33956
34692
  }
33957
34693
  const resolved = resolveSession(db, params.sessionId);
33958
34694
  const roundNumber = params.round ?? resolved.current_round;
33959
- const roundMetaPath = join16(
34695
+ const roundMetaPath = join17(
33960
34696
  resolved.session_dir,
33961
34697
  "rounds",
33962
34698
  `round-${roundNumber}`,
@@ -33977,8 +34713,8 @@ async function stateCompleteRound(params) {
33977
34713
  );
33978
34714
  }
33979
34715
  if (params.requireFinal) {
33980
- const finalPath = join16(resolved.session_dir, "rounds", `round-${roundNumber}`, "final.md");
33981
- if (!existsSync14(finalPath)) {
34716
+ const finalPath = join17(resolved.session_dir, "rounds", `round-${roundNumber}`, "final.md");
34717
+ if (!existsSync15(finalPath)) {
33982
34718
  throw new StateError(
33983
34719
  STATE_EXIT.INVARIANT_UNMET,
33984
34720
  `Cannot complete round: --require-final set but ${finalPath} is missing.`
@@ -33987,8 +34723,8 @@ async function stateCompleteRound(params) {
33987
34723
  }
33988
34724
  let metaPath;
33989
34725
  if (params.source === "stdin") {
33990
- const roundDir = join16(resolved.session_dir, "rounds", `round-${roundNumber}`);
33991
- mkdirSync5(roundDir, { recursive: true });
34726
+ const roundDir = join17(resolved.session_dir, "rounds", `round-${roundNumber}`);
34727
+ mkdirSync6(roundDir, { recursive: true });
33992
34728
  metaPath = roundMetaPath;
33993
34729
  writeFileSync7(metaPath, JSON.stringify(meta, null, 2));
33994
34730
  }
@@ -34042,7 +34778,7 @@ async function stateCompleteMap(params) {
34042
34778
  }
34043
34779
  const resolved = resolveSession(db, params.sessionId);
34044
34780
  const mapRunNumber = params.mapRun ?? resolved.current_map_run;
34045
- const mapMetaPath = join16(
34781
+ const mapMetaPath = join17(
34046
34782
  resolved.session_dir,
34047
34783
  "map",
34048
34784
  "runs",
@@ -34065,8 +34801,8 @@ async function stateCompleteMap(params) {
34065
34801
  }
34066
34802
  let metaPath;
34067
34803
  if (params.source === "stdin") {
34068
- const runDir = join16(resolved.session_dir, "map", "runs", `run-${mapRunNumber}`);
34069
- mkdirSync5(runDir, { recursive: true });
34804
+ const runDir = join17(resolved.session_dir, "map", "runs", `run-${mapRunNumber}`);
34805
+ mkdirSync6(runDir, { recursive: true });
34070
34806
  metaPath = mapMetaPath;
34071
34807
  writeFileSync7(metaPath, JSON.stringify(meta, null, 2));
34072
34808
  }
@@ -34151,17 +34887,17 @@ async function stateStatus(ocrDir, sessionId) {
34151
34887
  }
34152
34888
  async function stateSync(ocrDir) {
34153
34889
  const db = await ensureDatabase(ocrDir);
34154
- const sessionsRoot = join16(ocrDir, "sessions");
34155
- if (!existsSync14(sessionsRoot)) {
34890
+ const sessionsRoot = join17(ocrDir, "sessions");
34891
+ if (!existsSync15(sessionsRoot)) {
34156
34892
  return 0;
34157
34893
  }
34158
- const entries = readdirSync6(sessionsRoot).filter((name) => {
34159
- const fullPath = join16(sessionsRoot, name);
34160
- return statSync3(fullPath).isDirectory();
34894
+ const entries = readdirSync7(sessionsRoot).filter((name) => {
34895
+ const fullPath = join17(sessionsRoot, name);
34896
+ return statSync4(fullPath).isDirectory();
34161
34897
  });
34162
34898
  let synced = 0;
34163
34899
  for (const dirName of entries) {
34164
- const dirPath = join16(sessionsRoot, dirName);
34900
+ const dirPath = join17(sessionsRoot, dirName);
34165
34901
  const existing = getSession(db, dirName);
34166
34902
  if (existing) {
34167
34903
  continue;
@@ -34169,8 +34905,8 @@ async function stateSync(ocrDir) {
34169
34905
  if (!hasArtifacts(dirPath)) {
34170
34906
  continue;
34171
34907
  }
34172
- const hasRoundsDir = existsSync14(join16(dirPath, "rounds"));
34173
- const hasMapDir = existsSync14(join16(dirPath, "map"));
34908
+ const hasRoundsDir = existsSync15(join17(dirPath, "rounds"));
34909
+ const hasMapDir = existsSync15(join17(dirPath, "map"));
34174
34910
  const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
34175
34911
  const branchMatch = dirName.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
34176
34912
  const branch = branchMatch?.[1] ?? dirName;
@@ -34179,14 +34915,14 @@ async function stateSync(ocrDir) {
34179
34915
  let inferredRound = 1;
34180
34916
  let inferredMapRun = 1;
34181
34917
  if (workflowType === "review") {
34182
- const roundsDir = join16(dirPath, "rounds");
34183
- if (existsSync14(roundsDir)) {
34184
- 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);
34918
+ const roundsDir = join17(dirPath, "rounds");
34919
+ if (existsSync15(roundsDir)) {
34920
+ 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);
34185
34921
  const latestRoundNum = roundDirs[roundDirs.length - 1];
34186
34922
  if (latestRoundNum !== void 0) {
34187
34923
  inferredRound = latestRoundNum;
34188
- if (existsSync14(
34189
- join16(roundsDir, `round-${latestRoundNum}`, "final.md")
34924
+ if (existsSync15(
34925
+ join17(roundsDir, `round-${latestRoundNum}`, "final.md")
34190
34926
  )) {
34191
34927
  inferredPhase = "complete";
34192
34928
  inferredPhaseNumber = 8;
@@ -34194,13 +34930,13 @@ async function stateSync(ocrDir) {
34194
34930
  }
34195
34931
  }
34196
34932
  } else if (workflowType === "map") {
34197
- const runsDir = join16(dirPath, "map", "runs");
34198
- if (existsSync14(runsDir)) {
34199
- 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);
34933
+ const runsDir = join17(dirPath, "map", "runs");
34934
+ if (existsSync15(runsDir)) {
34935
+ 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);
34200
34936
  const latestRunNum = runDirs[runDirs.length - 1];
34201
34937
  if (latestRunNum !== void 0) {
34202
34938
  inferredMapRun = latestRunNum;
34203
- if (existsSync14(join16(runsDir, `run-${latestRunNum}`, "map.md"))) {
34939
+ if (existsSync15(join17(runsDir, `run-${latestRunNum}`, "map.md"))) {
34204
34940
  inferredPhase = "complete";
34205
34941
  inferredPhaseNumber = 6;
34206
34942
  }
@@ -34238,7 +34974,7 @@ init_command_log();
34238
34974
  init_db();
34239
34975
  init_db();
34240
34976
  function readDashboardSpawnMarker(ocrDir) {
34241
- const path2 = join17(ocrDir, "data", "dashboard-active-spawn.json");
34977
+ const path2 = join18(ocrDir, "data", "dashboard-active-spawn.json");
34242
34978
  let raw;
34243
34979
  try {
34244
34980
  raw = readFileSync11(path2, "utf-8");
@@ -34303,7 +35039,7 @@ async function linkDashboardInvocation(ocrDir, sessionId, explicitUid, label) {
34303
35039
  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) => {
34304
35040
  const targetDir = process.cwd();
34305
35041
  requireOcrSetup(targetDir);
34306
- const ocrDir = join17(targetDir, ".ocr");
35042
+ const ocrDir = join18(targetDir, ".ocr");
34307
35043
  try {
34308
35044
  const result = await stateShow(ocrDir, options.sessionId);
34309
35045
  if (!result) {
@@ -34372,7 +35108,7 @@ var showSubcommand = new Command("show").description("Show current session state
34372
35108
  var syncSubcommand = new Command("sync").description("Rebuild session state from filesystem artifacts").action(async () => {
34373
35109
  const targetDir = process.cwd();
34374
35110
  requireOcrSetup(targetDir);
34375
- const ocrDir = join17(targetDir, ".ocr");
35111
+ const ocrDir = join18(targetDir, ".ocr");
34376
35112
  try {
34377
35113
  const synced = await stateSync(ocrDir);
34378
35114
  console.log(`Synced ${synced} session${synced !== 1 ? "s" : ""} from filesystem.`);
@@ -34399,7 +35135,7 @@ var reconcileSubcommand = new Command("reconcile").description(
34399
35135
  ).option("--dry-run", "Print the repair plan without writing anything").option("--json", "Output the result as JSON").action(async (options) => {
34400
35136
  const targetDir = process.cwd();
34401
35137
  requireOcrSetup(targetDir);
34402
- const ocrDir = join17(targetDir, ".ocr");
35138
+ const ocrDir = join18(targetDir, ".ocr");
34403
35139
  try {
34404
35140
  const db = await ensureDatabase(ocrDir);
34405
35141
  const result = reconcileLegacyState(db, ocrDir, { dryRun: options.dryRun });
@@ -34458,9 +35194,9 @@ var beginSubcommand = new Command("begin").description("Start or resume a workfl
34458
35194
  async (options) => {
34459
35195
  const targetDir = process.cwd();
34460
35196
  requireOcrSetup(targetDir);
34461
- const ocrDir = join17(targetDir, ".ocr");
34462
- const sessionDir = options.sessionDir ?? join17(ocrDir, "sessions", options.sessionId);
34463
- if (!existsSync15(sessionDir)) mkdirSync6(sessionDir, { recursive: true });
35197
+ const ocrDir = join18(targetDir, ".ocr");
35198
+ const sessionDir = options.sessionDir ?? join18(ocrDir, "sessions", options.sessionId);
35199
+ if (!existsSync16(sessionDir)) mkdirSync7(sessionDir, { recursive: true });
34464
35200
  try {
34465
35201
  const result = await stateBegin({
34466
35202
  sessionId: options.sessionId,
@@ -34482,7 +35218,7 @@ var advanceSubcommand = new Command("advance").description("Advance the workflow
34482
35218
  async (options) => {
34483
35219
  const targetDir = process.cwd();
34484
35220
  requireOcrSetup(targetDir);
34485
- const ocrDir = join17(targetDir, ".ocr");
35221
+ const ocrDir = join18(targetDir, ".ocr");
34486
35222
  try {
34487
35223
  const { id: sessionId } = await resolveActiveSession(ocrDir, options.sessionId);
34488
35224
  await stateAdvance({
@@ -34502,7 +35238,7 @@ var completeRoundSubcommand = new Command("complete-round").description("Atomica
34502
35238
  async (options) => {
34503
35239
  const targetDir = process.cwd();
34504
35240
  requireOcrSetup(targetDir);
34505
- const ocrDir = join17(targetDir, ".ocr");
35241
+ const ocrDir = join18(targetDir, ".ocr");
34506
35242
  try {
34507
35243
  const base = options.stdin ? { source: "stdin", data: await readStdin() } : options.file ? { source: "file", filePath: options.file } : (() => {
34508
35244
  throw new StateError(STATE_EXIT.USAGE, "Provide --stdin or --file with round metadata");
@@ -34526,7 +35262,7 @@ var completeMapSubcommand = new Command("complete-map").description("Atomically
34526
35262
  async (options) => {
34527
35263
  const targetDir = process.cwd();
34528
35264
  requireOcrSetup(targetDir);
34529
- const ocrDir = join17(targetDir, ".ocr");
35265
+ const ocrDir = join18(targetDir, ".ocr");
34530
35266
  try {
34531
35267
  const base = options.stdin ? { source: "stdin", data: await readStdin() } : options.file ? { source: "file", filePath: options.file } : (() => {
34532
35268
  throw new StateError(STATE_EXIT.USAGE, "Provide --stdin or --file with map metadata");
@@ -34548,7 +35284,7 @@ var completeMapSubcommand = new Command("complete-map").description("Atomically
34548
35284
  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) => {
34549
35285
  const targetDir = process.cwd();
34550
35286
  requireOcrSetup(targetDir);
34551
- const ocrDir = join17(targetDir, ".ocr");
35287
+ const ocrDir = join18(targetDir, ".ocr");
34552
35288
  try {
34553
35289
  const { id: sessionId } = await resolveActiveSession(ocrDir, options.sessionId);
34554
35290
  await stateClose({ sessionId, ocrDir, abort: options.abort });
@@ -34560,7 +35296,7 @@ var finishSubcommand = new Command("finish").description("Close a workflow (refu
34560
35296
  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) => {
34561
35297
  const targetDir = process.cwd();
34562
35298
  requireOcrSetup(targetDir);
34563
- const ocrDir = join17(targetDir, ".ocr");
35299
+ const ocrDir = join18(targetDir, ".ocr");
34564
35300
  try {
34565
35301
  const result = await stateStatus(ocrDir, options.sessionId);
34566
35302
  if (options.json) {
@@ -34589,44 +35325,50 @@ var stateCommand = new Command("state").description("Manage OCR session state").
34589
35325
 
34590
35326
  // src/commands/session.ts
34591
35327
  import { randomUUID as randomUUID3 } from "node:crypto";
34592
- import { join as join19 } from "node:path";
35328
+ import { join as join20 } from "node:path";
34593
35329
  init_db();
34594
35330
 
34595
35331
  // src/lib/runtime-config.ts
34596
- import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
34597
- import { join as join18 } from "node:path";
35332
+ import { existsSync as existsSync17, readFileSync as readFileSync12 } from "node:fs";
35333
+ import { join as join19 } from "node:path";
34598
35334
  var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
34599
- function getAgentHeartbeatSeconds(ocrDir) {
34600
- const configPath = join18(ocrDir, "config.yaml");
34601
- if (!existsSync16(configPath)) {
34602
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
34603
- }
35335
+ function readRuntimePositiveInt(ocrDir, key, defaultValue) {
35336
+ const configPath = join19(ocrDir, "config.yaml");
35337
+ if (!existsSync17(configPath)) return defaultValue;
34604
35338
  let content;
34605
35339
  try {
34606
35340
  content = readFileSync12(configPath, "utf-8");
34607
35341
  } catch {
34608
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
35342
+ return defaultValue;
34609
35343
  }
34610
35344
  const blockMatch = content.match(
34611
- /^runtime:\s*\n(?:\s+[^\n]*\n)*?\s+agent_heartbeat_seconds:\s*([^\s#\n]+)/m
35345
+ new RegExp(
35346
+ String.raw`^runtime:\s*\n(?:\s+[^\n]*\n)*?\s+${key}:\s*([^\s#\n]+)`,
35347
+ "m"
35348
+ )
34612
35349
  );
34613
35350
  const inlineMatch = content.match(
34614
- /^runtime:\s*\{[^}]*\bagent_heartbeat_seconds:\s*([^\s,}]+)/m
35351
+ new RegExp(String.raw`^runtime:\s*\{[^}]*\b${key}:\s*([^\s,}]+)`, "m")
34615
35352
  );
34616
35353
  const raw = blockMatch?.[1] ?? inlineMatch?.[1];
34617
- if (!raw) {
34618
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
34619
- }
35354
+ if (!raw) return defaultValue;
34620
35355
  const parsed = Number(raw);
34621
35356
  if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
34622
35357
  process.stderr.write(
34623
- `[ocr] runtime.agent_heartbeat_seconds is not a positive integer (got "${raw}"); falling back to ${DEFAULT_AGENT_HEARTBEAT_SECONDS}s.
35358
+ `[ocr] runtime.${key} is not a positive integer (got "${raw}"); falling back to ${defaultValue}.
34624
35359
  `
34625
35360
  );
34626
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
35361
+ return defaultValue;
34627
35362
  }
34628
35363
  return parsed;
34629
35364
  }
35365
+ function getAgentHeartbeatSeconds(ocrDir) {
35366
+ return readRuntimePositiveInt(
35367
+ ocrDir,
35368
+ "agent_heartbeat_seconds",
35369
+ DEFAULT_AGENT_HEARTBEAT_SECONDS
35370
+ );
35371
+ }
34630
35372
 
34631
35373
  // src/commands/session.ts
34632
35374
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
@@ -34642,7 +35384,7 @@ function fail(message) {
34642
35384
  async function setup() {
34643
35385
  const targetDir = process.cwd();
34644
35386
  requireOcrSetup(targetDir);
34645
- const ocrDir = join19(targetDir, ".ocr");
35387
+ const ocrDir = join20(targetDir, ".ocr");
34646
35388
  return { ocrDir };
34647
35389
  }
34648
35390
  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(
@@ -34777,6 +35519,7 @@ var listSubcommand = new Command("list").description("List agent sessions for a
34777
35519
  var sessionCommand = new Command("session").description("Manage agent-CLI session lifecycle journal").addCommand(startInstanceSubcommand).addCommand(bindVendorIdSubcommand).addCommand(beatSubcommand).addCommand(endInstanceSubcommand).addCommand(listSubcommand);
34778
35520
 
34779
35521
  // src/lib/models.ts
35522
+ init_src();
34780
35523
  var BUNDLED_CLAUDE_MODELS = [
34781
35524
  { id: "claude-opus-4-7", displayName: "Claude Opus 4.7" },
34782
35525
  { id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" },
@@ -34897,8 +35640,8 @@ var modelsCommand = new Command("models").description("Inspect models available
34897
35640
 
34898
35641
  // src/commands/team.ts
34899
35642
  var import_yaml2 = __toESM(require_dist(), 1);
34900
- import { existsSync as existsSync17, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
34901
- import { join as join20 } from "node:path";
35643
+ import { existsSync as existsSync18, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
35644
+ import { join as join21 } from "node:path";
34902
35645
  async function readStdin2() {
34903
35646
  const chunks = [];
34904
35647
  for await (const chunk of process.stdin) {
@@ -34957,7 +35700,7 @@ var resolveSubcommand = new Command("resolve").description("Resolve and print th
34957
35700
  async (options) => {
34958
35701
  const targetDir = process.cwd();
34959
35702
  requireOcrSetup(targetDir);
34960
- const ocrDir = join20(targetDir, ".ocr");
35703
+ const ocrDir = join21(targetDir, ".ocr");
34961
35704
  try {
34962
35705
  const { team } = loadTeamConfig(ocrDir);
34963
35706
  let override;
@@ -34996,8 +35739,8 @@ var setSubcommand = new Command("set").description("Persist a new default_team c
34996
35739
  }
34997
35740
  const targetDir = process.cwd();
34998
35741
  requireOcrSetup(targetDir);
34999
- const ocrDir = join20(targetDir, ".ocr");
35000
- const configPath = join20(ocrDir, "config.yaml");
35742
+ const ocrDir = join21(targetDir, ".ocr");
35743
+ const configPath = join21(ocrDir, "config.yaml");
35001
35744
  try {
35002
35745
  const raw = await readStdin2();
35003
35746
  const team = parseSessionOverride(raw);
@@ -35007,12 +35750,12 @@ var setSubcommand = new Command("set").description("Persist a new default_team c
35007
35750
  list.push(inst);
35008
35751
  byPersona.set(inst.persona, list);
35009
35752
  }
35010
- const doc = existsSync17(configPath) ? (0, import_yaml2.parseDocument)(readFileSync13(configPath, "utf-8")) : new import_yaml2.Document({});
35753
+ const doc = existsSync18(configPath) ? (0, import_yaml2.parseDocument)(readFileSync13(configPath, "utf-8")) : new import_yaml2.Document({});
35011
35754
  applyDefaultTeamSurgically(doc, byPersona);
35012
35755
  const yamlOutput = doc.toString({ lineWidth: 0 });
35013
35756
  writeFileSync8(configPath, yamlOutput, "utf-8");
35014
- const reviewersDir = join20(ocrDir, "skills", "references", "reviewers");
35015
- const metaPath = join20(ocrDir, "reviewers-meta.json");
35757
+ const reviewersDir = join21(ocrDir, "skills", "references", "reviewers");
35758
+ const metaPath = join21(ocrDir, "reviewers-meta.json");
35016
35759
  let metaWritten = false;
35017
35760
  try {
35018
35761
  const meta = generateReviewersMeta(reviewersDir, configPath);
@@ -35108,7 +35851,7 @@ var teamCommand = new Command("team").description("Resolve and persist team comp
35108
35851
 
35109
35852
  // src/commands/review.ts
35110
35853
  import { spawn as spawn3 } from "node:child_process";
35111
- import { join as join21 } from "node:path";
35854
+ import { join as join22 } from "node:path";
35112
35855
  init_db();
35113
35856
 
35114
35857
  // src/lib/vendor-resume.ts
@@ -35147,7 +35890,7 @@ var reviewCommand = new Command("review").description("Run or resume an OCR revi
35147
35890
  }
35148
35891
  const targetDir = process.cwd();
35149
35892
  requireOcrSetup(targetDir);
35150
- const ocrDir = join21(targetDir, ".ocr");
35893
+ const ocrDir = join22(targetDir, ".ocr");
35151
35894
  const db = await ensureDatabase(ocrDir);
35152
35895
  const session = getSession(db, options.resume);
35153
35896
  if (!session) {
@@ -35189,23 +35932,23 @@ var reviewCommand = new Command("review").description("Run or resume an OCR revi
35189
35932
  });
35190
35933
 
35191
35934
  // src/commands/update.ts
35192
- import { existsSync as existsSync18 } from "node:fs";
35193
- import { join as join22 } from "node:path";
35935
+ import { existsSync as existsSync19 } from "node:fs";
35936
+ import { join as join23 } from "node:path";
35194
35937
  function detectConfiguredTools(targetDir) {
35195
35938
  return AI_TOOLS.filter((tool) => {
35196
35939
  if (tool.commandStrategy === "subdirectory") {
35197
- const ocrDir = join22(targetDir, tool.commandsDir, "ocr");
35198
- return existsSync18(ocrDir);
35940
+ const ocrDir = join23(targetDir, tool.commandsDir, "ocr");
35941
+ return existsSync19(ocrDir);
35199
35942
  } else {
35200
- const reviewCmd = join22(targetDir, tool.commandsDir, "ocr-review.md");
35201
- return existsSync18(reviewCmd);
35943
+ const reviewCmd = join23(targetDir, tool.commandsDir, "ocr-review.md");
35944
+ return existsSync19(reviewCmd);
35202
35945
  }
35203
35946
  });
35204
35947
  }
35205
35948
  var updateCommand = new Command("update").description("Update OCR assets after package upgrade").option("--commands", "Update only commands/workflows").option(
35206
35949
  "--skills",
35207
35950
  "Update only skills (includes templates, references, assets)"
35208
- ).option("--inject", "Update only AGENTS.md/CLAUDE.md injection").option("--dry-run", "Preview changes without modifying files").action(async (options) => {
35951
+ ).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) => {
35209
35952
  const targetDir = process.cwd();
35210
35953
  requireOcrSetup(targetDir);
35211
35954
  console.log();
@@ -35272,7 +36015,7 @@ var updateCommand = new Command("update").description("Update OCR assets after p
35272
36015
  const result = installForTool(tool, targetDir);
35273
36016
  results.push(result);
35274
36017
  }
35275
- ensureGitignore(join22(targetDir, ".ocr"));
36018
+ ensureGitignore(join23(targetDir, ".ocr"));
35276
36019
  spinner.stop();
35277
36020
  const successful = results.filter((r) => r.success);
35278
36021
  const failed = results.filter((r) => !r.success);
@@ -35306,30 +36049,34 @@ var updateCommand = new Command("update").description("Update OCR assets after p
35306
36049
  }
35307
36050
  }
35308
36051
  if (updateInject) {
36052
+ const planned = plannedInstructionFiles(toolsToUpdate);
35309
36053
  if (options.dryRun) {
35310
36054
  console.log(source_default.dim(" Would update:"));
35311
- if (existsSync18(join22(targetDir, "AGENTS.md"))) {
35312
- console.log(source_default.dim(" \u2022 AGENTS.md (OCR managed block)"));
36055
+ for (const path2 of planned) {
36056
+ const verb = existsSync19(join23(targetDir, path2)) ? "update" : "create";
36057
+ console.log(source_default.dim(` \u2022 ${path2} (${verb} OCR managed block)`));
35313
36058
  }
35314
- if (existsSync18(join22(targetDir, "CLAUDE.md"))) {
35315
- console.log(source_default.dim(" \u2022 CLAUDE.md (OCR managed block)"));
36059
+ const staleDry = findStaleInstructionFiles(targetDir, planned);
36060
+ for (const warning of formatStaleWarnings(staleDry, "dry-run")) {
36061
+ console.log(source_default.dim(` \u2022 ${warning}`));
35316
36062
  }
35317
36063
  console.log();
35318
36064
  } else {
35319
- const spinner = ora("Updating AGENTS.md/CLAUDE.md...").start();
35320
- const injectResults = injectIntoProjectFiles(targetDir);
36065
+ const spinner = ora("Updating instruction files...").start();
36066
+ const injectResults = injectIntoProjectFiles(targetDir, toolsToUpdate);
35321
36067
  spinner.stop();
35322
- if (injectResults.agentsMd || injectResults.claudeMd) {
36068
+ if (injectResults.written.length > 0) {
35323
36069
  console.log(source_default.green(" \u2713 Instructions updated"));
35324
- if (injectResults.agentsMd) {
35325
- console.log(` ${source_default.green("\u2713")} AGENTS.md`);
35326
- }
35327
- if (injectResults.claudeMd) {
35328
- console.log(` ${source_default.green("\u2713")} CLAUDE.md`);
36070
+ for (const path2 of injectResults.written) {
36071
+ console.log(` ${source_default.green("\u2713")} ${path2}`);
35329
36072
  }
35330
36073
  } else {
35331
36074
  console.log(source_default.dim(" No instruction files to update"));
35332
36075
  }
36076
+ const stale = findStaleInstructionFiles(targetDir, injectResults.written);
36077
+ for (const warning of formatStaleWarnings(stale, "update")) {
36078
+ console.log(source_default.yellow(` \u26A0 ${warning}`));
36079
+ }
35333
36080
  console.log();
35334
36081
  }
35335
36082
  }
@@ -35343,14 +36090,15 @@ var updateCommand = new Command("update").description("Update OCR assets after p
35343
36090
  });
35344
36091
 
35345
36092
  // src/commands/dashboard.ts
35346
- import { existsSync as existsSync19 } from "node:fs";
35347
- import { join as join23, dirname as dirname7 } from "node:path";
36093
+ import { existsSync as existsSync20 } from "node:fs";
36094
+ import { join as join24, dirname as dirname9 } from "node:path";
35348
36095
  import { fileURLToPath } from "node:url";
36096
+ init_src();
35349
36097
  init_db();
35350
36098
  var __filename = fileURLToPath(import.meta.url);
35351
- var __dirname = dirname7(__filename);
36099
+ var __dirname = dirname9(__filename);
35352
36100
  function resolveServerPath() {
35353
- return join23(__dirname, "dashboard", "server.js");
36101
+ return join24(__dirname, "dashboard", "server.js");
35354
36102
  }
35355
36103
  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(
35356
36104
  async (options) => {
@@ -35361,7 +36109,7 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
35361
36109
  console.error(source_default.red(`Error: Invalid port "${options.port}". Must be 1-65535.`));
35362
36110
  process.exit(1);
35363
36111
  }
35364
- const ocrDir = join23(targetDir, ".ocr");
36112
+ const ocrDir = join24(targetDir, ".ocr");
35365
36113
  try {
35366
36114
  await ensureDatabase(ocrDir);
35367
36115
  closeAllDatabases();
@@ -35375,7 +36123,7 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
35375
36123
  process.exit(1);
35376
36124
  }
35377
36125
  const serverPath = resolveServerPath();
35378
- if (!existsSync19(serverPath)) {
36126
+ if (!existsSync20(serverPath)) {
35379
36127
  console.error(source_default.red("Error: Dashboard server bundle not found."));
35380
36128
  console.error(
35381
36129
  source_default.dim(` Expected at: ${serverPath}`)
@@ -35409,11 +36157,53 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
35409
36157
  );
35410
36158
 
35411
36159
  // src/commands/doctor.ts
35412
- import { existsSync as existsSync20 } from "node:fs";
35413
- import { join as join24 } from "node:path";
36160
+ import { existsSync as existsSync21 } from "node:fs";
36161
+ import { join as join25 } from "node:path";
35414
36162
  init_db();
35415
- var doctorCommand = new Command("doctor").description("Check OCR installation and verify all dependencies").action(() => {
36163
+ function printStorageEngine(probeWriteEnabled) {
36164
+ console.log();
36165
+ console.log(source_default.bold(" Storage Engine"));
36166
+ console.log();
36167
+ const engine = probeEngine();
36168
+ if (!engine.ok) {
36169
+ console.log(` ${source_default.red("\u2717")} node:sqlite unavailable`);
36170
+ console.log(` ${source_default.dim(engine.error)}`);
36171
+ console.log(
36172
+ ` ${source_default.dim(
36173
+ "OCR requires Node >= 22.5 (node:sqlite). Upgrade Node, then re-run `ocr doctor`."
36174
+ )}`
36175
+ );
36176
+ return false;
36177
+ }
36178
+ console.log(
36179
+ ` ${source_default.green("\u2713")} node:sqlite (SQLite ${engine.version}, WAL)`
36180
+ );
36181
+ if (probeWriteEnabled) {
36182
+ const write = probeWrite();
36183
+ if (!write.ok) {
36184
+ console.log(` ${source_default.red("\u2717")} write probe failed`);
36185
+ console.log(` ${source_default.dim(write.error)}`);
36186
+ return false;
36187
+ }
36188
+ console.log(
36189
+ ` ${source_default.green("\u2713")} write probe (on-disk WAL transaction round-trip)`
36190
+ );
36191
+ }
36192
+ return true;
36193
+ }
36194
+ var doctorCommand = new Command("doctor").description("Check OCR installation and verify all dependencies").option(
36195
+ "--probe-write",
36196
+ "additionally exercise an on-disk WAL transaction round-trip (used by the release install gate)"
36197
+ ).option(
36198
+ "--engine-only",
36199
+ "check ONLY the storage engine and exit on its result \u2014 skips project/tool checks (used by the release install gate, which runs from a non-initialized dir with no AI tools)"
36200
+ ).action((options) => {
35416
36201
  printHeader();
36202
+ if (options.engineOnly) {
36203
+ const ok = printStorageEngine(options.probeWrite ?? false);
36204
+ console.log();
36205
+ process.exit(ok ? 0 : 1);
36206
+ }
35417
36207
  const targetDir = process.cwd();
35418
36208
  let hasIssues = false;
35419
36209
  const depResult = checkDependencies();
@@ -35425,10 +36215,10 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
35425
36215
  console.log(source_default.bold(" OCR Installation"));
35426
36216
  console.log();
35427
36217
  const ocrStatus = checkOcrSetup(targetDir);
35428
- const configPath = join24(targetDir, ".ocr", "config.yaml");
35429
- const dbPath = join24(targetDir, ".ocr", "data", "ocr.db");
35430
- const hasConfig = existsSync20(configPath);
35431
- const hasDb = existsSync20(dbPath);
36218
+ const configPath = join25(targetDir, ".ocr", "config.yaml");
36219
+ const dbPath = join25(targetDir, ".ocr", "data", "ocr.db");
36220
+ const hasConfig = existsSync21(configPath);
36221
+ const hasDb = existsSync21(dbPath);
35432
36222
  const ocrChecks = [
35433
36223
  { label: ".ocr/skills/", ok: ocrStatus.hasSkills },
35434
36224
  { label: ".ocr/sessions/", ok: ocrStatus.hasSessions },
@@ -35450,25 +36240,8 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
35450
36240
  if (!ocrStatus.valid) {
35451
36241
  hasIssues = true;
35452
36242
  }
35453
- console.log();
35454
- console.log(source_default.bold(" Storage Engine"));
35455
- console.log();
35456
- const engine = probeEngine();
35457
- if (engine.ok) {
35458
- console.log(
35459
- ` ${source_default.green("\u2713")} better-sqlite3 (SQLite ${engine.version}, WAL)`
35460
- );
35461
- } else {
36243
+ if (!printStorageEngine(options.probeWrite ?? false)) {
35462
36244
  hasIssues = true;
35463
- console.log(
35464
- ` ${source_default.red("\u2717")} better-sqlite3 failed to load`
35465
- );
35466
- console.log(` ${source_default.dim(engine.error)}`);
35467
- console.log(
35468
- ` ${source_default.dim(
35469
- "Reinstall the CLI; if it persists your platform may need build tools (python3 + a C++ toolchain) or lacks a prebuilt binary."
35470
- )}`
35471
- );
35472
36245
  }
35473
36246
  console.log();
35474
36247
  printCapabilities(depResult);
@@ -35521,9 +36294,331 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
35521
36294
  console.log();
35522
36295
  });
35523
36296
 
36297
+ // src/commands/db.ts
36298
+ import { existsSync as existsSync22, readFileSync as readFileSync14 } from "node:fs";
36299
+ import { join as join26 } from "node:path";
36300
+ init_src();
36301
+ init_db();
36302
+ function fail4(message) {
36303
+ console.error(source_default.red(`Error: ${message}`));
36304
+ process.exit(1);
36305
+ }
36306
+ function resolveOcrDir() {
36307
+ const targetDir = process.cwd();
36308
+ requireOcrSetup(targetDir);
36309
+ return join26(targetDir, ".ocr");
36310
+ }
36311
+ function dbPathFor(ocrDir) {
36312
+ return join26(ocrDir, "data", "ocr.db");
36313
+ }
36314
+ function formatBytes(n) {
36315
+ if (n < 1024) return `${n} B`;
36316
+ const units = ["KB", "MB", "GB", "TB"];
36317
+ let v = n / 1024;
36318
+ let i = 0;
36319
+ while (v >= 1024 && i < units.length - 1) {
36320
+ v /= 1024;
36321
+ i++;
36322
+ }
36323
+ return `${v.toFixed(v >= 100 ? 0 : 1)} ${units[i]}`;
36324
+ }
36325
+ function liveDashboardPid(ocrDir) {
36326
+ const pidFile = join26(ocrDir, "data", "dashboard.pid");
36327
+ if (!existsSync22(pidFile)) return null;
36328
+ try {
36329
+ const pid = parseInt(readFileSync14(pidFile, "utf-8").trim(), 10);
36330
+ if (!Number.isNaN(pid) && isProcessAlive(pid)) return pid;
36331
+ } catch {
36332
+ }
36333
+ return null;
36334
+ }
36335
+ function guardExclusive(ocrDir, force, op) {
36336
+ const pid = liveDashboardPid(ocrDir);
36337
+ if (pid !== null && !force) {
36338
+ fail4(
36339
+ `a dashboard appears to be running (PID ${pid}); ${op} needs exclusive access to the database.
36340
+ Stop it first, or pass --force to proceed anyway.`
36341
+ );
36342
+ }
36343
+ }
36344
+ function printHealth(report) {
36345
+ console.log();
36346
+ console.log(source_default.bold(" Database Health"));
36347
+ console.log();
36348
+ console.log(` File: ${report.dbPath}`);
36349
+ console.log(` Size: ${formatBytes(report.fileSizeBytes)}`);
36350
+ if (report.reclaimableBytes > 0) {
36351
+ console.log(
36352
+ ` Reclaimable: ${source_default.yellow(formatBytes(report.reclaimableBytes))} ` + source_default.dim(`(${report.freelistCount} free pages \u2014 run \`ocr db vacuum\`)`)
36353
+ );
36354
+ }
36355
+ console.log(
36356
+ ` Records: ${report.sessionCount} session(s), ${report.eventCount} event(s)`
36357
+ );
36358
+ console.log();
36359
+ const ok = (s) => ` ${source_default.green("\u2713")} ${s}`;
36360
+ const bad = (s) => ` ${source_default.red("\u2717")} ${s}`;
36361
+ console.log(
36362
+ report.integrityOk ? ok("integrity_check: ok") : bad(`integrity_check: ${report.integrityErrors.length} error(s)`)
36363
+ );
36364
+ if (!report.integrityOk) {
36365
+ for (const e of report.integrityErrors.slice(0, 5)) {
36366
+ console.log(` ${source_default.dim(e)}`);
36367
+ }
36368
+ }
36369
+ const fkTotal = report.fkViolations.reduce((n, g) => n + g.count, 0) + report.protectedFkViolations.reduce((n, g) => n + g.count, 0);
36370
+ if (fkTotal === 0) {
36371
+ console.log(ok("foreign_key_check: 0 violations"));
36372
+ } else {
36373
+ console.log(bad(`foreign_key_check: ${fkTotal} violation(s)`));
36374
+ for (const g of report.fkViolations) {
36375
+ console.log(` ${source_default.dim(`${g.table}: ${g.count} orphan(s)`)}`);
36376
+ }
36377
+ for (const g of report.protectedFkViolations) {
36378
+ console.log(
36379
+ ` ${source_default.yellow(`${g.table}: ${g.count} (protected \u2014 manual review)`)}`
36380
+ );
36381
+ }
36382
+ }
36383
+ if (report.markdownDuplicateRows === 0) {
36384
+ console.log(ok("markdown_artifacts: no duplicates"));
36385
+ } else {
36386
+ console.log(
36387
+ bad(`markdown_artifacts: ${report.markdownDuplicateRows} duplicate row(s)`)
36388
+ );
36389
+ }
36390
+ const reapable = report.orphanTempFiles.filter((f) => f.reapable);
36391
+ if (report.orphanTempFiles.length > 0) {
36392
+ console.log(
36393
+ ` ${reapable.length > 0 ? source_default.yellow("\u26A0") : source_default.dim("\xB7")} orphan temp files: ${report.orphanTempFiles.length} (${reapable.length} reapable)`
36394
+ );
36395
+ }
36396
+ if (report.backupFiles.length > 0) {
36397
+ const total = report.backupFiles.reduce((n, b) => n + b.sizeBytes, 0);
36398
+ console.log(
36399
+ ` ${source_default.dim("\xB7")} backups: ${report.backupFiles.length} (${formatBytes(total)})`
36400
+ );
36401
+ }
36402
+ console.log();
36403
+ }
36404
+ function needsFix(report) {
36405
+ return !report.integrityOk || report.fkViolations.length > 0 || report.markdownDuplicateRows > 0 || report.orphanTempFiles.some((f) => f.reapable) || report.reclaimableBytes > 0;
36406
+ }
36407
+ 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(
36408
+ async (options) => {
36409
+ const ocrDir = resolveOcrDir();
36410
+ const dbPath = dbPathFor(ocrDir);
36411
+ const db = await ensureDatabase(ocrDir);
36412
+ if (options.json) {
36413
+ console.log(JSON.stringify(collectDbHealth(db, dbPath), null, 2));
36414
+ return;
36415
+ }
36416
+ const before = collectDbHealth(db, dbPath);
36417
+ printHealth(before);
36418
+ if (!options.fix) {
36419
+ if (needsFix(before)) {
36420
+ console.log(
36421
+ source_default.dim(" Run `ocr db doctor --fix` to repair the issues above.")
36422
+ );
36423
+ console.log();
36424
+ } else {
36425
+ console.log(source_default.green(" \u2713 Database is healthy"));
36426
+ console.log();
36427
+ }
36428
+ return;
36429
+ }
36430
+ guardExclusive(ocrDir, options.force ?? false, "doctor --fix");
36431
+ const result = fixDb(db, dbPath, { snapshot: options.snapshot !== false });
36432
+ console.log(source_default.bold(" Repairs applied"));
36433
+ console.log();
36434
+ if (result.snapshotPath) {
36435
+ console.log(` ${source_default.dim("snapshot:")} ${result.snapshotPath}`);
36436
+ }
36437
+ if (result.totalFkOrphansDeleted > 0) {
36438
+ console.log(
36439
+ ` ${source_default.green("\u2713")} swept ${result.totalFkOrphansDeleted} FK-orphan row(s)`
36440
+ );
36441
+ for (const g of result.fkOrphansDeleted) {
36442
+ console.log(` ${source_default.dim(`${g.table}: ${g.count}`)}`);
36443
+ }
36444
+ }
36445
+ if (result.markdownDupsDeleted > 0) {
36446
+ console.log(
36447
+ ` ${source_default.green("\u2713")} removed ${result.markdownDupsDeleted} duplicate markdown row(s)`
36448
+ );
36449
+ }
36450
+ if (result.tempsReaped.length > 0) {
36451
+ console.log(
36452
+ ` ${source_default.green("\u2713")} reaped ${result.tempsReaped.length} orphan temp file(s)`
36453
+ );
36454
+ }
36455
+ if (result.vacuumed) {
36456
+ const saved = result.sizeBeforeBytes - result.sizeAfterBytes;
36457
+ console.log(
36458
+ ` ${source_default.green("\u2713")} VACUUM: ${formatBytes(result.sizeBeforeBytes)} \u2192 ${formatBytes(result.sizeAfterBytes)} ` + source_default.dim(`(reclaimed ${formatBytes(Math.max(0, saved))})`)
36459
+ );
36460
+ }
36461
+ console.log();
36462
+ if (result.protectedViolationsRemaining.length > 0) {
36463
+ console.log(
36464
+ source_default.yellow(
36465
+ " \u26A0 Violations remain in protected (system-of-record) tables:"
36466
+ )
36467
+ );
36468
+ for (const g of result.protectedViolationsRemaining) {
36469
+ console.log(` ${source_default.yellow(`${g.table}: ${g.count}`)}`);
36470
+ }
36471
+ console.log();
36472
+ }
36473
+ if (result.integrityOkAfter && result.fkViolationsAfter === 0) {
36474
+ console.log(source_default.green(" \u2713 Database repaired and healthy"));
36475
+ } else {
36476
+ console.log(
36477
+ source_default.red(
36478
+ ` \u2717 Post-fix check: integrity ${result.integrityOkAfter ? "ok" : "FAILED"}, ${result.fkViolationsAfter} FK violation(s) remaining`
36479
+ )
36480
+ );
36481
+ process.exitCode = 1;
36482
+ }
36483
+ console.log();
36484
+ }
36485
+ );
36486
+ 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) => {
36487
+ const ocrDir = resolveOcrDir();
36488
+ const dbPath = dbPathFor(ocrDir);
36489
+ guardExclusive(ocrDir, options.force ?? false, "vacuum");
36490
+ const db = await ensureDatabase(ocrDir);
36491
+ const result = vacuumDb(db, dbPath, { snapshot: options.snapshot !== false });
36492
+ console.log();
36493
+ if (result.snapshotPath) {
36494
+ console.log(` ${source_default.dim("snapshot:")} ${result.snapshotPath}`);
36495
+ }
36496
+ console.log(
36497
+ ` ${source_default.green("\u2713")} VACUUM: ${formatBytes(result.sizeBeforeBytes)} \u2192 ${formatBytes(result.sizeAfterBytes)} ` + source_default.dim(`(reclaimed ${formatBytes(result.reclaimedBytes)})`)
36498
+ );
36499
+ console.log();
36500
+ });
36501
+ var pruneSubcommand = new Command("prune").description(
36502
+ "Drop derived artifacts of old CLOSED sessions (events + sessions kept)"
36503
+ ).option(
36504
+ "--keep-sessions <n>",
36505
+ "protect the N most-recently-active closed sessions",
36506
+ (v) => parseInt(v, 10)
36507
+ ).option(
36508
+ "--older-than <days>",
36509
+ "only prune closed sessions quiet for more than D days",
36510
+ (v) => parseInt(v, 10)
36511
+ ).option("--dry-run", "show what would be pruned without deleting").option("--force", "proceed even if a live dashboard owns the database").action(
36512
+ async (options) => {
36513
+ const ocrDir = resolveOcrDir();
36514
+ const dbPath = dbPathFor(ocrDir);
36515
+ if (options.keepSessions === void 0 && options.olderThan === void 0) {
36516
+ fail4(
36517
+ "prune needs a bound: pass --older-than <days> and/or --keep-sessions <n>."
36518
+ );
36519
+ }
36520
+ if (!options.dryRun) {
36521
+ guardExclusive(ocrDir, options.force ?? false, "prune");
36522
+ }
36523
+ const db = await ensureDatabase(ocrDir);
36524
+ const result = pruneDb(db, dbPath, {
36525
+ keepSessions: options.keepSessions,
36526
+ olderThanDays: options.olderThan,
36527
+ dryRun: options.dryRun ?? false
36528
+ });
36529
+ console.log();
36530
+ if (result.prunedSessions.length === 0) {
36531
+ console.log(source_default.green(" \u2713 Nothing to prune"));
36532
+ console.log();
36533
+ return;
36534
+ }
36535
+ const verb = result.dryRun ? "Would prune" : "Pruned";
36536
+ console.log(
36537
+ source_default.bold(
36538
+ ` ${verb} ${result.totalArtifactRows} artifact row(s) across ${result.prunedSessions.length} session(s)`
36539
+ )
36540
+ );
36541
+ console.log();
36542
+ for (const p of result.prunedSessions.slice(0, 20)) {
36543
+ console.log(
36544
+ ` ${source_default.dim("\xB7")} ${p.sessionId} ${source_default.dim(`(${p.artifactRows} rows)`)}`
36545
+ );
36546
+ }
36547
+ if (result.prunedSessions.length > 20) {
36548
+ console.log(
36549
+ ` ${source_default.dim(`\u2026 and ${result.prunedSessions.length - 20} more`)}`
36550
+ );
36551
+ }
36552
+ console.log();
36553
+ if (result.snapshotPath) {
36554
+ console.log(` ${source_default.dim("snapshot:")} ${result.snapshotPath}`);
36555
+ }
36556
+ console.log(
36557
+ source_default.dim(
36558
+ result.dryRun ? " Re-run without --dry-run to apply. Events + session rows are always kept." : " Events + session rows were kept; sessions remain fully auditable."
36559
+ )
36560
+ );
36561
+ console.log();
36562
+ }
36563
+ );
36564
+ function validatePruneBackupsOptions(options) {
36565
+ if (!Number.isInteger(options.keep) || options.keep < 0) {
36566
+ return `--keep must be a non-negative integer (got "${String(options.keep)}").`;
36567
+ }
36568
+ if (options.keep === 0 && !options.force && !options.dryRun) {
36569
+ return "--keep 0 removes every backup (including any just-written snapshot). Re-run with --dry-run to preview, or --force to confirm.";
36570
+ }
36571
+ return null;
36572
+ }
36573
+ var pruneBackupsSubcommand = new Command("prune-backups").description("Delete old ocr.db.bak.* snapshots, keeping the most recent few").option(
36574
+ "--keep <n>",
36575
+ "retain the N most-recent backups (default 1; 0 removes all, requires --force)",
36576
+ // Raw conversion only — `Number('oops')` is NaN and flows into
36577
+ // validatePruneBackupsOptions, the single validation home. (parseInt would
36578
+ // also silently accept "3abc" → 3; Number rejects it as NaN.)
36579
+ (v) => Number(v),
36580
+ 1
36581
+ ).option("--force", "permit --keep 0 (removing the last backup / safety net)").option("--dry-run", "show what would be deleted without deleting").action(async (options) => {
36582
+ const ocrDir = resolveOcrDir();
36583
+ const dataDir = join26(ocrDir, "data");
36584
+ const invalid = validatePruneBackupsOptions(options);
36585
+ if (invalid !== null) {
36586
+ fail4(invalid);
36587
+ }
36588
+ const result = pruneBackups(dataDir, dbPathFor(ocrDir), {
36589
+ keep: options.keep,
36590
+ dryRun: options.dryRun ?? false
36591
+ });
36592
+ console.log();
36593
+ if (result.deleted.length === 0) {
36594
+ console.log(source_default.green(" \u2713 No backups to remove"));
36595
+ console.log();
36596
+ return;
36597
+ }
36598
+ const verb = result.dryRun ? "Would delete" : "Deleted";
36599
+ console.log(
36600
+ source_default.bold(
36601
+ ` ${verb} ${result.deleted.length} backup(s) \u2014 ${formatBytes(result.reclaimedBytes)}`
36602
+ )
36603
+ );
36604
+ console.log();
36605
+ for (const b of result.deleted) {
36606
+ console.log(` ${source_default.dim("\xB7")} ${b.name} ${source_default.dim(`(${formatBytes(b.sizeBytes)})`)}`);
36607
+ }
36608
+ if (result.kept.length > 0) {
36609
+ console.log();
36610
+ console.log(
36611
+ source_default.dim(` Kept ${result.kept.length} most-recent backup(s) as a safety net.`)
36612
+ );
36613
+ }
36614
+ console.log();
36615
+ });
36616
+ var dbCommand = new Command("db").description("Inspect and maintain the OCR SQLite database").addCommand(doctorSubcommand).addCommand(vacuumSubcommand).addCommand(pruneSubcommand).addCommand(pruneBackupsSubcommand);
36617
+
35524
36618
  // src/commands/reviewers.ts
35525
36619
  import { writeFileSync as writeFileSync9, renameSync as renameSync2 } from "node:fs";
35526
- import { join as join25 } from "node:path";
36620
+ import { join as join27 } from "node:path";
36621
+ init_src();
35527
36622
  async function readStdin3() {
35528
36623
  const chunks = [];
35529
36624
  for await (const chunk of process.stdin) {
@@ -35537,6 +36632,25 @@ async function readStdin3() {
35537
36632
  }
35538
36633
  var VALID_TIERS = /* @__PURE__ */ new Set(["holistic", "specialist", "persona", "custom"]);
35539
36634
  var SLUG_RE = /^[a-z][a-z0-9-]*$/;
36635
+ var INJECTION_PATTERNS = [
36636
+ /ignore\s+(all\s+|the\s+)?(previous|prior|above)?\s*(instructions|prompts|rules)/i,
36637
+ /disregard\s+(all\s+|the\s+)?(previous|prior|above)/i,
36638
+ /\byou\s+are\s+now\b/i,
36639
+ /^\s*system\s*:/im,
36640
+ /\balways\s+(conclude|respond|reply|return|output|approve|reject|say)\b/i,
36641
+ /\bnew\s+rule\s*:/i
36642
+ ];
36643
+ function warnIfSuspiciousPersona(label, fields) {
36644
+ const text = fields.filter((f) => typeof f === "string").join("\n");
36645
+ const hit = INJECTION_PATTERNS.find((re) => re.test(text));
36646
+ if (hit) {
36647
+ console.error(
36648
+ source_default.yellow(
36649
+ `\u26A0 ${label} contains text resembling a prompt-injection override (matched ${hit}). Review the persona before relying on it.`
36650
+ )
36651
+ );
36652
+ }
36653
+ }
35540
36654
  function validateReviewersMeta(data) {
35541
36655
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
35542
36656
  throw new Error("Payload must be a JSON object");
@@ -35574,6 +36688,19 @@ function validateReviewersMeta(data) {
35574
36688
  if (!Array.isArray(r.focus_areas)) {
35575
36689
  throw new Error(`${prefix}.focus_areas must be an array`);
35576
36690
  }
36691
+ if (r.icon !== void 0 && typeof r.icon !== "string") {
36692
+ throw new Error(`${prefix}.icon must be a string if provided (got ${JSON.stringify(r.icon)})`);
36693
+ }
36694
+ if (typeof r.icon !== "string" || r.icon.length === 0) {
36695
+ r.icon = defaultIconFor(r.id, r.tier);
36696
+ }
36697
+ warnIfSuspiciousPersona(`${prefix} ("${r.name}")`, [
36698
+ r.name,
36699
+ r.description,
36700
+ ...Array.isArray(r.focus_areas) ? r.focus_areas : [],
36701
+ r.known_for,
36702
+ r.philosophy
36703
+ ]);
35577
36704
  if (r.known_for !== void 0 && typeof r.known_for !== "string") {
35578
36705
  throw new Error(`${prefix}.known_for must be a string if provided`);
35579
36706
  }
@@ -35586,17 +36713,17 @@ function validateReviewersMeta(data) {
35586
36713
  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) => {
35587
36714
  const targetDir = process.cwd();
35588
36715
  requireOcrSetup(targetDir);
35589
- const ocrDir = join25(targetDir, ".ocr");
36716
+ const ocrDir = join27(targetDir, ".ocr");
35590
36717
  if (!options.stdin) {
35591
36718
  try {
35592
- const reviewersDir = join25(ocrDir, "skills", "references", "reviewers");
35593
- const configPath = join25(ocrDir, "config.yaml");
36719
+ const reviewersDir = join27(ocrDir, "skills", "references", "reviewers");
36720
+ const configPath = join27(ocrDir, "config.yaml");
35594
36721
  const meta = generateReviewersMeta(reviewersDir, configPath);
35595
36722
  if (!meta || meta.reviewers.length === 0) {
35596
36723
  console.error(source_default.yellow("No reviewer files found in .ocr/skills/references/reviewers/"));
35597
36724
  process.exit(1);
35598
36725
  }
35599
- const metaPath = join25(ocrDir, "reviewers-meta.json");
36726
+ const metaPath = join27(ocrDir, "reviewers-meta.json");
35600
36727
  const tmpPath = metaPath + ".tmp";
35601
36728
  writeFileSync9(tmpPath, JSON.stringify(meta, null, 2) + "\n");
35602
36729
  renameSync2(tmpPath, metaPath);
@@ -35626,7 +36753,7 @@ var syncSubcommand2 = new Command("sync").description("Sync reviewers-meta.json
35626
36753
  throw new Error("Invalid JSON on stdin");
35627
36754
  }
35628
36755
  const meta = validateReviewersMeta(parsed);
35629
- const metaPath = join25(ocrDir, "reviewers-meta.json");
36756
+ const metaPath = join27(ocrDir, "reviewers-meta.json");
35630
36757
  const tmpPath = metaPath + ".tmp";
35631
36758
  writeFileSync9(tmpPath, JSON.stringify(meta, null, 2) + "\n");
35632
36759
  renameSync2(tmpPath, metaPath);
@@ -35652,26 +36779,74 @@ var syncSubcommand2 = new Command("sync").description("Sync reviewers-meta.json
35652
36779
  });
35653
36780
  var reviewersCommand = new Command("reviewers").description("Manage OCR reviewer metadata").addCommand(syncSubcommand2);
35654
36781
 
36782
+ // src/commands/host.ts
36783
+ function describeRow(id) {
36784
+ const tool = getToolById(id);
36785
+ const caps = getHostCapabilities(id);
36786
+ return {
36787
+ id,
36788
+ name: tool?.name ?? id,
36789
+ subagentSpawn: caps.subagentSpawn,
36790
+ perTaskModel: caps.perTaskModel,
36791
+ phase4: caps.subagentSpawn ? "parallel-subagents" : "sequential"
36792
+ };
36793
+ }
36794
+ 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) => {
36795
+ if (options.tool) {
36796
+ const id = options.tool.trim().toLowerCase();
36797
+ if (!getToolIds().includes(id)) {
36798
+ console.error(
36799
+ source_default.red(
36800
+ `Error: unknown tool id "${options.tool}". Valid ids: ${getToolIds().join(", ")}`
36801
+ )
36802
+ );
36803
+ process.exit(1);
36804
+ }
36805
+ const row = describeRow(id);
36806
+ if (options.json) {
36807
+ console.log(JSON.stringify(row, null, 2));
36808
+ } else {
36809
+ printRows([row]);
36810
+ }
36811
+ return;
36812
+ }
36813
+ const rows = AI_TOOLS.map((t) => describeRow(t.id));
36814
+ if (options.json) {
36815
+ console.log(JSON.stringify(rows, null, 2));
36816
+ } else {
36817
+ printRows(rows);
36818
+ }
36819
+ });
36820
+ function printRows(rows) {
36821
+ const yn = (v) => v ? source_default.green("yes") : source_default.dim("no");
36822
+ for (const row of rows) {
36823
+ console.log(
36824
+ `${source_default.bold(row.name.padEnd(20))} subagentSpawn=${yn(row.subagentSpawn)} perTaskModel=${yn(row.perTaskModel)} \u2192 ${source_default.cyan(row.phase4)}`
36825
+ );
36826
+ }
36827
+ }
36828
+ var hostCommand = new Command("host").description("Inspect host (AI CLI) capabilities").addCommand(capabilitiesSubcommand);
36829
+
35655
36830
  // src/lib/update-check.ts
35656
36831
  import { homedir } from "node:os";
35657
- import { join as join26 } from "node:path";
35658
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, mkdirSync as mkdirSync7 } from "node:fs";
36832
+ import { join as join28 } from "node:path";
36833
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync10, mkdirSync as mkdirSync8 } from "node:fs";
35659
36834
  var PACKAGE_NAME = "@open-code-review/cli";
35660
36835
  var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
35661
- var CACHE_DIR2 = join26(homedir(), ".ocr");
35662
- var CACHE_FILE = join26(CACHE_DIR2, "update-check.json");
36836
+ var CACHE_DIR2 = join28(homedir(), ".ocr");
36837
+ var CACHE_FILE = join28(CACHE_DIR2, "update-check.json");
35663
36838
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
35664
36839
  var FETCH_TIMEOUT_MS = 3e3;
35665
36840
  function readCache(cacheFile) {
35666
36841
  try {
35667
- return JSON.parse(readFileSync14(cacheFile, "utf-8"));
36842
+ return JSON.parse(readFileSync15(cacheFile, "utf-8"));
35668
36843
  } catch {
35669
36844
  return null;
35670
36845
  }
35671
36846
  }
35672
36847
  function writeCache(cacheFile, cache) {
35673
36848
  try {
35674
- mkdirSync7(join26(cacheFile, ".."), { recursive: true });
36849
+ mkdirSync8(join28(cacheFile, ".."), { recursive: true });
35675
36850
  writeFileSync10(cacheFile, JSON.stringify(cache));
35676
36851
  } catch {
35677
36852
  }
@@ -35692,7 +36867,7 @@ async function checkForUpdate(currentVersion, options) {
35692
36867
  if (process.env.CI || process.env.OCR_NO_UPDATE_CHECK) {
35693
36868
  return null;
35694
36869
  }
35695
- const cacheFile = join26(options?.cacheDir ?? CACHE_DIR2, "update-check.json");
36870
+ const cacheFile = join28(options?.cacheDir ?? CACHE_DIR2, "update-check.json");
35696
36871
  try {
35697
36872
  const cache = readCache(cacheFile);
35698
36873
  if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {
@@ -35751,7 +36926,9 @@ program2.addCommand(reviewCommand);
35751
36926
  program2.addCommand(updateCommand);
35752
36927
  program2.addCommand(dashboardCommand);
35753
36928
  program2.addCommand(doctorCommand);
36929
+ program2.addCommand(dbCommand);
35754
36930
  program2.addCommand(reviewersCommand);
36931
+ program2.addCommand(hostCommand);
35755
36932
  await program2.parseAsync();
35756
36933
  if (subcommand && HUMAN_COMMANDS.has(subcommand)) {
35757
36934
  const drift = checkLocalArtifactVersion(process.cwd(), CLI_VERSION);