@open-code-review/cli 2.1.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 (60) hide show
  1. package/dist/dashboard/client/assets/{_basePickBy-B3ALyupE.js → _basePickBy-BBPb8BJA.js} +1 -1
  2. package/dist/dashboard/client/assets/{_baseUniq-b2RALAWc.js → _baseUniq-CFHdos6T.js} +1 -1
  3. package/dist/dashboard/client/assets/{arc-DcSVvhUd.js → arc-BKGGWA2F.js} +1 -1
  4. package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-BNUlmSCS.js → architectureDiagram-VXUJARFQ-B_ovNjX1.js} +1 -1
  5. package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-BmhiQVwa.js → blockDiagram-VD42YOAC-C2M-avVp.js} +1 -1
  6. package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-jyJ3WOv5.js → c4Diagram-YG6GDRKO-BtOBpAzH.js} +1 -1
  7. package/dist/dashboard/client/assets/channel-rgw7C1e7.js +1 -0
  8. package/dist/dashboard/client/assets/{chunk-4BX2VUAB-x1dQU_s3.js → chunk-4BX2VUAB-Cz2EbHPl.js} +1 -1
  9. package/dist/dashboard/client/assets/{chunk-55IACEB6-CwbsE2XQ.js → chunk-55IACEB6-C8xpXw9G.js} +1 -1
  10. package/dist/dashboard/client/assets/{chunk-B4BG7PRW-BaE7c-ti.js → chunk-B4BG7PRW-BSRfOovX.js} +1 -1
  11. package/dist/dashboard/client/assets/{chunk-DI55MBZ5-Bw5PUaMK.js → chunk-DI55MBZ5-CEUbYQWn.js} +1 -1
  12. package/dist/dashboard/client/assets/{chunk-FMBD7UC4-B7cF6P3s.js → chunk-FMBD7UC4-5xWP6GRj.js} +1 -1
  13. package/dist/dashboard/client/assets/{chunk-QN33PNHL-OY4evNHd.js → chunk-QN33PNHL-DfNCVcy8.js} +1 -1
  14. package/dist/dashboard/client/assets/{chunk-QZHKN3VN-BpjQwIWz.js → chunk-QZHKN3VN--OdToKKu.js} +1 -1
  15. package/dist/dashboard/client/assets/{chunk-TZMSLE5B-D8b_Oq9B.js → chunk-TZMSLE5B-B_0K0Qso.js} +1 -1
  16. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-DTGi7d9X.js +1 -0
  17. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-DTGi7d9X.js +1 -0
  18. package/dist/dashboard/client/assets/clone-Cz7hswqi.js +1 -0
  19. package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-C-sfP8PN.js → cose-bilkent-S5V4N54A-Cc_Dmnxz.js} +1 -1
  20. package/dist/dashboard/client/assets/{dagre-6UL2VRFP-Cqfo0NRg.js → dagre-6UL2VRFP-DaAfvUXU.js} +1 -1
  21. package/dist/dashboard/client/assets/{diagram-PSM6KHXK-BR3ppxqI.js → diagram-PSM6KHXK-7idwN0rC.js} +1 -1
  22. package/dist/dashboard/client/assets/{diagram-QEK2KX5R-Dvcx6x3R.js → diagram-QEK2KX5R-D9j9H13n.js} +1 -1
  23. package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DoyBLnVN.js → diagram-S2PKOQOG-SMF5SB0K.js} +1 -1
  24. package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-hy77l1cL.js → erDiagram-Q2GNP2WA-EVJ4Qa2F.js} +1 -1
  25. package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-Bz0B1rKM.js → flowDiagram-NV44I4VS-tZ7SFE77.js} +1 -1
  26. package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-CLgrZPoC.js → ganttDiagram-JELNMOA3-DFSqguY7.js} +1 -1
  27. package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js → gitGraphDiagram-V2S2FVAM-CqHdP3HE.js} +1 -1
  28. package/dist/dashboard/client/assets/{graph-DDBMM_t2.js → graph-C0XnkNkk.js} +1 -1
  29. package/dist/dashboard/client/assets/{index-Cr9yEo_B.js → index-C3NEq704.js} +133 -138
  30. package/dist/dashboard/client/assets/index-CzxeSSaQ.css +1 -0
  31. package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-Bhn1FmAk.js → infoDiagram-HS3SLOUP-DlXZo9U2.js} +1 -1
  32. package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CzGbjX1y.js → journeyDiagram-XKPGCS4Q-CgC8_7eN.js} +1 -1
  33. package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Da77-WYk.js → kanban-definition-3W4ZIXB7-BMAw_jNp.js} +1 -1
  34. package/dist/dashboard/client/assets/{layout-CVwSB-GS.js → layout-XjM3Q-ka.js} +1 -1
  35. package/dist/dashboard/client/assets/{linear-CTRAc5Jn.js → linear-CMUrrr1X.js} +1 -1
  36. package/dist/dashboard/client/assets/{mermaid-renderer-Bjo170ax.js → mermaid-renderer-D2jYNs7K.js} +4 -4
  37. package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-B55C2odl.js → mindmap-definition-VGOIOE7T-CL4hv-vg.js} +1 -1
  38. package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-5lrQLrSz.js → pieDiagram-ADFJNKIX-DTqv-1h1.js} +1 -1
  39. package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-Bg55gC30.js → quadrantDiagram-AYHSOK5B-BpFlSW9N.js} +1 -1
  40. package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-CyR4YFJY.js → requirementDiagram-UZGBJVZJ-BqYqqXL4.js} +1 -1
  41. package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BVWKr9_-.js → sankeyDiagram-TZEHDZUN-kEI9kntR.js} +1 -1
  42. package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-D0AJg_tE.js → sequenceDiagram-WL72ISMW-Cnu_1j-N.js} +1 -1
  43. package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-BuHpTgim.js → stateDiagram-FKZM4ZOC-BoC-rqoG.js} +1 -1
  44. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-COR3QD3v.js +1 -0
  45. package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-LDhpAmDd.js → timeline-definition-IT6M3QCI-CXMWuzDL.js} +1 -1
  46. package/dist/dashboard/client/assets/{treemap-GDKQZRPO-Dd4gjvUl.js → treemap-GDKQZRPO-o9ZFgpbJ.js} +1 -1
  47. package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-B9RDod39.js → xychartDiagram-PRI3JC2R-CfIuUpeA.js} +1 -1
  48. package/dist/dashboard/client/index.html +2 -2
  49. package/dist/dashboard/server.js +1031 -426
  50. package/dist/index.js +1252 -268
  51. package/dist/lib/db/index.js +485 -24
  52. package/dist/lib/runtime-config.js +29 -13
  53. package/dist/lib/state/index.js +2196 -0
  54. package/package.json +8 -2
  55. package/dist/dashboard/client/assets/channel-D3J8-GF_.js +0 -1
  56. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-tkFUL-1Y.js +0 -1
  57. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-tkFUL-1Y.js +0 -1
  58. package/dist/dashboard/client/assets/clone-CkY5ajLr.js +0 -1
  59. package/dist/dashboard/client/assets/index-Z1pPudAt.css +0 -1
  60. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +0 -1
package/dist/index.js CHANGED
@@ -16017,6 +16017,80 @@ var require_emoji_regex2 = __commonJS({
16017
16017
  }
16018
16018
  });
16019
16019
 
16020
+ // ../shared/platform/src/index.ts
16021
+ import { pathToFileURL } from "node:url";
16022
+ import {
16023
+ execFile,
16024
+ execFileSync,
16025
+ spawn as spawn2
16026
+ } from "node:child_process";
16027
+ import { promisify } from "node:util";
16028
+ async function importModule(absolutePath) {
16029
+ return import(pathToFileURL(absolutePath).href);
16030
+ }
16031
+ function execBinary(binary, args, opts) {
16032
+ return execFileSync(binary, args, {
16033
+ ...opts,
16034
+ shell: isWindows
16035
+ });
16036
+ }
16037
+ 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
+
16020
16094
  // ../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/identity.js
