@open-code-review/cli 1.11.0 → 2.0.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 +8 -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 +1113 -657
  50. package/dist/index.js +1719 -718
  51. package/dist/lib/db/index.js +638 -101
  52. package/package.json +4 -4
  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
@@ -23319,7 +23319,102 @@ var init_result_mapper = __esm({
23319
23319
  }
23320
23320
  });
23321
23321
 
23322
+ // src/lib/db/engine.ts
23323
+ import BetterSqlite3 from "better-sqlite3";
23324
+ function isBusyError(e) {
23325
+ if (e instanceof BetterSqlite3.SqliteError) {
23326
+ return e.code === "SQLITE_BUSY" || e.code === "SQLITE_BUSY_SNAPSHOT";
23327
+ }
23328
+ const code = e?.code;
23329
+ return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_SNAPSHOT";
23330
+ }
23331
+ function sleepSync(ms) {
23332
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
23333
+ }
23334
+ function probeEngine() {
23335
+ try {
23336
+ const db = new BetterSqlite3(":memory:");
23337
+ db.pragma("journal_mode = WAL");
23338
+ db.exec("CREATE TABLE _probe(x); INSERT INTO _probe VALUES (1);");
23339
+ const row = db.prepare("SELECT sqlite_version() AS v").get();
23340
+ db.close();
23341
+ return { ok: true, version: row.v };
23342
+ } catch (e) {
23343
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
23344
+ }
23345
+ }
23346
+ function openEngine(dbPath) {
23347
+ const native = new BetterSqlite3(dbPath);
23348
+ native.pragma("journal_mode = WAL");
23349
+ native.pragma("foreign_keys = ON");
23350
+ native.pragma("busy_timeout = 5000");
23351
+ native.pragma("synchronous = NORMAL");
23352
+ return new BetterSqliteAdapter(native);
23353
+ }
23354
+ var BUSY_RETRY_ATTEMPTS, BUSY_RETRY_BACKOFF_MS, BetterSqliteAdapter;
23355
+ var init_engine = __esm({
23356
+ "src/lib/db/engine.ts"() {
23357
+ "use strict";
23358
+ BUSY_RETRY_ATTEMPTS = 5;
23359
+ BUSY_RETRY_BACKOFF_MS = 50;
23360
+ BetterSqliteAdapter = class {
23361
+ raw;
23362
+ constructor(db) {
23363
+ this.raw = db;
23364
+ }
23365
+ exec(sql, params) {
23366
+ const stmt = this.raw.prepare(sql);
23367
+ if (!stmt.reader) {
23368
+ stmt.run(...params ?? []);
23369
+ return [];
23370
+ }
23371
+ const columns = stmt.columns().map((c) => c.name);
23372
+ const values = stmt.raw().all(...params ?? []);
23373
+ return values.length > 0 ? [{ columns, values }] : [];
23374
+ }
23375
+ run(sql, params) {
23376
+ if (params !== void 0) {
23377
+ this.raw.prepare(sql).run(...params);
23378
+ return;
23379
+ }
23380
+ this.raw.exec(sql);
23381
+ }
23382
+ prepare(sql) {
23383
+ return this.raw.prepare(sql);
23384
+ }
23385
+ transaction(fn) {
23386
+ const tx = this.raw.transaction(fn);
23387
+ for (let attempt = 0; ; attempt++) {
23388
+ try {
23389
+ return tx.immediate();
23390
+ } catch (e) {
23391
+ if (!isBusyError(e) || attempt >= BUSY_RETRY_ATTEMPTS - 1) throw e;
23392
+ sleepSync(BUSY_RETRY_BACKOFF_MS);
23393
+ }
23394
+ }
23395
+ }
23396
+ pragma(source) {
23397
+ return this.raw.pragma(source);
23398
+ }
23399
+ close() {
23400
+ try {
23401
+ this.raw.pragma("wal_checkpoint(TRUNCATE)");
23402
+ } catch {
23403
+ }
23404
+ this.raw.close();
23405
+ }
23406
+ };
23407
+ }
23408
+ });
23409
+
23322
23410
  // src/lib/db/migrations.ts
23411
+ function columnExists(db, table, column) {
23412
+ const result = db.exec(`PRAGMA table_info(${table})`);
23413
+ const first = result[0];
23414
+ if (!first) return false;
23415
+ const nameIdx = first.columns.indexOf("name");
23416
+ return first.values.some((row) => row[nameIdx] === column);
23417
+ }
23323
23418
  function ensureSchemaVersionTable(db) {
23324
23419
  db.run(`
23325
23420
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -23329,6 +23424,10 @@ function ensureSchemaVersionTable(db) {
23329
23424
  );
23330
23425
  `);
23331
23426
  }
