@open-code-review/cli 2.2.1 → 2.4.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 (64) hide show
  1. package/README.md +9 -0
  2. package/dist/dashboard/client/assets/{_basePickBy-BAlGnwHG.js → _basePickBy-CyrHyeyN.js} +1 -1
  3. package/dist/dashboard/client/assets/{_baseUniq-CoauyOeL.js → _baseUniq-Bg7NJSGS.js} +1 -1
  4. package/dist/dashboard/client/assets/{arc-DtS0aHfP.js → arc-zDGAKMur.js} +1 -1
  5. package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-CnWmtRTh.js → architectureDiagram-VXUJARFQ-BxlGxm0Q.js} +1 -1
  6. package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-DgPp4oGV.js → blockDiagram-VD42YOAC-BskTNyX5.js} +1 -1
  7. package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO--LV4qQaE.js → c4Diagram-YG6GDRKO-Dr9QQ-dn.js} +1 -1
  8. package/dist/dashboard/client/assets/channel-BUnm_-UQ.js +1 -0
  9. package/dist/dashboard/client/assets/{chunk-4BX2VUAB-BRglpc7Z.js → chunk-4BX2VUAB-xq9xoCTv.js} +1 -1
  10. package/dist/dashboard/client/assets/{chunk-55IACEB6-Bgx06_CV.js → chunk-55IACEB6-DYdXYVh5.js} +1 -1
  11. package/dist/dashboard/client/assets/{chunk-B4BG7PRW-D6HN3Yiy.js → chunk-B4BG7PRW-BGAyFRFS.js} +1 -1
  12. package/dist/dashboard/client/assets/{chunk-DI55MBZ5-NH9EgN9T.js → chunk-DI55MBZ5-C5ul9stk.js} +1 -1
  13. package/dist/dashboard/client/assets/{chunk-FMBD7UC4-xriO6WNP.js → chunk-FMBD7UC4-BSaPo2xa.js} +1 -1
  14. package/dist/dashboard/client/assets/{chunk-QN33PNHL-CV1h6_Zl.js → chunk-QN33PNHL-CyzabUv0.js} +1 -1
  15. package/dist/dashboard/client/assets/{chunk-QZHKN3VN-CV4VzxNq.js → chunk-QZHKN3VN-CceRbxt_.js} +1 -1
  16. package/dist/dashboard/client/assets/{chunk-TZMSLE5B-isdklocW.js → chunk-TZMSLE5B-Bjg9IoOQ.js} +1 -1
  17. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-D_fkmNvU.js +1 -0
  18. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-D_fkmNvU.js +1 -0
  19. package/dist/dashboard/client/assets/clone-DTyrNOLZ.js +1 -0
  20. package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-CCzlFSJf.js → cose-bilkent-S5V4N54A-DEdXBrCt.js} +1 -1
  21. package/dist/dashboard/client/assets/{dagre-6UL2VRFP-DVN3PkjZ.js → dagre-6UL2VRFP-DRdIiP58.js} +1 -1
  22. package/dist/dashboard/client/assets/{diagram-PSM6KHXK-SzJVoSsb.js → diagram-PSM6KHXK-Bo7Q2VlK.js} +1 -1
  23. package/dist/dashboard/client/assets/{diagram-QEK2KX5R-CgGn7ts-.js → diagram-QEK2KX5R-2Fmc2o5x.js} +1 -1
  24. package/dist/dashboard/client/assets/{diagram-S2PKOQOG-Bz1ukSx8.js → diagram-S2PKOQOG-5WE8f0p7.js} +1 -1
  25. package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-CpstUTMZ.js → erDiagram-Q2GNP2WA-DD-iXWd_.js} +1 -1
  26. package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-aYVydGhp.js → flowDiagram-NV44I4VS-CCWo8Ue9.js} +1 -1
  27. package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-Cb2DUSRk.js → ganttDiagram-JELNMOA3-CNY4d5UK.js} +1 -1
  28. package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-BUOnwA2w.js → gitGraphDiagram-V2S2FVAM-Dq5SBEJJ.js} +1 -1
  29. package/dist/dashboard/client/assets/{graph-4X5ddhLp.js → graph-BTt9lokK.js} +1 -1
  30. package/dist/dashboard/client/assets/{index-CKWqYAfu.js → index-B0k81q2b.js} +138 -138
  31. package/dist/dashboard/client/assets/index-Czwdh6UA.css +1 -0
  32. package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-BlMqcrwm.js → infoDiagram-HS3SLOUP-AnKZja-G.js} +1 -1
  33. package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-DF2ew7ju.js → journeyDiagram-XKPGCS4Q-nC-_WjPN.js} +1 -1
  34. package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-BKQMx0-n.js → kanban-definition-3W4ZIXB7-BEY73sWU.js} +1 -1
  35. package/dist/dashboard/client/assets/{layout-DNcn2g9w.js → layout-D4DfNpzH.js} +1 -1
  36. package/dist/dashboard/client/assets/{linear-Bqy9gvqb.js → linear-ZpGvKjeP.js} +1 -1
  37. package/dist/dashboard/client/assets/{mermaid-renderer-dJ71wgld.js → mermaid-renderer-BCDxmS9g.js} +4 -4
  38. package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-BARc8sqJ.js → mindmap-definition-VGOIOE7T-MzAaKESA.js} +1 -1
  39. package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-CULlNZTd.js → pieDiagram-ADFJNKIX-B_X1kySF.js} +1 -1
  40. package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-BJEZPVe9.js → quadrantDiagram-AYHSOK5B-CMoIEMLN.js} +1 -1
  41. package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-BhMsmUIs.js → requirementDiagram-UZGBJVZJ-v4CRsn1w.js} +1 -1
  42. package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BYbNgogG.js → sankeyDiagram-TZEHDZUN-CPcyN8Jj.js} +1 -1
  43. package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-MoM_NwWk.js → sequenceDiagram-WL72ISMW-CTg0Vx1H.js} +1 -1
  44. package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-ditrlbM3.js → stateDiagram-FKZM4ZOC-BMWBN6Nq.js} +1 -1
  45. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-C9Jk1xd0.js +1 -0
  46. package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-DOAJyjuz.js → timeline-definition-IT6M3QCI-B8xFcSGb.js} +1 -1
  47. package/dist/dashboard/client/assets/{treemap-GDKQZRPO-BBJkjnJl.js → treemap-GDKQZRPO-HQQuGl9w.js} +1 -1
  48. package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-CPW4s5vm.js → xychartDiagram-PRI3JC2R-Drz0SW3I.js} +1 -1
  49. package/dist/dashboard/client/index.html +2 -2
  50. package/dist/dashboard/server.js +926 -461
  51. package/dist/index.js +1344 -323
  52. package/package.json +5 -38
  53. package/dist/dashboard/client/assets/channel-BU2129fl.js +0 -1
  54. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-CVftFGiR.js +0 -1
  55. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-CVftFGiR.js +0 -1
  56. package/dist/dashboard/client/assets/clone-DC6LEEC5.js +0 -1
  57. package/dist/dashboard/client/assets/index-CzxeSSaQ.css +0 -1
  58. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-SqoG2LCn.js +0 -1
  59. package/dist/lib/db/index.js +0 -2177
  60. package/dist/lib/models.js +0 -160
  61. package/dist/lib/runtime-config.js +0 -55
  62. package/dist/lib/state/index.js +0 -2196
  63. package/dist/lib/team-config.js +0 -175
  64. package/dist/lib/vendor-resume.js +0 -31
@@ -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 dirname15 = path2.dirname;
18413
+ var dirname16 = path2.dirname;
18414
18414
  var basename5 = path2.basename;
18415
18415
  var extname = path2.extname;
18416
- var join20 = path2.join;
18416
+ var join21 = path2.join;
18417
18417
  var resolve3 = path2.resolve;
18418
18418
  module.exports = View;
18419
18419
  function View(name, options) {
@@ -18449,7 +18449,7 @@ 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 = dirname15(loc);
18452
+ var dir = dirname16(loc);
18453
18453
  var file = basename5(loc);
18454
18454
  path3 = this.resolve(dir, file);
18455
18455
  }
@@ -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 = join20(dir, file);
18464
+ var path3 = join21(dir, file);
18465
18465
  var stat = tryStat(path3);
18466
18466
  if (stat && stat.isFile()) {
18467
18467
  return path3;
18468
18468
  }
18469
- path3 = join20(dir, basename5(file, ext), "index" + ext);
18469
+ path3 = join21(dir, basename5(file, ext), "index" + ext);
18470
18470
  stat = tryStat(path3);