16021
16095
  var require_identity = __commonJS({
16022
16096
  "../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/identity.js"(exports) {
@@ -24044,6 +24118,35 @@ var init_migrations = __esm({
24044
24118
  db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
24045
24119
  db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
24046
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
+ }
24047
24150
  }
24048
24151
  ];
24049
24152
  }
@@ -24172,7 +24275,7 @@ var init_queries = __esm({
24172
24275
 
24173
24276
  // src/lib/db/reconcile.ts
24174
24277
  import { existsSync as existsSync10 } from "node:fs";
24175
- 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";
24176
24279
  function hasTerminalArtifactEvent(db, sessionId, workflowType, currentRound, currentMapRun) {
24177
24280
  const eventType = workflowType === "map" ? "map_completed" : "round_completed";
24178
24281
  const round = workflowType === "map" ? currentMapRun : currentRound;
@@ -24213,7 +24316,7 @@ function hasInFlightDependents(db, sessionId) {
24213
24316
  function resolveSessionDir(ocrDir, sessionDir) {
24214
24317
  if (!sessionDir) return null;
24215
24318
  if (isAbsolute2(sessionDir)) return sessionDir;
24216
- return join12(dirname4(ocrDir), sessionDir);
24319
+ return join12(dirname5(ocrDir), sessionDir);
24217
24320
  }
24218
24321
  function reconcileLegacyState(db, ocrDir, opts = {}) {
24219
24322
  const dryRun = opts.dryRun ?? false;
@@ -24331,7 +24434,7 @@ var init_liveness = __esm({
24331
24434
  });
24332
24435
 
24333
24436
  // src/lib/state/exit-codes.ts
24334
- 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;
24335
24438
  var init_exit_codes = __esm({
24336
24439
  "src/lib/state/exit-codes.ts"() {
24337
24440
  "use strict";
@@ -24356,6 +24459,7 @@ var init_exit_codes = __esm({
24356
24459
  CANCELLED_EXIT_CODE = -2;
24357
24460
  ORPHAN_EXIT_CODE = -3;
24358
24461
  CASCADE_CLOSE_EXIT_CODE = -4;
24462
+ WATCHDOG_DEADLINE_EXIT_CODE = -5;
24359
24463
  }
24360
24464
  });
24361
24465
 
@@ -24738,24 +24842,431 @@ var init_agent_sessions = __esm({
24738
24842
  }
24739
24843
  });
24740
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
+
24741
25252
  // src/lib/db/command-log.ts
24742
- import { appendFileSync, existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync6 } from "node:fs";
24743
- import { dirname as dirname5, join as join13 } from "node:path";
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";
24744
25255
  import { randomUUID as randomUUID2 } from "node:crypto";
24745
25256
  function generateCommandUid() {
24746
25257
  return randomUUID2();
24747
25258
  }
24748
25259
  function cacheDir(ocrDir) {
24749
- return join13(ocrDir, "data", CACHE_DIR);
25260
+ return join14(ocrDir, "data", CACHE_DIR);
24750
25261
  }
24751
25262
  function commandLogPath(ocrDir) {
24752
- return join13(cacheDir(ocrDir), FILENAME);
25263
+ return join14(cacheDir(ocrDir), FILENAME);
24753
25264
  }
24754
25265
  function appendCommandLog(ocrDir, entry) {
24755
25266
  try {
24756
25267
  const filePath = commandLogPath(ocrDir);
24757
- const dir = dirname5(filePath);
24758
- if (!existsSync11(dir)) mkdirSync3(dir, { recursive: true });
25268
+ const dir = dirname7(filePath);
25269
+ if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
24759
25270
  const line = JSON.stringify(entry) + "\n";
24760
25271
  appendFileSync(filePath, line, { encoding: "utf-8" });
24761
25272
  if (approxLineCount >= 0) approxLineCount++;
@@ -24765,7 +25276,7 @@ function appendCommandLog(ocrDir, entry) {
24765
25276
  }
24766
25277
  function readCommandLog(ocrDir) {
24767
25278
  const filePath = commandLogPath(ocrDir);
24768
- if (!existsSync11(filePath)) return [];
25279
+ if (!existsSync12(filePath)) return [];
24769
25280
  const content = readFileSync9(filePath, "utf-8");
24770
25281
  const entries = [];
24771
25282
  for (const line of content.split("\n")) {
@@ -24851,6 +25362,7 @@ __export(db_exports, {
24851
25362
  PID_REUSE_GUARD_MS: () => PID_REUSE_GUARD_MS,
24852
25363
  STATE_EXIT: () => STATE_EXIT,
24853
25364
  StateError: () => StateError,
25365
+ WATCHDOG_DEADLINE_EXIT_CODE: () => WATCHDOG_DEADLINE_EXIT_CODE,
24854
25366
  appendCommandLog: () => appendCommandLog,
24855
25367
  bindVendorSessionIdOpportunistically: () => bindVendorSessionIdOpportunistically,
24856
25368
  bumpAgentSessionHeartbeat: () => bumpAgentSessionHeartbeat,
@@ -24858,10 +25370,12 @@ __export(db_exports, {
24858
25370
  cascadeTerminateExecutions: () => cascadeTerminateExecutions,
24859
25371
  closeAllDatabases: () => closeAllDatabases,
24860
25372
  closeDatabase: () => closeDatabase,
25373
+ collectDbHealth: () => collectDbHealth,
24861
25374
  commandLogPath: () => commandLogPath,
24862
25375
  commitReasonClose: () => commitReasonClose,
24863
25376
  defaultIsAlive: () => defaultIsAlive,
24864
25377
  ensureDatabase: () => ensureDatabase,
25378
+ fixDb: () => fixDb,
24865
25379
  formatUpgradeNotice: () => formatUpgradeNotice,
24866
25380
  generateCommandUid: () => generateCommandUid,
24867
25381
  getAgentSession: () => getAgentSession,
@@ -24873,6 +25387,7 @@ __export(db_exports, {
24873
25387
  getLatestEventId: () => getLatestEventId,
24874
25388
  getSchemaVersion: () => getSchemaVersion,
24875
25389
  getSession: () => getSession,
25390
+ hasInFlightDependents: () => hasInFlightDependents,
24876
25391
  insertAgentSession: () => insertAgentSession,
24877
25392
  insertEvent: () => insertEvent,
24878
25393
  insertSession: () => insertSession,
@@ -24882,7 +25397,11 @@ __export(db_exports, {
24882
25397
  openDatabase: () => openDatabase,
24883
25398
  probeEngine: () => probeEngine,
24884
25399
  probeWrite: () => probeWrite,
25400
+ pruneBackups: () => pruneBackups,
25401
+ pruneDb: () => pruneDb,
24885
25402
  readCommandLog: () => readCommandLog,
25403
+ reapOrphanDbFiles: () => reapOrphanDbFiles,
25404
+ reapStaleExecLogs: () => reapStaleExecLogs,
24886
25405
  reconcileLegacyState: () => reconcileLegacyState,
24887
25406
  recordVendorSessionIdForExecution: () => recordVendorSessionIdForExecution,
24888
25407
  replayCommandLog: () => replayCommandLog,
@@ -24892,31 +25411,34 @@ __export(db_exports, {
24892
25411
  runMigrations: () => runMigrations,
24893
25412
  setAgentSessionStatus: () => setAgentSessionStatus,
24894
25413
  setAgentSessionVendorId: () => setAgentSessionVendorId,
25414
+ snapshotDb: () => snapshotDb,
24895
25415
  sqliteUtcMs: () => sqliteUtcMs,
24896
25416
  sweepStaleAgentSessions: () => sweepStaleAgentSessions,
24897
25417
  sweepStaleSessions: () => sweepStaleSessions,
24898
25418
  updateAgentSession: () => updateAgentSession,
24899
25419
  updateSession: () => updateSession,
24900
- walCheckpointTruncate: () => walCheckpointTruncate
25420
+ vacuumDb: () => vacuumDb,
25421
+ walCheckpointTruncate: () => walCheckpointTruncate,
25422
+ withForeignKeysDisabled: () => withForeignKeysDisabled
24901
25423
  });
24902
25424
  import {
24903
- existsSync as existsSync12,
24904
- mkdirSync as mkdirSync4,
24905
- copyFileSync,
24906
- statSync,
25425
+ existsSync as existsSync13,
25426
+ mkdirSync as mkdirSync5,
25427
+ copyFileSync as copyFileSync2,
25428
+ statSync as statSync2,
24907
25429
  mkdtempSync,
24908
25430
  rmSync
24909
25431
  } from "node:fs";
24910
25432
  import { tmpdir } from "node:os";
24911
- import { dirname as dirname6, join as join14 } from "node:path";
25433
+ import { dirname as dirname8, join as join15 } from "node:path";
24912
25434
  function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
24913
25435
  if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
24914
25436
  const bakPath = `${dbPath}.bak.v${fromVersion}`;
24915
- if (existsSync12(bakPath)) return bakPath;
25437
+ if (existsSync13(bakPath)) return bakPath;
24916
25438
  try {
24917
- if (!existsSync12(dbPath) || statSync(dbPath).size === 0) return null;
25439
+ if (!existsSync13(dbPath) || statSync2(dbPath).size === 0) return null;
24918
25440
  db.pragma("wal_checkpoint(TRUNCATE)");
24919
- copyFileSync(dbPath, bakPath);
25441
+ copyFileSync2(dbPath, bakPath);
24920
25442
  return bakPath;
24921
25443
  } catch {
24922
25444
  return null;
@@ -24949,24 +25471,24 @@ async function openDatabase(dbPath) {
24949
25471
  if (cached) {
24950
25472
  return cached;
24951
25473
  }
24952
- const dir = dirname6(dbPath);
24953
- if (!existsSync12(dir)) {
24954
- mkdirSync4(dir, { recursive: true });
25474
+ const dir = dirname8(dbPath);
25475
+ if (!existsSync13(dir)) {
25476
+ mkdirSync5(dir, { recursive: true });
24955
25477
  }
24956
25478
  const db = openEngine(dbPath);
24957
25479
  connections.set(dbPath, db);
24958
25480
  return db;
24959
25481
  }
24960
25482
  async function getDb(ocrDir) {
24961
- const dbPath = join14(ocrDir, "data", "ocr.db");
25483
+ const dbPath = join15(ocrDir, "data", "ocr.db");
24962
25484
  return openDatabase(dbPath);
24963
25485
  }
24964
25486
  async function ensureDatabase(ocrDir) {
24965
- const dataDir = join14(ocrDir, "data");
24966
- if (!existsSync12(dataDir)) {
24967
- mkdirSync4(dataDir, { recursive: true });
25487
+ const dataDir = join15(ocrDir, "data");
25488
+ if (!existsSync13(dataDir)) {
25489
+ mkdirSync5(dataDir, { recursive: true });
24968
25490
  }
24969
- const dbPath = join14(dataDir, "ocr.db");
25491
+ const dbPath = join15(dataDir, "ocr.db");
24970
25492
  const db = await openDatabase(dbPath);
24971
25493
  let before = 0;
24972
25494
  try {
@@ -24994,7 +25516,7 @@ async function ensureDatabase(ocrDir) {
24994
25516
  return db;
24995
25517
  }
24996
25518
  function walCheckpointTruncate(dbPath) {
24997
- if (!existsSync12(dbPath)) {
25519
+ if (!existsSync13(dbPath)) {
24998
25520
  return "skipped";
24999
25521
  }
25000
25522
  const cached = connections.get(dbPath);
@@ -25036,8 +25558,8 @@ function closeAllDatabases() {
25036
25558
  function probeWrite() {
25037
25559
  let dir;
25038
25560
  try {
25039
- dir = mkdtempSync(join14(tmpdir(), "ocr-probe-"));
25040
- const db = openEngine(join14(dir, "probe.db"));
25561
+ dir = mkdtempSync(join15(tmpdir(), "ocr-probe-"));
25562
+ const db = openEngine(join15(dir, "probe.db"));
25041
25563
  try {
25042
25564
  db.run("CREATE TABLE _probe_write (id INTEGER PRIMARY KEY, v TEXT)");
25043
25565
  db.transaction(() => {
@@ -25083,6 +25605,7 @@ var init_db = __esm({
25083
25605
  init_result_mapper();
25084
25606
  init_engine();
25085
25607
  init_reconcile();
25608
+ init_maintenance();
25086
25609
  init_migrations();
25087
25610
  init_command_log();
25088
25611
  V2_SCHEMA_VERSION = 12;
@@ -28613,6 +29136,8 @@ function ora(options) {
28613
29136
  }
28614
29137
 
28615
29138
  // src/lib/config.ts
29139
+ init_src();
29140
+ init_src();
28616
29141
  var AI_TOOLS = [
28617
29142
  {
28618
29143
  id: "amazon-q",
@@ -28636,7 +29161,9 @@ var AI_TOOLS = [
28636
29161
  configDir: ".claude",
28637
29162
  commandsDir: ".claude/commands",
28638
29163
  skillsDir: ".claude/skills",
28639
- commandStrategy: "subdirectory"
29164
+ commandStrategy: "subdirectory",
29165
+ instructionFiles: [{ path: "CLAUDE.md", format: "markdown" }],
29166
+ vendorBinary: "claude"
28640
29167
  },
28641
29168
  {
28642
29169
  id: "cline",
@@ -28652,7 +29179,9 @@ var AI_TOOLS = [
28652
29179
  configDir: ".codex",
28653
29180
  commandsDir: ".codex/commands",
28654
29181
  skillsDir: ".codex/skills",
28655
- commandStrategy: "subdirectory"
29182
+ commandStrategy: "subdirectory",
29183
+ // Codex reads AGENTS.md natively — no extra instruction file.
29184
+ vendorBinary: "codex"
28656
29185
  },
28657
29186
  {
28658
29187
  id: "continue",
@@ -28676,7 +29205,9 @@ var AI_TOOLS = [
28676
29205
  configDir: ".gemini",
28677
29206
  commandsDir: ".gemini/commands",
28678
29207
  skillsDir: ".gemini/skills",
28679
- commandStrategy: "subdirectory"
29208
+ commandStrategy: "subdirectory",
29209
+ instructionFiles: [{ path: "GEMINI.md", format: "markdown" }],
29210
+ vendorBinary: "gemini"
28680
29211
  },
28681
29212
  {
28682
29213
  id: "github-copilot",
@@ -28684,7 +29215,10 @@ var AI_TOOLS = [
28684
29215
  configDir: ".github",
28685
29216
  commandsDir: ".github/commands",
28686
29217
  skillsDir: ".github/skills",
28687
- commandStrategy: "subdirectory"
29218
+ commandStrategy: "subdirectory",
29219
+ instructionFiles: [
29220
+ { path: ".github/copilot-instructions.md", format: "markdown" }
29221
+ ]
28688
29222
  },
28689
29223
  {
28690
29224
  id: "kilo-code",
@@ -28700,7 +29234,9 @@ var AI_TOOLS = [
28700
29234
  configDir: ".opencode",
28701
29235
  commandsDir: ".opencode/commands",
28702
29236
  skillsDir: ".opencode/skills",
28703
- commandStrategy: "subdirectory"
29237
+ commandStrategy: "subdirectory",
29238
+ // OpenCode reads AGENTS.md natively — no extra instruction file.
29239
+ vendorBinary: "opencode"
28704
29240
  },
28705
29241
  {
28706
29242
  id: "qoder",
@@ -28724,9 +29260,16 @@ var AI_TOOLS = [
28724
29260
  configDir: ".windsurf",
28725
29261
  commandsDir: ".windsurf/workflows",
28726
29262
  skillsDir: ".windsurf/skills",
28727
- commandStrategy: "flat-prefixed"
29263
+ commandStrategy: "flat-prefixed",
29264
+ instructionFiles: [{ path: ".windsurfrules", format: "plaintext" }]
28728
29265
  }
28729
29266
  ];
29267
+ function getToolById(id) {
29268
+ return AI_TOOLS.find((tool) => tool.id === id);
29269
+ }
29270
+ function getHostCapabilities(id) {
29271
+ return hostCapabilitiesFor(id);
29272
+ }
28730
29273
  function getToolIds() {
28731
29274
  return AI_TOOLS.map((tool) => tool.id);
28732
29275
  }
@@ -28787,11 +29330,11 @@ function ensureGitignore(ocrDir) {
28787
29330
  const gitignorePath = join(ocrDir, ".gitignore");
28788
29331
  const block = buildManagedBlock();
28789
29332
  let content = existsSync(gitignorePath) ? readFileSync2(gitignorePath, "utf-8") : "";
28790
- const blockRegex = new RegExp(
29333
+ const blockRegex2 = new RegExp(
28791
29334
  `${escapeRegex(START_MARKER)}[\\s\\S]*?${escapeRegex(END_MARKER)}\\n?`,
28792
29335
  "g"
28793
29336
  );
28794
- if (blockRegex.test(content)) {
29337
+ if (blockRegex2.test(content)) {
28795
29338
  content = content.replace(
28796
29339
  new RegExp(
28797
29340
  `${escapeRegex(START_MARKER)}[\\s\\S]*?${escapeRegex(END_MARKER)}\\n?`,
@@ -28981,6 +29524,7 @@ function resolveTeamComposition(team, override) {
28981
29524
  }
28982
29525
 
28983
29526
  // src/lib/installer.ts
29527
+ init_src();
28984
29528
  var require2 = createRequire(import.meta.url);
28985
29529
  function ensureDir(dir) {
28986
29530
  if (!existsSync3(dir)) {
@@ -29089,27 +29633,6 @@ function installCommandsForTool(tool, commandsSource, targetDir) {
29089
29633
  return false;
29090
29634
  }
29091
29635
  }
29092
- var BUILTIN_ICON_MAP = {
29093
- architect: "blocks",
29094
- fullstack: "layers",
29095
- reliability: "activity",
29096
- "staff-engineer": "compass",
29097
- principal: "crown",
29098
- frontend: "layout",
29099
- backend: "server",
29100
- infrastructure: "cloud",
29101
- performance: "gauge",
29102
- accessibility: "accessibility",
29103
- data: "database",
29104
- devops: "rocket",
29105
- dx: "terminal",
29106
- mobile: "smartphone",
29107
- security: "shield-alert",
29108
- quality: "sparkles",
29109
- testing: "test-tubes",
29110
- ai: "bot",
29111
- "docs-writer": "file-text"
29112
- };
29113
29636
  var HOLISTIC_IDS = /* @__PURE__ */ new Set(["architect", "fullstack", "reliability", "staff-engineer", "principal"]);
29114
29637
  var SPECIALIST_IDS = /* @__PURE__ */ new Set([
29115
29638
  "frontend",
@@ -29212,7 +29735,7 @@ function generateReviewersMeta(reviewersDir, configPath) {
29212
29735
  id,
29213
29736
  name: extractReviewerName(content),
29214
29737
  tier,
29215
- icon: BUILTIN_ICON_MAP[id] ?? (tier === "persona" ? "brain" : "user"),
29738
+ icon: defaultIconFor(id, tier),
29216
29739
  description: extractReviewerDescription(content),
29217
29740
  focus_areas: extractFocusAreas(content),
29218
29741
  is_default: defaultTeamIds.has(id),
@@ -29313,7 +29836,9 @@ function installForTool(tool, targetDir) {
29313
29836
  if (meta) {
29314
29837
  writeFileSync3(metaPath, JSON.stringify(meta, null, 2) + "\n");
29315
29838
  }
29316
- } catch {
29839
+ } catch (err) {
29840
+ const msg = err instanceof Error ? err.message : "unknown error";
29841
+ warnings.push(`Could not generate reviewers-meta.json: ${msg}`);
29317
29842
  }
29318
29843
  const commandsOk = installCommandsForTool(tool, commandsSource, targetDir);
29319
29844
  if (!commandsOk) {
@@ -29347,12 +29872,14 @@ function detectInstalledTools(targetDir, tools) {
29347
29872
  }
29348
29873
 
29349
29874
  // src/lib/injector.ts
29350
- import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
29351
- import { join as join4 } from "node:path";
29352
- var START_MARKER2 = "<!-- OCR:START -->";
29353
- var END_MARKER2 = "<!-- OCR:END -->";
29354
- var OCR_INSTRUCTION_BLOCK = `${START_MARKER2}
29355
- ## Open Code Review Instructions
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
29356
29883
 
29357
29884
  These instructions are for AI assistants handling code review in this project.
29358
29885
 
@@ -29368,37 +29895,95 @@ Use \`.ocr/skills/SKILL.md\` to learn:
29368
29895
  - Available reviewer personas and their focus areas
29369
29896
  - Session management and output format
29370
29897
 
29371
- Keep this managed block so \`ocr init\` can refresh the instructions.
29372
-
29373
- ${END_MARKER2}`;
29374
- 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") {
29375
29916
  try {
29917
+ mkdirSync2(dirname2(filePath), { recursive: true });
29376
29918
  let content = existsSync4(filePath) ? readFileSync5(filePath, "utf-8") : "";
29377
- const regex2 = new RegExp(
29378
- `${escapeRegex2(START_MARKER2)}[\\s\\S]*?${escapeRegex2(END_MARKER2)}\\n?`,
29379
- "g"
29380
- );
29381
- content = content.replace(regex2, "");
29919
+ content = content.replace(blockRegex(format), "");
29382
29920
  content = content.trim();
29383
29921
  if (content.length > 0) {
29384
29922
  content += "\n\n";
29385
29923
  }
29386
- content += OCR_INSTRUCTION_BLOCK + "\n";
29924
+ content += buildBlock(format) + "\n";
29387
29925
  writeFileSync4(filePath, content);
29388
29926
  return true;
29389
29927
  } catch {
29390
29928
  return false;
29391
29929
  }
29392
29930
  }
29393
- function escapeRegex2(str) {
29394
- 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()];
29940
+ }
29941
+ function plannedInstructionFiles(selectedTools) {
29942
+ return resolveTargets(selectedTools).map((t) => t.path);
29395
29943
  }
29396
- function injectIntoProjectFiles(targetDir) {
29397
- const agentsMdPath = join4(targetDir, "AGENTS.md");
29398
- const claudeMdPath = join4(targetDir, "CLAUDE.md");
29399
- const agentsMd = injectOcrInstructions(agentsMdPath);
29400
- const claudeMd = injectOcrInstructions(claudeMdPath);
29401
- return { agentsMd, claudeMd };
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
+ );
29402
29987
  }
29403
29988
 
29404
29989
  // src/lib/banner.ts
@@ -29501,29 +30086,10 @@ ${hint}
29501
30086
  }
29502
30087
 
29503
30088
  // src/lib/version.ts
29504
- var CLI_VERSION = true ? "2.1.0" : createRequire(import.meta.url)("../../package.json").version;
29505
-
29506
- // ../shared/platform/src/index.ts
29507
- import { pathToFileURL } from "node:url";
29508
- import {
29509
- execFile,
29510
- execFileSync,
29511
- spawn as spawn2
29512
- } from "node:child_process";
29513
- import { promisify } from "node:util";
29514
- var execFilePromise = promisify(execFile);
29515
- var isWindows = process.platform === "win32";
29516
- async function importModule(absolutePath) {
29517
- return import(pathToFileURL(absolutePath).href);
29518
- }
29519
- function execBinary(binary, args, opts) {
29520
- return execFileSync(binary, args, {
29521
- ...opts,
29522
- shell: isWindows
29523
- });
29524
- }
30089
+ var CLI_VERSION = true ? "2.2.0" : createRequire(import.meta.url)("../../package.json").version;
29525
30090
 
29526
30091
  // src/lib/deps.ts
30092
+ init_src();
29527
30093
  var CATEGORY_ORDER = ["core", "ai-cli", "github"];
29528
30094
  var CATEGORY_INFO = {
29529
30095
  core: { label: "Core", hint: "" },
@@ -29695,7 +30261,7 @@ function printCapabilities(result) {
29695
30261
  }
29696
30262
 
29697
30263
  // src/commands/init.ts
29698
- var initCommand = new Command("init").description("Set up OCR for AI coding environments").option("-t, --tools <tools>", 'Comma-separated tool IDs or "all"').option("--no-inject", "Skip injecting instructions into AGENTS.md/CLAUDE.md").action(async (options) => {
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) => {
29699
30265
  printBanner();
29700
30266
  const depResult = checkDependencies();
29701
30267
  printDepChecks(depResult);
@@ -29786,17 +30352,19 @@ var initCommand = new Command("init").description("Set up OCR for AI coding envi
29786
30352
  const injectSpinner = ora(
29787
30353
  "Injecting OCR instructions into project files..."
29788
30354
  ).start();
29789
- const injectResults = injectIntoProjectFiles(targetDir);
30355
+ const installedTools = successful.map((r) => r.tool);
30356
+ const injectResults = injectIntoProjectFiles(targetDir, installedTools);
29790
30357
  injectSpinner.stop();
29791
- if (injectResults.agentsMd || injectResults.claudeMd) {
30358
+ if (injectResults.written.length > 0) {
29792
30359
  console.log(source_default.green("\u2713 OCR instructions injected"));
29793
- if (injectResults.agentsMd) {
29794
- console.log(` ${source_default.green("\u2713")} AGENTS.md`);
29795
- }
29796
- if (injectResults.claudeMd) {
29797
- console.log(` ${source_default.green("\u2713")} CLAUDE.md`);
30360
+ for (const path2 of injectResults.written) {
30361
+ console.log(` ${source_default.green("\u2713")} ${path2}`);
29798
30362
  }
29799
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
+ }
29800
30368
  }
29801
30369
  console.log();
29802
30370
  console.log(source_default.bold("Next steps:"));
@@ -29984,10 +30552,10 @@ var ReaddirpStream = class extends Readable {
29984
30552
  }
29985
30553
  async _formatEntry(dirent, path2) {
29986
30554
  let entry;
29987
- const basename8 = this._isDirent ? dirent.name : dirent;
30555
+ const basename9 = this._isDirent ? dirent.name : dirent;
29988
30556
  try {
29989
- const fullPath = presolve(pjoin(path2, basename8));
29990
- 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 };
29991
30559
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
29992
30560
  } catch (err) {
29993
30561
  this._onError(err);
@@ -30526,9 +31094,9 @@ var NodeFsHandler = class {
30526
31094
  _watchWithNodeFs(path2, listener) {
30527
31095
  const opts = this.fsw.options;
30528
31096
  const directory = sysPath.dirname(path2);
30529
- const basename8 = sysPath.basename(path2);
31097
+ const basename9 = sysPath.basename(path2);
30530
31098
  const parent = this.fsw._getWatchedDir(directory);
30531
- parent.add(basename8);
31099
+ parent.add(basename9);
30532
31100
  const absolutePath = sysPath.resolve(path2);
30533
31101
  const options = {
30534
31102
  persistent: opts.persistent
@@ -30538,7 +31106,7 @@ var NodeFsHandler = class {
30538
31106
  let closer;
30539
31107
  if (opts.usePolling) {
30540
31108
  const enableBin = opts.interval !== opts.binaryInterval;
30541
- options.interval = enableBin && isBinaryPath(basename8) ? opts.binaryInterval : opts.interval;
31109
+ options.interval = enableBin && isBinaryPath(basename9) ? opts.binaryInterval : opts.interval;
30542
31110
  closer = setFsWatchFileListener(path2, absolutePath, options, {
30543
31111
  listener,
30544
31112
  rawEmitter: this.fsw._emitRaw
@@ -30560,11 +31128,11 @@ var NodeFsHandler = class {
30560
31128
  if (this.fsw.closed) {
30561
31129
  return;
30562
31130
  }
30563
- const dirname8 = sysPath.dirname(file);
30564
- const basename8 = sysPath.basename(file);
30565
- 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);
30566
31134
  let prevStats = stats;
30567
- if (parent.has(basename8))
31135
+ if (parent.has(basename9))
30568
31136
  return;
30569
31137
  const listener = async (path2, newStats) => {
30570
31138
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
@@ -30589,9 +31157,9 @@ var NodeFsHandler = class {
30589
31157
  prevStats = newStats2;
30590
31158
  }
30591
31159
  } catch (error) {
30592
- this.fsw._remove(dirname8, basename8);
31160
+ this.fsw._remove(dirname10, basename9);
30593
31161
  }
30594
- } else if (parent.has(basename8)) {
31162
+ } else if (parent.has(basename9)) {
30595
31163
  const at = newStats.atimeMs;
30596
31164
  const mt = newStats.mtimeMs;
30597
31165
  if (!at || at <= mt || mt !== prevStats.mtimeMs) {
@@ -31522,8 +32090,8 @@ function watch(paths, options = {}) {
31522
32090
  }
31523
32091
 
31524
32092
  // src/commands/progress.ts
31525
- import { existsSync as existsSync13, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
31526
- import { join as join15, basename as basename7 } from "node:path";
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";
31527
32095
 
31528
32096
  // ../../node_modules/.pnpm/log-update@7.0.2/node_modules/log-update/index.js
31529
32097
  import process12 from "node:process";
@@ -32391,7 +32959,7 @@ var log_update_default = logUpdate;
32391
32959
  var logUpdateStderr = createLogUpdate(process12.stderr);
32392
32960
 
32393
32961
  // src/lib/guards.ts
32394
- import { existsSync as existsSync6, mkdirSync as mkdirSync2 } from "node:fs";
32962
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "node:fs";
32395
32963
  import { join as join8 } from "node:path";
32396
32964
  function checkOcrSetup(targetDir) {
32397
32965
  const ocrDir = join8(targetDir, ".ocr");
@@ -32437,7 +33005,7 @@ function requireOcrSetup(targetDir) {
32437
33005
  function ensureSessionsDir(targetDir) {
32438
33006
  const sessionsDir = join8(targetDir, ".ocr", "sessions");
32439
33007
  if (!existsSync6(sessionsDir)) {
32440
- mkdirSync2(sessionsDir, { recursive: true });
33008
+ mkdirSync3(sessionsDir, { recursive: true });
32441
33009
  }
32442
33010
  return sessionsDir;
32443
33011
  }
@@ -33159,15 +33727,15 @@ function debounce(fn, delay) {
33159
33727
  };
33160
33728
  }
33161
33729
  function findLatestActiveSession(sessionsDir) {
33162
- if (!existsSync13(sessionsDir)) {
33730
+ if (!existsSync14(sessionsDir)) {
33163
33731
  return null;
33164
33732
  }
33165
- const sessions = readdirSync5(sessionsDir).filter((name) => {
33166
- const sessionPath = join15(sessionsDir, name);
33167
- return statSync2(sessionPath).isDirectory();
33733
+ const sessions = readdirSync6(sessionsDir).filter((name) => {
33734
+ const sessionPath = join16(sessionsDir, name);
33735
+ return statSync3(sessionPath).isDirectory();
33168
33736
  }).sort().reverse();
33169
33737
  for (const session of sessions) {
33170
- const sessionPath = join15(sessionsDir, session);
33738
+ const sessionPath = join16(sessionsDir, session);
33171
33739
  if (isSessionActive(sessionPath)) {
33172
33740
  return session;
33173
33741
  }
@@ -33182,8 +33750,8 @@ function getStrategyForSession(sessionPath, explicitWorkflow) {
33182
33750
  return getStrategy(workflowType) ?? null;
33183
33751
  }
33184
33752
  async function initProgressDb(ocrDir) {
33185
- const dbPath = join15(ocrDir, "data", "ocr.db");
33186
- if (!existsSync13(dbPath)) {
33753
+ const dbPath = join16(ocrDir, "data", "ocr.db");
33754
+ if (!existsSync14(dbPath)) {
33187
33755
  return;
33188
33756
  }
33189
33757
  try {
@@ -33208,11 +33776,11 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33208
33776
  const targetDir = process.cwd();
33209
33777
  requireOcrSetup(targetDir);
33210
33778
  const sessionsDir = ensureSessionsDir(targetDir);
33211
- const ocrDir = join15(targetDir, ".ocr");
33779
+ const ocrDir = join16(targetDir, ".ocr");
33212
33780
  await initProgressDb(ocrDir);
33213
33781
  if (options.session) {
33214
- const sessionPath = join15(sessionsDir, options.session);
33215
- if (!existsSync13(sessionPath)) {
33782
+ const sessionPath = join16(sessionsDir, options.session);
33783
+ if (!existsSync14(sessionPath)) {
33216
33784
  console.error(source_default.red(`Session not found: ${options.session}`));
33217
33785
  process.exit(1);
33218
33786
  }
@@ -33273,7 +33841,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33273
33841
  return;
33274
33842
  }
33275
33843
  let currentSession = findLatestActiveSession(sessionsDir);
33276
- let currentSessionPath = currentSession ? join15(sessionsDir, currentSession) : null;
33844
+ let currentSessionPath = currentSession ? join16(sessionsDir, currentSession) : null;
33277
33845
  let sessionWatcher = null;
33278
33846
  const preservedStartTimes = {
33279
33847
  review: void 0,
@@ -33281,11 +33849,11 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33281
33849
  };
33282
33850
  let currentStrategy = null;
33283
33851
  const updateDisplayImpl = () => {
33284
- if (!currentSessionPath || !existsSync13(currentSessionPath) || !isSessionActive(currentSessionPath)) {
33852
+ if (!currentSessionPath || !existsSync14(currentSessionPath) || !isSessionActive(currentSessionPath)) {
33285
33853
  const latestActive = findLatestActiveSession(sessionsDir);
33286
33854
  if (latestActive && latestActive !== currentSession) {
33287
33855
  currentSession = latestActive;
33288
- currentSessionPath = join15(sessionsDir, latestActive);
33856
+ currentSessionPath = join16(sessionsDir, latestActive);
33289
33857
  preservedStartTimes.review = void 0;
33290
33858
  preservedStartTimes.map = void 0;
33291
33859
  currentStrategy = null;
@@ -33298,7 +33866,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33298
33866
  currentStrategy = null;
33299
33867
  }
33300
33868
  }
33301
- if (currentSessionPath && existsSync13(currentSessionPath)) {
33869
+ if (currentSessionPath && existsSync14(currentSessionPath)) {
33302
33870
  if (!options.workflow) {
33303
33871
  const activeWorkflows = detectActiveWorkflows(currentSessionPath);
33304
33872
  if (activeWorkflows.length > 1) {
@@ -33352,17 +33920,17 @@ var progressCommand = new Command("progress").description("Watch real-time progr
33352
33920
  watchSession(currentSessionPath);
33353
33921
  }
33354
33922
  const timerInterval = setInterval(updateDisplay, 1e3);
33355
- const watchDir = existsSync13(ocrDir) ? ocrDir : targetDir;
33923
+ const watchDir = existsSync14(ocrDir) ? ocrDir : targetDir;
33356
33924
  const dirWatcher = watch(watchDir, {
33357
33925
  persistent: true,
33358
33926
  ignoreInitial: true,
33359
33927
  depth: 3
33360
33928
  });
33361
33929
  dirWatcher.on("addDir", (dirPath) => {
33362
- const parentDir = join15(dirPath, "..");
33363
- 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"));
33364
33932
  if (isDirectChild && !dirPath.endsWith("sessions")) {
33365
- const newSession = basename7(dirPath);
33933
+ const newSession = basename8(dirPath);
33366
33934
  currentSession = newSession;
33367
33935
  currentSessionPath = dirPath;
33368
33936
  preservedStartTimes.review = void 0;
@@ -33401,7 +33969,7 @@ function renderGenericWaiting() {
33401
33969
  }
33402
33970
  function renderCombinedProgress(sessionPath, preservedStartTimes, ocrDir) {
33403
33971
  const lines = [];
33404
- const session = basename7(sessionPath);
33972
+ const session = basename8(sessionPath);
33405
33973
  lines.push("");
33406
33974
  lines.push(
33407
33975
  source_default.bold.white(" Open Code Review") + source_default.yellow(" \xB7 Parallel Workflows")
@@ -33462,21 +34030,21 @@ function renderCombinedProgress(sessionPath, preservedStartTimes, ocrDir) {
33462
34030
  }
33463
34031
 
33464
34032
  // src/commands/state.ts
33465
- import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync11 } from "node:fs";
33466
- import { join as join17 } from "node:path";
34033
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11 } from "node:fs";
34034
+ import { join as join18 } from "node:path";
33467
34035
 
33468
34036
  // src/lib/state/index.ts
33469
34037
  init_db();
33470
34038
  init_exit_codes();
33471
34039
  import {
33472
- existsSync as existsSync14,
33473
- mkdirSync as mkdirSync5,
33474
- readdirSync as readdirSync6,
34040
+ existsSync as existsSync15,
34041
+ mkdirSync as mkdirSync6,
34042
+ readdirSync as readdirSync7,
33475
34043
  readFileSync as readFileSync10,
33476
- statSync as statSync3,
34044
+ statSync as statSync4,
33477
34045
  writeFileSync as writeFileSync7
33478
34046
  } from "node:fs";
33479
- import { join as join16 } from "node:path";
34047
+ import { join as join17 } from "node:path";
33480
34048
 
33481
34049
  // src/lib/state/phase-graph.ts
33482
34050
  init_exit_codes();
@@ -33784,9 +34352,9 @@ function deriveNextRound(db, sessionId, fallbackRound) {
33784
34352
  }
33785
34353
  function hasArtifacts(dir) {
33786
34354
  try {
33787
- for (const entry of readdirSync6(dir, { withFileTypes: true })) {
34355
+ for (const entry of readdirSync7(dir, { withFileTypes: true })) {
33788
34356
  if (entry.isDirectory()) {
33789
- if (hasArtifacts(join16(dir, entry.name))) return true;
34357
+ if (hasArtifacts(join17(dir, entry.name))) return true;
33790
34358
  } else if (/\.(md|json)$/.test(entry.name)) {
33791
34359
  return true;
33792
34360
  }
@@ -33797,7 +34365,7 @@ function hasArtifacts(dir) {
33797
34365
  }
33798
34366
  function readJsonFromSource(params) {
33799
34367
  if (params.source === "file") {
33800
- if (!existsSync14(params.filePath)) {
34368
+ if (!existsSync15(params.filePath)) {
33801
34369
  throw new StateError(STATE_EXIT.NOT_FOUND, `File not found: ${params.filePath}`);
33802
34370
  }
33803
34371
  return readFileSync10(params.filePath, "utf-8");
@@ -34124,7 +34692,7 @@ async function stateCompleteRound(params) {
34124
34692
  }
34125
34693
  const resolved = resolveSession(db, params.sessionId);
34126
34694
  const roundNumber = params.round ?? resolved.current_round;
34127
- const roundMetaPath = join16(
34695
+ const roundMetaPath = join17(
34128
34696
  resolved.session_dir,
34129
34697
  "rounds",
34130
34698
  `round-${roundNumber}`,
@@ -34145,8 +34713,8 @@ async function stateCompleteRound(params) {
34145
34713
  );
34146
34714
  }
34147
34715
  if (params.requireFinal) {
34148
- const finalPath = join16(resolved.session_dir, "rounds", `round-${roundNumber}`, "final.md");
34149
- if (!existsSync14(finalPath)) {
34716
+ const finalPath = join17(resolved.session_dir, "rounds", `round-${roundNumber}`, "final.md");
34717
+ if (!existsSync15(finalPath)) {
34150
34718
  throw new StateError(
34151
34719
  STATE_EXIT.INVARIANT_UNMET,
34152
34720
  `Cannot complete round: --require-final set but ${finalPath} is missing.`
@@ -34155,8 +34723,8 @@ async function stateCompleteRound(params) {
34155
34723
  }
34156
34724
  let metaPath;
34157
34725
  if (params.source === "stdin") {
34158
- const roundDir = join16(resolved.session_dir, "rounds", `round-${roundNumber}`);
34159
- mkdirSync5(roundDir, { recursive: true });
34726
+ const roundDir = join17(resolved.session_dir, "rounds", `round-${roundNumber}`);
34727
+ mkdirSync6(roundDir, { recursive: true });
34160
34728
  metaPath = roundMetaPath;
34161
34729
  writeFileSync7(metaPath, JSON.stringify(meta, null, 2));
34162
34730
  }
@@ -34210,7 +34778,7 @@ async function stateCompleteMap(params) {
34210
34778
  }
34211
34779
  const resolved = resolveSession(db, params.sessionId);
34212
34780
  const mapRunNumber = params.mapRun ?? resolved.current_map_run;
34213
- const mapMetaPath = join16(
34781
+ const mapMetaPath = join17(
34214
34782
  resolved.session_dir,
34215
34783
  "map",
34216
34784
  "runs",
@@ -34233,8 +34801,8 @@ async function stateCompleteMap(params) {
34233
34801
  }
34234
34802
  let metaPath;
34235
34803
  if (params.source === "stdin") {
34236
- const runDir = join16(resolved.session_dir, "map", "runs", `run-${mapRunNumber}`);
34237
- mkdirSync5(runDir, { recursive: true });
34804
+ const runDir = join17(resolved.session_dir, "map", "runs", `run-${mapRunNumber}`);
34805
+ mkdirSync6(runDir, { recursive: true });
34238
34806
  metaPath = mapMetaPath;
34239
34807
  writeFileSync7(metaPath, JSON.stringify(meta, null, 2));
34240
34808
  }
@@ -34319,17 +34887,17 @@ async function stateStatus(ocrDir, sessionId) {
34319
34887
  }
34320
34888
  async function stateSync(ocrDir) {
34321
34889
  const db = await ensureDatabase(ocrDir);
34322
- const sessionsRoot = join16(ocrDir, "sessions");
34323
- if (!existsSync14(sessionsRoot)) {
34890
+ const sessionsRoot = join17(ocrDir, "sessions");
34891
+ if (!existsSync15(sessionsRoot)) {
34324
34892
  return 0;
34325
34893
  }
34326
- const entries = readdirSync6(sessionsRoot).filter((name) => {
34327
- const fullPath = join16(sessionsRoot, name);
34328
- return statSync3(fullPath).isDirectory();
34894
+ const entries = readdirSync7(sessionsRoot).filter((name) => {
34895
+ const fullPath = join17(sessionsRoot, name);
34896
+ return statSync4(fullPath).isDirectory();
34329
34897
  });
34330
34898
  let synced = 0;
34331
34899
  for (const dirName of entries) {
34332
- const dirPath = join16(sessionsRoot, dirName);
34900
+ const dirPath = join17(sessionsRoot, dirName);
34333
34901
  const existing = getSession(db, dirName);
34334
34902
  if (existing) {
34335
34903
  continue;
@@ -34337,8 +34905,8 @@ async function stateSync(ocrDir) {
34337
34905
  if (!hasArtifacts(dirPath)) {
34338
34906
  continue;
34339
34907
  }
34340
- const hasRoundsDir = existsSync14(join16(dirPath, "rounds"));
34341
- const hasMapDir = existsSync14(join16(dirPath, "map"));
34908
+ const hasRoundsDir = existsSync15(join17(dirPath, "rounds"));
34909
+ const hasMapDir = existsSync15(join17(dirPath, "map"));
34342
34910
  const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
34343
34911
  const branchMatch = dirName.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
34344
34912
  const branch = branchMatch?.[1] ?? dirName;
@@ -34347,14 +34915,14 @@ async function stateSync(ocrDir) {
34347
34915
  let inferredRound = 1;
34348
34916
  let inferredMapRun = 1;
34349
34917
  if (workflowType === "review") {
34350
- const roundsDir = join16(dirPath, "rounds");
34351
- if (existsSync14(roundsDir)) {
34352
- const roundDirs = readdirSync6(roundsDir).filter((d) => /^round-\d+$/.test(d)).map((d) => parseInt(d.replace("round-", ""), 10)).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
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);
34353
34921
  const latestRoundNum = roundDirs[roundDirs.length - 1];
34354
34922
  if (latestRoundNum !== void 0) {
34355
34923
  inferredRound = latestRoundNum;
34356
- if (existsSync14(
34357
- join16(roundsDir, `round-${latestRoundNum}`, "final.md")
34924
+ if (existsSync15(
34925
+ join17(roundsDir, `round-${latestRoundNum}`, "final.md")
34358
34926
  )) {
34359
34927
  inferredPhase = "complete";
34360
34928
  inferredPhaseNumber = 8;
@@ -34362,13 +34930,13 @@ async function stateSync(ocrDir) {
34362
34930
  }
34363
34931
  }
34364
34932
  } else if (workflowType === "map") {
34365
- const runsDir = join16(dirPath, "map", "runs");
34366
- if (existsSync14(runsDir)) {
34367
- const runDirs = readdirSync6(runsDir).filter((d) => /^run-\d+$/.test(d)).map((d) => parseInt(d.replace("run-", ""), 10)).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
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);
34368
34936
  const latestRunNum = runDirs[runDirs.length - 1];
34369
34937
  if (latestRunNum !== void 0) {
34370
34938
  inferredMapRun = latestRunNum;
34371
- if (existsSync14(join16(runsDir, `run-${latestRunNum}`, "map.md"))) {
34939
+ if (existsSync15(join17(runsDir, `run-${latestRunNum}`, "map.md"))) {
34372
34940
  inferredPhase = "complete";
34373
34941
  inferredPhaseNumber = 6;
34374
34942
  }
@@ -34406,7 +34974,7 @@ init_command_log();
34406
34974
  init_db();
34407
34975
  init_db();
34408
34976
  function readDashboardSpawnMarker(ocrDir) {
34409
- const path2 = join17(ocrDir, "data", "dashboard-active-spawn.json");
34977
+ const path2 = join18(ocrDir, "data", "dashboard-active-spawn.json");
34410
34978
  let raw;
34411
34979
  try {
34412
34980
  raw = readFileSync11(path2, "utf-8");
@@ -34471,7 +35039,7 @@ async function linkDashboardInvocation(ocrDir, sessionId, explicitUid, label) {
34471
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) => {
34472
35040
  const targetDir = process.cwd();
34473
35041
  requireOcrSetup(targetDir);
34474
- const ocrDir = join17(targetDir, ".ocr");
35042
+ const ocrDir = join18(targetDir, ".ocr");
34475
35043
  try {
34476
35044
  const result = await stateShow(ocrDir, options.sessionId);
34477
35045
  if (!result) {
@@ -34540,7 +35108,7 @@ var showSubcommand = new Command("show").description("Show current session state
34540
35108
  var syncSubcommand = new Command("sync").description("Rebuild session state from filesystem artifacts").action(async () => {
34541
35109
  const targetDir = process.cwd();
34542
35110
  requireOcrSetup(targetDir);
34543
- const ocrDir = join17(targetDir, ".ocr");
35111
+ const ocrDir = join18(targetDir, ".ocr");
34544
35112
  try {
34545
35113
  const synced = await stateSync(ocrDir);
34546
35114
  console.log(`Synced ${synced} session${synced !== 1 ? "s" : ""} from filesystem.`);
@@ -34567,7 +35135,7 @@ var reconcileSubcommand = new Command("reconcile").description(
34567
35135
  ).option("--dry-run", "Print the repair plan without writing anything").option("--json", "Output the result as JSON").action(async (options) => {
34568
35136
  const targetDir = process.cwd();
34569
35137
  requireOcrSetup(targetDir);
34570
- const ocrDir = join17(targetDir, ".ocr");
35138
+ const ocrDir = join18(targetDir, ".ocr");
34571
35139
  try {
34572
35140
  const db = await ensureDatabase(ocrDir);
34573
35141
  const result = reconcileLegacyState(db, ocrDir, { dryRun: options.dryRun });
@@ -34626,9 +35194,9 @@ var beginSubcommand = new Command("begin").description("Start or resume a workfl
34626
35194
  async (options) => {
34627
35195
  const targetDir = process.cwd();
34628
35196
  requireOcrSetup(targetDir);
34629
- const ocrDir = join17(targetDir, ".ocr");
34630
- const sessionDir = options.sessionDir ?? join17(ocrDir, "sessions", options.sessionId);
34631
- if (!existsSync15(sessionDir)) mkdirSync6(sessionDir, { recursive: true });
35197
+ const ocrDir = join18(targetDir, ".ocr");
35198
+ const sessionDir = options.sessionDir ?? join18(ocrDir, "sessions", options.sessionId);
35199
+ if (!existsSync16(sessionDir)) mkdirSync7(sessionDir, { recursive: true });
34632
35200
  try {
34633
35201
  const result = await stateBegin({
34634
35202
  sessionId: options.sessionId,
@@ -34650,7 +35218,7 @@ var advanceSubcommand = new Command("advance").description("Advance the workflow
34650
35218
  async (options) => {
34651
35219
  const targetDir = process.cwd();
34652
35220
  requireOcrSetup(targetDir);
34653
- const ocrDir = join17(targetDir, ".ocr");
35221
+ const ocrDir = join18(targetDir, ".ocr");
34654
35222
  try {
34655
35223
  const { id: sessionId } = await resolveActiveSession(ocrDir, options.sessionId);
34656
35224
  await stateAdvance({
@@ -34670,7 +35238,7 @@ var completeRoundSubcommand = new Command("complete-round").description("Atomica
34670
35238
  async (options) => {
34671
35239
  const targetDir = process.cwd();
34672
35240
  requireOcrSetup(targetDir);
34673
- const ocrDir = join17(targetDir, ".ocr");
35241
+ const ocrDir = join18(targetDir, ".ocr");
34674
35242
  try {
34675
35243
  const base = options.stdin ? { source: "stdin", data: await readStdin() } : options.file ? { source: "file", filePath: options.file } : (() => {
34676
35244
  throw new StateError(STATE_EXIT.USAGE, "Provide --stdin or --file with round metadata");
@@ -34694,7 +35262,7 @@ var completeMapSubcommand = new Command("complete-map").description("Atomically
34694
35262
  async (options) => {
34695
35263
  const targetDir = process.cwd();
34696
35264
  requireOcrSetup(targetDir);
34697
- const ocrDir = join17(targetDir, ".ocr");
35265
+ const ocrDir = join18(targetDir, ".ocr");
34698
35266
  try {
34699
35267
  const base = options.stdin ? { source: "stdin", data: await readStdin() } : options.file ? { source: "file", filePath: options.file } : (() => {
34700
35268
  throw new StateError(STATE_EXIT.USAGE, "Provide --stdin or --file with map metadata");
@@ -34716,7 +35284,7 @@ var completeMapSubcommand = new Command("complete-map").description("Atomically
34716
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) => {
34717
35285
  const targetDir = process.cwd();
34718
35286
  requireOcrSetup(targetDir);
34719
- const ocrDir = join17(targetDir, ".ocr");
35287
+ const ocrDir = join18(targetDir, ".ocr");
34720
35288
  try {
34721
35289
  const { id: sessionId } = await resolveActiveSession(ocrDir, options.sessionId);
34722
35290
  await stateClose({ sessionId, ocrDir, abort: options.abort });
@@ -34728,7 +35296,7 @@ var finishSubcommand = new Command("finish").description("Close a workflow (refu
34728
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) => {
34729
35297
  const targetDir = process.cwd();
34730
35298
  requireOcrSetup(targetDir);
34731
- const ocrDir = join17(targetDir, ".ocr");
35299
+ const ocrDir = join18(targetDir, ".ocr");
34732
35300
  try {
34733
35301
  const result = await stateStatus(ocrDir, options.sessionId);
34734
35302
  if (options.json) {
@@ -34757,44 +35325,50 @@ var stateCommand = new Command("state").description("Manage OCR session state").
34757
35325
 
34758
35326
  // src/commands/session.ts
34759
35327
  import { randomUUID as randomUUID3 } from "node:crypto";
34760
- import { join as join19 } from "node:path";
35328
+ import { join as join20 } from "node:path";
34761
35329
  init_db();
34762
35330
 
34763
35331
  // src/lib/runtime-config.ts
34764
- import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
34765
- 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";
34766
35334
  var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
34767
- function getAgentHeartbeatSeconds(ocrDir) {
34768
- const configPath = join18(ocrDir, "config.yaml");
34769
- if (!existsSync16(configPath)) {
34770
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
34771
- }
35335
+ function readRuntimePositiveInt(ocrDir, key, defaultValue) {
35336
+ const configPath = join19(ocrDir, "config.yaml");
35337
+ if (!existsSync17(configPath)) return defaultValue;
34772
35338
  let content;
34773
35339
  try {
34774
35340
  content = readFileSync12(configPath, "utf-8");
34775
35341
  } catch {
34776
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
35342
+ return defaultValue;
34777
35343
  }
34778
35344
  const blockMatch = content.match(
34779
- /^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
+ )
34780
35349
  );
34781
35350
  const inlineMatch = content.match(
34782
- /^runtime:\s*\{[^}]*\bagent_heartbeat_seconds:\s*([^\s,}]+)/m
35351
+ new RegExp(String.raw`^runtime:\s*\{[^}]*\b${key}:\s*([^\s,}]+)`, "m")
34783
35352
  );
34784
35353
  const raw = blockMatch?.[1] ?? inlineMatch?.[1];
34785
- if (!raw) {
34786
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
34787
- }
35354
+ if (!raw) return defaultValue;
34788
35355
  const parsed = Number(raw);
34789
35356
  if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
34790
35357
  process.stderr.write(
34791
- `[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}.
34792
35359
  `
34793
35360
  );
34794
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
35361
+ return defaultValue;
34795
35362
  }
34796
35363
  return parsed;
34797
35364
  }
35365
+ function getAgentHeartbeatSeconds(ocrDir) {
35366
+ return readRuntimePositiveInt(
35367
+ ocrDir,
35368
+ "agent_heartbeat_seconds",
35369
+ DEFAULT_AGENT_HEARTBEAT_SECONDS
35370
+ );
35371
+ }
34798
35372
 
34799
35373
  // src/commands/session.ts
34800
35374
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
@@ -34810,7 +35384,7 @@ function fail(message) {
34810
35384
  async function setup() {
34811
35385
  const targetDir = process.cwd();
34812
35386
  requireOcrSetup(targetDir);
34813
- const ocrDir = join19(targetDir, ".ocr");
35387
+ const ocrDir = join20(targetDir, ".ocr");
34814
35388
  return { ocrDir };
34815
35389
  }
34816
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(
@@ -34945,6 +35519,7 @@ var listSubcommand = new Command("list").description("List agent sessions for a
34945
35519
  var sessionCommand = new Command("session").description("Manage agent-CLI session lifecycle journal").addCommand(startInstanceSubcommand).addCommand(bindVendorIdSubcommand).addCommand(beatSubcommand).addCommand(endInstanceSubcommand).addCommand(listSubcommand);
34946
35520
 
34947
35521
  // src/lib/models.ts
35522
+ init_src();
34948
35523
  var BUNDLED_CLAUDE_MODELS = [
34949
35524
  { id: "claude-opus-4-7", displayName: "Claude Opus 4.7" },
34950
35525
  { id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" },
@@ -35065,8 +35640,8 @@ var modelsCommand = new Command("models").description("Inspect models available
35065
35640
 
35066
35641
  // src/commands/team.ts
35067
35642
  var import_yaml2 = __toESM(require_dist(), 1);
35068
- import { existsSync as existsSync17, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
35069
- import { join as join20 } from "node:path";
35643
+ import { existsSync as existsSync18, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
35644
+ import { join as join21 } from "node:path";
35070
35645
  async function readStdin2() {
35071
35646
  const chunks = [];
35072
35647
  for await (const chunk of process.stdin) {
@@ -35125,7 +35700,7 @@ var resolveSubcommand = new Command("resolve").description("Resolve and print th
35125
35700
  async (options) => {
35126
35701
  const targetDir = process.cwd();
35127
35702
  requireOcrSetup(targetDir);
35128
- const ocrDir = join20(targetDir, ".ocr");
35703
+ const ocrDir = join21(targetDir, ".ocr");
35129
35704
  try {
35130
35705
  const { team } = loadTeamConfig(ocrDir);
35131
35706
  let override;
@@ -35164,8 +35739,8 @@ var setSubcommand = new Command("set").description("Persist a new default_team c
35164
35739
  }
35165
35740
  const targetDir = process.cwd();
35166
35741
  requireOcrSetup(targetDir);
35167
- const ocrDir = join20(targetDir, ".ocr");
35168
- const configPath = join20(ocrDir, "config.yaml");
35742
+ const ocrDir = join21(targetDir, ".ocr");
35743
+ const configPath = join21(ocrDir, "config.yaml");
35169
35744
  try {
35170
35745
  const raw = await readStdin2();
35171
35746
  const team = parseSessionOverride(raw);
@@ -35175,12 +35750,12 @@ var setSubcommand = new Command("set").description("Persist a new default_team c
35175
35750
  list.push(inst);
35176
35751
  byPersona.set(inst.persona, list);
35177
35752
  }
35178
- 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({});
35179
35754
  applyDefaultTeamSurgically(doc, byPersona);
35180
35755
  const yamlOutput = doc.toString({ lineWidth: 0 });
35181
35756
  writeFileSync8(configPath, yamlOutput, "utf-8");
35182
- const reviewersDir = join20(ocrDir, "skills", "references", "reviewers");
35183
- const metaPath = join20(ocrDir, "reviewers-meta.json");
35757
+ const reviewersDir = join21(ocrDir, "skills", "references", "reviewers");
35758
+ const metaPath = join21(ocrDir, "reviewers-meta.json");
35184
35759
  let metaWritten = false;
35185
35760
  try {
35186
35761
  const meta = generateReviewersMeta(reviewersDir, configPath);
@@ -35276,7 +35851,7 @@ var teamCommand = new Command("team").description("Resolve and persist team comp
35276
35851
 
35277
35852
  // src/commands/review.ts
35278
35853
  import { spawn as spawn3 } from "node:child_process";
35279
- import { join as join21 } from "node:path";
35854
+ import { join as join22 } from "node:path";
35280
35855
  init_db();
35281
35856
 
35282
35857
  // src/lib/vendor-resume.ts
@@ -35315,7 +35890,7 @@ var reviewCommand = new Command("review").description("Run or resume an OCR revi
35315
35890
  }
35316
35891
  const targetDir = process.cwd();
35317
35892
  requireOcrSetup(targetDir);
35318
- const ocrDir = join21(targetDir, ".ocr");
35893
+ const ocrDir = join22(targetDir, ".ocr");
35319
35894
  const db = await ensureDatabase(ocrDir);
35320
35895
  const session = getSession(db, options.resume);
35321
35896
  if (!session) {
@@ -35357,23 +35932,23 @@ var reviewCommand = new Command("review").description("Run or resume an OCR revi
35357
35932
  });
35358
35933
 
35359
35934
  // src/commands/update.ts
35360
- import { existsSync as existsSync18 } from "node:fs";
35361
- import { join as join22 } from "node:path";
35935
+ import { existsSync as existsSync19 } from "node:fs";
35936
+ import { join as join23 } from "node:path";
35362
35937
  function detectConfiguredTools(targetDir) {
35363
35938
  return AI_TOOLS.filter((tool) => {
35364
35939
  if (tool.commandStrategy === "subdirectory") {
35365
- const ocrDir = join22(targetDir, tool.commandsDir, "ocr");
35366
- return existsSync18(ocrDir);
35940
+ const ocrDir = join23(targetDir, tool.commandsDir, "ocr");
35941
+ return existsSync19(ocrDir);
35367
35942
  } else {
35368
- const reviewCmd = join22(targetDir, tool.commandsDir, "ocr-review.md");
35369
- return existsSync18(reviewCmd);
35943
+ const reviewCmd = join23(targetDir, tool.commandsDir, "ocr-review.md");
35944
+ return existsSync19(reviewCmd);
35370
35945
  }
35371
35946
  });
35372
35947
  }
35373
35948
  var updateCommand = new Command("update").description("Update OCR assets after package upgrade").option("--commands", "Update only commands/workflows").option(
35374
35949
  "--skills",
35375
35950
  "Update only skills (includes templates, references, assets)"
35376
- ).option("--inject", "Update only AGENTS.md/CLAUDE.md injection").option("--dry-run", "Preview changes without modifying files").action(async (options) => {
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) => {
35377
35952
  const targetDir = process.cwd();
35378
35953
  requireOcrSetup(targetDir);
35379
35954
  console.log();
@@ -35440,7 +36015,7 @@ var updateCommand = new Command("update").description("Update OCR assets after p
35440
36015
  const result = installForTool(tool, targetDir);
35441
36016
  results.push(result);
35442
36017
  }
35443
- ensureGitignore(join22(targetDir, ".ocr"));
36018
+ ensureGitignore(join23(targetDir, ".ocr"));
35444
36019
  spinner.stop();
35445
36020
  const successful = results.filter((r) => r.success);
35446
36021
  const failed = results.filter((r) => !r.success);
@@ -35474,30 +36049,34 @@ var updateCommand = new Command("update").description("Update OCR assets after p
35474
36049
  }
35475
36050
  }
35476
36051
  if (updateInject) {
36052
+ const planned = plannedInstructionFiles(toolsToUpdate);
35477
36053
  if (options.dryRun) {
35478
36054
  console.log(source_default.dim(" Would update:"));
35479
- if (existsSync18(join22(targetDir, "AGENTS.md"))) {
35480
- console.log(source_default.dim(" \u2022 AGENTS.md (OCR managed block)"));
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)`));
35481
36058
  }
35482
- if (existsSync18(join22(targetDir, "CLAUDE.md"))) {
35483
- 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}`));
35484
36062
  }
35485
36063
  console.log();
35486
36064
  } else {
35487
- const spinner = ora("Updating AGENTS.md/CLAUDE.md...").start();
35488
- const injectResults = injectIntoProjectFiles(targetDir);
36065
+ const spinner = ora("Updating instruction files...").start();
36066
+ const injectResults = injectIntoProjectFiles(targetDir, toolsToUpdate);
35489
36067
  spinner.stop();
35490
- if (injectResults.agentsMd || injectResults.claudeMd) {
36068
+ if (injectResults.written.length > 0) {
35491
36069
  console.log(source_default.green(" \u2713 Instructions updated"));
35492
- if (injectResults.agentsMd) {
35493
- console.log(` ${source_default.green("\u2713")} AGENTS.md`);
35494
- }
35495
- if (injectResults.claudeMd) {
35496
- console.log(` ${source_default.green("\u2713")} CLAUDE.md`);
36070
+ for (const path2 of injectResults.written) {
36071
+ console.log(` ${source_default.green("\u2713")} ${path2}`);
35497
36072
  }
35498
36073
  } else {
35499
36074
  console.log(source_default.dim(" No instruction files to update"));
35500
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
+ }
35501
36080
  console.log();
35502
36081
  }
35503
36082
  }
@@ -35511,14 +36090,15 @@ var updateCommand = new Command("update").description("Update OCR assets after p
35511
36090
  });
35512
36091
 
35513
36092
  // src/commands/dashboard.ts
35514
- import { existsSync as existsSync19 } from "node:fs";
35515
- 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";
35516
36095
  import { fileURLToPath } from "node:url";
36096
+ init_src();
35517
36097
  init_db();
35518
36098
  var __filename = fileURLToPath(import.meta.url);
35519
- var __dirname = dirname7(__filename);
36099
+ var __dirname = dirname9(__filename);
35520
36100
  function resolveServerPath() {
35521
- return join23(__dirname, "dashboard", "server.js");
36101
+ return join24(__dirname, "dashboard", "server.js");
35522
36102
  }
35523
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(
35524
36104
  async (options) => {
@@ -35529,7 +36109,7 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
35529
36109
  console.error(source_default.red(`Error: Invalid port "${options.port}". Must be 1-65535.`));
35530
36110
  process.exit(1);
35531
36111
  }
35532
- const ocrDir = join23(targetDir, ".ocr");
36112
+ const ocrDir = join24(targetDir, ".ocr");
35533
36113
  try {
35534
36114
  await ensureDatabase(ocrDir);
35535
36115
  closeAllDatabases();
@@ -35543,7 +36123,7 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
35543
36123
  process.exit(1);
35544
36124
  }
35545
36125
  const serverPath = resolveServerPath();
35546
- if (!existsSync19(serverPath)) {
36126
+ if (!existsSync20(serverPath)) {
35547
36127
  console.error(source_default.red("Error: Dashboard server bundle not found."));
35548
36128
  console.error(
35549
36129
  source_default.dim(` Expected at: ${serverPath}`)
@@ -35577,8 +36157,8 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
35577
36157
  );
35578
36158
 
35579
36159
  // src/commands/doctor.ts
35580
- import { existsSync as existsSync20 } from "node:fs";
35581
- import { join as join24 } from "node:path";
36160
+ import { existsSync as existsSync21 } from "node:fs";
36161
+ import { join as join25 } from "node:path";
35582
36162
  init_db();
35583
36163
  function printStorageEngine(probeWriteEnabled) {
35584
36164
  console.log();
@@ -35635,10 +36215,10 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
35635
36215
  console.log(source_default.bold(" OCR Installation"));
35636
36216
  console.log();
35637
36217
  const ocrStatus = checkOcrSetup(targetDir);
35638
- const configPath = join24(targetDir, ".ocr", "config.yaml");
35639
- const dbPath = join24(targetDir, ".ocr", "data", "ocr.db");
35640
- const hasConfig = existsSync20(configPath);
35641
- const hasDb = existsSync20(dbPath);
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);
35642
36222
  const ocrChecks = [
35643
36223
  { label: ".ocr/skills/", ok: ocrStatus.hasSkills },
35644
36224
  { label: ".ocr/sessions/", ok: ocrStatus.hasSessions },
@@ -35714,9 +36294,331 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
35714
36294
  console.log();
35715
36295
  });
35716
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
+
35717
36618
  // src/commands/reviewers.ts
35718
36619
  import { writeFileSync as writeFileSync9, renameSync as renameSync2 } from "node:fs";
35719
- import { join as join25 } from "node:path";
36620
+ import { join as join27 } from "node:path";
36621
+ init_src();
35720
36622
  async function readStdin3() {
35721
36623
  const chunks = [];
35722
36624
  for await (const chunk of process.stdin) {
@@ -35730,6 +36632,25 @@ async function readStdin3() {
35730
36632
  }
35731
36633
  var VALID_TIERS = /* @__PURE__ */ new Set(["holistic", "specialist", "persona", "custom"]);
35732
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
+ }
35733
36654
  function validateReviewersMeta(data) {
35734
36655
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
35735
36656
  throw new Error("Payload must be a JSON object");
@@ -35767,6 +36688,19 @@ function validateReviewersMeta(data) {
35767
36688
  if (!Array.isArray(r.focus_areas)) {
35768
36689
  throw new Error(`${prefix}.focus_areas must be an array`);
35769
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
+ ]);
35770
36704
  if (r.known_for !== void 0 && typeof r.known_for !== "string") {
35771
36705
  throw new Error(`${prefix}.known_for must be a string if provided`);
35772
36706
  }
@@ -35779,17 +36713,17 @@ function validateReviewersMeta(data) {
35779
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) => {
35780
36714
  const targetDir = process.cwd();
35781
36715
  requireOcrSetup(targetDir);
35782
- const ocrDir = join25(targetDir, ".ocr");
36716
+ const ocrDir = join27(targetDir, ".ocr");
35783
36717
  if (!options.stdin) {
35784
36718
  try {
35785
- const reviewersDir = join25(ocrDir, "skills", "references", "reviewers");
35786
- const configPath = join25(ocrDir, "config.yaml");
36719
+ const reviewersDir = join27(ocrDir, "skills", "references", "reviewers");
36720
+ const configPath = join27(ocrDir, "config.yaml");
35787
36721
  const meta = generateReviewersMeta(reviewersDir, configPath);
35788
36722
  if (!meta || meta.reviewers.length === 0) {
35789
36723
  console.error(source_default.yellow("No reviewer files found in .ocr/skills/references/reviewers/"));
35790
36724
  process.exit(1);
35791
36725
  }
35792
- const metaPath = join25(ocrDir, "reviewers-meta.json");
36726
+ const metaPath = join27(ocrDir, "reviewers-meta.json");
35793
36727
  const tmpPath = metaPath + ".tmp";
35794
36728
  writeFileSync9(tmpPath, JSON.stringify(meta, null, 2) + "\n");
35795
36729
  renameSync2(tmpPath, metaPath);
@@ -35819,7 +36753,7 @@ var syncSubcommand2 = new Command("sync").description("Sync reviewers-meta.json
35819
36753
  throw new Error("Invalid JSON on stdin");
35820
36754
  }
35821
36755
  const meta = validateReviewersMeta(parsed);
35822
- const metaPath = join25(ocrDir, "reviewers-meta.json");
36756
+ const metaPath = join27(ocrDir, "reviewers-meta.json");
35823
36757
  const tmpPath = metaPath + ".tmp";
35824
36758
  writeFileSync9(tmpPath, JSON.stringify(meta, null, 2) + "\n");
35825
36759
  renameSync2(tmpPath, metaPath);
@@ -35845,26 +36779,74 @@ var syncSubcommand2 = new Command("sync").description("Sync reviewers-meta.json
35845
36779
  });
35846
36780
  var reviewersCommand = new Command("reviewers").description("Manage OCR reviewer metadata").addCommand(syncSubcommand2);
35847
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
+
35848
36830
  // src/lib/update-check.ts
35849
36831
  import { homedir } from "node:os";
35850
- import { join as join26 } from "node:path";
35851
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, mkdirSync as mkdirSync7 } from "node:fs";
36832
+ import { join as join28 } from "node:path";
36833
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync10, mkdirSync as mkdirSync8 } from "node:fs";
35852
36834
  var PACKAGE_NAME = "@open-code-review/cli";
35853
36835
  var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
35854
- var CACHE_DIR2 = join26(homedir(), ".ocr");
35855
- var CACHE_FILE = join26(CACHE_DIR2, "update-check.json");
36836
+ var CACHE_DIR2 = join28(homedir(), ".ocr");
36837
+ var CACHE_FILE = join28(CACHE_DIR2, "update-check.json");
35856
36838
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
35857
36839
  var FETCH_TIMEOUT_MS = 3e3;
35858
36840
  function readCache(cacheFile) {
35859
36841
  try {
35860
- return JSON.parse(readFileSync14(cacheFile, "utf-8"));
36842
+ return JSON.parse(readFileSync15(cacheFile, "utf-8"));
35861
36843
  } catch {
35862
36844
  return null;
35863
36845
  }
35864
36846
  }
35865
36847
  function writeCache(cacheFile, cache) {
35866
36848
  try {
35867
- mkdirSync7(join26(cacheFile, ".."), { recursive: true });
36849
+ mkdirSync8(join28(cacheFile, ".."), { recursive: true });
35868
36850
  writeFileSync10(cacheFile, JSON.stringify(cache));
35869
36851
  } catch {
35870
36852
  }
@@ -35885,7 +36867,7 @@ async function checkForUpdate(currentVersion, options) {
35885
36867
  if (process.env.CI || process.env.OCR_NO_UPDATE_CHECK) {
35886
36868
  return null;
35887
36869
  }
35888
- const cacheFile = join26(options?.cacheDir ?? CACHE_DIR2, "update-check.json");
36870
+ const cacheFile = join28(options?.cacheDir ?? CACHE_DIR2, "update-check.json");
35889
36871
  try {
35890
36872
  const cache = readCache(cacheFile);
35891
36873
  if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {
@@ -35944,7 +36926,9 @@ program2.addCommand(reviewCommand);
35944
36926
  program2.addCommand(updateCommand);
35945
36927
  program2.addCommand(dashboardCommand);
35946
36928
  program2.addCommand(doctorCommand);
36929
+ program2.addCommand(dbCommand);
35947
36930
  program2.addCommand(reviewersCommand);
36931
+ program2.addCommand(hostCommand);
35948
36932
  await program2.parseAsync();
35949
36933
  if (subcommand && HUMAN_COMMANDS.has(subcommand)) {
35950
36934
  const drift = checkLocalArtifactVersion(process.cwd(), CLI_VERSION);