23427
+ function getSchemaVersion(db) {
23428
+ ensureSchemaVersionTable(db);
23429
+ return getCurrentVersion(db);
23430
+ }
23332
23431
  function getCurrentVersion(db) {
23333
23432
  const result = db.exec(
23334
23433
  "SELECT MAX(version) as v FROM schema_version"
@@ -23346,9 +23445,10 @@ function runMigrations(db) {
23346
23445
  if (migration.version <= currentVersion) {
23347
23446
  continue;
23348
23447
  }
23349
- db.run("BEGIN TRANSACTION;");
23448
+ db.run("BEGIN IMMEDIATE;");
23350
23449
  try {
23351
- db.run(migration.sql);
23450
+ if (migration.sql) db.run(migration.sql);
23451
+ migration.run?.(db);
23352
23452
  db.run(
23353
23453
  "INSERT INTO schema_version (version, description) VALUES (?, ?);",
23354
23454
  [migration.version, migration.description]
@@ -23685,6 +23785,148 @@ var init_migrations = __esm({
23685
23785
  DROP INDEX IF EXISTS idx_agent_sessions_status_heartbeat;
23686
23786
  DROP TABLE IF EXISTS agent_sessions;
23687
23787
  `
23788
+ },
23789
+ {
23790
+ version: 12,
23791
+ description: "Event-sourced lifecycle hardening: event_type taxonomy guard, sweep indexes, session_completeness view",
23792
+ sql: `
23793
+ -- \u2500\u2500 Indexes for the now-periodic stale-session sweep + round derivation \u2500\u2500
23794
+ -- The sweep filters sessions by status and rolls up MAX(created_at) per
23795
+ -- session over the event log; deriveNextRound does MAX(round). Index both.
23796
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
23797
+ CREATE INDEX IF NOT EXISTS idx_events_session_created
23798
+ ON orchestration_events(session_id, created_at);
23799
+
23800
+ -- \u2500\u2500 Event-type taxonomy guard \u2500\u2500
23801
+ -- orchestration_events.event_type is the spine of all lifecycle
23802
+ -- derivation. A typo (e.g. 'round_complete' vs 'round_completed') would
23803
+ -- silently break deriveNextRound and the completeness view. SQLite cannot
23804
+ -- add a CHECK to an existing column without a table rebuild, so enforce
23805
+ -- the closed vocabulary with a BEFORE INSERT trigger instead.
23806
+ CREATE TRIGGER IF NOT EXISTS trg_events_known_type
23807
+ BEFORE INSERT ON orchestration_events
23808
+ WHEN NEW.event_type NOT IN (
23809
+ 'session_created', 'session_resumed', 'round_started', 'phase_transition',
23810
+ 'round_completed', 'map_completed', 'session_closed', 'session_aborted',
23811
+ 'session_auto_closed_stale', 'session_synced', 'session_legacy_import'
23812
+ )
23813
+ BEGIN
23814
+ SELECT RAISE(ABORT, 'unknown orchestration_events.event_type');
23815
+ END;
23816
+
23817
+ -- \u2500\u2500 Close-guard (DB backstop for the completion invariant) \u2500\u2500
23818
+ -- A session cannot transition active \u2192 closed unless its current
23819
+ -- round/run has a terminal artifact event, OR an explicit reason event
23820
+ -- (abort / auto-close-stale / sync / legacy-import) is present. Only a
23821
+ -- *silent* premature close is banned \u2014 every legitimate non-artifact
23822
+ -- close carries a reason event and passes. App-level guards in
23823
+ -- stateClose/finish are the primary check; this makes the illegal state
23824
+ -- unrepresentable even via raw SQL.
23825
+ --
23826
+ -- DEFENCE-IN-DEPTH NOTE (intentional, documented gap): the reason-event
23827
+ -- branch below (event_type IN (...)) is NOT round-scoped \u2014 a reason event
23828
+ -- recorded for an earlier round would also satisfy a later close. The
23829
+ -- app-level guards ARE round-scoped (hasCompletionInvariant checks the
23830
+ -- current round/run), so the precise check lives in the application; this
23831
+ -- trigger is a coarse backstop against a *silent* premature close via raw
23832
+ -- SQL. Tightening it to be round-scoped would require a new migration
23833
+ -- (this v12 trigger is append-only and already shipped); the residual
23834
+ -- risk is a non-artifact close carrying a stale reason event, which is
23835
+ -- still an explicit, audited terminal \u2014 not the failure mode this guards.
23836
+ CREATE TRIGGER IF NOT EXISTS trg_sessions_close_guard
23837
+ BEFORE UPDATE OF status ON sessions
23838
+ WHEN NEW.status = 'closed' AND OLD.status <> 'closed'
23839
+ AND NOT EXISTS (
23840
+ SELECT 1 FROM orchestration_events e
23841
+ WHERE e.session_id = NEW.id
23842
+ AND (
23843
+ (NEW.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = NEW.current_round)
23844
+ OR (NEW.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = NEW.current_map_run)
23845
+ OR e.event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
23846
+ )
23847
+ )
23848
+ BEGIN
23849
+ SELECT RAISE(ABORT, 'cannot close session without a completed round/run or an explicit reason event');
23850
+ END;
23851
+
23852
+ -- \u2500\u2500 session_completeness view \u2500\u2500
23853
+ -- The published contract for "is this session actually complete, and if
23854
+ -- not, what's missing". Completion is DERIVED from the event log, never a
23855
+ -- mutable flag: a session is complete iff it is closed AND a terminal
23856
+ -- artifact event exists for its current round/run. The dashboard's
23857
+ -- outcome derivation and the agent 'status' command read this view, so
23858
+ -- they cannot disagree.
23859
+ --
23860
+ -- completeness_state is an INTENTIONAL HYBRID: it combines the mutable
23861
+ -- status column (marked_closed) with append-only event evidence (the
23862
+ -- terminal artifact event). This is sound precisely because the
23863
+ -- close-guard trigger above makes the status column trustworthy \u2014 a row
23864
+ -- can only reach status='closed' with a completed round/run or an
23865
+ -- explicit reason event \u2014 so reading the column is not a regression to
23866
+ -- the old "mutable flag that could lie" model.
23867
+ --
23868
+ -- completeness_state:
23869
+ -- 'complete' \u2014 closed + terminal artifact for current round/run
23870
+ -- 'closed_without_artifact' \u2014 closed but no terminal artifact (the
23871
+ -- "completed too soon" condition)
23872
+ -- 'in_flight' \u2014 open with a dependent process still running
23873
+ -- 'open_no_artifact' \u2014 open, no in-flight dependents
23874
+ CREATE VIEW IF NOT EXISTS session_completeness AS
23875
+ SELECT
23876
+ s.id AS session_id,
23877
+ s.workflow_type AS workflow_type,
23878
+ s.status AS status,
23879
+ s.current_round AS current_round,
23880
+ s.current_map_run AS current_map_run,
23881
+ CASE WHEN EXISTS (
23882
+ SELECT 1 FROM orchestration_events e
23883
+ WHERE e.session_id = s.id
23884
+ AND (
23885
+ (s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
23886
+ OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
23887
+ )
23888
+ ) THEN 1 ELSE 0 END AS has_terminal_artifact,
23889
+ CASE WHEN s.status = 'closed' THEN 1 ELSE 0 END AS marked_closed,
23890
+ CASE WHEN NOT EXISTS (
23891
+ SELECT 1 FROM command_executions ce
23892
+ WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
23893
+ ) THEN 1 ELSE 0 END AS dependents_settled,
23894
+ CASE
23895
+ WHEN s.status = 'closed' AND EXISTS (
23896
+ SELECT 1 FROM orchestration_events e
23897
+ WHERE e.session_id = s.id
23898
+ AND (
23899
+ (s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
23900
+ OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
23901
+ )
23902
+ ) THEN 'complete'
23903
+ WHEN s.status = 'closed' THEN 'closed_without_artifact'
23904
+ WHEN EXISTS (
23905
+ SELECT 1 FROM command_executions ce
23906
+ WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
23907
+ ) THEN 'in_flight'
23908
+ ELSE 'open_no_artifact'
23909
+ END AS completeness_state
23910
+ FROM sessions s;
23911
+ `
23912
+ },
23913
+ {
23914
+ version: 13,
23915
+ description: "Retire dead parent_id column on command_executions (never written; row kind is derived from command)",
23916
+ // parent_id was reserved for an AI-instance → dashboard-spawn lineage link
23917
+ // that was never wired (no writer, no reader). A process's KIND (supervisor
23918
+ // / reviewer-instance / utility) is derived from columns that are always
23919
+ // present (command + last_heartbeat_at), so the dead lineage column and its
23920
+ // all-NULL index are removed. Re-add a wired parent_id alongside a real
23921
+ // consumer (e.g. a parent→child tree view) if lineage is ever needed.
23922
+ //
23923
+ // Imperative + guarded so the DROP COLUMN (which SQLite can't express as
23924
+ // IF EXISTS) is idempotent under re-application.
23925
+ run: (db) => {
23926
+ if (!columnExists(db, "command_executions", "parent_id")) return;
23927
+ db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
23928
+ db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
23929
+ }
23688
23930
  }
23689
23931
  ];
23690
23932
  }
@@ -23798,6 +24040,12 @@ function getLatestEventId(db) {
23798
24040
  const val = result[0]?.values[0]?.[0];
23799
24041
  return typeof val === "number" ? val : 0;
23800
24042
  }
24043
+ function commitReasonClose(db, sessionId, reasonEvent, projectionUpdates) {
24044
+ db.transaction(() => {
24045
+ insertEvent(db, { session_id: sessionId, ...reasonEvent });
24046
+ updateSession(db, sessionId, projectionUpdates);
24047
+ });
24048
+ }
23801
24049
  var init_queries = __esm({
23802
24050
  "src/lib/db/queries.ts"() {
23803
24051
  "use strict";
@@ -23805,7 +24053,208 @@ var init_queries = __esm({
23805
24053
  }
23806
24054
  });
23807
24055
 
24056
+ // src/lib/db/reconcile.ts
24057
+ import { existsSync as existsSync10 } from "node:fs";
24058
+ import { isAbsolute as isAbsolute2, join as join12, dirname as dirname4 } from "node:path";
24059
+ function hasTerminalArtifactEvent(db, sessionId, workflowType, currentRound, currentMapRun) {
24060
+ const eventType = workflowType === "map" ? "map_completed" : "round_completed";
24061
+ const round = workflowType === "map" ? currentMapRun : currentRound;
24062
+ const r = db.exec(
24063
+ `SELECT 1 FROM orchestration_events
24064
+ WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
24065
+ [sessionId, eventType, round]
24066
+ );
24067
+ return (r[0]?.values.length ?? 0) > 0;
24068
+ }
24069
+ function hasReasonEvent(db, sessionId) {
24070
+ const r = db.exec(
24071
+ `SELECT 1 FROM orchestration_events
24072
+ WHERE session_id = ?
24073
+ AND event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
24074
+ LIMIT 1`,
24075
+ [sessionId]
24076
+ );
24077
+ return (r[0]?.values.length ?? 0) > 0;
24078
+ }
24079
+ function lastEventAgeSeconds(db, sessionId) {
24080
+ const r = db.exec(
24081
+ `SELECT (julianday('now') - julianday(MAX(created_at))) * 86400
24082
+ FROM orchestration_events WHERE session_id = ?`,
24083
+ [sessionId]
24084
+ );
24085
+ const v = r[0]?.values[0]?.[0];
24086
+ return typeof v === "number" ? v : null;
24087
+ }
24088
+ function hasInFlightDependents(db, sessionId) {
24089
+ const r = db.exec(
24090
+ `SELECT 1 FROM command_executions
24091
+ WHERE workflow_id = ? AND finished_at IS NULL LIMIT 1`,
24092
+ [sessionId]
24093
+ );
24094
+ return (r[0]?.values.length ?? 0) > 0;
24095
+ }
24096
+ function resolveSessionDir(ocrDir, sessionDir) {
24097
+ if (!sessionDir) return null;
24098
+ if (isAbsolute2(sessionDir)) return sessionDir;
24099
+ return join12(dirname4(ocrDir), sessionDir);
24100
+ }
24101
+ function reconcileLegacyState(db, ocrDir, opts = {}) {
24102
+ const dryRun = opts.dryRun ?? false;
24103
+ const threshold = opts.staleThresholdSeconds ?? DEFAULT_STALE_THRESHOLD_SECONDS;
24104
+ const actions = [];
24105
+ for (const s of getAllSessions(db)) {
24106
+ const dir = resolveSessionDir(ocrDir, s.session_dir);
24107
+ if (s.status === "closed") {
24108
+ if (hasTerminalArtifactEvent(db, s.id, s.workflow_type, s.current_round, s.current_map_run) || hasReasonEvent(db, s.id)) {
24109
+ continue;
24110
+ }
24111
+ const reviewFinal = s.workflow_type === "review" && dir ? existsSync10(join12(dir, "rounds", `round-${s.current_round}`, "final.md")) : false;
24112
+ const mapFinal = s.workflow_type === "map" && dir ? existsSync10(join12(dir, "map", "runs", `run-${s.current_map_run}`, "map.md")) : false;
24113
+ if (reviewFinal) {
24114
+ actions.push({
24115
+ sessionId: s.id,
24116
+ kind: "synthesize-round-completed",
24117
+ detail: `final.md present for round ${s.current_round}; synthesizing round_completed`
24118
+ });
24119
+ if (!dryRun) {
24120
+ insertEvent(db, {
24121
+ session_id: s.id,
24122
+ event_type: "round_completed",
24123
+ phase: "synthesis",
24124
+ phase_number: 7,
24125
+ round: s.current_round,
24126
+ metadata: JSON.stringify({ source: "reconciled", synthesized_from: "final.md" })
24127
+ });
24128
+ }
24129
+ } else if (mapFinal) {
24130
+ actions.push({
24131
+ sessionId: s.id,
24132
+ kind: "synthesize-map-completed",
24133
+ detail: `map.md present for run ${s.current_map_run}; synthesizing map_completed`
24134
+ });
24135
+ if (!dryRun) {
24136
+ insertEvent(db, {
24137
+ session_id: s.id,
24138
+ event_type: "map_completed",
24139
+ phase: "synthesis",
24140
+ phase_number: 5,
24141
+ round: s.current_map_run,
24142
+ metadata: JSON.stringify({ source: "reconciled", synthesized_from: "map.md" })
24143
+ });
24144
+ }
24145
+ } else {
24146
+ actions.push({
24147
+ sessionId: s.id,
24148
+ kind: "grandfather",
24149
+ detail: "no provable artifact; recording session_legacy_import"
24150
+ });
24151
+ if (!dryRun) {
24152
+ insertEvent(db, {
24153
+ session_id: s.id,
24154
+ event_type: "session_legacy_import",
24155
+ phase: "complete",
24156
+ metadata: JSON.stringify({ source: "reconciled" })
24157
+ });
24158
+ }
24159
+ }
24160
+ continue;
24161
+ }
24162
+ const age = lastEventAgeSeconds(db, s.id);
24163
+ const stale = (age === null || age > threshold) && !hasInFlightDependents(db, s.id);
24164
+ if (stale) {
24165
+ actions.push({
24166
+ sessionId: s.id,
24167
+ kind: "stale-close",
24168
+ 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`
24169
+ });
24170
+ if (!dryRun) {
24171
+ commitReasonClose(
24172
+ db,
24173
+ s.id,
24174
+ {
24175
+ event_type: "session_auto_closed_stale",
24176
+ phase: "complete",
24177
+ metadata: JSON.stringify({ source: "reconciled", threshold_seconds: threshold })
24178
+ },
24179
+ { status: "closed", current_phase: "complete" }
24180
+ );
24181
+ }
24182
+ }
24183
+ }
24184
+ return { dryRun, actions };
24185
+ }
24186
+ var DEFAULT_STALE_THRESHOLD_SECONDS;
24187
+ var init_reconcile = __esm({
24188
+ "src/lib/db/reconcile.ts"() {
24189
+ "use strict";
24190
+ init_queries();
24191
+ DEFAULT_STALE_THRESHOLD_SECONDS = 7 * 24 * 60 * 60;
24192
+ }
24193
+ });
24194
+
24195
+ // src/lib/db/liveness.ts
24196
+ function defaultIsAlive(pid) {
24197
+ try {
24198
+ process.kill(pid, 0);
24199
+ return true;
24200
+ } catch (err) {
24201
+ return !(err instanceof Error && "code" in err && err.code === "ESRCH");
24202
+ }
24203
+ }
24204
+ function sqliteUtcMs(ts) {
24205
+ const sqliteShape = ts.includes(" ");
24206
+ return new Date(sqliteShape ? ts.replace(" ", "T") + "Z" : ts).getTime();
24207
+ }
24208
+ var PID_REUSE_GUARD_MS;
24209
+ var init_liveness = __esm({
24210
+ "src/lib/db/liveness.ts"() {
24211
+ "use strict";
24212
+ PID_REUSE_GUARD_MS = 24 * 60 * 60 * 1e3;
24213
+ }
24214
+ });
24215
+
24216
+ // src/lib/state/exit-codes.ts
24217
+ var STATE_EXIT, StateError, CANCELLED_EXIT_CODE, ORPHAN_EXIT_CODE, CASCADE_CLOSE_EXIT_CODE;
24218
+ var init_exit_codes = __esm({
24219
+ "src/lib/state/exit-codes.ts"() {
24220
+ "use strict";
24221
+ STATE_EXIT = {
24222
+ OK: 0,
24223
+ USAGE: 2,
24224
+ AMBIGUOUS: 3,
24225
+ NOT_FOUND: 4,
24226
+ ILLEGAL_TRANSITION: 5,
24227
+ INVARIANT_UNMET: 6,
24228
+ SCHEMA_INVALID: 7,
24229
+ /** Database was locked past the bounded retry budget (SQLITE_BUSY). */
24230
+ BUSY: 8
24231
+ };
24232
+ StateError = class extends Error {
24233
+ constructor(code, message) {
24234
+ super(message);
24235
+ this.code = code;
24236
+ this.name = "StateError";
24237
+ }
24238
+ };
24239
+ CANCELLED_EXIT_CODE = -2;
24240
+ ORPHAN_EXIT_CODE = -3;
24241
+ CASCADE_CLOSE_EXIT_CODE = -4;
24242
+ }
24243
+ });
24244
+
23808
24245
  // src/lib/db/agent-sessions.ts
24246
+ function cascadeTerminateExecutions(db, workflowId, exitCode, note) {
24247
+ db.run(
24248
+ `UPDATE command_executions
24249
+ SET finished_at = datetime('now'),
24250
+ exit_code = ?,
24251
+ pid = NULL,
24252
+ notes = COALESCE(notes || char(10), '') || ?
24253
+ WHERE workflow_id = ?
24254
+ AND finished_at IS NULL`,
24255
+ [exitCode, note, workflowId]
24256
+ );
24257
+ }
23809
24258
  function rowToAgentSession(row) {
23810
24259
  return {
23811
24260
  // The OCR-owned id is the `uid` column. Fall back to the integer
@@ -23820,6 +24269,7 @@ function rowToAgentSession(row) {
23820
24269
  resolved_model: row.resolved_model,
23821
24270
  phase: null,
23822
24271
  status: deriveStatus(row),
24272
+ kind: rowKind(row),
23823
24273
  pid: row.pid,
23824
24274
  started_at: row.started_at,
23825
24275
  last_heartbeat_at: row.last_heartbeat_at ?? row.started_at,
@@ -23833,7 +24283,9 @@ function deriveStatus(row) {
23833
24283
  return "running";
23834
24284
  }
23835
24285
  if (row.exit_code === ORPHAN_EXIT_CODE) return "orphaned";
23836
- if (row.exit_code === CANCELLED_EXIT_CODE) return "cancelled";
24286
+ if (row.exit_code === CANCELLED_EXIT_CODE || row.exit_code === CASCADE_CLOSE_EXIT_CODE) {
24287
+ return "cancelled";
24288
+ }
23837
24289
  if (row.exit_code === 0) return "done";
23838
24290
  return "crashed";
23839
24291
  }
@@ -23849,7 +24301,7 @@ function insertAgentSession(db, params) {
23849
24301
  pid = null,
23850
24302
  notes = null
23851
24303
  } = params;
23852
- const command = persona && instance_index !== null ? `session-instance:${persona}-${instance_index}` : "session-instance";
24304
+ const command = persona && instance_index !== null ? `${INSTANCE_COMMAND}:${persona}-${instance_index}` : INSTANCE_COMMAND;
23853
24305
  db.run(
23854
24306
  `INSERT INTO command_executions
23855
24307
  (uid, command, args, workflow_id, vendor, persona, instance_index, name,
@@ -24054,63 +24506,139 @@ function updateAgentSession(db, id, params) {
24054
24506
  values
24055
24507
  );
24056
24508
  }
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])
24509
+ function sweepStaleAgentSessions(db, thresholdSeconds, isAlive = defaultIsAlive) {
24510
+ const candidates = resultToRows(
24511
+ db.exec(
24512
+ `SELECT uid, id, pid, started_at, workflow_id, command, last_heartbeat_at
24513
+ FROM command_executions
24514
+ WHERE finished_at IS NULL
24515
+ AND pid IS NOT NULL
24516
+ AND last_heartbeat_at IS NOT NULL
24517
+ AND (julianday('now') - julianday(last_heartbeat_at)) * 86400 > ?`,
24518
+ [thresholdSeconds]
24519
+ )
24066
24520
  );
24067
- if (stale.length === 0) {
24068
- return { orphanedIds: [] };
24521
+ if (candidates.length === 0) {
24522
+ return { orphanedIds: [], cascadedWorkflowIds: [] };
24523
+ }
24524
+ const reuseCutoffMs = Date.now() - PID_REUSE_GUARD_MS;
24525
+ const dead = candidates.filter((row) => {
24526
+ if (row.pid === null) return false;
24527
+ if (sqliteUtcMs(row.started_at) < reuseCutoffMs) return false;
24528
+ return !isAlive(row.pid);
24529
+ });
24530
+ if (dead.length === 0) {
24531
+ return { orphanedIds: [], cascadedWorkflowIds: [] };
24069
24532
  }
24070
24533
  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
- );
24534
+ const placeholders = dead.map(() => "?").join(", ");
24535
+ const cascadedWorkflowIds = [];
24536
+ db.transaction(() => {
24537
+ db.run(
24538
+ `UPDATE command_executions
24539
+ SET finished_at = datetime('now'),
24540
+ exit_code = ?,
24541
+ pid = NULL,
24542
+ notes = COALESCE(notes || char(10), '') || ?
24543
+ WHERE id IN (${placeholders})
24544
+ AND finished_at IS NULL`,
24545
+ [ORPHAN_EXIT_CODE, note, ...dead.map((r) => r.id)]
24546
+ );
24547
+ for (const row of dead) {
24548
+ if (row.workflow_id && rowKind(row) === "supervisor") {
24549
+ cascadeTerminateExecutions(
24550
+ db,
24551
+ row.workflow_id,
24552
+ CASCADE_CLOSE_EXIT_CODE,
24553
+ "cascade-closed: workflow process orphaned by liveness sweep"
24554
+ );
24555
+ cascadedWorkflowIds.push(row.workflow_id);
24556
+ }
24557
+ }
24558
+ });
24081
24559
  return {
24082
- orphanedIds: stale.map((row) => row.uid ?? String(row.id))
24560
+ orphanedIds: dead.map((r) => r.uid ?? String(r.id)),
24561
+ cascadedWorkflowIds
24083
24562
  };
24084
24563
  }
24085
- var ORPHAN_EXIT_CODE, CANCELLED_EXIT_CODE, NOTE_ORPHAN_PREFIX;
24564
+ function rowKind(row) {
24565
+ if (row.command === INSTANCE_COMMAND || row.command.startsWith(`${INSTANCE_COMMAND}:`)) {
24566
+ return "instance";
24567
+ }
24568
+ return row.last_heartbeat_at == null ? "utility" : "supervisor";
24569
+ }
24570
+ function sweepStaleSessions(db, thresholdSeconds) {
24571
+ const sql = `
24572
+ SELECT s.id
24573
+ FROM sessions s
24574
+ LEFT JOIN (
24575
+ SELECT session_id, MAX(created_at) AS last_event_at
24576
+ FROM orchestration_events
24577
+ GROUP BY session_id
24578
+ ) e ON e.session_id = s.id
24579
+ WHERE s.status = 'active'
24580
+ AND (
24581
+ e.last_event_at IS NULL
24582
+ OR (julianday('now') - julianday(e.last_event_at)) * 86400 > ?
24583
+ )
24584
+ AND NOT EXISTS (
24585
+ SELECT 1 FROM command_executions ce
24586
+ WHERE ce.workflow_id = s.id
24587
+ AND ce.finished_at IS NULL
24588
+ )
24589
+ `;
24590
+ const rows = resultToRows(db.exec(sql, [thresholdSeconds]));
24591
+ if (rows.length === 0) {
24592
+ return { closedSessionIds: [] };
24593
+ }
24594
+ for (const row of rows) {
24595
+ commitReasonClose(
24596
+ db,
24597
+ row.id,
24598
+ {
24599
+ event_type: "session_auto_closed_stale",
24600
+ phase: "complete",
24601
+ metadata: JSON.stringify({
24602
+ reason: "no events past threshold; no in-flight dependents",
24603
+ threshold_seconds: thresholdSeconds
24604
+ })
24605
+ },
24606
+ { status: "closed", current_phase: "complete" }
24607
+ );
24608
+ }
24609
+ return { closedSessionIds: rows.map((r) => r.id) };
24610
+ }
24611
+ var NOTE_ORPHAN_PREFIX, INSTANCE_COMMAND;
24086
24612
  var init_agent_sessions = __esm({
24087
24613
  "src/lib/db/agent-sessions.ts"() {
24088
24614
  "use strict";
24089
24615
  init_result_mapper();
24090
- ORPHAN_EXIT_CODE = -3;
24091
- CANCELLED_EXIT_CODE = -2;
24616
+ init_queries();
24617
+ init_liveness();
24618
+ init_exit_codes();
24092
24619
  NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
24620
+ INSTANCE_COMMAND = "session-instance";
24093
24621
  }
24094
24622
  });
24095
24623
 
24096
24624
  // 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";
24625
+ import { appendFileSync, existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync6 } from "node:fs";
24626
+ import { dirname as dirname5, join as join13 } from "node:path";
24099
24627
  import { randomUUID as randomUUID2 } from "node:crypto";
24100
24628
  function generateCommandUid() {
24101
24629
  return randomUUID2();
24102
24630
  }
24103
24631
  function cacheDir(ocrDir) {
24104
- return join12(ocrDir, "data", CACHE_DIR);
24632
+ return join13(ocrDir, "data", CACHE_DIR);
24105
24633
  }
24106
24634
  function commandLogPath(ocrDir) {
24107
- return join12(cacheDir(ocrDir), FILENAME);
24635
+ return join13(cacheDir(ocrDir), FILENAME);
24108
24636
  }
24109
24637
  function appendCommandLog(ocrDir, entry) {
24110
24638
  try {
24111
24639
  const filePath = commandLogPath(ocrDir);
24112
- const dir = dirname4(filePath);
24113
- if (!existsSync10(dir)) mkdirSync3(dir, { recursive: true });
24640
+ const dir = dirname5(filePath);
24641
+ if (!existsSync11(dir)) mkdirSync3(dir, { recursive: true });
24114
24642
  const line = JSON.stringify(entry) + "\n";
24115
24643
  appendFileSync(filePath, line, { encoding: "utf-8" });
24116
24644
  if (approxLineCount >= 0) approxLineCount++;
@@ -24120,7 +24648,7 @@ function appendCommandLog(ocrDir, entry) {
24120
24648
  }
24121
24649
  function readCommandLog(ocrDir) {
24122
24650
  const filePath = commandLogPath(ocrDir);
24123
- if (!existsSync10(filePath)) return [];
24651
+ if (!existsSync11(filePath)) return [];
24124
24652
  const content = readFileSync9(filePath, "utf-8");
24125
24653
  const entries = [];
24126
24654
  for (const line of content.split("\n")) {
@@ -24199,16 +24727,25 @@ var init_command_log = __esm({
24199
24727
  // src/lib/db/index.ts
24200
24728
  var db_exports = {};
24201
24729
  __export(db_exports, {
24730
+ CANCELLED_EXIT_CODE: () => CANCELLED_EXIT_CODE,
24731
+ CASCADE_CLOSE_EXIT_CODE: () => CASCADE_CLOSE_EXIT_CODE,
24202
24732
  MIGRATIONS: () => MIGRATIONS,
24733
+ ORPHAN_EXIT_CODE: () => ORPHAN_EXIT_CODE,
24734
+ PID_REUSE_GUARD_MS: () => PID_REUSE_GUARD_MS,
24735
+ STATE_EXIT: () => STATE_EXIT,
24736
+ StateError: () => StateError,
24203
24737
  appendCommandLog: () => appendCommandLog,
24204
- applyPragmas: () => applyPragmas,
24205
24738
  bindVendorSessionIdOpportunistically: () => bindVendorSessionIdOpportunistically,
24206
24739
  bumpAgentSessionHeartbeat: () => bumpAgentSessionHeartbeat,
24207
24740
  cacheDir: () => cacheDir,
24741
+ cascadeTerminateExecutions: () => cascadeTerminateExecutions,
24208
24742
  closeAllDatabases: () => closeAllDatabases,
24209
24743
  closeDatabase: () => closeDatabase,
24210
24744
  commandLogPath: () => commandLogPath,
24745
+ commitReasonClose: () => commitReasonClose,
24746
+ defaultIsAlive: () => defaultIsAlive,
24211
24747
  ensureDatabase: () => ensureDatabase,
24748
+ formatUpgradeNotice: () => formatUpgradeNotice,
24212
24749
  generateCommandUid: () => generateCommandUid,
24213
24750
  getAgentSession: () => getAgentSession,
24214
24751
  getAllSessions: () => getAllSessions,
@@ -24217,119 +24754,144 @@ __export(db_exports, {
24217
24754
  getLatestActiveSession: () => getLatestActiveSession,
24218
24755
  getLatestAgentSessionWithVendorId: () => getLatestAgentSessionWithVendorId,
24219
24756
  getLatestEventId: () => getLatestEventId,
24757
+ getSchemaVersion: () => getSchemaVersion,
24220
24758
  getSession: () => getSession,
24221
24759
  insertAgentSession: () => insertAgentSession,
24222
24760
  insertEvent: () => insertEvent,
24223
24761
  insertSession: () => insertSession,
24762
+ isBusyError: () => isBusyError,
24224
24763
  linkDashboardInvocationToWorkflow: () => linkDashboardInvocationToWorkflow,
24225
24764
  listAgentSessionsForWorkflow: () => listAgentSessionsForWorkflow,
24226
- locateWasm: () => locateWasm,
24227
24765
  openDatabase: () => openDatabase,
24766
+ probeEngine: () => probeEngine,
24228
24767
  readCommandLog: () => readCommandLog,
24768
+ reconcileLegacyState: () => reconcileLegacyState,
24229
24769
  recordVendorSessionIdForExecution: () => recordVendorSessionIdForExecution,
24230
24770
  replayCommandLog: () => replayCommandLog,
24231
24771
  resultToRow: () => resultToRow,
24232
24772
  resultToRows: () => resultToRows,
24773
+ rowKind: () => rowKind,
24233
24774
  runMigrations: () => runMigrations,
24234
- saveDatabase: () => saveDatabase,
24235
24775
  setAgentSessionStatus: () => setAgentSessionStatus,
24236
24776
  setAgentSessionVendorId: () => setAgentSessionVendorId,
24777
+ sqliteUtcMs: () => sqliteUtcMs,
24237
24778
  sweepStaleAgentSessions: () => sweepStaleAgentSessions,
24779
+ sweepStaleSessions: () => sweepStaleSessions,
24238
24780
  updateAgentSession: () => updateAgentSession,
24239
24781
  updateSession: () => updateSession,
24240
24782
  walCheckpointTruncate: () => walCheckpointTruncate
24241
24783
  });
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;");
24784
+ import { existsSync as existsSync12, mkdirSync as mkdirSync4, copyFileSync, statSync } from "node:fs";
24785
+ import { dirname as dirname6, join as join14 } from "node:path";
24786
+ function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
24787
+ if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
24788
+ const bakPath = `${dbPath}.bak.v${fromVersion}`;
24789
+ if (existsSync12(bakPath)) return bakPath;
24790
+ try {
24791
+ if (!existsSync12(dbPath) || statSync(dbPath).size === 0) return null;
24792
+ db.pragma("wal_checkpoint(TRUNCATE)");
24793
+ copyFileSync(dbPath, bakPath);
24794
+ return bakPath;
24795
+ } catch {
24796
+ return null;
24797
+ }
24798
+ }
24799
+ function formatUpgradeNotice(bakPath, reconcile) {
24800
+ const lines = [
24801
+ "Storage upgraded to v2.0 \u2014 durable SQLite engine (WAL), event-sourced lifecycle."
24802
+ ];
24803
+ if (bakPath) {
24804
+ lines.push(` A backup of your previous database was saved to: ${bakPath}`);
24805
+ }
24806
+ const repairs = (reconcile?.actions ?? []).filter((a) => a.kind !== "ok");
24807
+ if (repairs.length > 0) {
24808
+ const n = (kind) => repairs.filter((a) => a.kind === kind).length;
24809
+ const parts = [];
24810
+ const finalized = n("synthesize-round-completed") + n("synthesize-map-completed");
24811
+ if (finalized > 0) parts.push(`${finalized} finalized from artifacts`);
24812
+ if (n("grandfather") > 0) parts.push(`${n("grandfather")} grandfathered`);
24813
+ if (n("stale-close") > 0) parts.push(`${n("stale-close")} stale closed`);
24814
+ lines.push(
24815
+ ` Reconciled ${repairs.length} legacy session(s): ${parts.join(", ")}.`
24816
+ );
24817
+ }
24818
+ lines.push(" Run `ocr doctor` to verify the storage engine.");
24819
+ return lines.map((l) => `[ocr] ${l}`).join("\n");
24256
24820
  }
24257
24821
  async function openDatabase(dbPath) {
24258
24822
  const cached = connections.get(dbPath);
24259
24823
  if (cached) {
24260
24824
  return cached;
24261
24825
  }
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();
24826
+ const dir = dirname6(dbPath);
24827
+ if (!existsSync12(dir)) {
24828
+ mkdirSync4(dir, { recursive: true });
24276
24829
  }
24277
- applyPragmas(db);
24830
+ const db = openEngine(dbPath);
24278
24831
  connections.set(dbPath, db);
24279
24832
  return db;
24280
24833
  }
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
24834
  async function getDb(ocrDir) {
24292
- const dbPath = join13(ocrDir, "data", "ocr.db");
24835
+ const dbPath = join14(ocrDir, "data", "ocr.db");
24293
24836
  return openDatabase(dbPath);
24294
24837
  }
24295
24838
  async function ensureDatabase(ocrDir) {
24296
- const dataDir = join13(ocrDir, "data");
24297
- if (!existsSync11(dataDir)) {
24839
+ const dataDir = join14(ocrDir, "data");
24840
+ if (!existsSync12(dataDir)) {
24298
24841
  mkdirSync4(dataDir, { recursive: true });
24299
24842
  }
24300
- const dbPath = join13(dataDir, "ocr.db");
24843
+ const dbPath = join14(dataDir, "ocr.db");
24301
24844
  const db = await openDatabase(dbPath);
24845
+ let before = 0;
24846
+ try {
24847
+ before = getSchemaVersion(db);
24848
+ } catch {
24849
+ before = 0;
24850
+ }
24851
+ const isLegacyUpgrade = before >= 1 && before < V2_SCHEMA_VERSION;
24852
+ const bakPath = maybeSnapshotBeforeUpgrade(db, dbPath, before);
24302
24853
  runMigrations(db);
24303
- saveDatabase(db, dbPath);
24854
+ let reconcile;
24855
+ if (before < V2_SCHEMA_VERSION) {
24856
+ try {
24857
+ reconcile = reconcileLegacyState(db, ocrDir);
24858
+ } catch (err) {
24859
+ console.error(
24860
+ `[ocr] legacy reconciliation skipped: ${err instanceof Error ? err.message : String(err)}`
24861
+ );
24862
+ }
24863
+ }
24864
+ if (isLegacyUpgrade) {
24865
+ const notice = formatUpgradeNotice(bakPath, reconcile);
24866
+ if (notice) console.error(notice);
24867
+ }
24304
24868
  return db;
24305
24869
  }
24306
24870
  function walCheckpointTruncate(dbPath) {
24307
- if (!existsSync11(dbPath)) {
24871
+ if (!existsSync12(dbPath)) {
24308
24872
  return "skipped";
24309
24873
  }
24310
- try {
24311
- const probe = spawnSync2("sqlite3", ["-version"], {
24312
- stdio: "ignore",
24313
- timeout: 2e3
24314
- });
24315
- if (probe.status !== 0) {
24316
- return "skipped";
24874
+ const cached = connections.get(dbPath);
24875
+ if (cached) {
24876
+ try {
24877
+ cached.pragma("wal_checkpoint(TRUNCATE)");
24878
+ return "checkpointed";
24879
+ } catch {
24880
+ return "failed";
24317
24881
  }
24318
- } catch {
24319
- return "skipped";
24320
24882
  }
24883
+ let transient;
24321
24884
  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";
24885
+ transient = openEngine(dbPath);
24886
+ transient.pragma("wal_checkpoint(TRUNCATE)");
24887
+ return "checkpointed";
24331
24888
  } catch {
24332
24889
  return "failed";
24890
+ } finally {
24891
+ try {
24892
+ transient?.raw.close();
24893
+ } catch {
24894
+ }
24333
24895
  }
24334
24896
  }
24335
24897
  function closeDatabase(dbPath) {
@@ -24345,16 +24907,24 @@ function closeAllDatabases() {
24345
24907
  connections.delete(path2);
24346
24908
  }
24347
24909
  }
24348
- var connections;
24910
+ var V2_SCHEMA_VERSION, connections;
24349
24911
  var init_db = __esm({
24350
24912
  "src/lib/db/index.ts"() {
24351
24913
  "use strict";
24914
+ init_engine();
24352
24915
  init_migrations();
24916
+ init_reconcile();
24353
24917
  init_queries();
24354
24918
  init_agent_sessions();
24919
+ init_liveness();
24920
+ init_exit_codes();
24355
24921
  init_migrations();
24356
24922
  init_result_mapper();
24923
+ init_engine();
24924
+ init_reconcile();
24925
+ init_migrations();
24357
24926
  init_command_log();
24927
+ V2_SCHEMA_VERSION = 12;
24358
24928
  connections = /* @__PURE__ */ new Map();
24359
24929
  }
24360
24930
  });
@@ -28763,7 +29333,7 @@ ${hint}
28763
29333
  }
28764
29334
 
28765
29335
  // src/lib/version.ts
28766
- var CLI_VERSION = true ? "1.11.0" : createRequire(import.meta.url)("../../package.json").version;
29336
+ var CLI_VERSION = true ? "2.0.0" : createRequire(import.meta.url)("../../package.json").version;
28767
29337
 
28768
29338
  // ../shared/platform/src/index.ts
28769
29339
  import { pathToFileURL } from "node:url";
@@ -29822,9 +30392,9 @@ var NodeFsHandler = class {
29822
30392
  if (this.fsw.closed) {
29823
30393
  return;
29824
30394
  }
29825
- const dirname7 = sysPath.dirname(file);
30395
+ const dirname8 = sysPath.dirname(file);
29826
30396
  const basename8 = sysPath.basename(file);
29827
- const parent = this.fsw._getWatchedDir(dirname7);
30397
+ const parent = this.fsw._getWatchedDir(dirname8);
29828
30398
  let prevStats = stats;
29829
30399
  if (parent.has(basename8))
29830
30400
  return;
@@ -29851,7 +30421,7 @@ var NodeFsHandler = class {
29851
30421
  prevStats = newStats2;
29852
30422
  }
29853
30423
  } catch (error) {
29854
- this.fsw._remove(dirname7, basename8);
30424
+ this.fsw._remove(dirname8, basename8);
29855
30425
  }
29856
30426
  } else if (parent.has(basename8)) {
29857
30427
  const at = newStats.atimeMs;
@@ -30784,8 +31354,8 @@ function watch(paths, options = {}) {
30784
31354
  }
30785
31355
 
30786
31356
  // 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";
31357
+ import { existsSync as existsSync13, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
31358
+ import { join as join15, basename as basename7 } from "node:path";
30789
31359
 
30790
31360
  // ../../node_modules/.pnpm/log-update@7.0.2/node_modules/log-update/index.js
30791
31361
  import process12 from "node:process";
@@ -32421,15 +32991,15 @@ function debounce(fn, delay) {
32421
32991
  };
32422
32992
  }
32423
32993
  function findLatestActiveSession(sessionsDir) {
32424
- if (!existsSync12(sessionsDir)) {
32994
+ if (!existsSync13(sessionsDir)) {
32425
32995
  return null;
32426
32996
  }
32427
32997
  const sessions = readdirSync5(sessionsDir).filter((name) => {
32428
- const sessionPath = join14(sessionsDir, name);
32429
- return statSync(sessionPath).isDirectory();
32998
+ const sessionPath = join15(sessionsDir, name);
32999
+ return statSync2(sessionPath).isDirectory();
32430
33000
  }).sort().reverse();
32431
33001
  for (const session of sessions) {
32432
- const sessionPath = join14(sessionsDir, session);
33002
+ const sessionPath = join15(sessionsDir, session);
32433
33003
  if (isSessionActive(sessionPath)) {
32434
33004
  return session;
32435
33005
  }
@@ -32444,8 +33014,8 @@ function getStrategyForSession(sessionPath, explicitWorkflow) {
32444
33014
  return getStrategy(workflowType) ?? null;
32445
33015
  }
32446
33016
  async function initProgressDb(ocrDir) {
32447
- const dbPath = join14(ocrDir, "data", "ocr.db");
32448
- if (!existsSync12(dbPath)) {
33017
+ const dbPath = join15(ocrDir, "data", "ocr.db");
33018
+ if (!existsSync13(dbPath)) {
32449
33019
  return;
32450
33020
  }
32451
33021
  try {
@@ -32470,11 +33040,11 @@ var progressCommand = new Command("progress").description("Watch real-time progr
32470
33040
  const targetDir = process.cwd();
32471
33041
  requireOcrSetup(targetDir);
32472
33042
  const sessionsDir = ensureSessionsDir(targetDir);
32473
- const ocrDir = join14(targetDir, ".ocr");
33043
+ const ocrDir = join15(targetDir, ".ocr");
32474
33044
  await initProgressDb(ocrDir);
32475
33045
  if (options.session) {
32476
- const sessionPath = join14(sessionsDir, options.session);
32477
- if (!existsSync12(sessionPath)) {
33046
+ const sessionPath = join15(sessionsDir, options.session);
33047
+ if (!existsSync13(sessionPath)) {
32478
33048
  console.error(source_default.red(`Session not found: ${options.session}`));
32479
33049
  process.exit(1);
32480
33050
  }
@@ -32499,7 +33069,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
32499
33069
  );
32500
33070
  console.error(
32501
33071
  source_default.dim(
32502
- `The orchestrating agent must create state via 'ocr state init' for progress tracking.`
33072
+ `The orchestrating agent must create state via 'ocr state begin' for progress tracking.`
32503
33073
  )
32504
33074
  );
32505
33075
  process.exit(1);
@@ -32535,7 +33105,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
32535
33105
  return;
32536
33106
  }
32537
33107
  let currentSession = findLatestActiveSession(sessionsDir);
32538
- let currentSessionPath = currentSession ? join14(sessionsDir, currentSession) : null;
33108
+ let currentSessionPath = currentSession ? join15(sessionsDir, currentSession) : null;
32539
33109
  let sessionWatcher = null;
32540
33110
  const preservedStartTimes = {
32541
33111
  review: void 0,
@@ -32543,11 +33113,11 @@ var progressCommand = new Command("progress").description("Watch real-time progr
32543
33113
  };
32544
33114
  let currentStrategy = null;
32545
33115
  const updateDisplayImpl = () => {
32546
- if (!currentSessionPath || !existsSync12(currentSessionPath) || !isSessionActive(currentSessionPath)) {
33116
+ if (!currentSessionPath || !existsSync13(currentSessionPath) || !isSessionActive(currentSessionPath)) {
32547
33117
  const latestActive = findLatestActiveSession(sessionsDir);
32548
33118
  if (latestActive && latestActive !== currentSession) {
32549
33119
  currentSession = latestActive;
32550
- currentSessionPath = join14(sessionsDir, latestActive);
33120
+ currentSessionPath = join15(sessionsDir, latestActive);
32551
33121
  preservedStartTimes.review = void 0;
32552
33122
  preservedStartTimes.map = void 0;
32553
33123
  currentStrategy = null;
@@ -32560,7 +33130,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
32560
33130
  currentStrategy = null;
32561
33131
  }
32562
33132
  }
32563
- if (currentSessionPath && existsSync12(currentSessionPath)) {
33133
+ if (currentSessionPath && existsSync13(currentSessionPath)) {
32564
33134
  if (!options.workflow) {
32565
33135
  const activeWorkflows = detectActiveWorkflows(currentSessionPath);
32566
33136
  if (activeWorkflows.length > 1) {
@@ -32614,15 +33184,15 @@ var progressCommand = new Command("progress").description("Watch real-time progr
32614
33184
  watchSession(currentSessionPath);
32615
33185
  }
32616
33186
  const timerInterval = setInterval(updateDisplay, 1e3);
32617
- const watchDir = existsSync12(ocrDir) ? ocrDir : targetDir;
33187
+ const watchDir = existsSync13(ocrDir) ? ocrDir : targetDir;
32618
33188
  const dirWatcher = watch(watchDir, {
32619
33189
  persistent: true,
32620
33190
  ignoreInitial: true,
32621
33191
  depth: 3
32622
33192
  });
32623
33193
  dirWatcher.on("addDir", (dirPath) => {
32624
- const parentDir = join14(dirPath, "..");
32625
- const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(join14(".ocr", "sessions"));
33194
+ const parentDir = join15(dirPath, "..");
33195
+ const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(join15(".ocr", "sessions"));
32626
33196
  if (isDirectChild && !dirPath.endsWith("sessions")) {
32627
33197
  const newSession = basename7(dirPath);
32628
33198
  currentSession = newSession;
@@ -32724,226 +33294,115 @@ function renderCombinedProgress(sessionPath, preservedStartTimes, ocrDir) {
32724
33294
  }
32725
33295
 
32726
33296
  // 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";
33297
+ import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync11 } from "node:fs";
33298
+ import { join as join17 } from "node:path";
32729
33299
 
32730
33300
  // src/lib/state/index.ts
32731
33301
  init_db();
33302
+ init_exit_codes();
32732
33303
  import {
32733
- existsSync as existsSync13,
33304
+ existsSync as existsSync14,
32734
33305
  mkdirSync as mkdirSync5,
32735
33306
  readdirSync as readdirSync6,
32736
- readFileSync as readFileSync11,
32737
- statSync as statSync2,
32738
- writeFileSync as writeFileSync8
33307
+ readFileSync as readFileSync10,
33308
+ statSync as statSync3,
33309
+ writeFileSync as writeFileSync7
32739
33310
  } 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 {
33311
+ import { join as join16 } from "node:path";
33312
+
33313
+ // src/lib/state/phase-graph.ts
33314
+ init_exit_codes();
33315
+ var REVIEW_PHASE_NUMBERS = {
33316
+ context: 1,
33317
+ "change-context": 2,
33318
+ analysis: 3,
33319
+ reviews: 4,
33320
+ aggregation: 5,
33321
+ discourse: 6,
33322
+ synthesis: 7,
33323
+ complete: 8
33324
+ };
33325
+ var MAP_PHASE_NUMBERS = {
33326
+ "map-context": 1,
33327
+ topology: 2,
33328
+ "flow-analysis": 3,
33329
+ "requirements-mapping": 4,
33330
+ synthesis: 5,
33331
+ complete: 6
33332
+ };
33333
+ function phaseNumberFor(workflowType, phase) {
33334
+ const map = workflowType === "map" ? MAP_PHASE_NUMBERS : REVIEW_PHASE_NUMBERS;
33335
+ const n = map[phase];
33336
+ if (n === void 0) {
33337
+ throw new StateError(
33338
+ STATE_EXIT.ILLEGAL_TRANSITION,
33339
+ `Invalid phase "${phase}" for workflow_type "${workflowType}". Valid: ${Object.keys(map).join(", ")}`
33340
+ );
32751
33341
  }
32752
- return false;
33342
+ return n;
32753
33343
  }
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;
33344
+ var REVIEW_PHASE_GRAPH = {
33345
+ context: ["change-context"],
33346
+ "change-context": ["analysis"],
33347
+ analysis: ["reviews"],
33348
+ reviews: ["aggregation"],
33349
+ aggregation: ["discourse"],
33350
+ discourse: ["synthesis"],
33351
+ synthesis: ["complete"],
33352
+ complete: ["context"]
33353
+ };
33354
+ var MAP_PHASE_GRAPH = {
33355
+ "map-context": ["topology"],
33356
+ topology: ["flow-analysis"],
33357
+ "flow-analysis": ["requirements-mapping"],
33358
+ "requirements-mapping": ["synthesis"],
33359
+ synthesis: ["complete"],
33360
+ complete: ["map-context"]
33361
+ };
33362
+ function graphFor(workflowType) {
33363
+ return workflowType === "review" ? REVIEW_PHASE_GRAPH : MAP_PHASE_GRAPH;
32807
33364
  }
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
- });
32838
- }
32839
- saveDatabase(db, dbPath);
33365
+ function initialPhaseFor(workflowType) {
33366
+ return workflowType === "map" ? "map-context" : "context";
32840
33367
  }
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}`);
33368
+ function validatePhaseTransition(workflowType, source, target, isRoundBoundary) {
33369
+ const graph = graphFor(workflowType);
33370
+ if (!(target in graph)) {
33371
+ const validPhases = Object.keys(graph).join(", ");
33372
+ throw new StateError(
33373
+ STATE_EXIT.ILLEGAL_TRANSITION,
33374
+ `Invalid phase "${target}" for workflow_type "${workflowType}". Valid phases: ${validPhases}`
33375
+ );
32848
33376
  }
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);
32861
- }
32862
- async function stateShow(ocrDir, sessionId) {
32863
- let db;
32864
- try {
32865
- db = await ensureDatabase(ocrDir);
32866
- } catch {
32867
- return null;
33377
+ if (source === target) return;
33378
+ if (isRoundBoundary) {
33379
+ const initial = initialPhaseFor(workflowType);
33380
+ if (target === initial) return;
33381
+ throw new StateError(
33382
+ STATE_EXIT.ILLEGAL_TRANSITION,
33383
+ `Illegal round-boundary transition: a new round/run must reset to "${initial}", not "${target}".`
33384
+ );
32868
33385
  }
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
- };
32897
- }
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");
32903
- }
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");
32915
- }
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"}`
32924
- );
33386
+ const allowed = graph[source];
33387
+ if (!allowed || !allowed.includes(target)) {
33388
+ throw new StateError(
33389
+ STATE_EXIT.ILLEGAL_TRANSITION,
33390
+ `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.`
33391
+ );
32925
33392
  }
32926
33393
  }
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
- };
33394
+
33395
+ // src/lib/state/meta-util.ts
33396
+ var DEFAULT_METADATA_MAX_LEN = 4096;
33397
+ function sanitizeMetadataString(s, opts = {}) {
33398
+ const maxLen = opts.maxLen ?? DEFAULT_METADATA_MAX_LEN;
33399
+ let out = s.replace(/[\x00-\x08\x0b-\x1f]/g, "");
33400
+ out = out.replace(/^\s*\[ocr\]\s*/i, "");
33401
+ if (out.length > maxLen) out = out.slice(0, maxLen);
33402
+ return out;
32946
33403
  }
33404
+
33405
+ // src/lib/state/round-meta.ts
32947
33406
  var VALID_CATEGORIES = /* @__PURE__ */ new Set(["blocker", "should_fix", "suggestion", "style"]);
32948
33407
  var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low", "info"]);
32949
33408
  function validateRoundMeta(meta) {
@@ -32959,6 +33418,7 @@ function validateRoundMeta(meta) {
32959
33418
  if (typeof obj.verdict !== "string" || obj.verdict.trim().length === 0) {
32960
33419
  throw new Error("round-meta.json must contain a non-empty verdict string");
32961
33420
  }
33421
+ obj.verdict = sanitizeMetadataString(obj.verdict);
32962
33422
  if (!Array.isArray(obj.reviewers)) {
32963
33423
  throw new Error("round-meta.json must contain a reviewers array");
32964
33424
  }
@@ -32984,6 +33444,7 @@ function validateRoundMeta(meta) {
32984
33444
  if (typeof f.title !== "string" || f.title.trim().length === 0) {
32985
33445
  throw new Error("Each finding must have a non-empty title");
32986
33446
  }
33447
+ f.title = sanitizeMetadataString(f.title);
32987
33448
  if (typeof f.category !== "string" || !VALID_CATEGORIES.has(f.category)) {
32988
33449
  throw new Error(
32989
33450
  `Finding "${f.title}" has invalid category: "${String(f.category)}". Must be one of: ${[...VALID_CATEGORIES].join(", ")}`
@@ -32997,6 +33458,7 @@ function validateRoundMeta(meta) {
32997
33458
  if (typeof f.summary !== "string") {
32998
33459
  throw new Error(`Finding "${f.title}" must have a summary string`);
32999
33460
  }
33461
+ f.summary = sanitizeMetadataString(f.summary);
33000
33462
  if (f.file_path !== void 0 && typeof f.file_path !== "string") {
33001
33463
  throw new Error(`Finding "${f.title}" has invalid file_path: expected string`);
33002
33464
  }
@@ -33042,43 +33504,8 @@ function computeRoundCounts(meta) {
33042
33504
  totalFindingCount: allFindings.length
33043
33505
  };
33044
33506
  }
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
- }
33507
+
33508
+ // src/lib/state/map-meta.ts
33082
33509
  function validateMapMeta(meta) {
33083
33510
  if (!meta || typeof meta !== "object") {
33084
33511
  throw new Error("map-meta.json must be a JSON object");
@@ -33103,6 +33530,13 @@ function validateMapMeta(meta) {
33103
33530
  if (typeof s.title !== "string" || s.title.trim().length === 0) {
33104
33531
  throw new Error("Each section must have a non-empty title");
33105
33532
  }
33533
+ s.title = sanitizeMetadataString(s.title);
33534
+ if (s.description !== void 0) {
33535
+ if (typeof s.description !== "string") {
33536
+ throw new Error(`Section "${s.title}" description must be a string if provided`);
33537
+ }
33538
+ s.description = sanitizeMetadataString(s.description);
33539
+ }
33106
33540
  if (!Array.isArray(s.files)) {
33107
33541
  throw new Error(`Section "${s.title}" must have a files array`);
33108
33542
  }
@@ -33117,6 +33551,7 @@ function validateMapMeta(meta) {
33117
33551
  if (typeof f.role !== "string") {
33118
33552
  throw new Error(`File "${f.file_path}" must have a role string`);
33119
33553
  }
33554
+ f.role = sanitizeMetadataString(f.role);
33120
33555
  if (typeof f.lines_added !== "number") {
33121
33556
  throw new Error(`File "${f.file_path}" must have a lines_added number`);
33122
33557
  }
@@ -33136,53 +33571,597 @@ function computeMapCounts(meta) {
33136
33571
  fileCount: meta.sections.reduce((sum, s) => sum + s.files.length, 0)
33137
33572
  };
33138
33573
  }
33139
- async function stateMapComplete(params) {
33140
- const { ocrDir } = params;
33141
- 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;
33150
- let metaPath;
33151
- if (params.source === "stdin") {
33152
- const runDir = join15(session.session_dir, "map", "runs", `run-${mapRunNumber}`);
33153
- 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
- })
33168
- });
33169
- saveDatabase(db, dbPath);
33170
- return { sessionId: session.id, mapRun: mapRunNumber, metaPath };
33171
- }
33574
+
33575
+ // src/lib/state/projection.ts
33576
+ init_db();
33577
+ var REASON_EVENT_TYPES = [
33578
+ "session_aborted",
33579
+ "session_auto_closed_stale",
33580
+ "session_synced",
33581
+ "session_legacy_import"
33582
+ ];
33583
+ var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
33584
+ "session_closed",
33585
+ ...REASON_EVENT_TYPES
33586
+ ]);
33587
+ function hasCompletionInvariant(db, session) {
33588
+ const eventType = session.workflow_type === "map" ? "map_completed" : "round_completed";
33589
+ const round = session.workflow_type === "map" ? session.current_map_run : session.current_round;
33590
+ const r = db.exec(
33591
+ `SELECT 1 FROM orchestration_events
33592
+ WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
33593
+ [session.id, eventType, round]
33594
+ );
33595
+ return (r[0]?.values.length ?? 0) > 0;
33596
+ }
33597
+ function getCompletenessState(db, sessionId) {
33598
+ const r = db.exec(
33599
+ "SELECT completeness_state FROM session_completeness WHERE session_id = ?",
33600
+ [sessionId]
33601
+ );
33602
+ return r[0]?.values[0]?.[0] ?? null;
33603
+ }
33604
+
33605
+ // src/lib/state/index.ts
33606
+ init_exit_codes();
33607
+ function deriveNextRound(db, sessionId, fallbackRound) {
33608
+ const result = db.exec(
33609
+ `SELECT MAX(round) FROM orchestration_events
33610
+ WHERE session_id = ? AND event_type = 'round_completed'`,
33611
+ [sessionId]
33612
+ );
33613
+ const max = result[0]?.values[0]?.[0];
33614
+ if (typeof max === "number") return max + 1;
33615
+ return fallbackRound;
33616
+ }
33617
+ function hasArtifacts(dir) {
33618
+ try {
33619
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
33620
+ if (entry.isDirectory()) {
33621
+ if (hasArtifacts(join16(dir, entry.name))) return true;
33622
+ } else if (/\.(md|json)$/.test(entry.name)) {
33623
+ return true;
33624
+ }
33625
+ }
33626
+ } catch {
33627
+ }
33628
+ return false;
33629
+ }
33630
+ function readJsonFromSource(params) {
33631
+ if (params.source === "file") {
33632
+ if (!existsSync14(params.filePath)) {
33633
+ throw new StateError(STATE_EXIT.NOT_FOUND, `File not found: ${params.filePath}`);
33634
+ }
33635
+ return readFileSync10(params.filePath, "utf-8");
33636
+ }
33637
+ return params.data;
33638
+ }
33639
+ function parseRawJson(raw, label) {
33640
+ try {
33641
+ return JSON.parse(raw);
33642
+ } catch (err) {
33643
+ throw new StateError(
33644
+ STATE_EXIT.SCHEMA_INVALID,
33645
+ `Failed to parse ${label}: ${err instanceof Error ? err.message : "invalid JSON"}`
33646
+ );
33647
+ }
33648
+ }
33649
+ async function stateInit(params) {
33650
+ const { sessionId, branch, workflowType, sessionDir, ocrDir } = params;
33651
+ const db = await ensureDatabase(ocrDir);
33652
+ const existing = getSession(db, sessionId);
33653
+ if (existing) {
33654
+ if (existing.workflow_type !== workflowType) {
33655
+ throw new StateError(
33656
+ STATE_EXIT.USAGE,
33657
+ `Cannot re-open session ${sessionId} as workflow_type "${workflowType}": existing workflow_type is "${existing.workflow_type}". Maps and reviews have disjoint phase graphs.`
33658
+ );
33659
+ }
33660
+ const nextRound = deriveNextRound(db, sessionId, existing.current_round);
33661
+ const initialPhase2 = workflowType === "map" ? "map-context" : "context";
33662
+ db.transaction(() => {
33663
+ updateSession(db, sessionId, {
33664
+ status: "active",
33665
+ current_phase: initialPhase2,
33666
+ phase_number: 1,
33667
+ current_round: nextRound
33668
+ });
33669
+ insertEvent(db, {
33670
+ session_id: sessionId,
33671
+ event_type: nextRound > (existing.current_round ?? 1) ? "round_started" : "session_resumed",
33672
+ phase: initialPhase2,
33673
+ phase_number: 1,
33674
+ round: nextRound
33675
+ });
33676
+ });
33677
+ return sessionId;
33678
+ }
33679
+ const initialPhase = workflowType === "map" ? "map-context" : "context";
33680
+ db.transaction(() => {
33681
+ insertSession(db, {
33682
+ id: sessionId,
33683
+ branch,
33684
+ workflow_type: workflowType,
33685
+ current_phase: initialPhase,
33686
+ phase_number: 1,
33687
+ current_round: 1,
33688
+ current_map_run: 1,
33689
+ session_dir: sessionDir
33690
+ });
33691
+ insertEvent(db, {
33692
+ session_id: sessionId,
33693
+ event_type: "session_created",
33694
+ phase: initialPhase,
33695
+ phase_number: 1,
33696
+ round: 1
33697
+ });
33698
+ });
33699
+ return sessionId;
33700
+ }
33701
+ async function stateTransition(params, db) {
33702
+ const { sessionId, phase, phaseNumber, round, mapRun, ocrDir } = params;
33703
+ db ??= await ensureDatabase(ocrDir);
33704
+ const existing = getSession(db, sessionId);
33705
+ if (!existing) {
33706
+ throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
33707
+ }
33708
+ const previousRound = existing.current_round;
33709
+ const previousMapRun = existing.current_map_run;
33710
+ const isRoundBoundary = round !== void 0 && round !== previousRound || mapRun !== void 0 && mapRun !== previousMapRun;
33711
+ validatePhaseTransition(
33712
+ existing.workflow_type,
33713
+ existing.current_phase,
33714
+ phase,
33715
+ isRoundBoundary
33716
+ );
33717
+ db.transaction(() => {
33718
+ updateSession(db, sessionId, {
33719
+ current_phase: phase,
33720
+ phase_number: phaseNumber,
33721
+ ...round !== void 0 ? { current_round: round } : {},
33722
+ ...mapRun !== void 0 ? { current_map_run: mapRun } : {}
33723
+ });
33724
+ insertEvent(db, {
33725
+ session_id: sessionId,
33726
+ event_type: "phase_transition",
33727
+ phase,
33728
+ phase_number: phaseNumber,
33729
+ round: round ?? existing.current_round
33730
+ });
33731
+ if (round !== void 0 && round !== previousRound) {
33732
+ insertEvent(db, {
33733
+ session_id: sessionId,
33734
+ event_type: "round_started",
33735
+ phase,
33736
+ phase_number: phaseNumber,
33737
+ round
33738
+ });
33739
+ }
33740
+ });
33741
+ }
33742
+ async function stateClose(params) {
33743
+ const { sessionId, ocrDir, abort } = params;
33744
+ const db = await ensureDatabase(ocrDir);
33745
+ const existing = getSession(db, sessionId);
33746
+ if (!existing) {
33747
+ throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
33748
+ }
33749
+ if (existing.status === "closed") {
33750
+ console.error(`[ocr] Session already closed: ${sessionId}`);
33751
+ return;
33752
+ }
33753
+ if (!abort && !hasCompletionInvariant(db, existing)) {
33754
+ 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`;
33755
+ throw new StateError(
33756
+ STATE_EXIT.INVARIANT_UNMET,
33757
+ `Cannot close session ${sessionId}: ${what}. Run 'ocr state complete-round' to finalize it, or pass --abort to record an abandoned session.`
33758
+ );
33759
+ }
33760
+ const note = "closed by parent workflow close";
33761
+ db.transaction(() => {
33762
+ if (abort) {
33763
+ insertEvent(db, {
33764
+ session_id: sessionId,
33765
+ event_type: "session_aborted",
33766
+ phase: existing.current_phase,
33767
+ phase_number: existing.phase_number,
33768
+ round: existing.current_round
33769
+ });
33770
+ }
33771
+ updateSession(db, sessionId, {
33772
+ status: "closed",
33773
+ current_phase: "complete"
33774
+ });
33775
+ if (!abort) {
33776
+ insertEvent(db, {
33777
+ session_id: sessionId,
33778
+ event_type: "session_closed",
33779
+ phase: "complete",
33780
+ phase_number: existing.phase_number,
33781
+ round: existing.current_round
33782
+ });
33783
+ }
33784
+ cascadeTerminateExecutions(db, sessionId, CASCADE_CLOSE_EXIT_CODE, note);
33785
+ });
33786
+ }
33787
+ async function stateShow(ocrDir, sessionId) {
33788
+ let db;
33789
+ try {
33790
+ db = await ensureDatabase(ocrDir);
33791
+ } catch {
33792
+ return null;
33793
+ }
33794
+ const session = sessionId ? getSession(db, sessionId) : getLatestActiveSession(db);
33795
+ if (!session) {
33796
+ return null;
33797
+ }
33798
+ const events = getEventsForSession(db, session.id);
33799
+ return {
33800
+ session: {
33801
+ id: session.id,
33802
+ branch: session.branch,
33803
+ status: session.status,
33804
+ workflow_type: session.workflow_type,
33805
+ current_phase: session.current_phase,
33806
+ phase_number: session.phase_number,
33807
+ current_round: session.current_round,
33808
+ current_map_run: session.current_map_run,
33809
+ started_at: session.started_at,
33810
+ updated_at: session.updated_at
33811
+ },
33812
+ events: events.map((e) => ({
33813
+ id: e.id,
33814
+ event_type: e.event_type,
33815
+ phase: e.phase,
33816
+ phase_number: e.phase_number,
33817
+ round: e.round,
33818
+ metadata: e.metadata,
33819
+ created_at: e.created_at
33820
+ }))
33821
+ };
33822
+ }
33823
+ function resolveSession(db, explicitId) {
33824
+ if (explicitId) {
33825
+ const s = getSession(db, explicitId);
33826
+ if (!s) throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${explicitId}`);
33827
+ return {
33828
+ id: s.id,
33829
+ session_dir: s.session_dir,
33830
+ current_round: s.current_round,
33831
+ current_map_run: s.current_map_run,
33832
+ workflow_type: s.workflow_type,
33833
+ status: s.status,
33834
+ current_phase: s.current_phase,
33835
+ phase_number: s.phase_number,
33836
+ branch: s.branch,
33837
+ decision: "explicit"
33838
+ };
33839
+ }
33840
+ const uid = process.env["OCR_DASHBOARD_EXECUTION_UID"];
33841
+ if (uid) {
33842
+ const result = db.exec(
33843
+ "SELECT workflow_id FROM command_executions WHERE uid = ?",
33844
+ [uid]
33845
+ );
33846
+ const workflowId = result[0]?.values[0]?.[0];
33847
+ if (workflowId) {
33848
+ const s = getSession(db, workflowId);
33849
+ if (s) {
33850
+ return {
33851
+ id: s.id,
33852
+ session_dir: s.session_dir,
33853
+ current_round: s.current_round,
33854
+ current_map_run: s.current_map_run,
33855
+ workflow_type: s.workflow_type,
33856
+ status: s.status,
33857
+ current_phase: s.current_phase,
33858
+ phase_number: s.phase_number,
33859
+ branch: s.branch,
33860
+ decision: "dashboard-uid"
33861
+ };
33862
+ }
33863
+ }
33864
+ }
33865
+ const activeRows = db.exec(
33866
+ `SELECT id, session_dir, current_round, current_map_run, workflow_type,
33867
+ status, current_phase, phase_number, branch
33868
+ FROM sessions
33869
+ WHERE status = 'active'
33870
+ ORDER BY started_at DESC`
33871
+ );
33872
+ const rows = activeRows[0]?.values ?? [];
33873
+ if (rows.length === 0) throw new StateError(STATE_EXIT.NOT_FOUND, "No active session found");
33874
+ if (rows.length > 1) {
33875
+ const ids = rows.map((r) => r[0]);
33876
+ throw new StateError(
33877
+ STATE_EXIT.AMBIGUOUS,
33878
+ `Ambiguous auto-detect: ${rows.length} active sessions exist. Pass --session-id explicitly. Candidates: ${ids.join(", ")}`
33879
+ );
33880
+ }
33881
+ const row = rows[0];
33882
+ return {
33883
+ id: row[0],
33884
+ session_dir: row[1],
33885
+ current_round: row[2],
33886
+ current_map_run: row[3],
33887
+ workflow_type: row[4],
33888
+ status: row[5],
33889
+ current_phase: row[6],
33890
+ phase_number: row[7],
33891
+ branch: row[8],
33892
+ decision: "latest-active"
33893
+ };
33894
+ }
33895
+ function announceResolveDecision(r) {
33896
+ if (r.decision === "explicit") return;
33897
+ const path2 = r.decision === "dashboard-uid" ? "via OCR_DASHBOARD_EXECUTION_UID" : "via latest-active";
33898
+ console.error(`[ocr] Auto-detected session: ${r.id} (${path2})`);
33899
+ }
33900
+ async function resolveActiveSession(ocrDir, explicitId) {
33901
+ const db = await ensureDatabase(ocrDir);
33902
+ const result = resolveSession(db, explicitId);
33903
+ announceResolveDecision(result);
33904
+ return {
33905
+ id: result.id,
33906
+ sessionDir: result.session_dir,
33907
+ decision: result.decision
33908
+ };
33909
+ }
33910
+ async function stateBegin(params) {
33911
+ const id = await stateInit(params);
33912
+ const db = await ensureDatabase(params.ocrDir);
33913
+ const s = getSession(db, id);
33914
+ return {
33915
+ schema_version: 1,
33916
+ session_id: id,
33917
+ round: s?.current_round ?? 1,
33918
+ phase: s?.current_phase ?? "context",
33919
+ completeness: getCompletenessState(db, id)
33920
+ };
33921
+ }
33922
+ async function stateAdvance(params) {
33923
+ const db = await ensureDatabase(params.ocrDir);
33924
+ const existing = getSession(db, params.sessionId);
33925
+ if (!existing) {
33926
+ throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${params.sessionId}`);
33927
+ }
33928
+ const phaseNumber = phaseNumberFor(existing.workflow_type, params.phase);
33929
+ await stateTransition(
33930
+ {
33931
+ sessionId: params.sessionId,
33932
+ phase: params.phase,
33933
+ phaseNumber,
33934
+ round: params.round,
33935
+ mapRun: params.mapRun,
33936
+ ocrDir: params.ocrDir
33937
+ },
33938
+ db
33939
+ );
33940
+ }
33941
+ async function stateCompleteRound(params) {
33942
+ const { ocrDir } = params;
33943
+ const db = await ensureDatabase(ocrDir);
33944
+ let meta;
33945
+ let counts;
33946
+ try {
33947
+ const rawJsonString = readJsonFromSource(params);
33948
+ const label = params.source === "file" ? params.filePath : "stdin";
33949
+ meta = validateRoundMeta(parseRawJson(rawJsonString, label));
33950
+ counts = computeRoundCounts(meta);
33951
+ } catch (e) {
33952
+ throw new StateError(
33953
+ STATE_EXIT.SCHEMA_INVALID,
33954
+ e instanceof Error ? e.message : "invalid round metadata"
33955
+ );
33956
+ }
33957
+ const resolved = resolveSession(db, params.sessionId);
33958
+ const roundNumber = params.round ?? resolved.current_round;
33959
+ const roundMetaPath = join16(
33960
+ resolved.session_dir,
33961
+ "rounds",
33962
+ `round-${roundNumber}`,
33963
+ "round-meta.json"
33964
+ );
33965
+ const already = db.exec(
33966
+ `SELECT 1 FROM orchestration_events
33967
+ WHERE session_id = ? AND event_type = 'round_completed' AND round = ? LIMIT 1`,
33968
+ [resolved.id, roundNumber]
33969
+ );
33970
+ if ((already[0]?.values.length ?? 0) > 0) {
33971
+ return { sessionId: resolved.id, round: roundNumber, metaPath: roundMetaPath, schema_version: 1 };
33972
+ }
33973
+ if (resolved.current_phase !== "synthesis") {
33974
+ throw new StateError(
33975
+ STATE_EXIT.INVARIANT_UNMET,
33976
+ `Cannot complete round: workflow is at "${resolved.current_phase}", not "synthesis". Advance through the phases first.`
33977
+ );
33978
+ }
33979
+ if (params.requireFinal) {
33980
+ const finalPath = join16(resolved.session_dir, "rounds", `round-${roundNumber}`, "final.md");
33981
+ if (!existsSync14(finalPath)) {
33982
+ throw new StateError(
33983
+ STATE_EXIT.INVARIANT_UNMET,
33984
+ `Cannot complete round: --require-final set but ${finalPath} is missing.`
33985
+ );
33986
+ }
33987
+ }
33988
+ let metaPath;
33989
+ if (params.source === "stdin") {
33990
+ const roundDir = join16(resolved.session_dir, "rounds", `round-${roundNumber}`);
33991
+ mkdirSync5(roundDir, { recursive: true });
33992
+ metaPath = roundMetaPath;
33993
+ writeFileSync7(metaPath, JSON.stringify(meta, null, 2));
33994
+ }
33995
+ db.transaction(() => {
33996
+ insertEvent(db, {
33997
+ session_id: resolved.id,
33998
+ event_type: "round_completed",
33999
+ phase: "synthesis",
34000
+ phase_number: 7,
34001
+ round: roundNumber,
34002
+ metadata: JSON.stringify({
34003
+ verdict: meta.verdict,
34004
+ blocker_count: counts.blockerCount,
34005
+ should_fix_count: counts.shouldFixCount,
34006
+ suggestion_count: counts.suggestionCount,
34007
+ reviewer_count: counts.reviewerCount,
34008
+ total_finding_count: counts.totalFindingCount,
34009
+ source: "orchestrator"
34010
+ })
34011
+ });
34012
+ if (roundNumber >= resolved.current_round) {
34013
+ updateSession(db, resolved.id, { current_round: roundNumber });
34014
+ }
34015
+ validatePhaseTransition("review", resolved.current_phase, "complete", false);
34016
+ updateSession(db, resolved.id, { current_phase: "complete", phase_number: 8 });
34017
+ insertEvent(db, {
34018
+ session_id: resolved.id,
34019
+ event_type: "phase_transition",
34020
+ phase: "complete",
34021
+ phase_number: 8,
34022
+ round: roundNumber
34023
+ });
34024
+ });
34025
+ return { sessionId: resolved.id, round: roundNumber, metaPath, schema_version: 1 };
34026
+ }
34027
+ async function stateCompleteMap(params) {
34028
+ const { ocrDir } = params;
34029
+ const db = await ensureDatabase(ocrDir);
34030
+ let meta;
34031
+ let counts;
34032
+ try {
34033
+ const rawJsonString = readJsonFromSource(params);
34034
+ const label = params.source === "file" ? params.filePath : "stdin";
34035
+ meta = validateMapMeta(parseRawJson(rawJsonString, label));
34036
+ counts = computeMapCounts(meta);
34037
+ } catch (e) {
34038
+ throw new StateError(
34039
+ STATE_EXIT.SCHEMA_INVALID,
34040
+ e instanceof Error ? e.message : "invalid map metadata"
34041
+ );
34042
+ }
34043
+ const resolved = resolveSession(db, params.sessionId);
34044
+ const mapRunNumber = params.mapRun ?? resolved.current_map_run;
34045
+ const mapMetaPath = join16(
34046
+ resolved.session_dir,
34047
+ "map",
34048
+ "runs",
34049
+ `run-${mapRunNumber}`,
34050
+ "map-meta.json"
34051
+ );
34052
+ const already = db.exec(
34053
+ `SELECT 1 FROM orchestration_events
34054
+ WHERE session_id = ? AND event_type = 'map_completed' AND round = ? LIMIT 1`,
34055
+ [resolved.id, mapRunNumber]
34056
+ );
34057
+ if ((already[0]?.values.length ?? 0) > 0) {
34058
+ return { sessionId: resolved.id, mapRun: mapRunNumber, metaPath: mapMetaPath, schema_version: 1 };
34059
+ }
34060
+ if (resolved.current_phase !== "synthesis") {
34061
+ throw new StateError(
34062
+ STATE_EXIT.INVARIANT_UNMET,
34063
+ `Cannot complete map: workflow is at "${resolved.current_phase}", not "synthesis". Advance first.`
34064
+ );
34065
+ }
34066
+ let metaPath;
34067
+ if (params.source === "stdin") {
34068
+ const runDir = join16(resolved.session_dir, "map", "runs", `run-${mapRunNumber}`);
34069
+ mkdirSync5(runDir, { recursive: true });
34070
+ metaPath = mapMetaPath;
34071
+ writeFileSync7(metaPath, JSON.stringify(meta, null, 2));
34072
+ }
34073
+ db.transaction(() => {
34074
+ insertEvent(db, {
34075
+ session_id: resolved.id,
34076
+ event_type: "map_completed",
34077
+ phase: "synthesis",
34078
+ phase_number: 5,
34079
+ round: mapRunNumber,
34080
+ metadata: JSON.stringify({
34081
+ section_count: counts.sectionCount,
34082
+ file_count: counts.fileCount,
34083
+ source: "orchestrator"
34084
+ })
34085
+ });
34086
+ validatePhaseTransition("map", resolved.current_phase, "complete", false);
34087
+ updateSession(db, resolved.id, { current_phase: "complete", phase_number: 6 });
34088
+ insertEvent(db, {
34089
+ session_id: resolved.id,
34090
+ event_type: "phase_transition",
34091
+ phase: "complete",
34092
+ phase_number: 6,
34093
+ round: mapRunNumber
34094
+ });
34095
+ });
34096
+ return { sessionId: resolved.id, mapRun: mapRunNumber, metaPath, schema_version: 1 };
34097
+ }
34098
+ async function stateStatus(ocrDir, sessionId) {
34099
+ const db = await ensureDatabase(ocrDir);
34100
+ const resolved = resolveSession(db, sessionId);
34101
+ const view = db.exec(
34102
+ `SELECT completeness_state, has_terminal_artifact, marked_closed, dependents_settled
34103
+ FROM session_completeness WHERE session_id = ?`,
34104
+ [resolved.id]
34105
+ );
34106
+ const row = view[0]?.values[0];
34107
+ const completenessState = row?.[0] ?? null;
34108
+ const hasTerminalArtifact = row?.[1] === 1;
34109
+ let nextAction;
34110
+ let nextActionKind;
34111
+ switch (completenessState) {
34112
+ case "complete":
34113
+ nextAction = "none \u2014 session is complete";
34114
+ nextActionKind = "none";
34115
+ break;
34116
+ case "closed_without_artifact":
34117
+ nextAction = "re-open and finalize: this session was closed without a completed round/run";
34118
+ nextActionKind = "reopen";
34119
+ break;
34120
+ case "in_flight":
34121
+ nextAction = "wait for in-flight agent processes to finish";
34122
+ nextActionKind = "wait";
34123
+ break;
34124
+ default:
34125
+ if (hasTerminalArtifact) {
34126
+ nextAction = "run 'ocr state finish' to close the workflow";
34127
+ nextActionKind = "finish";
34128
+ } else if (resolved.current_phase === "synthesis") {
34129
+ nextAction = "pipe round metadata to 'ocr state complete-round --stdin'";
34130
+ nextActionKind = "complete_round";
34131
+ } else {
34132
+ nextAction = "advance through the phases, then 'ocr state complete-round'";
34133
+ nextActionKind = "advance";
34134
+ }
34135
+ }
34136
+ return {
34137
+ schema_version: 1,
34138
+ session_id: resolved.id,
34139
+ workflow_type: resolved.workflow_type,
34140
+ status: resolved.status,
34141
+ current_phase: resolved.current_phase,
34142
+ current_round: resolved.current_round,
34143
+ current_map_run: resolved.current_map_run,
34144
+ completeness_state: completenessState,
34145
+ has_terminal_artifact: hasTerminalArtifact,
34146
+ marked_closed: row?.[2] === 1,
34147
+ dependents_settled: row?.[3] === 1,
34148
+ next_action: nextAction,
34149
+ next_action_kind: nextActionKind
34150
+ };
34151
+ }
33172
34152
  async function stateSync(ocrDir) {
33173
34153
  const db = await ensureDatabase(ocrDir);
33174
- const dbPath = join15(ocrDir, "data", "ocr.db");
33175
- const sessionsRoot = join15(ocrDir, "sessions");
33176
- if (!existsSync13(sessionsRoot)) {
34154
+ const sessionsRoot = join16(ocrDir, "sessions");
34155
+ if (!existsSync14(sessionsRoot)) {
33177
34156
  return 0;
33178
34157
  }
33179
34158
  const entries = readdirSync6(sessionsRoot).filter((name) => {
33180
- const fullPath = join15(sessionsRoot, name);
33181
- return statSync2(fullPath).isDirectory();
34159
+ const fullPath = join16(sessionsRoot, name);
34160
+ return statSync3(fullPath).isDirectory();
33182
34161
  });
33183
34162
  let synced = 0;
33184
34163
  for (const dirName of entries) {
33185
- const dirPath = join15(sessionsRoot, dirName);
34164
+ const dirPath = join16(sessionsRoot, dirName);
33186
34165
  const existing = getSession(db, dirName);
33187
34166
  if (existing) {
33188
34167
  continue;
@@ -33190,28 +34169,41 @@ async function stateSync(ocrDir) {
33190
34169
  if (!hasArtifacts(dirPath)) {
33191
34170
  continue;
33192
34171
  }
33193
- const hasRoundsDir = existsSync13(join15(dirPath, "rounds"));
33194
- const hasMapDir = existsSync13(join15(dirPath, "map"));
34172
+ const hasRoundsDir = existsSync14(join16(dirPath, "rounds"));
34173
+ const hasMapDir = existsSync14(join16(dirPath, "map"));
33195
34174
  const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
33196
34175
  const branchMatch = dirName.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
33197
34176
  const branch = branchMatch?.[1] ?? dirName;
33198
34177
  let inferredPhase = "context";
34178
+ let inferredPhaseNumber = 1;
34179
+ let inferredRound = 1;
34180
+ let inferredMapRun = 1;
33199
34181
  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";
34182
+ const roundsDir = join16(dirPath, "rounds");
34183
+ if (existsSync14(roundsDir)) {
34184
+ const roundDirs = readdirSync6(roundsDir).filter((d) => /^round-\d+$/.test(d)).map((d) => parseInt(d.replace("round-", ""), 10)).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
34185
+ const latestRoundNum = roundDirs[roundDirs.length - 1];
34186
+ if (latestRoundNum !== void 0) {
34187
+ inferredRound = latestRoundNum;
34188
+ if (existsSync14(
34189
+ join16(roundsDir, `round-${latestRoundNum}`, "final.md")
34190
+ )) {
34191
+ inferredPhase = "complete";
34192
+ inferredPhaseNumber = 8;
34193
+ }
33206
34194
  }
33207
34195
  }
33208
34196
  } 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";
34197
+ const runsDir = join16(dirPath, "map", "runs");
34198
+ if (existsSync14(runsDir)) {
34199
+ const runDirs = readdirSync6(runsDir).filter((d) => /^run-\d+$/.test(d)).map((d) => parseInt(d.replace("run-", ""), 10)).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
34200
+ const latestRunNum = runDirs[runDirs.length - 1];
34201
+ if (latestRunNum !== void 0) {
34202
+ inferredMapRun = latestRunNum;
34203
+ if (existsSync14(join16(runsDir, `run-${latestRunNum}`, "map.md"))) {
34204
+ inferredPhase = "complete";
34205
+ inferredPhaseNumber = 6;
34206
+ }
33215
34207
  }
33216
34208
  }
33217
34209
  }
@@ -33220,35 +34212,36 @@ async function stateSync(ocrDir) {
33220
34212
  branch,
33221
34213
  workflow_type: workflowType,
33222
34214
  current_phase: inferredPhase,
33223
- phase_number: 1,
33224
- current_round: 1,
33225
- current_map_run: 1,
34215
+ phase_number: inferredPhaseNumber,
34216
+ current_round: inferredRound,
34217
+ current_map_run: inferredMapRun,
33226
34218
  session_dir: dirPath
33227
34219
  });
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
- });
34220
+ commitReasonClose(
34221
+ db,
34222
+ dirName,
34223
+ {
34224
+ event_type: "session_synced",
34225
+ phase: inferredPhase,
34226
+ phase_number: 1,
34227
+ metadata: JSON.stringify({ source: "filesystem_backfill" })
34228
+ },
34229
+ { status: "closed" }
34230
+ );
33236
34231
  synced++;
33237
34232
  }
33238
- if (synced > 0) {
33239
- saveDatabase(db, dbPath);
33240
- }
33241
34233
  return synced;
33242
34234
  }
33243
34235
 
33244
34236
  // src/commands/state.ts
33245
34237
  init_command_log();
33246
34238
  init_db();
34239
+ init_db();
33247
34240
  function readDashboardSpawnMarker(ocrDir) {
33248
- const path2 = join16(ocrDir, "data", "dashboard-active-spawn.json");
34241
+ const path2 = join17(ocrDir, "data", "dashboard-active-spawn.json");
33249
34242
  let raw;
33250
34243
  try {
33251
- raw = readFileSync12(path2, "utf-8");
34244
+ raw = readFileSync11(path2, "utf-8");
33252
34245
  } catch {
33253
34246
  return null;
33254
34247
  }
@@ -33280,143 +34273,37 @@ async function readStdin() {
33280
34273
  }
33281
34274
  return data;
33282
34275
  }
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
- }
34276
+ async function linkDashboardInvocation(ocrDir, sessionId, explicitUid, label) {
34277
+ const markerUid = readDashboardSpawnMarker(ocrDir)?.execution_uid;
34278
+ const dashboardUid = explicitUid ?? process.env["OCR_DASHBOARD_EXECUTION_UID"] ?? markerUid;
34279
+ if (!dashboardUid) {
34280
+ console.error(
34281
+ source_default.gray(
34282
+ `[state ${label}] no dashboard linkage available (flag, env var, and marker file all absent \u2014 CLI-only invocation)`
34283
+ )
34284
+ );
34285
+ return;
33394
34286
  }
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
34287
  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) {
34288
+ const db = await getDb(ocrDir);
34289
+ linkDashboardInvocationToWorkflow(db, dashboardUid, sessionId);
33408
34290
  console.error(
33409
- source_default.red(
33410
- `Error: ${error instanceof Error ? error.message : "Failed to close session"}`
34291
+ source_default.gray(
34292
+ `[state ${label}] linked workflow_id=${sessionId} \u2192 dashboard uid=${dashboardUid}`
34293
+ )
34294
+ );
34295
+ } catch (linkErr) {
34296
+ console.error(
34297
+ source_default.yellow(
34298
+ `Warning: failed to link dashboard command_execution to session: ${linkErr instanceof Error ? linkErr.message : String(linkErr)}`
33411
34299
  )
33412
34300
  );
33413
- process.exit(1);
33414
34301
  }
33415
- });
34302
+ }
33416
34303
  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
34304
  const targetDir = process.cwd();
33418
34305
  requireOcrSetup(targetDir);
33419
- const ocrDir = join16(targetDir, ".ocr");
34306
+ const ocrDir = join17(targetDir, ".ocr");
33420
34307
  try {
33421
34308
  const result = await stateShow(ocrDir, options.sessionId);
33422
34309
  if (!result) {
@@ -33485,7 +34372,7 @@ var showSubcommand = new Command("show").description("Show current session state
33485
34372
  var syncSubcommand = new Command("sync").description("Rebuild session state from filesystem artifacts").action(async () => {
33486
34373
  const targetDir = process.cwd();
33487
34374
  requireOcrSetup(targetDir);
33488
- const ocrDir = join16(targetDir, ".ocr");
34375
+ const ocrDir = join17(targetDir, ".ocr");
33489
34376
  try {
33490
34377
  const synced = await stateSync(ocrDir);
33491
34378
  console.log(`Synced ${synced} session${synced !== 1 ? "s" : ""} from filesystem.`);
@@ -33495,7 +34382,6 @@ var syncSubcommand = new Command("sync").description("Rebuild session state from
33495
34382
  if (totalCmds === 0) {
33496
34383
  const recovered = replayCommandLog(db, ocrDir);
33497
34384
  if (recovered > 0) {
33498
- saveDatabase(db, join16(ocrDir, "data", "ocr.db"));
33499
34385
  console.log(`Recovered ${recovered} command${recovered !== 1 ? "s" : ""} from backup log.`);
33500
34386
  }
33501
34387
  }
@@ -33508,123 +34394,216 @@ var syncSubcommand = new Command("sync").description("Rebuild session state from
33508
34394
  process.exit(1);
33509
34395
  }
33510
34396
  });
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(
34397
+ var reconcileSubcommand = new Command("reconcile").description(
34398
+ "Heal legacy/drifted session state by deriving truth from events + artifacts"
34399
+ ).option("--dry-run", "Print the repair plan without writing anything").option("--json", "Output the result as JSON").action(async (options) => {
34400
+ const targetDir = process.cwd();
34401
+ requireOcrSetup(targetDir);
34402
+ const ocrDir = join17(targetDir, ".ocr");
34403
+ try {
34404
+ const db = await ensureDatabase(ocrDir);
34405
+ const result = reconcileLegacyState(db, ocrDir, { dryRun: options.dryRun });
34406
+ if (options.json) {
34407
+ console.log(JSON.stringify(result, null, 2));
34408
+ return;
34409
+ }
34410
+ const repairs = result.actions.filter((a) => a.kind !== "ok");
34411
+ if (repairs.length === 0) {
34412
+ console.log(source_default.dim("Nothing to reconcile \u2014 all sessions consistent."));
34413
+ return;
34414
+ }
34415
+ console.log(
34416
+ result.dryRun ? source_default.bold(`Reconciliation plan (${repairs.length} change(s), dry run):`) : source_default.bold(`Reconciled ${repairs.length} session(s):`)
34417
+ );
34418
+ for (const a of repairs) {
34419
+ console.log(` ${source_default.cyan(a.kind)} ${a.sessionId}`);
34420
+ console.log(` ${source_default.dim(a.detail)}`);
34421
+ }
34422
+ } catch (error) {
34423
+ console.error(
34424
+ source_default.red(
34425
+ `Error: ${error instanceof Error ? error.message : "Failed to reconcile"}`
34426
+ )
34427
+ );
34428
+ process.exit(1);
34429
+ }
34430
+ });
34431
+ function exitFromStateError(error, fallback2) {
34432
+ if (error instanceof StateError) {
34433
+ console.error(source_default.red(`Error: ${error.message}`));
34434
+ process.exit(error.code);
34435
+ }
34436
+ if (isBusyError(error)) {
34437
+ console.error(
34438
+ source_default.red(
34439
+ `Error: database is busy (locked past retry budget): ${error instanceof Error ? error.message : String(error)}`
34440
+ )
34441
+ );
34442
+ process.exit(STATE_EXIT.BUSY);
34443
+ }
34444
+ console.error(
34445
+ source_default.red(`Error: ${error instanceof Error ? error.message : fallback2}`)
34446
+ );
34447
+ process.exit(1);
34448
+ }
34449
+ 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) => {
34450
+ if (v !== "review" && v !== "map") {
34451
+ throw new Error(`Invalid workflow type: "${v}". Must be "review" or "map".`);
34452
+ }
34453
+ return v;
34454
+ }).option("--session-dir <dir>", "Session directory path (auto-resolved if omitted)").option(
34455
+ "--dashboard-uid <uid>",
34456
+ "Dashboard command_executions uid to link this workflow to (takes precedence over OCR_DASHBOARD_EXECUTION_UID)"
34457
+ ).option("--json", "Output the result as JSON").action(
33512
34458
  async (options) => {
33513
34459
  const targetDir = process.cwd();
33514
34460
  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
- }
34461
+ const ocrDir = join17(targetDir, ".ocr");
34462
+ const sessionDir = options.sessionDir ?? join17(ocrDir, "sessions", options.sessionId);
34463
+ if (!existsSync15(sessionDir)) mkdirSync6(sessionDir, { recursive: true });
33524
34464
  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
- )
34465
+ const result = await stateBegin({
34466
+ sessionId: options.sessionId,
34467
+ branch: options.branch,
34468
+ workflowType: options.workflowType,
34469
+ sessionDir,
34470
+ ocrDir
34471
+ });
34472
+ await linkDashboardInvocation(ocrDir, result.session_id, options.dashboardUid, "begin");
34473
+ console.log(
34474
+ options.json ? JSON.stringify(result, null, 2) : `${result.session_id}: round ${result.round}, phase ${result.phase} (${result.completeness ?? "unknown"})`
33555
34475
  );
33556
- process.exit(1);
34476
+ } catch (error) {
34477
+ exitFromStateError(error, "Failed to begin session");
33557
34478
  }
33558
34479
  }
33559
34480
  );
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(
34481
+ 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
34482
  async (options) => {
33562
34483
  const targetDir = process.cwd();
33563
34484
  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);
34485
+ const ocrDir = join17(targetDir, ".ocr");
34486
+ try {
34487
+ const { id: sessionId } = await resolveActiveSession(ocrDir, options.sessionId);
34488
+ await stateAdvance({
34489
+ sessionId,
34490
+ phase: options.phase,
34491
+ round: options.currentRound,
34492
+ mapRun: options.currentMapRun,
34493
+ ocrDir
34494
+ });
34495
+ console.log(`${sessionId}: ${options.phase}`);
34496
+ } catch (error) {
34497
+ exitFromStateError(error, "Failed to advance");
33572
34498
  }
34499
+ }
34500
+ );
34501
+ 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(
34502
+ async (options) => {
34503
+ const targetDir = process.cwd();
34504
+ requireOcrSetup(targetDir);
34505
+ const ocrDir = join17(targetDir, ".ocr");
33573
34506
  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
- }
34507
+ const base = options.stdin ? { source: "stdin", data: await readStdin() } : options.file ? { source: "file", filePath: options.file } : (() => {
34508
+ throw new StateError(STATE_EXIT.USAGE, "Provide --stdin or --file with round metadata");
34509
+ })();
34510
+ const result = await stateCompleteRound({
34511
+ ...base,
34512
+ ocrDir,
34513
+ sessionId: options.sessionId,
34514
+ round: options.round,
34515
+ requireFinal: options.requireFinal
34516
+ });
34517
+ console.log(
34518
+ options.json ? JSON.stringify(result, null, 2) : `${result.sessionId}: round ${result.round} complete`
34519
+ );
33599
34520
  } catch (error) {
33600
- console.error(
33601
- source_default.red(
33602
- `Error: ${error instanceof Error ? error.message : "Failed to import map data"}`
33603
- )
34521
+ exitFromStateError(error, "Failed to complete round");
34522
+ }
34523
+ }
34524
+ );
34525
+ 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(
34526
+ async (options) => {
34527
+ const targetDir = process.cwd();
34528
+ requireOcrSetup(targetDir);
34529
+ const ocrDir = join17(targetDir, ".ocr");
34530
+ try {
34531
+ const base = options.stdin ? { source: "stdin", data: await readStdin() } : options.file ? { source: "file", filePath: options.file } : (() => {
34532
+ throw new StateError(STATE_EXIT.USAGE, "Provide --stdin or --file with map metadata");
34533
+ })();
34534
+ const result = await stateCompleteMap({
34535
+ ...base,
34536
+ ocrDir,
34537
+ sessionId: options.sessionId,
34538
+ mapRun: options.mapRun
34539
+ });
34540
+ console.log(
34541
+ options.json ? JSON.stringify(result, null, 2) : `${result.sessionId}: map run ${result.mapRun} complete`
33604
34542
  );
33605
- process.exit(1);
34543
+ } catch (error) {
34544
+ exitFromStateError(error, "Failed to complete map");
33606
34545
  }
33607
34546
  }
33608
34547
  );
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);
34548
+ var finishSubcommand = new Command("finish").description("Close a workflow (refuses unless the current round/run is complete)").option("--session-id <id>", "Session ID (auto-detects active if omitted)").option("--abort", "Abandon the session \u2014 records a distinct, non-success terminal").action(async (options) => {
34549
+ const targetDir = process.cwd();
34550
+ requireOcrSetup(targetDir);
34551
+ const ocrDir = join17(targetDir, ".ocr");
34552
+ try {
34553
+ const { id: sessionId } = await resolveActiveSession(ocrDir, options.sessionId);
34554
+ await stateClose({ sessionId, ocrDir, abort: options.abort });
34555
+ console.log(`${sessionId}: ${options.abort ? "aborted" : "finished"}`);
34556
+ } catch (error) {
34557
+ exitFromStateError(error, "Failed to finish");
34558
+ }
34559
+ });
34560
+ var statusSubcommand = new Command("status").description("Report whether a session is complete and, if not, what's missing").option("--session-id <id>", "Session ID (auto-detects active if omitted)").option("--json", "Output the result as JSON").action(async (options) => {
34561
+ const targetDir = process.cwd();
34562
+ requireOcrSetup(targetDir);
34563
+ const ocrDir = join17(targetDir, ".ocr");
34564
+ try {
34565
+ const result = await stateStatus(ocrDir, options.sessionId);
34566
+ if (options.json) {
34567
+ console.log(JSON.stringify(result, null, 2));
34568
+ } else {
34569
+ console.log(`${result.session_id}: ${result.completeness_state}`);
34570
+ console.log(source_default.dim(` next: ${result.next_action}`));
34571
+ }
34572
+ } catch (error) {
34573
+ exitFromStateError(error, "Failed to read status");
34574
+ }
34575
+ });
34576
+ var RETIRED_STATE_VERBS = {
34577
+ init: "begin",
34578
+ transition: "advance",
34579
+ "round-complete": "complete-round",
34580
+ "map-complete": "complete-map",
34581
+ close: "finish"
34582
+ };
34583
+ 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) => {
34584
+ const verb = operands[0] ?? "";
34585
+ const replacement = RETIRED_STATE_VERBS[verb];
34586
+ 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'.`;
34587
+ exitFromStateError(new StateError(STATE_EXIT.USAGE, msg), msg);
34588
+ });
33610
34589
 
33611
34590
  // src/commands/session.ts
33612
34591
  import { randomUUID as randomUUID3 } from "node:crypto";
33613
- import { join as join18 } from "node:path";
34592
+ import { join as join19 } from "node:path";
33614
34593
  init_db();
33615
34594
 
33616
34595
  // src/lib/runtime-config.ts
33617
- import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
33618
- import { join as join17 } from "node:path";
34596
+ import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
34597
+ import { join as join18 } from "node:path";
33619
34598
  var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
33620
34599
  function getAgentHeartbeatSeconds(ocrDir) {
33621
- const configPath = join17(ocrDir, "config.yaml");
33622
- if (!existsSync15(configPath)) {
34600
+ const configPath = join18(ocrDir, "config.yaml");
34601
+ if (!existsSync16(configPath)) {
33623
34602
  return DEFAULT_AGENT_HEARTBEAT_SECONDS;
33624
34603
  }
33625
34604
  let content;
33626
34605
  try {
33627
- content = readFileSync13(configPath, "utf-8");
34606
+ content = readFileSync12(configPath, "utf-8");
33628
34607
  } catch {
33629
34608
  return DEFAULT_AGENT_HEARTBEAT_SECONDS;
33630
34609
  }
@@ -33663,16 +34642,18 @@ function fail(message) {
33663
34642
  async function setup() {
33664
34643
  const targetDir = process.cwd();
33665
34644
  requireOcrSetup(targetDir);
33666
- const ocrDir = join18(targetDir, ".ocr");
33667
- const dbPath = join18(ocrDir, "data", "ocr.db");
33668
- return { ocrDir, dbPath };
34645
+ const ocrDir = join19(targetDir, ".ocr");
34646
+ return { ocrDir };
33669
34647
  }
33670
34648
  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
34649
  async (options) => {
33672
- const { ocrDir, dbPath } = await setup();
34650
+ const { ocrDir } = await setup();
33673
34651
  const db = await ensureDatabase(ocrDir);
33674
34652
  try {
33675
- const workflowId = options.workflow ?? (await resolveActiveSession(ocrDir)).id;
34653
+ const { id: workflowId } = await resolveActiveSession(
34654
+ ocrDir,
34655
+ options.workflow
34656
+ );
33676
34657
  const id = randomUUID3();
33677
34658
  const persona = options.persona ?? null;
33678
34659
  const instanceIndex = options.instance ?? null;
@@ -33691,7 +34672,6 @@ var startInstanceSubcommand = new Command("start-instance").description("Journal
33691
34672
  pid: options.pid ?? null,
33692
34673
  notes: options.note ?? null
33693
34674
  });
33694
- saveDatabase(db, dbPath);
33695
34675
  console.log(id);
33696
34676
  } catch (error) {
33697
34677
  fail(error instanceof Error ? error.message : "Failed to start agent session");
@@ -33699,18 +34679,17 @@ var startInstanceSubcommand = new Command("start-instance").description("Journal
33699
34679
  }
33700
34680
  );
33701
34681
  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();
34682
+ const { ocrDir } = await setup();
33703
34683
  const db = await ensureDatabase(ocrDir);
33704
34684
  try {
33705
34685
  setAgentSessionVendorId(db, agentId, vendorId);
33706
- saveDatabase(db, dbPath);
33707
34686
  console.log(`${agentId}: vendor_session_id=${vendorId}`);
33708
34687
  } catch (error) {
33709
34688
  fail(error instanceof Error ? error.message : "Failed to bind vendor session id");
33710
34689
  }
33711
34690
  });
33712
34691
  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();
34692
+ const { ocrDir } = await setup();
33714
34693
  const db = await ensureDatabase(ocrDir);
33715
34694
  try {
33716
34695
  const existing = getAgentSession(db, agentId);
@@ -33718,7 +34697,6 @@ var beatSubcommand = new Command("beat").description("Bump last_heartbeat_at on
33718
34697
  fail(`Agent session not found: ${agentId}`);
33719
34698
  }
33720
34699
  bumpAgentSessionHeartbeat(db, agentId);
33721
- saveDatabase(db, dbPath);
33722
34700
  console.log(`${agentId}: heartbeat`);
33723
34701
  } catch (error) {
33724
34702
  fail(error instanceof Error ? error.message : "Failed to bump heartbeat");
@@ -33729,7 +34707,7 @@ var endInstanceSubcommand = new Command("end-instance").description("Transition
33729
34707
  "Terminal status (done | crashed | cancelled). Default inferred from --exit-code (0 \u2192 done, non-zero \u2192 crashed)"
33730
34708
  ).option("--exit-code <code>", "Process exit code", parseInt).option("--note <text>", "Free-form note to append").action(
33731
34709
  async (agentId, options) => {
33732
- const { ocrDir, dbPath } = await setup();
34710
+ const { ocrDir } = await setup();
33733
34711
  const db = await ensureDatabase(ocrDir);
33734
34712
  try {
33735
34713
  const existing = getAgentSession(db, agentId);
@@ -33760,7 +34738,6 @@ var endInstanceSubcommand = new Command("end-instance").description("Transition
33760
34738
  exitCode: options.exitCode ?? null,
33761
34739
  note: options.note
33762
34740
  });
33763
- saveDatabase(db, dbPath);
33764
34741
  console.log(`${agentId}: ${status}`);
33765
34742
  } catch (error) {
33766
34743
  fail(error instanceof Error ? error.message : "Failed to end agent session");
@@ -33771,7 +34748,10 @@ var listSubcommand = new Command("list").description("List agent sessions for a
33771
34748
  const { ocrDir } = await setup();
33772
34749
  const db = await ensureDatabase(ocrDir);
33773
34750
  try {
33774
- const workflowId = options.workflow ?? (await resolveActiveSession(ocrDir)).id;
34751
+ const { id: workflowId } = await resolveActiveSession(
34752
+ ocrDir,
34753
+ options.workflow
34754
+ );
33775
34755
  const rows = listAgentSessionsForWorkflow(db, workflowId);
33776
34756
  if (options.json) {
33777
34757
  console.log(JSON.stringify(rows, null, 2));
@@ -33917,8 +34897,8 @@ var modelsCommand = new Command("models").description("Inspect models available
33917
34897
 
33918
34898
  // src/commands/team.ts
33919
34899
  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";
34900
+ import { existsSync as existsSync17, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
34901
+ import { join as join20 } from "node:path";
33922
34902
  async function readStdin2() {
33923
34903
  const chunks = [];
33924
34904
  for await (const chunk of process.stdin) {
@@ -33977,7 +34957,7 @@ var resolveSubcommand = new Command("resolve").description("Resolve and print th
33977
34957
  async (options) => {
33978
34958
  const targetDir = process.cwd();
33979
34959
  requireOcrSetup(targetDir);
33980
- const ocrDir = join19(targetDir, ".ocr");
34960
+ const ocrDir = join20(targetDir, ".ocr");
33981
34961
  try {
33982
34962
  const { team } = loadTeamConfig(ocrDir);
33983
34963
  let override;
@@ -34016,8 +34996,8 @@ var setSubcommand = new Command("set").description("Persist a new default_team c
34016
34996
  }
34017
34997
  const targetDir = process.cwd();
34018
34998
  requireOcrSetup(targetDir);
34019
- const ocrDir = join19(targetDir, ".ocr");
34020
- const configPath = join19(ocrDir, "config.yaml");
34999
+ const ocrDir = join20(targetDir, ".ocr");
35000
+ const configPath = join20(ocrDir, "config.yaml");
34021
35001
  try {
34022
35002
  const raw = await readStdin2();
34023
35003
  const team = parseSessionOverride(raw);
@@ -34027,17 +35007,17 @@ var setSubcommand = new Command("set").description("Persist a new default_team c
34027
35007
  list.push(inst);
34028
35008
  byPersona.set(inst.persona, list);
34029
35009
  }
34030
- const doc = existsSync16(configPath) ? (0, import_yaml2.parseDocument)(readFileSync14(configPath, "utf-8")) : new import_yaml2.Document({});
35010
+ const doc = existsSync17(configPath) ? (0, import_yaml2.parseDocument)(readFileSync13(configPath, "utf-8")) : new import_yaml2.Document({});
34031
35011
  applyDefaultTeamSurgically(doc, byPersona);
34032
35012
  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");
35013
+ writeFileSync8(configPath, yamlOutput, "utf-8");
35014
+ const reviewersDir = join20(ocrDir, "skills", "references", "reviewers");
35015
+ const metaPath = join20(ocrDir, "reviewers-meta.json");
34036
35016
  let metaWritten = false;
34037
35017
  try {
34038
35018
  const meta = generateReviewersMeta(reviewersDir, configPath);
34039
35019
  if (meta) {
34040
- writeFileSync9(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
35020
+ writeFileSync8(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
34041
35021
  metaWritten = true;
34042
35022
  }
34043
35023
  } catch (err) {
@@ -34128,7 +35108,7 @@ var teamCommand = new Command("team").description("Resolve and persist team comp
34128
35108
 
34129
35109
  // src/commands/review.ts
34130
35110
  import { spawn as spawn3 } from "node:child_process";
34131
- import { join as join20 } from "node:path";
35111
+ import { join as join21 } from "node:path";
34132
35112
  init_db();
34133
35113
 
34134
35114
  // src/lib/vendor-resume.ts
@@ -34167,7 +35147,7 @@ var reviewCommand = new Command("review").description("Run or resume an OCR revi
34167
35147
  }
34168
35148
  const targetDir = process.cwd();
34169
35149
  requireOcrSetup(targetDir);
34170
- const ocrDir = join20(targetDir, ".ocr");
35150
+ const ocrDir = join21(targetDir, ".ocr");
34171
35151
  const db = await ensureDatabase(ocrDir);
34172
35152
  const session = getSession(db, options.resume);
34173
35153
  if (!session) {
@@ -34209,16 +35189,16 @@ var reviewCommand = new Command("review").description("Run or resume an OCR revi
34209
35189
  });
34210
35190
 
34211
35191
  // src/commands/update.ts
34212
- import { existsSync as existsSync17 } from "node:fs";
34213
- import { join as join21 } from "node:path";
35192
+ import { existsSync as existsSync18 } from "node:fs";
35193
+ import { join as join22 } from "node:path";
34214
35194
  function detectConfiguredTools(targetDir) {
34215
35195
  return AI_TOOLS.filter((tool) => {
34216
35196
  if (tool.commandStrategy === "subdirectory") {
34217
- const ocrDir = join21(targetDir, tool.commandsDir, "ocr");
34218
- return existsSync17(ocrDir);
35197
+ const ocrDir = join22(targetDir, tool.commandsDir, "ocr");
35198
+ return existsSync18(ocrDir);
34219
35199
  } else {
34220
- const reviewCmd = join21(targetDir, tool.commandsDir, "ocr-review.md");
34221
- return existsSync17(reviewCmd);
35200
+ const reviewCmd = join22(targetDir, tool.commandsDir, "ocr-review.md");
35201
+ return existsSync18(reviewCmd);
34222
35202
  }
34223
35203
  });
34224
35204
  }
@@ -34292,7 +35272,7 @@ var updateCommand = new Command("update").description("Update OCR assets after p
34292
35272
  const result = installForTool(tool, targetDir);
34293
35273
  results.push(result);
34294
35274
  }
34295
- ensureGitignore(join21(targetDir, ".ocr"));
35275
+ ensureGitignore(join22(targetDir, ".ocr"));
34296
35276
  spinner.stop();
34297
35277
  const successful = results.filter((r) => r.success);
34298
35278
  const failed = results.filter((r) => !r.success);
@@ -34328,10 +35308,10 @@ var updateCommand = new Command("update").description("Update OCR assets after p
34328
35308
  if (updateInject) {
34329
35309
  if (options.dryRun) {
34330
35310
  console.log(source_default.dim(" Would update:"));
34331
- if (existsSync17(join21(targetDir, "AGENTS.md"))) {
35311
+ if (existsSync18(join22(targetDir, "AGENTS.md"))) {
34332
35312
  console.log(source_default.dim(" \u2022 AGENTS.md (OCR managed block)"));
34333
35313
  }
34334
- if (existsSync17(join21(targetDir, "CLAUDE.md"))) {
35314
+ if (existsSync18(join22(targetDir, "CLAUDE.md"))) {
34335
35315
  console.log(source_default.dim(" \u2022 CLAUDE.md (OCR managed block)"));
34336
35316
  }
34337
35317
  console.log();
@@ -34363,14 +35343,14 @@ var updateCommand = new Command("update").description("Update OCR assets after p
34363
35343
  });
34364
35344
 
34365
35345
  // src/commands/dashboard.ts
34366
- import { existsSync as existsSync18 } from "node:fs";
34367
- import { join as join22, dirname as dirname6 } from "node:path";
35346
+ import { existsSync as existsSync19 } from "node:fs";
35347
+ import { join as join23, dirname as dirname7 } from "node:path";
34368
35348
  import { fileURLToPath } from "node:url";
34369
35349
  init_db();
34370
35350
  var __filename = fileURLToPath(import.meta.url);
34371
- var __dirname = dirname6(__filename);
35351
+ var __dirname = dirname7(__filename);
34372
35352
  function resolveServerPath() {
34373
- return join22(__dirname, "dashboard", "server.js");
35353
+ return join23(__dirname, "dashboard", "server.js");
34374
35354
  }
34375
35355
  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
35356
  async (options) => {
@@ -34381,7 +35361,7 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
34381
35361
  console.error(source_default.red(`Error: Invalid port "${options.port}". Must be 1-65535.`));
34382
35362
  process.exit(1);
34383
35363
  }
34384
- const ocrDir = join22(targetDir, ".ocr");
35364
+ const ocrDir = join23(targetDir, ".ocr");
34385
35365
  try {
34386
35366
  await ensureDatabase(ocrDir);
34387
35367
  closeAllDatabases();
@@ -34395,7 +35375,7 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
34395
35375
  process.exit(1);
34396
35376
  }
34397
35377
  const serverPath = resolveServerPath();
34398
- if (!existsSync18(serverPath)) {
35378
+ if (!existsSync19(serverPath)) {
34399
35379
  console.error(source_default.red("Error: Dashboard server bundle not found."));
34400
35380
  console.error(
34401
35381
  source_default.dim(` Expected at: ${serverPath}`)
@@ -34429,8 +35409,9 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
34429
35409
  );
34430
35410
 
34431
35411
  // src/commands/doctor.ts
34432
- import { existsSync as existsSync19 } from "node:fs";
34433
- import { join as join23 } from "node:path";
35412
+ import { existsSync as existsSync20 } from "node:fs";
35413
+ import { join as join24 } from "node:path";
35414
+ init_db();
34434
35415
  var doctorCommand = new Command("doctor").description("Check OCR installation and verify all dependencies").action(() => {
34435
35416
  printHeader();
34436
35417
  const targetDir = process.cwd();
@@ -34444,10 +35425,10 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
34444
35425
  console.log(source_default.bold(" OCR Installation"));
34445
35426
  console.log();
34446
35427
  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);
35428
+ const configPath = join24(targetDir, ".ocr", "config.yaml");
35429
+ const dbPath = join24(targetDir, ".ocr", "data", "ocr.db");
35430
+ const hasConfig = existsSync20(configPath);
35431
+ const hasDb = existsSync20(dbPath);
34451
35432
  const ocrChecks = [
34452
35433
  { label: ".ocr/skills/", ok: ocrStatus.hasSkills },
34453
35434
  { label: ".ocr/sessions/", ok: ocrStatus.hasSessions },
@@ -34470,6 +35451,26 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
34470
35451
  hasIssues = true;
34471
35452
  }
34472
35453
  console.log();
35454
+ console.log(source_default.bold(" Storage Engine"));
35455
+ console.log();
35456
+ const engine = probeEngine();
35457
+ if (engine.ok) {
35458
+ console.log(
35459
+ ` ${source_default.green("\u2713")} better-sqlite3 (SQLite ${engine.version}, WAL)`
35460
+ );
35461
+ } else {
35462
+ hasIssues = true;
35463
+ console.log(
35464
+ ` ${source_default.red("\u2717")} better-sqlite3 failed to load`
35465
+ );
35466
+ console.log(` ${source_default.dim(engine.error)}`);
35467
+ console.log(
35468
+ ` ${source_default.dim(
35469
+ "Reinstall the CLI; if it persists your platform may need build tools (python3 + a C++ toolchain) or lacks a prebuilt binary."
35470
+ )}`
35471
+ );
35472
+ }
35473
+ console.log();
34473
35474
  printCapabilities(depResult);
34474
35475
  console.log();
34475
35476
  if (hasIssues) {
@@ -34521,8 +35522,8 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
34521
35522
  });
34522
35523
 
34523
35524
  // src/commands/reviewers.ts
34524
- import { writeFileSync as writeFileSync10, renameSync as renameSync3 } from "node:fs";
34525
- import { join as join24 } from "node:path";
35525
+ import { writeFileSync as writeFileSync9, renameSync as renameSync2 } from "node:fs";
35526
+ import { join as join25 } from "node:path";
34526
35527
  async function readStdin3() {
34527
35528
  const chunks = [];
34528
35529
  for await (const chunk of process.stdin) {
@@ -34585,20 +35586,20 @@ function validateReviewersMeta(data) {
34585
35586
  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
35587
  const targetDir = process.cwd();
34587
35588
  requireOcrSetup(targetDir);
34588
- const ocrDir = join24(targetDir, ".ocr");
35589
+ const ocrDir = join25(targetDir, ".ocr");
34589
35590
  if (!options.stdin) {
34590
35591
  try {
34591
- const reviewersDir = join24(ocrDir, "skills", "references", "reviewers");
34592
- const configPath = join24(ocrDir, "config.yaml");
35592
+ const reviewersDir = join25(ocrDir, "skills", "references", "reviewers");
35593
+ const configPath = join25(ocrDir, "config.yaml");
34593
35594
  const meta = generateReviewersMeta(reviewersDir, configPath);
34594
35595
  if (!meta || meta.reviewers.length === 0) {
34595
35596
  console.error(source_default.yellow("No reviewer files found in .ocr/skills/references/reviewers/"));
34596
35597
  process.exit(1);
34597
35598
  }
34598
- const metaPath = join24(ocrDir, "reviewers-meta.json");
35599
+ const metaPath = join25(ocrDir, "reviewers-meta.json");
34599
35600
  const tmpPath = metaPath + ".tmp";
34600
- writeFileSync10(tmpPath, JSON.stringify(meta, null, 2) + "\n");
34601
- renameSync3(tmpPath, metaPath);
35601
+ writeFileSync9(tmpPath, JSON.stringify(meta, null, 2) + "\n");
35602
+ renameSync2(tmpPath, metaPath);
34602
35603
  const tierCounts = meta.reviewers.reduce(
34603
35604
  (acc, r) => {
34604
35605
  acc[r.tier] = (acc[r.tier] ?? 0) + 1;
@@ -34625,10 +35626,10 @@ var syncSubcommand2 = new Command("sync").description("Sync reviewers-meta.json
34625
35626
  throw new Error("Invalid JSON on stdin");
34626
35627
  }
34627
35628
  const meta = validateReviewersMeta(parsed);
34628
- const metaPath = join24(ocrDir, "reviewers-meta.json");
35629
+ const metaPath = join25(ocrDir, "reviewers-meta.json");
34629
35630
  const tmpPath = metaPath + ".tmp";
34630
- writeFileSync10(tmpPath, JSON.stringify(meta, null, 2) + "\n");
34631
- renameSync3(tmpPath, metaPath);
35631
+ writeFileSync9(tmpPath, JSON.stringify(meta, null, 2) + "\n");
35632
+ renameSync2(tmpPath, metaPath);
34632
35633
  const tierCounts = meta.reviewers.reduce(
34633
35634
  (acc, r) => {
34634
35635
  acc[r.tier] = (acc[r.tier] ?? 0) + 1;
@@ -34653,25 +35654,25 @@ var reviewersCommand = new Command("reviewers").description("Manage OCR reviewer
34653
35654
 
34654
35655
  // src/lib/update-check.ts
34655
35656
  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";
35657
+ import { join as join26 } from "node:path";
35658
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, mkdirSync as mkdirSync7 } from "node:fs";
34658
35659
  var PACKAGE_NAME = "@open-code-review/cli";
34659
35660
  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");
35661
+ var CACHE_DIR2 = join26(homedir(), ".ocr");
35662
+ var CACHE_FILE = join26(CACHE_DIR2, "update-check.json");
34662
35663
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
34663
35664
  var FETCH_TIMEOUT_MS = 3e3;
34664
35665
  function readCache(cacheFile) {
34665
35666
  try {
34666
- return JSON.parse(readFileSync15(cacheFile, "utf-8"));
35667
+ return JSON.parse(readFileSync14(cacheFile, "utf-8"));
34667
35668
  } catch {
34668
35669
  return null;
34669
35670
  }
34670
35671
  }
34671
35672
  function writeCache(cacheFile, cache) {
34672
35673
  try {
34673
- mkdirSync7(join25(cacheFile, ".."), { recursive: true });
34674
- writeFileSync11(cacheFile, JSON.stringify(cache));
35674
+ mkdirSync7(join26(cacheFile, ".."), { recursive: true });
35675
+ writeFileSync10(cacheFile, JSON.stringify(cache));
34675
35676
  } catch {
34676
35677
  }
34677
35678
  }
@@ -34691,7 +35692,7 @@ async function checkForUpdate(currentVersion, options) {
34691
35692
  if (process.env.CI || process.env.OCR_NO_UPDATE_CHECK) {
34692
35693
  return null;
34693
35694
  }
34694
- const cacheFile = join25(options?.cacheDir ?? CACHE_DIR2, "update-check.json");
35695
+ const cacheFile = join26(options?.cacheDir ?? CACHE_DIR2, "update-check.json");
34695
35696
  try {
34696
35697
  const cache = readCache(cacheFile);
34697
35698
  if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {