@open-code-review/cli 1.11.0 → 2.1.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 (57) hide show
  1. package/README.md +10 -4
  2. package/dist/dashboard/client/assets/{_basePickBy-D8RU9s_y.js → _basePickBy-B3ALyupE.js} +1 -1
  3. package/dist/dashboard/client/assets/{_baseUniq-CjVeYx1J.js → _baseUniq-b2RALAWc.js} +1 -1
  4. package/dist/dashboard/client/assets/{arc-DsFstmf9.js → arc-DcSVvhUd.js} +1 -1
  5. package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-iNJB-g1N.js → architectureDiagram-VXUJARFQ-BNUlmSCS.js} +1 -1
  6. package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-Zp2Aw0zR.js → blockDiagram-VD42YOAC-BmhiQVwa.js} +1 -1
  7. package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-BGppUmwT.js → c4Diagram-YG6GDRKO-jyJ3WOv5.js} +1 -1
  8. package/dist/dashboard/client/assets/channel-D3J8-GF_.js +1 -0
  9. package/dist/dashboard/client/assets/{chunk-4BX2VUAB-CZcRxeE4.js → chunk-4BX2VUAB-x1dQU_s3.js} +1 -1
  10. package/dist/dashboard/client/assets/{chunk-55IACEB6-CVdL59yY.js → chunk-55IACEB6-CwbsE2XQ.js} +1 -1
  11. package/dist/dashboard/client/assets/{chunk-B4BG7PRW-CFPp6g6e.js → chunk-B4BG7PRW-BaE7c-ti.js} +1 -1
  12. package/dist/dashboard/client/assets/{chunk-DI55MBZ5-DH9BzE6I.js → chunk-DI55MBZ5-Bw5PUaMK.js} +1 -1
  13. package/dist/dashboard/client/assets/{chunk-FMBD7UC4-DZ2DTwqS.js → chunk-FMBD7UC4-B7cF6P3s.js} +1 -1
  14. package/dist/dashboard/client/assets/{chunk-QN33PNHL-DODPm0CR.js → chunk-QN33PNHL-OY4evNHd.js} +1 -1
  15. package/dist/dashboard/client/assets/{chunk-QZHKN3VN-CNI_LxUf.js → chunk-QZHKN3VN-BpjQwIWz.js} +1 -1
  16. package/dist/dashboard/client/assets/{chunk-TZMSLE5B-sxZQF02c.js → chunk-TZMSLE5B-D8b_Oq9B.js} +1 -1
  17. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-tkFUL-1Y.js +1 -0
  18. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-tkFUL-1Y.js +1 -0
  19. package/dist/dashboard/client/assets/clone-CkY5ajLr.js +1 -0
  20. package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-BHa2lABH.js → cose-bilkent-S5V4N54A-C-sfP8PN.js} +1 -1
  21. package/dist/dashboard/client/assets/{dagre-6UL2VRFP-CvCLBtkz.js → dagre-6UL2VRFP-Cqfo0NRg.js} +1 -1
  22. package/dist/dashboard/client/assets/{diagram-PSM6KHXK-Cklwd4YA.js → diagram-PSM6KHXK-BR3ppxqI.js} +1 -1
  23. package/dist/dashboard/client/assets/{diagram-QEK2KX5R-3bDERTbp.js → diagram-QEK2KX5R-Dvcx6x3R.js} +1 -1
  24. package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DbiWlPc6.js → diagram-S2PKOQOG-DoyBLnVN.js} +1 -1
  25. package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-BQa_VNbt.js → erDiagram-Q2GNP2WA-hy77l1cL.js} +1 -1
  26. package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-BDaJyl9N.js → flowDiagram-NV44I4VS-Bz0B1rKM.js} +1 -1
  27. package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-DsTnleSr.js → ganttDiagram-JELNMOA3-CLgrZPoC.js} +1 -1
  28. package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-BRuBadgn.js → gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js} +1 -1
  29. package/dist/dashboard/client/assets/{graph-CYYqXm9c.js → graph-DDBMM_t2.js} +1 -1
  30. package/dist/dashboard/client/assets/{index-eZMoytob.js → index-Cr9yEo_B.js} +123 -123
  31. package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-CHnA8k7H.js → infoDiagram-HS3SLOUP-Bhn1FmAk.js} +1 -1
  32. package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CAXR1-Ju.js → journeyDiagram-XKPGCS4Q-CzGbjX1y.js} +1 -1
  33. package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Clf3HfHz.js → kanban-definition-3W4ZIXB7-Da77-WYk.js} +1 -1
  34. package/dist/dashboard/client/assets/{layout-DQPaNqnO.js → layout-CVwSB-GS.js} +1 -1
  35. package/dist/dashboard/client/assets/{linear-qUnNXvWB.js → linear-CTRAc5Jn.js} +1 -1
  36. package/dist/dashboard/client/assets/{mermaid-renderer-C7Se8vjl.js → mermaid-renderer-Bjo170ax.js} +4 -4
  37. package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-DBIdG0OR.js → mindmap-definition-VGOIOE7T-B55C2odl.js} +1 -1
  38. package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-DXAIiG6W.js → pieDiagram-ADFJNKIX-5lrQLrSz.js} +1 -1
  39. package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-D4yAxif0.js → quadrantDiagram-AYHSOK5B-Bg55gC30.js} +1 -1
  40. package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-D27ME1VO.js → requirementDiagram-UZGBJVZJ-CyR4YFJY.js} +1 -1
  41. package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BeEaA_QM.js → sankeyDiagram-TZEHDZUN-BVWKr9_-.js} +1 -1
  42. package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-GTI12qU0.js → sequenceDiagram-WL72ISMW-D0AJg_tE.js} +1 -1
  43. package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-ClSoeZM0.js → stateDiagram-FKZM4ZOC-BuHpTgim.js} +1 -1
  44. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +1 -0
  45. package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-cj5d_Kyh.js → timeline-definition-IT6M3QCI-LDhpAmDd.js} +1 -1
  46. package/dist/dashboard/client/assets/{treemap-GDKQZRPO-BrRT1igb.js → treemap-GDKQZRPO-Dd4gjvUl.js} +1 -1
  47. package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-DlzGitHh.js → xychartDiagram-PRI3JC2R-B9RDod39.js} +1 -1
  48. package/dist/dashboard/client/index.html +1 -1
  49. package/dist/dashboard/server.js +1232 -656
  50. package/dist/index.js +1905 -711
  51. package/dist/lib/db/index.js +794 -100
  52. package/package.json +3 -5
  53. package/dist/dashboard/client/assets/channel-C8plpfdz.js +0 -1
  54. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-Dqn6u1oQ.js +0 -1
  55. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-Dqn6u1oQ.js +0 -1
  56. package/dist/dashboard/client/assets/clone-BQ8hOLqM.js +0 -1
  57. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-Bim3s-dq.js +0 -1
package/dist/index.js CHANGED
@@ -39,6 +39,30 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
39
39
  mod
40
40
  ));
41
41
 
42
+ // src/lib/runtime-checks.ts
43
+ function isSupportedNode(version) {
44
+ const [major = 0, minor = 0] = version.split(".").map((n) => Number.parseInt(n, 10) || 0);
45
+ return major > NODE_FLOOR.major || major === NODE_FLOOR.major && minor >= NODE_FLOOR.minor;
46
+ }
47
+ function nodeVersionGuardMessage(version) {
48
+ return `
49
+ Open Code Review requires Node.js >= ${NODE_FLOOR.major}.${NODE_FLOOR.minor} (it uses Node's built-in SQLite, \`node:sqlite\`).
50
+ You have Node ${version}. Upgrade Node (e.g. \`nvm install 22 && nvm use 22\`) and re-run.
51
+
52
+ `;
53
+ }
54
+ function isSuppressibleSqliteWarning(warning) {
55
+ const message = typeof warning === "string" ? warning : warning?.message;
56
+ return typeof message === "string" && message.includes("SQLite is an experimental feature");
57
+ }
58
+ var NODE_FLOOR;
59
+ var init_runtime_checks = __esm({
60
+ "src/lib/runtime-checks.ts"() {
61
+ "use strict";
62
+ NODE_FLOOR = { major: 22, minor: 5 };
63
+ }
64
+ });
65
+
42
66
  // ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/error.js
