@open-code-review/cli 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/dashboard/client/assets/{_basePickBy-B3ALyupE.js → _basePickBy-BBPb8BJA.js} +1 -1
  2. package/dist/dashboard/client/assets/{_baseUniq-b2RALAWc.js → _baseUniq-CFHdos6T.js} +1 -1
  3. package/dist/dashboard/client/assets/{arc-DcSVvhUd.js → arc-BKGGWA2F.js} +1 -1
  4. package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-BNUlmSCS.js → architectureDiagram-VXUJARFQ-B_ovNjX1.js} +1 -1
  5. package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-BmhiQVwa.js → blockDiagram-VD42YOAC-C2M-avVp.js} +1 -1
  6. package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-jyJ3WOv5.js → c4Diagram-YG6GDRKO-BtOBpAzH.js} +1 -1
  7. package/dist/dashboard/client/assets/channel-rgw7C1e7.js +1 -0
  8. package/dist/dashboard/client/assets/{chunk-4BX2VUAB-x1dQU_s3.js → chunk-4BX2VUAB-Cz2EbHPl.js} +1 -1
  9. package/dist/dashboard/client/assets/{chunk-55IACEB6-CwbsE2XQ.js → chunk-55IACEB6-C8xpXw9G.js} +1 -1
  10. package/dist/dashboard/client/assets/{chunk-B4BG7PRW-BaE7c-ti.js → chunk-B4BG7PRW-BSRfOovX.js} +1 -1
  11. package/dist/dashboard/client/assets/{chunk-DI55MBZ5-Bw5PUaMK.js → chunk-DI55MBZ5-CEUbYQWn.js} +1 -1
  12. package/dist/dashboard/client/assets/{chunk-FMBD7UC4-B7cF6P3s.js → chunk-FMBD7UC4-5xWP6GRj.js} +1 -1
  13. package/dist/dashboard/client/assets/{chunk-QN33PNHL-OY4evNHd.js → chunk-QN33PNHL-DfNCVcy8.js} +1 -1
  14. package/dist/dashboard/client/assets/{chunk-QZHKN3VN-BpjQwIWz.js → chunk-QZHKN3VN--OdToKKu.js} +1 -1
  15. package/dist/dashboard/client/assets/{chunk-TZMSLE5B-D8b_Oq9B.js → chunk-TZMSLE5B-B_0K0Qso.js} +1 -1
  16. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-DTGi7d9X.js +1 -0
  17. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-DTGi7d9X.js +1 -0
  18. package/dist/dashboard/client/assets/clone-Cz7hswqi.js +1 -0
  19. package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-C-sfP8PN.js → cose-bilkent-S5V4N54A-Cc_Dmnxz.js} +1 -1
  20. package/dist/dashboard/client/assets/{dagre-6UL2VRFP-Cqfo0NRg.js → dagre-6UL2VRFP-DaAfvUXU.js} +1 -1
  21. package/dist/dashboard/client/assets/{diagram-PSM6KHXK-BR3ppxqI.js → diagram-PSM6KHXK-7idwN0rC.js} +1 -1
  22. package/dist/dashboard/client/assets/{diagram-QEK2KX5R-Dvcx6x3R.js → diagram-QEK2KX5R-D9j9H13n.js} +1 -1
  23. package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DoyBLnVN.js → diagram-S2PKOQOG-SMF5SB0K.js} +1 -1
  24. package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-hy77l1cL.js → erDiagram-Q2GNP2WA-EVJ4Qa2F.js} +1 -1
  25. package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-Bz0B1rKM.js → flowDiagram-NV44I4VS-tZ7SFE77.js} +1 -1
  26. package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-CLgrZPoC.js → ganttDiagram-JELNMOA3-DFSqguY7.js} +1 -1
  27. package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js → gitGraphDiagram-V2S2FVAM-CqHdP3HE.js} +1 -1
  28. package/dist/dashboard/client/assets/{graph-DDBMM_t2.js → graph-C0XnkNkk.js} +1 -1
  29. package/dist/dashboard/client/assets/{index-Cr9yEo_B.js → index-C3NEq704.js} +133 -138
  30. package/dist/dashboard/client/assets/index-CzxeSSaQ.css +1 -0
  31. package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-Bhn1FmAk.js → infoDiagram-HS3SLOUP-DlXZo9U2.js} +1 -1
  32. package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CzGbjX1y.js → journeyDiagram-XKPGCS4Q-CgC8_7eN.js} +1 -1
  33. package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Da77-WYk.js → kanban-definition-3W4ZIXB7-BMAw_jNp.js} +1 -1
  34. package/dist/dashboard/client/assets/{layout-CVwSB-GS.js → layout-XjM3Q-ka.js} +1 -1
  35. package/dist/dashboard/client/assets/{linear-CTRAc5Jn.js → linear-CMUrrr1X.js} +1 -1
  36. package/dist/dashboard/client/assets/{mermaid-renderer-Bjo170ax.js → mermaid-renderer-D2jYNs7K.js} +4 -4
  37. package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-B55C2odl.js → mindmap-definition-VGOIOE7T-CL4hv-vg.js} +1 -1
  38. package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-5lrQLrSz.js → pieDiagram-ADFJNKIX-DTqv-1h1.js} +1 -1
  39. package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-Bg55gC30.js → quadrantDiagram-AYHSOK5B-BpFlSW9N.js} +1 -1
  40. package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-CyR4YFJY.js → requirementDiagram-UZGBJVZJ-BqYqqXL4.js} +1 -1
  41. package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BVWKr9_-.js → sankeyDiagram-TZEHDZUN-kEI9kntR.js} +1 -1
  42. package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-D0AJg_tE.js → sequenceDiagram-WL72ISMW-Cnu_1j-N.js} +1 -1
  43. package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-BuHpTgim.js → stateDiagram-FKZM4ZOC-BoC-rqoG.js} +1 -1
  44. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-COR3QD3v.js +1 -0
  45. package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-LDhpAmDd.js → timeline-definition-IT6M3QCI-CXMWuzDL.js} +1 -1
  46. package/dist/dashboard/client/assets/{treemap-GDKQZRPO-Dd4gjvUl.js → treemap-GDKQZRPO-o9ZFgpbJ.js} +1 -1
  47. package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-B9RDod39.js → xychartDiagram-PRI3JC2R-CfIuUpeA.js} +1 -1
  48. package/dist/dashboard/client/index.html +2 -2
  49. package/dist/dashboard/server.js +1031 -426
  50. package/dist/index.js +1252 -268
  51. package/dist/lib/db/index.js +485 -24
  52. package/dist/lib/runtime-config.js +29 -13
  53. package/dist/lib/state/index.js +2196 -0
  54. package/package.json +8 -2
  55. package/dist/dashboard/client/assets/channel-D3J8-GF_.js +0 -1
  56. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-tkFUL-1Y.js +0 -1
  57. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-tkFUL-1Y.js +0 -1
  58. package/dist/dashboard/client/assets/clone-CkY5ajLr.js +0 -1
  59. package/dist/dashboard/client/assets/index-Z1pPudAt.css +0 -1
  60. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +0 -1
@@ -1579,14 +1579,14 @@ var require_internal = __commonJS({
1579
1579
  }
1580
1580
  InternalCodec.prototype.encoder = InternalEncoder;
1581
1581
  InternalCodec.prototype.decoder = InternalDecoder;
1582
- var StringDecoder = __require("string_decoder").StringDecoder;
1583
- if (!StringDecoder.prototype.end)
1584
- StringDecoder.prototype.end = function() {
1582
+ var StringDecoder2 = __require("string_decoder").StringDecoder;
1583
+ if (!StringDecoder2.prototype.end)
1584
+ StringDecoder2.prototype.end = function() {
1585
1585
  };
1586
1586
  function InternalDecoder(options, codec) {
1587
- StringDecoder.call(this, codec.enc);
1587
+ StringDecoder2.call(this, codec.enc);
1588
1588
  }
1589
- InternalDecoder.prototype = StringDecoder.prototype;
1589
+ InternalDecoder.prototype = StringDecoder2.prototype;
1590
1590
  function InternalEncoder(options, codec) {
1591
1591
  this.enc = codec.enc;
1592
1592
  }
@@ -18410,10 +18410,10 @@ var require_view = __commonJS({
18410
18410
  var debug = require_src()("express:view");
18411
18411
  var path2 = __require("path");
18412
18412
  var fs6 = __require("fs");
18413
- var dirname14 = path2.dirname;
18414
- var basename4 = path2.basename;
18413
+ var dirname15 = path2.dirname;
18414
+ var basename5 = path2.basename;
18415
18415
  var extname = path2.extname;
18416
- var join19 = path2.join;
18416
+ var join20 = path2.join;
18417
18417
  var resolve3 = path2.resolve;
18418
18418
  module.exports = View;
18419
18419
  function View(name, options) {
@@ -18449,8 +18449,8 @@ var require_view = __commonJS({
18449
18449
  for (var i = 0; i < roots.length && !path3; i++) {
18450
18450
  var root = roots[i];
18451
18451
  var loc = resolve3(root, name);
18452
- var dir = dirname14(loc);
18453
- var file = basename4(loc);
18452
+ var dir = dirname15(loc);
18453
+ var file = basename5(loc);
18454
18454
  path3 = this.resolve(dir, file);
18455
18455
  }
18456
18456
  return path3;
@@ -18461,12 +18461,12 @@ var require_view = __commonJS({
18461
18461
  };
18462
18462
  View.prototype.resolve = function resolve4(dir, file) {
18463
18463
  var ext = this.ext;
18464
- var path3 = join19(dir, file);
18464
+ var path3 = join20(dir, file);
18465
18465
  var stat = tryStat(path3);
18466
18466
  if (stat && stat.isFile()) {
18467
18467
  return path3;
18468
18468
  }
18469
- path3 = join19(dir, basename4(file, ext), "index" + ext);
18469
+ path3 = join20(dir, basename5(file, ext), "index" + ext);
18470
18470
  stat = tryStat(path3);
18471
18471
  if (stat && stat.isFile()) {
18472
18472
  return path3;
@@ -18547,7 +18547,7 @@ var require_content_disposition = __commonJS({
18547
18547
  "use strict";
18548
18548
  module.exports = contentDisposition;
18549
18549
  module.exports.parse = parse;
18550
- var basename4 = __require("path").basename;
18550
+ var basename5 = __require("path").basename;
18551
18551
  var Buffer3 = require_safe_buffer().Buffer;
18552
18552
  var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g;
18553
18553
  var HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/;
@@ -18583,9 +18583,9 @@ var require_content_disposition = __commonJS({
18583
18583
  if (typeof fallback === "string" && NON_LATIN1_REGEXP.test(fallback)) {
18584
18584
  throw new TypeError("fallback must be ISO-8859-1 string");
18585
18585
  }
18586
- var name = basename4(filename);
18586
+ var name = basename5(filename);
18587
18587
  var isQuotedString = TEXT_REGEXP.test(name);
18588
- var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name) : basename4(fallback);
18588
+ var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name) : basename5(fallback);
18589
18589
  var hasFallback = typeof fallbackName === "string" && fallbackName !== name;
18590
18590
  if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name)) {
18591
18591
  params["filename*"] = name;
@@ -19099,7 +19099,7 @@ var require_send = __commonJS({
19099
19099
  var Stream = __require("stream");
19100
19100
  var util = __require("util");
19101
19101
  var extname = path2.extname;
19102
- var join19 = path2.join;
19102
+ var join20 = path2.join;
19103
19103
  var normalize = path2.normalize;
19104
19104
  var resolve3 = path2.resolve;
19105
19105
  var sep = path2.sep;
@@ -19318,7 +19318,7 @@ var require_send = __commonJS({
19318
19318
  return res;
19319
19319
  }
19320
19320
  parts = path3.split(sep);
19321
- path3 = normalize(join19(root, path3));
19321
+ path3 = normalize(join20(root, path3));
19322
19322
  } else {
19323
19323
  if (UP_PATH_REGEXP.test(path3)) {
19324
19324
  debug('malicious path "%s"', path3);
@@ -19453,7 +19453,7 @@ var require_send = __commonJS({
19453
19453
  if (err) return self.onStatError(err);
19454
19454
  return self.error(404);
19455
19455
  }
19456
- var p = join19(path3, self._index[i]);
19456
+ var p = join20(path3, self._index[i]);
19457
19457
  debug('stat "%s"', p);
19458
19458
  fs6.stat(p, function(err2, stat) {
19459
19459
  if (err2) return next(err2);
@@ -30483,10 +30483,152 @@ var init_open = __esm({
30483
30483
  // src/server/index.ts
30484
30484
  var import_express15 = __toESM(require_express2(), 1);
30485
30485
  import { createServer } from "node:http";
30486
- import { existsSync as existsSync15, readFileSync as readFileSync12, writeFileSync as writeFileSync5, unlinkSync as unlinkSync3, mkdirSync as mkdirSync6 } from "node:fs";
30487
- import { join as join18, dirname as dirname13, resolve as resolve2 } from "node:path";
30486
+ import { existsSync as existsSync17, readFileSync as readFileSync12, writeFileSync as writeFileSync6, unlinkSync as unlinkSync5, mkdirSync as mkdirSync7 } from "node:fs";
30487
+ import { join as join19, dirname as dirname14, resolve as resolve2 } from "node:path";
30488
+
30489
+ // ../shared/platform/src/index.ts
30490
+ import {
30491
+ execFile,
30492
+ execFileSync,
30493
+ spawn
30494
+ } from "node:child_process";
30495
+ import { promisify } from "node:util";
30496
+ var execFilePromise = promisify(execFile);
30497
+ var isWindows = process.platform === "win32";
30498
+ function execBinary(binary, args, opts) {
30499
+ return execFileSync(binary, args, {
30500
+ ...opts,
30501
+ shell: isWindows
30502
+ });
30503
+ }
30504
+ async function execBinaryAsync(binary, args, opts) {
30505
+ return execFilePromise(binary, args, {
30506
+ ...opts,
30507
+ shell: isWindows
30508
+ });
30509
+ }
30510
+ function spawnBinary(binary, args, opts) {
30511
+ return spawn(binary, args, {
30512
+ ...opts,
30513
+ ...isWindows && { shell: true, windowsHide: true }
30514
+ });
30515
+ }
30516
+ function isProcessAlive(pid) {
30517
+ try {
30518
+ process.kill(pid, 0);
30519
+ return true;
30520
+ } catch (err) {
30521
+ return !(err instanceof Error && "code" in err && err.code === "ESRCH");
30522
+ }
30523
+ }
30524
+ function walkDescendants(rootPid) {
30525
+ if (isWindows) return { pids: [], psAvailable: false };
30526
+ let out;
30527
+ try {
30528
+ out = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
30529
+ encoding: "utf-8",
30530
+ timeout: 5e3
30531
+ });
30532
+ } catch {
30533
+ return { pids: [], psAvailable: false };
30534
+ }
30535
+ const children = /* @__PURE__ */ new Map();
30536
+ for (const line of out.split("\n")) {
30537
+ const m = line.trim().match(/^(\d+)\s+(\d+)$/);
30538
+ if (!m) continue;
30539
+ const pid = Number(m[1]);
30540
+ const ppid = Number(m[2]);
30541
+ if (!children.has(ppid)) children.set(ppid, []);
30542
+ children.get(ppid).push(pid);
30543
+ }
30544
+ const acc = [];
30545
+ const queue = [rootPid];
30546
+ const seen = /* @__PURE__ */ new Set([rootPid]);
30547
+ while (queue.length) {
30548
+ const p = queue.shift();
30549
+ for (const c of children.get(p) ?? []) {
30550
+ if (seen.has(c)) continue;
30551
+ seen.add(c);
30552
+ acc.push(c);
30553
+ queue.push(c);
30554
+ }
30555
+ }
30556
+ return { pids: acc, psAvailable: true };
30557
+ }
30558
+ function descendantPids(rootPid) {
30559
+ return walkDescendants(rootPid).pids;
30560
+ }
30561
+ function reapTree(rootPid, graceMs = 5e3) {
30562
+ if (isWindows) {
30563
+ try {
30564
+ execFileSync("taskkill", ["/PID", String(rootPid), "/T", "/F"], {
30565
+ timeout: 5e3
30566
+ });
30567
+ } catch {
30568
+ }
30569
+ return { signaled: 1, psAvailable: false };
30570
+ }
30571
+ const { pids: descendants, psAvailable } = walkDescendants(rootPid);
30572
+ if (!psAvailable) {
30573
+ console.warn(
30574
+ `[reapTree] 'ps' unavailable \u2014 signalling only the root (PID ${rootPid}); escaped descendants cannot be enumerated on this system.`
30575
+ );
30576
+ }
30577
+ const term = [...descendants, rootPid];
30578
+ for (const pid of term) {
30579
+ try {
30580
+ process.kill(pid, "SIGTERM");
30581
+ } catch {
30582
+ }
30583
+ }
30584
+ setTimeout(() => {
30585
+ const kill = [...descendantPids(rootPid), rootPid];
30586
+ let stragglers = 0;
30587
+ for (const pid of kill) {
30588
+ if (!isProcessAlive(pid)) continue;
30589
+ stragglers++;
30590
+ try {
30591
+ process.kill(pid, "SIGKILL");
30592
+ } catch {
30593
+ }
30594
+ }
30595
+ if (stragglers > 0) {
30596
+ console.warn(
30597
+ `[reapTree] ${stragglers} process(es) under PID ${rootPid} survived SIGTERM after ${graceMs}ms; sending SIGKILL \u2014 investigate a leaked daemon.`
30598
+ );
30599
+ }
30600
+ }, graceMs).unref();
30601
+ return { signaled: term.length, psAvailable };
30602
+ }
30603
+ var BUILTIN_ICON_MAP = {
30604
+ architect: "blocks",
30605
+ fullstack: "layers",
30606
+ reliability: "activity",
30607
+ "staff-engineer": "compass",
30608
+ principal: "crown",
30609
+ frontend: "layout",
30610
+ backend: "server",
30611
+ infrastructure: "cloud",
30612
+ performance: "gauge",
30613
+ accessibility: "accessibility",
30614
+ data: "database",
30615
+ devops: "rocket",
30616
+ dx: "terminal",
30617
+ mobile: "smartphone",
30618
+ security: "shield-alert",
30619
+ quality: "sparkles",
30620
+ testing: "test-tubes",
30621
+ ai: "bot",
30622
+ "docs-writer": "file-text"
30623
+ };
30624
+ function defaultIconFor(id, tier) {
30625
+ return BUILTIN_ICON_MAP[id] ?? (tier === "persona" ? "brain" : "user");
30626
+ }
30627
+
30628
+ // src/server/index.ts
30488
30629
  import { fileURLToPath as fileURLToPath3 } from "node:url";
30489
30630
  import { randomBytes } from "node:crypto";
30631
+ import { execFileSync as execFileSync3 } from "node:child_process";
30490
30632
  import { Server as SocketIOServer } from "socket.io";
30491
30633
 
30492
30634
  // src/server/services/ocr-resolver.ts
@@ -30511,14 +30653,14 @@ function resolveOcrDir(startDir) {
30511
30653
 
30512
30654
  // ../cli/src/lib/db/index.ts
30513
30655
  import {
30514
- existsSync as existsSync4,
30656
+ existsSync as existsSync5,
30515
30657
  mkdirSync as mkdirSync2,
30516
- copyFileSync,
30517
- statSync,
30658
+ copyFileSync as copyFileSync2,
30659
+ statSync as statSync2,
30518
30660
  mkdtempSync,
30519
30661
  rmSync
30520
30662
  } from "node:fs";
30521
- import { dirname as dirname4, join as join4 } from "node:path";
30663
+ import { dirname as dirname5, join as join5 } from "node:path";
30522
30664
 
30523
30665
  // ../cli/src/lib/db/engine.ts
30524
30666
  import { createRequire } from "node:module";
@@ -31167,6 +31309,35 @@ var MIGRATIONS = [
31167
31309
  db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
31168
31310
  db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
31169
31311
  }
31312
+ },
31313
+ {
31314
+ version: 14,
31315
+ description: "Self-heal markdown_artifacts duplication: collapse NULL-round duplicate rows and add a NULL-safe unique index so the dedup bug cannot recur",
31316
+ // The table's `UNIQUE(session_id, artifact_type, round_number, file_path)`
31317
+ // never deduped session-level artifacts because SQLite treats NULL ≠ NULL,
31318
+ // and the writer used `INSERT OR REPLACE` — so every re-parse of a
31319
+ // NULL-round artifact (context.md, map.md, …) appended a duplicate (one
31320
+ // context.md reached 775 identical rows, ~177 MB). The writer is now an
31321
+ // explicit UPDATE-or-INSERT; this migration heals existing DBs and adds a
31322
+ // NULL-collapsing unique index as a DB-level backstop.
31323
+ //
31324
+ // Orphan-row sweep (FK-dangling children from the pre-FK-enforcement era)
31325
+ // is intentionally NOT done here — it needs `PRAGMA foreign_keys = OFF`,
31326
+ // which is a no-op inside the migration transaction. `ocr db doctor --fix`
31327
+ // performs it outside a transaction.
31328
+ run: (db) => {
31329
+ db.run(`
31330
+ DELETE FROM markdown_artifacts
31331
+ WHERE rowid NOT IN (
31332
+ SELECT MAX(rowid) FROM markdown_artifacts
31333
+ GROUP BY session_id, artifact_type, IFNULL(round_number, -1), file_path
31334
+ )
31335
+ `);
31336
+ db.run(`
31337
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_markdown_artifacts_logical
31338
+ ON markdown_artifacts(session_id, artifact_type, IFNULL(round_number, -1), file_path)
31339
+ `);
31340
+ }
31170
31341
  }
31171
31342
  ];
31172
31343
  function columnExists(db, table, column) {
@@ -31481,9 +31652,28 @@ function sqliteUtcMs(ts) {
31481
31652
  }
31482
31653
 
31483
31654
  // ../cli/src/lib/state/exit-codes.ts
31655
+ var STATE_EXIT = {
31656
+ OK: 0,
31657
+ USAGE: 2,
31658
+ AMBIGUOUS: 3,
31659
+ NOT_FOUND: 4,
31660
+ ILLEGAL_TRANSITION: 5,
31661
+ INVARIANT_UNMET: 6,
31662
+ SCHEMA_INVALID: 7,
31663
+ /** Database was locked past the bounded retry budget (SQLITE_BUSY). */
31664
+ BUSY: 8
31665
+ };
31666
+ var StateError = class extends Error {
31667
+ constructor(code, message) {
31668
+ super(message);
31669
+ this.code = code;
31670
+ this.name = "StateError";
31671
+ }
31672
+ };
31484
31673
  var CANCELLED_EXIT_CODE = -2;
31485
31674
  var ORPHAN_EXIT_CODE = -3;
31486
31675
  var CASCADE_CLOSE_EXIT_CODE = -4;
31676
+ var WATCHDOG_DEADLINE_EXIT_CODE = -5;
31487
31677
 
31488
31678
  // ../cli/src/lib/db/agent-sessions.ts
31489
31679
  var NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
@@ -31678,9 +31868,84 @@ function sweepStaleSessions(db, thresholdSeconds) {
31678
31868
  return { closedSessionIds: rows.map((r) => r.id) };
31679
31869
  }
31680
31870
 
31871
+ // ../cli/src/lib/db/maintenance.ts
31872
+ import {
31873
+ existsSync as existsSync3,
31874
+ readdirSync,
31875
+ statSync,
31876
+ unlinkSync,
31877
+ copyFileSync
31878
+ } from "node:fs";
31879
+ import { dirname as dirname3, join as join3, basename } from "node:path";
31880
+ var ONE_HOUR_MS = 60 * 60 * 1e3;
31881
+ function scanOrphanTempFiles(dataDir) {
31882
+ let entries;
31883
+ try {
31884
+ entries = readdirSync(dataDir);
31885
+ } catch {
31886
+ return [];
31887
+ }
31888
+ const out = [];
31889
+ for (const name of entries) {
31890
+ const m = name.match(/^ocr\.db\.(\d+)\.tmp$/);
31891
+ if (!m) continue;
31892
+ const pid = Number(m[1]);
31893
+ let ageMs = 0;
31894
+ try {
31895
+ ageMs = Date.now() - statSync(join3(dataDir, name)).mtimeMs;
31896
+ } catch {
31897
+ continue;
31898
+ }
31899
+ const alive = isProcessAlive(pid);
31900
+ out.push({
31901
+ name,
31902
+ pid,
31903
+ ageMs,
31904
+ // Reapable only when the writer PID is dead AND the file is old enough
31905
+ // that no live mid-write could plausibly own it.
31906
+ reapable: !alive && ageMs > ONE_HOUR_MS
31907
+ });
31908
+ }
31909
+ return out;
31910
+ }
31911
+ function reapOrphanDbFiles(dataDir) {
31912
+ const reaped = [];
31913
+ for (const f of scanOrphanTempFiles(dataDir)) {
31914
+ if (!f.reapable) continue;
31915
+ try {
31916
+ unlinkSync(join3(dataDir, f.name));
31917
+ reaped.push(f.name);
31918
+ } catch {
31919
+ }
31920
+ }
31921
+ return reaped;
31922
+ }
31923
+ var SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
31924
+ function reapStaleExecLogs(execLogsDir, maxAgeMs = SEVEN_DAYS_MS) {
31925
+ let entries;
31926
+ try {
31927
+ entries = readdirSync(execLogsDir);
31928
+ } catch {
31929
+ return [];
31930
+ }
31931
+ const cutoff = Date.now() - maxAgeMs;
31932
+ const reaped = [];
31933
+ for (const name of entries) {
31934
+ if (!name.endsWith(".log")) continue;
31935
+ const full = join3(execLogsDir, name);
31936
+ try {
31937
+ if (statSync(full).mtimeMs > cutoff) continue;
31938
+ unlinkSync(full);
31939
+ reaped.push(name);
31940
+ } catch {
31941
+ }
31942
+ }
31943
+ return reaped;
31944
+ }
31945
+
31681
31946
  // ../cli/src/lib/db/command-log.ts
31682
- import { appendFileSync, existsSync as existsSync3, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
31683
- import { dirname as dirname3, join as join3 } from "node:path";
31947
+ import { appendFileSync, existsSync as existsSync4, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
31948
+ import { dirname as dirname4, join as join4 } from "node:path";
31684
31949
  import { randomUUID } from "node:crypto";
31685
31950
  var CACHE_DIR = ".cache";
31686
31951
  var FILENAME = "command-history.jsonl";
@@ -31691,16 +31956,16 @@ function generateCommandUid() {
31691
31956
  return randomUUID();
31692
31957
  }
31693
31958
  function cacheDir(ocrDir) {
31694
- return join3(ocrDir, "data", CACHE_DIR);
31959
+ return join4(ocrDir, "data", CACHE_DIR);
31695
31960
  }
31696
31961
  function commandLogPath(ocrDir) {
31697
- return join3(cacheDir(ocrDir), FILENAME);
31962
+ return join4(cacheDir(ocrDir), FILENAME);
31698
31963
  }
31699
31964
  function appendCommandLog(ocrDir, entry) {
31700
31965
  try {
31701
31966
  const filePath = commandLogPath(ocrDir);
31702
- const dir = dirname3(filePath);
31703
- if (!existsSync3(dir)) mkdirSync(dir, { recursive: true });
31967
+ const dir = dirname4(filePath);
31968
+ if (!existsSync4(dir)) mkdirSync(dir, { recursive: true });
31704
31969
  const line = JSON.stringify(entry) + "\n";
31705
31970
  appendFileSync(filePath, line, { encoding: "utf-8" });
31706
31971
  if (approxLineCount >= 0) approxLineCount++;
@@ -31710,7 +31975,7 @@ function appendCommandLog(ocrDir, entry) {
31710
31975
  }
31711
31976
  function readCommandLog(ocrDir) {
31712
31977
  const filePath = commandLogPath(ocrDir);
31713
- if (!existsSync3(filePath)) return [];
31978
+ if (!existsSync4(filePath)) return [];
31714
31979
  const content = readFileSync(filePath, "utf-8");
31715
31980
  const entries = [];
31716
31981
  for (const line of content.split("\n")) {
@@ -31780,11 +32045,11 @@ var V2_SCHEMA_VERSION = 12;
31780
32045
  function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
31781
32046
  if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
31782
32047
  const bakPath = `${dbPath}.bak.v${fromVersion}`;
31783
- if (existsSync4(bakPath)) return bakPath;
32048
+ if (existsSync5(bakPath)) return bakPath;
31784
32049
  try {
31785
- if (!existsSync4(dbPath) || statSync(dbPath).size === 0) return null;
32050
+ if (!existsSync5(dbPath) || statSync2(dbPath).size === 0) return null;
31786
32051
  db.pragma("wal_checkpoint(TRUNCATE)");
31787
- copyFileSync(dbPath, bakPath);
32052
+ copyFileSync2(dbPath, bakPath);
31788
32053
  return bakPath;
31789
32054
  } catch {
31790
32055
  return null;
@@ -31818,8 +32083,8 @@ async function openDatabase(dbPath) {
31818
32083
  if (cached) {
31819
32084
  return cached;
31820
32085
  }
31821
- const dir = dirname4(dbPath);
31822
- if (!existsSync4(dir)) {
32086
+ const dir = dirname5(dbPath);
32087
+ if (!existsSync5(dir)) {
31823
32088
  mkdirSync2(dir, { recursive: true });
31824
32089
  }
31825
32090
  const db = openEngine(dbPath);
@@ -31827,11 +32092,11 @@ async function openDatabase(dbPath) {
31827
32092
  return db;
31828
32093
  }
31829
32094
  async function ensureDatabase(ocrDir) {
31830
- const dataDir = join4(ocrDir, "data");
31831
- if (!existsSync4(dataDir)) {
32095
+ const dataDir = join5(ocrDir, "data");
32096
+ if (!existsSync5(dataDir)) {
31832
32097
  mkdirSync2(dataDir, { recursive: true });
31833
32098
  }
31834
- const dbPath = join4(dataDir, "ocr.db");
32099
+ const dbPath = join5(dataDir, "ocr.db");
31835
32100
  const db = await openDatabase(dbPath);
31836
32101
  let before = 0;
31837
32102
  try {
@@ -31859,7 +32124,7 @@ async function ensureDatabase(ocrDir) {
31859
32124
  return db;
31860
32125
  }
31861
32126
  function walCheckpointTruncate(dbPath) {
31862
- if (!existsSync4(dbPath)) {
32127
+ if (!existsSync5(dbPath)) {
31863
32128
  return "skipped";
31864
32129
  }
31865
32130
  const cached = connections.get(dbPath);
@@ -31894,11 +32159,11 @@ function closeDatabase(dbPath) {
31894
32159
  }
31895
32160
 
31896
32161
  // src/server/db.ts
31897
- import { join as join5 } from "node:path";
32162
+ import { join as join6 } from "node:path";
31898
32163
  var cachedDb = null;
31899
32164
  var cachedDbPath = null;
31900
32165
  async function openDb(ocrDir) {
31901
- const dbPath = join5(ocrDir, "data", "ocr.db");
32166
+ const dbPath = join6(ocrDir, "data", "ocr.db");
31902
32167
  const db = await ensureDatabase(ocrDir);
31903
32168
  cachedDb = db;
31904
32169
  cachedDbPath = dbPath;
@@ -33080,37 +33345,9 @@ function createStatsRouter(db) {
33080
33345
  // src/server/routes/commands.ts
33081
33346
  var import_express8 = __toESM(require_express2(), 1);
33082
33347
 
33083
- // ../shared/platform/src/index.ts
33084
- import {
33085
- execFile,
33086
- execFileSync,
33087
- spawn
33088
- } from "node:child_process";
33089
- import { promisify } from "node:util";
33090
- var execFilePromise = promisify(execFile);
33091
- var isWindows = process.platform === "win32";
33092
- function execBinary(binary, args, opts) {
33093
- return execFileSync(binary, args, {
33094
- ...opts,
33095
- shell: isWindows
33096
- });
33097
- }
33098
- async function execBinaryAsync(binary, args, opts) {
33099
- return execFilePromise(binary, args, {
33100
- ...opts,
33101
- shell: isWindows
33102
- });
33103
- }
33104
- function spawnBinary(binary, args, opts) {
33105
- return spawn(binary, args, {
33106
- ...opts,
33107
- ...isWindows && { shell: true, windowsHide: true }
33108
- });
33109
- }
33110
-
33111
33348
  // src/server/socket/command-runner.ts
33112
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync4, existsSync as existsSync7 } from "node:fs";
33113
- import { dirname as dirname6, join as join10 } from "node:path";
33349
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, mkdirSync as mkdirSync5, existsSync as existsSync10 } from "node:fs";
33350
+ import { dirname as dirname7, join as join12 } from "node:path";
33114
33351
 
33115
33352
  // src/server/services/command-outcome.ts
33116
33353
  function deriveCommandOutcome(exitCode, completeness) {
@@ -33118,6 +33355,7 @@ function deriveCommandOutcome(exitCode, completeness) {
33118
33355
  if (exitCode === CANCELLED_EXIT_CODE || exitCode === CASCADE_CLOSE_EXIT_CODE) {
33119
33356
  return "cancelled";
33120
33357
  }
33358
+ if (exitCode === WATCHDOG_DEADLINE_EXIT_CODE) return "failed";
33121
33359
  if (exitCode !== 0) return "failed";
33122
33360
  if (completeness === null || completeness === "complete") return "success";
33123
33361
  return "incomplete";
@@ -33146,7 +33384,50 @@ function getWorkflowCompletenessForExecution(db, executionId) {
33146
33384
 
33147
33385
  // src/server/services/ai-cli/index.ts
33148
33386
  import { readFileSync as readFileSync3 } from "node:fs";
33149
- import { join as join8 } from "node:path";
33387
+ import { join as join9 } from "node:path";
33388
+
33389
+ // src/server/services/ai-cli/helpers.ts
33390
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, openSync, closeSync } from "node:fs";
33391
+ import { tmpdir } from "node:os";
33392
+ import { join as join7 } from "node:path";
33393
+ function buildFileStdio(stdin, logFile) {
33394
+ if (!logFile) {
33395
+ return { stdio: [stdin, "pipe", "pipe"], logFd: null, logPath: void 0 };
33396
+ }
33397
+ const logFd = openSync(logFile, "a");
33398
+ return { stdio: [stdin, logFd, logFd], logFd, logPath: logFile };
33399
+ }
33400
+ function closeFileStdio(logFd) {
33401
+ if (logFd === null) return;
33402
+ try {
33403
+ closeSync(logFd);
33404
+ } catch {
33405
+ }
33406
+ }
33407
+ function formatToolDetail(tool, input) {
33408
+ switch (tool) {
33409
+ case "Read":
33410
+ return `Reading ${input["file_path"] ?? "file"}`;
33411
+ case "Write":
33412
+ return `Writing ${input["file_path"] ?? "file"}`;
33413
+ case "Edit":
33414
+ return `Editing ${input["file_path"] ?? "file"}`;
33415
+ case "Grep":
33416
+ return `Searching for "${input["pattern"] ?? "..."}"`;
33417
+ case "Glob":
33418
+ return `Finding files matching ${input["pattern"] ?? "..."}`;
33419
+ case "Bash": {
33420
+ let cmd = input["command"] ?? "...";
33421
+ cmd = cmd.replace(/^cd\s+\S+\s*&&\s*/, "");
33422
+ return `Running: ${cmd.slice(0, 120)}`;
33423
+ }
33424
+ case "Agent":
33425
+ return `Spawning agent: ${input["description"] ?? "..."}`;
33426
+ default:
33427
+ return `Using ${tool}`;
33428
+ }
33429
+ }
33430
+ var TEMP_BASE = join7(tmpdir(), "ocr-ai-prompts");
33150
33431
 
33151
33432
  // src/server/socket/env.ts
33152
33433
  var ENV_ALLOWLIST = [
@@ -33219,6 +33500,8 @@ var ClaudeCodeAdapter = class {
33219
33500
  // Claude Code subagent definitions support per-subagent model frontmatter,
33220
33501
  // so per-task model overrides are honored at the host level.
33221
33502
  supportsPerTaskModel = true;
33503
+ // Claude Code can spawn reviewer sub-agents via its Task tool.
33504
+ supportsSubagentSpawn = true;
33222
33505
  buildResumeArgs(vendorSessionId) {
33223
33506
  return buildResumeArgs("claude", vendorSessionId);
33224
33507
  }
@@ -33259,15 +33542,25 @@ var ClaudeCodeAdapter = class {
33259
33542
  if (opts.model) {
33260
33543
  flags.push("--model", opts.model);
33261
33544
  }
33545
+ const { stdio, logFd, logPath } = buildFileStdio(
33546
+ "pipe",
33547
+ isWorkflow ? opts.logFile : void 0
33548
+ );
33262
33549
  const proc = spawnBinary("claude", flags, {
33263
33550
  cwd: opts.cwd,
33264
33551
  env: { ...cleanEnv(), ...opts.env ?? {} },
33265
33552
  detached: isWorkflow,
33266
- stdio: ["pipe", "pipe", "pipe"]
33553
+ stdio
33267
33554
  });
33555
+ closeFileStdio(logFd);
33556
+ if (isWorkflow) proc.unref();
33268
33557
  proc.stdin?.write(opts.prompt);
33269
33558
  proc.stdin?.end();
33270
- return { process: proc, detached: isWorkflow };
33559
+ return {
33560
+ process: proc,
33561
+ detached: isWorkflow,
33562
+ ...logPath ? { logPath } : {}
33563
+ };
33271
33564
  }
33272
33565
  async listModels() {
33273
33566
  try {
@@ -33426,6 +33719,13 @@ var ClaudeLineParser = class {
33426
33719
  const message = typeof parsed["message"] === "string" ? parsed["message"] : "Agent error";
33427
33720
  events.push({ type: "error", source: "agent", message });
33428
33721
  }
33722
+ if (type === "result") {
33723
+ events.push({
33724
+ type: "result",
33725
+ isError: parsed["is_error"] === true,
33726
+ subtype: typeof parsed["subtype"] === "string" ? parsed["subtype"] : void 0
33727
+ });
33728
+ }
33429
33729
  return events;
33430
33730
  }
33431
33731
  };
@@ -33461,6 +33761,9 @@ var OpenCodeAdapter = class {
33461
33761
  // until OpenCode adds per-task model support; OCR surfaces a warning to
33462
33762
  // the user when this happens.
33463
33763
  supportsPerTaskModel = false;
33764
+ // OpenCode exposes a sub-agent primitive (`--agent`), so reviewer sub-agents
33765
+ // can be spawned in-agent (uniform model — see supportsPerTaskModel above).
33766
+ supportsSubagentSpawn = true;
33464
33767
  buildResumeArgs(vendorSessionId) {
33465
33768
  return buildResumeArgs("opencode", vendorSessionId);
33466
33769
  }
@@ -33498,13 +33801,23 @@ var OpenCodeAdapter = class {
33498
33801
  if (opts.model) {
33499
33802
  args.push("--model", opts.model);
33500
33803
  }
33804
+ const { stdio, logFd, logPath } = buildFileStdio(
33805
+ "ignore",
33806
+ isWorkflow ? opts.logFile : void 0
33807
+ );
33501
33808
  const proc = spawnBinary("opencode", args, {
33502
33809
  cwd: opts.cwd,
33503
33810
  env: { ...cleanEnv(), ...opts.env ?? {} },
33504
33811
  detached: isWorkflow,
33505
- stdio: ["ignore", "pipe", "pipe"]
33812
+ stdio
33506
33813
  });
33507
- return { process: proc, detached: isWorkflow };
33814
+ closeFileStdio(logFd);
33815
+ if (isWorkflow) proc.unref();
33816
+ return {
33817
+ process: proc,
33818
+ detached: isWorkflow,
33819
+ ...logPath ? { logPath } : {}
33820
+ };
33508
33821
  }
33509
33822
  async listModels() {
33510
33823
  try {
@@ -33635,20 +33948,20 @@ function extractToolOutput(part) {
33635
33948
  // src/server/services/event-journal.ts
33636
33949
  import {
33637
33950
  createWriteStream,
33638
- existsSync as existsSync5,
33639
- mkdirSync as mkdirSync3,
33951
+ existsSync as existsSync6,
33952
+ mkdirSync as mkdirSync4,
33640
33953
  readFileSync as readFileSync2
33641
33954
  } from "node:fs";
33642
- import { join as join6 } from "node:path";
33955
+ import { join as join8 } from "node:path";
33643
33956
  function eventsDir(ocrDir) {
33644
- const dir = join6(ocrDir, "data", "events");
33645
- if (!existsSync5(dir)) {
33646
- mkdirSync3(dir, { recursive: true });
33957
+ const dir = join8(ocrDir, "data", "events");
33958
+ if (!existsSync6(dir)) {
33959
+ mkdirSync4(dir, { recursive: true });
33647
33960
  }
33648
33961
  return dir;
33649
33962
  }
33650
33963
  function eventJournalPath(ocrDir, executionId) {
33651
- return join6(eventsDir(ocrDir), `${executionId}.jsonl`);
33964
+ return join8(eventsDir(ocrDir), `${executionId}.jsonl`);
33652
33965
  }
33653
33966
  var EventJournalAppender = class {
33654
33967
  stream;
@@ -33685,7 +33998,7 @@ var EventJournalAppender = class {
33685
33998
  };
33686
33999
  function readEventJournal(ocrDir, executionId) {
33687
34000
  const path2 = eventJournalPath(ocrDir, executionId);
33688
- if (!existsSync5(path2)) return [];
34001
+ if (!existsSync6(path2)) return [];
33689
34002
  let raw;
33690
34003
  try {
33691
34004
  raw = readFileSync2(path2, "utf-8");
@@ -33705,38 +34018,10 @@ function readEventJournal(ocrDir, executionId) {
33705
34018
  return events;
33706
34019
  }
33707
34020
 
33708
- // src/server/services/ai-cli/helpers.ts
33709
- import { tmpdir } from "node:os";
33710
- import { join as join7 } from "node:path";
33711
- function formatToolDetail(tool, input) {
33712
- switch (tool) {
33713
- case "Read":
33714
- return `Reading ${input["file_path"] ?? "file"}`;
33715
- case "Write":
33716
- return `Writing ${input["file_path"] ?? "file"}`;
33717
- case "Edit":
33718
- return `Editing ${input["file_path"] ?? "file"}`;
33719
- case "Grep":
33720
- return `Searching for "${input["pattern"] ?? "..."}"`;
33721
- case "Glob":
33722
- return `Finding files matching ${input["pattern"] ?? "..."}`;
33723
- case "Bash": {
33724
- let cmd = input["command"] ?? "...";
33725
- cmd = cmd.replace(/^cd\s+\S+\s*&&\s*/, "");
33726
- return `Running: ${cmd.slice(0, 120)}`;
33727
- }
33728
- case "Agent":
33729
- return `Spawning agent: ${input["description"] ?? "..."}`;
33730
- default:
33731
- return `Using ${tool}`;
33732
- }
33733
- }
33734
- var TEMP_BASE = join7(tmpdir(), "ocr-ai-prompts");
33735
-
33736
34021
  // src/server/services/ai-cli/index.ts
33737
34022
  function readAiCliPreference(ocrDir) {
33738
34023
  try {
33739
- const configPath = join8(ocrDir, "config.yaml");
34024
+ const configPath = join9(ocrDir, "config.yaml");
33740
34025
  const content = readFileSync3(configPath, "utf-8");
33741
34026
  const match = content.match(/^\s*ai_cli:\s*(\S+)/m);
33742
34027
  const value = match?.[1] ?? "auto";
@@ -33845,31 +34130,245 @@ var AiCliService = class {
33845
34130
  }
33846
34131
  };
33847
34132
 
34133
+ // src/server/services/ai-cli/file-tailer.ts
34134
+ import { openSync as openSync2, readSync, closeSync as closeSync2, existsSync as existsSync7 } from "node:fs";
34135
+ import { StringDecoder } from "node:string_decoder";
34136
+ var DEFAULT_POLL_MS = 100;
34137
+ var READ_CHUNK_BYTES = 64 * 1024;
34138
+ var FileTailer = class {
34139
+ constructor(path2, onChunk, pollMs = DEFAULT_POLL_MS) {
34140
+ this.path = path2;
34141
+ this.onChunk = onChunk;
34142
+ this.pollMs = pollMs;
34143
+ }
34144
+ fd = null;
34145
+ offset = 0;
34146
+ decoder = new StringDecoder("utf8");
34147
+ timer = null;
34148
+ buf = Buffer.allocUnsafe(READ_CHUNK_BYTES);
34149
+ stopped = false;
34150
+ /** Begin polling for appended bytes. Idempotent. */
34151
+ start() {
34152
+ if (this.timer || this.stopped) return;
34153
+ this.timer = setInterval(() => this.poll(), this.pollMs);
34154
+ this.timer.unref?.();
34155
+ }
34156
+ ensureOpen() {
34157
+ if (this.fd !== null) return true;
34158
+ if (!existsSync7(this.path)) return false;
34159
+ try {
34160
+ this.fd = openSync2(this.path, "r");
34161
+ } catch {
34162
+ return false;
34163
+ }
34164
+ return true;
34165
+ }
34166
+ /** Read everything currently available from `offset` to EOF. */
34167
+ poll() {
34168
+ if (!this.ensureOpen()) return;
34169
+ let bytes;
34170
+ do {
34171
+ try {
34172
+ bytes = readSync(this.fd, this.buf, 0, this.buf.length, this.offset);
34173
+ } catch {
34174
+ return;
34175
+ }
34176
+ if (bytes > 0) {
34177
+ this.offset += bytes;
34178
+ const chunk = this.decoder.write(this.buf.subarray(0, bytes));
34179
+ if (chunk) this.onChunk(chunk);
34180
+ }
34181
+ } while (bytes === this.buf.length);
34182
+ }
34183
+ /**
34184
+ * Stop tailing: do one final drain to EOF, flush any partial multi-byte
34185
+ * remainder, and close the fd. Safe to call more than once. Synchronous so a
34186
+ * `close` handler can finalize the stream with no lost-tail race.
34187
+ */
34188
+ stop() {
34189
+ if (this.stopped) return;
34190
+ this.stopped = true;
34191
+ if (this.timer) {
34192
+ clearInterval(this.timer);
34193
+ this.timer = null;
34194
+ }
34195
+ this.poll();
34196
+ const tail = this.decoder.end();
34197
+ if (tail) this.onChunk(tail);
34198
+ if (this.fd !== null) {
34199
+ try {
34200
+ closeSync2(this.fd);
34201
+ } catch {
34202
+ }
34203
+ this.fd = null;
34204
+ }
34205
+ }
34206
+ };
34207
+
33848
34208
  // src/server/socket/cli-resolver.ts
33849
- import { existsSync as existsSync6 } from "node:fs";
33850
- import { dirname as dirname5, join as join9 } from "node:path";
34209
+ import { existsSync as existsSync8 } from "node:fs";
34210
+ import { dirname as dirname6, join as join10 } from "node:path";
33851
34211
  import { fileURLToPath } from "node:url";
33852
- var __dirname = dirname5(fileURLToPath(import.meta.url));
34212
+ var __dirname = dirname6(fileURLToPath(import.meta.url));
33853
34213
  function resolveLocalCli() {
33854
- const parentDir = join9(__dirname, "..");
33855
- const bundledCli = join9(parentDir, "index.js");
33856
- if (existsSync6(bundledCli) && existsSync6(join9(parentDir, "dashboard", "server.js"))) {
34214
+ const parentDir = join10(__dirname, "..");
34215
+ const bundledCli = join10(parentDir, "index.js");
34216
+ if (existsSync8(bundledCli) && existsSync8(join10(parentDir, "dashboard", "server.js"))) {
33857
34217
  return bundledCli;
33858
34218
  }
33859
34219
  let dir = __dirname;
33860
34220
  for (let i = 0; i < 8; i++) {
33861
- if (existsSync6(join9(dir, "nx.json"))) {
33862
- const candidate = join9(dir, "packages", "cli", "dist", "index.js");
33863
- if (existsSync6(candidate)) return candidate;
34221
+ if (existsSync8(join10(dir, "nx.json"))) {
34222
+ const candidate = join10(dir, "packages", "cli", "dist", "index.js");
34223
+ if (existsSync8(candidate)) return candidate;
33864
34224
  break;
33865
34225
  }
33866
- const parent = dirname5(dir);
34226
+ const parent = dirname6(dir);
33867
34227
  if (parent === dir) break;
33868
34228
  dir = parent;
33869
34229
  }
33870
34230
  return null;
33871
34231
  }
33872
34232
 
34233
+ // ../cli/src/lib/state/projection.ts
34234
+ var REASON_EVENT_TYPES = [
34235
+ "session_aborted",
34236
+ "session_auto_closed_stale",
34237
+ "session_synced",
34238
+ "session_legacy_import"
34239
+ ];
34240
+ var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
34241
+ "session_closed",
34242
+ ...REASON_EVENT_TYPES
34243
+ ]);
34244
+ function hasCompletionInvariant(db, session) {
34245
+ const eventType = session.workflow_type === "map" ? "map_completed" : "round_completed";
34246
+ const round = session.workflow_type === "map" ? session.current_map_run : session.current_round;
34247
+ const r = db.exec(
34248
+ `SELECT 1 FROM orchestration_events
34249
+ WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
34250
+ [session.id, eventType, round]
34251
+ );
34252
+ return (r[0]?.values.length ?? 0) > 0;
34253
+ }
34254
+
34255
+ // ../cli/src/lib/state/index.ts
34256
+ async function stateClose(params) {
34257
+ const { sessionId, ocrDir, abort } = params;
34258
+ const db = await ensureDatabase(ocrDir);
34259
+ const existing = getSession(db, sessionId);
34260
+ if (!existing) {
34261
+ throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
34262
+ }
34263
+ if (existing.status === "closed") {
34264
+ console.error(`[ocr] Session already closed: ${sessionId}`);
34265
+ return;
34266
+ }
34267
+ if (!abort && !hasCompletionInvariant(db, existing)) {
34268
+ 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`;
34269
+ throw new StateError(
34270
+ STATE_EXIT.INVARIANT_UNMET,
34271
+ `Cannot close session ${sessionId}: ${what}. Run 'ocr state complete-round' to finalize it, or pass --abort to record an abandoned session.`
34272
+ );
34273
+ }
34274
+ const note = "closed by parent workflow close";
34275
+ db.transaction(() => {
34276
+ if (abort) {
34277
+ insertEvent(db, {
34278
+ session_id: sessionId,
34279
+ event_type: "session_aborted",
34280
+ phase: existing.current_phase,
34281
+ phase_number: existing.phase_number,
34282
+ round: existing.current_round
34283
+ });
34284
+ }
34285
+ updateSession(db, sessionId, {
34286
+ status: "closed",
34287
+ current_phase: "complete"
34288
+ });
34289
+ if (!abort) {
34290
+ insertEvent(db, {
34291
+ session_id: sessionId,
34292
+ event_type: "session_closed",
34293
+ phase: "complete",
34294
+ phase_number: existing.phase_number,
34295
+ round: existing.current_round
34296
+ });
34297
+ }
34298
+ cascadeTerminateExecutions(db, sessionId, CASCADE_CLOSE_EXIT_CODE, note);
34299
+ });
34300
+ }
34301
+ async function reconcileWorkflowOnExit(ocrDir, sessionId, db) {
34302
+ db ??= await ensureDatabase(ocrDir);
34303
+ const existing = getSession(db, sessionId);
34304
+ if (!existing) return "not-found";
34305
+ if (existing.status === "closed") return "already-closed";
34306
+ if (!hasCompletionInvariant(db, existing)) return "incomplete";
34307
+ if (hasInFlightDependents(db, sessionId)) return "in-flight";
34308
+ await stateClose({ sessionId, ocrDir, abort: false });
34309
+ return "closed";
34310
+ }
34311
+ async function reconcileCompletedSessions(ocrDir) {
34312
+ const db = await ensureDatabase(ocrDir);
34313
+ const closed = [];
34314
+ for (const s of getAllSessions(db)) {
34315
+ if (s.status !== "active") continue;
34316
+ const outcome = await reconcileWorkflowOnExit(ocrDir, s.id, db);
34317
+ if (outcome === "closed") closed.push(s.id);
34318
+ }
34319
+ return closed;
34320
+ }
34321
+
34322
+ // ../cli/src/lib/runtime-config.ts
34323
+ import { existsSync as existsSync9, readFileSync as readFileSync4 } from "node:fs";
34324
+ import { join as join11 } from "node:path";
34325
+ var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
34326
+ var DEFAULT_WORKFLOW_HARD_DEADLINE_MINUTES = 60;
34327
+ function readRuntimePositiveInt(ocrDir, key, defaultValue) {
34328
+ const configPath = join11(ocrDir, "config.yaml");
34329
+ if (!existsSync9(configPath)) return defaultValue;
34330
+ let content;
34331
+ try {
34332
+ content = readFileSync4(configPath, "utf-8");
34333
+ } catch {
34334
+ return defaultValue;
34335
+ }
34336
+ const blockMatch = content.match(
34337
+ new RegExp(
34338
+ String.raw`^runtime:\s*\n(?:\s+[^\n]*\n)*?\s+${key}:\s*([^\s#\n]+)`,
34339
+ "m"
34340
+ )
34341
+ );
34342
+ const inlineMatch = content.match(
34343
+ new RegExp(String.raw`^runtime:\s*\{[^}]*\b${key}:\s*([^\s,}]+)`, "m")
34344
+ );
34345
+ const raw = blockMatch?.[1] ?? inlineMatch?.[1];
34346
+ if (!raw) return defaultValue;
34347
+ const parsed = Number(raw);
34348
+ if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
34349
+ process.stderr.write(
34350
+ `[ocr] runtime.${key} is not a positive integer (got "${raw}"); falling back to ${defaultValue}.
34351
+ `
34352
+ );
34353
+ return defaultValue;
34354
+ }
34355
+ return parsed;
34356
+ }
34357
+ function getAgentHeartbeatSeconds(ocrDir) {
34358
+ return readRuntimePositiveInt(
34359
+ ocrDir,
34360
+ "agent_heartbeat_seconds",
34361
+ DEFAULT_AGENT_HEARTBEAT_SECONDS
34362
+ );
34363
+ }
34364
+ function getWorkflowHardDeadlineMs(ocrDir) {
34365
+ return readRuntimePositiveInt(
34366
+ ocrDir,
34367
+ "workflow_hard_deadline_minutes",
34368
+ DEFAULT_WORKFLOW_HARD_DEADLINE_MINUTES
34369
+ ) * 60 * 1e3;
34370
+ }
34371
+
33873
34372
  // src/server/socket/command-runner.ts
33874
34373
  function shellSplit(str) {
33875
34374
  const tokens = [];
@@ -33903,7 +34402,7 @@ var ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
33903
34402
  ]);
33904
34403
  var AI_COMMANDS = /* @__PURE__ */ new Set(["map", "review", "translate-review-to-single-human", "address", "create-reviewer", "sync-reviewers"]);
33905
34404
  function escapeUserHeaders(value) {
33906
- return value.replace(/^([ \t]{0,3})(#+)/gm, "$1\\$2").replace(/^([ \t]{0,3})(#+)/gm, "$1\\$2").replace(/^([ \t]{0,3})(={3,}|-{3,})\s*$/gm, "$1\\$2").replace(/^([ \t]{0,3})(```+)/gm, "$1\\$2");
34405
+ return value.normalize("NFKC").replace(/[\u2028\u2029]/g, "\n").replace(/\p{Cf}/gu, "").replace(/^([ \t]{0,3})(#+)/gm, "$1\\$2").replace(/^([ \t]{0,3})(#+)/gm, "$1\\$2").replace(/^([ \t]{0,3})(={3,}|-{3,})\s*$/gm, "$1\\$2").replace(/^([ \t]{0,3})(```+)/gm, "$1\\$2");
33907
34406
  }
33908
34407
  function buildPrompt(opts) {
33909
34408
  const { baseCommand, subArgs, commandContent, executionUid, localCli } = opts;
@@ -34050,23 +34549,45 @@ function extractPerInstanceModels(subArgs) {
34050
34549
  return [...models];
34051
34550
  }
34052
34551
  var MAX_CONCURRENT = 3;
34552
+ var WATCHDOG_TICK_MS = 1e4;
34553
+ var POST_RESULT_GRACE_MS = 3e4;
34554
+ var HEARTBEAT_THROTTLE_MS = 5e3;
34555
+ function decideWatchdogTick(i) {
34556
+ if (i.resultSeenAt !== void 0 && i.nowMs - i.resultSeenAt > i.postResultGraceMs) {
34557
+ return {
34558
+ action: "finalize",
34559
+ reap: !i.exited,
34560
+ exitCode: i.resultIsError ? 1 : 0,
34561
+ reason: "result-grace"
34562
+ };
34563
+ }
34564
+ if (i.nowMs - i.startedAtMs > i.hardDeadlineMs) {
34565
+ return {
34566
+ action: "finalize",
34567
+ reap: !i.exited,
34568
+ exitCode: WATCHDOG_DEADLINE_EXIT_CODE,
34569
+ reason: "hard-deadline"
34570
+ };
34571
+ }
34572
+ return i.exited ? { action: "wait" } : { action: "beat" };
34573
+ }
34053
34574
  var activeCommands = /* @__PURE__ */ new Map();
34054
34575
  function spawnMarkerPath(ocrDir) {
34055
- return join10(ocrDir, "data", "dashboard-active-spawn.json");
34576
+ return join12(ocrDir, "data", "dashboard-active-spawn.json");
34056
34577
  }
34057
34578
  function writeSpawnMarker(ocrDir, executionUid, pid) {
34058
- const dataDir = join10(ocrDir, "data");
34059
- if (!existsSync7(dataDir)) mkdirSync4(dataDir, { recursive: true });
34579
+ const dataDir = join12(ocrDir, "data");
34580
+ if (!existsSync10(dataDir)) mkdirSync5(dataDir, { recursive: true });
34060
34581
  const payload = JSON.stringify({
34061
34582
  execution_uid: executionUid,
34062
34583
  pid,
34063
34584
  started_at: (/* @__PURE__ */ new Date()).toISOString()
34064
34585
  });
34065
- writeFileSync2(spawnMarkerPath(ocrDir), payload, { mode: 384 });
34586
+ writeFileSync3(spawnMarkerPath(ocrDir), payload, { mode: 384 });
34066
34587
  }
34067
34588
  function clearSpawnMarker(ocrDir) {
34068
34589
  try {
34069
- unlinkSync(spawnMarkerPath(ocrDir));
34590
+ unlinkSync3(spawnMarkerPath(ocrDir));
34070
34591
  } catch {
34071
34592
  }
34072
34593
  }
@@ -34193,25 +34714,14 @@ function registerCommandHandlers(io2, socket, db, ocrDir, aiCliService, sessionC
34193
34714
  if (!proc) return;
34194
34715
  const pid = proc.pid;
34195
34716
  if (entry.detached && pid) {
34196
- try {
34197
- process.kill(-pid, "SIGTERM");
34198
- } catch {
34199
- proc.kill("SIGTERM");
34200
- }
34717
+ reapTree(pid);
34201
34718
  } else {
34202
34719
  proc.kill("SIGTERM");
34720
+ const killTimer = setTimeout(() => {
34721
+ if (activeCommands.has(targetId)) proc.kill("SIGKILL");
34722
+ }, 5e3);
34723
+ proc.once("close", () => clearTimeout(killTimer));
34203
34724
  }
34204
- const killTimer = setTimeout(() => {
34205
- if (!activeCommands.has(targetId)) return;
34206
- if (entry.detached && pid) {
34207
- try {
34208
- process.kill(-pid, "SIGKILL");
34209
- } catch {
34210
- }
34211
- }
34212
- proc.kill("SIGKILL");
34213
- }, 5e3);
34214
- proc.once("close", () => clearTimeout(killTimer));
34215
34725
  } catch (err) {
34216
34726
  console.error("Error in command:cancel handler:", err);
34217
34727
  socket.emit("error", { message: "Internal error" });
@@ -34220,7 +34730,7 @@ function registerCommandHandlers(io2, socket, db, ocrDir, aiCliService, sessionC
34220
34730
  }
34221
34731
  function spawnCliCommand(io2, db, ocrDir, executionId, baseCommand, subArgs, entry) {
34222
34732
  const localCli = resolveLocalCli();
34223
- const repoRoot = dirname6(ocrDir);
34733
+ const repoRoot = dirname7(ocrDir);
34224
34734
  const proc = localCli ? spawnBinary("node", [localCli, baseCommand, ...subArgs], {
34225
34735
  cwd: repoRoot,
34226
34736
  env: cleanEnv()
@@ -34246,8 +34756,7 @@ function spawnCliCommand(io2, db, ocrDir, executionId, baseCommand, subArgs, ent
34246
34756
  io2.emit("command:output", { execution_id: executionId, content: chunk });
34247
34757
  });
34248
34758
  proc.on("close", (code) => {
34249
- const finalCode = code ?? (entry.cancelled ? -2 : -1);
34250
- finishExecution(io2, db, ocrDir, executionId, finalCode, entry.outputBuffer);
34759
+ finishExecution(io2, db, ocrDir, executionId, code ?? -1, entry.outputBuffer);
34251
34760
  });
34252
34761
  proc.on("error", (err) => {
34253
34762
  entry.outputBuffer += `Process error: ${err.message}`;
@@ -34270,10 +34779,10 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34270
34779
  io2.emit("command:output", { execution_id: executionId, content: warning });
34271
34780
  }
34272
34781
  }
34273
- const commandMdPath = join10(ocrDir, "commands", `${baseCommand}.md`);
34782
+ const commandMdPath = join12(ocrDir, "commands", `${baseCommand}.md`);
34274
34783
  let commandContent;
34275
34784
  try {
34276
- commandContent = readFileSync4(commandMdPath, "utf-8");
34785
+ commandContent = readFileSync5(commandMdPath, "utf-8");
34277
34786
  } catch {
34278
34787
  const content = `Error: Could not read command file at ${commandMdPath}
34279
34788
  `;
@@ -34317,7 +34826,20 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34317
34826
  console.error("Failed to resolve resume context:", err);
34318
34827
  }
34319
34828
  }
34320
- const repoRoot = dirname6(ocrDir);
34829
+ const repoRoot = dirname7(ocrDir);
34830
+ let logFile;
34831
+ if (entry.uid) {
34832
+ try {
34833
+ const logDir = join12(ocrDir, "data", "exec-logs");
34834
+ mkdirSync5(logDir, { recursive: true });
34835
+ logFile = join12(logDir, `${entry.uid}.log`);
34836
+ } catch (err) {
34837
+ console.error(
34838
+ "[command-runner] could not prepare exec-log dir \u2014 falling back to pipe stdio (degraded: close may be withheld by a leaked grandchild; watchdog deadlines still finalize):",
34839
+ err
34840
+ );
34841
+ }
34842
+ }
34321
34843
  const spawnOpts = {
34322
34844
  mode: "workflow",
34323
34845
  prompt,
@@ -34327,7 +34849,10 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34327
34849
  if (resumeSessionId) {
34328
34850
  spawnOpts.resumeSessionId = resumeSessionId;
34329
34851
  }
34330
- const { process: proc, detached } = adapter.spawn(spawnOpts);
34852
+ if (logFile) {
34853
+ spawnOpts.logFile = logFile;
34854
+ }
34855
+ const { process: proc, detached, logPath } = adapter.spawn(spawnOpts);
34331
34856
  entry.process = proc;
34332
34857
  entry.detached = detached;
34333
34858
  if (proc.pid) {
@@ -34363,6 +34888,61 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34363
34888
  }
34364
34889
  }, POLL_INTERVAL_MS);
34365
34890
  entry.linkPoll = linkPoll;
34891
+ const bumpHeartbeat = () => {
34892
+ if (entry.finalized) return;
34893
+ const now = Date.now();
34894
+ if (now - (entry.lastBeatWrite ?? 0) < HEARTBEAT_THROTTLE_MS) return;
34895
+ entry.lastBeatWrite = now;
34896
+ try {
34897
+ db.run(
34898
+ `UPDATE command_executions SET last_heartbeat_at = datetime('now') WHERE id = ? AND finished_at IS NULL`,
34899
+ [executionId]
34900
+ );
34901
+ } catch (err) {
34902
+ console.error("[command-runner] heartbeat bump failed:", err);
34903
+ }
34904
+ };
34905
+ const hardDeadlineMs = getWorkflowHardDeadlineMs(ocrDir);
34906
+ entry.watchdog = setInterval(() => {
34907
+ if (entry.finalized) return;
34908
+ const child = entry.process;
34909
+ const pid = child?.pid;
34910
+ if (!child || !pid) return;
34911
+ const exited = child.exitCode !== null || child.signalCode !== null;
34912
+ const decision = decideWatchdogTick({
34913
+ exited,
34914
+ resultSeenAt: entry.resultSeenAt,
34915
+ resultIsError: entry.resultIsError,
34916
+ startedAtMs: Date.parse(entry.startedAt),
34917
+ nowMs: Date.now(),
34918
+ postResultGraceMs: POST_RESULT_GRACE_MS,
34919
+ hardDeadlineMs
34920
+ });
34921
+ switch (decision.action) {
34922
+ case "beat":
34923
+ bumpHeartbeat();
34924
+ return;
34925
+ case "wait":
34926
+ return;
34927
+ case "finalize": {
34928
+ if (decision.reason === "hard-deadline") {
34929
+ const minutes = Math.round(hardDeadlineMs / 6e4);
34930
+ console.warn(`[watchdog] execution ${executionId}: exceeded hard deadline (${minutes}m) \u2014 finalizing${decision.reap ? " + reaping tree" : ""}`);
34931
+ const notice = `
34932
+ [watchdog] Reaped after exceeding the ${minutes}-minute hard deadline. Raise runtime.workflow_hard_deadline_minutes in .ocr/config.yaml for large reviewer fleets.
34933
+ `;
34934
+ entry.outputBuffer += notice;
34935
+ io2.emit("command:output", { execution_id: executionId, content: notice });
34936
+ } else {
34937
+ console.warn(`[watchdog] execution ${executionId}: result seen but no close after grace \u2014 finalizing${decision.reap ? " + reaping tree" : ""}`);
34938
+ }
34939
+ if (decision.reap) reapTree(pid);
34940
+ finishExecution(io2, db, ocrDir, executionId, decision.exitCode, entry.outputBuffer);
34941
+ return;
34942
+ }
34943
+ }
34944
+ }, WATCHDOG_TICK_MS);
34945
+ entry.watchdog.unref();
34366
34946
  io2.emit("command:output", {
34367
34947
  execution_id: executionId,
34368
34948
  content: `\u25B8 Starting OCR ${baseCommand} workflow...
@@ -34427,11 +35007,16 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34427
35007
  emitStreamEvent(evt);
34428
35008
  break;
34429
35009
  }
35010
+ case "result": {
35011
+ entry.resultSeenAt = Date.now();
35012
+ entry.resultIsError = evt.isError;
35013
+ emitStreamEvent(evt);
35014
+ break;
35015
+ }
34430
35016
  }
34431
35017
  }
34432
- proc.stdout?.setEncoding("utf-8");
34433
- proc.stderr?.setEncoding("utf-8");
34434
- proc.stdout?.on("data", (chunk) => {
35018
+ function onOutputChunk(chunk) {
35019
+ bumpHeartbeat();
34435
35020
  lineBuffer += chunk;
34436
35021
  const lines = lineBuffer.split("\n");
34437
35022
  lineBuffer = lines.pop() ?? "";
@@ -34446,17 +35031,30 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34446
35031
  handleEvent(evt);
34447
35032
  }
34448
35033
  }
34449
- });
35034
+ }
34450
35035
  let stderrBuffer = "";
34451
- proc.stderr?.on("data", (chunk) => {
34452
- stderrBuffer += chunk;
34453
- });
35036
+ if (logPath) {
35037
+ const tailer = new FileTailer(logPath, onOutputChunk);
35038
+ tailer.start();
35039
+ entry.tailer = tailer;
35040
+ } else {
35041
+ proc.stdout?.setEncoding("utf-8");
35042
+ proc.stderr?.setEncoding("utf-8");
35043
+ proc.stdout?.on("data", onOutputChunk);
35044
+ proc.stderr?.on("data", (chunk) => {
35045
+ stderrBuffer += chunk;
35046
+ });
35047
+ }
34454
35048
  proc.on("close", (code) => {
34455
35049
  if (entry.linkPoll) {
34456
35050
  clearInterval(entry.linkPoll);
34457
35051
  entry.linkPoll = void 0;
34458
35052
  }
34459
35053
  clearSpawnMarker(ocrDir);
35054
+ if (entry.tailer) {
35055
+ entry.tailer.stop();
35056
+ entry.tailer = void 0;
35057
+ }
34460
35058
  if (lineBuffer.trim()) {
34461
35059
  const events = parser.parseLine(lineBuffer);
34462
35060
  for (const evt of events) {
@@ -34480,8 +35078,7 @@ ${stderrBuffer}`;
34480
35078
  journal.close().catch((err) => {
34481
35079
  console.error("[event-journal] close failed:", err);
34482
35080
  });
34483
- const finalCode = code ?? (entry.cancelled ? -2 : -1);
34484
- finishExecution(io2, db, ocrDir, executionId, finalCode, entry.outputBuffer);
35081
+ finishExecution(io2, db, ocrDir, executionId, code ?? -1, entry.outputBuffer);
34485
35082
  });
34486
35083
  proc.on("error", (err) => {
34487
35084
  if (entry.linkPoll) {
@@ -34495,15 +35092,28 @@ ${stderrBuffer}`;
34495
35092
  finishExecution(io2, db, ocrDir, executionId, -1, entry.outputBuffer);
34496
35093
  });
34497
35094
  }
34498
- function finishExecution(io2, db, ocrDir, executionId, code, output) {
35095
+ function finishExecution(io2, db, ocrDir, executionId, rawCode, output) {
34499
35096
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
34500
35097
  const entry = activeCommands.get(executionId);
34501
- db.run(
35098
+ const code = entry?.cancelled ? CANCELLED_EXIT_CODE : rawCode;
35099
+ if (entry?.finalized) return;
35100
+ if (entry) {
35101
+ entry.finalized = true;
35102
+ if (entry.watchdog) {
35103
+ clearInterval(entry.watchdog);
35104
+ entry.watchdog = void 0;
35105
+ }
35106
+ if (entry.tailer) {
35107
+ entry.tailer.stop();
35108
+ entry.tailer = void 0;
35109
+ }
35110
+ }
35111
+ const res = db.prepare(
34502
35112
  `UPDATE command_executions
34503
- SET exit_code = ?, finished_at = ?, output = ?, pid = NULL
34504
- WHERE id = ?`,
34505
- [code, finishedAt, output, executionId]
34506
- );
35113
+ SET exit_code = ?, finished_at = ?, output = ?, pid = NULL
35114
+ WHERE id = ? AND finished_at IS NULL`
35115
+ ).run(code, finishedAt, output, executionId);
35116
+ if (Number(res.changes) === 0 && !entry) return;
34507
35117
  const completeness = getWorkflowCompletenessForExecution(db, executionId);
34508
35118
  const outcome = deriveCommandOutcome(code, completeness);
34509
35119
  const cancellationReason = deriveCancellationReason(code);
@@ -34518,7 +35128,7 @@ function finishExecution(io2, db, ocrDir, executionId, code, output) {
34518
35128
  started_at: entry.startedAt,
34519
35129
  finished_at: finishedAt,
34520
35130
  is_detached: entry.detached ? 1 : 0,
34521
- event: code === -2 ? "cancel" : "finish",
35131
+ event: code === CANCELLED_EXIT_CODE ? "cancel" : "finish",
34522
35132
  writer: "dashboard"
34523
35133
  });
34524
35134
  }
@@ -34530,6 +35140,27 @@ function finishExecution(io2, db, ocrDir, executionId, code, output) {
34530
35140
  cancellation_reason: cancellationReason
34531
35141
  });
34532
35142
  activeCommands.delete(executionId);
35143
+ const workflowRow = db.exec(
35144
+ "SELECT workflow_id FROM command_executions WHERE id = ?",
35145
+ [executionId]
35146
+ );
35147
+ const workflowId = workflowRow[0]?.values[0]?.[0];
35148
+ if (typeof workflowId === "string" && workflowId.length > 0) {
35149
+ void reconcileWorkflowOnExit(ocrDir, workflowId, db).then((outcome2) => {
35150
+ if (outcome2 === "closed") {
35151
+ console.log(`[command-runner] auto-finalized workflow ${workflowId}`);
35152
+ } else if (outcome2 === "incomplete" || outcome2 === "in-flight") {
35153
+ console.debug(
35154
+ `[command-runner] workflow ${workflowId} not finalized: ${outcome2}`
35155
+ );
35156
+ }
35157
+ }).catch((err) => {
35158
+ console.error(
35159
+ `[command-runner] reconcileWorkflowOnExit(${workflowId}) failed:`,
35160
+ err instanceof Error ? err.message : err
35161
+ );
35162
+ });
35163
+ }
34533
35164
  }
34534
35165
 
34535
35166
  // src/server/routes/commands.ts
@@ -34617,8 +35248,8 @@ function createCommandsRouter(db, ocrDir) {
34617
35248
 
34618
35249
  // src/server/routes/config.ts
34619
35250
  var import_express9 = __toESM(require_express2(), 1);
34620
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
34621
- import { join as join11, dirname as dirname7, basename } from "node:path";
35251
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
35252
+ import { join as join13, dirname as dirname8, basename as basename2 } from "node:path";
34622
35253
  var VALID_IDES = ["vscode", "cursor", "windsurf", "jetbrains", "sublime"];
34623
35254
  function detectIde() {
34624
35255
  const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? "";
@@ -34635,8 +35266,8 @@ function detectIde() {
34635
35266
  }
34636
35267
  function readIdeFromConfig(ocrDir) {
34637
35268
  try {
34638
- const configPath = join11(ocrDir, "config.yaml");
34639
- const content = readFileSync5(configPath, "utf-8");
35269
+ const configPath = join13(ocrDir, "config.yaml");
35270
+ const content = readFileSync6(configPath, "utf-8");
34640
35271
  const match = content.match(/^\s*ide:\s*(\S+)/m);
34641
35272
  return match?.[1] ?? "auto";
34642
35273
  } catch {
@@ -34663,8 +35294,8 @@ function detectGitBranch(cwd) {
34663
35294
  }
34664
35295
  function createConfigRouter(ocrDir, aiCliService) {
34665
35296
  const router = (0, import_express9.Router)();
34666
- const projectRoot = dirname7(ocrDir);
34667
- const workspaceName = basename(projectRoot);
35297
+ const projectRoot = dirname8(ocrDir);
35298
+ const workspaceName = basename2(projectRoot);
34668
35299
  const gitBranch = detectGitBranch(projectRoot);
34669
35300
  router.get("/", (_req, res) => {
34670
35301
  res.json({
@@ -34682,8 +35313,8 @@ function createConfigRouter(ocrDir, aiCliService) {
34682
35313
  return;
34683
35314
  }
34684
35315
  try {
34685
- const configPath = join11(ocrDir, "config.yaml");
34686
- let content = readFileSync5(configPath, "utf-8");
35316
+ const configPath = join13(ocrDir, "config.yaml");
35317
+ let content = readFileSync6(configPath, "utf-8");
34687
35318
  if (content.match(/^\s*ide:\s*\S+/m)) {
34688
35319
  content = content.replace(/^(\s*ide:\s*)\S+/m, `$1${ide}`);
34689
35320
  } else if (content.includes("dashboard:")) {
@@ -34695,7 +35326,7 @@ dashboard:
34695
35326
  ide: ${ide}
34696
35327
  `;
34697
35328
  }
34698
- writeFileSync3(configPath, content);
35329
+ writeFileSync4(configPath, content);
34699
35330
  res.json({ ide });
34700
35331
  } catch (err) {
34701
35332
  console.error("Failed to update config:", err);
@@ -34771,17 +35402,20 @@ function createChatRouter(db) {
34771
35402
 
34772
35403
  // src/server/routes/reviewers.ts
34773
35404
  var import_express11 = __toESM(require_express2(), 1);
34774
- import { readFileSync as readFileSync6, existsSync as existsSync8, watch } from "node:fs";
34775
- import { join as join12 } from "node:path";
35405
+ import { readFileSync as readFileSync7, existsSync as existsSync11, watch } from "node:fs";
35406
+ import { join as join14 } from "node:path";
34776
35407
  function readReviewersMeta(ocrDir) {
34777
- const metaPath = join12(ocrDir, "reviewers-meta.json");
34778
- if (!existsSync8(metaPath)) {
35408
+ const metaPath = join14(ocrDir, "reviewers-meta.json");
35409
+ if (!existsSync11(metaPath)) {
34779
35410
  return { reviewers: [], defaults: [] };
34780
35411
  }
34781
35412
  try {
34782
- const raw = readFileSync6(metaPath, "utf-8");
35413
+ const raw = readFileSync7(metaPath, "utf-8");
34783
35414
  const meta = JSON.parse(raw);
34784
- const reviewers = meta.reviewers ?? [];
35415
+ const reviewers = (meta.reviewers ?? []).map((r) => ({
35416
+ ...r,
35417
+ icon: r.icon || defaultIconFor(r.id, r.tier)
35418
+ }));
34785
35419
  const defaults = reviewers.filter((r) => r.is_default).map((r) => r.id);
34786
35420
  return { reviewers, defaults };
34787
35421
  } catch {
@@ -34800,13 +35434,13 @@ function createReviewersRouter(ocrDir) {
34800
35434
  res.status(400).json({ error: "Invalid reviewer ID" });
34801
35435
  return;
34802
35436
  }
34803
- const filePath = join12(ocrDir, "skills", "references", "reviewers", `${id}.md`);
34804
- if (!existsSync8(filePath)) {
35437
+ const filePath = join14(ocrDir, "skills", "references", "reviewers", `${id}.md`);
35438
+ if (!existsSync11(filePath)) {
34805
35439
  res.status(404).json({ error: "Reviewer not found", id });
34806
35440
  return;
34807
35441
  }
34808
35442
  try {
34809
- const content = readFileSync6(filePath, "utf-8");
35443
+ const content = readFileSync7(filePath, "utf-8");
34810
35444
  res.json({ id, content });
34811
35445
  } catch {
34812
35446
  res.status(500).json({ error: "Failed to read reviewer file", id });
@@ -34815,7 +35449,7 @@ function createReviewersRouter(ocrDir) {
34815
35449
  return router;
34816
35450
  }
34817
35451
  function watchReviewersMeta(ocrDir, io2) {
34818
- const metaPath = join12(ocrDir, "reviewers-meta.json");
35452
+ const metaPath = join14(ocrDir, "reviewers-meta.json");
34819
35453
  let watcher = null;
34820
35454
  let debounce;
34821
35455
  try {
@@ -34861,11 +35495,11 @@ function createAgentSessionsRouter(db, syncFromDisk = () => {
34861
35495
 
34862
35496
  // src/server/routes/handoff.ts
34863
35497
  var import_express13 = __toESM(require_express2(), 1);
34864
- import { dirname as dirname8 } from "node:path";
35498
+ import { dirname as dirname9 } from "node:path";
34865
35499
  function createHandoffRouter(sessionCapture, ocrDir, syncFromDisk = () => {
34866
35500
  }) {
34867
35501
  const router = (0, import_express13.Router)();
34868
- const projectDir = dirname8(ocrDir);
35502
+ const projectDir = dirname9(ocrDir);
34869
35503
  router.get("/:id/handoff", (req, res) => {
34870
35504
  const workflowId = req.params["id"];
34871
35505
  if (!workflowId) {
@@ -34895,14 +35529,14 @@ import { spawnSync } from "node:child_process";
34895
35529
 
34896
35530
  // ../cli/src/lib/team-config.ts
34897
35531
  var import_yaml = __toESM(require_dist(), 1);
34898
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
34899
- import { join as join13 } from "node:path";
35532
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
35533
+ import { join as join15 } from "node:path";
34900
35534
  function loadTeamConfig(ocrDir) {
34901
- const configPath = join13(ocrDir, "config.yaml");
34902
- if (!existsSync9(configPath)) {
35535
+ const configPath = join15(ocrDir, "config.yaml");
35536
+ if (!existsSync12(configPath)) {
34903
35537
  return { team: [], aliases: {}, defaultModel: null };
34904
35538
  }
34905
- const content = readFileSync7(configPath, "utf-8");
35539
+ const content = readFileSync8(configPath, "utf-8");
34906
35540
  return parseTeamConfigYaml(content);
34907
35541
  }
34908
35542
  function parseTeamConfigYaml(content) {
@@ -35502,8 +36136,8 @@ function buildDiagnostics(input) {
35502
36136
  }
35503
36137
 
35504
36138
  // src/server/services/filesystem-sync.ts
35505
- import { readdirSync, readFileSync as readFileSync8, statSync as statSync2, existsSync as existsSync10 } from "node:fs";
35506
- import { join as join14, basename as basename2, dirname as dirname9, relative } from "node:path";
36139
+ import { readdirSync as readdirSync2, readFileSync as readFileSync9, statSync as statSync3, existsSync as existsSync13 } from "node:fs";
36140
+ import { join as join16, basename as basename3, dirname as dirname10, relative } from "node:path";
35507
36141
  import { watch as watch2 } from "chokidar";
35508
36142
 
35509
36143
  // src/server/services/parsers/reviewer-parser.ts
@@ -35706,67 +36340,67 @@ var FilesystemSync = class {
35706
36340
  debounceTimers = /* @__PURE__ */ new Map();
35707
36341
  // ── 6.1: Full Scan ──
35708
36342
  async fullScan() {
35709
- if (!existsSync10(this.sessionsDir)) return;
35710
- const entries = readdirSync(this.sessionsDir, { withFileTypes: true });
36343
+ if (!existsSync13(this.sessionsDir)) return;
36344
+ const entries = readdirSync2(this.sessionsDir, { withFileTypes: true });
35711
36345
  for (const entry of entries) {
35712
36346
  if (!entry.isDirectory()) continue;
35713
36347
  const sessionId = entry.name;
35714
- const sessionDir = join14(this.sessionsDir, sessionId);
36348
+ const sessionDir = join16(this.sessionsDir, sessionId);
35715
36349
  this.syncSession(sessionId, sessionDir);
35716
36350
  }
35717
36351
  }
35718
36352
  syncSession(sessionId, sessionDir) {
35719
36353
  this.ensureSessionRow(sessionId, sessionDir);
35720
- const roundsDir = join14(sessionDir, "rounds");
35721
- if (existsSync10(roundsDir)) {
35722
- const rounds = readdirSync(roundsDir, { withFileTypes: true });
36354
+ const roundsDir = join16(sessionDir, "rounds");
36355
+ if (existsSync13(roundsDir)) {
36356
+ const rounds = readdirSync2(roundsDir, { withFileTypes: true });
35723
36357
  for (const roundEntry of rounds) {
35724
36358
  if (!roundEntry.isDirectory()) continue;
35725
36359
  const roundMatch = roundEntry.name.match(/^round-(\d+)$/);
35726
36360
  if (!roundMatch) continue;
35727
36361
  const roundNumber = parseInt(roundMatch[1] ?? "0", 10);
35728
- const roundDir = join14(roundsDir, roundEntry.name);
35729
- const reviewsDir = join14(roundDir, "reviews");
35730
- if (existsSync10(reviewsDir)) {
35731
- const reviewFiles = readdirSync(reviewsDir).filter((f) => f.endsWith(".md"));
36362
+ const roundDir = join16(roundsDir, roundEntry.name);
36363
+ const reviewsDir = join16(roundDir, "reviews");
36364
+ if (existsSync13(reviewsDir)) {
36365
+ const reviewFiles = readdirSync2(reviewsDir).filter((f) => f.endsWith(".md"));
35732
36366
  for (const reviewFile of reviewFiles) {
35733
- const filePath = join14(reviewsDir, reviewFile);
36367
+ const filePath = join16(reviewsDir, reviewFile);
35734
36368
  this.processReviewerOutput(sessionId, roundNumber, filePath, reviewFile);
35735
36369
  }
35736
36370
  }
35737
- const roundMetaPath = join14(roundDir, "round-meta.json");
35738
- if (existsSync10(roundMetaPath)) {
36371
+ const roundMetaPath = join16(roundDir, "round-meta.json");
36372
+ if (existsSync13(roundMetaPath)) {
35739
36373
  this.processRoundMeta(sessionId, roundNumber, roundMetaPath);
35740
36374
  }
35741
- const finalPath = join14(roundDir, "final.md");
35742
- if (existsSync10(finalPath)) {
36375
+ const finalPath = join16(roundDir, "final.md");
36376
+ if (existsSync13(finalPath)) {
35743
36377
  this.processFinalMd(sessionId, roundNumber, finalPath);
35744
36378
  }
35745
- const finalHumanPath = join14(roundDir, "final-human.md");
35746
- if (existsSync10(finalHumanPath)) {
36379
+ const finalHumanPath = join16(roundDir, "final-human.md");
36380
+ if (existsSync13(finalHumanPath)) {
35747
36381
  this.processGenericArtifact(sessionId, "final-human", finalHumanPath, roundNumber);
35748
36382
  }
35749
- const discoursePath = join14(roundDir, "discourse.md");
35750
- if (existsSync10(discoursePath)) {
36383
+ const discoursePath = join16(roundDir, "discourse.md");
36384
+ if (existsSync13(discoursePath)) {
35751
36385
  this.processGenericArtifact(sessionId, "discourse", discoursePath, roundNumber);
35752
36386
  }
35753
36387
  }
35754
36388
  }
35755
- const mapDir = join14(sessionDir, "map", "runs");
35756
- if (existsSync10(mapDir)) {
35757
- const runs = readdirSync(mapDir, { withFileTypes: true });
36389
+ const mapDir = join16(sessionDir, "map", "runs");
36390
+ if (existsSync13(mapDir)) {
36391
+ const runs = readdirSync2(mapDir, { withFileTypes: true });
35758
36392
  for (const runEntry of runs) {
35759
36393
  if (!runEntry.isDirectory()) continue;
35760
36394
  const runMatch = runEntry.name.match(/^run-(\d+)$/);
35761
36395
  if (!runMatch) continue;
35762
36396
  const runNumber = parseInt(runMatch[1] ?? "0", 10);
35763
- const runDir = join14(mapDir, runEntry.name);
35764
- const mapMetaPath = join14(runDir, "map-meta.json");
35765
- if (existsSync10(mapMetaPath)) {
36397
+ const runDir = join16(mapDir, runEntry.name);
36398
+ const mapMetaPath = join16(runDir, "map-meta.json");
36399
+ if (existsSync13(mapMetaPath)) {
35766
36400
  this.processMapMeta(sessionId, runNumber, mapMetaPath);
35767
36401
  }
35768
- const mapPath = join14(runDir, "map.md");
35769
- if (existsSync10(mapPath)) {
36402
+ const mapPath = join16(runDir, "map.md");
36403
+ if (existsSync13(mapPath)) {
35770
36404
  this.processMapMd(sessionId, runNumber, mapPath);
35771
36405
  }
35772
36406
  const mapArtifacts = [
@@ -35775,8 +36409,8 @@ var FilesystemSync = class {
35775
36409
  ["requirements-mapping.md", "requirements-mapping"]
35776
36410
  ];
35777
36411
  for (const [fileName, artifactType] of mapArtifacts) {
35778
- const filePath = join14(runDir, fileName);
35779
- if (existsSync10(filePath)) {
36412
+ const filePath = join16(runDir, fileName);
36413
+ if (existsSync13(filePath)) {
35780
36414
  this.processGenericArtifact(sessionId, artifactType, filePath, void 0, runNumber);
35781
36415
  }
35782
36416
  }
@@ -35787,8 +36421,8 @@ var FilesystemSync = class {
35787
36421
  ["discovered-standards.md", "discovered-standards"]
35788
36422
  ];
35789
36423
  for (const [fileName, artifactType] of sessionArtifacts) {
35790
- const filePath = join14(sessionDir, fileName);
35791
- if (existsSync10(filePath)) {
36424
+ const filePath = join16(sessionDir, fileName);
36425
+ if (existsSync13(filePath)) {
35792
36426
  this.processGenericArtifact(sessionId, artifactType, filePath);
35793
36427
  }
35794
36428
  }
@@ -35797,58 +36431,58 @@ var FilesystemSync = class {
35797
36431
  ensureSessionRow(sessionId, sessionDir) {
35798
36432
  const branchMatch = sessionId.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
35799
36433
  const branch = branchMatch?.[1] ?? "unknown";
35800
- const hasRoundsDir = existsSync10(join14(sessionDir, "rounds"));
35801
- const hasMapDir = existsSync10(join14(sessionDir, "map"));
36434
+ const hasRoundsDir = existsSync13(join16(sessionDir, "rounds"));
36435
+ const hasMapDir = existsSync13(join16(sessionDir, "map"));
35802
36436
  const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
35803
36437
  let currentRound = 1;
35804
36438
  if (hasRoundsDir) {
35805
- const roundDirs = readdirSync(join14(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
36439
+ const roundDirs = readdirSync2(join16(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
35806
36440
  currentRound = Math.max(1, roundDirs.length);
35807
36441
  }
35808
36442
  let currentMapRun = 1;
35809
- const mapRunsDir = join14(sessionDir, "map", "runs");
35810
- if (existsSync10(mapRunsDir)) {
35811
- const runDirs = readdirSync(mapRunsDir).filter((d) => d.match(/^run-\d+$/));
36443
+ const mapRunsDir = join16(sessionDir, "map", "runs");
36444
+ if (existsSync13(mapRunsDir)) {
36445
+ const runDirs = readdirSync2(mapRunsDir).filter((d) => d.match(/^run-\d+$/));
35812
36446
  currentMapRun = Math.max(1, runDirs.length);
35813
36447
  }
35814
36448
  let phase = "context";
35815
36449
  let phaseNumber = 1;
35816
36450
  let status = "closed";
35817
36451
  if (workflowType === "review" && hasRoundsDir) {
35818
- const roundDir = join14(sessionDir, "rounds", `round-${currentRound}`);
35819
- if (existsSync10(join14(roundDir, "final.md"))) {
36452
+ const roundDir = join16(sessionDir, "rounds", `round-${currentRound}`);
36453
+ if (existsSync13(join16(roundDir, "final.md"))) {
35820
36454
  phase = "complete";
35821
36455
  phaseNumber = 8;
35822
36456
  status = "closed";
35823
- } else if (existsSync10(join14(roundDir, "discourse.md"))) {
36457
+ } else if (existsSync13(join16(roundDir, "discourse.md"))) {
35824
36458
  phase = "synthesis";
35825
36459
  phaseNumber = 7;
35826
- } else if (existsSync10(join14(roundDir, "reviews")) && readdirSync(join14(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
36460
+ } else if (existsSync13(join16(roundDir, "reviews")) && readdirSync2(join16(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
35827
36461
  phase = "reviews";
35828
36462
  phaseNumber = 4;
35829
- } else if (existsSync10(join14(sessionDir, "context.md"))) {
36463
+ } else if (existsSync13(join16(sessionDir, "context.md"))) {
35830
36464
  phase = "analysis";
35831
36465
  phaseNumber = 3;
35832
- } else if (existsSync10(join14(sessionDir, "discovered-standards.md"))) {
36466
+ } else if (existsSync13(join16(sessionDir, "discovered-standards.md"))) {
35833
36467
  phase = "change-context";
35834
36468
  phaseNumber = 2;
35835
36469
  }
35836
36470
  } else if (workflowType === "map" && hasMapDir) {
35837
- const runDir = join14(mapRunsDir, `run-${currentMapRun}`);
35838
- if (existsSync10(join14(runDir, "map.md"))) {
36471
+ const runDir = join16(mapRunsDir, `run-${currentMapRun}`);
36472
+ if (existsSync13(join16(runDir, "map.md"))) {
35839
36473
  phase = "complete";
35840
36474
  phaseNumber = 6;
35841
36475
  status = "closed";
35842
- } else if (existsSync10(join14(runDir, "requirements-mapping.md"))) {
36476
+ } else if (existsSync13(join16(runDir, "requirements-mapping.md"))) {
35843
36477
  phase = "synthesis";
35844
36478
  phaseNumber = 5;
35845
- } else if (existsSync10(join14(runDir, "flow-analysis.md"))) {
36479
+ } else if (existsSync13(join16(runDir, "flow-analysis.md"))) {
35846
36480
  phase = "requirements-mapping";
35847
36481
  phaseNumber = 4;
35848
- } else if (existsSync10(join14(runDir, "topology.md"))) {
36482
+ } else if (existsSync13(join16(runDir, "topology.md"))) {
35849
36483
  phase = "flow-analysis";
35850
36484
  phaseNumber = 3;
35851
- } else if (existsSync10(join14(sessionDir, "discovered-standards.md"))) {
36485
+ } else if (existsSync13(join16(sessionDir, "discovered-standards.md"))) {
35852
36486
  phase = "topology";
35853
36487
  phaseNumber = 2;
35854
36488
  }
@@ -35901,9 +36535,9 @@ var FilesystemSync = class {
35901
36535
  /** Returns true if the directory contains at least one .md or .json file (recursively). */
35902
36536
  hasArtifacts(dir) {
35903
36537
  try {
35904
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
36538
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
35905
36539
  if (entry.isDirectory()) {
35906
- if (this.hasArtifacts(join14(dir, entry.name))) return true;
36540
+ if (this.hasArtifacts(join16(dir, entry.name))) return true;
35907
36541
  } else if (/\.(md|json)$/.test(entry.name)) {
35908
36542
  return true;
35909
36543
  }
@@ -35916,7 +36550,7 @@ var FilesystemSync = class {
35916
36550
  shouldSkip(filePath, existingParsedAt) {
35917
36551
  if (!existingParsedAt) return false;
35918
36552
  try {
35919
- const mtime = statSync2(filePath).mtime;
36553
+ const mtime = statSync3(filePath).mtime;
35920
36554
  const parsedAt = new Date(existingParsedAt);
35921
36555
  return mtime <= parsedAt;
35922
36556
  } catch {
@@ -35931,13 +36565,19 @@ var FilesystemSync = class {
35931
36565
  "SELECT id FROM markdown_artifacts WHERE session_id = ? AND artifact_type = ? AND round_number IS ? AND file_path = ?",
35932
36566
  [sessionId, artifactType, roundNumber ?? null, relPath]
35933
36567
  );
35934
- const isUpdate = existing !== null;
36568
+ if (existing !== null) {
36569
+ this.db.run(
36570
+ `UPDATE markdown_artifacts SET content = ?, parsed_at = datetime('now') WHERE id = ?`,
36571
+ [content, existing]
36572
+ );
36573
+ return "updated";
36574
+ }
35935
36575
  this.db.run(
35936
- `INSERT OR REPLACE INTO markdown_artifacts (session_id, artifact_type, round_number, file_path, content, parsed_at)
36576
+ `INSERT INTO markdown_artifacts (session_id, artifact_type, round_number, file_path, content, parsed_at)
35937
36577
  VALUES (?, ?, ?, ?, ?, datetime('now'))`,
35938
36578
  [sessionId, artifactType, roundNumber ?? null, relPath, content]
35939
36579
  );
35940
- return isUpdate ? "updated" : "created";
36580
+ return "created";
35941
36581
  }
35942
36582
  // ── 6.7: Socket.IO Emission ──
35943
36583
  emitArtifactEvent(action, event) {
@@ -35953,12 +36593,12 @@ var FilesystemSync = class {
35953
36593
  );
35954
36594
  if (existingRun && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
35955
36595
  if (existingRun?.["source"] === "orchestrator") {
35956
- const content2 = readFileSync8(filePath, "utf-8");
36596
+ const content2 = readFileSync9(filePath, "utf-8");
35957
36597
  const action2 = this.upsertMarkdownArtifact(sessionId, "map", filePath, content2, void 0);
35958
36598
  this.emitArtifactEvent(action2, { sessionId, artifactType: "map", filePath });
35959
36599
  return;
35960
36600
  }
35961
- const content = readFileSync8(filePath, "utf-8");
36601
+ const content = readFileSync9(filePath, "utf-8");
35962
36602
  const parsed = parseMapMd(content);
35963
36603
  this.db.run(
35964
36604
  `INSERT OR REPLACE INTO map_runs (session_id, run_number, file_count, map_md_path, parsed_at, source)
@@ -36084,7 +36724,7 @@ var FilesystemSync = class {
36084
36724
  const roundId = roundRow?.["id"];
36085
36725
  if (!roundId) return;
36086
36726
  if (roundRow?.["source"] === "orchestrator") {
36087
- const content2 = readFileSync8(filePath, "utf-8");
36727
+ const content2 = readFileSync9(filePath, "utf-8");
36088
36728
  const action2 = this.upsertMarkdownArtifact(sessionId, "reviewer-output", filePath, content2, roundNumber);
36089
36729
  this.emitArtifactEvent(action2, {
36090
36730
  sessionId,
@@ -36103,7 +36743,7 @@ var FilesystemSync = class {
36103
36743
  [roundId, reviewerType, instanceNumber]
36104
36744
  );
36105
36745
  if (existingOutput && this.shouldSkip(filePath, existingOutput["parsed_at"] ?? null)) return;
36106
- const content = readFileSync8(filePath, "utf-8");
36746
+ const content = readFileSync9(filePath, "utf-8");
36107
36747
  const parsed = parseReviewerOutput(content);
36108
36748
  this.db.run(
36109
36749
  `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
@@ -36191,7 +36831,7 @@ var FilesystemSync = class {
36191
36831
  if (existingRound?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
36192
36832
  let raw;
36193
36833
  try {
36194
- raw = JSON.parse(readFileSync8(filePath, "utf-8"));
36834
+ raw = JSON.parse(readFileSync9(filePath, "utf-8"));
36195
36835
  } catch {
36196
36836
  console.error(`[FilesystemSync] Failed to parse ${filePath}`);
36197
36837
  return;
@@ -36237,12 +36877,12 @@ var FilesystemSync = class {
36237
36877
  this.db.run("COMMIT");
36238
36878
  return;
36239
36879
  }
36240
- const roundDir = dirname9(filePath);
36880
+ const roundDir = dirname10(filePath);
36241
36881
  for (const reviewer of meta.reviewers) {
36242
36882
  const reviewerType = reviewer.type ?? "unknown";
36243
36883
  const instanceNumber = reviewer.instance ?? 1;
36244
36884
  const findings = reviewer.findings ?? [];
36245
- const reviewerMdPath = join14(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
36885
+ const reviewerMdPath = join16(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
36246
36886
  this.db.run(
36247
36887
  `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
36248
36888
  VALUES (?, ?, ?, ?, ?, ?)`,
@@ -36340,7 +36980,7 @@ var FilesystemSync = class {
36340
36980
  if (existingRun?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
36341
36981
  let raw;
36342
36982
  try {
36343
- raw = JSON.parse(readFileSync8(filePath, "utf-8"));
36983
+ raw = JSON.parse(readFileSync9(filePath, "utf-8"));
36344
36984
  } catch {
36345
36985
  console.error(`[FilesystemSync] Failed to parse ${filePath}`);
36346
36986
  return;
@@ -36468,7 +37108,7 @@ var FilesystemSync = class {
36468
37108
  );
36469
37109
  const isOrchestratorSource = existingRound?.["source"] === "orchestrator";
36470
37110
  if (!isOrchestratorSource && existingRound && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
36471
- const content = readFileSync8(filePath, "utf-8");
37111
+ const content = readFileSync9(filePath, "utf-8");
36472
37112
  if (isOrchestratorSource) {
36473
37113
  this.db.run(
36474
37114
  `UPDATE review_rounds SET final_md_path = ?, parsed_at = ?
@@ -36547,7 +37187,7 @@ var FilesystemSync = class {
36547
37187
  [sessionId, artifactType, relPath]
36548
37188
  );
36549
37189
  if (existing && this.shouldSkip(filePath, existing["parsed_at"] ?? null)) return;
36550
- const content = readFileSync8(filePath, "utf-8");
37190
+ const content = readFileSync9(filePath, "utf-8");
36551
37191
  const action = this.upsertMarkdownArtifact(sessionId, artifactType, filePath, content, roundNumber);
36552
37192
  this.emitArtifactEvent(action, {
36553
37193
  sessionId,
@@ -36566,7 +37206,7 @@ var FilesystemSync = class {
36566
37206
  ignored: [
36567
37207
  // Only ignore entries whose own name starts with a dot — the old regex
36568
37208
  // /(^|[/\\])\../ matched `.ocr` in the parent path, silencing ALL events.
36569
- (filePath) => basename2(filePath).startsWith("."),
37209
+ (filePath) => basename3(filePath).startsWith("."),
36570
37210
  /node_modules/,
36571
37211
  /\.db$/
36572
37212
  ]
@@ -36605,9 +37245,9 @@ var FilesystemSync = class {
36605
37245
  const parts = relFromSessions.split("/");
36606
37246
  const sessionId = parts[0];
36607
37247
  if (!sessionId) return;
36608
- const sessionDir = join14(this.sessionsDir, sessionId);
37248
+ const sessionDir = join16(this.sessionsDir, sessionId);
36609
37249
  this.ensureSessionRow(sessionId, sessionDir);
36610
- const fileName = basename2(filePath);
37250
+ const fileName = basename3(filePath);
36611
37251
  const reviewerMatch = relFromSessions.match(/rounds\/round-(\d+)\/reviews\/(.+\.md)$/);
36612
37252
  if (reviewerMatch) {
36613
37253
  const roundNumber = parseInt(reviewerMatch[1] ?? "0", 10);
@@ -36677,8 +37317,8 @@ var FilesystemSync = class {
36677
37317
  };
36678
37318
 
36679
37319
  // src/server/services/db-sync-watcher.ts
36680
- import { existsSync as existsSync11 } from "node:fs";
36681
- import { dirname as dirname10, basename as basename3 } from "node:path";
37320
+ import { existsSync as existsSync14 } from "node:fs";
37321
+ import { dirname as dirname11, basename as basename4 } from "node:path";
36682
37322
  import { watch as watch3 } from "chokidar";
36683
37323
  function col(row, key) {
36684
37324
  return row[key] ?? null;
@@ -36719,9 +37359,9 @@ var DbSyncWatcher = class {
36719
37359
  }
36720
37360
  /** Start watching the DB file (and its WAL sidecar) for external writes. */
36721
37361
  startWatching() {
36722
- if (!existsSync11(this.dbFilePath)) return;
36723
- const watchDir = dirname10(this.dbFilePath);
36724
- const dbFile = basename3(this.dbFilePath);
37362
+ if (!existsSync14(this.dbFilePath)) return;
37363
+ const watchDir = dirname11(this.dbFilePath);
37364
+ const dbFile = basename4(this.dbFilePath);
36725
37365
  const walFile = `${dbFile}-wal`;
36726
37366
  this.watcher = watch3(watchDir, {
36727
37367
  persistent: true,
@@ -36731,7 +37371,7 @@ var DbSyncWatcher = class {
36731
37371
  interval: 200
36732
37372
  });
36733
37373
  const onAnyEvent = (path2) => {
36734
- const name = basename3(path2);
37374
+ const name = basename4(path2);
36735
37375
  if (name === dbFile || name === walFile) this.debouncedSync();
36736
37376
  };
36737
37377
  this.watcher.on("change", onAnyEvent);
@@ -36982,20 +37622,20 @@ function commandFingerprint(row) {
36982
37622
  }
36983
37623
 
36984
37624
  // src/server/socket/chat-handler.ts
36985
- import { dirname as dirname11 } from "node:path";
37625
+ import { dirname as dirname12 } from "node:path";
36986
37626
 
36987
37627
  // src/server/services/chat-context.ts
36988
- import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync12 } from "node:fs";
36989
- import { join as join15 } from "node:path";
37628
+ import { readFileSync as readFileSync10, readdirSync as readdirSync3, existsSync as existsSync15 } from "node:fs";
37629
+ import { join as join17 } from "node:path";
36990
37630
  function buildChatContext(ocrDir, target) {
36991
- const sessionsDir = join15(ocrDir, "sessions");
37631
+ const sessionsDir = join17(ocrDir, "sessions");
36992
37632
  if (target.type === "map_run") {
36993
37633
  return buildMapRunContext(sessionsDir, target.sessionId, target.runNumber);
36994
37634
  }
36995
37635
  return buildReviewRoundContext(sessionsDir, target.sessionId, target.roundNumber);
36996
37636
  }
36997
37637
  function buildMapRunContext(sessionsDir, sessionId, runNumber) {
36998
- const mapPath = join15(
37638
+ const mapPath = join17(
36999
37639
  sessionsDir,
37000
37640
  sessionId,
37001
37641
  "map",
@@ -37009,8 +37649,8 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
37009
37649
  "",
37010
37650
  `Below is the Code Review Map that organizes the changeset into reviewable sections:`
37011
37651
  ];
37012
- if (existsSync12(mapPath)) {
37013
- const content = readFileSync9(mapPath, "utf-8");
37652
+ if (existsSync15(mapPath)) {
37653
+ const content = readFileSync10(mapPath, "utf-8");
37014
37654
  parts.push("");
37015
37655
  parts.push("<map>");
37016
37656
  parts.push(content);
@@ -37022,26 +37662,26 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
37022
37662
  return parts.join("\n");
37023
37663
  }
37024
37664
  function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
37025
- const roundDir = join15(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
37026
- const finalPath = join15(roundDir, "final.md");
37027
- const reviewersDir = join15(roundDir, "reviews");
37665
+ const roundDir = join17(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
37666
+ const finalPath = join17(roundDir, "final.md");
37667
+ const reviewersDir = join17(roundDir, "reviews");
37028
37668
  const parts = [
37029
37669
  `You are an expert code reviewer assisting with a code review session.`,
37030
37670
  `You are looking at review round #${roundNumber} for session "${sessionId}".`,
37031
37671
  "",
37032
37672
  `Below are the review artifacts for this round:`
37033
37673
  ];
37034
- if (existsSync12(finalPath)) {
37035
- const content = readFileSync9(finalPath, "utf-8");
37674
+ if (existsSync15(finalPath)) {
37675
+ const content = readFileSync10(finalPath, "utf-8");
37036
37676
  parts.push("");
37037
37677
  parts.push("<final-synthesis>");
37038
37678
  parts.push(content);
37039
37679
  parts.push("</final-synthesis>");
37040
37680
  }
37041
- if (existsSync12(reviewersDir)) {
37042
- const files = readdirSync2(reviewersDir).filter((f) => f.endsWith(".md")).sort();
37681
+ if (existsSync15(reviewersDir)) {
37682
+ const files = readdirSync3(reviewersDir).filter((f) => f.endsWith(".md")).sort();
37043
37683
  for (const file of files) {
37044
- const content = readFileSync9(join15(reviewersDir, file), "utf-8");
37684
+ const content = readFileSync10(join17(reviewersDir, file), "utf-8");
37045
37685
  const reviewerName = file.replace(/\.md$/, "");
37046
37686
  parts.push("");
37047
37687
  parts.push(`<reviewer name="${reviewerName}">`);
@@ -37049,7 +37689,7 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
37049
37689
  parts.push("</reviewer>");
37050
37690
  }
37051
37691
  }
37052
- if (!existsSync12(finalPath) && !existsSync12(reviewersDir)) {
37692
+ if (!existsSync15(finalPath) && !existsSync15(reviewersDir)) {
37053
37693
  parts.push("");
37054
37694
  parts.push("(No review artifacts found on disk for this round.)");
37055
37695
  }
@@ -37195,7 +37835,7 @@ User: ${message}`;
37195
37835
  });
37196
37836
  return;
37197
37837
  }
37198
- const repoRoot = dirname11(ocrDir);
37838
+ const repoRoot = dirname12(ocrDir);
37199
37839
  const spawnResult = adapter.spawn({
37200
37840
  prompt,
37201
37841
  cwd: repoRoot,
@@ -37371,13 +38011,13 @@ function cleanupAllChats() {
37371
38011
  }
37372
38012
 
37373
38013
  // src/server/socket/post-handler.ts
37374
- import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync10, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
38014
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
37375
38015
  import { tmpdir as tmpdir2 } from "node:os";
37376
- import { join as join16, dirname as dirname12, isAbsolute as isAbsolute2 } from "node:path";
38016
+ import { join as join18, dirname as dirname13, isAbsolute as isAbsolute2 } from "node:path";
37377
38017
  import { randomUUID as randomUUID2 } from "node:crypto";
37378
38018
  function resolveSessionDir2(sessionDir, ocrDir) {
37379
38019
  if (isAbsolute2(sessionDir)) return sessionDir;
37380
- return join16(dirname12(ocrDir), sessionDir);
38020
+ return join18(dirname13(ocrDir), sessionDir);
37381
38021
  }
37382
38022
  var BRANCH_PREFIXES = [
37383
38023
  "feat",
@@ -37446,7 +38086,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37446
38086
  return;
37447
38087
  }
37448
38088
  const branch = session.branch;
37449
- const repoRoot = dirname12(ocrDir);
38089
+ const repoRoot = dirname13(ocrDir);
37450
38090
  try {
37451
38091
  await execBinaryAsync("gh", ["auth", "status"], { env: cleanEnv(), cwd: repoRoot, encoding: "utf-8" });
37452
38092
  } catch {
@@ -37547,19 +38187,19 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37547
38187
  socket.emit("post:error", { error: "Session not found" });
37548
38188
  return;
37549
38189
  }
37550
- const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join16(ocrDir, "sessions", sessionId);
37551
- const roundDir = join16(sessionDir, "rounds", `round-${roundNumber}`);
37552
- const finalPath = join16(roundDir, "final.md");
37553
- if (!existsSync13(finalPath)) {
38190
+ const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join18(ocrDir, "sessions", sessionId);
38191
+ const roundDir = join18(sessionDir, "rounds", `round-${roundNumber}`);
38192
+ const finalPath = join18(roundDir, "final.md");
38193
+ if (!existsSync16(finalPath)) {
37554
38194
  socket.emit("post:error", { error: "final.md not found for this round" });
37555
38195
  return;
37556
38196
  }
37557
- const humanReviewPath = join16(roundDir, "final-human.md");
37558
- const repoRoot = dirname12(ocrDir);
37559
- const commandMdPath = join16(ocrDir, "commands", "translate-review-to-single-human.md");
38197
+ const humanReviewPath = join18(roundDir, "final-human.md");
38198
+ const repoRoot = dirname13(ocrDir);
38199
+ const commandMdPath = join18(ocrDir, "commands", "translate-review-to-single-human.md");
37560
38200
  let commandContent;
37561
38201
  try {
37562
- commandContent = readFileSync10(commandMdPath, "utf-8");
38202
+ commandContent = readFileSync11(commandMdPath, "utf-8");
37563
38203
  } catch {
37564
38204
  socket.emit("post:error", {
37565
38205
  error: `Command file not found: ${commandMdPath}. Run \`ocr init\` to set up.`
@@ -37639,9 +38279,9 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37639
38279
  }
37640
38280
  }
37641
38281
  let generatedContent = "";
37642
- if (existsSync13(humanReviewPath)) {
38282
+ if (existsSync16(humanReviewPath)) {
37643
38283
  try {
37644
- generatedContent = readFileSync10(humanReviewPath, "utf-8").trim();
38284
+ generatedContent = readFileSync11(humanReviewPath, "utf-8").trim();
37645
38285
  } catch {
37646
38286
  }
37647
38287
  }
@@ -37716,11 +38356,11 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37716
38356
  socket.emit("post:save-result", { success: false, error: "Session not found" });
37717
38357
  return;
37718
38358
  }
37719
- const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join16(ocrDir, "sessions", sessionId);
37720
- const roundDir = join16(sessionDir, "rounds", `round-${roundNumber}`);
37721
- mkdirSync5(roundDir, { recursive: true });
37722
- const filePath = join16(roundDir, "final-human.md");
37723
- writeFileSync4(filePath, content, { mode: 420 });
38359
+ const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join18(ocrDir, "sessions", sessionId);
38360
+ const roundDir = join18(sessionDir, "rounds", `round-${roundNumber}`);
38361
+ mkdirSync6(roundDir, { recursive: true });
38362
+ const filePath = join18(roundDir, "final-human.md");
38363
+ writeFileSync5(filePath, content, { mode: 420 });
37724
38364
  socket.emit("post:save-result", { success: true });
37725
38365
  } catch (err) {
37726
38366
  console.error("Error in post:save handler:", err);
@@ -37746,14 +38386,14 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37746
38386
  );
37747
38387
  tracker.appendOutput(`\u25B8 Posting review to PR #${prNumber}...
37748
38388
  `);
37749
- const tmpDir = join16(tmpdir2(), "ocr-post-comments");
38389
+ const tmpDir = join18(tmpdir2(), "ocr-post-comments");
37750
38390
  try {
37751
- mkdirSync5(tmpDir, { recursive: true, mode: 448 });
38391
+ mkdirSync6(tmpDir, { recursive: true, mode: 448 });
37752
38392
  } catch {
37753
38393
  }
37754
- const tmpFile = join16(tmpDir, `${randomUUID2()}.md`);
37755
- writeFileSync4(tmpFile, content, { mode: 384 });
37756
- const repoRoot = dirname12(ocrDir);
38394
+ const tmpFile = join18(tmpDir, `${randomUUID2()}.md`);
38395
+ writeFileSync5(tmpFile, content, { mode: 384 });
38396
+ const repoRoot = dirname13(ocrDir);
37757
38397
  try {
37758
38398
  const { stdout } = await execBinaryAsync(
37759
38399
  "gh",
@@ -37776,7 +38416,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37776
38416
  });
37777
38417
  } finally {
37778
38418
  try {
37779
- unlinkSync2(tmpFile);
38419
+ unlinkSync4(tmpFile);
37780
38420
  } catch {
37781
38421
  }
37782
38422
  }
@@ -37796,45 +38436,9 @@ function cleanupAllPostGenerations() {
37796
38436
  }
37797
38437
  }
37798
38438
 
37799
- // ../cli/src/lib/runtime-config.ts
37800
- import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
37801
- import { join as join17 } from "node:path";
37802
- var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
37803
- function getAgentHeartbeatSeconds(ocrDir) {
37804
- const configPath = join17(ocrDir, "config.yaml");
37805
- if (!existsSync14(configPath)) {
37806
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
37807
- }
37808
- let content;
37809
- try {
37810
- content = readFileSync11(configPath, "utf-8");
37811
- } catch {
37812
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
37813
- }
37814
- const blockMatch = content.match(
37815
- /^runtime:\s*\n(?:\s+[^\n]*\n)*?\s+agent_heartbeat_seconds:\s*([^\s#\n]+)/m
37816
- );
37817
- const inlineMatch = content.match(
37818
- /^runtime:\s*\{[^}]*\bagent_heartbeat_seconds:\s*([^\s,}]+)/m
37819
- );
37820
- const raw = blockMatch?.[1] ?? inlineMatch?.[1];
37821
- if (!raw) {
37822
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
37823
- }
37824
- const parsed = Number(raw);
37825
- if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
37826
- process.stderr.write(
37827
- `[ocr] runtime.agent_heartbeat_seconds is not a positive integer (got "${raw}"); falling back to ${DEFAULT_AGENT_HEARTBEAT_SECONDS}s.
37828
- `
37829
- );
37830
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
37831
- }
37832
- return parsed;
37833
- }
37834
-
37835
38439
  // src/server/index.ts
37836
38440
  import { homedir } from "node:os";
37837
- var __dirname3 = dirname13(fileURLToPath3(import.meta.url));
38441
+ var __dirname3 = dirname14(fileURLToPath3(import.meta.url));
37838
38442
  function shortenPath(p) {
37839
38443
  const home = homedir();
37840
38444
  return p.startsWith(home) ? "~" + p.slice(home.length) : p;
@@ -37898,40 +38502,61 @@ if (process.env.NODE_ENV !== "production") {
37898
38502
  res.json({ token: AUTH_TOKEN });
37899
38503
  });
37900
38504
  }
38505
+ function isOcrDashboardProcess(pid) {
38506
+ if (process.platform === "win32") return false;
38507
+ try {
38508
+ const cmd = execFileSync3("ps", ["-p", String(pid), "-o", "command="], {
38509
+ encoding: "utf-8",
38510
+ timeout: 3e3
38511
+ }).trim();
38512
+ if (/^ocr-dashboard\b/.test(cmd)) return true;
38513
+ return /dashboard\/server\.js|server\/index\.ts/.test(cmd);
38514
+ } catch {
38515
+ return false;
38516
+ }
38517
+ }
37901
38518
  async function startServer(options = {}) {
37902
38519
  const port = options.port ?? parseInt(process.env.PORT ?? "4173", 10);
38520
+ process.title = "ocr-dashboard";
37903
38521
  const ocrDir = resolveOcrDir();
37904
38522
  const aiCliService = new AiCliService(ocrDir);
37905
- const dbPathForCheckpoint = join18(ocrDir, "data", "ocr.db");
38523
+ const dbPathForCheckpoint = join19(ocrDir, "data", "ocr.db");
37906
38524
  const walResult = walCheckpointTruncate(dbPathForCheckpoint);
37907
38525
  if (walResult === "checkpointed") {
37908
38526
  console.log(" WAL checkpoint: truncated stale write-ahead-log file");
37909
38527
  }
38528
+ for (const reaped of reapOrphanDbFiles(join19(ocrDir, "data"))) {
38529
+ console.log(` Orphan reap: removed stale ${reaped}`);
38530
+ }
38531
+ const staleLogs = reapStaleExecLogs(join19(ocrDir, "data", "exec-logs"));
38532
+ if (staleLogs.length > 0) {
38533
+ console.log(` Exec-log reap: removed ${staleLogs.length} stale agent log(s)`);
38534
+ }
37910
38535
  const db = await openDb(ocrDir);
37911
- const dataDir = join18(ocrDir, "data");
37912
- const pidFilePath = join18(dataDir, "dashboard.pid");
37913
- const portFilePath = join18(dataDir, "server-port");
37914
- mkdirSync6(dataDir, { recursive: true });
38536
+ const dataDir = join19(ocrDir, "data");
38537
+ const pidFilePath = join19(dataDir, "dashboard.pid");
38538
+ const portFilePath = join19(dataDir, "server-port");
38539
+ mkdirSync7(dataDir, { recursive: true });
37915
38540
  try {
37916
- unlinkSync3(portFilePath);
38541
+ unlinkSync5(portFilePath);
37917
38542
  } catch {
37918
38543
  }
37919
- if (existsSync15(pidFilePath)) {
38544
+ if (existsSync17(pidFilePath)) {
37920
38545
  try {
37921
38546
  const oldPid = parseInt(readFileSync12(pidFilePath, "utf-8").trim(), 10);
37922
- if (!isNaN(oldPid)) {
37923
- try {
37924
- process.kill(oldPid, 0);
37925
- console.warn(
37926
- `Warning: another dashboard server (PID ${oldPid}) appears to be running. If this is stale, delete ${pidFilePath} and restart.`
37927
- );
37928
- } catch {
38547
+ if (!isNaN(oldPid) && oldPid !== process.pid && isProcessAlive(oldPid) && isOcrDashboardProcess(oldPid)) {
38548
+ console.log(` Single-instance: reaping prior dashboard server (PID ${oldPid}) and taking over`);
38549
+ reapTree(oldPid);
38550
+ const deadline = Date.now() + 6e3;
38551
+ while (isProcessAlive(oldPid) && Date.now() < deadline) {
38552
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
37929
38553
  }
38554
+ walCheckpointTruncate(dbPathForCheckpoint);
37930
38555
  }
37931
38556
  } catch {
37932
38557
  }
37933
38558
  }
37934
- writeFileSync5(pidFilePath, String(process.pid), { mode: 384 });
38559
+ writeFileSync6(pidFilePath, String(process.pid), { mode: 384 });
37935
38560
  const cmdCountResult = db.exec("SELECT COUNT(*) as c FROM command_executions");
37936
38561
  const totalCmds = cmdCountResult[0]?.values[0]?.[0] ?? 0;
37937
38562
  if (totalCmds === 0) {
@@ -37941,7 +38566,7 @@ async function startServer(options = {}) {
37941
38566
  }
37942
38567
  }
37943
38568
  const orphanResult = db.exec(
37944
- `SELECT id, pid, is_detached, started_at FROM command_executions
38569
+ `SELECT id, pid, started_at FROM command_executions
37945
38570
  WHERE pid IS NOT NULL AND finished_at IS NULL`
37946
38571
  );
37947
38572
  if (orphanResult.length > 0 && orphanResult[0]) {
@@ -37951,37 +38576,15 @@ async function startServer(options = {}) {
37951
38576
  let killedCount = 0;
37952
38577
  for (const row of orphanRows) {
37953
38578
  const pid = row[colIdx["pid"]];
37954
- const isDetached = row[colIdx["is_detached"]] === 1;
37955
38579
  const startedAt = row[colIdx["started_at"]];
37956
38580
  if (sqliteUtcMs(startedAt) < cutoff) continue;
37957
38581
  if (defaultIsAlive(pid)) {
37958
- if (isDetached) {
37959
- try {
37960
- process.kill(-pid, "SIGTERM");
37961
- } catch {
37962
- process.kill(pid, "SIGTERM");
37963
- }
37964
- } else {
37965
- process.kill(pid, "SIGTERM");
37966
- }
38582
+ reapTree(pid);
37967
38583
  killedCount++;
37968
- setTimeout(() => {
37969
- try {
37970
- process.kill(pid, 0);
37971
- if (isDetached) {
37972
- try {
37973
- process.kill(-pid, "SIGKILL");
37974
- } catch {
37975
- }
37976
- }
37977
- process.kill(pid, "SIGKILL");
37978
- } catch {
37979
- }
37980
- }, 2e3);
37981
38584
  }
37982
38585
  }
37983
38586
  if (killedCount > 0) {
37984
- console.log(` Cleaned up ${killedCount} orphaned process(es)`);
38587
+ console.log(` Reaped ${killedCount} orphaned process tree(s)`);
37985
38588
  }
37986
38589
  }
37987
38590
  const legacyResult = db.exec(
@@ -37991,10 +38594,11 @@ async function startServer(options = {}) {
37991
38594
  if (legacyCount > 0) {
37992
38595
  db.run(
37993
38596
  `UPDATE command_executions
37994
- SET exit_code = -2,
38597
+ SET exit_code = ?,
37995
38598
  output = COALESCE(output, '') || '
37996
38599
  [Cancelled]'
37997
- WHERE finished_at IS NOT NULL AND exit_code IS NULL`
38600
+ WHERE finished_at IS NOT NULL AND exit_code IS NULL`,
38601
+ [CANCELLED_EXIT_CODE]
37998
38602
  );
37999
38603
  console.log(` Backfilled ${legacyCount} finished command(s) missing an exit code`);
38000
38604
  }
@@ -38017,11 +38621,25 @@ async function startServer(options = {}) {
38017
38621
  ` Auto-closed ${staleSessionResult.closedSessionIds.length} stale active session(s) (threshold 7 days)`
38018
38622
  );
38019
38623
  }
38624
+ const reconcileCompleted = async () => {
38625
+ try {
38626
+ const closed = await reconcileCompletedSessions(ocrDir);
38627
+ if (closed.length > 0) {
38628
+ console.log(
38629
+ ` Auto-finalized ${closed.length} completed-but-open session(s)`
38630
+ );
38631
+ }
38632
+ } catch (err) {
38633
+ console.error("[reconcile] completed-session reconciliation failed:", err);
38634
+ }
38635
+ };
38636
+ await reconcileCompleted();
38020
38637
  const SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
38021
38638
  const sweepTimer = setInterval(() => {
38022
38639
  try {
38023
38640
  logAgentSweep(sweepStaleAgentSessions(db, heartbeatSeconds, defaultIsAlive));
38024
38641
  sweepStaleSessions(db, STALE_SESSION_THRESHOLD_SECONDS);
38642
+ void reconcileCompleted();
38025
38643
  } catch (err) {
38026
38644
  console.error("[sweep] periodic sweep failed:", err);
38027
38645
  }
@@ -38057,11 +38675,11 @@ async function startServer(options = {}) {
38057
38675
  app.use("/api/agent-sessions", createAgentSessionsRouter(db, () => pullSync()));
38058
38676
  app.use("/api/sessions", createHandoffRouter(sessionCapture, ocrDir, () => pullSync()));
38059
38677
  app.use("/api/team", createTeamRouter(ocrDir));
38060
- const clientDir = join18(__dirname3, "client");
38061
- if (process.env.NODE_ENV === "production" && existsSync15(clientDir)) {
38678
+ const clientDir = join19(__dirname3, "client");
38679
+ if (process.env.NODE_ENV === "production" && existsSync17(clientDir)) {
38062
38680
  app.use(import_express15.default.static(clientDir, { index: false }));
38063
- const indexHtmlPath = join18(clientDir, "index.html");
38064
- const rawIndexHtml = existsSync15(indexHtmlPath) ? readFileSync12(indexHtmlPath, "utf-8") : "";
38681
+ const indexHtmlPath = join19(clientDir, "index.html");
38682
+ const rawIndexHtml = existsSync17(indexHtmlPath) ? readFileSync12(indexHtmlPath, "utf-8") : "";
38065
38683
  const tokenScript = `<script>window.__OCR_TOKEN__=${JSON.stringify(AUTH_TOKEN)};</script>`;
38066
38684
  const injectedIndexHtml = rawIndexHtml.replace(
38067
38685
  "</head>",
@@ -38080,7 +38698,7 @@ async function startServer(options = {}) {
38080
38698
  registerChatHandlers(io, socket, db, ocrDir, aiCliService);
38081
38699
  registerPostHandlers(io, socket, db, ocrDir, aiCliService);
38082
38700
  });
38083
- const dbFilePath = join18(ocrDir, "data", "ocr.db");
38701
+ const dbFilePath = join19(ocrDir, "data", "ocr.db");
38084
38702
  const dbSyncWatcher = new DbSyncWatcher(
38085
38703
  db,
38086
38704
  dbFilePath,
@@ -38096,7 +38714,7 @@ async function startServer(options = {}) {
38096
38714
  dbSyncWatcher.startWatching();
38097
38715
  pullSync = () => dbSyncWatcher.syncFromDisk();
38098
38716
  console.log(` Watching DB: ${shortenPath(dbFilePath)}`);
38099
- const sessionsDir = join18(ocrDir, "sessions");
38717
+ const sessionsDir = join19(ocrDir, "sessions");
38100
38718
  const fsSync = new FilesystemSync(db, sessionsDir, io);
38101
38719
  await fsSync.fullScan();
38102
38720
  fsSync.startWatching();
@@ -38135,7 +38753,7 @@ async function startServer(options = {}) {
38135
38753
  if (actualPort !== port) {
38136
38754
  console.log(` Note: using port ${actualPort} (${port} was in use)`);
38137
38755
  }
38138
- writeFileSync5(portFilePath, String(actualPort), { mode: 384 });
38756
+ writeFileSync6(portFilePath, String(actualPort), { mode: 384 });
38139
38757
  console.log(` Server: http://localhost:${actualPort}`);
38140
38758
  console.log(` OCR directory: ${shortenPath(ocrDir)}`);
38141
38759
  console.log();
@@ -38148,53 +38766,40 @@ async function startServer(options = {}) {
38148
38766
  } catch {
38149
38767
  }
38150
38768
  }
38151
- const shutdown = (signal) => {
38769
+ const shutdown = async (signal) => {
38152
38770
  console.log(
38153
38771
  `Shutting down dashboard server${signal ? ` (received ${signal})` : ""}...`
38154
38772
  );
38155
38773
  try {
38156
- unlinkSync3(pidFilePath);
38774
+ unlinkSync5(pidFilePath);
38157
38775
  } catch {
38158
38776
  }
38159
38777
  try {
38160
- unlinkSync3(portFilePath);
38161
- } catch {
38162
- }
38163
- try {
38164
- unlinkSync3(join18(dataDir, "dashboard-active-spawn.json"));
38778
+ unlinkSync5(portFilePath);
38165
38779
  } catch {
38166
38780
  }
38781
+ clearSpawnMarker(ocrDir);
38167
38782
  try {
38168
38783
  const activeResult = db.exec(
38169
- "SELECT id, pid, is_detached FROM command_executions WHERE pid IS NOT NULL AND finished_at IS NULL"
38784
+ "SELECT id, pid FROM command_executions WHERE pid IS NOT NULL AND finished_at IS NULL"
38170
38785
  );
38171
38786
  if (activeResult.length > 0 && activeResult[0]) {
38172
38787
  const { columns, values: activeRows } = activeResult[0];
38173
38788
  const colIdx = Object.fromEntries(columns.map((c, i) => [c, i]));
38174
38789
  for (const row of activeRows) {
38175
38790
  const pid = row[colIdx["pid"]];
38176
- const isDetached = row[colIdx["is_detached"]] === 1;
38177
- try {
38178
- if (isDetached) {
38179
- try {
38180
- process.kill(-pid, "SIGTERM");
38181
- } catch {
38182
- process.kill(pid, "SIGTERM");
38183
- }
38184
- } else {
38185
- process.kill(pid, "SIGTERM");
38186
- }
38187
- console.log(`Sent SIGTERM to child process (PID ${pid})`);
38188
- } catch {
38189
- }
38791
+ reapTree(pid, 750);
38792
+ console.log(`Reaping child process tree (PID ${pid})`);
38190
38793
  }
38794
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
38191
38795
  db.run(
38192
38796
  `UPDATE command_executions
38193
- SET exit_code = -2, finished_at = datetime('now'),
38797
+ SET exit_code = ?, finished_at = datetime('now'),
38194
38798
  output = COALESCE(output, '') || '
38195
38799
  [Cancelled \u2014 server shutdown]',
38196
38800
  pid = NULL
38197
- WHERE pid IS NOT NULL AND finished_at IS NULL`
38801
+ WHERE pid IS NOT NULL AND finished_at IS NULL`,
38802
+ [CANCELLED_EXIT_CODE]
38198
38803
  );
38199
38804
  }
38200
38805
  } catch (err) {
@@ -38217,9 +38822,9 @@ async function startServer(options = {}) {
38217
38822
  process.exit(1);
38218
38823
  }, 2e3).unref();
38219
38824
  };
38220
- process.on("SIGINT", () => shutdown("SIGINT"));
38221
- process.on("SIGTERM", () => shutdown("SIGTERM"));
38222
- process.on("SIGHUP", () => shutdown("SIGHUP"));
38825
+ process.on("SIGINT", () => void shutdown("SIGINT"));
38826
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
38827
+ process.on("SIGHUP", () => void shutdown("SIGHUP"));
38223
38828
  process.on("uncaughtException", (err) => {
38224
38829
  console.error("[dashboard] uncaughtException:", err);
38225
38830
  });