18471
18471
  if (stat && stat.isFile()) {
18472
18472
  return path3;
@@ -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 join20 = path2.join;
19102
+ var join21 = 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(join20(root, path3));
19321
+ path3 = normalize(join21(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 = join20(path3, self._index[i]);
19456
+ var p = join21(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);
@@ -30047,9 +30047,9 @@ var init_define_lazy_prop = __esm({
30047
30047
  });
30048
30048
 
30049
30049
  // ../../node_modules/.pnpm/default-browser-id@5.0.1/node_modules/default-browser-id/index.js
30050
- import { promisify as promisify2 } from "node:util";
30050
+ import { promisify } from "node:util";
30051
30051
  import process4 from "node:process";
30052
- import { execFile as execFile2 } from "node:child_process";
30052
+ import { execFile } from "node:child_process";
30053
30053
  async function defaultBrowserId() {
30054
30054
  if (process4.platform !== "darwin") {
30055
30055
  throw new Error("macOS only");
@@ -30065,14 +30065,14 @@ async function defaultBrowserId() {
30065
30065
  var execFileAsync;
30066
30066
  var init_default_browser_id = __esm({
30067
30067
  "../../node_modules/.pnpm/default-browser-id@5.0.1/node_modules/default-browser-id/index.js"() {
30068
- execFileAsync = promisify2(execFile2);
30068
+ execFileAsync = promisify(execFile);
30069
30069
  }
30070
30070
  });
30071
30071
 
30072
30072
  // ../../node_modules/.pnpm/run-applescript@7.1.0/node_modules/run-applescript/index.js
30073
30073
  import process5 from "node:process";
30074
- import { promisify as promisify3 } from "node:util";
30075
- import { execFile as execFile3, execFileSync as execFileSync2 } from "node:child_process";
30074
+ import { promisify as promisify2 } from "node:util";
30075
+ import { execFile as execFile2, execFileSync as execFileSync2 } from "node:child_process";
30076
30076
  async function runAppleScript(script, { humanReadableOutput = true, signal } = {}) {
30077
30077
  if (process5.platform !== "darwin") {
30078
30078
  throw new Error("macOS only");
@@ -30088,7 +30088,7 @@ async function runAppleScript(script, { humanReadableOutput = true, signal } = {
30088
30088
  var execFileAsync2;
30089
30089
  var init_run_applescript = __esm({
30090
30090
  "../../node_modules/.pnpm/run-applescript@7.1.0/node_modules/run-applescript/index.js"() {
30091
- execFileAsync2 = promisify3(execFile3);
30091
+ execFileAsync2 = promisify2(execFile2);
30092
30092
  }
30093
30093
  });
30094
30094
 
@@ -30104,8 +30104,8 @@ var init_bundle_name = __esm({
30104
30104
  });
30105
30105
 
30106
30106
  // ../../node_modules/.pnpm/default-browser@5.5.0/node_modules/default-browser/windows.js
30107
- import { promisify as promisify4 } from "node:util";
30108
- import { execFile as execFile4 } from "node:child_process";
30107
+ import { promisify as promisify3 } from "node:util";
30108
+ import { execFile as execFile3 } from "node:child_process";
30109
30109
  async function defaultBrowser(_execFileAsync = execFileAsync3) {
30110
30110
  const { stdout } = await _execFileAsync("reg", [
30111
30111
  "QUERY",
@@ -30127,7 +30127,7 @@ async function defaultBrowser(_execFileAsync = execFileAsync3) {
30127
30127
  var execFileAsync3, windowsBrowserProgIds, _windowsBrowserProgIdMap, UnknownBrowserError;
30128
30128
  var init_windows = __esm({
30129
30129
  "../../node_modules/.pnpm/default-browser@5.5.0/node_modules/default-browser/windows.js"() {
30130
- execFileAsync3 = promisify4(execFile4);
30130
+ execFileAsync3 = promisify3(execFile3);
30131
30131
  windowsBrowserProgIds = {
30132
30132
  MSEdgeHTM: { name: "Edge", id: "com.microsoft.edge" },
30133
30133
  // The missing `L` is correct.
@@ -30154,9 +30154,9 @@ var init_windows = __esm({
30154
30154
  });
30155
30155
 
30156
30156
  // ../../node_modules/.pnpm/default-browser@5.5.0/node_modules/default-browser/index.js
30157
- import { promisify as promisify5 } from "node:util";
30157
+ import { promisify as promisify4 } from "node:util";
30158
30158
  import process6 from "node:process";
30159
- import { execFile as execFile5 } from "node:child_process";
30159
+ import { execFile as execFile4 } from "node:child_process";
30160
30160
  async function defaultBrowser2() {
30161
30161
  if (process6.platform === "darwin") {
30162
30162
  const id = await defaultBrowserId();
@@ -30180,7 +30180,7 @@ var init_default_browser = __esm({
30180
30180
  init_default_browser_id();
30181
30181
  init_bundle_name();
30182
30182
  init_windows();
30183
- execFileAsync4 = promisify5(execFile5);
30183
+ execFileAsync4 = promisify4(execFile4);
30184
30184
  titleize = (string) => string.toLowerCase().replaceAll(/(?:^|\s|-)\S/g, (x) => x.toUpperCase());
30185
30185
  }
30186
30186
  });
@@ -30196,14 +30196,14 @@ import process7 from "node:process";
30196
30196
  import { Buffer as Buffer2 } from "node:buffer";
30197
30197
  import path from "node:path";
30198
30198
  import { fileURLToPath as fileURLToPath2 } from "node:url";
30199
- import { promisify as promisify6 } from "node:util";
30199
+ import { promisify as promisify5 } from "node:util";
30200
30200
  import childProcess from "node:child_process";
30201
30201
  import fs5, { constants as fsConstants2 } from "node:fs/promises";
30202
30202
  async function getWindowsDefaultBrowserFromWsl() {
30203
30203
  const powershellPath = await powerShellPath();
30204
30204
  const rawCommand = String.raw`(Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice").ProgId`;
30205
30205
  const encodedCommand = Buffer2.from(rawCommand, "utf16le").toString("base64");
30206
- const { stdout } = await execFile6(
30206
+ const { stdout } = await execFile5(
30207
30207
  powershellPath,
30208
30208
  [
30209
30209
  "-NoProfile",
@@ -30243,14 +30243,14 @@ function detectPlatformBinary({ [platform]: platformBinary }, { wsl }) {
30243
30243
  }
30244
30244
  return detectArchBinary(platformBinary);
30245
30245
  }
30246
- var execFile6, __dirname2, localXdgOpenPath, platform, arch, pTryEach, baseOpen, open, openApp, apps, open_default;
30246
+ var execFile5, __dirname2, localXdgOpenPath, platform, arch, pTryEach, baseOpen, open, openApp, apps, open_default;
30247
30247
  var init_open = __esm({
30248
30248
  "../../node_modules/.pnpm/open@10.2.0/node_modules/open/index.js"() {
30249
30249
  init_wsl_utils();
30250
30250
  init_define_lazy_prop();
30251
30251
  init_default_browser();
30252
30252
  init_is_inside_container();
30253
- execFile6 = promisify6(childProcess.execFile);
30253
+ execFile5 = promisify5(childProcess.execFile);
30254
30254
  __dirname2 = path.dirname(fileURLToPath2(import.meta.url));
30255
30255
  localXdgOpenPath = path.join(__dirname2, "xdg-open");
30256
30256
  ({ platform, arch } = process7);
@@ -30483,42 +30483,197 @@ 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 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";
30486
+ import { existsSync as existsSync17, readFileSync as readFileSync12, writeFileSync as writeFileSync6, unlinkSync as unlinkSync5, mkdirSync as mkdirSync8 } from "node:fs";
30487
+ import { join as join20, dirname as dirname15, resolve as resolve2 } from "node:path";
30488
30488
 
30489
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";
30490
+ import { execFileSync } from "node:child_process";
30491
+
30492
+ // ../shared/platform/src/spawn.ts
30493
+ import crossSpawn from "cross-spawn";
30494
+ var DEFAULT_MAX_BUFFER = 1024 * 1024;
30498
30495
  function execBinary(binary, args, opts) {
30499
- return execFileSync(binary, args, {
30500
- ...opts,
30501
- shell: isWindows
30496
+ const result = crossSpawn.sync(binary, args, {
30497
+ maxBuffer: DEFAULT_MAX_BUFFER,
30498
+ ...opts
30502
30499
  });
30500
+ if (result.error) throw result.error;
30501
+ if (result.status !== 0) {
30502
+ throw Object.assign(
30503
+ new Error(
30504
+ `Command failed: ${binary} ${args.join(" ")}
30505
+ ${String(result.stderr ?? "")}`
30506
+ ),
30507
+ {
30508
+ status: result.status,
30509
+ code: result.status,
30510
+ signal: result.signal,
30511
+ stdout: result.stdout,
30512
+ stderr: result.stderr,
30513
+ pid: result.pid
30514
+ }
30515
+ );
30516
+ }
30517
+ return result.stdout;
30503
30518
  }
30504
30519
  async function execBinaryAsync(binary, args, opts) {
30505
- return execFilePromise(binary, args, {
30506
- ...opts,
30507
- shell: isWindows
30520
+ return new Promise((resolvePromise, rejectPromise) => {
30521
+ const child = crossSpawn(binary, args, {
30522
+ cwd: opts.cwd,
30523
+ env: opts.env,
30524
+ windowsHide: true
30525
+ });
30526
+ const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
30527
+ let stdout = "";
30528
+ let stderr = "";
30529
+ let killed = false;
30530
+ let settled = false;
30531
+ const settle = (fn) => {
30532
+ if (settled) return;
30533
+ settled = true;
30534
+ if (timer) clearTimeout(timer);
30535
+ fn();
30536
+ };
30537
+ const overflow = () => {
30538
+ killed = true;
30539
+ child.kill();
30540
+ };
30541
+ child.stdout?.setEncoding(opts.encoding);
30542
+ child.stderr?.setEncoding(opts.encoding);
30543
+ child.stdout?.on("data", (chunk) => {
30544
+ stdout += chunk;
30545
+ if (stdout.length > maxBuffer) overflow();
30546
+ });
30547
+ child.stderr?.on("data", (chunk) => {
30548
+ stderr += chunk;
30549
+ if (stderr.length > maxBuffer) overflow();
30550
+ });
30551
+ const timer = opts.timeout ? setTimeout(() => {
30552
+ killed = true;
30553
+ child.kill();
30554
+ }, opts.timeout) : void 0;
30555
+ child.on("error", (err) => {
30556
+ settle(
30557
+ () => rejectPromise(Object.assign(err, { stdout, stderr, killed }))
30558
+ );
30559
+ });
30560
+ child.on("close", (code, signal) => {
30561
+ settle(() => {
30562
+ if (code === 0 && !killed) {
30563
+ resolvePromise({ stdout, stderr });
30564
+ return;
30565
+ }
30566
+ rejectPromise(
30567
+ Object.assign(
30568
+ new Error(
30569
+ `Command failed: ${binary} ${args.join(" ")}
30570
+ ${stderr}`
30571
+ ),
30572
+ {
30573
+ code: code ?? void 0,
30574
+ signal,
30575
+ stdout,
30576
+ stderr,
30577
+ killed
30578
+ }
30579
+ )
30580
+ );
30581
+ });
30582
+ });
30508
30583
  });
30509
30584
  }
30510
30585
  function spawnBinary(binary, args, opts) {
30511
- return spawn(binary, args, {
30586
+ return crossSpawn(binary, args, {
30512
30587
  ...opts,
30513
- ...isWindows && { shell: true, windowsHide: true }
30588
+ windowsHide: true
30514
30589
  });
30515
30590
  }
30591
+
30592
+ // ../shared/platform/src/verdict.ts
30593
+ var CANONICAL_VERDICTS = [
30594
+ "APPROVE",
30595
+ "REQUEST CHANGES",
30596
+ "NEEDS DISCUSSION"
30597
+ ];
30598
+ var VERDICT_SET = new Set(CANONICAL_VERDICTS);
30599
+ function isCanonicalVerdict(v) {
30600
+ return VERDICT_SET.has(v);
30601
+ }
30602
+ var VERDICT_ALIASES = {
30603
+ // Approve-gate aliases (including the retired composites)
30604
+ APPROVED: "APPROVE",
30605
+ LGTM: "APPROVE",
30606
+ "APPROVE WITH SUGGESTIONS": "APPROVE",
30607
+ APPROVE_WITH_SUGGESTIONS: "APPROVE",
30608
+ "ACCEPT WITH FOLLOW-UPS": "APPROVE",
30609
+ "ACCEPT WITH FOLLOWUPS": "APPROVE",
30610
+ ACCEPT_WITH_FOLLOWUPS: "APPROVE",
30611
+ ACCEPT_WITH_FOLLOW_UPS: "APPROVE",
30612
+ // Request-changes-gate aliases
30613
+ "CHANGES REQUESTED": "REQUEST CHANGES",
30614
+ REQUEST_CHANGES: "REQUEST CHANGES",
30615
+ BLOCK: "REQUEST CHANGES",
30616
+ REJECT: "REQUEST CHANGES",
30617
+ // Needs-discussion-gate aliases
30618
+ "NEEDS WORK": "NEEDS DISCUSSION",
30619
+ NEEDS_DISCUSSION: "NEEDS DISCUSSION"
30620
+ };
30621
+ function normalizeVerdict(raw) {
30622
+ const key = raw.trim().toUpperCase();
30623
+ if (isCanonicalVerdict(key)) return key;
30624
+ return VERDICT_ALIASES[key] ?? null;
30625
+ }
30626
+
30627
+ // ../shared/platform/src/counts.ts
30628
+ function deriveCounts(findings) {
30629
+ const counts = {
30630
+ blocker: 0,
30631
+ should_fix: 0,
30632
+ suggestion: 0,
30633
+ style: 0
30634
+ };
30635
+ for (const finding of findings) {
30636
+ const category = finding?.category;
30637
+ if (category === "blocker" || category === "should_fix" || category === "suggestion" || category === "style") {
30638
+ counts[category]++;
30639
+ }
30640
+ }
30641
+ return counts;
30642
+ }
30643
+ function collectFindings(meta) {
30644
+ const all = [];
30645
+ for (const reviewer of meta.reviewers ?? []) {
30646
+ for (const finding of reviewer?.findings ?? []) all.push(finding);
30647
+ }
30648
+ return all;
30649
+ }
30650
+ function preferred(scValue, derivedValue) {
30651
+ return typeof scValue === "number" && Number.isFinite(scValue) ? scValue : derivedValue;
30652
+ }
30653
+ function resolveRoundCounts(meta) {
30654
+ const allFindings = collectFindings(meta);
30655
+ const derived = deriveCounts(allFindings);
30656
+ const sc = meta.synthesis_counts ?? void 0;
30657
+ return {
30658
+ blockerCount: sc ? preferred(sc.blockers, derived.blocker) : derived.blocker,
30659
+ shouldFixCount: sc ? preferred(sc.should_fix, derived.should_fix) : derived.should_fix,
30660
+ suggestionCount: sc ? preferred(sc.suggestions, derived.suggestion) : derived.suggestion,
30661
+ reviewerCount: (meta.reviewers ?? []).length,
30662
+ totalFindingCount: allFindings.length
30663
+ };
30664
+ }
30665
+
30666
+ // ../shared/platform/src/index.ts
30667
+ var isWindows = process.platform === "win32";
30668
+ function killErrorMeansDead(err) {
30669
+ return err instanceof Error && "code" in err && err.code === "ESRCH";
30670
+ }
30516
30671
  function isProcessAlive(pid) {
30517
30672
  try {
30518
30673
  process.kill(pid, 0);
30519
30674
  return true;
30520
30675
  } catch (err) {
30521
- return !(err instanceof Error && "code" in err && err.code === "ESRCH");
30676
+ return !killErrorMeansDead(err);
30522
30677
  }
30523
30678
  }
30524
30679
  function walkDescendants(rootPid) {
@@ -30538,14 +30693,15 @@ function walkDescendants(rootPid) {
30538
30693
  if (!m) continue;
30539
30694
  const pid = Number(m[1]);
30540
30695
  const ppid = Number(m[2]);
30541
- if (!children.has(ppid)) children.set(ppid, []);
30542
- children.get(ppid).push(pid);
30696
+ const siblings = children.get(ppid) ?? [];
30697
+ siblings.push(pid);
30698
+ children.set(ppid, siblings);
30543
30699
  }
30544
30700
  const acc = [];
30545
30701
  const queue = [rootPid];
30546
30702
  const seen = /* @__PURE__ */ new Set([rootPid]);
30547
- while (queue.length) {
30548
- const p = queue.shift();
30703
+ let p;
30704
+ while ((p = queue.shift()) !== void 0) {
30549
30705
  for (const c of children.get(p) ?? []) {
30550
30706
  if (seen.has(c)) continue;
30551
30707
  seen.add(c);
@@ -30628,7 +30784,6 @@ function defaultIconFor(id, tier) {
30628
30784
  // src/server/index.ts
30629
30785
  import { fileURLToPath as fileURLToPath3 } from "node:url";
30630
30786
  import { randomBytes } from "node:crypto";
30631
- import { execFileSync as execFileSync3 } from "node:child_process";
30632
30787
  import { Server as SocketIOServer } from "socket.io";
30633
30788
 
30634
30789
  // src/server/services/ocr-resolver.ts
@@ -30651,7 +30806,7 @@ function resolveOcrDir(startDir) {
30651
30806
  }
30652
30807
  }
30653
30808
 
30654
- // ../cli/src/lib/db/index.ts
30809
+ // ../shared/persistence/src/db/index.ts
30655
30810
  import {
30656
30811
  existsSync as existsSync5,
30657
30812
  mkdirSync as mkdirSync2,
@@ -30662,10 +30817,10 @@ import {
30662
30817
  } from "node:fs";
30663
30818
  import { dirname as dirname5, join as join5 } from "node:path";
30664
30819
 
30665
- // ../cli/src/lib/db/engine.ts
30820
+ // ../shared/persistence/src/db/engine.ts
30666
30821
  import { createRequire } from "node:module";
30667
30822
 
30668
- // ../cli/src/lib/runtime-checks.ts
30823
+ // ../shared/persistence/src/runtime-checks.ts
30669
30824
  var NODE_FLOOR = { major: 22, minor: 5 };
30670
30825
  function isSupportedNode(version) {
30671
30826
  const [major = 0, minor = 0] = version.split(".").map((n) => Number.parseInt(n, 10) || 0);
@@ -30683,7 +30838,7 @@ function isSuppressibleSqliteWarning(warning) {
30683
30838
  return typeof message === "string" && message.includes("SQLite is an experimental feature");
30684
30839
  }
30685
30840
 
30686
- // ../cli/src/lib/db/engine.ts
30841
+ // ../shared/persistence/src/db/engine.ts
30687
30842
  var SQLITE_BUSY = 5;
30688
30843
  var SQLITE_BUSY_SNAPSHOT = 261;
30689
30844
  var BUSY_RETRY_ATTEMPTS = 5;
@@ -30845,7 +31000,7 @@ function openEngine(dbPath) {
30845
31000
  return new NodeSqliteAdapter(native);
30846
31001
  }
30847
31002
 
30848
- // ../cli/src/lib/db/migrations.ts
31003
+ // ../shared/persistence/src/db/migrations.ts
30849
31004
  var MIGRATIONS = [
30850
31005
  {
30851
31006
  version: 1,
@@ -31393,11 +31548,11 @@ function runMigrations(db) {
31393
31548
  }
31394
31549
  }
31395
31550
 
31396
- // ../cli/src/lib/db/reconcile.ts
31551
+ // ../shared/persistence/src/db/reconcile.ts
31397
31552
  import { existsSync as existsSync2 } from "node:fs";
31398
31553
  import { isAbsolute, join as join2, dirname as dirname2 } from "node:path";
31399
31554
 
31400
- // ../cli/src/lib/db/result-mapper.ts
31555
+ // ../shared/persistence/src/db/result-mapper.ts
31401
31556
  function resultToRows(result) {
31402
31557
  if (result.length === 0 || !result[0]) {
31403
31558
  return [];
@@ -31416,7 +31571,7 @@ function resultToRow(result) {
31416
31571
  return rows[0];
31417
31572
  }
31418
31573
 
31419
- // ../cli/src/lib/db/queries.ts
31574
+ // ../shared/persistence/src/db/queries.ts
31420
31575
  function insertSession(db, params) {
31421
31576
  const {
31422
31577
  id,
@@ -31499,6 +31654,14 @@ function insertEvent(db, params) {
31499
31654
  ]
31500
31655
  );
31501
31656
  }
31657
+ function getEventsForSession(db, sessionId) {
31658
+ return resultToRows(
31659
+ db.exec(
31660
+ "SELECT * FROM orchestration_events WHERE session_id = ? ORDER BY id ASC",
31661
+ [sessionId]
31662
+ )
31663
+ );
31664
+ }
31502
31665
  function commitReasonClose(db, sessionId, reasonEvent, projectionUpdates) {
31503
31666
  db.transaction(() => {
31504
31667
  insertEvent(db, { session_id: sessionId, ...reasonEvent });
@@ -31506,7 +31669,7 @@ function commitReasonClose(db, sessionId, reasonEvent, projectionUpdates) {
31506
31669
  });
31507
31670
  }
31508
31671
 
31509
- // ../cli/src/lib/db/reconcile.ts
31672
+ // ../shared/persistence/src/db/reconcile.ts
31510
31673
  var DEFAULT_STALE_THRESHOLD_SECONDS = 7 * 24 * 60 * 60;
31511
31674
  function hasTerminalArtifactEvent(db, sessionId, workflowType, currentRound, currentMapRun) {
31512
31675
  const eventType = workflowType === "map" ? "map_completed" : "round_completed";
@@ -31636,14 +31799,14 @@ function reconcileLegacyState(db, ocrDir, opts = {}) {
31636
31799
  return { dryRun, actions };
31637
31800
  }
31638
31801
 
31639
- // ../cli/src/lib/db/liveness.ts
31802
+ // ../shared/persistence/src/db/liveness.ts
31640
31803
  var PID_REUSE_GUARD_MS = 24 * 60 * 60 * 1e3;
31641
31804
  function defaultIsAlive(pid) {
31642
31805
  try {
31643
31806
  process.kill(pid, 0);
31644
31807
  return true;
31645
31808
  } catch (err) {
31646
- return !(err instanceof Error && "code" in err && err.code === "ESRCH");
31809
+ return !killErrorMeansDead(err);
31647
31810
  }
31648
31811
  }
31649
31812
  function sqliteUtcMs(ts) {
@@ -31651,7 +31814,7 @@ function sqliteUtcMs(ts) {
31651
31814
  return new Date(sqliteShape ? ts.replace(" ", "T") + "Z" : ts).getTime();
31652
31815
  }
31653
31816
 
31654
- // ../cli/src/lib/state/exit-codes.ts
31817
+ // ../shared/persistence/src/state/exit-codes.ts
31655
31818
  var STATE_EXIT = {
31656
31819
  OK: 0,
31657
31820
  USAGE: 2,
@@ -31675,7 +31838,7 @@ var ORPHAN_EXIT_CODE = -3;
31675
31838
  var CASCADE_CLOSE_EXIT_CODE = -4;
31676
31839
  var WATCHDOG_DEADLINE_EXIT_CODE = -5;
31677
31840
 
31678
- // ../cli/src/lib/db/agent-sessions.ts
31841
+ // ../shared/persistence/src/db/agent-sessions.ts
31679
31842
  var NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
31680
31843
  var INSTANCE_COMMAND = "session-instance";
31681
31844
  function cascadeTerminateExecutions(db, workflowId, exitCode, note) {
@@ -31747,6 +31910,10 @@ function getLatestAgentSessionWithVendorId(db, workflowId) {
31747
31910
  );
31748
31911
  return row ? rowToAgentSession(row) : void 0;
31749
31912
  }
31913
+ var SAFE_VENDOR_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
31914
+ function isSafeVendorSessionId(id) {
31915
+ return SAFE_VENDOR_SESSION_ID.test(id);
31916
+ }
31750
31917
  function recordVendorSessionIdForExecution(db, executionId, vendorSessionId) {
31751
31918
  db.run(
31752
31919
  `UPDATE command_executions
@@ -31868,7 +32035,7 @@ function sweepStaleSessions(db, thresholdSeconds) {
31868
32035
  return { closedSessionIds: rows.map((r) => r.id) };
31869
32036
  }
31870
32037
 
31871
- // ../cli/src/lib/db/maintenance.ts
32038
+ // ../shared/persistence/src/db/maintenance.ts
31872
32039
  import {
31873
32040
  existsSync as existsSync3,
31874
32041
  readdirSync,
@@ -31943,7 +32110,7 @@ function reapStaleExecLogs(execLogsDir, maxAgeMs = SEVEN_DAYS_MS) {
31943
32110
  return reaped;
31944
32111
  }
31945
32112
 
31946
- // ../cli/src/lib/db/command-log.ts
32113
+ // ../shared/persistence/src/db/command-log.ts
31947
32114
  import { appendFileSync, existsSync as existsSync4, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
31948
32115
  import { dirname as dirname4, join as join4 } from "node:path";
31949
32116
  import { randomUUID } from "node:crypto";
@@ -32040,7 +32207,7 @@ function rotateIfNeeded(filePath) {
32040
32207
  }
32041
32208
  }
32042
32209
 
32043
- // ../cli/src/lib/db/index.ts
32210
+ // ../shared/persistence/src/db/index.ts
32044
32211
  var V2_SCHEMA_VERSION = 12;
32045
32212
  function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
32046
32213
  if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
@@ -33346,41 +33513,8 @@ function createStatsRouter(db) {
33346
33513
  var import_express8 = __toESM(require_express2(), 1);
33347
33514
 
33348
33515
  // src/server/socket/command-runner.ts
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";
33351
-
33352
- // src/server/services/command-outcome.ts
33353
- function deriveCommandOutcome(exitCode, completeness) {
33354
- if (exitCode === null) return null;
33355
- if (exitCode === CANCELLED_EXIT_CODE || exitCode === CASCADE_CLOSE_EXIT_CODE) {
33356
- return "cancelled";
33357
- }
33358
- if (exitCode === WATCHDOG_DEADLINE_EXIT_CODE) return "failed";
33359
- if (exitCode !== 0) return "failed";
33360
- if (completeness === null || completeness === "complete") return "success";
33361
- return "incomplete";
33362
- }
33363
- function deriveCancellationReason(exitCode) {
33364
- if (exitCode === CANCELLED_EXIT_CODE) return "user";
33365
- if (exitCode === CASCADE_CLOSE_EXIT_CODE) return "cascade";
33366
- return null;
33367
- }
33368
- function getWorkflowCompletenessForExecution(db, executionId) {
33369
- const result = db.exec(
33370
- `SELECT sc.completeness_state
33371
- FROM command_executions ce
33372
- LEFT JOIN session_completeness sc ON sc.session_id = ce.workflow_id
33373
- WHERE ce.id = ?`,
33374
- [executionId]
33375
- );
33376
- const row = result[0]?.values[0];
33377
- if (!row) return null;
33378
- const state = row[0];
33379
- if (state === "complete" || state === "closed_without_artifact" || state === "in_flight" || state === "open_no_artifact") {
33380
- return state;
33381
- }
33382
- return null;
33383
- }
33516
+ import { readFileSync as readFileSync5, mkdirSync as mkdirSync6 } from "node:fs";
33517
+ import { dirname as dirname7, join as join13 } from "node:path";
33384
33518
 
33385
33519
  // src/server/services/ai-cli/index.ts
33386
33520
  import { readFileSync as readFileSync3 } from "node:fs";
@@ -33390,12 +33524,24 @@ import { join as join9 } from "node:path";
33390
33524
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, openSync, closeSync } from "node:fs";
33391
33525
  import { tmpdir } from "node:os";
33392
33526
  import { join as join7 } from "node:path";
33393
- function buildFileStdio(stdin, logFile) {
33527
+ function buildFileStdio(logFile) {
33394
33528
  if (!logFile) {
33395
- return { stdio: [stdin, "pipe", "pipe"], logFd: null, logPath: void 0 };
33529
+ return { stdio: ["pipe", "pipe", "pipe"], logFd: null, logPath: void 0 };
33396
33530
  }
33397
33531
  const logFd = openSync(logFile, "a");
33398
- return { stdio: [stdin, logFd, logFd], logFd, logPath: logFile };
33532
+ return { stdio: ["pipe", logFd, logFd], logFd, logPath: logFile };
33533
+ }
33534
+ function assertNonEmptyPrompt(prompt) {
33535
+ if (prompt.length === 0) {
33536
+ throw new Error("refusing to spawn with an empty prompt");
33537
+ }
33538
+ }
33539
+ function deliverPrompt(proc, prompt) {
33540
+ assertNonEmptyPrompt(prompt);
33541
+ proc.stdin?.on("error", () => {
33542
+ });
33543
+ proc.stdin?.write(prompt);
33544
+ proc.stdin?.end();
33399
33545
  }
33400
33546
  function closeFileStdio(logFd) {
33401
33547
  if (logFd === null) return;
@@ -33459,7 +33605,7 @@ function cleanEnv() {
33459
33605
  return env;
33460
33606
  }
33461
33607
 
33462
- // ../cli/src/lib/vendor-resume.ts
33608
+ // ../shared/persistence/src/vendor-resume.ts
33463
33609
  var VENDOR_BINARIES = {
33464
33610
  claude: "claude",
33465
33611
  opencode: "opencode"
@@ -33517,6 +33663,7 @@ var ClaudeCodeAdapter = class {
33517
33663
  }
33518
33664
  }
33519
33665
  spawn(opts) {
33666
+ assertNonEmptyPrompt(opts.prompt);
33520
33667
  const isWorkflow = opts.mode === "workflow";
33521
33668
  const maxTurns = opts.maxTurns ?? (isWorkflow ? 500 : 1);
33522
33669
  const tools = opts.allowedTools ?? (isWorkflow ? WORKFLOW_TOOLS : QUERY_TOOLS);
@@ -33538,7 +33685,6 @@ var ClaudeCodeAdapter = class {
33538
33685
  flags.push("--model", opts.model);
33539
33686
  }
33540
33687
  const { stdio, logFd, logPath } = buildFileStdio(
33541
- "pipe",
33542
33688
  isWorkflow ? opts.logFile : void 0
33543
33689
  );
33544
33690
  const proc = spawnBinary("claude", flags, {
@@ -33549,8 +33695,7 @@ var ClaudeCodeAdapter = class {
33549
33695
  });
33550
33696
  closeFileStdio(logFd);
33551
33697
  if (isWorkflow) proc.unref();
33552
- proc.stdin?.write(opts.prompt);
33553
- proc.stdin?.end();
33698
+ deliverPrompt(proc, opts.prompt);
33554
33699
  return {
33555
33700
  process: proc,
33556
33701
  detached: isWorkflow,
@@ -33742,11 +33887,11 @@ var OpenCodeAdapter = class {
33742
33887
  }
33743
33888
  }
33744
33889
  spawn(opts) {
33890
+ assertNonEmptyPrompt(opts.prompt);
33745
33891
  const isWorkflow = opts.mode === "workflow";
33746
33892
  const agent = opts.allowedTools ? void 0 : isWorkflow ? "build" : "plan";
33747
33893
  const args = [
33748
33894
  "run",
33749
- opts.prompt,
33750
33895
  "--format",
33751
33896
  "json"
33752
33897
  ];
@@ -33760,7 +33905,6 @@ var OpenCodeAdapter = class {
33760
33905
  args.push("--model", opts.model);
33761
33906
  }
33762
33907
  const { stdio, logFd, logPath } = buildFileStdio(
33763
- "ignore",
33764
33908
  isWorkflow ? opts.logFile : void 0
33765
33909
  );
33766
33910
  const proc = spawnBinary("opencode", args, {
@@ -33771,6 +33915,7 @@ var OpenCodeAdapter = class {
33771
33915
  });
33772
33916
  closeFileStdio(logFd);
33773
33917
  if (isWorkflow) proc.unref();
33918
+ deliverPrompt(proc, opts.prompt);
33774
33919
  return {
33775
33920
  process: proc,
33776
33921
  detached: isWorkflow,
@@ -34044,8 +34189,8 @@ var AiCliService = class {
34044
34189
  const available = this.entries.filter((e) => e.detection.found);
34045
34190
  if (available.length === 0) return null;
34046
34191
  if (this.preference !== "auto") {
34047
- const preferred = available.find((e) => e.adapter.binary === this.preference);
34048
- if (preferred) return preferred.adapter;
34192
+ const preferred2 = available.find((e) => e.adapter.binary === this.preference);
34193
+ if (preferred2) return preferred2.adapter;
34049
34194
  console.warn(
34050
34195
  ` AI CLI: Preferred "${this.preference}" not found, falling back to auto-detection`
34051
34196
  );
@@ -34092,10 +34237,12 @@ var FileTailer = class {
34092
34237
  /** Read everything currently available from `offset` to EOF. */
34093
34238
  poll() {
34094
34239
  if (!this.ensureOpen()) return;
34240
+ const fd = this.fd;
34241
+ if (fd === null) return;
34095
34242
  let bytes;
34096
34243
  do {
34097
34244
  try {
34098
- bytes = readSync(this.fd, this.buf, 0, this.buf.length, this.offset);
34245
+ bytes = readSync(fd, this.buf, 0, this.buf.length, this.offset);
34099
34246
  } catch {
34100
34247
  return;
34101
34248
  }
@@ -34156,100 +34303,12 @@ function resolveLocalCli() {
34156
34303
  return null;
34157
34304
  }
34158
34305
 
34159
- // ../cli/src/lib/state/projection.ts
34160
- var REASON_EVENT_TYPES = [
34161
- "session_aborted",
34162
- "session_auto_closed_stale",
34163
- "session_synced",
34164
- "session_legacy_import"
34165
- ];
34166
- var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
34167
- "session_closed",
34168
- ...REASON_EVENT_TYPES
34169
- ]);
34170
- function hasCompletionInvariant(db, session) {
34171
- const eventType = session.workflow_type === "map" ? "map_completed" : "round_completed";
34172
- const round = session.workflow_type === "map" ? session.current_map_run : session.current_round;
34173
- const r = db.exec(
34174
- `SELECT 1 FROM orchestration_events
34175
- WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
34176
- [session.id, eventType, round]
34177
- );
34178
- return (r[0]?.values.length ?? 0) > 0;
34179
- }
34180
-
34181
- // ../cli/src/lib/state/index.ts
34182
- async function stateClose(params) {
34183
- const { sessionId, ocrDir, abort } = params;
34184
- const db = await ensureDatabase(ocrDir);
34185
- const existing = getSession(db, sessionId);
34186
- if (!existing) {
34187
- throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
34188
- }
34189
- if (existing.status === "closed") {
34190
- console.error(`[ocr] Session already closed: ${sessionId}`);
34191
- return;
34192
- }
34193
- if (!abort && !hasCompletionInvariant(db, existing)) {
34194
- 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`;
34195
- throw new StateError(
34196
- STATE_EXIT.INVARIANT_UNMET,
34197
- `Cannot close session ${sessionId}: ${what}. Run 'ocr state complete-round' to finalize it, or pass --abort to record an abandoned session.`
34198
- );
34199
- }
34200
- const note = "closed by parent workflow close";
34201
- db.transaction(() => {
34202
- if (abort) {
34203
- insertEvent(db, {
34204
- session_id: sessionId,
34205
- event_type: "session_aborted",
34206
- phase: existing.current_phase,
34207
- phase_number: existing.phase_number,
34208
- round: existing.current_round
34209
- });
34210
- }
34211
- updateSession(db, sessionId, {
34212
- status: "closed",
34213
- current_phase: "complete"
34214
- });
34215
- if (!abort) {
34216
- insertEvent(db, {
34217
- session_id: sessionId,
34218
- event_type: "session_closed",
34219
- phase: "complete",
34220
- phase_number: existing.phase_number,
34221
- round: existing.current_round
34222
- });
34223
- }
34224
- cascadeTerminateExecutions(db, sessionId, CASCADE_CLOSE_EXIT_CODE, note);
34225
- });
34226
- }
34227
- async function reconcileWorkflowOnExit(ocrDir, sessionId, db) {
34228
- db ??= await ensureDatabase(ocrDir);
34229
- const existing = getSession(db, sessionId);
34230
- if (!existing) return "not-found";
34231
- if (existing.status === "closed") return "already-closed";
34232
- if (!hasCompletionInvariant(db, existing)) return "incomplete";
34233
- if (hasInFlightDependents(db, sessionId)) return "in-flight";
34234
- await stateClose({ sessionId, ocrDir, abort: false });
34235
- return "closed";
34236
- }
34237
- async function reconcileCompletedSessions(ocrDir) {
34238
- const db = await ensureDatabase(ocrDir);
34239
- const closed = [];
34240
- for (const s of getAllSessions(db)) {
34241
- if (s.status !== "active") continue;
34242
- const outcome = await reconcileWorkflowOnExit(ocrDir, s.id, db);
34243
- if (outcome === "closed") closed.push(s.id);
34244
- }
34245
- return closed;
34246
- }
34247
-
34248
- // ../cli/src/lib/runtime-config.ts
34306
+ // ../shared/config/src/runtime-config.ts
34249
34307
  import { existsSync as existsSync9, readFileSync as readFileSync4 } from "node:fs";
34250
34308
  import { join as join11 } from "node:path";
34251
34309
  var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
34252
34310
  var DEFAULT_WORKFLOW_HARD_DEADLINE_MINUTES = 60;
34311
+ var DEFAULT_FORWARD_RESUME_MAX_ATTEMPTS = 2;
34253
34312
  function readRuntimePositiveInt(ocrDir, key, defaultValue) {
34254
34313
  const configPath = join11(ocrDir, "config.yaml");
34255
34314
  if (!existsSync9(configPath)) return defaultValue;
@@ -34294,8 +34353,15 @@ function getWorkflowHardDeadlineMs(ocrDir) {
34294
34353
  DEFAULT_WORKFLOW_HARD_DEADLINE_MINUTES
34295
34354
  ) * 60 * 1e3;
34296
34355
  }
34356
+ function getForwardResumeMaxAttempts(ocrDir) {
34357
+ return readRuntimePositiveInt(
34358
+ ocrDir,
34359
+ "forward_resume_max_attempts",
34360
+ DEFAULT_FORWARD_RESUME_MAX_ATTEMPTS
34361
+ );
34362
+ }
34297
34363
 
34298
- // src/server/socket/command-runner.ts
34364
+ // src/server/socket/prompt-builder.ts
34299
34365
  function shellSplit(str) {
34300
34366
  const tokens = [];
34301
34367
  let current = "";
@@ -34322,11 +34388,6 @@ function shellSplit(str) {
34322
34388
  if (current) tokens.push(current);
34323
34389
  return tokens;
34324
34390
  }
34325
- var ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
34326
- "progress",
34327
- "state"
34328
- ]);
34329
- var AI_COMMANDS = /* @__PURE__ */ new Set(["map", "review", "translate-review-to-single-human", "address", "create-reviewer", "sync-reviewers"]);
34330
34391
  function escapeUserHeaders(value) {
34331
34392
  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");
34332
34393
  }
@@ -34351,8 +34412,8 @@ function buildPrompt(opts) {
34351
34412
  options.push("--fresh");
34352
34413
  i++;
34353
34414
  } else if (arg === "--requirements" && i + 1 < subArgs.length) {
34354
- requirements = subArgs.slice(i + 1).join(" ");
34355
- break;
34415
+ requirements = subArgs[i + 1] ?? "";
34416
+ i += 2;
34356
34417
  } else if (arg === "--team" && i + 1 < subArgs.length) {
34357
34418
  team = subArgs[i + 1] ?? "";
34358
34419
  i += 2;
@@ -34362,8 +34423,9 @@ function buildPrompt(opts) {
34362
34423
  } else if (arg === "--reviewer" && i + 1 < subArgs.length) {
34363
34424
  const raw = subArgs[i + 1] ?? "";
34364
34425
  const countMatch = raw.match(/^(\d+):(.+)$/);
34365
- if (countMatch) {
34366
- reviewerDescriptions.push({ description: countMatch[2], count: parseInt(countMatch[1], 10) });
34426
+ const [, countStr, description] = countMatch ?? [];
34427
+ if (countStr && description) {
34428
+ reviewerDescriptions.push({ description, count: parseInt(countStr, 10) });
34367
34429
  } else {
34368
34430
  reviewerDescriptions.push({ description: raw, count: 1 });
34369
34431
  }
@@ -34474,7 +34536,60 @@ function extractPerInstanceModels(subArgs) {
34474
34536
  }
34475
34537
  return [...models];
34476
34538
  }
34539
+
34540
+ // src/server/socket/process-registry.ts
34477
34541
  var MAX_CONCURRENT = 3;
34542
+ var activeCommands = /* @__PURE__ */ new Map();
34543
+ function getActiveCommands() {
34544
+ return Array.from(activeCommands.values()).map((entry) => ({
34545
+ execution_id: entry.executionId,
34546
+ command: entry.commandStr,
34547
+ started_at: entry.startedAt,
34548
+ output: entry.outputBuffer
34549
+ }));
34550
+ }
34551
+
34552
+ // src/server/socket/spawn-markers.ts
34553
+ import { writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, mkdirSync as mkdirSync5, existsSync as existsSync10, rmSync as rmSync2 } from "node:fs";
34554
+ import { join as join12 } from "node:path";
34555
+ function spawnMarkerDir(ocrDir) {
34556
+ return join12(ocrDir, "data", "dashboard-active-spawn");
34557
+ }
34558
+ function spawnMarkerPath(ocrDir, executionUid) {
34559
+ const safe = executionUid.replace(/[^A-Za-z0-9._-]/g, "_");
34560
+ return join12(spawnMarkerDir(ocrDir), `${safe}.json`);
34561
+ }
34562
+ function legacySpawnMarkerPath(ocrDir) {
34563
+ return join12(ocrDir, "data", "dashboard-active-spawn.json");
34564
+ }
34565
+ function writeSpawnMarker(ocrDir, executionUid, pid) {
34566
+ const dir = spawnMarkerDir(ocrDir);
34567
+ if (!existsSync10(dir)) mkdirSync5(dir, { recursive: true });
34568
+ const payload = JSON.stringify({
34569
+ execution_uid: executionUid,
34570
+ pid,
34571
+ started_at: (/* @__PURE__ */ new Date()).toISOString()
34572
+ });
34573
+ writeFileSync3(spawnMarkerPath(ocrDir, executionUid), payload, { mode: 384 });
34574
+ }
34575
+ function clearSpawnMarker(ocrDir, executionUid) {
34576
+ try {
34577
+ unlinkSync3(spawnMarkerPath(ocrDir, executionUid));
34578
+ } catch {
34579
+ }
34580
+ }
34581
+ function clearAllSpawnMarkers(ocrDir) {
34582
+ try {
34583
+ rmSync2(spawnMarkerDir(ocrDir), { recursive: true, force: true });
34584
+ } catch {
34585
+ }
34586
+ try {
34587
+ unlinkSync3(legacySpawnMarkerPath(ocrDir));
34588
+ } catch {
34589
+ }
34590
+ }
34591
+
34592
+ // src/server/socket/watchdog.ts
34478
34593
  var WATCHDOG_TICK_MS = 1e4;
34479
34594
  var POST_RESULT_GRACE_MS = 3e4;
34480
34595
  var HEARTBEAT_THROTTLE_MS = 5e3;
@@ -34497,34 +34612,296 @@ function decideWatchdogTick(i) {
34497
34612
  }
34498
34613
  return i.exited ? { action: "wait" } : { action: "beat" };
34499
34614
  }
34500
- var activeCommands = /* @__PURE__ */ new Map();
34501
- function spawnMarkerPath(ocrDir) {
34502
- return join12(ocrDir, "data", "dashboard-active-spawn.json");
34615
+ function makeHeartbeatBumper(db, executionId, entry) {
34616
+ return () => {
34617
+ if (entry.finalized) return;
34618
+ const now = Date.now();
34619
+ if (now - (entry.lastBeatWrite ?? 0) < HEARTBEAT_THROTTLE_MS) return;
34620
+ entry.lastBeatWrite = now;
34621
+ try {
34622
+ db.run(
34623
+ `UPDATE command_executions SET last_heartbeat_at = datetime('now') WHERE id = ? AND finished_at IS NULL`,
34624
+ [executionId]
34625
+ );
34626
+ } catch (err) {
34627
+ console.error("[command-runner] heartbeat bump failed:", err);
34628
+ }
34629
+ };
34503
34630
  }
34504
- function writeSpawnMarker(ocrDir, executionUid, pid) {
34505
- const dataDir = join12(ocrDir, "data");
34506
- if (!existsSync10(dataDir)) mkdirSync5(dataDir, { recursive: true });
34507
- const payload = JSON.stringify({
34508
- execution_uid: executionUid,
34509
- pid,
34510
- started_at: (/* @__PURE__ */ new Date()).toISOString()
34511
- });
34512
- writeFileSync3(spawnMarkerPath(ocrDir), payload, { mode: 384 });
34513
- }
34514
- function clearSpawnMarker(ocrDir) {
34631
+
34632
+ // ../shared/persistence/src/state/phase-graph.ts
34633
+ var REVIEW_PHASE_NUMBERS = {
34634
+ context: 1,
34635
+ "change-context": 2,
34636
+ analysis: 3,
34637
+ reviews: 4,
34638
+ aggregation: 5,
34639
+ discourse: 6,
34640
+ synthesis: 7,
34641
+ complete: 8
34642
+ };
34643
+ var MAP_PHASE_NUMBERS = {
34644
+ "map-context": 1,
34645
+ topology: 2,
34646
+ "flow-analysis": 3,
34647
+ "requirements-mapping": 4,
34648
+ synthesis: 5,
34649
+ complete: 6
34650
+ };
34651
+
34652
+ // ../shared/persistence/src/state/forward-resume.ts
34653
+ var FORWARD_RESUME_KIND = "forward_resume";
34654
+ var FORWARD_RESUME_EXHAUSTED_REASON = "forward_resume_exhausted";
34655
+ function parseLeaseMetadata(e) {
34656
+ if (e.event_type !== "session_resumed" || !e.metadata) return null;
34515
34657
  try {
34516
- unlinkSync3(spawnMarkerPath(ocrDir));
34658
+ return JSON.parse(e.metadata);
34517
34659
  } catch {
34660
+ return null;
34518
34661
  }
34519
34662
  }
34520
- function getActiveCommands() {
34521
- return Array.from(activeCommands.values()).map((entry) => ({
34522
- execution_id: entry.executionId,
34523
- command: entry.commandStr,
34524
- started_at: entry.startedAt,
34525
- output: entry.outputBuffer
34526
- }));
34663
+ function remainingPhasesAfter(workflowType, currentPhase) {
34664
+ const numbers = workflowType === "map" ? MAP_PHASE_NUMBERS : REVIEW_PHASE_NUMBERS;
34665
+ const cur = numbers[currentPhase];
34666
+ if (cur === void 0) return [];
34667
+ return Object.entries(numbers).filter(([, n]) => n > cur).sort((a, b) => a[1] - b[1]).map(([phase]) => phase);
34668
+ }
34669
+ function hasTerminalArtifactEvent2(events, workflowType, round) {
34670
+ const terminal = workflowType === "map" ? "map_completed" : "round_completed";
34671
+ return events.some((e) => e.event_type === terminal && e.round === round);
34672
+ }
34673
+ function countForwardResumeLeases(events, round) {
34674
+ return events.filter((e) => parseLeaseMetadata(e)?.kind === FORWARD_RESUME_KIND && parseLeaseMetadata(e)?.round === round).length;
34675
+ }
34676
+ function strandedActionByCap(db, session, maxAttempts) {
34677
+ const events = getEventsForSession(db, session.id);
34678
+ const leaseCount = countForwardResumeLeases(events, session.current_round);
34679
+ const workflowType = session.workflow_type === "map" ? "map" : "review";
34680
+ return {
34681
+ action: leaseCount >= maxAttempts ? "abort_or_fresh" : "forward_resume",
34682
+ remainingPhases: remainingPhasesAfter(workflowType, session.current_phase),
34683
+ attemptsRemaining: Math.max(0, maxAttempts - leaseCount)
34684
+ };
34685
+ }
34686
+ function closeForwardResumeExhausted(db, sessionId, attempts) {
34687
+ commitReasonClose(
34688
+ db,
34689
+ sessionId,
34690
+ {
34691
+ event_type: "session_auto_closed_stale",
34692
+ phase: "complete",
34693
+ metadata: JSON.stringify({
34694
+ reason: FORWARD_RESUME_EXHAUSTED_REASON,
34695
+ attempts
34696
+ })
34697
+ },
34698
+ { status: "closed", current_phase: "complete" }
34699
+ );
34700
+ }
34701
+
34702
+ // ../shared/persistence/src/state/projection.ts
34703
+ var REASON_EVENT_TYPES = [
34704
+ "session_aborted",
34705
+ "session_auto_closed_stale",
34706
+ "session_synced",
34707
+ "session_legacy_import"
34708
+ ];
34709
+ var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
34710
+ "session_closed",
34711
+ ...REASON_EVENT_TYPES
34712
+ ]);
34713
+ function hasCompletionInvariant(db, session) {
34714
+ const eventType = session.workflow_type === "map" ? "map_completed" : "round_completed";
34715
+ const round = session.workflow_type === "map" ? session.current_map_run : session.current_round;
34716
+ const r = db.exec(
34717
+ `SELECT 1 FROM orchestration_events
34718
+ WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
34719
+ [session.id, eventType, round]
34720
+ );
34721
+ return (r[0]?.values.length ?? 0) > 0;
34722
+ }
34723
+
34724
+ // ../shared/persistence/src/state/index.ts
34725
+ async function stateClose(params) {
34726
+ const { sessionId, ocrDir, abort } = params;
34727
+ const db = await ensureDatabase(ocrDir);
34728
+ const existing = getSession(db, sessionId);
34729
+ if (!existing) {
34730
+ throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
34731
+ }
34732
+ if (existing.status === "closed") {
34733
+ console.error(`[ocr] Session already closed: ${sessionId}`);
34734
+ return;
34735
+ }
34736
+ if (!abort && !hasCompletionInvariant(db, existing)) {
34737
+ 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`;
34738
+ throw new StateError(
34739
+ STATE_EXIT.INVARIANT_UNMET,
34740
+ `Cannot close session ${sessionId}: ${what}. Run 'ocr state complete-round' to finalize it, or pass --abort to record an abandoned session.`
34741
+ );
34742
+ }
34743
+ const note = "closed by parent workflow close";
34744
+ db.transaction(() => {
34745
+ if (abort) {
34746
+ insertEvent(db, {
34747
+ session_id: sessionId,
34748
+ event_type: "session_aborted",
34749
+ phase: existing.current_phase,
34750
+ phase_number: existing.phase_number,
34751
+ round: existing.current_round
34752
+ });
34753
+ }
34754
+ updateSession(db, sessionId, {
34755
+ status: "closed",
34756
+ current_phase: "complete"
34757
+ });
34758
+ if (!abort) {
34759
+ insertEvent(db, {
34760
+ session_id: sessionId,
34761
+ event_type: "session_closed",
34762
+ phase: "complete",
34763
+ phase_number: existing.phase_number,
34764
+ round: existing.current_round
34765
+ });
34766
+ }
34767
+ cascadeTerminateExecutions(db, sessionId, CASCADE_CLOSE_EXIT_CODE, note);
34768
+ });
34769
+ }
34770
+ async function reconcileWorkflowOnExit(ocrDir, sessionId, db) {
34771
+ db ??= await ensureDatabase(ocrDir);
34772
+ const existing = getSession(db, sessionId);
34773
+ if (!existing) return "not-found";
34774
+ if (existing.status === "closed") return "already-closed";
34775
+ if (!hasCompletionInvariant(db, existing)) return "incomplete";
34776
+ if (hasInFlightDependents(db, sessionId)) return "in-flight";
34777
+ await stateClose({ sessionId, ocrDir, abort: false });
34778
+ return "closed";
34779
+ }
34780
+ async function reconcileCompletedSessions(ocrDir) {
34781
+ const db = await ensureDatabase(ocrDir);
34782
+ const closed = [];
34783
+ for (const s of getAllSessions(db)) {
34784
+ if (s.status !== "active") continue;
34785
+ const outcome = await reconcileWorkflowOnExit(ocrDir, s.id, db);
34786
+ if (outcome === "closed") closed.push(s.id);
34787
+ }
34788
+ return closed;
34789
+ }
34790
+
34791
+ // src/server/services/command-outcome.ts
34792
+ function deriveCommandOutcome(exitCode, completeness) {
34793
+ if (exitCode === null) return null;
34794
+ if (exitCode === CANCELLED_EXIT_CODE || exitCode === CASCADE_CLOSE_EXIT_CODE) {
34795
+ return "cancelled";
34796
+ }
34797
+ if (exitCode === WATCHDOG_DEADLINE_EXIT_CODE) return "failed";
34798
+ if (exitCode !== 0) return "failed";
34799
+ if (completeness === null || completeness === "complete") return "success";
34800
+ return "incomplete";
34801
+ }
34802
+ function deriveCancellationReason(exitCode) {
34803
+ if (exitCode === CANCELLED_EXIT_CODE) return "user";
34804
+ if (exitCode === CASCADE_CLOSE_EXIT_CODE) return "cascade";
34805
+ return null;
34806
+ }
34807
+ function getWorkflowCompletenessForExecution(db, executionId) {
34808
+ const result = db.exec(
34809
+ `SELECT sc.completeness_state
34810
+ FROM command_executions ce
34811
+ LEFT JOIN session_completeness sc ON sc.session_id = ce.workflow_id
34812
+ WHERE ce.id = ?`,
34813
+ [executionId]
34814
+ );
34815
+ const row = result[0]?.values[0];
34816
+ if (!row) return null;
34817
+ const state = row[0];
34818
+ if (state === "complete" || state === "closed_without_artifact" || state === "in_flight" || state === "open_no_artifact") {
34819
+ return state;
34820
+ }
34821
+ return null;
34822
+ }
34823
+
34824
+ // src/server/socket/finalizer.ts
34825
+ function tryClaimFinalization(entry) {
34826
+ if (!entry) return true;
34827
+ if (entry.finalized) return false;
34828
+ entry.finalized = true;
34829
+ if (entry.watchdog) {
34830
+ clearInterval(entry.watchdog);
34831
+ entry.watchdog = void 0;
34832
+ }
34833
+ if (entry.tailer) {
34834
+ entry.tailer.stop();
34835
+ entry.tailer = void 0;
34836
+ }
34837
+ return true;
34838
+ }
34839
+ function finishExecution(io2, db, ocrDir, executionId, rawCode, output) {
34840
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
34841
+ const entry = activeCommands.get(executionId);
34842
+ const code = entry?.cancelled ? CANCELLED_EXIT_CODE : rawCode;
34843
+ if (!tryClaimFinalization(entry)) return;
34844
+ const res = db.prepare(
34845
+ `UPDATE command_executions
34846
+ SET exit_code = ?, finished_at = ?, output = ?, pid = NULL
34847
+ WHERE id = ? AND finished_at IS NULL`
34848
+ ).run(code, finishedAt, output, executionId);
34849
+ if (Number(res.changes) === 0 && !entry) return;
34850
+ const completeness = getWorkflowCompletenessForExecution(db, executionId);
34851
+ const outcome = deriveCommandOutcome(code, completeness);
34852
+ const cancellationReason = deriveCancellationReason(code);
34853
+ if (entry?.uid) {
34854
+ appendCommandLog(ocrDir, {
34855
+ v: 1,
34856
+ uid: entry.uid,
34857
+ db_id: executionId,
34858
+ command: entry.commandStr,
34859
+ args: entry.argsJson ?? null,
34860
+ exit_code: code,
34861
+ started_at: entry.startedAt,
34862
+ finished_at: finishedAt,
34863
+ is_detached: entry.detached ? 1 : 0,
34864
+ event: code === CANCELLED_EXIT_CODE ? "cancel" : "finish",
34865
+ writer: "dashboard"
34866
+ });
34867
+ }
34868
+ io2.emit("command:finished", {
34869
+ execution_id: executionId,
34870
+ exitCode: code,
34871
+ finished_at: finishedAt,
34872
+ outcome,
34873
+ cancellation_reason: cancellationReason
34874
+ });
34875
+ activeCommands.delete(executionId);
34876
+ const workflowRow = db.exec(
34877
+ "SELECT workflow_id FROM command_executions WHERE id = ?",
34878
+ [executionId]
34879
+ );
34880
+ const workflowId = workflowRow[0]?.values[0]?.[0];
34881
+ if (typeof workflowId === "string" && workflowId.length > 0) {
34882
+ void reconcileWorkflowOnExit(ocrDir, workflowId, db).then((outcome2) => {
34883
+ if (outcome2 === "closed") {
34884
+ console.log(`[command-runner] auto-finalized workflow ${workflowId}`);
34885
+ } else if (outcome2 === "incomplete" || outcome2 === "in-flight") {
34886
+ console.debug(
34887
+ `[command-runner] workflow ${workflowId} not finalized: ${outcome2}`
34888
+ );
34889
+ }
34890
+ }).catch((err) => {
34891
+ console.error(
34892
+ `[command-runner] reconcileWorkflowOnExit(${workflowId}) failed:`,
34893
+ err instanceof Error ? err.message : err
34894
+ );
34895
+ });
34896
+ }
34527
34897
  }
34898
+
34899
+ // src/server/socket/command-runner.ts
34900
+ var ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
34901
+ "progress",
34902
+ "state"
34903
+ ]);
34904
+ var AI_COMMANDS = /* @__PURE__ */ new Set(["map", "review", "translate-review-to-single-human", "address", "create-reviewer", "sync-reviewers"]);
34528
34905
  function registerCommandHandlers(io2, socket, db, ocrDir, aiCliService, sessionCapture) {
34529
34906
  socket.on("command:run", (payload) => {
34530
34907
  try {
@@ -34697,15 +35074,14 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34697
35074
  finishExecution(io2, db, ocrDir, executionId, 1, content);
34698
35075
  return;
34699
35076
  }
35077
+ let capabilityWarning = null;
34700
35078
  if (adapter.supportsPerTaskModel === false) {
34701
35079
  const perInstanceModels = extractPerInstanceModels(subArgs);
34702
35080
  if (perInstanceModels.length > 0) {
34703
- const warning = `[ocr] Warning: ${adapter.name} does not support per-subagent model overrides. The configured per-instance models (${perInstanceModels.join(", ")}) will be ignored \u2014 all reviewers will run on the parent process model.
34704
- `;
34705
- io2.emit("command:output", { execution_id: executionId, content: warning });
35081
+ capabilityWarning = `[ocr] Warning: ${adapter.name} does not support per-subagent model overrides. The configured per-instance models (${perInstanceModels.join(", ")}) will be ignored \u2014 all reviewers will run on the parent process model.`;
34706
35082
  }
34707
35083
  }
34708
- const commandMdPath = join12(ocrDir, "commands", `${baseCommand}.md`);
35084
+ const commandMdPath = join13(ocrDir, "commands", `${baseCommand}.md`);
34709
35085
  let commandContent;
34710
35086
  try {
34711
35087
  commandContent = readFileSync5(commandMdPath, "utf-8");
@@ -34756,9 +35132,9 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34756
35132
  let logFile;
34757
35133
  if (entry.uid) {
34758
35134
  try {
34759
- const logDir = join12(ocrDir, "data", "exec-logs");
34760
- mkdirSync5(logDir, { recursive: true });
34761
- logFile = join12(logDir, `${entry.uid}.log`);
35135
+ const logDir = join13(ocrDir, "data", "exec-logs");
35136
+ mkdirSync6(logDir, { recursive: true });
35137
+ logFile = join13(logDir, `${entry.uid}.log`);
34762
35138
  } catch (err) {
34763
35139
  console.error(
34764
35140
  "[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):",
@@ -34814,20 +35190,7 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34814
35190
  }
34815
35191
  }, POLL_INTERVAL_MS);
34816
35192
  entry.linkPoll = linkPoll;
34817
- const bumpHeartbeat = () => {
34818
- if (entry.finalized) return;
34819
- const now = Date.now();
34820
- if (now - (entry.lastBeatWrite ?? 0) < HEARTBEAT_THROTTLE_MS) return;
34821
- entry.lastBeatWrite = now;
34822
- try {
34823
- db.run(
34824
- `UPDATE command_executions SET last_heartbeat_at = datetime('now') WHERE id = ? AND finished_at IS NULL`,
34825
- [executionId]
34826
- );
34827
- } catch (err) {
34828
- console.error("[command-runner] heartbeat bump failed:", err);
34829
- }
34830
- };
35193
+ const bumpHeartbeat = makeHeartbeatBumper(db, executionId, entry);
34831
35194
  const hardDeadlineMs = getWorkflowHardDeadlineMs(ocrDir);
34832
35195
  entry.watchdog = setInterval(() => {
34833
35196
  if (entry.finalized) return;
@@ -34859,6 +35222,12 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34859
35222
  `;
34860
35223
  entry.outputBuffer += notice;
34861
35224
  io2.emit("command:output", { execution_id: executionId, content: notice });
35225
+ emitStreamEvent({
35226
+ type: "notice",
35227
+ level: "warning",
35228
+ code: "hard_deadline_reaped",
35229
+ message: notice.trim()
35230
+ });
34862
35231
  } else {
34863
35232
  console.warn(`[watchdog] execution ${executionId}: result seen but no close after grace \u2014 finalizing${decision.reap ? " + reaping tree" : ""}`);
34864
35233
  }
@@ -34866,6 +35235,10 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34866
35235
  finishExecution(io2, db, ocrDir, executionId, decision.exitCode, entry.outputBuffer);
34867
35236
  return;
34868
35237
  }
35238
+ default: {
35239
+ const _exhaustive = decision;
35240
+ throw new Error(`unhandled watchdog action: ${JSON.stringify(_exhaustive)}`);
35241
+ }
34869
35242
  }
34870
35243
  }, WATCHDOG_TICK_MS);
34871
35244
  entry.watchdog.unref();
@@ -34893,6 +35266,16 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34893
35266
  journal.append(stream);
34894
35267
  io2.emit("command:event", stream);
34895
35268
  }
35269
+ if (capabilityWarning) {
35270
+ emitContent(`${capabilityWarning}
35271
+ `);
35272
+ emitStreamEvent({
35273
+ type: "notice",
35274
+ level: "warning",
35275
+ code: "per_instance_model_unsupported",
35276
+ message: capabilityWarning
35277
+ });
35278
+ }
34896
35279
  function handleEvent(evt) {
34897
35280
  switch (evt.type) {
34898
35281
  case "text_delta":
@@ -34976,7 +35359,7 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34976
35359
  clearInterval(entry.linkPoll);
34977
35360
  entry.linkPoll = void 0;
34978
35361
  }
34979
- clearSpawnMarker(ocrDir);
35362
+ clearSpawnMarker(ocrDir, entry.uid);
34980
35363
  if (entry.tailer) {
34981
35364
  entry.tailer.stop();
34982
35365
  entry.tailer = void 0;
@@ -35018,76 +35401,6 @@ ${stderrBuffer}`;
35018
35401
  finishExecution(io2, db, ocrDir, executionId, -1, entry.outputBuffer);
35019
35402
  });
35020
35403
  }
35021
- function finishExecution(io2, db, ocrDir, executionId, rawCode, output) {
35022
- const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
35023
- const entry = activeCommands.get(executionId);
35024
- const code = entry?.cancelled ? CANCELLED_EXIT_CODE : rawCode;
35025
- if (entry?.finalized) return;
35026
- if (entry) {
35027
- entry.finalized = true;
35028
- if (entry.watchdog) {
35029
- clearInterval(entry.watchdog);
35030
- entry.watchdog = void 0;
35031
- }
35032
- if (entry.tailer) {
35033
- entry.tailer.stop();
35034
- entry.tailer = void 0;
35035
- }
35036
- }
35037
- const res = db.prepare(
35038
- `UPDATE command_executions
35039
- SET exit_code = ?, finished_at = ?, output = ?, pid = NULL
35040
- WHERE id = ? AND finished_at IS NULL`
35041
- ).run(code, finishedAt, output, executionId);
35042
- if (Number(res.changes) === 0 && !entry) return;
35043
- const completeness = getWorkflowCompletenessForExecution(db, executionId);
35044
- const outcome = deriveCommandOutcome(code, completeness);
35045
- const cancellationReason = deriveCancellationReason(code);
35046
- if (entry?.uid) {
35047
- appendCommandLog(ocrDir, {
35048
- v: 1,
35049
- uid: entry.uid,
35050
- db_id: executionId,
35051
- command: entry.commandStr,
35052
- args: entry.argsJson ?? null,
35053
- exit_code: code,
35054
- started_at: entry.startedAt,
35055
- finished_at: finishedAt,
35056
- is_detached: entry.detached ? 1 : 0,
35057
- event: code === CANCELLED_EXIT_CODE ? "cancel" : "finish",
35058
- writer: "dashboard"
35059
- });
35060
- }
35061
- io2.emit("command:finished", {
35062
- execution_id: executionId,
35063
- exitCode: code,
35064
- finished_at: finishedAt,
35065
- outcome,
35066
- cancellation_reason: cancellationReason
35067
- });
35068
- activeCommands.delete(executionId);
35069
- const workflowRow = db.exec(
35070
- "SELECT workflow_id FROM command_executions WHERE id = ?",
35071
- [executionId]
35072
- );
35073
- const workflowId = workflowRow[0]?.values[0]?.[0];
35074
- if (typeof workflowId === "string" && workflowId.length > 0) {
35075
- void reconcileWorkflowOnExit(ocrDir, workflowId, db).then((outcome2) => {
35076
- if (outcome2 === "closed") {
35077
- console.log(`[command-runner] auto-finalized workflow ${workflowId}`);
35078
- } else if (outcome2 === "incomplete" || outcome2 === "in-flight") {
35079
- console.debug(
35080
- `[command-runner] workflow ${workflowId} not finalized: ${outcome2}`
35081
- );
35082
- }
35083
- }).catch((err) => {
35084
- console.error(
35085
- `[command-runner] reconcileWorkflowOnExit(${workflowId}) failed:`,
35086
- err instanceof Error ? err.message : err
35087
- );
35088
- });
35089
- }
35090
- }
35091
35404
 
35092
35405
  // src/server/routes/commands.ts
35093
35406
  var AVAILABLE_COMMANDS = [
@@ -35175,7 +35488,7 @@ function createCommandsRouter(db, ocrDir) {
35175
35488
  // src/server/routes/config.ts
35176
35489
  var import_express9 = __toESM(require_express2(), 1);
35177
35490
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
35178
- import { join as join13, dirname as dirname8, basename as basename2 } from "node:path";
35491
+ import { join as join14, dirname as dirname8, basename as basename2 } from "node:path";
35179
35492
  var VALID_IDES = ["vscode", "cursor", "windsurf", "jetbrains", "sublime"];
35180
35493
  function detectIde() {
35181
35494
  const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? "";
@@ -35192,7 +35505,7 @@ function detectIde() {
35192
35505
  }
35193
35506
  function readIdeFromConfig(ocrDir) {
35194
35507
  try {
35195
- const configPath = join13(ocrDir, "config.yaml");
35508
+ const configPath = join14(ocrDir, "config.yaml");
35196
35509
  const content = readFileSync6(configPath, "utf-8");
35197
35510
  const match = content.match(/^\s*ide:\s*(\S+)/m);
35198
35511
  return match?.[1] ?? "auto";
@@ -35239,7 +35552,7 @@ function createConfigRouter(ocrDir, aiCliService) {
35239
35552
  return;
35240
35553
  }
35241
35554
  try {
35242
- const configPath = join13(ocrDir, "config.yaml");
35555
+ const configPath = join14(ocrDir, "config.yaml");
35243
35556
  let content = readFileSync6(configPath, "utf-8");
35244
35557
  if (content.match(/^\s*ide:\s*\S+/m)) {
35245
35558
  content = content.replace(/^(\s*ide:\s*)\S+/m, `$1${ide}`);
@@ -35329,9 +35642,9 @@ function createChatRouter(db) {
35329
35642
  // src/server/routes/reviewers.ts
35330
35643
  var import_express11 = __toESM(require_express2(), 1);
35331
35644
  import { readFileSync as readFileSync7, existsSync as existsSync11, watch } from "node:fs";
35332
- import { join as join14 } from "node:path";
35645
+ import { join as join15 } from "node:path";
35333
35646
  function readReviewersMeta(ocrDir) {
35334
- const metaPath = join14(ocrDir, "reviewers-meta.json");
35647
+ const metaPath = join15(ocrDir, "reviewers-meta.json");
35335
35648
  if (!existsSync11(metaPath)) {
35336
35649
  return { reviewers: [], defaults: [] };
35337
35650
  }
@@ -35360,7 +35673,7 @@ function createReviewersRouter(ocrDir) {
35360
35673
  res.status(400).json({ error: "Invalid reviewer ID" });
35361
35674
  return;
35362
35675
  }
35363
- const filePath = join14(ocrDir, "skills", "references", "reviewers", `${id}.md`);
35676
+ const filePath = join15(ocrDir, "skills", "references", "reviewers", `${id}.md`);
35364
35677
  if (!existsSync11(filePath)) {
35365
35678
  res.status(404).json({ error: "Reviewer not found", id });
35366
35679
  return;
@@ -35375,7 +35688,7 @@ function createReviewersRouter(ocrDir) {
35375
35688
  return router;
35376
35689
  }
35377
35690
  function watchReviewersMeta(ocrDir, io2) {
35378
- const metaPath = join14(ocrDir, "reviewers-meta.json");
35691
+ const metaPath = join15(ocrDir, "reviewers-meta.json");
35379
35692
  let watcher = null;
35380
35693
  let debounce;
35381
35694
  try {
@@ -35451,14 +35764,14 @@ function createHandoffRouter(sessionCapture, ocrDir, syncFromDisk = () => {
35451
35764
 
35452
35765
  // src/server/routes/team.ts
35453
35766
  var import_express14 = __toESM(require_express2(), 1);
35454
- import { spawnSync } from "node:child_process";
35455
35767
 
35456
- // ../cli/src/lib/team-config.ts
35768
+ // ../shared/config/src/team-config.ts
35457
35769
  var import_yaml = __toESM(require_dist(), 1);
35458
35770
  import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
35459
- import { join as join15 } from "node:path";
35771
+ import { join as join16 } from "node:path";
35772
+ var MAX_INSTANCES_PER_PERSONA = 50;
35460
35773
  function loadTeamConfig(ocrDir) {
35461
- const configPath = join15(ocrDir, "config.yaml");
35774
+ const configPath = join16(ocrDir, "config.yaml");
35462
35775
  if (!existsSync12(configPath)) {
35463
35776
  return { team: [], aliases: {}, defaultModel: null };
35464
35777
  }
@@ -35498,6 +35811,9 @@ function parseTeamConfigYaml(content) {
35498
35811
  aliases,
35499
35812
  defaultModel
35500
35813
  );
35814
+ if (resolvedModel !== null) {
35815
+ assertSafeModelId(resolvedModel, `default_team.${persona}[${i}]`);
35816
+ }
35501
35817
  team.push({
35502
35818
  persona,
35503
35819
  instance_index: i + 1,
@@ -35515,6 +35831,11 @@ function parseEntry(persona, entry) {
35515
35831
  `default_team.${persona}: count must be a positive integer (got ${entry})`
35516
35832
  );
35517
35833
  }
35834
+ if (entry > MAX_INSTANCES_PER_PERSONA) {
35835
+ throw new Error(
35836
+ `default_team.${persona}: count must be <= ${MAX_INSTANCES_PER_PERSONA} (got ${entry})`
35837
+ );
35838
+ }
35518
35839
  return Array.from({ length: entry }, () => ({}));
35519
35840
  }
35520
35841
  if (Array.isArray(entry)) {
@@ -35523,6 +35844,11 @@ function parseEntry(persona, entry) {
35523
35844
  `default_team.${persona}: list form must contain at least one instance`
35524
35845
  );
35525
35846
  }
35847
+ if (entry.length > MAX_INSTANCES_PER_PERSONA) {
35848
+ throw new Error(
35849
+ `default_team.${persona}: list form must contain <= ${MAX_INSTANCES_PER_PERSONA} instances (got ${entry.length})`
35850
+ );
35851
+ }
35526
35852
  return entry.map((item, idx) => parseListItem(persona, idx, item));
35527
35853
  }
35528
35854
  if (entry && typeof entry === "object") {
@@ -35544,6 +35870,11 @@ function parseEntry(persona, entry) {
35544
35870
  `default_team.${persona}: count must be a positive integer (got ${String(count)})`
35545
35871
  );
35546
35872
  }
35873
+ if (count > MAX_INSTANCES_PER_PERSONA) {
35874
+ throw new Error(
35875
+ `default_team.${persona}: count must be <= ${MAX_INSTANCES_PER_PERSONA} (got ${count})`
35876
+ );
35877
+ }
35547
35878
  const teamModel = readOptionalString(obj, "model", `default_team.${persona}.model`);
35548
35879
  return Array.from({ length: count }, () => ({ teamModel }));
35549
35880
  }
@@ -35578,6 +35909,16 @@ function readOptionalString(obj, key, pathLabel) {
35578
35909
  }
35579
35910
  return value;
35580
35911
  }
35912
+ var SAFE_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._/:@[\]+-]{0,255}$/;
35913
+ function assertSafeModelId(value, pathLabel) {
35914
+ if (SAFE_MODEL_ID.test(value)) return;
35915
+ const allowed = /[A-Za-z0-9._/:@[\]+-]/;
35916
+ const offending = [...value].find((ch) => !allowed.test(ch));
35917
+ const detail = value.length === 0 ? "empty string" : value.length > 256 ? "longer than 256 characters" : offending !== void 0 ? `contains ${JSON.stringify(offending)}` : `starts with ${JSON.stringify(value[0])}`;
35918
+ throw new Error(
35919
+ `${pathLabel}: model id ${detail} \u2014 no vendor model id uses that. Allowed: letters and digits plus . _ / : @ [ ] + - (max 256 chars).`
35920
+ );
35921
+ }
35581
35922
  function readAliases(root) {
35582
35923
  const models = root["models"];
35583
35924
  if (!models || typeof models !== "object" || Array.isArray(models)) return {};
@@ -35619,12 +35960,15 @@ function resolveTeamComposition(team, override) {
35619
35960
  result.push(inst);
35620
35961
  }
35621
35962
  for (const inst of override) {
35963
+ if (inst.model !== null) {
35964
+ assertSafeModelId(inst.model, `override ${inst.persona}#${inst.instance_index}`);
35965
+ }
35622
35966
  result.push(inst);
35623
35967
  }
35624
35968
  return result;
35625
35969
  }
35626
35970
 
35627
- // ../cli/src/lib/models.ts
35971
+ // ../shared/config/src/models.ts
35628
35972
  function parseOpenCodeModelList(stdout) {
35629
35973
  const models = [];
35630
35974
  for (const rawLine of stdout.split(/\r?\n/)) {
@@ -35758,6 +36102,7 @@ async function listModelsForVendor(vendor) {
35758
36102
  }
35759
36103
 
35760
36104
  // src/server/routes/team.ts
36105
+ import { dirname as dirname10 } from "node:path";
35761
36106
  function isReviewerInstanceArray(input) {
35762
36107
  if (!Array.isArray(input)) return false;
35763
36108
  for (const entry of input) {
@@ -35810,32 +36155,25 @@ function createTeamRouter(ocrDir) {
35810
36155
  return;
35811
36156
  }
35812
36157
  try {
35813
- const result = spawnSync("ocr", ["team", "set", "--stdin"], {
36158
+ execBinary("ocr", ["team", "set", "--stdin"], {
35814
36159
  input: JSON.stringify(body.team),
35815
36160
  encoding: "utf-8",
35816
- cwd: ocrDir.replace(/\/\.ocr$/, ""),
36161
+ // Run from the project root (parent of `.ocr`). `dirname` is
36162
+ // separator-correct on every platform — a prior `/\/\.ocr$/` regex
36163
+ // silently no-op'd on Windows (join builds the path with `\`), running
36164
+ // `ocr team set` inside the `.ocr` dir itself (blocker B2). Matches the
36165
+ // `dirname(ocrDir)` derivation used across the socket handlers.
36166
+ cwd: dirname10(ocrDir),
35817
36167
  timeout: 1e4
35818
36168
  });
35819
- if (result.error) {
35820
- res.status(500).json({
35821
- error: "Failed to invoke ocr team set",
35822
- detail: result.error.message
35823
- });
35824
- return;
35825
- }
35826
- if (result.status !== 0) {
35827
- res.status(500).json({
35828
- error: "ocr team set exited non-zero",
35829
- stderr: result.stderr
35830
- });
35831
- return;
35832
- }
35833
36169
  res.json({ ok: true, team: body.team });
35834
36170
  } catch (err) {
35835
36171
  console.error("Failed to persist team:", err);
36172
+ const e = err;
35836
36173
  res.status(500).json({
35837
36174
  error: "Failed to persist team",
35838
- detail: err instanceof Error ? err.message : String(err)
36175
+ detail: err instanceof Error ? err.message : String(err),
36176
+ ...typeof e.stderr === "string" && e.stderr ? { stderr: e.stderr } : {}
35839
36177
  });
35840
36178
  }
35841
36179
  });
@@ -35952,6 +36290,7 @@ function recoverFromEventsJsonl(ocrDir, db, workflowId) {
35952
36290
  function createSessionCaptureService(deps) {
35953
36291
  const { db, ocrDir, aiCliService } = deps;
35954
36292
  const driftLoggedFor = /* @__PURE__ */ new Set();
36293
+ const rejectLoggedFor = /* @__PURE__ */ new Set();
35955
36294
  function readBoundSessionId(executionId) {
35956
36295
  const result = db.exec(
35957
36296
  "SELECT vendor_session_id FROM command_executions WHERE id = ?",
@@ -35962,6 +36301,15 @@ function createSessionCaptureService(deps) {
35962
36301
  }
35963
36302
  function recordSessionId(executionId, vendorSessionId) {
35964
36303
  try {
36304
+ if (!isSafeVendorSessionId(vendorSessionId)) {
36305
+ if (!rejectLoggedFor.has(executionId)) {
36306
+ rejectLoggedFor.add(executionId);
36307
+ console.warn(
36308
+ `[session-capture] rejecting implausible vendor session id on execution ${executionId}: ${JSON.stringify(vendorSessionId)} (allowed: letters/digits plus . _ : - , max 256 chars). Not recorded \u2014 resume for this execution is unavailable.`
36309
+ );
36310
+ }
36311
+ return;
36312
+ }
35965
36313
  const existing = readBoundSessionId(executionId);
35966
36314
  if (existing === vendorSessionId) return;
35967
36315
  if (existing) {
@@ -36141,7 +36489,7 @@ function buildDiagnostics(input) {
36141
36489
 
36142
36490
  // src/server/services/filesystem-sync.ts
36143
36491
  import { readdirSync as readdirSync2, readFileSync as readFileSync9, statSync as statSync3, existsSync as existsSync13 } from "node:fs";
36144
- import { join as join16, basename as basename3, dirname as dirname10, relative } from "node:path";
36492
+ import { join as join17, basename as basename3, dirname as dirname11, relative } from "node:path";
36145
36493
  import { watch as watch2 } from "chokidar";
36146
36494
 
36147
36495
  // src/server/services/parsers/reviewer-parser.ts
@@ -36236,21 +36584,12 @@ var VERDICT_RE = /^\*?\*?\s*(?:##\s*)?Verdict\s*\*?\*?\s*:?\s*\*?\*?\s*(.*)/im;
36236
36584
  var BLOCKERS_RE = /^\*\*Blockers?\*\*\s*:?\s*(\d+)/im;
36237
36585
  var SHOULD_FIX_RE = /^\*\*Should\s*Fix\*\*\s*:?\s*(\d+)/im;
36238
36586
  var SUGGESTIONS_RE = /^\*\*Suggestions?\*\*\s*:?\s*(\d+)/im;
36239
- var KNOWN_VERDICTS = [
36240
- "REQUEST CHANGES",
36241
- "CHANGES REQUESTED",
36242
- "NEEDS DISCUSSION",
36243
- "NEEDS WORK",
36244
- "APPROVED",
36245
- "APPROVE",
36246
- "LGTM",
36247
- "BLOCK",
36248
- "REJECT"
36249
- ];
36250
- function normalizeVerdict(raw) {
36587
+ function extractVerdictLabel(raw) {
36251
36588
  const cleaned = raw.trim().replace(/^\*+|\*+$/g, "").trim();
36589
+ const canonical = normalizeVerdict(cleaned);
36590
+ if (canonical) return canonical;
36252
36591
  const upper = cleaned.toUpperCase();
36253
- for (const verdict of KNOWN_VERDICTS) {
36592
+ for (const verdict of CANONICAL_VERDICTS) {
36254
36593
  if (upper.startsWith(verdict)) return verdict;
36255
36594
  }
36256
36595
  const truncated = cleaned.split(/\s+[—:.]\s+|\n/, 1)[0] ?? cleaned;
@@ -36262,7 +36601,7 @@ function parseFinalMd(content) {
36262
36601
  if (verdictMatch) {
36263
36602
  const captured = (verdictMatch[1] ?? "").trim();
36264
36603
  if (captured.length > 0) {
36265
- verdict = normalizeVerdict(captured);
36604
+ verdict = extractVerdictLabel(captured);
36266
36605
  }
36267
36606
  }
36268
36607
  const blockerMatch = content.match(BLOCKERS_RE);
@@ -36349,13 +36688,13 @@ var FilesystemSync = class {
36349
36688
  for (const entry of entries) {
36350
36689
  if (!entry.isDirectory()) continue;
36351
36690
  const sessionId = entry.name;
36352
- const sessionDir = join16(this.sessionsDir, sessionId);
36691
+ const sessionDir = join17(this.sessionsDir, sessionId);
36353
36692
  this.syncSession(sessionId, sessionDir);
36354
36693
  }
36355
36694
  }
36356
36695
  syncSession(sessionId, sessionDir) {
36357
36696
  this.ensureSessionRow(sessionId, sessionDir);
36358
- const roundsDir = join16(sessionDir, "rounds");
36697
+ const roundsDir = join17(sessionDir, "rounds");
36359
36698
  if (existsSync13(roundsDir)) {
36360
36699
  const rounds = readdirSync2(roundsDir, { withFileTypes: true });
36361
36700
  for (const roundEntry of rounds) {
@@ -36363,34 +36702,34 @@ var FilesystemSync = class {
36363
36702
  const roundMatch = roundEntry.name.match(/^round-(\d+)$/);
36364
36703
  if (!roundMatch) continue;
36365
36704
  const roundNumber = parseInt(roundMatch[1] ?? "0", 10);
36366
- const roundDir = join16(roundsDir, roundEntry.name);
36367
- const reviewsDir = join16(roundDir, "reviews");
36705
+ const roundDir = join17(roundsDir, roundEntry.name);
36706
+ const reviewsDir = join17(roundDir, "reviews");
36368
36707
  if (existsSync13(reviewsDir)) {
36369
36708
  const reviewFiles = readdirSync2(reviewsDir).filter((f) => f.endsWith(".md"));
36370
36709
  for (const reviewFile of reviewFiles) {
36371
- const filePath = join16(reviewsDir, reviewFile);
36710
+ const filePath = join17(reviewsDir, reviewFile);
36372
36711
  this.processReviewerOutput(sessionId, roundNumber, filePath, reviewFile);
36373
36712
  }
36374
36713
  }
36375
- const roundMetaPath = join16(roundDir, "round-meta.json");
36714
+ const roundMetaPath = join17(roundDir, "round-meta.json");
36376
36715
  if (existsSync13(roundMetaPath)) {
36377
36716
  this.processRoundMeta(sessionId, roundNumber, roundMetaPath);
36378
36717
  }
36379
- const finalPath = join16(roundDir, "final.md");
36718
+ const finalPath = join17(roundDir, "final.md");
36380
36719
  if (existsSync13(finalPath)) {
36381
36720
  this.processFinalMd(sessionId, roundNumber, finalPath);
36382
36721
  }
36383
- const finalHumanPath = join16(roundDir, "final-human.md");
36722
+ const finalHumanPath = join17(roundDir, "final-human.md");
36384
36723
  if (existsSync13(finalHumanPath)) {
36385
36724
  this.processGenericArtifact(sessionId, "final-human", finalHumanPath, roundNumber);
36386
36725
  }
36387
- const discoursePath = join16(roundDir, "discourse.md");
36726
+ const discoursePath = join17(roundDir, "discourse.md");
36388
36727
  if (existsSync13(discoursePath)) {
36389
36728
  this.processGenericArtifact(sessionId, "discourse", discoursePath, roundNumber);
36390
36729
  }
36391
36730
  }
36392
36731
  }
36393
- const mapDir = join16(sessionDir, "map", "runs");
36732
+ const mapDir = join17(sessionDir, "map", "runs");
36394
36733
  if (existsSync13(mapDir)) {
36395
36734
  const runs = readdirSync2(mapDir, { withFileTypes: true });
36396
36735
  for (const runEntry of runs) {
@@ -36398,12 +36737,12 @@ var FilesystemSync = class {
36398
36737
  const runMatch = runEntry.name.match(/^run-(\d+)$/);
36399
36738
  if (!runMatch) continue;
36400
36739
  const runNumber = parseInt(runMatch[1] ?? "0", 10);
36401
- const runDir = join16(mapDir, runEntry.name);
36402
- const mapMetaPath = join16(runDir, "map-meta.json");
36740
+ const runDir = join17(mapDir, runEntry.name);
36741
+ const mapMetaPath = join17(runDir, "map-meta.json");
36403
36742
  if (existsSync13(mapMetaPath)) {
36404
36743
  this.processMapMeta(sessionId, runNumber, mapMetaPath);
36405
36744
  }
36406
- const mapPath = join16(runDir, "map.md");
36745
+ const mapPath = join17(runDir, "map.md");
36407
36746
  if (existsSync13(mapPath)) {
36408
36747
  this.processMapMd(sessionId, runNumber, mapPath);
36409
36748
  }
@@ -36413,7 +36752,7 @@ var FilesystemSync = class {
36413
36752
  ["requirements-mapping.md", "requirements-mapping"]
36414
36753
  ];
36415
36754
  for (const [fileName, artifactType] of mapArtifacts) {
36416
- const filePath = join16(runDir, fileName);
36755
+ const filePath = join17(runDir, fileName);
36417
36756
  if (existsSync13(filePath)) {
36418
36757
  this.processGenericArtifact(sessionId, artifactType, filePath, void 0, runNumber);
36419
36758
  }
@@ -36425,68 +36764,113 @@ var FilesystemSync = class {
36425
36764
  ["discovered-standards.md", "discovered-standards"]
36426
36765
  ];
36427
36766
  for (const [fileName, artifactType] of sessionArtifacts) {
36428
- const filePath = join16(sessionDir, fileName);
36767
+ const filePath = join17(sessionDir, fileName);
36429
36768
  if (existsSync13(filePath)) {
36430
36769
  this.processGenericArtifact(sessionId, artifactType, filePath);
36431
36770
  }
36432
36771
  }
36433
36772
  }
36773
+ // ── Terminal-completion evidence (defect D1) ──
36774
+ //
36775
+ // The dashboard read/sync path NEVER originates terminal workflow completion.
36776
+ // A `final.md` / `map.md` artifact on disk is evidence of the **synthesis**
36777
+ // phase only; terminal completion is the CLI's to declare and is recognized
36778
+ // solely from the CLI-produced evidence — a `round_completed` / `map_completed`
36779
+ // orchestration event. Closing on artifact presence alone is the fabrication
36780
+ // these helpers exist to prevent.
36781
+ /** Whether the CLI has recorded a `round_completed` event for this round. */
36782
+ hasRoundCompletedEvent(sessionId, round) {
36783
+ return queryFirst(
36784
+ this.db,
36785
+ `SELECT 1 FROM orchestration_events
36786
+ WHERE session_id = ? AND event_type = 'round_completed' AND round = ? LIMIT 1`,
36787
+ [sessionId, round]
36788
+ ) != null;
36789
+ }
36790
+ /** Whether the CLI has recorded a `map_completed` event for this map run. */
36791
+ hasMapCompletedEvent(sessionId, mapRun) {
36792
+ return queryFirst(
36793
+ this.db,
36794
+ `SELECT 1 FROM orchestration_events
36795
+ WHERE session_id = ? AND event_type = 'map_completed' AND round = ? LIMIT 1`,
36796
+ [sessionId, mapRun]
36797
+ ) != null;
36798
+ }
36799
+ /**
36800
+ * Full CLI terminal evidence for a review round: a `round_completed` event AND
36801
+ * a validated `round-meta.json` on disk. Used by the backfill reconciler to
36802
+ * decide whether a discovered-on-disk session is genuinely complete.
36803
+ */
36804
+ hasTerminalRoundEvidence(sessionId, round, roundDir) {
36805
+ return existsSync13(join17(roundDir, "round-meta.json")) && this.hasRoundCompletedEvent(sessionId, round);
36806
+ }
36807
+ /** Full CLI terminal evidence for a map run: a `map_completed` event AND a
36808
+ * validated `map-meta.json` on disk. */
36809
+ hasTerminalMapEvidence(sessionId, mapRun, runDir) {
36810
+ return existsSync13(join17(runDir, "map-meta.json")) && this.hasMapCompletedEvent(sessionId, mapRun);
36811
+ }
36434
36812
  // ── Session Backfill ──
36435
36813
  ensureSessionRow(sessionId, sessionDir) {
36436
36814
  const branchMatch = sessionId.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
36437
36815
  const branch = branchMatch?.[1] ?? "unknown";
36438
- const hasRoundsDir = existsSync13(join16(sessionDir, "rounds"));
36439
- const hasMapDir = existsSync13(join16(sessionDir, "map"));
36816
+ const hasRoundsDir = existsSync13(join17(sessionDir, "rounds"));
36817
+ const hasMapDir = existsSync13(join17(sessionDir, "map"));
36440
36818
  const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
36441
36819
  let currentRound = 1;
36442
36820
  if (hasRoundsDir) {
36443
- const roundDirs = readdirSync2(join16(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
36821
+ const roundDirs = readdirSync2(join17(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
36444
36822
  currentRound = Math.max(1, roundDirs.length);
36445
36823
  }
36446
36824
  let currentMapRun = 1;
36447
- const mapRunsDir = join16(sessionDir, "map", "runs");
36825
+ const mapRunsDir = join17(sessionDir, "map", "runs");
36448
36826
  if (existsSync13(mapRunsDir)) {
36449
36827
  const runDirs = readdirSync2(mapRunsDir).filter((d) => d.match(/^run-\d+$/));
36450
36828
  currentMapRun = Math.max(1, runDirs.length);
36451
36829
  }
36452
36830
  let phase = "context";
36453
36831
  let phaseNumber = 1;
36454
- let status = "closed";
36832
+ let status = "active";
36455
36833
  if (workflowType === "review" && hasRoundsDir) {
36456
- const roundDir = join16(sessionDir, "rounds", `round-${currentRound}`);
36457
- if (existsSync13(join16(roundDir, "final.md"))) {
36834
+ const roundDir = join17(sessionDir, "rounds", `round-${currentRound}`);
36835
+ if (existsSync13(join17(roundDir, "final.md")) && this.hasTerminalRoundEvidence(sessionId, currentRound, roundDir)) {
36458
36836
  phase = "complete";
36459
36837
  phaseNumber = 8;
36460
36838
  status = "closed";
36461
- } else if (existsSync13(join16(roundDir, "discourse.md"))) {
36839
+ } else if (existsSync13(join17(roundDir, "final.md"))) {
36462
36840
  phase = "synthesis";
36463
36841
  phaseNumber = 7;
36464
- } else if (existsSync13(join16(roundDir, "reviews")) && readdirSync2(join16(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
36842
+ } else if (existsSync13(join17(roundDir, "discourse.md"))) {
36843
+ phase = "synthesis";
36844
+ phaseNumber = 7;
36845
+ } else if (existsSync13(join17(roundDir, "reviews")) && readdirSync2(join17(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
36465
36846
  phase = "reviews";
36466
36847
  phaseNumber = 4;
36467
- } else if (existsSync13(join16(sessionDir, "context.md"))) {
36848
+ } else if (existsSync13(join17(sessionDir, "context.md"))) {
36468
36849
  phase = "analysis";
36469
36850
  phaseNumber = 3;
36470
- } else if (existsSync13(join16(sessionDir, "discovered-standards.md"))) {
36851
+ } else if (existsSync13(join17(sessionDir, "discovered-standards.md"))) {
36471
36852
  phase = "change-context";
36472
36853
  phaseNumber = 2;
36473
36854
  }
36474
36855
  } else if (workflowType === "map" && hasMapDir) {
36475
- const runDir = join16(mapRunsDir, `run-${currentMapRun}`);
36476
- if (existsSync13(join16(runDir, "map.md"))) {
36856
+ const runDir = join17(mapRunsDir, `run-${currentMapRun}`);
36857
+ if (existsSync13(join17(runDir, "map.md")) && this.hasTerminalMapEvidence(sessionId, currentMapRun, runDir)) {
36477
36858
  phase = "complete";
36478
36859
  phaseNumber = 6;
36479
36860
  status = "closed";
36480
- } else if (existsSync13(join16(runDir, "requirements-mapping.md"))) {
36861
+ } else if (existsSync13(join17(runDir, "map.md"))) {
36481
36862
  phase = "synthesis";
36482
36863
  phaseNumber = 5;
36483
- } else if (existsSync13(join16(runDir, "flow-analysis.md"))) {
36864
+ } else if (existsSync13(join17(runDir, "requirements-mapping.md"))) {
36865
+ phase = "synthesis";
36866
+ phaseNumber = 5;
36867
+ } else if (existsSync13(join17(runDir, "flow-analysis.md"))) {
36484
36868
  phase = "requirements-mapping";
36485
36869
  phaseNumber = 4;
36486
- } else if (existsSync13(join16(runDir, "topology.md"))) {
36870
+ } else if (existsSync13(join17(runDir, "topology.md"))) {
36487
36871
  phase = "flow-analysis";
36488
36872
  phaseNumber = 3;
36489
- } else if (existsSync13(join16(sessionDir, "discovered-standards.md"))) {
36873
+ } else if (existsSync13(join17(sessionDir, "discovered-standards.md"))) {
36490
36874
  phase = "topology";
36491
36875
  phaseNumber = 2;
36492
36876
  }
@@ -36541,7 +36925,7 @@ var FilesystemSync = class {
36541
36925
  try {
36542
36926
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
36543
36927
  if (entry.isDirectory()) {
36544
- if (this.hasArtifacts(join16(dir, entry.name))) return true;
36928
+ if (this.hasArtifacts(join17(dir, entry.name))) return true;
36545
36929
  } else if (/\.(md|json)$/.test(entry.name)) {
36546
36930
  return true;
36547
36931
  }
@@ -36687,7 +37071,7 @@ var FilesystemSync = class {
36687
37071
  "SELECT current_phase, phase_number, workflow_type FROM sessions WHERE id = ?",
36688
37072
  [sessionId]
36689
37073
  );
36690
- if (session && session["workflow_type"] === "map" && (session["current_phase"] !== "complete" || session["phase_number"] < 6)) {
37074
+ if (session && session["workflow_type"] === "map" && this.hasMapCompletedEvent(sessionId, runNumber) && (session["current_phase"] !== "complete" || session["phase_number"] < 6)) {
36691
37075
  commitReasonClose(
36692
37076
  this.db,
36693
37077
  sessionId,
@@ -36845,13 +37229,8 @@ var FilesystemSync = class {
36845
37229
  console.error(`[FilesystemSync] Invalid round-meta.json at ${filePath}`);
36846
37230
  return;
36847
37231
  }
36848
- const allFindings = meta.reviewers.flatMap((r) => r.findings ?? []);
36849
- const sc = meta.synthesis_counts;
36850
- const blockerCount = sc?.blockers ?? allFindings.filter((f) => f.category === "blocker").length;
36851
- const shouldFixCount = sc?.should_fix ?? allFindings.filter((f) => f.category === "should_fix").length;
36852
- const suggestionCount = sc?.suggestions ?? allFindings.filter((f) => f.category === "suggestion").length;
36853
- const reviewerCount = meta.reviewers.length;
36854
- const totalFindingCount = allFindings.length;
37232
+ const normalizedVerdict = normalizeVerdict(meta.verdict) ?? meta.verdict;
37233
+ const { blockerCount, shouldFixCount, suggestionCount, reviewerCount, totalFindingCount } = resolveRoundCounts(meta);
36855
37234
  this.db.run("BEGIN TRANSACTION");
36856
37235
  try {
36857
37236
  this.db.run(
@@ -36860,7 +37239,7 @@ var FilesystemSync = class {
36860
37239
  reviewer_count = ?, total_finding_count = ?, source = 'orchestrator', parsed_at = ?
36861
37240
  WHERE session_id = ? AND round_number = ?`,
36862
37241
  [
36863
- meta.verdict,
37242
+ normalizedVerdict,
36864
37243
  blockerCount,
36865
37244
  suggestionCount,
36866
37245
  shouldFixCount,
@@ -36881,12 +37260,12 @@ var FilesystemSync = class {
36881
37260
  this.db.run("COMMIT");
36882
37261
  return;
36883
37262
  }
36884
- const roundDir = dirname10(filePath);
37263
+ const roundDir = dirname11(filePath);
36885
37264
  for (const reviewer of meta.reviewers) {
36886
37265
  const reviewerType = reviewer.type ?? "unknown";
36887
37266
  const instanceNumber = reviewer.instance ?? 1;
36888
37267
  const findings = reviewer.findings ?? [];
36889
- const reviewerMdPath = join16(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
37268
+ const reviewerMdPath = join17(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
36890
37269
  this.db.run(
36891
37270
  `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
36892
37271
  VALUES (?, ?, ?, ?, ?, ?)`,
@@ -36960,7 +37339,7 @@ var FilesystemSync = class {
36960
37339
  this.io?.to(`session:${sessionId}`).emit("round:updated", {
36961
37340
  sessionId,
36962
37341
  roundNumber,
36963
- verdict: meta.verdict,
37342
+ verdict: normalizedVerdict,
36964
37343
  blockerCount,
36965
37344
  shouldFixCount,
36966
37345
  suggestionCount,
@@ -37121,11 +37500,12 @@ var FilesystemSync = class {
37121
37500
  );
37122
37501
  } else {
37123
37502
  const parsed = parseFinalMd(content);
37503
+ const parsedVerdict = parsed.verdict ? normalizeVerdict(parsed.verdict) ?? parsed.verdict : parsed.verdict;
37124
37504
  this.db.run(
37125
37505
  `UPDATE review_rounds SET verdict = ?, blocker_count = ?, suggestion_count = ?, should_fix_count = ?, final_md_path = ?, parsed_at = ?, source = 'parser'
37126
37506
  WHERE session_id = ? AND round_number = ?`,
37127
37507
  [
37128
- parsed.verdict,
37508
+ parsedVerdict,
37129
37509
  parsed.blockerCount,
37130
37510
  parsed.suggestionCount,
37131
37511
  parsed.shouldFixCount,
@@ -37155,7 +37535,7 @@ var FilesystemSync = class {
37155
37535
  "SELECT current_phase, phase_number, status FROM sessions WHERE id = ?",
37156
37536
  [sessionId]
37157
37537
  );
37158
- if (session && (session["current_phase"] !== "complete" || session["phase_number"] < 8)) {
37538
+ if (session && this.hasRoundCompletedEvent(sessionId, roundNumber) && (session["current_phase"] !== "complete" || session["phase_number"] < 8)) {
37159
37539
  commitReasonClose(
37160
37540
  this.db,
37161
37541
  sessionId,
@@ -37249,7 +37629,7 @@ var FilesystemSync = class {
37249
37629
  const parts = relFromSessions.split("/");
37250
37630
  const sessionId = parts[0];
37251
37631
  if (!sessionId) return;
37252
- const sessionDir = join16(this.sessionsDir, sessionId);
37632
+ const sessionDir = join17(this.sessionsDir, sessionId);
37253
37633
  this.ensureSessionRow(sessionId, sessionDir);
37254
37634
  const fileName = basename3(filePath);
37255
37635
  const reviewerMatch = relFromSessions.match(/rounds\/round-(\d+)\/reviews\/(.+\.md)$/);
@@ -37322,7 +37702,7 @@ var FilesystemSync = class {
37322
37702
 
37323
37703
  // src/server/services/db-sync-watcher.ts
37324
37704
  import { existsSync as existsSync14 } from "node:fs";
37325
- import { dirname as dirname11, basename as basename4 } from "node:path";
37705
+ import { dirname as dirname12, basename as basename4 } from "node:path";
37326
37706
  import { watch as watch3 } from "chokidar";
37327
37707
  function col(row, key) {
37328
37708
  return row[key] ?? null;
@@ -37364,7 +37744,7 @@ var DbSyncWatcher = class {
37364
37744
  /** Start watching the DB file (and its WAL sidecar) for external writes. */
37365
37745
  startWatching() {
37366
37746
  if (!existsSync14(this.dbFilePath)) return;
37367
- const watchDir = dirname11(this.dbFilePath);
37747
+ const watchDir = dirname12(this.dbFilePath);
37368
37748
  const dbFile = basename4(this.dbFilePath);
37369
37749
  const walFile = `${dbFile}-wal`;
37370
37750
  this.watcher = watch3(watchDir, {
@@ -37626,20 +38006,20 @@ function commandFingerprint(row) {
37626
38006
  }
37627
38007
 
37628
38008
  // src/server/socket/chat-handler.ts
37629
- import { dirname as dirname12 } from "node:path";
38009
+ import { dirname as dirname13 } from "node:path";
37630
38010
 
37631
38011
  // src/server/services/chat-context.ts
37632
38012
  import { readFileSync as readFileSync10, readdirSync as readdirSync3, existsSync as existsSync15 } from "node:fs";
37633
- import { join as join17 } from "node:path";
38013
+ import { join as join18 } from "node:path";
37634
38014
  function buildChatContext(ocrDir, target) {
37635
- const sessionsDir = join17(ocrDir, "sessions");
38015
+ const sessionsDir = join18(ocrDir, "sessions");
37636
38016
  if (target.type === "map_run") {
37637
38017
  return buildMapRunContext(sessionsDir, target.sessionId, target.runNumber);
37638
38018
  }
37639
38019
  return buildReviewRoundContext(sessionsDir, target.sessionId, target.roundNumber);
37640
38020
  }
37641
38021
  function buildMapRunContext(sessionsDir, sessionId, runNumber) {
37642
- const mapPath = join17(
38022
+ const mapPath = join18(
37643
38023
  sessionsDir,
37644
38024
  sessionId,
37645
38025
  "map",
@@ -37666,9 +38046,9 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
37666
38046
  return parts.join("\n");
37667
38047
  }
37668
38048
  function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
37669
- const roundDir = join17(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
37670
- const finalPath = join17(roundDir, "final.md");
37671
- const reviewersDir = join17(roundDir, "reviews");
38049
+ const roundDir = join18(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
38050
+ const finalPath = join18(roundDir, "final.md");
38051
+ const reviewersDir = join18(roundDir, "reviews");
37672
38052
  const parts = [
37673
38053
  `You are an expert code reviewer assisting with a code review session.`,
37674
38054
  `You are looking at review round #${roundNumber} for session "${sessionId}".`,
@@ -37685,7 +38065,7 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
37685
38065
  if (existsSync15(reviewersDir)) {
37686
38066
  const files = readdirSync3(reviewersDir).filter((f) => f.endsWith(".md")).sort();
37687
38067
  for (const file of files) {
37688
- const content = readFileSync10(join17(reviewersDir, file), "utf-8");
38068
+ const content = readFileSync10(join18(reviewersDir, file), "utf-8");
37689
38069
  const reviewerName = file.replace(/\.md$/, "");
37690
38070
  parts.push("");
37691
38071
  parts.push(`<reviewer name="${reviewerName}">`);
@@ -37749,7 +38129,7 @@ function startTrackedExecution(io2, db, ocrDir, command, args = []) {
37749
38129
  db.run(
37750
38130
  `UPDATE command_executions
37751
38131
  SET exit_code = ?, finished_at = ?, output = ?, pid = NULL
37752
- WHERE id = ?`,
38132
+ WHERE id = ? AND finished_at IS NULL`,
37753
38133
  [exitCode, finishedAt, outputBuffer, executionId]
37754
38134
  );
37755
38135
  appendCommandLog(ocrDir, {
@@ -37839,7 +38219,7 @@ User: ${message}`;
37839
38219
  });
37840
38220
  return;
37841
38221
  }
37842
- const repoRoot = dirname12(ocrDir);
38222
+ const repoRoot = dirname13(ocrDir);
37843
38223
  const spawnResult = adapter.spawn({
37844
38224
  prompt,
37845
38225
  cwd: repoRoot,
@@ -38015,13 +38395,13 @@ function cleanupAllChats() {
38015
38395
  }
38016
38396
 
38017
38397
  // src/server/socket/post-handler.ts
38018
- import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
38398
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
38019
38399
  import { tmpdir as tmpdir2 } from "node:os";
38020
- import { join as join18, dirname as dirname13, isAbsolute as isAbsolute2 } from "node:path";
38400
+ import { join as join19, dirname as dirname14, isAbsolute as isAbsolute2 } from "node:path";
38021
38401
  import { randomUUID as randomUUID2 } from "node:crypto";
38022
38402
  function resolveSessionDir2(sessionDir, ocrDir) {
38023
38403
  if (isAbsolute2(sessionDir)) return sessionDir;
38024
- return join18(dirname13(ocrDir), sessionDir);
38404
+ return join19(dirname14(ocrDir), sessionDir);
38025
38405
  }
38026
38406
  var BRANCH_PREFIXES = [
38027
38407
  "feat",
@@ -38090,7 +38470,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
38090
38470
  return;
38091
38471
  }
38092
38472
  const branch = session.branch;
38093
- const repoRoot = dirname13(ocrDir);
38473
+ const repoRoot = dirname14(ocrDir);
38094
38474
  try {
38095
38475
  await execBinaryAsync("gh", ["auth", "status"], { env: cleanEnv(), cwd: repoRoot, encoding: "utf-8" });
38096
38476
  } catch {
@@ -38191,16 +38571,16 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
38191
38571
  socket.emit("post:error", { error: "Session not found" });
38192
38572
  return;
38193
38573
  }
38194
- const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join18(ocrDir, "sessions", sessionId);
38195
- const roundDir = join18(sessionDir, "rounds", `round-${roundNumber}`);
38196
- const finalPath = join18(roundDir, "final.md");
38574
+ const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join19(ocrDir, "sessions", sessionId);
38575
+ const roundDir = join19(sessionDir, "rounds", `round-${roundNumber}`);
38576
+ const finalPath = join19(roundDir, "final.md");
38197
38577
  if (!existsSync16(finalPath)) {
38198
38578
  socket.emit("post:error", { error: "final.md not found for this round" });
38199
38579
  return;
38200
38580
  }
38201
- const humanReviewPath = join18(roundDir, "final-human.md");
38202
- const repoRoot = dirname13(ocrDir);
38203
- const commandMdPath = join18(ocrDir, "commands", "translate-review-to-single-human.md");
38581
+ const humanReviewPath = join19(roundDir, "final-human.md");
38582
+ const repoRoot = dirname14(ocrDir);
38583
+ const commandMdPath = join19(ocrDir, "commands", "translate-review-to-single-human.md");
38204
38584
  let commandContent;
38205
38585
  try {
38206
38586
  commandContent = readFileSync11(commandMdPath, "utf-8");
@@ -38360,10 +38740,10 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
38360
38740
  socket.emit("post:save-result", { success: false, error: "Session not found" });
38361
38741
  return;
38362
38742
  }
38363
- const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join18(ocrDir, "sessions", sessionId);
38364
- const roundDir = join18(sessionDir, "rounds", `round-${roundNumber}`);
38365
- mkdirSync6(roundDir, { recursive: true });
38366
- const filePath = join18(roundDir, "final-human.md");
38743
+ const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join19(ocrDir, "sessions", sessionId);
38744
+ const roundDir = join19(sessionDir, "rounds", `round-${roundNumber}`);
38745
+ mkdirSync7(roundDir, { recursive: true });
38746
+ const filePath = join19(roundDir, "final-human.md");
38367
38747
  writeFileSync5(filePath, content, { mode: 420 });
38368
38748
  socket.emit("post:save-result", { success: true });
38369
38749
  } catch (err) {
@@ -38390,14 +38770,14 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
38390
38770
  );
38391
38771
  tracker.appendOutput(`\u25B8 Posting review to PR #${prNumber}...
38392
38772
  `);
38393
- const tmpDir = join18(tmpdir2(), "ocr-post-comments");
38773
+ const tmpDir = join19(tmpdir2(), "ocr-post-comments");
38394
38774
  try {
38395
- mkdirSync6(tmpDir, { recursive: true, mode: 448 });
38775
+ mkdirSync7(tmpDir, { recursive: true, mode: 448 });
38396
38776
  } catch {
38397
38777
  }
38398
- const tmpFile = join18(tmpDir, `${randomUUID2()}.md`);
38778
+ const tmpFile = join19(tmpDir, `${randomUUID2()}.md`);
38399
38779
  writeFileSync5(tmpFile, content, { mode: 384 });
38400
- const repoRoot = dirname13(ocrDir);
38780
+ const repoRoot = dirname14(ocrDir);
38401
38781
  try {
38402
38782
  const { stdout } = await execBinaryAsync(
38403
38783
  "gh",
@@ -38440,9 +38820,67 @@ function cleanupAllPostGenerations() {
38440
38820
  }
38441
38821
  }
38442
38822
 
38823
+ // src/server/services/forward-resume-sweep.ts
38824
+ function hasPositiveDeathEvidence(db, sessionId, isAlive) {
38825
+ const instances = listAgentSessionsForWorkflow(db, sessionId);
38826
+ if (instances.length === 0) return false;
38827
+ return instances.every(
38828
+ (s) => s.ended_at != null || s.pid != null && !isAlive(s.pid)
38829
+ );
38830
+ }
38831
+ function planForwardResume(db, cfg) {
38832
+ const isAlive = cfg.isAlive ?? defaultIsAlive;
38833
+ const plan = [];
38834
+ for (const session of getAllSessions(db)) {
38835
+ if (session.status !== "active") continue;
38836
+ const events = getEventsForSession(db, session.id);
38837
+ const workflowType = session.workflow_type === "map" ? "map" : "review";
38838
+ if (hasTerminalArtifactEvent2(events, workflowType, session.current_round)) {
38839
+ continue;
38840
+ }
38841
+ if (!hasPositiveDeathEvidence(db, session.id, isAlive)) continue;
38842
+ const stranded = strandedActionByCap(db, session, cfg.maxAttempts);
38843
+ if (stranded.action === "abort_or_fresh") {
38844
+ plan.push({ sessionId: session.id, action: "cap_close" });
38845
+ continue;
38846
+ }
38847
+ const latest = getLatestAgentSessionWithVendorId(db, session.id);
38848
+ plan.push({
38849
+ sessionId: session.id,
38850
+ action: latest?.vendor_session_id ? "resume" : "handoff"
38851
+ });
38852
+ }
38853
+ return plan;
38854
+ }
38855
+ function runForwardResumeSweep(deps) {
38856
+ const plan = planForwardResume(deps.db, deps.config);
38857
+ for (const item of plan) {
38858
+ try {
38859
+ if (item.action === "cap_close") {
38860
+ closeForwardResumeExhausted(deps.db, item.sessionId, deps.maxAttempts);
38861
+ deps.log?.(
38862
+ `[ForwardResume] ${item.sessionId}: attempts exhausted \u2192 closed non-success`
38863
+ );
38864
+ } else if (item.action === "resume") {
38865
+ deps.spawnResume(item.sessionId);
38866
+ deps.log?.(`[ForwardResume] ${item.sessionId}: auto-resuming (ocr review --resume)`);
38867
+ } else {
38868
+ deps.log?.(
38869
+ `[ForwardResume] ${item.sessionId}: stranded, no resume adapter \u2014 pick up in terminal`
38870
+ );
38871
+ }
38872
+ } catch (err) {
38873
+ deps.log?.(
38874
+ `[ForwardResume] ${item.sessionId}: ${err instanceof Error ? err.message : String(err)}`
38875
+ );
38876
+ }
38877
+ }
38878
+ return plan;
38879
+ }
38880
+
38443
38881
  // src/server/index.ts
38444
38882
  import { homedir } from "node:os";
38445
- var __dirname3 = dirname14(fileURLToPath3(import.meta.url));
38883
+ var __dirname3 = dirname15(fileURLToPath3(import.meta.url));
38446
38884
  function shortenPath(p) {
38447
38885
  const home = homedir();
38448
38886
  return p.startsWith(home) ? "~" + p.slice(home.length) : p;
@@ -38509,7 +38947,7 @@ if (process.env.NODE_ENV !== "production") {
38509
38947
  function isOcrDashboardProcess(pid) {
38510
38948
  if (process.platform === "win32") return false;
38511
38949
  try {
38512
- const cmd = execFileSync3("ps", ["-p", String(pid), "-o", "command="], {
38950
+ const cmd = execBinary("ps", ["-p", String(pid), "-o", "command="], {
38513
38951
  encoding: "utf-8",
38514
38952
  timeout: 3e3
38515
38953
  }).trim();
@@ -38524,23 +38962,23 @@ async function startServer(options = {}) {
38524
38962
  process.title = "ocr-dashboard";
38525
38963
  const ocrDir = resolveOcrDir();
38526
38964
  const aiCliService = new AiCliService(ocrDir);
38527
- const dbPathForCheckpoint = join19(ocrDir, "data", "ocr.db");
38965
+ const dbPathForCheckpoint = join20(ocrDir, "data", "ocr.db");
38528
38966
  const walResult = walCheckpointTruncate(dbPathForCheckpoint);
38529
38967
  if (walResult === "checkpointed") {
38530
38968
  console.log(" WAL checkpoint: truncated stale write-ahead-log file");
38531
38969
  }
38532
- for (const reaped of reapOrphanDbFiles(join19(ocrDir, "data"))) {
38970
+ for (const reaped of reapOrphanDbFiles(join20(ocrDir, "data"))) {
38533
38971
  console.log(` Orphan reap: removed stale ${reaped}`);
38534
38972
  }
38535
- const staleLogs = reapStaleExecLogs(join19(ocrDir, "data", "exec-logs"));
38973
+ const staleLogs = reapStaleExecLogs(join20(ocrDir, "data", "exec-logs"));
38536
38974
  if (staleLogs.length > 0) {
38537
38975
  console.log(` Exec-log reap: removed ${staleLogs.length} stale agent log(s)`);
38538
38976
  }
38539
38977
  const db = await openDb(ocrDir);
38540
- const dataDir = join19(ocrDir, "data");
38541
- const pidFilePath = join19(dataDir, "dashboard.pid");
38542
- const portFilePath = join19(dataDir, "server-port");
38543
- mkdirSync7(dataDir, { recursive: true });
38978
+ const dataDir = join20(ocrDir, "data");
38979
+ const pidFilePath = join20(dataDir, "dashboard.pid");
38980
+ const portFilePath = join20(dataDir, "server-port");
38981
+ mkdirSync8(dataDir, { recursive: true });
38544
38982
  try {
38545
38983
  unlinkSync5(portFilePath);
38546
38984
  } catch {
@@ -38638,12 +39076,39 @@ async function startServer(options = {}) {
38638
39076
  }
38639
39077
  };
38640
39078
  await reconcileCompleted();
39079
+ const forwardResumeMaxAttempts = getForwardResumeMaxAttempts(ocrDir);
39080
+ const spawnResume = (sessionId) => {
39081
+ const child = spawnBinary("ocr", ["review", "--resume", sessionId], {
39082
+ cwd: ocrDir.replace(/\.ocr$/, "") || process.cwd(),
39083
+ stdio: "ignore",
39084
+ detached: true
39085
+ });
39086
+ child.on("error", (err) => {
39087
+ console.error(`[ForwardResume] spawn failed for ${sessionId}:`, err.message);
39088
+ });
39089
+ child.unref();
39090
+ };
39091
+ const runForwardResume = () => {
39092
+ try {
39093
+ runForwardResumeSweep({
39094
+ db,
39095
+ config: { maxAttempts: forwardResumeMaxAttempts, heartbeatMs: heartbeatSeconds * 1e3 },
39096
+ maxAttempts: forwardResumeMaxAttempts,
39097
+ spawnResume,
39098
+ log: (m) => console.log(` ${m}`)
39099
+ });
39100
+ } catch (err) {
39101
+ console.error("[ForwardResume] sweep failed:", err);
39102
+ }
39103
+ };
39104
+ runForwardResume();
38641
39105
  const SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
38642
39106
  const sweepTimer = setInterval(() => {
38643
39107
  try {
38644
39108
  logAgentSweep(sweepStaleAgentSessions(db, heartbeatSeconds, defaultIsAlive));
38645
39109
  sweepStaleSessions(db, STALE_SESSION_THRESHOLD_SECONDS);
38646
39110
  void reconcileCompleted();
39111
+ runForwardResume();
38647
39112
  } catch (err) {
38648
39113
  console.error("[sweep] periodic sweep failed:", err);
38649
39114
  }
@@ -38679,10 +39144,10 @@ async function startServer(options = {}) {
38679
39144
  app.use("/api/agent-sessions", createAgentSessionsRouter(db, () => pullSync()));
38680
39145
  app.use("/api/sessions", createHandoffRouter(sessionCapture, ocrDir, () => pullSync()));
38681
39146
  app.use("/api/team", createTeamRouter(ocrDir));
38682
- const clientDir = join19(__dirname3, "client");
39147
+ const clientDir = join20(__dirname3, "client");
38683
39148
  if (process.env.NODE_ENV === "production" && existsSync17(clientDir)) {
38684
39149
  app.use(import_express15.default.static(clientDir, { index: false }));
38685
- const indexHtmlPath = join19(clientDir, "index.html");
39150
+ const indexHtmlPath = join20(clientDir, "index.html");
38686
39151
  const rawIndexHtml = existsSync17(indexHtmlPath) ? readFileSync12(indexHtmlPath, "utf-8") : "";
38687
39152
  const tokenScript = `<script>window.__OCR_TOKEN__=${JSON.stringify(AUTH_TOKEN)};</script>`;
38688
39153
  const injectedIndexHtml = rawIndexHtml.replace(
@@ -38702,7 +39167,7 @@ async function startServer(options = {}) {
38702
39167
  registerChatHandlers(io, socket, db, ocrDir, aiCliService);
38703
39168
  registerPostHandlers(io, socket, db, ocrDir, aiCliService);
38704
39169
  });
38705
- const dbFilePath = join19(ocrDir, "data", "ocr.db");
39170
+ const dbFilePath = join20(ocrDir, "data", "ocr.db");
38706
39171
  const dbSyncWatcher = new DbSyncWatcher(
38707
39172
  db,
38708
39173
  dbFilePath,
@@ -38718,7 +39183,7 @@ async function startServer(options = {}) {
38718
39183
  dbSyncWatcher.startWatching();
38719
39184
  pullSync = () => dbSyncWatcher.syncFromDisk();
38720
39185
  console.log(` Watching DB: ${shortenPath(dbFilePath)}`);
38721
- const sessionsDir = join19(ocrDir, "sessions");
39186
+ const sessionsDir = join20(ocrDir, "sessions");
38722
39187
  const fsSync = new FilesystemSync(db, sessionsDir, io);
38723
39188
  await fsSync.fullScan();
38724
39189
  fsSync.startWatching();
@@ -38782,7 +39247,7 @@ async function startServer(options = {}) {
38782
39247
  unlinkSync5(portFilePath);
38783
39248
  } catch {
38784
39249
  }
38785
- clearSpawnMarker(ocrDir);
39250
+ clearAllSpawnMarkers(ocrDir);
38786
39251
  try {
38787
39252
  const activeResult = db.exec(
38788
39253
  "SELECT id, pid FROM command_executions WHERE pid IS NOT NULL AND finished_at IS NULL"