43
67
  var require_error = __commonJS({
44
68
  "../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/error.js"(exports) {
@@ -23319,7 +23343,195 @@ var init_result_mapper = __esm({
23319
23343
  }
23320
23344
  });
23321
23345
 
23346
+ // src/lib/db/engine.ts
23347
+ import { createRequire as createRequire2 } from "node:module";
23348
+ function applyEnginePreconditions() {
23349
+ if (_preconditionsApplied) return;
23350
+ _preconditionsApplied = true;
23351
+ const originalEmitWarning = process.emitWarning.bind(process);
23352
+ process.emitWarning = (warning, ...args) => {
23353
+ if (isSuppressibleSqliteWarning(warning)) return;
23354
+ originalEmitWarning(warning, ...args);
23355
+ };
23356
+ }
23357
+ function newDatabase(path2) {
23358
+ if (!_DatabaseSyncCtor) {
23359
+ applyEnginePreconditions();
23360
+ try {
23361
+ _DatabaseSyncCtor = nodeRequire("node:sqlite").DatabaseSync;
23362
+ } catch (e) {
23363
+ if (!isSupportedNode(process.versions.node)) {
23364
+ throw new Error(nodeVersionGuardMessage(process.versions.node).trim());
23365
+ }
23366
+ throw e;
23367
+ }
23368
+ }
23369
+ return new _DatabaseSyncCtor(path2);
23370
+ }
23371
+ function isBusyError(e) {
23372
+ const errcode = e?.errcode;
23373
+ return errcode === SQLITE_BUSY || errcode === SQLITE_BUSY_SNAPSHOT;
23374
+ }
23375
+ function sleepSync(ms) {
23376
+ Atomics.wait(SLEEP_BUF, 0, 0, ms);
23377
+ }
23378
+ function probeEngine() {
23379
+ try {
23380
+ const db = newDatabase(":memory:");
23381
+ db.exec("PRAGMA journal_mode = WAL");
23382
+ db.exec("CREATE TABLE _probe(x); INSERT INTO _probe VALUES (1);");
23383
+ const row = db.prepare("SELECT sqlite_version() AS v").get();
23384
+ db.close();
23385
+ return { ok: true, version: row.v };
23386
+ } catch (e) {
23387
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
23388
+ }
23389
+ }
23390
+ function openEngine(dbPath) {
23391
+ const native = newDatabase(dbPath);
23392
+ native.exec("PRAGMA journal_mode = WAL");
23393
+ native.exec("PRAGMA foreign_keys = ON");
23394
+ native.exec("PRAGMA busy_timeout = 5000");
23395
+ native.exec("PRAGMA synchronous = NORMAL");
23396
+ return new NodeSqliteAdapter(native);
23397
+ }
23398
+ var SQLITE_BUSY, SQLITE_BUSY_SNAPSHOT, BUSY_RETRY_ATTEMPTS, BUSY_RETRY_BACKOFF_MS, savepointName, nodeRequire, _preconditionsApplied, _DatabaseSyncCtor, SLEEP_BUF, NodeSqliteAdapter;
23399
+ var init_engine = __esm({
23400
+ "src/lib/db/engine.ts"() {
23401
+ "use strict";
23402
+ init_runtime_checks();
23403
+ SQLITE_BUSY = 5;
23404
+ SQLITE_BUSY_SNAPSHOT = 261;
23405
+ BUSY_RETRY_ATTEMPTS = 5;
23406
+ BUSY_RETRY_BACKOFF_MS = 50;
23407
+ savepointName = (depth) => `ocr_sp_${depth}`;
23408
+ nodeRequire = createRequire2(import.meta.url);
23409
+ _preconditionsApplied = false;
23410
+ SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
23411
+ NodeSqliteAdapter = class {
23412
+ raw;
23413
+ /**
23414
+ * Transaction nesting depth. `node:sqlite` has no transaction helper, so we
23415
+ * drive `BEGIN IMMEDIATE` ourselves and use SAVEPOINTs for nested calls
23416
+ * (better-sqlite3 did this automatically). 0 = no transaction open.
23417
+ */
23418
+ txnDepth = 0;
23419
+ constructor(db) {
23420
+ this.raw = db;
23421
+ }
23422
+ exec(sql, params) {
23423
+ const stmt = this.raw.prepare(sql);
23424
+ const cols = stmt.columns();
23425
+ if (cols.length === 0) {
23426
+ stmt.run(...params ?? []);
23427
+ return [];
23428
+ }
23429
+ stmt.setReturnArrays(true);
23430
+ const values = stmt.all(...params ?? []);
23431
+ return values.length > 0 ? [{ columns: cols.map((c) => c.name), values }] : [];
23432
+ }
23433
+ run(sql, params) {
23434
+ if (params !== void 0) {
23435
+ this.raw.prepare(sql).run(...params);
23436
+ return;
23437
+ }
23438
+ this.raw.exec(sql);
23439
+ }
23440
+ prepare(sql) {
23441
+ return this.raw.prepare(sql);
23442
+ }
23443
+ transaction(fn) {
23444
+ return this.txnDepth > 0 ? this.runNested(fn) : this.runOuter(fn);
23445
+ }
23446
+ /**
23447
+ * Nested call: a SAVEPOINT within the outer transaction's write lock. No
23448
+ * busy-retry — the outer transaction already holds the lock. The savepoint
23449
+ * lets the inner block roll back independently while the outer continues.
23450
+ */
23451
+ runNested(fn) {
23452
+ const name = savepointName(this.txnDepth);
23453
+ this.raw.exec(`SAVEPOINT ${name}`);
23454
+ this.txnDepth++;
23455
+ try {
23456
+ const result = fn();
23457
+ this.raw.exec(`RELEASE ${name}`);
23458
+ return result;
23459
+ } catch (e) {
23460
+ try {
23461
+ this.raw.exec(`ROLLBACK TO ${name}`);
23462
+ this.raw.exec(`RELEASE ${name}`);
23463
+ } catch {
23464
+ }
23465
+ throw e;
23466
+ } finally {
23467
+ this.txnDepth--;
23468
+ }
23469
+ }
23470
+ /**
23471
+ * Outer transaction: `BEGIN IMMEDIATE` acquires the write lock up front so
23472
+ * cross-process writers serialize cleanly under WAL instead of failing late
23473
+ * on upgrade. `busy_timeout` covers most contention; a bounded synchronous
23474
+ * retry absorbs the residual SQLITE_BUSY (another connection holds the lock
23475
+ * past the timeout, or BUSY_SNAPSHOT). Non-busy errors and the final attempt
23476
+ * re-throw so genuine failures propagate.
23477
+ */
23478
+ runOuter(fn) {
23479
+ for (let attempt = 0; attempt < BUSY_RETRY_ATTEMPTS; attempt++) {
23480
+ try {
23481
+ return this.runOnce(fn);
23482
+ } catch (e) {
23483
+ if (!isBusyError(e) || attempt === BUSY_RETRY_ATTEMPTS - 1) throw e;
23484
+ sleepSync(BUSY_RETRY_BACKOFF_MS);
23485
+ }
23486
+ }
23487
+ throw new Error("transaction retry budget exhausted");
23488
+ }
23489
+ /** One `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK` lifecycle. */
23490
+ runOnce(fn) {
23491
+ this.raw.exec("BEGIN IMMEDIATE");
23492
+ this.txnDepth = 1;
23493
+ try {
23494
+ const result = fn();
23495
+ this.raw.exec("COMMIT");
23496
+ return result;
23497
+ } catch (e) {
23498
+ try {
23499
+ this.raw.exec("ROLLBACK");
23500
+ } catch {
23501
+ }
23502
+ throw e;
23503
+ } finally {
23504
+ this.txnDepth = 0;
23505
+ }
23506
+ }
23507
+ pragma(source) {
23508
+ this.raw.exec(`PRAGMA ${source}`);
23509
+ return void 0;
23510
+ }
23511
+ close() {
23512
+ try {
23513
+ this.raw.exec("PRAGMA wal_checkpoint(TRUNCATE)");
23514
+ } catch {
23515
+ }
23516
+ try {
23517
+ this.raw.close();
23518
+ } catch (e) {
23519
+ const message = e?.message ?? "";
23520
+ if (!/database is not open/i.test(message)) throw e;
23521
+ }
23522
+ }
23523
+ };
23524
+ }
23525
+ });
23526
+
23322
23527
  // src/lib/db/migrations.ts
23528
+ function columnExists(db, table, column) {
23529
+ const result = db.exec(`PRAGMA table_info(${table})`);
23530
+ const first = result[0];
23531
+ if (!first) return false;
23532
+ const nameIdx = first.columns.indexOf("name");
23533
+ return first.values.some((row) => row[nameIdx] === column);
23534
+ }
23323
23535
  function ensureSchemaVersionTable(db) {
23324
23536
  db.run(`
23325
23537
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -23329,6 +23541,10 @@ function ensureSchemaVersionTable(db) {
23329
23541
  );
23330
23542
  `);
23331
23543
  }
23544
+ function getSchemaVersion(db) {
23545
+ ensureSchemaVersionTable(db);
23546
+ return getCurrentVersion(db);
23547
+ }
23332
23548
  function getCurrentVersion(db) {
23333
23549
  const result = db.exec(
23334
23550
  "SELECT MAX(version) as v FROM schema_version"
@@ -23346,9 +23562,10 @@ function runMigrations(db) {
23346
23562
  if (migration.version <= currentVersion) {
23347
23563
  continue;
23348
23564
  }
23349
- db.run("BEGIN TRANSACTION;");
23565
+ db.run("BEGIN IMMEDIATE;");
23350
23566
  try {
23351
- db.run(migration.sql);
23567
+ if (migration.sql) db.run(migration.sql);
23568
+ migration.run?.(db);
23352
23569
  db.run(
23353
23570
  "INSERT INTO schema_version (version, description) VALUES (?, ?);",
23354
23571
  [migration.version, migration.description]
@@ -23685,6 +23902,148 @@ var init_migrations = __esm({
23685
23902
  DROP INDEX IF EXISTS idx_agent_sessions_status_heartbeat;
23686
23903
  DROP TABLE IF EXISTS agent_sessions;
23687
23904
  `
23905
+ },
23906
+ {
23907
+ version: 12,
23908
+ description: "Event-sourced lifecycle hardening: event_type taxonomy guard, sweep indexes, session_completeness view",
23909
+ sql: `
23910
+ -- \u2500\u2500 Indexes for the now-periodic stale-session sweep + round derivation \u2500\u2500
23911
+ -- The sweep filters sessions by status and rolls up MAX(created_at) per
23912
+ -- session over the event log; deriveNextRound does MAX(round). Index both.
23913
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
23914
+ CREATE INDEX IF NOT EXISTS idx_events_session_created
23915
+ ON orchestration_events(session_id, created_at);
23916
+
23917
+ -- \u2500\u2500 Event-type taxonomy guard \u2500\u2500
23918
+ -- orchestration_events.event_type is the spine of all lifecycle
23919
+ -- derivation. A typo (e.g. 'round_complete' vs 'round_completed') would
23920
+ -- silently break deriveNextRound and the completeness view. SQLite cannot
23921
+ -- add a CHECK to an existing column without a table rebuild, so enforce
23922
+ -- the closed vocabulary with a BEFORE INSERT trigger instead.
23923
+ CREATE TRIGGER IF NOT EXISTS trg_events_known_type
23924
+ BEFORE INSERT ON orchestration_events
23925
+ WHEN NEW.event_type NOT IN (
23926
+ 'session_created', 'session_resumed', 'round_started', 'phase_transition',
23927
+ 'round_completed', 'map_completed', 'session_closed', 'session_aborted',
23928
+ 'session_auto_closed_stale', 'session_synced', 'session_legacy_import'
23929
+ )
23930
+ BEGIN
23931
+ SELECT RAISE(ABORT, 'unknown orchestration_events.event_type');
23932
+ END;
23933
+
23934
+ -- \u2500\u2500 Close-guard (DB backstop for the completion invariant) \u2500\u2500
23935
+ -- A session cannot transition active \u2192 closed unless its current
23936
+ -- round/run has a terminal artifact event, OR an explicit reason event
23937
+ -- (abort / auto-close-stale / sync / legacy-import) is present. Only a
23938
+ -- *silent* premature close is banned \u2014 every legitimate non-artifact
23939
+ -- close carries a reason event and passes. App-level guards in
23940
+ -- stateClose/finish are the primary check; this makes the illegal state
23941
+ -- unrepresentable even via raw SQL.
23942
+ --
23943
+ -- DEFENCE-IN-DEPTH NOTE (intentional, documented gap): the reason-event
23944
+ -- branch below (event_type IN (...)) is NOT round-scoped \u2014 a reason event
23945
+ -- recorded for an earlier round would also satisfy a later close. The
23946
+ -- app-level guards ARE round-scoped (hasCompletionInvariant checks the
23947
+ -- current round/run), so the precise check lives in the application; this
23948
+ -- trigger is a coarse backstop against a *silent* premature close via raw
23949
+ -- SQL. Tightening it to be round-scoped would require a new migration
23950
+ -- (this v12 trigger is append-only and already shipped); the residual
23951
+ -- risk is a non-artifact close carrying a stale reason event, which is
23952
+ -- still an explicit, audited terminal \u2014 not the failure mode this guards.
23953
+ CREATE TRIGGER IF NOT EXISTS trg_sessions_close_guard
23954
+ BEFORE UPDATE OF status ON sessions
23955
+ WHEN NEW.status = 'closed' AND OLD.status <> 'closed'
23956
+ AND NOT EXISTS (
23957
+ SELECT 1 FROM orchestration_events e
23958
+ WHERE e.session_id = NEW.id
23959
+ AND (
23960
+ (NEW.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = NEW.current_round)
23961
+ OR (NEW.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = NEW.current_map_run)
23962
+ OR e.event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
23963
+ )
23964
+ )
23965
+ BEGIN
23966
+ SELECT RAISE(ABORT, 'cannot close session without a completed round/run or an explicit reason event');
23967
+ END;
23968
+
23969
+ -- \u2500\u2500 session_completeness view \u2500\u2500
23970
+ -- The published contract for "is this session actually complete, and if
23971
+ -- not, what's missing". Completion is DERIVED from the event log, never a
23972
+ -- mutable flag: a session is complete iff it is closed AND a terminal
23973
+ -- artifact event exists for its current round/run. The dashboard's
23974
+ -- outcome derivation and the agent 'status' command read this view, so
23975
+ -- they cannot disagree.
23976
+ --
23977
+ -- completeness_state is an INTENTIONAL HYBRID: it combines the mutable
23978
+ -- status column (marked_closed) with append-only event evidence (the
23979
+ -- terminal artifact event). This is sound precisely because the
23980
+ -- close-guard trigger above makes the status column trustworthy \u2014 a row
23981
+ -- can only reach status='closed' with a completed round/run or an
23982
+ -- explicit reason event \u2014 so reading the column is not a regression to
23983
+ -- the old "mutable flag that could lie" model.
23984
+ --
23985
+ -- completeness_state:
23986
+ -- 'complete' \u2014 closed + terminal artifact for current round/run
23987
+ -- 'closed_without_artifact' \u2014 closed but no terminal artifact (the
23988
+ -- "completed too soon" condition)
23989
+ -- 'in_flight' \u2014 open with a dependent process still running
23990
+ -- 'open_no_artifact' \u2014 open, no in-flight dependents
23991
+ CREATE VIEW IF NOT EXISTS session_completeness AS
23992
+ SELECT
23993
+ s.id AS session_id,
23994
+ s.workflow_type AS workflow_type,
23995
+ s.status AS status,
23996
+ s.current_round AS current_round,
23997
+ s.current_map_run AS current_map_run,
23998
+ CASE WHEN EXISTS (
23999
+ SELECT 1 FROM orchestration_events e
24000
+ WHERE e.session_id = s.id
24001
+ AND (
24002
+ (s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
24003
+ OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
24004
+ )
24005
+ ) THEN 1 ELSE 0 END AS has_terminal_artifact,
24006
+ CASE WHEN s.status = 'closed' THEN 1 ELSE 0 END AS marked_closed,
24007
+ CASE WHEN NOT EXISTS (
24008
+ SELECT 1 FROM command_executions ce
24009
+ WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
24010
+ ) THEN 1 ELSE 0 END AS dependents_settled,
24011
+ CASE
24012
+ WHEN s.status = 'closed' AND EXISTS (
24013
+ SELECT 1 FROM orchestration_events e
24014
+ WHERE e.session_id = s.id
24015
+ AND (
24016
+ (s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
24017
+ OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
24018
+ )
24019
+ ) THEN 'complete'
24020
+ WHEN s.status = 'closed' THEN 'closed_without_artifact'
24021
+ WHEN EXISTS (
24022
+ SELECT 1 FROM command_executions ce
24023
+ WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
24024
+ ) THEN 'in_flight'
24025
+ ELSE 'open_no_artifact'
24026
+ END AS completeness_state
24027
+ FROM sessions s;
24028
+ `
24029
+ },
24030
+ {
24031
+ version: 13,
24032
+ description: "Retire dead parent_id column on command_executions (never written; row kind is derived from command)",
24033
+ // parent_id was reserved for an AI-instance → dashboard-spawn lineage link
24034
+ // that was never wired (no writer, no reader). A process's KIND (supervisor
24035
+ // / reviewer-instance / utility) is derived from columns that are always
24036
+ // present (command + last_heartbeat_at), so the dead lineage column and its
24037
+ // all-NULL index are removed. Re-add a wired parent_id alongside a real
24038
+ // consumer (e.g. a parent→child tree view) if lineage is ever needed.
24039
+ //
24040
+ // Imperative + guarded so the DROP COLUMN (which SQLite can't express as
24041
+ // IF EXISTS) is idempotent under re-application.
24042
+ run: (db) => {
24043
+ if (!columnExists(db, "command_executions", "parent_id")) return;
24044
+ db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
24045
+ db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
24046
+ }
23688
24047
  }
23689
24048
  ];
23690
24049
  }
@@ -23798,6 +24157,12 @@ function getLatestEventId(db) {
23798
24157
  const val = result[0]?.values[0]?.[0];
23799
24158
  return typeof val === "number" ? val : 0;
23800
24159
  }
24160
+ function commitReasonClose(db, sessionId, reasonEvent, projectionUpdates) {
24161
+ db.transaction(() => {
24162
+ insertEvent(db, { session_id: sessionId, ...reasonEvent });
24163
+ updateSession(db, sessionId, projectionUpdates);
24164
+ });
24165
+ }
23801
24166
  var init_queries = __esm({
23802
24167
  "src/lib/db/queries.ts"() {
23803
24168
  "use strict";
@@ -23805,7 +24170,208 @@ var init_queries = __esm({
23805
24170
  }
23806
24171
  });
23807
24172
 
24173
+ // src/lib/db/reconcile.ts
24174
+ import { existsSync as existsSync10 } from "node:fs";
24175
+ import { isAbsolute as isAbsolute2, join as join12, dirname as dirname4 } from "node:path";
24176
+ function hasTerminalArtifactEvent(db, sessionId, workflowType, currentRound, currentMapRun) {
24177
+ const eventType = workflowType === "map" ? "map_completed" : "round_completed";
24178
+ const round = workflowType === "map" ? currentMapRun : currentRound;
24179
+ const r = db.exec(
24180
+ `SELECT 1 FROM orchestration_events
24181
+ WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
24182
+ [sessionId, eventType, round]
24183
+ );
24184
+ return (r[0]?.values.length ?? 0) > 0;
24185
+ }
24186
+ function hasReasonEvent(db, sessionId) {
24187
+ const r = db.exec(
24188
+ `SELECT 1 FROM orchestration_events
24189
+ WHERE session_id = ?
24190
+ AND event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
24191
+ LIMIT 1`,
24192
+ [sessionId]
24193
+ );
24194
+ return (r[0]?.values.length ?? 0) > 0;
24195
+ }
24196
+ function lastEventAgeSeconds(db, sessionId) {
24197
+ const r = db.exec(
24198
+ `SELECT (julianday('now') - julianday(MAX(created_at))) * 86400
24199
+ FROM orchestration_events WHERE session_id = ?`,
24200
+ [sessionId]
24201
+ );
24202
+ const v = r[0]?.values[0]?.[0];
24203
+ return typeof v === "number" ? v : null;
24204
+ }
24205
+ function hasInFlightDependents(db, sessionId) {
24206
+ const r = db.exec(
24207
+ `SELECT 1 FROM command_executions
24208
+ WHERE workflow_id = ? AND finished_at IS NULL LIMIT 1`,
24209
+ [sessionId]
24210
+ );
24211
+ return (r[0]?.values.length ?? 0) > 0;
24212
+ }
24213
+ function resolveSessionDir(ocrDir, sessionDir) {
24214
+ if (!sessionDir) return null;
24215
+ if (isAbsolute2(sessionDir)) return sessionDir;
24216
+ return join12(dirname4(ocrDir), sessionDir);
24217
+ }
24218
+ function reconcileLegacyState(db, ocrDir, opts = {}) {
24219
+ const dryRun = opts.dryRun ?? false;
24220
+ const threshold = opts.staleThresholdSeconds ?? DEFAULT_STALE_THRESHOLD_SECONDS;
24221
+ const actions = [];
24222
+ for (const s of getAllSessions(db)) {
24223
+ const dir = resolveSessionDir(ocrDir, s.session_dir);
24224
+ if (s.status === "closed") {
24225
+ if (hasTerminalArtifactEvent(db, s.id, s.workflow_type, s.current_round, s.current_map_run) || hasReasonEvent(db, s.id)) {
24226
+ continue;
24227
+ }
24228
+ const reviewFinal = s.workflow_type === "review" && dir ? existsSync10(join12(dir, "rounds", `round-${s.current_round}`, "final.md")) : false;
24229
+ const mapFinal = s.workflow_type === "map" && dir ? existsSync10(join12(dir, "map", "runs", `run-${s.current_map_run}`, "map.md")) : false;
24230
+ if (reviewFinal) {
24231
+ actions.push({
24232
+ sessionId: s.id,
24233
+ kind: "synthesize-round-completed",
24234
+ detail: `final.md present for round ${s.current_round}; synthesizing round_completed`
24235
+ });
24236
+ if (!dryRun) {
24237
+ insertEvent(db, {
24238
+ session_id: s.id,
24239
+ event_type: "round_completed",
24240
+ phase: "synthesis",
24241
+ phase_number: 7,
24242
+ round: s.current_round,
24243
+ metadata: JSON.stringify({ source: "reconciled", synthesized_from: "final.md" })
24244
+ });
24245
+ }
24246
+ } else if (mapFinal) {
24247
+ actions.push({
24248
+ sessionId: s.id,
24249
+ kind: "synthesize-map-completed",
24250
+ detail: `map.md present for run ${s.current_map_run}; synthesizing map_completed`
24251
+ });
24252
+ if (!dryRun) {
24253
+ insertEvent(db, {
24254
+ session_id: s.id,
24255
+ event_type: "map_completed",
24256
+ phase: "synthesis",
24257
+ phase_number: 5,
24258
+ round: s.current_map_run,
24259
+ metadata: JSON.stringify({ source: "reconciled", synthesized_from: "map.md" })
24260
+ });
24261
+ }
24262
+ } else {
24263
+ actions.push({
24264
+ sessionId: s.id,
24265
+ kind: "grandfather",
24266
+ detail: "no provable artifact; recording session_legacy_import"
24267
+ });
24268
+ if (!dryRun) {
24269
+ insertEvent(db, {
24270
+ session_id: s.id,
24271
+ event_type: "session_legacy_import",
24272
+ phase: "complete",
24273
+ metadata: JSON.stringify({ source: "reconciled" })
24274
+ });
24275
+ }
24276
+ }
24277
+ continue;
24278
+ }
24279
+ const age = lastEventAgeSeconds(db, s.id);
24280
+ const stale = (age === null || age > threshold) && !hasInFlightDependents(db, s.id);
24281
+ if (stale) {
24282
+ actions.push({
24283
+ sessionId: s.id,
24284
+ kind: "stale-close",
24285
+ detail: age === null ? "active with no events and no in-flight dependents" : `active, last event ${Math.round(age / 86400)}d ago, no in-flight dependents`
24286
+ });
24287
+ if (!dryRun) {
24288
+ commitReasonClose(
24289
+ db,
24290
+ s.id,
24291
+ {
24292
+ event_type: "session_auto_closed_stale",
24293
+ phase: "complete",
24294
+ metadata: JSON.stringify({ source: "reconciled", threshold_seconds: threshold })
24295
+ },
24296
+ { status: "closed", current_phase: "complete" }
24297
+ );
24298
+ }
24299
+ }
24300
+ }
24301
+ return { dryRun, actions };
24302
+ }
24303
+ var DEFAULT_STALE_THRESHOLD_SECONDS;
24304
+ var init_reconcile = __esm({
24305
+ "src/lib/db/reconcile.ts"() {
24306
+ "use strict";
24307
+ init_queries();
24308
+ DEFAULT_STALE_THRESHOLD_SECONDS = 7 * 24 * 60 * 60;
24309
+ }
24310
+ });
24311
+
24312
+ // src/lib/db/liveness.ts
24313
+ function defaultIsAlive(pid) {
24314
+ try {
24315
+ process.kill(pid, 0);
24316
+ return true;
24317
+ } catch (err) {
24318
+ return !(err instanceof Error && "code" in err && err.code === "ESRCH");
24319
+ }
24320
+ }
24321
+ function sqliteUtcMs(ts) {
24322
+ const sqliteShape = ts.includes(" ");
24323
+ return new Date(sqliteShape ? ts.replace(" ", "T") + "Z" : ts).getTime();
24324
+ }
24325
+ var PID_REUSE_GUARD_MS;
24326
+ var init_liveness = __esm({
24327
+ "src/lib/db/liveness.ts"() {
24328
+ "use strict";
24329
+ PID_REUSE_GUARD_MS = 24 * 60 * 60 * 1e3;
24330
+ }
24331
+ });
24332
+
24333
+ // src/lib/state/exit-codes.ts
24334
+ var STATE_EXIT, StateError, CANCELLED_EXIT_CODE, ORPHAN_EXIT_CODE, CASCADE_CLOSE_EXIT_CODE;
24335
+ var init_exit_codes = __esm({
24336
+ "src/lib/state/exit-codes.ts"() {
24337
+ "use strict";
24338
+ STATE_EXIT = {
24339
+ OK: 0,
24340
+ USAGE: 2,
24341
+ AMBIGUOUS: 3,
24342
+ NOT_FOUND: 4,
24343
+ ILLEGAL_TRANSITION: 5,
24344
+ INVARIANT_UNMET: 6,
24345
+ SCHEMA_INVALID: 7,
24346
+ /** Database was locked past the bounded retry budget (SQLITE_BUSY). */
24347
+ BUSY: 8
24348
+ };
24349
+ StateError = class extends Error {
24350
+ constructor(code, message) {
24351
+ super(message);
24352
+ this.code = code;
24353
+ this.name = "StateError";
24354
+ }
24355
+ };
24356
+ CANCELLED_EXIT_CODE = -2;
24357
+ ORPHAN_EXIT_CODE = -3;
24358
+ CASCADE_CLOSE_EXIT_CODE = -4;
24359
+ }
24360
+ });
24361
+
23808
24362
  // src/lib/db/agent-sessions.ts
24363
+ function cascadeTerminateExecutions(db, workflowId, exitCode, note) {
24364
+ db.run(
24365
+ `UPDATE command_executions
24366
+ SET finished_at = datetime('now'),
24367
+ exit_code = ?,
24368
+ pid = NULL,
24369
+ notes = COALESCE(notes || char(10), '') || ?
24370
+ WHERE workflow_id = ?
24371
+ AND finished_at IS NULL`,
24372
+ [exitCode, note, workflowId]
24373
+ );
24374
+ }
23809
24375
  function rowToAgentSession(row) {
23810
24376
  return {
23811
24377
  // The OCR-owned id is the `uid` column. Fall back to the integer
@@ -23820,6 +24386,7 @@ function rowToAgentSession(row) {
23820
24386
  resolved_model: row.resolved_model,
23821
24387
  phase: null,
23822
24388
  status: deriveStatus(row),
24389
+ kind: rowKind(row),
23823
24390
  pid: row.pid,
23824
24391
  started_at: row.started_at,
23825
24392
  last_heartbeat_at: row.last_heartbeat_at ?? row.started_at,
@@ -23833,7 +24400,9 @@ function deriveStatus(row) {
23833
24400
  return "running";
23834
24401
  }
23835
24402
  if (row.exit_code === ORPHAN_EXIT_CODE) return "orphaned";
23836
- if (row.exit_code === CANCELLED_EXIT_CODE) return "cancelled";
24403
+ if (row.exit_code === CANCELLED_EXIT_CODE || row.exit_code === CASCADE_CLOSE_EXIT_CODE) {
24404
+ return "cancelled";
24405
+ }
23837
24406
  if (row.exit_code === 0) return "done";
23838
24407
  return "crashed";
23839
24408
  }
@@ -23849,7 +24418,7 @@ function insertAgentSession(db, params) {
23849
24418
  pid = null,
23850
24419
  notes = null
23851
24420
  } = params;
23852
- const command = persona && instance_index !== null ? `session-instance:${persona}-${instance_index}` : "session-instance";
24421
+ const command = persona && instance_index !== null ? `${INSTANCE_COMMAND}:${persona}-${instance_index}` : INSTANCE_COMMAND;
23853
24422
  db.run(
23854
24423
  `INSERT INTO command_executions
23855
24424
  (uid, command, args, workflow_id, vendor, persona, instance_index, name,
@@ -24054,63 +24623,139 @@ function updateAgentSession(db, id, params) {
24054
24623
  values
24055
24624
  );
24056
24625
  }
24057
- function sweepStaleAgentSessions(db, thresholdSeconds) {
24058
- const staleSql = `
24059
- SELECT uid, id FROM command_executions
24060
- WHERE finished_at IS NULL
24061
- AND last_heartbeat_at IS NOT NULL
24062
- AND (julianday('now') - julianday(last_heartbeat_at)) * 86400 > ?
24063
- `;
24064
- const stale = resultToRows(
24065
- db.exec(staleSql, [thresholdSeconds])
24626
+ function sweepStaleAgentSessions(db, thresholdSeconds, isAlive = defaultIsAlive) {
24627
+ const candidates = resultToRows(
24628
+ db.exec(
24629
+ `SELECT uid, id, pid, started_at, workflow_id, command, last_heartbeat_at
24630
+ FROM command_executions
24631
+ WHERE finished_at IS NULL
24632
+ AND pid IS NOT NULL
24633
+ AND last_heartbeat_at IS NOT NULL
24634
+ AND (julianday('now') - julianday(last_heartbeat_at)) * 86400 > ?`,
24635
+ [thresholdSeconds]
24636
+ )
24066
24637
  );
24067
- if (stale.length === 0) {
24068
- return { orphanedIds: [] };
24638
+ if (candidates.length === 0) {
24639
+ return { orphanedIds: [], cascadedWorkflowIds: [] };
24640
+ }
24641
+ const reuseCutoffMs = Date.now() - PID_REUSE_GUARD_MS;
24642
+ const dead = candidates.filter((row) => {
24643
+ if (row.pid === null) return false;
24644
+ if (sqliteUtcMs(row.started_at) < reuseCutoffMs) return false;
24645
+ return !isAlive(row.pid);
24646
+ });
24647
+ if (dead.length === 0) {
24648
+ return { orphanedIds: [], cascadedWorkflowIds: [] };
24069
24649
  }
24070
24650
  const note = `${NOTE_ORPHAN_PREFIX} (threshold ${thresholdSeconds}s)`;
24071
- db.run(
24072
- `UPDATE command_executions
24073
- SET finished_at = datetime('now'),
24074
- exit_code = ?,
24075
- notes = COALESCE(notes || char(10), '') || ?
24076
- WHERE finished_at IS NULL
24077
- AND last_heartbeat_at IS NOT NULL
24078
- AND (julianday('now') - julianday(last_heartbeat_at)) * 86400 > ?`,
24079
- [ORPHAN_EXIT_CODE, note, thresholdSeconds]
24080
- );
24651
+ const placeholders = dead.map(() => "?").join(", ");
24652
+ const cascadedWorkflowIds = [];
24653
+ db.transaction(() => {
24654
+ db.run(
24655
+ `UPDATE command_executions
24656
+ SET finished_at = datetime('now'),
24657
+ exit_code = ?,
24658
+ pid = NULL,
24659
+ notes = COALESCE(notes || char(10), '') || ?
24660
+ WHERE id IN (${placeholders})
24661
+ AND finished_at IS NULL`,
24662
+ [ORPHAN_EXIT_CODE, note, ...dead.map((r) => r.id)]
24663
+ );
24664
+ for (const row of dead) {
24665
+ if (row.workflow_id && rowKind(row) === "supervisor") {
24666
+ cascadeTerminateExecutions(
24667
+ db,
24668
+ row.workflow_id,
24669
+ CASCADE_CLOSE_EXIT_CODE,
24670
+ "cascade-closed: workflow process orphaned by liveness sweep"
24671
+ );
24672
+ cascadedWorkflowIds.push(row.workflow_id);
24673
+ }
24674
+ }
24675
+ });
24081
24676
  return {
24082
- orphanedIds: stale.map((row) => row.uid ?? String(row.id))
24677
+ orphanedIds: dead.map((r) => r.uid ?? String(r.id)),
24678
+ cascadedWorkflowIds
24083
24679
  };
24084
24680
  }
24085
- var ORPHAN_EXIT_CODE, CANCELLED_EXIT_CODE, NOTE_ORPHAN_PREFIX;
24681
+ function rowKind(row) {
24682
+ if (row.command === INSTANCE_COMMAND || row.command.startsWith(`${INSTANCE_COMMAND}:`)) {
24683
+ return "instance";
24684
+ }
24685
+ return row.last_heartbeat_at == null ? "utility" : "supervisor";
24686
+ }
24687
+ function sweepStaleSessions(db, thresholdSeconds) {
24688
+ const sql = `
24689
+ SELECT s.id
24690
+ FROM sessions s
24691
+ LEFT JOIN (
24692
+ SELECT session_id, MAX(created_at) AS last_event_at
24693
+ FROM orchestration_events
24694
+ GROUP BY session_id
24695
+ ) e ON e.session_id = s.id
24696
+ WHERE s.status = 'active'
24697
+ AND (
24698
+ e.last_event_at IS NULL
24699
+ OR (julianday('now') - julianday(e.last_event_at)) * 86400 > ?
24700
+ )
24701
+ AND NOT EXISTS (
24702
+ SELECT 1 FROM command_executions ce
24703
+ WHERE ce.workflow_id = s.id
24704
+ AND ce.finished_at IS NULL
24705
+ )
24706
+ `;
24707
+ const rows = resultToRows(db.exec(sql, [thresholdSeconds]));
24708
+ if (rows.length === 0) {
24709
+ return { closedSessionIds: [] };
24710
+ }
24711
+ for (const row of rows) {
24712
+ commitReasonClose(
24713
+ db,
24714
+ row.id,
24715
+ {
24716
+ event_type: "session_auto_closed_stale",
24717
+ phase: "complete",
24718
+ metadata: JSON.stringify({
24719
+ reason: "no events past threshold; no in-flight dependents",
24720
+ threshold_seconds: thresholdSeconds
24721
+ })
24722
+ },
24723
+ { status: "closed", current_phase: "complete" }
24724
+ );
24725
+ }
24726
+ return { closedSessionIds: rows.map((r) => r.id) };
24727
+ }
24728
+ var NOTE_ORPHAN_PREFIX, INSTANCE_COMMAND;
24086
24729
  var init_agent_sessions = __esm({
24087
24730
  "src/lib/db/agent-sessions.ts"() {
24088
24731
  "use strict";
24089
24732
  init_result_mapper();
24090
- ORPHAN_EXIT_CODE = -3;
24091
- CANCELLED_EXIT_CODE = -2;
24733
+ init_queries();
24734
+ init_liveness();
24735
+ init_exit_codes();
24092
24736
  NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
24737
+ INSTANCE_COMMAND = "session-instance";
24093
24738
  }
24094
24739
  });
24095
24740
 
24096
24741
  // src/lib/db/command-log.ts
24097
- import { appendFileSync, existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync6 } from "node:fs";
24098
- import { dirname as dirname4, join as join12 } from "node:path";
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";
24099
24744
  import { randomUUID as randomUUID2 } from "node:crypto";
24100
24745
  function generateCommandUid() {
24101
24746
  return randomUUID2();
24102
24747
  }
24103
24748
  function cacheDir(ocrDir) {
24104
- return join12(ocrDir, "data", CACHE_DIR);
24749
+ return join13(ocrDir, "data", CACHE_DIR);
24105
24750
  }
24106
24751
  function commandLogPath(ocrDir) {
24107
- return join12(cacheDir(ocrDir), FILENAME);
24752
+ return join13(cacheDir(ocrDir), FILENAME);
24108
24753
  }
24109
24754
  function appendCommandLog(ocrDir, entry) {
24110
24755
  try {
24111
24756
  const filePath = commandLogPath(ocrDir);
24112
- const dir = dirname4(filePath);
24113
- if (!existsSync10(dir)) mkdirSync3(dir, { recursive: true });
24757
+ const dir = dirname5(filePath);
24758
+ if (!existsSync11(dir)) mkdirSync3(dir, { recursive: true });
24114
24759
  const line = JSON.stringify(entry) + "\n";
24115
24760
  appendFileSync(filePath, line, { encoding: "utf-8" });
24116
24761
  if (approxLineCount >= 0) approxLineCount++;
@@ -24120,7 +24765,7 @@ function appendCommandLog(ocrDir, entry) {
24120
24765
  }
24121
24766
  function readCommandLog(ocrDir) {
24122
24767
  const filePath = commandLogPath(ocrDir);
24123
- if (!existsSync10(filePath)) return [];
24768
+ if (!existsSync11(filePath)) return [];
24124
24769
  const content = readFileSync9(filePath, "utf-8");
24125
24770
  const entries = [];
24126
24771
  for (const line of content.split("\n")) {
@@ -24199,16 +24844,25 @@ var init_command_log = __esm({
24199
24844
  // src/lib/db/index.ts
24200
24845
  var db_exports = {};
24201
24846
  __export(db_exports, {
24847
+ CANCELLED_EXIT_CODE: () => CANCELLED_EXIT_CODE,
24848
+ CASCADE_CLOSE_EXIT_CODE: () => CASCADE_CLOSE_EXIT_CODE,
24202
24849
  MIGRATIONS: () => MIGRATIONS,
24850
+ ORPHAN_EXIT_CODE: () => ORPHAN_EXIT_CODE,
24851
+ PID_REUSE_GUARD_MS: () => PID_REUSE_GUARD_MS,
24852
+ STATE_EXIT: () => STATE_EXIT,
24853
+ StateError: () => StateError,
24203
24854
  appendCommandLog: () => appendCommandLog,
24204
- applyPragmas: () => applyPragmas,
24205
24855
  bindVendorSessionIdOpportunistically: () => bindVendorSessionIdOpportunistically,
24206
24856
  bumpAgentSessionHeartbeat: () => bumpAgentSessionHeartbeat,
24207
24857
  cacheDir: () => cacheDir,
24858
+ cascadeTerminateExecutions: () => cascadeTerminateExecutions,
24208
24859
  closeAllDatabases: () => closeAllDatabases,
24209
24860
  closeDatabase: () => closeDatabase,
24210
24861
  commandLogPath: () => commandLogPath,
24862
+ commitReasonClose: () => commitReasonClose,
24863
+ defaultIsAlive: () => defaultIsAlive,
24211
24864
  ensureDatabase: () => ensureDatabase,
24865
+ formatUpgradeNotice: () => formatUpgradeNotice,
24212
24866
  generateCommandUid: () => generateCommandUid,
24213
24867
  getAgentSession: () => getAgentSession,
24214
24868
  getAllSessions: () => getAllSessions,
@@ -24217,119 +24871,153 @@ __export(db_exports, {
24217
24871
  getLatestActiveSession: () => getLatestActiveSession,
24218
24872
  getLatestAgentSessionWithVendorId: () => getLatestAgentSessionWithVendorId,
24219
24873
  getLatestEventId: () => getLatestEventId,
24874
+ getSchemaVersion: () => getSchemaVersion,
24220
24875
  getSession: () => getSession,
24221
24876
  insertAgentSession: () => insertAgentSession,
24222
24877
  insertEvent: () => insertEvent,
24223
24878
  insertSession: () => insertSession,
24879
+ isBusyError: () => isBusyError,
24224
24880
  linkDashboardInvocationToWorkflow: () => linkDashboardInvocationToWorkflow,
24225
24881
  listAgentSessionsForWorkflow: () => listAgentSessionsForWorkflow,
24226
- locateWasm: () => locateWasm,
24227
24882
  openDatabase: () => openDatabase,
24883
+ probeEngine: () => probeEngine,
24884
+ probeWrite: () => probeWrite,
24228
24885
  readCommandLog: () => readCommandLog,
24886
+ reconcileLegacyState: () => reconcileLegacyState,
24229
24887
  recordVendorSessionIdForExecution: () => recordVendorSessionIdForExecution,
24230
24888
  replayCommandLog: () => replayCommandLog,
24231
24889
  resultToRow: () => resultToRow,
24232
24890
  resultToRows: () => resultToRows,
24891
+ rowKind: () => rowKind,
24233
24892
  runMigrations: () => runMigrations,
24234
- saveDatabase: () => saveDatabase,
24235
24893
  setAgentSessionStatus: () => setAgentSessionStatus,
24236
24894
  setAgentSessionVendorId: () => setAgentSessionVendorId,
24895
+ sqliteUtcMs: () => sqliteUtcMs,
24237
24896
  sweepStaleAgentSessions: () => sweepStaleAgentSessions,
24897
+ sweepStaleSessions: () => sweepStaleSessions,
24238
24898
  updateAgentSession: () => updateAgentSession,
24239
24899
  updateSession: () => updateSession,
24240
24900
  walCheckpointTruncate: () => walCheckpointTruncate
24241
24901
  });
24242
- import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync10, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
24243
- import { dirname as dirname5, join as join13 } from "node:path";
24244
- import { createRequire as createRequire2 } from "node:module";
24245
- import { spawnSync as spawnSync2 } from "node:child_process";
24246
- import initSqlJs from "sql.js";
24247
- function locateWasm() {
24248
- const require3 = createRequire2(import.meta.url);
24249
- const sqlJsPath = require3.resolve("sql.js");
24250
- return join13(dirname5(sqlJsPath), "sql-wasm.wasm");
24251
- }
24252
- function applyPragmas(db) {
24253
- db.run("PRAGMA foreign_keys = ON;");
24254
- db.run("PRAGMA journal_mode = WAL;");
24255
- db.run("PRAGMA busy_timeout = 5000;");
24902
+ import {
24903
+ existsSync as existsSync12,
24904
+ mkdirSync as mkdirSync4,
24905
+ copyFileSync,
24906
+ statSync,
24907
+ mkdtempSync,
24908
+ rmSync
24909
+ } from "node:fs";
24910
+ import { tmpdir } from "node:os";
24911
+ import { dirname as dirname6, join as join14 } from "node:path";
24912
+ function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
24913
+ if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
24914
+ const bakPath = `${dbPath}.bak.v${fromVersion}`;
24915
+ if (existsSync12(bakPath)) return bakPath;
24916
+ try {
24917
+ if (!existsSync12(dbPath) || statSync(dbPath).size === 0) return null;
24918
+ db.pragma("wal_checkpoint(TRUNCATE)");
24919
+ copyFileSync(dbPath, bakPath);
24920
+ return bakPath;
24921
+ } catch {
24922
+ return null;
24923
+ }
24924
+ }
24925
+ function formatUpgradeNotice(bakPath, reconcile) {
24926
+ const lines = [
24927
+ "Storage upgraded to v2.0 \u2014 durable SQLite engine (WAL), event-sourced lifecycle."
24928
+ ];
24929
+ if (bakPath) {
24930
+ lines.push(` A backup of your previous database was saved to: ${bakPath}`);
24931
+ }
24932
+ const repairs = (reconcile?.actions ?? []).filter((a) => a.kind !== "ok");
24933
+ if (repairs.length > 0) {
24934
+ const n = (kind) => repairs.filter((a) => a.kind === kind).length;
24935
+ const parts = [];
24936
+ const finalized = n("synthesize-round-completed") + n("synthesize-map-completed");
24937
+ if (finalized > 0) parts.push(`${finalized} finalized from artifacts`);
24938
+ if (n("grandfather") > 0) parts.push(`${n("grandfather")} grandfathered`);
24939
+ if (n("stale-close") > 0) parts.push(`${n("stale-close")} stale closed`);
24940
+ lines.push(
24941
+ ` Reconciled ${repairs.length} legacy session(s): ${parts.join(", ")}.`
24942
+ );
24943
+ }
24944
+ lines.push(" Run `ocr doctor` to verify the storage engine.");
24945
+ return lines.map((l) => `[ocr] ${l}`).join("\n");
24256
24946
  }
24257
24947
  async function openDatabase(dbPath) {
24258
24948
  const cached = connections.get(dbPath);
24259
24949
  if (cached) {
24260
24950
  return cached;
24261
24951
  }
24262
- const wasmBuffer = readFileSync10(locateWasm());
24263
- const wasmBinary = wasmBuffer.buffer.slice(
24264
- wasmBuffer.byteOffset,
24265
- wasmBuffer.byteOffset + wasmBuffer.byteLength
24266
- );
24267
- const SQL = await initSqlJs({
24268
- wasmBinary
24269
- });
24270
- let db;
24271
- if (existsSync11(dbPath)) {
24272
- const fileBuffer = readFileSync10(dbPath);
24273
- db = new SQL.Database(fileBuffer);
24274
- } else {
24275
- db = new SQL.Database();
24952
+ const dir = dirname6(dbPath);
24953
+ if (!existsSync12(dir)) {
24954
+ mkdirSync4(dir, { recursive: true });
24276
24955
  }
24277
- applyPragmas(db);
24956
+ const db = openEngine(dbPath);
24278
24957
  connections.set(dbPath, db);
24279
24958
  return db;
24280
24959
  }
24281
- function saveDatabase(db, dbPath) {
24282
- const data = db.export();
24283
- const dir = dirname5(dbPath);
24284
- if (!existsSync11(dir)) {
24285
- mkdirSync4(dir, { recursive: true });
24286
- }
24287
- const tmpPath = `${dbPath}.${process.pid}.tmp`;
24288
- writeFileSync7(tmpPath, Buffer.from(data));
24289
- renameSync2(tmpPath, dbPath);
24290
- }
24291
24960
  async function getDb(ocrDir) {
24292
- const dbPath = join13(ocrDir, "data", "ocr.db");
24961
+ const dbPath = join14(ocrDir, "data", "ocr.db");
24293
24962
  return openDatabase(dbPath);
24294
24963
  }
24295
24964
  async function ensureDatabase(ocrDir) {
24296
- const dataDir = join13(ocrDir, "data");
24297
- if (!existsSync11(dataDir)) {
24965
+ const dataDir = join14(ocrDir, "data");
24966
+ if (!existsSync12(dataDir)) {
24298
24967
  mkdirSync4(dataDir, { recursive: true });
24299
24968
  }
24300
- const dbPath = join13(dataDir, "ocr.db");
24969
+ const dbPath = join14(dataDir, "ocr.db");
24301
24970
  const db = await openDatabase(dbPath);
24971
+ let before = 0;
24972
+ try {
24973
+ before = getSchemaVersion(db);
24974
+ } catch {
24975
+ before = 0;
24976
+ }
24977
+ const isLegacyUpgrade = before >= 1 && before < V2_SCHEMA_VERSION;
24978
+ const bakPath = maybeSnapshotBeforeUpgrade(db, dbPath, before);
24302
24979
  runMigrations(db);
24303
- saveDatabase(db, dbPath);
24980
+ let reconcile;
24981
+ if (before < V2_SCHEMA_VERSION) {
24982
+ try {
24983
+ reconcile = reconcileLegacyState(db, ocrDir);
24984
+ } catch (err) {
24985
+ console.error(
24986
+ `[ocr] legacy reconciliation skipped: ${err instanceof Error ? err.message : String(err)}`
24987
+ );
24988
+ }
24989
+ }
24990
+ if (isLegacyUpgrade) {
24991
+ const notice = formatUpgradeNotice(bakPath, reconcile);
24992
+ if (notice) console.error(notice);
24993
+ }
24304
24994
  return db;
24305
24995
  }
24306
24996
  function walCheckpointTruncate(dbPath) {
24307
- if (!existsSync11(dbPath)) {
24997
+ if (!existsSync12(dbPath)) {
24308
24998
  return "skipped";
24309
24999
  }
24310
- try {
24311
- const probe = spawnSync2("sqlite3", ["-version"], {
24312
- stdio: "ignore",
24313
- timeout: 2e3
24314
- });
24315
- if (probe.status !== 0) {
24316
- return "skipped";
25000
+ const cached = connections.get(dbPath);
25001
+ if (cached) {
25002
+ try {
25003
+ cached.pragma("wal_checkpoint(TRUNCATE)");
25004
+ return "checkpointed";
25005
+ } catch {
25006
+ return "failed";
24317
25007
  }
24318
- } catch {
24319
- return "skipped";
24320
25008
  }
25009
+ let transient;
24321
25010
  try {
24322
- const result = spawnSync2(
24323
- "sqlite3",
24324
- [dbPath, "PRAGMA wal_checkpoint(TRUNCATE);"],
24325
- {
24326
- stdio: "ignore",
24327
- timeout: 5e3
24328
- }
24329
- );
24330
- return result.status === 0 ? "checkpointed" : "failed";
25011
+ transient = openEngine(dbPath);
25012
+ transient.pragma("wal_checkpoint(TRUNCATE)");
25013
+ return "checkpointed";
24331
25014
  } catch {
24332
25015
  return "failed";
25016
+ } finally {
25017
+ try {
25018
+ transient?.close();
25019
+ } catch {
25020
+ }
24333
25021
  }
24334
25022
  }
24335
25023
  function closeDatabase(dbPath) {
@@ -24345,20 +25033,70 @@ function closeAllDatabases() {
24345
25033
  connections.delete(path2);
24346
25034
  }
24347
25035
  }
24348
- var connections;
25036
+ function probeWrite() {
25037
+ let dir;
25038
+ try {
25039
+ dir = mkdtempSync(join14(tmpdir(), "ocr-probe-"));
25040
+ const db = openEngine(join14(dir, "probe.db"));
25041
+ try {
25042
+ db.run("CREATE TABLE _probe_write (id INTEGER PRIMARY KEY, v TEXT)");
25043
+ db.transaction(() => {
25044
+ db.run("INSERT INTO _probe_write (v) VALUES (?)", ["written-in-txn"]);
25045
+ });
25046
+ const value = db.exec("SELECT v FROM _probe_write")[0]?.values[0]?.[0];
25047
+ if (value !== "written-in-txn") {
25048
+ return { ok: false, error: `unexpected probe value: ${String(value)}` };
25049
+ }
25050
+ return { ok: true };
25051
+ } finally {
25052
+ db.close();
25053
+ }
25054
+ } catch (e) {
25055
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
25056
+ } finally {
25057
+ if (dir) rmDirBestEffort(dir);
25058
+ }
25059
+ }
25060
+ function rmDirBestEffort(dir) {
25061
+ for (let attempt = 0; attempt < 3; attempt++) {
25062
+ try {
25063
+ rmSync(dir, { recursive: true, force: true });
25064
+ return;
25065
+ } catch {
25066
+ if (attempt === 2) return;
25067
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
25068
+ }
25069
+ }
25070
+ }
25071
+ var V2_SCHEMA_VERSION, connections;
24349
25072
  var init_db = __esm({
24350
25073
  "src/lib/db/index.ts"() {
24351
25074
  "use strict";
25075
+ init_engine();
24352
25076
  init_migrations();
25077
+ init_reconcile();
24353
25078
  init_queries();
24354
25079
  init_agent_sessions();
25080
+ init_liveness();
25081
+ init_exit_codes();
24355
25082
  init_migrations();
24356
25083
  init_result_mapper();
25084
+ init_engine();
25085
+ init_reconcile();
25086
+ init_migrations();
24357
25087
  init_command_log();
25088
+ V2_SCHEMA_VERSION = 12;
24358
25089
  connections = /* @__PURE__ */ new Map();
24359
25090
  }
24360
25091
  });
24361
25092
 
25093
+ // src/lib/runtime-guard.ts
25094
+ init_runtime_checks();
25095
+ if (!isSupportedNode(process.versions.node)) {
25096
+ process.stderr.write(nodeVersionGuardMessage(process.versions.node));
25097
+ process.exit(1);
25098
+ }
25099
+
24362
25100
  // ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/esm.mjs
24363
25101
  var import_index = __toESM(require_commander(), 1);
24364
25102
  var {
@@ -28763,7 +29501,7 @@ ${hint}
28763
29501
  }
28764
29502
 
28765
29503
  // src/lib/version.ts
28766
- var CLI_VERSION = true ? "1.11.0" : createRequire(import.meta.url)("../../package.json").version;
29504
+ var CLI_VERSION = true ? "2.1.0" : createRequire(import.meta.url)("../../package.json").version;
28767
29505
 
28768
29506
  // ../shared/platform/src/index.ts
28769
29507
  import { pathToFileURL } from "node:url";
@@ -29822,9 +30560,9 @@ var NodeFsHandler = class {
29822
30560
  if (this.fsw.closed) {
29823
30561
  return;
29824
30562
  }
29825
- const dirname7 = sysPath.dirname(file);
30563
+ const dirname8 = sysPath.dirname(file);
29826
30564
  const basename8 = sysPath.basename(file);
29827
- const parent = this.fsw._getWatchedDir(dirname7);
30565
+ const parent = this.fsw._getWatchedDir(dirname8);
29828
30566
  let prevStats = stats;
29829
30567
  if (parent.has(basename8))
29830
30568
  return;
@@ -29851,7 +30589,7 @@ var NodeFsHandler = class {
29851
30589
  prevStats = newStats2;
29852
30590
  }
29853
30591
  } catch (error) {
29854
- this.fsw._remove(dirname7, basename8);
30592
+ this.fsw._remove(dirname8, basename8);
29855
30593
  }
29856
30594
  } else if (parent.has(basename8)) {
29857
30595
  const at = newStats.atimeMs;
@@ -30784,8 +31522,8 @@ function watch(paths, options = {}) {
30784
31522
  }
30785
31523
 
30786
31524
  // src/commands/progress.ts
30787
- import { existsSync as existsSync12, readdirSync as readdirSync5, statSync } from "node:fs";
30788
- import { join as join14, basename as basename7 } from "node:path";
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";
30789
31527
 
30790
31528
  // ../../node_modules/.pnpm/log-update@7.0.2/node_modules/log-update/index.js
30791
31529
  import process12 from "node:process";
@@ -32421,15 +33159,15 @@ function debounce(fn, delay) {
32421
33159
  };
32422
33160
  }
32423
33161
  function findLatestActiveSession(sessionsDir) {
32424
- if (!existsSync12(sessionsDir)) {
33162
+ if (!existsSync13(sessionsDir)) {
32425
33163
  return null;
32426
33164
  }
32427
33165
  const sessions = readdirSync5(sessionsDir).filter((name) => {
32428
- const sessionPath = join14(sessionsDir, name);
32429
- return statSync(sessionPath).isDirectory();
33166
+ const sessionPath = join15(sessionsDir, name);
33167
+ return statSync2(sessionPath).isDirectory();
32430
33168
  }).sort().reverse();
32431
33169
  for (const session of sessions) {
32432
- const sessionPath = join14(sessionsDir, session);
33170
+ const sessionPath = join15(sessionsDir, session);
32433
33171
  if (isSessionActive(sessionPath)) {
32434
33172
  return session;
32435
33173
  }
@@ -32444,8 +33182,8 @@ function getStrategyForSession(sessionPath, explicitWorkflow) {
32444
33182
  return getStrategy(workflowType) ?? null;
32445
33183
  }
32446
33184
  async function initProgressDb(ocrDir) {
32447
- const dbPath = join14(ocrDir, "data", "ocr.db");
32448
- if (!existsSync12(dbPath)) {
33185
+ const dbPath = join15(ocrDir, "data", "ocr.db");
33186
+ if (!existsSync13(dbPath)) {
32449
33187
  return;
32450
33188
  }
32451
33189
  try {
@@ -32470,11 +33208,11 @@ var progressCommand = new Command("progress").description("Watch real-time progr
32470
33208
  const targetDir = process.cwd();
32471
33209
  requireOcrSetup(targetDir);
32472
33210
  const sessionsDir = ensureSessionsDir(targetDir);
32473
- const ocrDir = join14(targetDir, ".ocr");
33211
+ const ocrDir = join15(targetDir, ".ocr");
32474
33212
  await initProgressDb(ocrDir);
32475
33213
  if (options.session) {
32476
- const sessionPath = join14(sessionsDir, options.session);
32477
- if (!existsSync12(sessionPath)) {
33214
+ const sessionPath = join15(sessionsDir, options.session);
33215
+ if (!existsSync13(sessionPath)) {
32478
33216
  console.error(source_default.red(`Session not found: ${options.session}`));
32479
33217
  process.exit(1);
32480
33218
  }
@@ -32499,7 +33237,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
32499
33237
  );
32500
33238
  console.error(
32501
33239
  source_default.dim(
32502
- `The orchestrating agent must create state via 'ocr state init' for progress tracking.`
33240
+ `The orchestrating agent must create state via 'ocr state begin' for progress tracking.`
32503
33241
  )
32504
33242
  );
32505
33243
  process.exit(1);
@@ -32535,7 +33273,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
32535
33273
  return;
32536
33274
  }
32537
33275
  let currentSession = findLatestActiveSession(sessionsDir);
32538
- let currentSessionPath = currentSession ? join14(sessionsDir, currentSession) : null;
33276
+ let currentSessionPath = currentSession ? join15(sessionsDir, currentSession) : null;
32539
33277
  let sessionWatcher = null;
32540
33278
  const preservedStartTimes = {
32541
33279
  review: void 0,
@@ -32543,11 +33281,11 @@ var progressCommand = new Command("progress").description("Watch real-time progr
32543
33281
  };
32544
33282
  let currentStrategy = null;
32545
33283
  const updateDisplayImpl = () => {
32546
- if (!currentSessionPath || !existsSync12(currentSessionPath) || !isSessionActive(currentSessionPath)) {
33284
+ if (!currentSessionPath || !existsSync13(currentSessionPath) || !isSessionActive(currentSessionPath)) {
32547
33285
  const latestActive = findLatestActiveSession(sessionsDir);
32548
33286
  if (latestActive && latestActive !== currentSession) {
32549
33287
  currentSession = latestActive;
32550
- currentSessionPath = join14(sessionsDir, latestActive);
33288
+ currentSessionPath = join15(sessionsDir, latestActive);
32551
33289
  preservedStartTimes.review = void 0;
32552
33290
  preservedStartTimes.map = void 0;
32553
33291
  currentStrategy = null;
@@ -32560,7 +33298,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
32560
33298
  currentStrategy = null;
32561
33299
  }
32562
33300
  }
32563
- if (currentSessionPath && existsSync12(currentSessionPath)) {
33301
+ if (currentSessionPath && existsSync13(currentSessionPath)) {
32564
33302
  if (!options.workflow) {
32565
33303
  const activeWorkflows = detectActiveWorkflows(currentSessionPath);
32566
33304
  if (activeWorkflows.length > 1) {
@@ -32614,15 +33352,15 @@ var progressCommand = new Command("progress").description("Watch real-time progr
32614
33352
  watchSession(currentSessionPath);
32615
33353
  }
32616
33354
  const timerInterval = setInterval(updateDisplay, 1e3);
32617
- const watchDir = existsSync12(ocrDir) ? ocrDir : targetDir;
33355
+ const watchDir = existsSync13(ocrDir) ? ocrDir : targetDir;
32618
33356
  const dirWatcher = watch(watchDir, {
32619
33357
  persistent: true,
32620
33358
  ignoreInitial: true,
32621
33359
  depth: 3
32622
33360
  });
32623
33361
  dirWatcher.on("addDir", (dirPath) => {
32624
- const parentDir = join14(dirPath, "..");
32625
- const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(join14(".ocr", "sessions"));
33362
+ const parentDir = join15(dirPath, "..");
33363
+ const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(join15(".ocr", "sessions"));
32626
33364
  if (isDirectChild && !dirPath.endsWith("sessions")) {
32627
33365
  const newSession = basename7(dirPath);
32628
33366
  currentSession = newSession;
@@ -32724,226 +33462,115 @@ function renderCombinedProgress(sessionPath, preservedStartTimes, ocrDir) {
32724
33462
  }
32725
33463
 
32726
33464
  // src/commands/state.ts
32727
- import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync12 } from "node:fs";
32728
- import { join as join16 } from "node:path";
33465
+ import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync11 } from "node:fs";
33466
+ import { join as join17 } from "node:path";
32729
33467
 
32730
33468
  // src/lib/state/index.ts
32731
33469
  init_db();
33470
+ init_exit_codes();
32732
33471
  import {
32733
- existsSync as existsSync13,
33472
+ existsSync as existsSync14,
32734
33473
  mkdirSync as mkdirSync5,
32735
33474
  readdirSync as readdirSync6,
32736
- readFileSync as readFileSync11,
32737
- statSync as statSync2,
32738
- writeFileSync as writeFileSync8
33475
+ readFileSync as readFileSync10,
33476
+ statSync as statSync3,
33477
+ writeFileSync as writeFileSync7
32739
33478
  } from "node:fs";
32740
- import { join as join15 } from "node:path";
32741
- function hasArtifacts(dir) {
32742
- try {
32743
- for (const entry of readdirSync6(dir, { withFileTypes: true })) {
32744
- if (entry.isDirectory()) {
32745
- if (hasArtifacts(join15(dir, entry.name))) return true;
32746
- } else if (/\.(md|json)$/.test(entry.name)) {
32747
- return true;
32748
- }
32749
- }
32750
- } catch {
32751
- }
32752
- return false;
32753
- }
32754
- async function stateInit(params) {
32755
- const { sessionId, branch, workflowType, sessionDir, ocrDir } = params;
32756
- const db = await ensureDatabase(ocrDir);
32757
- const dbPath = join15(ocrDir, "data", "ocr.db");
32758
- const existing = getSession(db, sessionId);
32759
- if (existing) {
32760
- const roundsDir = join15(sessionDir, "rounds");
32761
- let nextRound = 1;
32762
- if (existsSync13(roundsDir)) {
32763
- const roundDirs = readdirSync6(roundsDir).filter((d) => /^round-\d+$/.test(d)).map((d) => parseInt(d.replace("round-", ""), 10)).sort((a, b) => a - b);
32764
- if (roundDirs.length > 0) {
32765
- const highest = roundDirs[roundDirs.length - 1];
32766
- const hasFinal = existsSync13(
32767
- join15(roundsDir, `round-${highest}`, "final.md")
32768
- );
32769
- nextRound = hasFinal ? highest + 1 : highest;
32770
- }
32771
- }
32772
- updateSession(db, sessionId, {
32773
- status: "active",
32774
- current_phase: "context",
32775
- phase_number: 1,
32776
- current_round: nextRound
32777
- });
32778
- insertEvent(db, {
32779
- session_id: sessionId,
32780
- event_type: nextRound > (existing.current_round ?? 1) ? "round_started" : "session_resumed",
32781
- phase: "context",
32782
- phase_number: 1,
32783
- round: nextRound
32784
- });
32785
- saveDatabase(db, dbPath);
32786
- return sessionId;
32787
- }
32788
- insertSession(db, {
32789
- id: sessionId,
32790
- branch,
32791
- workflow_type: workflowType,
32792
- current_phase: "context",
32793
- phase_number: 1,
32794
- current_round: 1,
32795
- current_map_run: 1,
32796
- session_dir: sessionDir
32797
- });
32798
- insertEvent(db, {
32799
- session_id: sessionId,
32800
- event_type: "session_created",
32801
- phase: "context",
32802
- phase_number: 1,
32803
- round: 1
32804
- });
32805
- saveDatabase(db, dbPath);
32806
- return sessionId;
32807
- }
32808
- async function stateTransition(params) {
32809
- const { sessionId, phase, phaseNumber, round, mapRun, ocrDir } = params;
32810
- const db = await ensureDatabase(ocrDir);
32811
- const dbPath = join15(ocrDir, "data", "ocr.db");
32812
- const existing = getSession(db, sessionId);
32813
- if (!existing) {
32814
- throw new Error(`Session not found: ${sessionId}`);
32815
- }
32816
- const previousRound = existing.current_round;
32817
- updateSession(db, sessionId, {
32818
- current_phase: phase,
32819
- phase_number: phaseNumber,
32820
- ...round !== void 0 ? { current_round: round } : {},
32821
- ...mapRun !== void 0 ? { current_map_run: mapRun } : {}
32822
- });
32823
- insertEvent(db, {
32824
- session_id: sessionId,
32825
- event_type: "phase_transition",
32826
- phase,
32827
- phase_number: phaseNumber,
32828
- round: round ?? existing.current_round
32829
- });
32830
- if (round !== void 0 && round !== previousRound) {
32831
- insertEvent(db, {
32832
- session_id: sessionId,
32833
- event_type: "round_started",
32834
- phase,
32835
- phase_number: phaseNumber,
32836
- round
32837
- });
33479
+ import { join as join16 } from "node:path";
33480
+
33481
+ // src/lib/state/phase-graph.ts
33482
+ init_exit_codes();
33483
+ var REVIEW_PHASE_NUMBERS = {
33484
+ context: 1,
33485
+ "change-context": 2,
33486
+ analysis: 3,
33487
+ reviews: 4,
33488
+ aggregation: 5,
33489
+ discourse: 6,
33490
+ synthesis: 7,
33491
+ complete: 8
33492
+ };
33493
+ var MAP_PHASE_NUMBERS = {
33494
+ "map-context": 1,
33495
+ topology: 2,
33496
+ "flow-analysis": 3,
33497
+ "requirements-mapping": 4,
33498
+ synthesis: 5,
33499
+ complete: 6
33500
+ };
33501
+ function phaseNumberFor(workflowType, phase) {
33502
+ const map = workflowType === "map" ? MAP_PHASE_NUMBERS : REVIEW_PHASE_NUMBERS;
33503
+ const n = map[phase];
33504
+ if (n === void 0) {
33505
+ throw new StateError(
33506
+ STATE_EXIT.ILLEGAL_TRANSITION,
33507
+ `Invalid phase "${phase}" for workflow_type "${workflowType}". Valid: ${Object.keys(map).join(", ")}`
33508
+ );
32838
33509
  }
32839
- saveDatabase(db, dbPath);
33510
+ return n;
32840
33511
  }
32841
- async function stateClose(params) {
32842
- const { sessionId, ocrDir } = params;
32843
- const db = await ensureDatabase(ocrDir);
32844
- const dbPath = join15(ocrDir, "data", "ocr.db");
32845
- const existing = getSession(db, sessionId);
32846
- if (!existing) {
32847
- throw new Error(`Session not found: ${sessionId}`);
32848
- }
32849
- updateSession(db, sessionId, {
32850
- status: "closed",
32851
- current_phase: "complete"
32852
- });
32853
- insertEvent(db, {
32854
- session_id: sessionId,
32855
- event_type: "session_closed",
32856
- phase: "complete",
32857
- phase_number: existing.phase_number,
32858
- round: existing.current_round
32859
- });
32860
- saveDatabase(db, dbPath);
33512
+ var REVIEW_PHASE_GRAPH = {
33513
+ context: ["change-context"],
33514
+ "change-context": ["analysis"],
33515
+ analysis: ["reviews"],
33516
+ reviews: ["aggregation"],
33517
+ aggregation: ["discourse"],
33518
+ discourse: ["synthesis"],
33519
+ synthesis: ["complete"],
33520
+ complete: ["context"]
33521
+ };
33522
+ var MAP_PHASE_GRAPH = {
33523
+ "map-context": ["topology"],
33524
+ topology: ["flow-analysis"],
33525
+ "flow-analysis": ["requirements-mapping"],
33526
+ "requirements-mapping": ["synthesis"],
33527
+ synthesis: ["complete"],
33528
+ complete: ["map-context"]
33529
+ };
33530
+ function graphFor(workflowType) {
33531
+ return workflowType === "review" ? REVIEW_PHASE_GRAPH : MAP_PHASE_GRAPH;
32861
33532
  }
32862
- async function stateShow(ocrDir, sessionId) {
32863
- let db;
32864
- try {
32865
- db = await ensureDatabase(ocrDir);
32866
- } catch {
32867
- return null;
32868
- }
32869
- const session = sessionId ? getSession(db, sessionId) : getLatestActiveSession(db);
32870
- if (!session) {
32871
- return null;
32872
- }
32873
- const events = getEventsForSession(db, session.id);
32874
- return {
32875
- session: {
32876
- id: session.id,
32877
- branch: session.branch,
32878
- status: session.status,
32879
- workflow_type: session.workflow_type,
32880
- current_phase: session.current_phase,
32881
- phase_number: session.phase_number,
32882
- current_round: session.current_round,
32883
- current_map_run: session.current_map_run,
32884
- started_at: session.started_at,
32885
- updated_at: session.updated_at
32886
- },
32887
- events: events.map((e) => ({
32888
- id: e.id,
32889
- event_type: e.event_type,
32890
- phase: e.phase,
32891
- phase_number: e.phase_number,
32892
- round: e.round,
32893
- metadata: e.metadata,
32894
- created_at: e.created_at
32895
- }))
32896
- };
33533
+ function initialPhaseFor(workflowType) {
33534
+ return workflowType === "map" ? "map-context" : "context";
32897
33535
  }
32898
- async function resolveActiveSession(ocrDir) {
32899
- const db = await ensureDatabase(ocrDir);
32900
- const session = getLatestActiveSession(db);
32901
- if (!session) {
32902
- throw new Error("No active session found");
33536
+ function validatePhaseTransition(workflowType, source, target, isRoundBoundary) {
33537
+ const graph = graphFor(workflowType);
33538
+ if (!(target in graph)) {
33539
+ const validPhases = Object.keys(graph).join(", ");
33540
+ throw new StateError(
33541
+ STATE_EXIT.ILLEGAL_TRANSITION,
33542
+ `Invalid phase "${target}" for workflow_type "${workflowType}". Valid phases: ${validPhases}`
33543
+ );
32903
33544
  }
32904
- return {
32905
- id: session.id,
32906
- sessionDir: session.session_dir
32907
- };
32908
- }
32909
- function readJsonFromSource(params) {
32910
- if (params.source === "file") {
32911
- if (!existsSync13(params.filePath)) {
32912
- throw new Error(`File not found: ${params.filePath}`);
32913
- }
32914
- return readFileSync11(params.filePath, "utf-8");
33545
+ if (source === target) return;
33546
+ if (isRoundBoundary) {
33547
+ const initial = initialPhaseFor(workflowType);
33548
+ if (target === initial) return;
33549
+ throw new StateError(
33550
+ STATE_EXIT.ILLEGAL_TRANSITION,
33551
+ `Illegal round-boundary transition: a new round/run must reset to "${initial}", not "${target}".`
33552
+ );
32915
33553
  }
32916
- return params.data;
32917
- }
32918
- function parseRawJson(raw, label) {
32919
- try {
32920
- return JSON.parse(raw);
32921
- } catch (err) {
32922
- throw new Error(
32923
- `Failed to parse ${label}: ${err instanceof Error ? err.message : "invalid JSON"}`
33554
+ const allowed = graph[source];
33555
+ if (!allowed || !allowed.includes(target)) {
33556
+ throw new StateError(
33557
+ STATE_EXIT.ILLEGAL_TRANSITION,
33558
+ `Illegal phase transition: ${source} \u2192 ${target}. From "${source}", only ${allowed && allowed.length > 0 ? allowed.join(", ") : "(no edges)"} are reachable. Pass --current-round to start a new round if the workflow is resetting.`
32924
33559
  );
32925
33560
  }
32926
33561
  }
32927
- function resolveSessionForCompletion(db, explicitId) {
32928
- if (explicitId) {
32929
- const existing = getSession(db, explicitId);
32930
- if (!existing) throw new Error(`Session not found: ${explicitId}`);
32931
- return {
32932
- id: existing.id,
32933
- session_dir: existing.session_dir,
32934
- current_round: existing.current_round,
32935
- current_map_run: existing.current_map_run
32936
- };
32937
- }
32938
- const active = getLatestActiveSession(db);
32939
- if (!active) throw new Error("No active session found");
32940
- return {
32941
- id: active.id,
32942
- session_dir: active.session_dir,
32943
- current_round: active.current_round,
32944
- current_map_run: active.current_map_run
32945
- };
33562
+
33563
+ // src/lib/state/meta-util.ts
33564
+ var DEFAULT_METADATA_MAX_LEN = 4096;
33565
+ function sanitizeMetadataString(s, opts = {}) {
33566
+ const maxLen = opts.maxLen ?? DEFAULT_METADATA_MAX_LEN;
33567
+ let out = s.replace(/[\x00-\x08\x0b-\x1f]/g, "");
33568
+ out = out.replace(/^\s*\[ocr\]\s*/i, "");
33569
+ if (out.length > maxLen) out = out.slice(0, maxLen);
33570
+ return out;
32946
33571
  }
33572
+
33573
+ // src/lib/state/round-meta.ts
32947
33574
  var VALID_CATEGORIES = /* @__PURE__ */ new Set(["blocker", "should_fix", "suggestion", "style"]);
32948
33575
  var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low", "info"]);
32949
33576
  function validateRoundMeta(meta) {
@@ -32959,6 +33586,7 @@ function validateRoundMeta(meta) {
32959
33586
  if (typeof obj.verdict !== "string" || obj.verdict.trim().length === 0) {
32960
33587
  throw new Error("round-meta.json must contain a non-empty verdict string");
32961
33588
  }
33589
+ obj.verdict = sanitizeMetadataString(obj.verdict);
32962
33590
  if (!Array.isArray(obj.reviewers)) {
32963
33591
  throw new Error("round-meta.json must contain a reviewers array");
32964
33592
  }
@@ -32984,6 +33612,7 @@ function validateRoundMeta(meta) {
32984
33612
  if (typeof f.title !== "string" || f.title.trim().length === 0) {
32985
33613
  throw new Error("Each finding must have a non-empty title");
32986
33614
  }
33615
+ f.title = sanitizeMetadataString(f.title);
32987
33616
  if (typeof f.category !== "string" || !VALID_CATEGORIES.has(f.category)) {
32988
33617
  throw new Error(
32989
33618
  `Finding "${f.title}" has invalid category: "${String(f.category)}". Must be one of: ${[...VALID_CATEGORIES].join(", ")}`
@@ -32997,6 +33626,7 @@ function validateRoundMeta(meta) {
32997
33626
  if (typeof f.summary !== "string") {
32998
33627
  throw new Error(`Finding "${f.title}" must have a summary string`);
32999
33628
  }
33629
+ f.summary = sanitizeMetadataString(f.summary);
33000
33630
  if (f.file_path !== void 0 && typeof f.file_path !== "string") {
33001
33631
  throw new Error(`Finding "${f.title}" has invalid file_path: expected string`);
33002
33632
  }
@@ -33042,43 +33672,8 @@ function computeRoundCounts(meta) {
33042
33672
  totalFindingCount: allFindings.length
33043
33673
  };
33044
33674
  }
33045
- async function stateRoundComplete(params) {
33046
- const { ocrDir } = params;
33047
- const db = await ensureDatabase(ocrDir);
33048
- const dbPath = join15(ocrDir, "data", "ocr.db");
33049
- const rawJsonString = readJsonFromSource(params);
33050
- const label = params.source === "file" ? params.filePath : "stdin";
33051
- const raw = parseRawJson(rawJsonString, label);
33052
- const meta = validateRoundMeta(raw);
33053
- const counts = computeRoundCounts(meta);
33054
- const session = resolveSessionForCompletion(db, params.sessionId);
33055
- const roundNumber = params.round ?? session.current_round;
33056
- let metaPath;
33057
- if (params.source === "stdin") {
33058
- const roundDir = join15(session.session_dir, "rounds", `round-${roundNumber}`);
33059
- mkdirSync5(roundDir, { recursive: true });
33060
- metaPath = join15(roundDir, "round-meta.json");
33061
- writeFileSync8(metaPath, JSON.stringify(meta, null, 2));
33062
- }
33063
- insertEvent(db, {
33064
- session_id: session.id,
33065
- event_type: "round_completed",
33066
- phase: "synthesis",
33067
- phase_number: 7,
33068
- round: roundNumber,
33069
- metadata: JSON.stringify({
33070
- verdict: meta.verdict,
33071
- blocker_count: counts.blockerCount,
33072
- should_fix_count: counts.shouldFixCount,
33073
- suggestion_count: counts.suggestionCount,
33074
- reviewer_count: counts.reviewerCount,
33075
- total_finding_count: counts.totalFindingCount,
33076
- source: "orchestrator"
33077
- })
33078
- });
33079
- saveDatabase(db, dbPath);
33080
- return { sessionId: session.id, round: roundNumber, metaPath };
33081
- }
33675
+
33676
+ // src/lib/state/map-meta.ts
33082
33677
  function validateMapMeta(meta) {
33083
33678
  if (!meta || typeof meta !== "object") {
33084
33679
  throw new Error("map-meta.json must be a JSON object");
@@ -33103,6 +33698,13 @@ function validateMapMeta(meta) {
33103
33698
  if (typeof s.title !== "string" || s.title.trim().length === 0) {
33104
33699
  throw new Error("Each section must have a non-empty title");
33105
33700
  }
33701
+ s.title = sanitizeMetadataString(s.title);
33702
+ if (s.description !== void 0) {
33703
+ if (typeof s.description !== "string") {
33704
+ throw new Error(`Section "${s.title}" description must be a string if provided`);
33705
+ }
33706
+ s.description = sanitizeMetadataString(s.description);
33707
+ }
33106
33708
  if (!Array.isArray(s.files)) {
33107
33709
  throw new Error(`Section "${s.title}" must have a files array`);
33108
33710
  }
@@ -33117,6 +33719,7 @@ function validateMapMeta(meta) {
33117
33719
  if (typeof f.role !== "string") {
33118
33720
  throw new Error(`File "${f.file_path}" must have a role string`);
33119
33721
  }
33722
+ f.role = sanitizeMetadataString(f.role);
33120
33723
  if (typeof f.lines_added !== "number") {
33121
33724
  throw new Error(`File "${f.file_path}" must have a lines_added number`);
33122
33725
  }
@@ -33136,53 +33739,597 @@ function computeMapCounts(meta) {
33136
33739
  fileCount: meta.sections.reduce((sum, s) => sum + s.files.length, 0)
33137
33740
  };
33138
33741
  }
33139
- async function stateMapComplete(params) {
33742
+
33743
+ // src/lib/state/projection.ts
33744
+ init_db();
33745
+ var REASON_EVENT_TYPES = [
33746
+ "session_aborted",
33747
+ "session_auto_closed_stale",
33748
+ "session_synced",
33749
+ "session_legacy_import"
33750
+ ];
33751
+ var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
33752
+ "session_closed",
33753
+ ...REASON_EVENT_TYPES
33754
+ ]);
33755
+ function hasCompletionInvariant(db, session) {
33756
+ const eventType = session.workflow_type === "map" ? "map_completed" : "round_completed";
33757
+ const round = session.workflow_type === "map" ? session.current_map_run : session.current_round;
33758
+ const r = db.exec(
33759
+ `SELECT 1 FROM orchestration_events
33760
+ WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
33761
+ [session.id, eventType, round]
33762
+ );
33763
+ return (r[0]?.values.length ?? 0) > 0;
33764
+ }
33765
+ function getCompletenessState(db, sessionId) {
33766
+ const r = db.exec(
33767
+ "SELECT completeness_state FROM session_completeness WHERE session_id = ?",
33768
+ [sessionId]
33769
+ );
33770
+ return r[0]?.values[0]?.[0] ?? null;
33771
+ }
33772
+
33773
+ // src/lib/state/index.ts
33774
+ init_exit_codes();
33775
+ function deriveNextRound(db, sessionId, fallbackRound) {
33776
+ const result = db.exec(
33777
+ `SELECT MAX(round) FROM orchestration_events
33778
+ WHERE session_id = ? AND event_type = 'round_completed'`,
33779
+ [sessionId]
33780
+ );
33781
+ const max = result[0]?.values[0]?.[0];
33782
+ if (typeof max === "number") return max + 1;
33783
+ return fallbackRound;
33784
+ }
33785
+ function hasArtifacts(dir) {
33786
+ try {
33787
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
33788
+ if (entry.isDirectory()) {
33789
+ if (hasArtifacts(join16(dir, entry.name))) return true;
33790
+ } else if (/\.(md|json)$/.test(entry.name)) {
33791
+ return true;
33792
+ }
33793
+ }
33794
+ } catch {
33795
+ }
33796
+ return false;
33797
+ }
33798
+ function readJsonFromSource(params) {
33799
+ if (params.source === "file") {
33800
+ if (!existsSync14(params.filePath)) {
33801
+ throw new StateError(STATE_EXIT.NOT_FOUND, `File not found: ${params.filePath}`);
33802
+ }
33803
+ return readFileSync10(params.filePath, "utf-8");
33804
+ }
33805
+ return params.data;
33806
+ }
33807
+ function parseRawJson(raw, label) {
33808
+ try {
33809
+ return JSON.parse(raw);
33810
+ } catch (err) {
33811
+ throw new StateError(
33812
+ STATE_EXIT.SCHEMA_INVALID,
33813
+ `Failed to parse ${label}: ${err instanceof Error ? err.message : "invalid JSON"}`
33814
+ );
33815
+ }
33816
+ }
33817
+ async function stateInit(params) {
33818
+ const { sessionId, branch, workflowType, sessionDir, ocrDir } = params;
33819
+ const db = await ensureDatabase(ocrDir);
33820
+ const existing = getSession(db, sessionId);
33821
+ if (existing) {
33822
+ if (existing.workflow_type !== workflowType) {
33823
+ throw new StateError(
33824
+ STATE_EXIT.USAGE,
33825
+ `Cannot re-open session ${sessionId} as workflow_type "${workflowType}": existing workflow_type is "${existing.workflow_type}". Maps and reviews have disjoint phase graphs.`
33826
+ );
33827
+ }
33828
+ const nextRound = deriveNextRound(db, sessionId, existing.current_round);
33829
+ const initialPhase2 = workflowType === "map" ? "map-context" : "context";
33830
+ db.transaction(() => {
33831
+ updateSession(db, sessionId, {
33832
+ status: "active",
33833
+ current_phase: initialPhase2,
33834
+ phase_number: 1,
33835
+ current_round: nextRound
33836
+ });
33837
+ insertEvent(db, {
33838
+ session_id: sessionId,
33839
+ event_type: nextRound > (existing.current_round ?? 1) ? "round_started" : "session_resumed",
33840
+ phase: initialPhase2,
33841
+ phase_number: 1,
33842
+ round: nextRound
33843
+ });
33844
+ });
33845
+ return sessionId;
33846
+ }
33847
+ const initialPhase = workflowType === "map" ? "map-context" : "context";
33848
+ db.transaction(() => {
33849
+ insertSession(db, {
33850
+ id: sessionId,
33851
+ branch,
33852
+ workflow_type: workflowType,
33853
+ current_phase: initialPhase,
33854
+ phase_number: 1,
33855
+ current_round: 1,
33856
+ current_map_run: 1,
33857
+ session_dir: sessionDir
33858
+ });
33859
+ insertEvent(db, {
33860
+ session_id: sessionId,
33861
+ event_type: "session_created",
33862
+ phase: initialPhase,
33863
+ phase_number: 1,
33864
+ round: 1
33865
+ });
33866
+ });
33867
+ return sessionId;
33868
+ }
33869
+ async function stateTransition(params, db) {
33870
+ const { sessionId, phase, phaseNumber, round, mapRun, ocrDir } = params;
33871
+ db ??= await ensureDatabase(ocrDir);
33872
+ const existing = getSession(db, sessionId);
33873
+ if (!existing) {
33874
+ throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
33875
+ }
33876
+ const previousRound = existing.current_round;
33877
+ const previousMapRun = existing.current_map_run;
33878
+ const isRoundBoundary = round !== void 0 && round !== previousRound || mapRun !== void 0 && mapRun !== previousMapRun;
33879
+ validatePhaseTransition(
33880
+ existing.workflow_type,
33881
+ existing.current_phase,
33882
+ phase,
33883
+ isRoundBoundary
33884
+ );
33885
+ db.transaction(() => {
33886
+ updateSession(db, sessionId, {
33887
+ current_phase: phase,
33888
+ phase_number: phaseNumber,
33889
+ ...round !== void 0 ? { current_round: round } : {},
33890
+ ...mapRun !== void 0 ? { current_map_run: mapRun } : {}
33891
+ });
33892
+ insertEvent(db, {
33893
+ session_id: sessionId,
33894
+ event_type: "phase_transition",
33895
+ phase,
33896
+ phase_number: phaseNumber,
33897
+ round: round ?? existing.current_round
33898
+ });
33899
+ if (round !== void 0 && round !== previousRound) {
33900
+ insertEvent(db, {
33901
+ session_id: sessionId,
33902
+ event_type: "round_started",
33903
+ phase,
33904
+ phase_number: phaseNumber,
33905
+ round
33906
+ });
33907
+ }
33908
+ });
33909
+ }
33910
+ async function stateClose(params) {
33911
+ const { sessionId, ocrDir, abort } = params;
33912
+ const db = await ensureDatabase(ocrDir);
33913
+ const existing = getSession(db, sessionId);
33914
+ if (!existing) {
33915
+ throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
33916
+ }
33917
+ if (existing.status === "closed") {
33918
+ console.error(`[ocr] Session already closed: ${sessionId}`);
33919
+ return;
33920
+ }
33921
+ if (!abort && !hasCompletionInvariant(db, existing)) {
33922
+ const what = existing.workflow_type === "map" ? `map run ${existing.current_map_run} has no map_completed event` : `round ${existing.current_round} has no round_completed event`;
33923
+ throw new StateError(
33924
+ STATE_EXIT.INVARIANT_UNMET,
33925
+ `Cannot close session ${sessionId}: ${what}. Run 'ocr state complete-round' to finalize it, or pass --abort to record an abandoned session.`
33926
+ );
33927
+ }
33928
+ const note = "closed by parent workflow close";
33929
+ db.transaction(() => {
33930
+ if (abort) {
33931
+ insertEvent(db, {
33932
+ session_id: sessionId,
33933
+ event_type: "session_aborted",
33934
+ phase: existing.current_phase,
33935
+ phase_number: existing.phase_number,
33936
+ round: existing.current_round
33937
+ });
33938
+ }
33939
+ updateSession(db, sessionId, {
33940
+ status: "closed",
33941
+ current_phase: "complete"
33942
+ });
33943
+ if (!abort) {
33944
+ insertEvent(db, {
33945
+ session_id: sessionId,
33946
+ event_type: "session_closed",
33947
+ phase: "complete",
33948
+ phase_number: existing.phase_number,
33949
+ round: existing.current_round
33950
+ });
33951
+ }
33952
+ cascadeTerminateExecutions(db, sessionId, CASCADE_CLOSE_EXIT_CODE, note);
33953
+ });
33954
+ }
33955
+ async function stateShow(ocrDir, sessionId) {
33956
+ let db;
33957
+ try {
33958
+ db = await ensureDatabase(ocrDir);
33959
+ } catch {
33960
+ return null;
33961
+ }
33962
+ const session = sessionId ? getSession(db, sessionId) : getLatestActiveSession(db);
33963
+ if (!session) {
33964
+ return null;
33965
+ }
33966
+ const events = getEventsForSession(db, session.id);
33967
+ return {
33968
+ session: {
33969
+ id: session.id,
33970
+ branch: session.branch,
33971
+ status: session.status,
33972
+ workflow_type: session.workflow_type,
33973
+ current_phase: session.current_phase,
33974
+ phase_number: session.phase_number,
33975
+ current_round: session.current_round,
33976
+ current_map_run: session.current_map_run,
33977
+ started_at: session.started_at,
33978
+ updated_at: session.updated_at
33979
+ },
33980
+ events: events.map((e) => ({
33981
+ id: e.id,
33982
+ event_type: e.event_type,
33983
+ phase: e.phase,
33984
+ phase_number: e.phase_number,
33985
+ round: e.round,
33986
+ metadata: e.metadata,
33987
+ created_at: e.created_at
33988
+ }))
33989
+ };
33990
+ }
33991
+ function resolveSession(db, explicitId) {
33992
+ if (explicitId) {
33993
+ const s = getSession(db, explicitId);
33994
+ if (!s) throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${explicitId}`);
33995
+ return {
33996
+ id: s.id,
33997
+ session_dir: s.session_dir,
33998
+ current_round: s.current_round,
33999
+ current_map_run: s.current_map_run,
34000
+ workflow_type: s.workflow_type,
34001
+ status: s.status,
34002
+ current_phase: s.current_phase,
34003
+ phase_number: s.phase_number,
34004
+ branch: s.branch,
34005
+ decision: "explicit"
34006
+ };
34007
+ }
34008
+ const uid = process.env["OCR_DASHBOARD_EXECUTION_UID"];
34009
+ if (uid) {
34010
+ const result = db.exec(
34011
+ "SELECT workflow_id FROM command_executions WHERE uid = ?",
34012
+ [uid]
34013
+ );
34014
+ const workflowId = result[0]?.values[0]?.[0];
34015
+ if (workflowId) {
34016
+ const s = getSession(db, workflowId);
34017
+ if (s) {
34018
+ return {
34019
+ id: s.id,
34020
+ session_dir: s.session_dir,
34021
+ current_round: s.current_round,
34022
+ current_map_run: s.current_map_run,
34023
+ workflow_type: s.workflow_type,
34024
+ status: s.status,
34025
+ current_phase: s.current_phase,
34026
+ phase_number: s.phase_number,
34027
+ branch: s.branch,
34028
+ decision: "dashboard-uid"
34029
+ };
34030
+ }
34031
+ }
34032
+ }
34033
+ const activeRows = db.exec(
34034
+ `SELECT id, session_dir, current_round, current_map_run, workflow_type,
34035
+ status, current_phase, phase_number, branch
34036
+ FROM sessions
34037
+ WHERE status = 'active'
34038
+ ORDER BY started_at DESC`
34039
+ );
34040
+ const rows = activeRows[0]?.values ?? [];
34041
+ if (rows.length === 0) throw new StateError(STATE_EXIT.NOT_FOUND, "No active session found");
34042
+ if (rows.length > 1) {
34043
+ const ids = rows.map((r) => r[0]);
34044
+ throw new StateError(
34045
+ STATE_EXIT.AMBIGUOUS,
34046
+ `Ambiguous auto-detect: ${rows.length} active sessions exist. Pass --session-id explicitly. Candidates: ${ids.join(", ")}`
34047
+ );
34048
+ }
34049
+ const row = rows[0];
34050
+ return {
34051
+ id: row[0],
34052
+ session_dir: row[1],
34053
+ current_round: row[2],
34054
+ current_map_run: row[3],
34055
+ workflow_type: row[4],
34056
+ status: row[5],
34057
+ current_phase: row[6],
34058
+ phase_number: row[7],
34059
+ branch: row[8],
34060
+ decision: "latest-active"
34061
+ };
34062
+ }
34063
+ function announceResolveDecision(r) {
34064
+ if (r.decision === "explicit") return;
34065
+ const path2 = r.decision === "dashboard-uid" ? "via OCR_DASHBOARD_EXECUTION_UID" : "via latest-active";
34066
+ console.error(`[ocr] Auto-detected session: ${r.id} (${path2})`);
34067
+ }
34068
+ async function resolveActiveSession(ocrDir, explicitId) {
34069
+ const db = await ensureDatabase(ocrDir);
34070
+ const result = resolveSession(db, explicitId);
34071
+ announceResolveDecision(result);
34072
+ return {
34073
+ id: result.id,
34074
+ sessionDir: result.session_dir,
34075
+ decision: result.decision
34076
+ };
34077
+ }
34078
+ async function stateBegin(params) {
34079
+ const id = await stateInit(params);
34080
+ const db = await ensureDatabase(params.ocrDir);
34081
+ const s = getSession(db, id);
34082
+ return {
34083
+ schema_version: 1,
34084
+ session_id: id,
34085
+ round: s?.current_round ?? 1,
34086
+ phase: s?.current_phase ?? "context",
34087
+ completeness: getCompletenessState(db, id)
34088
+ };
34089
+ }
34090
+ async function stateAdvance(params) {
34091
+ const db = await ensureDatabase(params.ocrDir);
34092
+ const existing = getSession(db, params.sessionId);
34093
+ if (!existing) {
34094
+ throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${params.sessionId}`);
34095
+ }
34096
+ const phaseNumber = phaseNumberFor(existing.workflow_type, params.phase);
34097
+ await stateTransition(
34098
+ {
34099
+ sessionId: params.sessionId,
34100
+ phase: params.phase,
34101
+ phaseNumber,
34102
+ round: params.round,
34103
+ mapRun: params.mapRun,
34104
+ ocrDir: params.ocrDir
34105
+ },
34106
+ db
34107
+ );
34108
+ }
34109
+ async function stateCompleteRound(params) {
33140
34110
  const { ocrDir } = params;
33141
34111
  const db = await ensureDatabase(ocrDir);
33142
- const dbPath = join15(ocrDir, "data", "ocr.db");
33143
- const rawJsonString = readJsonFromSource(params);
33144
- const label = params.source === "file" ? params.filePath : "stdin";
33145
- const raw = parseRawJson(rawJsonString, label);
33146
- const meta = validateMapMeta(raw);
33147
- const counts = computeMapCounts(meta);
33148
- const session = resolveSessionForCompletion(db, params.sessionId);
33149
- const mapRunNumber = params.mapRun ?? session.current_map_run;
34112
+ let meta;
34113
+ let counts;
34114
+ try {
34115
+ const rawJsonString = readJsonFromSource(params);
34116
+ const label = params.source === "file" ? params.filePath : "stdin";
34117
+ meta = validateRoundMeta(parseRawJson(rawJsonString, label));
34118
+ counts = computeRoundCounts(meta);
34119
+ } catch (e) {
34120
+ throw new StateError(
34121
+ STATE_EXIT.SCHEMA_INVALID,
34122
+ e instanceof Error ? e.message : "invalid round metadata"
34123
+ );
34124
+ }
34125
+ const resolved = resolveSession(db, params.sessionId);
34126
+ const roundNumber = params.round ?? resolved.current_round;
34127
+ const roundMetaPath = join16(
34128
+ resolved.session_dir,
34129
+ "rounds",
34130
+ `round-${roundNumber}`,
34131
+ "round-meta.json"
34132
+ );
34133
+ const already = db.exec(
34134
+ `SELECT 1 FROM orchestration_events
34135
+ WHERE session_id = ? AND event_type = 'round_completed' AND round = ? LIMIT 1`,
34136
+ [resolved.id, roundNumber]
34137
+ );
34138
+ if ((already[0]?.values.length ?? 0) > 0) {
34139
+ return { sessionId: resolved.id, round: roundNumber, metaPath: roundMetaPath, schema_version: 1 };
34140
+ }
34141
+ if (resolved.current_phase !== "synthesis") {
34142
+ throw new StateError(
34143
+ STATE_EXIT.INVARIANT_UNMET,
34144
+ `Cannot complete round: workflow is at "${resolved.current_phase}", not "synthesis". Advance through the phases first.`
34145
+ );
34146
+ }
34147
+ if (params.requireFinal) {
34148
+ const finalPath = join16(resolved.session_dir, "rounds", `round-${roundNumber}`, "final.md");
34149
+ if (!existsSync14(finalPath)) {
34150
+ throw new StateError(
34151
+ STATE_EXIT.INVARIANT_UNMET,
34152
+ `Cannot complete round: --require-final set but ${finalPath} is missing.`
34153
+ );
34154
+ }
34155
+ }
34156
+ let metaPath;
34157
+ if (params.source === "stdin") {
34158
+ const roundDir = join16(resolved.session_dir, "rounds", `round-${roundNumber}`);
34159
+ mkdirSync5(roundDir, { recursive: true });
34160
+ metaPath = roundMetaPath;
34161
+ writeFileSync7(metaPath, JSON.stringify(meta, null, 2));
34162
+ }
34163
+ db.transaction(() => {
34164
+ insertEvent(db, {
34165
+ session_id: resolved.id,
34166
+ event_type: "round_completed",
34167
+ phase: "synthesis",
34168
+ phase_number: 7,
34169
+ round: roundNumber,
34170
+ metadata: JSON.stringify({
34171
+ verdict: meta.verdict,
34172
+ blocker_count: counts.blockerCount,
34173
+ should_fix_count: counts.shouldFixCount,
34174
+ suggestion_count: counts.suggestionCount,
34175
+ reviewer_count: counts.reviewerCount,
34176
+ total_finding_count: counts.totalFindingCount,
34177
+ source: "orchestrator"
34178
+ })
34179
+ });
34180
+ if (roundNumber >= resolved.current_round) {
34181
+ updateSession(db, resolved.id, { current_round: roundNumber });
34182
+ }
34183
+ validatePhaseTransition("review", resolved.current_phase, "complete", false);
34184
+ updateSession(db, resolved.id, { current_phase: "complete", phase_number: 8 });
34185
+ insertEvent(db, {
34186
+ session_id: resolved.id,
34187
+ event_type: "phase_transition",
34188
+ phase: "complete",
34189
+ phase_number: 8,
34190
+ round: roundNumber
34191
+ });
34192
+ });
34193
+ return { sessionId: resolved.id, round: roundNumber, metaPath, schema_version: 1 };
34194
+ }
34195
+ async function stateCompleteMap(params) {
34196
+ const { ocrDir } = params;
34197
+ const db = await ensureDatabase(ocrDir);
34198
+ let meta;
34199
+ let counts;
34200
+ try {
34201
+ const rawJsonString = readJsonFromSource(params);
34202
+ const label = params.source === "file" ? params.filePath : "stdin";
34203
+ meta = validateMapMeta(parseRawJson(rawJsonString, label));
34204
+ counts = computeMapCounts(meta);
34205
+ } catch (e) {
34206
+ throw new StateError(
34207
+ STATE_EXIT.SCHEMA_INVALID,
34208
+ e instanceof Error ? e.message : "invalid map metadata"
34209
+ );
34210
+ }
34211
+ const resolved = resolveSession(db, params.sessionId);
34212
+ const mapRunNumber = params.mapRun ?? resolved.current_map_run;
34213
+ const mapMetaPath = join16(
34214
+ resolved.session_dir,
34215
+ "map",
34216
+ "runs",
34217
+ `run-${mapRunNumber}`,
34218
+ "map-meta.json"
34219
+ );
34220
+ const already = db.exec(
34221
+ `SELECT 1 FROM orchestration_events
34222
+ WHERE session_id = ? AND event_type = 'map_completed' AND round = ? LIMIT 1`,
34223
+ [resolved.id, mapRunNumber]
34224
+ );
34225
+ if ((already[0]?.values.length ?? 0) > 0) {
34226
+ return { sessionId: resolved.id, mapRun: mapRunNumber, metaPath: mapMetaPath, schema_version: 1 };
34227
+ }
34228
+ if (resolved.current_phase !== "synthesis") {
34229
+ throw new StateError(
34230
+ STATE_EXIT.INVARIANT_UNMET,
34231
+ `Cannot complete map: workflow is at "${resolved.current_phase}", not "synthesis". Advance first.`
34232
+ );
34233
+ }
33150
34234
  let metaPath;
33151
34235
  if (params.source === "stdin") {
33152
- const runDir = join15(session.session_dir, "map", "runs", `run-${mapRunNumber}`);
34236
+ const runDir = join16(resolved.session_dir, "map", "runs", `run-${mapRunNumber}`);
33153
34237
  mkdirSync5(runDir, { recursive: true });
33154
- metaPath = join15(runDir, "map-meta.json");
33155
- writeFileSync8(metaPath, JSON.stringify(meta, null, 2));
33156
- }
33157
- insertEvent(db, {
33158
- session_id: session.id,
33159
- event_type: "map_completed",
33160
- phase: "synthesis",
33161
- phase_number: 5,
33162
- round: mapRunNumber,
33163
- metadata: JSON.stringify({
33164
- section_count: counts.sectionCount,
33165
- file_count: counts.fileCount,
33166
- source: "orchestrator"
33167
- })
34238
+ metaPath = mapMetaPath;
34239
+ writeFileSync7(metaPath, JSON.stringify(meta, null, 2));
34240
+ }
34241
+ db.transaction(() => {
34242
+ insertEvent(db, {
34243
+ session_id: resolved.id,
34244
+ event_type: "map_completed",
34245
+ phase: "synthesis",
34246
+ phase_number: 5,
34247
+ round: mapRunNumber,
34248
+ metadata: JSON.stringify({
34249
+ section_count: counts.sectionCount,
34250
+ file_count: counts.fileCount,
34251
+ source: "orchestrator"
34252
+ })
34253
+ });
34254
+ validatePhaseTransition("map", resolved.current_phase, "complete", false);
34255
+ updateSession(db, resolved.id, { current_phase: "complete", phase_number: 6 });
34256
+ insertEvent(db, {
34257
+ session_id: resolved.id,
34258
+ event_type: "phase_transition",
34259
+ phase: "complete",
34260
+ phase_number: 6,
34261
+ round: mapRunNumber
34262
+ });
33168
34263
  });
33169
- saveDatabase(db, dbPath);
33170
- return { sessionId: session.id, mapRun: mapRunNumber, metaPath };
34264
+ return { sessionId: resolved.id, mapRun: mapRunNumber, metaPath, schema_version: 1 };
34265
+ }
34266
+ async function stateStatus(ocrDir, sessionId) {
34267
+ const db = await ensureDatabase(ocrDir);
34268
+ const resolved = resolveSession(db, sessionId);
34269
+ const view = db.exec(
34270
+ `SELECT completeness_state, has_terminal_artifact, marked_closed, dependents_settled
34271
+ FROM session_completeness WHERE session_id = ?`,
34272
+ [resolved.id]
34273
+ );
34274
+ const row = view[0]?.values[0];
34275
+ const completenessState = row?.[0] ?? null;
34276
+ const hasTerminalArtifact = row?.[1] === 1;
34277
+ let nextAction;
34278
+ let nextActionKind;
34279
+ switch (completenessState) {
34280
+ case "complete":
34281
+ nextAction = "none \u2014 session is complete";
34282
+ nextActionKind = "none";
34283
+ break;
34284
+ case "closed_without_artifact":
34285
+ nextAction = "re-open and finalize: this session was closed without a completed round/run";
34286
+ nextActionKind = "reopen";
34287
+ break;
34288
+ case "in_flight":
34289
+ nextAction = "wait for in-flight agent processes to finish";
34290
+ nextActionKind = "wait";
34291
+ break;
34292
+ default:
34293
+ if (hasTerminalArtifact) {
34294
+ nextAction = "run 'ocr state finish' to close the workflow";
34295
+ nextActionKind = "finish";
34296
+ } else if (resolved.current_phase === "synthesis") {
34297
+ nextAction = "pipe round metadata to 'ocr state complete-round --stdin'";
34298
+ nextActionKind = "complete_round";
34299
+ } else {
34300
+ nextAction = "advance through the phases, then 'ocr state complete-round'";
34301
+ nextActionKind = "advance";
34302
+ }
34303
+ }
34304
+ return {
34305
+ schema_version: 1,
34306
+ session_id: resolved.id,
34307
+ workflow_type: resolved.workflow_type,
34308
+ status: resolved.status,
34309
+ current_phase: resolved.current_phase,
34310
+ current_round: resolved.current_round,
34311
+ current_map_run: resolved.current_map_run,
34312
+ completeness_state: completenessState,
34313
+ has_terminal_artifact: hasTerminalArtifact,
34314
+ marked_closed: row?.[2] === 1,
34315
+ dependents_settled: row?.[3] === 1,
34316
+ next_action: nextAction,
34317
+ next_action_kind: nextActionKind
34318
+ };
33171
34319
  }
33172
34320
  async function stateSync(ocrDir) {
33173
34321
  const db = await ensureDatabase(ocrDir);
33174
- const dbPath = join15(ocrDir, "data", "ocr.db");
33175
- const sessionsRoot = join15(ocrDir, "sessions");
33176
- if (!existsSync13(sessionsRoot)) {
34322
+ const sessionsRoot = join16(ocrDir, "sessions");
34323
+ if (!existsSync14(sessionsRoot)) {
33177
34324
  return 0;
33178
34325
  }
33179
34326
  const entries = readdirSync6(sessionsRoot).filter((name) => {
33180
- const fullPath = join15(sessionsRoot, name);
33181
- return statSync2(fullPath).isDirectory();
34327
+ const fullPath = join16(sessionsRoot, name);
34328
+ return statSync3(fullPath).isDirectory();
33182
34329
  });
33183
34330
  let synced = 0;
33184
34331
  for (const dirName of entries) {
33185
- const dirPath = join15(sessionsRoot, dirName);
34332
+ const dirPath = join16(sessionsRoot, dirName);
33186
34333
  const existing = getSession(db, dirName);
33187
34334
  if (existing) {
33188
34335
  continue;
@@ -33190,28 +34337,41 @@ async function stateSync(ocrDir) {
33190
34337
  if (!hasArtifacts(dirPath)) {
33191
34338
  continue;
33192
34339
  }
33193
- const hasRoundsDir = existsSync13(join15(dirPath, "rounds"));
33194
- const hasMapDir = existsSync13(join15(dirPath, "map"));
34340
+ const hasRoundsDir = existsSync14(join16(dirPath, "rounds"));
34341
+ const hasMapDir = existsSync14(join16(dirPath, "map"));
33195
34342
  const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
33196
34343
  const branchMatch = dirName.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
33197
34344
  const branch = branchMatch?.[1] ?? dirName;
33198
34345
  let inferredPhase = "context";
34346
+ let inferredPhaseNumber = 1;
34347
+ let inferredRound = 1;
34348
+ let inferredMapRun = 1;
33199
34349
  if (workflowType === "review") {
33200
- const roundsDir = join15(dirPath, "rounds");
33201
- if (existsSync13(roundsDir)) {
33202
- const roundDirs = readdirSync6(roundsDir).filter((d) => /^round-\d+$/.test(d)).sort((a, b) => parseInt(a.replace(/^\D+-/, ""), 10) - parseInt(b.replace(/^\D+-/, ""), 10));
33203
- const latestRound = roundDirs[roundDirs.length - 1];
33204
- if (latestRound && existsSync13(join15(roundsDir, latestRound, "final.md"))) {
33205
- inferredPhase = "complete";
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);
34353
+ const latestRoundNum = roundDirs[roundDirs.length - 1];
34354
+ if (latestRoundNum !== void 0) {
34355
+ inferredRound = latestRoundNum;
34356
+ if (existsSync14(
34357
+ join16(roundsDir, `round-${latestRoundNum}`, "final.md")
34358
+ )) {
34359
+ inferredPhase = "complete";
34360
+ inferredPhaseNumber = 8;
34361
+ }
33206
34362
  }
33207
34363
  }
33208
34364
  } else if (workflowType === "map") {
33209
- const runsDir = join15(dirPath, "map", "runs");
33210
- if (existsSync13(runsDir)) {
33211
- const runDirs = readdirSync6(runsDir).filter((d) => /^run-\d+$/.test(d)).sort((a, b) => parseInt(a.replace(/^\D+-/, ""), 10) - parseInt(b.replace(/^\D+-/, ""), 10));
33212
- const latestRun = runDirs[runDirs.length - 1];
33213
- if (latestRun && existsSync13(join15(runsDir, latestRun, "map.md"))) {
33214
- inferredPhase = "complete";
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);
34368
+ const latestRunNum = runDirs[runDirs.length - 1];
34369
+ if (latestRunNum !== void 0) {
34370
+ inferredMapRun = latestRunNum;
34371
+ if (existsSync14(join16(runsDir, `run-${latestRunNum}`, "map.md"))) {
34372
+ inferredPhase = "complete";
34373
+ inferredPhaseNumber = 6;
34374
+ }
33215
34375
  }
33216
34376
  }
33217
34377
  }
@@ -33220,35 +34380,36 @@ async function stateSync(ocrDir) {
33220
34380
  branch,
33221
34381
  workflow_type: workflowType,
33222
34382
  current_phase: inferredPhase,
33223
- phase_number: 1,
33224
- current_round: 1,
33225
- current_map_run: 1,
34383
+ phase_number: inferredPhaseNumber,
34384
+ current_round: inferredRound,
34385
+ current_map_run: inferredMapRun,
33226
34386
  session_dir: dirPath
33227
34387
  });
33228
- updateSession(db, dirName, { status: "closed" });
33229
- insertEvent(db, {
33230
- session_id: dirName,
33231
- event_type: "session_synced",
33232
- phase: inferredPhase,
33233
- phase_number: 1,
33234
- metadata: JSON.stringify({ source: "filesystem_backfill" })
33235
- });
34388
+ commitReasonClose(
34389
+ db,
34390
+ dirName,
34391
+ {
34392
+ event_type: "session_synced",
34393
+ phase: inferredPhase,
34394
+ phase_number: 1,
34395
+ metadata: JSON.stringify({ source: "filesystem_backfill" })
34396
+ },
34397
+ { status: "closed" }
34398
+ );
33236
34399
  synced++;
33237
34400
  }
33238
- if (synced > 0) {
33239
- saveDatabase(db, dbPath);
33240
- }
33241
34401
  return synced;
33242
34402
  }
33243
34403
 
33244
34404
  // src/commands/state.ts
33245
34405
  init_command_log();
33246
34406
  init_db();
34407
+ init_db();
33247
34408
  function readDashboardSpawnMarker(ocrDir) {
33248
- const path2 = join16(ocrDir, "data", "dashboard-active-spawn.json");
34409
+ const path2 = join17(ocrDir, "data", "dashboard-active-spawn.json");
33249
34410
  let raw;
33250
34411
  try {
33251
- raw = readFileSync12(path2, "utf-8");
34412
+ raw = readFileSync11(path2, "utf-8");
33252
34413
  } catch {
33253
34414
  return null;
33254
34415
  }
@@ -33280,143 +34441,37 @@ async function readStdin() {
33280
34441
  }
33281
34442
  return data;
33282
34443
  }
33283
- var initSubcommand = new Command("init").description("Initialize a new OCR session").requiredOption("--session-id <id>", "Session ID").requiredOption("--branch <branch>", "Branch name").requiredOption(
33284
- "--workflow-type <type>",
33285
- "Workflow type (review or map)",
33286
- (value) => {
33287
- if (value !== "review" && value !== "map") {
33288
- throw new Error(
33289
- `Invalid workflow type: "${value}". Must be "review" or "map".`
33290
- );
33291
- }
33292
- return value;
33293
- }
33294
- ).option("--session-dir <dir>", "Session directory path (auto-resolved if omitted)").option(
33295
- "--dashboard-uid <uid>",
33296
- "Dashboard command_executions uid to link this workflow to. Takes precedence over the OCR_DASHBOARD_EXECUTION_UID env var so AI shells that strip env vars can still wire the linkage."
33297
- ).action(
33298
- async (options) => {
33299
- const targetDir = process.cwd();
33300
- requireOcrSetup(targetDir);
33301
- const ocrDir = join16(targetDir, ".ocr");
33302
- const sessionDir = options.sessionDir ?? join16(ocrDir, "sessions", options.sessionId);
33303
- if (!existsSync14(sessionDir)) {
33304
- mkdirSync6(sessionDir, { recursive: true });
33305
- }
33306
- try {
33307
- const sessionId = await stateInit({
33308
- sessionId: options.sessionId,
33309
- branch: options.branch,
33310
- workflowType: options.workflowType,
33311
- sessionDir,
33312
- ocrDir
33313
- });
33314
- const markerUid = readDashboardSpawnMarker(ocrDir)?.execution_uid;
33315
- const dashboardUid = options.dashboardUid ?? process.env["OCR_DASHBOARD_EXECUTION_UID"] ?? markerUid;
33316
- if (dashboardUid) {
33317
- try {
33318
- const db = await getDb(ocrDir);
33319
- linkDashboardInvocationToWorkflow(db, dashboardUid, sessionId);
33320
- saveDatabase(db, join16(ocrDir, "data", "ocr.db"));
33321
- console.error(
33322
- source_default.gray(
33323
- `[state init] linked workflow_id=${sessionId} \u2192 dashboard uid=${dashboardUid}`
33324
- )
33325
- );
33326
- } catch (linkErr) {
33327
- console.error(
33328
- source_default.yellow(
33329
- `Warning: failed to link dashboard command_execution to session: ${linkErr instanceof Error ? linkErr.message : String(linkErr)}`
33330
- )
33331
- );
33332
- }
33333
- } else {
33334
- console.error(
33335
- source_default.gray(
33336
- `[state init] no dashboard linkage available (flag, env var, and marker file all absent \u2014 CLI-only invocation)`
33337
- )
33338
- );
33339
- }
33340
- console.log(sessionId);
33341
- } catch (error) {
33342
- console.error(
33343
- source_default.red(
33344
- `Error: ${error instanceof Error ? error.message : "Failed to initialize session"}`
33345
- )
33346
- );
33347
- process.exit(1);
33348
- }
33349
- }
33350
- );
33351
- var transitionSubcommand = new Command("transition").description("Transition session to a new phase").option("--session-id <id>", "Session ID (auto-detects latest active if omitted)").requiredOption("--phase <phase>", "Target phase name").requiredOption("--phase-number <number>", "Phase number", parseInt).option("--current-round <number>", "Round number", parseInt).option("--current-map-run <number>", "Map run number", parseInt).action(
33352
- async (options) => {
33353
- const targetDir = process.cwd();
33354
- requireOcrSetup(targetDir);
33355
- const ocrDir = join16(targetDir, ".ocr");
33356
- const VALID_PHASES = /* @__PURE__ */ new Set([
33357
- "context",
33358
- "change-context",
33359
- "analysis",
33360
- "reviews",
33361
- "aggregation",
33362
- "discourse",
33363
- "synthesis",
33364
- "complete",
33365
- "map-context",
33366
- "topology",
33367
- "flow-analysis",
33368
- "requirements-mapping"
33369
- ]);
33370
- if (!VALID_PHASES.has(options.phase)) {
33371
- throw new Error(`Invalid phase: "${options.phase}". Must be one of: ${[...VALID_PHASES].join(", ")}`);
33372
- }
33373
- try {
33374
- const sessionId = options.sessionId ?? (await resolveActiveSession(ocrDir)).id;
33375
- await stateTransition({
33376
- sessionId,
33377
- phase: options.phase,
33378
- phaseNumber: options.phaseNumber,
33379
- round: options.currentRound,
33380
- mapRun: options.currentMapRun,
33381
- ocrDir
33382
- });
33383
- console.log(
33384
- `${sessionId}: ${options.phase} (phase ${options.phaseNumber})`
33385
- );
33386
- } catch (error) {
33387
- console.error(
33388
- source_default.red(
33389
- `Error: ${error instanceof Error ? error.message : "Failed to transition"}`
33390
- )
33391
- );
33392
- process.exit(1);
33393
- }
34444
+ async function linkDashboardInvocation(ocrDir, sessionId, explicitUid, label) {
34445
+ const markerUid = readDashboardSpawnMarker(ocrDir)?.execution_uid;
34446
+ const dashboardUid = explicitUid ?? process.env["OCR_DASHBOARD_EXECUTION_UID"] ?? markerUid;
34447
+ if (!dashboardUid) {
34448
+ console.error(
34449
+ source_default.gray(
34450
+ `[state ${label}] no dashboard linkage available (flag, env var, and marker file all absent \u2014 CLI-only invocation)`
34451
+ )
34452
+ );
34453
+ return;
33394
34454
  }
33395
- );
33396
- var closeSubcommand = new Command("close").description("Close a session").option("--session-id <id>", "Session ID (auto-detects latest active if omitted)").action(async (options) => {
33397
- const targetDir = process.cwd();
33398
- requireOcrSetup(targetDir);
33399
- const ocrDir = join16(targetDir, ".ocr");
33400
34455
  try {
33401
- const sessionId = options.sessionId ?? (await resolveActiveSession(ocrDir)).id;
33402
- await stateClose({
33403
- sessionId,
33404
- ocrDir
33405
- });
33406
- console.log(`${sessionId}: closed`);
33407
- } catch (error) {
34456
+ const db = await getDb(ocrDir);
34457
+ linkDashboardInvocationToWorkflow(db, dashboardUid, sessionId);
33408
34458
  console.error(
33409
- source_default.red(
33410
- `Error: ${error instanceof Error ? error.message : "Failed to close session"}`
34459
+ source_default.gray(
34460
+ `[state ${label}] linked workflow_id=${sessionId} \u2192 dashboard uid=${dashboardUid}`
34461
+ )
34462
+ );
34463
+ } catch (linkErr) {
34464
+ console.error(
34465
+ source_default.yellow(
34466
+ `Warning: failed to link dashboard command_execution to session: ${linkErr instanceof Error ? linkErr.message : String(linkErr)}`
33411
34467
  )
33412
34468
  );
33413
- process.exit(1);
33414
34469
  }
33415
- });
34470
+ }
33416
34471
  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) => {
33417
34472
  const targetDir = process.cwd();
33418
34473
  requireOcrSetup(targetDir);
33419
- const ocrDir = join16(targetDir, ".ocr");
34474
+ const ocrDir = join17(targetDir, ".ocr");
33420
34475
  try {
33421
34476
  const result = await stateShow(ocrDir, options.sessionId);
33422
34477
  if (!result) {
@@ -33485,7 +34540,7 @@ var showSubcommand = new Command("show").description("Show current session state
33485
34540
  var syncSubcommand = new Command("sync").description("Rebuild session state from filesystem artifacts").action(async () => {
33486
34541
  const targetDir = process.cwd();
33487
34542
  requireOcrSetup(targetDir);
33488
- const ocrDir = join16(targetDir, ".ocr");
34543
+ const ocrDir = join17(targetDir, ".ocr");
33489
34544
  try {
33490
34545
  const synced = await stateSync(ocrDir);
33491
34546
  console.log(`Synced ${synced} session${synced !== 1 ? "s" : ""} from filesystem.`);
@@ -33495,7 +34550,6 @@ var syncSubcommand = new Command("sync").description("Rebuild session state from
33495
34550
  if (totalCmds === 0) {
33496
34551
  const recovered = replayCommandLog(db, ocrDir);
33497
34552
  if (recovered > 0) {
33498
- saveDatabase(db, join16(ocrDir, "data", "ocr.db"));
33499
34553
  console.log(`Recovered ${recovered} command${recovered !== 1 ? "s" : ""} from backup log.`);
33500
34554
  }
33501
34555
  }
@@ -33508,123 +34562,216 @@ var syncSubcommand = new Command("sync").description("Rebuild session state from
33508
34562
  process.exit(1);
33509
34563
  }
33510
34564
  });
33511
- var roundCompleteSubcommand = new Command("round-complete").description("Import structured round data into SQLite").option("--file <path>", "Path to round-meta.json").option("--stdin", "Read round-meta JSON from stdin (recommended)").option("--session-id <id>", "Session ID (auto-detects latest active if omitted)").option("--round <number>", "Round number (auto-detects current if omitted)", parseInt).action(
34565
+ var reconcileSubcommand = new Command("reconcile").description(
34566
+ "Heal legacy/drifted session state by deriving truth from events + artifacts"
34567
+ ).option("--dry-run", "Print the repair plan without writing anything").option("--json", "Output the result as JSON").action(async (options) => {
34568
+ const targetDir = process.cwd();
34569
+ requireOcrSetup(targetDir);
34570
+ const ocrDir = join17(targetDir, ".ocr");
34571
+ try {
34572
+ const db = await ensureDatabase(ocrDir);
34573
+ const result = reconcileLegacyState(db, ocrDir, { dryRun: options.dryRun });
34574
+ if (options.json) {
34575
+ console.log(JSON.stringify(result, null, 2));
34576
+ return;
34577
+ }
34578
+ const repairs = result.actions.filter((a) => a.kind !== "ok");
34579
+ if (repairs.length === 0) {
34580
+ console.log(source_default.dim("Nothing to reconcile \u2014 all sessions consistent."));
34581
+ return;
34582
+ }
34583
+ console.log(
34584
+ result.dryRun ? source_default.bold(`Reconciliation plan (${repairs.length} change(s), dry run):`) : source_default.bold(`Reconciled ${repairs.length} session(s):`)
34585
+ );
34586
+ for (const a of repairs) {
34587
+ console.log(` ${source_default.cyan(a.kind)} ${a.sessionId}`);
34588
+ console.log(` ${source_default.dim(a.detail)}`);
34589
+ }
34590
+ } catch (error) {
34591
+ console.error(
34592
+ source_default.red(
34593
+ `Error: ${error instanceof Error ? error.message : "Failed to reconcile"}`
34594
+ )
34595
+ );
34596
+ process.exit(1);
34597
+ }
34598
+ });
34599
+ function exitFromStateError(error, fallback2) {
34600
+ if (error instanceof StateError) {
34601
+ console.error(source_default.red(`Error: ${error.message}`));
34602
+ process.exit(error.code);
34603
+ }
34604
+ if (isBusyError(error)) {
34605
+ console.error(
34606
+ source_default.red(
34607
+ `Error: database is busy (locked past retry budget): ${error instanceof Error ? error.message : String(error)}`
34608
+ )
34609
+ );
34610
+ process.exit(STATE_EXIT.BUSY);
34611
+ }
34612
+ console.error(
34613
+ source_default.red(`Error: ${error instanceof Error ? error.message : fallback2}`)
34614
+ );
34615
+ process.exit(1);
34616
+ }
34617
+ var beginSubcommand = new Command("begin").description("Start or resume a workflow and report where it stands").requiredOption("--session-id <id>", "Session ID").requiredOption("--branch <branch>", "Branch name").requiredOption("--workflow-type <type>", "Workflow type (review or map)", (v) => {
34618
+ if (v !== "review" && v !== "map") {
34619
+ throw new Error(`Invalid workflow type: "${v}". Must be "review" or "map".`);
34620
+ }
34621
+ return v;
34622
+ }).option("--session-dir <dir>", "Session directory path (auto-resolved if omitted)").option(
34623
+ "--dashboard-uid <uid>",
34624
+ "Dashboard command_executions uid to link this workflow to (takes precedence over OCR_DASHBOARD_EXECUTION_UID)"
34625
+ ).option("--json", "Output the result as JSON").action(
33512
34626
  async (options) => {
33513
34627
  const targetDir = process.cwd();
33514
34628
  requireOcrSetup(targetDir);
33515
- const ocrDir = join16(targetDir, ".ocr");
33516
- if (!options.file && !options.stdin) {
33517
- console.error(source_default.red("Error: Provide either --file <path> or --stdin"));
33518
- process.exit(1);
33519
- }
33520
- if (options.file && options.stdin) {
33521
- console.error(source_default.red("Error: --file and --stdin are mutually exclusive"));
33522
- process.exit(1);
33523
- }
34629
+ const ocrDir = join17(targetDir, ".ocr");
34630
+ const sessionDir = options.sessionDir ?? join17(ocrDir, "sessions", options.sessionId);
34631
+ if (!existsSync15(sessionDir)) mkdirSync6(sessionDir, { recursive: true });
33524
34632
  try {
33525
- let result;
33526
- if (options.stdin) {
33527
- const data = await readStdin();
33528
- result = await stateRoundComplete({
33529
- source: "stdin",
33530
- ocrDir,
33531
- data,
33532
- sessionId: options.sessionId,
33533
- round: options.round
33534
- });
33535
- } else if (options.file) {
33536
- result = await stateRoundComplete({
33537
- source: "file",
33538
- ocrDir,
33539
- filePath: options.file,
33540
- sessionId: options.sessionId,
33541
- round: options.round
33542
- });
33543
- } else {
33544
- process.exit(1);
33545
- }
33546
- console.log(source_default.green("Round data imported successfully."));
33547
- if (result.metaPath) {
33548
- console.log(source_default.dim(`Wrote ${result.metaPath}`));
33549
- }
33550
- } catch (error) {
33551
- console.error(
33552
- source_default.red(
33553
- `Error: ${error instanceof Error ? error.message : "Failed to import round data"}`
33554
- )
34633
+ const result = await stateBegin({
34634
+ sessionId: options.sessionId,
34635
+ branch: options.branch,
34636
+ workflowType: options.workflowType,
34637
+ sessionDir,
34638
+ ocrDir
34639
+ });
34640
+ await linkDashboardInvocation(ocrDir, result.session_id, options.dashboardUid, "begin");
34641
+ console.log(
34642
+ options.json ? JSON.stringify(result, null, 2) : `${result.session_id}: round ${result.round}, phase ${result.phase} (${result.completeness ?? "unknown"})`
33555
34643
  );
33556
- process.exit(1);
34644
+ } catch (error) {
34645
+ exitFromStateError(error, "Failed to begin session");
33557
34646
  }
33558
34647
  }
33559
34648
  );
33560
- var mapCompleteSubcommand = new Command("map-complete").description("Import structured map run data into SQLite").option("--file <path>", "Path to map-meta.json").option("--stdin", "Read map-meta JSON from stdin (recommended)").option("--session-id <id>", "Session ID (auto-detects latest active if omitted)").option("--map-run <number>", "Map run number (auto-detects current if omitted)", parseInt).action(
34649
+ var advanceSubcommand = new Command("advance").description("Advance the workflow to a phase (graph-validated; phase number derived)").requiredOption("--phase <phase>", "Target phase name").option("--session-id <id>", "Session ID (auto-detects active if omitted)").option("--current-round <number>", "Round number", parseInt).option("--current-map-run <number>", "Map run number", parseInt).option("--phase-number <number>", "(ignored \u2014 derived from --phase)", parseInt).action(
33561
34650
  async (options) => {
33562
34651
  const targetDir = process.cwd();
33563
34652
  requireOcrSetup(targetDir);
33564
- const ocrDir = join16(targetDir, ".ocr");
33565
- if (!options.file && !options.stdin) {
33566
- console.error(source_default.red("Error: Provide either --file <path> or --stdin"));
33567
- process.exit(1);
33568
- }
33569
- if (options.file && options.stdin) {
33570
- console.error(source_default.red("Error: --file and --stdin are mutually exclusive"));
33571
- process.exit(1);
34653
+ const ocrDir = join17(targetDir, ".ocr");
34654
+ try {
34655
+ const { id: sessionId } = await resolveActiveSession(ocrDir, options.sessionId);
34656
+ await stateAdvance({
34657
+ sessionId,
34658
+ phase: options.phase,
34659
+ round: options.currentRound,
34660
+ mapRun: options.currentMapRun,
34661
+ ocrDir
34662
+ });
34663
+ console.log(`${sessionId}: ${options.phase}`);
34664
+ } catch (error) {
34665
+ exitFromStateError(error, "Failed to advance");
33572
34666
  }
34667
+ }
34668
+ );
34669
+ var completeRoundSubcommand = new Command("complete-round").description("Atomically finalize a review round (validate + record + transition)").option("--session-id <id>", "Session ID (auto-detects active if omitted)").option("--round <number>", "Round number (defaults to current)", parseInt).option("--stdin", "Read round metadata JSON from stdin").option("--file <path>", "Read round metadata JSON from a file").option("--require-final", "Require rounds/round-N/final.md to exist").option("--json", "Output the result as JSON").action(
34670
+ async (options) => {
34671
+ const targetDir = process.cwd();
34672
+ requireOcrSetup(targetDir);
34673
+ const ocrDir = join17(targetDir, ".ocr");
33573
34674
  try {
33574
- let result;
33575
- if (options.stdin) {
33576
- const data = await readStdin();
33577
- result = await stateMapComplete({
33578
- source: "stdin",
33579
- ocrDir,
33580
- data,
33581
- sessionId: options.sessionId,
33582
- mapRun: options.mapRun
33583
- });
33584
- } else if (options.file) {
33585
- result = await stateMapComplete({
33586
- source: "file",
33587
- ocrDir,
33588
- filePath: options.file,
33589
- sessionId: options.sessionId,
33590
- mapRun: options.mapRun
33591
- });
33592
- } else {
33593
- process.exit(1);
33594
- }
33595
- console.log(source_default.green("Map data imported successfully."));
33596
- if (result.metaPath) {
33597
- console.log(source_default.dim(`Wrote ${result.metaPath}`));
33598
- }
34675
+ const base = options.stdin ? { source: "stdin", data: await readStdin() } : options.file ? { source: "file", filePath: options.file } : (() => {
34676
+ throw new StateError(STATE_EXIT.USAGE, "Provide --stdin or --file with round metadata");
34677
+ })();
34678
+ const result = await stateCompleteRound({
34679
+ ...base,
34680
+ ocrDir,
34681
+ sessionId: options.sessionId,
34682
+ round: options.round,
34683
+ requireFinal: options.requireFinal
34684
+ });
34685
+ console.log(
34686
+ options.json ? JSON.stringify(result, null, 2) : `${result.sessionId}: round ${result.round} complete`
34687
+ );
33599
34688
  } catch (error) {
33600
- console.error(
33601
- source_default.red(
33602
- `Error: ${error instanceof Error ? error.message : "Failed to import map data"}`
33603
- )
34689
+ exitFromStateError(error, "Failed to complete round");
34690
+ }
34691
+ }
34692
+ );
34693
+ var completeMapSubcommand = new Command("complete-map").description("Atomically finalize a map run (validate + record + transition)").option("--session-id <id>", "Session ID (auto-detects active if omitted)").option("--map-run <number>", "Map run number (defaults to current)", parseInt).option("--stdin", "Read map metadata JSON from stdin").option("--file <path>", "Read map metadata JSON from a file").option("--json", "Output the result as JSON").action(
34694
+ async (options) => {
34695
+ const targetDir = process.cwd();
34696
+ requireOcrSetup(targetDir);
34697
+ const ocrDir = join17(targetDir, ".ocr");
34698
+ try {
34699
+ const base = options.stdin ? { source: "stdin", data: await readStdin() } : options.file ? { source: "file", filePath: options.file } : (() => {
34700
+ throw new StateError(STATE_EXIT.USAGE, "Provide --stdin or --file with map metadata");
34701
+ })();
34702
+ const result = await stateCompleteMap({
34703
+ ...base,
34704
+ ocrDir,
34705
+ sessionId: options.sessionId,
34706
+ mapRun: options.mapRun
34707
+ });
34708
+ console.log(
34709
+ options.json ? JSON.stringify(result, null, 2) : `${result.sessionId}: map run ${result.mapRun} complete`
33604
34710
  );
33605
- process.exit(1);
34711
+ } catch (error) {
34712
+ exitFromStateError(error, "Failed to complete map");
33606
34713
  }
33607
34714
  }
33608
34715
  );
33609
- var stateCommand = new Command("state").description("Manage OCR session state").addCommand(initSubcommand).addCommand(transitionSubcommand).addCommand(closeSubcommand).addCommand(showSubcommand).addCommand(syncSubcommand).addCommand(roundCompleteSubcommand).addCommand(mapCompleteSubcommand);
34716
+ 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
+ const targetDir = process.cwd();
34718
+ requireOcrSetup(targetDir);
34719
+ const ocrDir = join17(targetDir, ".ocr");
34720
+ try {
34721
+ const { id: sessionId } = await resolveActiveSession(ocrDir, options.sessionId);
34722
+ await stateClose({ sessionId, ocrDir, abort: options.abort });
34723
+ console.log(`${sessionId}: ${options.abort ? "aborted" : "finished"}`);
34724
+ } catch (error) {
34725
+ exitFromStateError(error, "Failed to finish");
34726
+ }
34727
+ });
34728
+ 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
+ const targetDir = process.cwd();
34730
+ requireOcrSetup(targetDir);
34731
+ const ocrDir = join17(targetDir, ".ocr");
34732
+ try {
34733
+ const result = await stateStatus(ocrDir, options.sessionId);
34734
+ if (options.json) {
34735
+ console.log(JSON.stringify(result, null, 2));
34736
+ } else {
34737
+ console.log(`${result.session_id}: ${result.completeness_state}`);
34738
+ console.log(source_default.dim(` next: ${result.next_action}`));
34739
+ }
34740
+ } catch (error) {
34741
+ exitFromStateError(error, "Failed to read status");
34742
+ }
34743
+ });
34744
+ var RETIRED_STATE_VERBS = {
34745
+ init: "begin",
34746
+ transition: "advance",
34747
+ "round-complete": "complete-round",
34748
+ "map-complete": "complete-map",
34749
+ close: "finish"
34750
+ };
34751
+ var stateCommand = new Command("state").description("Manage OCR session state").addCommand(beginSubcommand).addCommand(advanceSubcommand).addCommand(completeRoundSubcommand).addCommand(completeMapSubcommand).addCommand(finishSubcommand).addCommand(statusSubcommand).addCommand(showSubcommand).addCommand(syncSubcommand).addCommand(reconcileSubcommand).showSuggestionAfterError(false).on("command:*", (operands) => {
34752
+ const verb = operands[0] ?? "";
34753
+ const replacement = RETIRED_STATE_VERBS[verb];
34754
+ const msg = replacement ? `'ocr state ${verb}' was retired in v2.0 \u2014 use 'ocr state ${replacement}'. See 'ocr state --help'.` : `Unknown 'ocr state' subcommand: '${verb}'. See 'ocr state --help'.`;
34755
+ exitFromStateError(new StateError(STATE_EXIT.USAGE, msg), msg);
34756
+ });
33610
34757
 
33611
34758
  // src/commands/session.ts
33612
34759
  import { randomUUID as randomUUID3 } from "node:crypto";
33613
- import { join as join18 } from "node:path";
34760
+ import { join as join19 } from "node:path";
33614
34761
  init_db();
33615
34762
 
33616
34763
  // src/lib/runtime-config.ts
33617
- import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
33618
- import { join as join17 } from "node:path";
34764
+ import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
34765
+ import { join as join18 } from "node:path";
33619
34766
  var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
33620
34767
  function getAgentHeartbeatSeconds(ocrDir) {
33621
- const configPath = join17(ocrDir, "config.yaml");
33622
- if (!existsSync15(configPath)) {
34768
+ const configPath = join18(ocrDir, "config.yaml");
34769
+ if (!existsSync16(configPath)) {
33623
34770
  return DEFAULT_AGENT_HEARTBEAT_SECONDS;
33624
34771
  }
33625
34772
  let content;
33626
34773
  try {
33627
- content = readFileSync13(configPath, "utf-8");
34774
+ content = readFileSync12(configPath, "utf-8");
33628
34775
  } catch {
33629
34776
  return DEFAULT_AGENT_HEARTBEAT_SECONDS;
33630
34777
  }
@@ -33663,16 +34810,18 @@ function fail(message) {
33663
34810
  async function setup() {
33664
34811
  const targetDir = process.cwd();
33665
34812
  requireOcrSetup(targetDir);
33666
- const ocrDir = join18(targetDir, ".ocr");
33667
- const dbPath = join18(ocrDir, "data", "ocr.db");
33668
- return { ocrDir, dbPath };
34813
+ const ocrDir = join19(targetDir, ".ocr");
34814
+ return { ocrDir };
33669
34815
  }
33670
34816
  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(
33671
34817
  async (options) => {
33672
- const { ocrDir, dbPath } = await setup();
34818
+ const { ocrDir } = await setup();
33673
34819
  const db = await ensureDatabase(ocrDir);
33674
34820
  try {
33675
- const workflowId = options.workflow ?? (await resolveActiveSession(ocrDir)).id;
34821
+ const { id: workflowId } = await resolveActiveSession(
34822
+ ocrDir,
34823
+ options.workflow
34824
+ );
33676
34825
  const id = randomUUID3();
33677
34826
  const persona = options.persona ?? null;
33678
34827
  const instanceIndex = options.instance ?? null;
@@ -33691,7 +34840,6 @@ var startInstanceSubcommand = new Command("start-instance").description("Journal
33691
34840
  pid: options.pid ?? null,
33692
34841
  notes: options.note ?? null
33693
34842
  });
33694
- saveDatabase(db, dbPath);
33695
34843
  console.log(id);
33696
34844
  } catch (error) {
33697
34845
  fail(error instanceof Error ? error.message : "Failed to start agent session");
@@ -33699,18 +34847,17 @@ var startInstanceSubcommand = new Command("start-instance").description("Journal
33699
34847
  }
33700
34848
  );
33701
34849
  var bindVendorIdSubcommand = new Command("bind-vendor-id").description("Bind the underlying CLI's session id to an OCR agent session").argument("<agent-session-id>", "OCR agent session id").argument("<vendor-session-id>", "Underlying CLI's session id").action(async (agentId, vendorId) => {
33702
- const { ocrDir, dbPath } = await setup();
34850
+ const { ocrDir } = await setup();
33703
34851
  const db = await ensureDatabase(ocrDir);
33704
34852
  try {
33705
34853
  setAgentSessionVendorId(db, agentId, vendorId);
33706
- saveDatabase(db, dbPath);
33707
34854
  console.log(`${agentId}: vendor_session_id=${vendorId}`);
33708
34855
  } catch (error) {
33709
34856
  fail(error instanceof Error ? error.message : "Failed to bind vendor session id");
33710
34857
  }
33711
34858
  });
33712
34859
  var beatSubcommand = new Command("beat").description("Bump last_heartbeat_at on an agent session").argument("<agent-session-id>", "OCR agent session id").action(async (agentId) => {
33713
- const { ocrDir, dbPath } = await setup();
34860
+ const { ocrDir } = await setup();
33714
34861
  const db = await ensureDatabase(ocrDir);
33715
34862
  try {
33716
34863
  const existing = getAgentSession(db, agentId);
@@ -33718,7 +34865,6 @@ var beatSubcommand = new Command("beat").description("Bump last_heartbeat_at on
33718
34865
  fail(`Agent session not found: ${agentId}`);
33719
34866
  }
33720
34867
  bumpAgentSessionHeartbeat(db, agentId);
33721
- saveDatabase(db, dbPath);
33722
34868
  console.log(`${agentId}: heartbeat`);
33723
34869
  } catch (error) {
33724
34870
  fail(error instanceof Error ? error.message : "Failed to bump heartbeat");
@@ -33729,7 +34875,7 @@ var endInstanceSubcommand = new Command("end-instance").description("Transition
33729
34875
  "Terminal status (done | crashed | cancelled). Default inferred from --exit-code (0 \u2192 done, non-zero \u2192 crashed)"
33730
34876
  ).option("--exit-code <code>", "Process exit code", parseInt).option("--note <text>", "Free-form note to append").action(
33731
34877
  async (agentId, options) => {
33732
- const { ocrDir, dbPath } = await setup();
34878
+ const { ocrDir } = await setup();
33733
34879
  const db = await ensureDatabase(ocrDir);
33734
34880
  try {
33735
34881
  const existing = getAgentSession(db, agentId);
@@ -33760,7 +34906,6 @@ var endInstanceSubcommand = new Command("end-instance").description("Transition
33760
34906
  exitCode: options.exitCode ?? null,
33761
34907
  note: options.note
33762
34908
  });
33763
- saveDatabase(db, dbPath);
33764
34909
  console.log(`${agentId}: ${status}`);
33765
34910
  } catch (error) {
33766
34911
  fail(error instanceof Error ? error.message : "Failed to end agent session");
@@ -33771,7 +34916,10 @@ var listSubcommand = new Command("list").description("List agent sessions for a
33771
34916
  const { ocrDir } = await setup();
33772
34917
  const db = await ensureDatabase(ocrDir);
33773
34918
  try {
33774
- const workflowId = options.workflow ?? (await resolveActiveSession(ocrDir)).id;
34919
+ const { id: workflowId } = await resolveActiveSession(
34920
+ ocrDir,
34921
+ options.workflow
34922
+ );
33775
34923
  const rows = listAgentSessionsForWorkflow(db, workflowId);
33776
34924
  if (options.json) {
33777
34925
  console.log(JSON.stringify(rows, null, 2));
@@ -33917,8 +35065,8 @@ var modelsCommand = new Command("models").description("Inspect models available
33917
35065
 
33918
35066
  // src/commands/team.ts
33919
35067
  var import_yaml2 = __toESM(require_dist(), 1);
33920
- import { existsSync as existsSync16, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "node:fs";
33921
- import { join as join19 } from "node:path";
35068
+ import { existsSync as existsSync17, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
35069
+ import { join as join20 } from "node:path";
33922
35070
  async function readStdin2() {
33923
35071
  const chunks = [];
33924
35072
  for await (const chunk of process.stdin) {
@@ -33977,7 +35125,7 @@ var resolveSubcommand = new Command("resolve").description("Resolve and print th
33977
35125
  async (options) => {
33978
35126
  const targetDir = process.cwd();
33979
35127
  requireOcrSetup(targetDir);
33980
- const ocrDir = join19(targetDir, ".ocr");
35128
+ const ocrDir = join20(targetDir, ".ocr");
33981
35129
  try {
33982
35130
  const { team } = loadTeamConfig(ocrDir);
33983
35131
  let override;
@@ -34016,8 +35164,8 @@ var setSubcommand = new Command("set").description("Persist a new default_team c
34016
35164
  }
34017
35165
  const targetDir = process.cwd();
34018
35166
  requireOcrSetup(targetDir);
34019
- const ocrDir = join19(targetDir, ".ocr");
34020
- const configPath = join19(ocrDir, "config.yaml");
35167
+ const ocrDir = join20(targetDir, ".ocr");
35168
+ const configPath = join20(ocrDir, "config.yaml");
34021
35169
  try {
34022
35170
  const raw = await readStdin2();
34023
35171
  const team = parseSessionOverride(raw);
@@ -34027,17 +35175,17 @@ var setSubcommand = new Command("set").description("Persist a new default_team c
34027
35175
  list.push(inst);
34028
35176
  byPersona.set(inst.persona, list);
34029
35177
  }
34030
- const doc = existsSync16(configPath) ? (0, import_yaml2.parseDocument)(readFileSync14(configPath, "utf-8")) : new import_yaml2.Document({});
35178
+ const doc = existsSync17(configPath) ? (0, import_yaml2.parseDocument)(readFileSync13(configPath, "utf-8")) : new import_yaml2.Document({});
34031
35179
  applyDefaultTeamSurgically(doc, byPersona);
34032
35180
  const yamlOutput = doc.toString({ lineWidth: 0 });
34033
- writeFileSync9(configPath, yamlOutput, "utf-8");
34034
- const reviewersDir = join19(ocrDir, "skills", "references", "reviewers");
34035
- const metaPath = join19(ocrDir, "reviewers-meta.json");
35181
+ writeFileSync8(configPath, yamlOutput, "utf-8");
35182
+ const reviewersDir = join20(ocrDir, "skills", "references", "reviewers");
35183
+ const metaPath = join20(ocrDir, "reviewers-meta.json");
34036
35184
  let metaWritten = false;
34037
35185
  try {
34038
35186
  const meta = generateReviewersMeta(reviewersDir, configPath);
34039
35187
  if (meta) {
34040
- writeFileSync9(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
35188
+ writeFileSync8(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
34041
35189
  metaWritten = true;
34042
35190
  }
34043
35191
  } catch (err) {
@@ -34128,7 +35276,7 @@ var teamCommand = new Command("team").description("Resolve and persist team comp
34128
35276
 
34129
35277
  // src/commands/review.ts
34130
35278
  import { spawn as spawn3 } from "node:child_process";
34131
- import { join as join20 } from "node:path";
35279
+ import { join as join21 } from "node:path";
34132
35280
  init_db();
34133
35281
 
34134
35282
  // src/lib/vendor-resume.ts
@@ -34167,7 +35315,7 @@ var reviewCommand = new Command("review").description("Run or resume an OCR revi
34167
35315
  }
34168
35316
  const targetDir = process.cwd();
34169
35317
  requireOcrSetup(targetDir);
34170
- const ocrDir = join20(targetDir, ".ocr");
35318
+ const ocrDir = join21(targetDir, ".ocr");
34171
35319
  const db = await ensureDatabase(ocrDir);
34172
35320
  const session = getSession(db, options.resume);
34173
35321
  if (!session) {
@@ -34209,16 +35357,16 @@ var reviewCommand = new Command("review").description("Run or resume an OCR revi
34209
35357
  });
34210
35358
 
34211
35359
  // src/commands/update.ts
34212
- import { existsSync as existsSync17 } from "node:fs";
34213
- import { join as join21 } from "node:path";
35360
+ import { existsSync as existsSync18 } from "node:fs";
35361
+ import { join as join22 } from "node:path";
34214
35362
  function detectConfiguredTools(targetDir) {
34215
35363
  return AI_TOOLS.filter((tool) => {
34216
35364
  if (tool.commandStrategy === "subdirectory") {
34217
- const ocrDir = join21(targetDir, tool.commandsDir, "ocr");
34218
- return existsSync17(ocrDir);
35365
+ const ocrDir = join22(targetDir, tool.commandsDir, "ocr");
35366
+ return existsSync18(ocrDir);
34219
35367
  } else {
34220
- const reviewCmd = join21(targetDir, tool.commandsDir, "ocr-review.md");
34221
- return existsSync17(reviewCmd);
35368
+ const reviewCmd = join22(targetDir, tool.commandsDir, "ocr-review.md");
35369
+ return existsSync18(reviewCmd);
34222
35370
  }
34223
35371
  });
34224
35372
  }
@@ -34292,7 +35440,7 @@ var updateCommand = new Command("update").description("Update OCR assets after p
34292
35440
  const result = installForTool(tool, targetDir);
34293
35441
  results.push(result);
34294
35442
  }
34295
- ensureGitignore(join21(targetDir, ".ocr"));
35443
+ ensureGitignore(join22(targetDir, ".ocr"));
34296
35444
  spinner.stop();
34297
35445
  const successful = results.filter((r) => r.success);
34298
35446
  const failed = results.filter((r) => !r.success);
@@ -34328,10 +35476,10 @@ var updateCommand = new Command("update").description("Update OCR assets after p
34328
35476
  if (updateInject) {
34329
35477
  if (options.dryRun) {
34330
35478
  console.log(source_default.dim(" Would update:"));
34331
- if (existsSync17(join21(targetDir, "AGENTS.md"))) {
35479
+ if (existsSync18(join22(targetDir, "AGENTS.md"))) {
34332
35480
  console.log(source_default.dim(" \u2022 AGENTS.md (OCR managed block)"));
34333
35481
  }
34334
- if (existsSync17(join21(targetDir, "CLAUDE.md"))) {
35482
+ if (existsSync18(join22(targetDir, "CLAUDE.md"))) {
34335
35483
  console.log(source_default.dim(" \u2022 CLAUDE.md (OCR managed block)"));
34336
35484
  }
34337
35485
  console.log();
@@ -34363,14 +35511,14 @@ var updateCommand = new Command("update").description("Update OCR assets after p
34363
35511
  });
34364
35512
 
34365
35513
  // src/commands/dashboard.ts
34366
- import { existsSync as existsSync18 } from "node:fs";
34367
- import { join as join22, dirname as dirname6 } from "node:path";
35514
+ import { existsSync as existsSync19 } from "node:fs";
35515
+ import { join as join23, dirname as dirname7 } from "node:path";
34368
35516
  import { fileURLToPath } from "node:url";
34369
35517
  init_db();
34370
35518
  var __filename = fileURLToPath(import.meta.url);
34371
- var __dirname = dirname6(__filename);
35519
+ var __dirname = dirname7(__filename);
34372
35520
  function resolveServerPath() {
34373
- return join22(__dirname, "dashboard", "server.js");
35521
+ return join23(__dirname, "dashboard", "server.js");
34374
35522
  }
34375
35523
  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(
34376
35524
  async (options) => {
@@ -34381,7 +35529,7 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
34381
35529
  console.error(source_default.red(`Error: Invalid port "${options.port}". Must be 1-65535.`));
34382
35530
  process.exit(1);
34383
35531
  }
34384
- const ocrDir = join22(targetDir, ".ocr");
35532
+ const ocrDir = join23(targetDir, ".ocr");
34385
35533
  try {
34386
35534
  await ensureDatabase(ocrDir);
34387
35535
  closeAllDatabases();
@@ -34395,7 +35543,7 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
34395
35543
  process.exit(1);
34396
35544
  }
34397
35545
  const serverPath = resolveServerPath();
34398
- if (!existsSync18(serverPath)) {
35546
+ if (!existsSync19(serverPath)) {
34399
35547
  console.error(source_default.red("Error: Dashboard server bundle not found."));
34400
35548
  console.error(
34401
35549
  source_default.dim(` Expected at: ${serverPath}`)
@@ -34429,10 +35577,53 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
34429
35577
  );
34430
35578
 
34431
35579
  // src/commands/doctor.ts
34432
- import { existsSync as existsSync19 } from "node:fs";
34433
- import { join as join23 } from "node:path";
34434
- var doctorCommand = new Command("doctor").description("Check OCR installation and verify all dependencies").action(() => {
35580
+ import { existsSync as existsSync20 } from "node:fs";
35581
+ import { join as join24 } from "node:path";
35582
+ init_db();
35583
+ function printStorageEngine(probeWriteEnabled) {
35584
+ console.log();
35585
+ console.log(source_default.bold(" Storage Engine"));
35586
+ console.log();
35587
+ const engine = probeEngine();
35588
+ if (!engine.ok) {
35589
+ console.log(` ${source_default.red("\u2717")} node:sqlite unavailable`);
35590
+ console.log(` ${source_default.dim(engine.error)}`);
35591
+ console.log(
35592
+ ` ${source_default.dim(
35593
+ "OCR requires Node >= 22.5 (node:sqlite). Upgrade Node, then re-run `ocr doctor`."
35594
+ )}`
35595
+ );
35596
+ return false;
35597
+ }
35598
+ console.log(
35599
+ ` ${source_default.green("\u2713")} node:sqlite (SQLite ${engine.version}, WAL)`
35600
+ );
35601
+ if (probeWriteEnabled) {
35602
+ const write = probeWrite();
35603
+ if (!write.ok) {
35604
+ console.log(` ${source_default.red("\u2717")} write probe failed`);
35605
+ console.log(` ${source_default.dim(write.error)}`);
35606
+ return false;
35607
+ }
35608
+ console.log(
35609
+ ` ${source_default.green("\u2713")} write probe (on-disk WAL transaction round-trip)`
35610
+ );
35611
+ }
35612
+ return true;
35613
+ }
35614
+ var doctorCommand = new Command("doctor").description("Check OCR installation and verify all dependencies").option(
35615
+ "--probe-write",
35616
+ "additionally exercise an on-disk WAL transaction round-trip (used by the release install gate)"
35617
+ ).option(
35618
+ "--engine-only",
35619
+ "check ONLY the storage engine and exit on its result \u2014 skips project/tool checks (used by the release install gate, which runs from a non-initialized dir with no AI tools)"
35620
+ ).action((options) => {
34435
35621
  printHeader();
35622
+ if (options.engineOnly) {
35623
+ const ok = printStorageEngine(options.probeWrite ?? false);
35624
+ console.log();
35625
+ process.exit(ok ? 0 : 1);
35626
+ }
34436
35627
  const targetDir = process.cwd();
34437
35628
  let hasIssues = false;
34438
35629
  const depResult = checkDependencies();
@@ -34444,10 +35635,10 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
34444
35635
  console.log(source_default.bold(" OCR Installation"));
34445
35636
  console.log();
34446
35637
  const ocrStatus = checkOcrSetup(targetDir);
34447
- const configPath = join23(targetDir, ".ocr", "config.yaml");
34448
- const dbPath = join23(targetDir, ".ocr", "data", "ocr.db");
34449
- const hasConfig = existsSync19(configPath);
34450
- const hasDb = existsSync19(dbPath);
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);
34451
35642
  const ocrChecks = [
34452
35643
  { label: ".ocr/skills/", ok: ocrStatus.hasSkills },
34453
35644
  { label: ".ocr/sessions/", ok: ocrStatus.hasSessions },
@@ -34469,6 +35660,9 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
34469
35660
  if (!ocrStatus.valid) {
34470
35661
  hasIssues = true;
34471
35662
  }
35663
+ if (!printStorageEngine(options.probeWrite ?? false)) {
35664
+ hasIssues = true;
35665
+ }
34472
35666
  console.log();
34473
35667
  printCapabilities(depResult);
34474
35668
  console.log();
@@ -34521,8 +35715,8 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
34521
35715
  });
34522
35716
 
34523
35717
  // src/commands/reviewers.ts
34524
- import { writeFileSync as writeFileSync10, renameSync as renameSync3 } from "node:fs";
34525
- import { join as join24 } from "node:path";
35718
+ import { writeFileSync as writeFileSync9, renameSync as renameSync2 } from "node:fs";
35719
+ import { join as join25 } from "node:path";
34526
35720
  async function readStdin3() {
34527
35721
  const chunks = [];
34528
35722
  for await (const chunk of process.stdin) {
@@ -34585,20 +35779,20 @@ function validateReviewersMeta(data) {
34585
35779
  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) => {
34586
35780
  const targetDir = process.cwd();
34587
35781
  requireOcrSetup(targetDir);
34588
- const ocrDir = join24(targetDir, ".ocr");
35782
+ const ocrDir = join25(targetDir, ".ocr");
34589
35783
  if (!options.stdin) {
34590
35784
  try {
34591
- const reviewersDir = join24(ocrDir, "skills", "references", "reviewers");
34592
- const configPath = join24(ocrDir, "config.yaml");
35785
+ const reviewersDir = join25(ocrDir, "skills", "references", "reviewers");
35786
+ const configPath = join25(ocrDir, "config.yaml");
34593
35787
  const meta = generateReviewersMeta(reviewersDir, configPath);
34594
35788
  if (!meta || meta.reviewers.length === 0) {
34595
35789
  console.error(source_default.yellow("No reviewer files found in .ocr/skills/references/reviewers/"));
34596
35790
  process.exit(1);
34597
35791
  }
34598
- const metaPath = join24(ocrDir, "reviewers-meta.json");
35792
+ const metaPath = join25(ocrDir, "reviewers-meta.json");
34599
35793
  const tmpPath = metaPath + ".tmp";
34600
- writeFileSync10(tmpPath, JSON.stringify(meta, null, 2) + "\n");
34601
- renameSync3(tmpPath, metaPath);
35794
+ writeFileSync9(tmpPath, JSON.stringify(meta, null, 2) + "\n");
35795
+ renameSync2(tmpPath, metaPath);
34602
35796
  const tierCounts = meta.reviewers.reduce(
34603
35797
  (acc, r) => {
34604
35798
  acc[r.tier] = (acc[r.tier] ?? 0) + 1;
@@ -34625,10 +35819,10 @@ var syncSubcommand2 = new Command("sync").description("Sync reviewers-meta.json
34625
35819
  throw new Error("Invalid JSON on stdin");
34626
35820
  }
34627
35821
  const meta = validateReviewersMeta(parsed);
34628
- const metaPath = join24(ocrDir, "reviewers-meta.json");
35822
+ const metaPath = join25(ocrDir, "reviewers-meta.json");
34629
35823
  const tmpPath = metaPath + ".tmp";
34630
- writeFileSync10(tmpPath, JSON.stringify(meta, null, 2) + "\n");
34631
- renameSync3(tmpPath, metaPath);
35824
+ writeFileSync9(tmpPath, JSON.stringify(meta, null, 2) + "\n");
35825
+ renameSync2(tmpPath, metaPath);
34632
35826
  const tierCounts = meta.reviewers.reduce(
34633
35827
  (acc, r) => {
34634
35828
  acc[r.tier] = (acc[r.tier] ?? 0) + 1;
@@ -34653,25 +35847,25 @@ var reviewersCommand = new Command("reviewers").description("Manage OCR reviewer
34653
35847
 
34654
35848
  // src/lib/update-check.ts
34655
35849
  import { homedir } from "node:os";
34656
- import { join as join25 } from "node:path";
34657
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, mkdirSync as mkdirSync7 } from "node:fs";
35850
+ import { join as join26 } from "node:path";
35851
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, mkdirSync as mkdirSync7 } from "node:fs";
34658
35852
  var PACKAGE_NAME = "@open-code-review/cli";
34659
35853
  var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
34660
- var CACHE_DIR2 = join25(homedir(), ".ocr");
34661
- var CACHE_FILE = join25(CACHE_DIR2, "update-check.json");
35854
+ var CACHE_DIR2 = join26(homedir(), ".ocr");
35855
+ var CACHE_FILE = join26(CACHE_DIR2, "update-check.json");
34662
35856
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
34663
35857
  var FETCH_TIMEOUT_MS = 3e3;
34664
35858
  function readCache(cacheFile) {
34665
35859
  try {
34666
- return JSON.parse(readFileSync15(cacheFile, "utf-8"));
35860
+ return JSON.parse(readFileSync14(cacheFile, "utf-8"));
34667
35861
  } catch {
34668
35862
  return null;
34669
35863
  }
34670
35864
  }
34671
35865
  function writeCache(cacheFile, cache) {
34672
35866
  try {
34673
- mkdirSync7(join25(cacheFile, ".."), { recursive: true });
34674
- writeFileSync11(cacheFile, JSON.stringify(cache));
35867
+ mkdirSync7(join26(cacheFile, ".."), { recursive: true });
35868
+ writeFileSync10(cacheFile, JSON.stringify(cache));
34675
35869
  } catch {
34676
35870
  }
34677
35871
  }
@@ -34691,7 +35885,7 @@ async function checkForUpdate(currentVersion, options) {
34691
35885
  if (process.env.CI || process.env.OCR_NO_UPDATE_CHECK) {
34692
35886
  return null;
34693
35887
  }
34694
- const cacheFile = join25(options?.cacheDir ?? CACHE_DIR2, "update-check.json");
35888
+ const cacheFile = join26(options?.cacheDir ?? CACHE_DIR2, "update-check.json");
34695
35889
  try {
34696
35890
  const cache = readCache(cacheFile);
34697
35891
  if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {