@open-code-review/cli 2.2.1 → 2.3.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 +910 -461
  51. package/dist/index.js +1257 -321
  52. package/package.json +6 -39
  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";
34527
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
+ }
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,13 @@ 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";
35460
35772
  function loadTeamConfig(ocrDir) {
35461
- const configPath = join15(ocrDir, "config.yaml");
35773
+ const configPath = join16(ocrDir, "config.yaml");
35462
35774
  if (!existsSync12(configPath)) {
35463
35775
  return { team: [], aliases: {}, defaultModel: null };
35464
35776
  }
@@ -35498,6 +35810,9 @@ function parseTeamConfigYaml(content) {
35498
35810
  aliases,
35499
35811
  defaultModel
35500
35812
  );
35813
+ if (resolvedModel !== null) {
35814
+ assertSafeModelId(resolvedModel, `default_team.${persona}[${i}]`);
35815
+ }
35501
35816
  team.push({
35502
35817
  persona,
35503
35818
  instance_index: i + 1,
@@ -35578,6 +35893,16 @@ function readOptionalString(obj, key, pathLabel) {
35578
35893
  }
35579
35894
  return value;
35580
35895
  }
35896
+ var SAFE_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._/:@[\]+-]{0,255}$/;
35897
+ function assertSafeModelId(value, pathLabel) {
35898
+ if (SAFE_MODEL_ID.test(value)) return;
35899
+ const allowed = /[A-Za-z0-9._/:@[\]+-]/;
35900
+ const offending = [...value].find((ch) => !allowed.test(ch));
35901
+ 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])}`;
35902
+ throw new Error(
35903
+ `${pathLabel}: model id ${detail} \u2014 no vendor model id uses that. Allowed: letters and digits plus . _ / : @ [ ] + - (max 256 chars).`
35904
+ );
35905
+ }
35581
35906
  function readAliases(root) {
35582
35907
  const models = root["models"];
35583
35908
  if (!models || typeof models !== "object" || Array.isArray(models)) return {};
@@ -35619,12 +35944,15 @@ function resolveTeamComposition(team, override) {
35619
35944
  result.push(inst);
35620
35945
  }
35621
35946
  for (const inst of override) {
35947
+ if (inst.model !== null) {
35948
+ assertSafeModelId(inst.model, `override ${inst.persona}#${inst.instance_index}`);
35949
+ }
35622
35950
  result.push(inst);
35623
35951
  }
35624
35952
  return result;
35625
35953
  }
35626
35954
 
35627
- // ../cli/src/lib/models.ts
35955
+ // ../shared/config/src/models.ts
35628
35956
  function parseOpenCodeModelList(stdout) {
35629
35957
  const models = [];
35630
35958
  for (const rawLine of stdout.split(/\r?\n/)) {
@@ -35758,6 +36086,7 @@ async function listModelsForVendor(vendor) {
35758
36086
  }
35759
36087
 
35760
36088
  // src/server/routes/team.ts
36089
+ import { dirname as dirname10 } from "node:path";
35761
36090
  function isReviewerInstanceArray(input) {
35762
36091
  if (!Array.isArray(input)) return false;
35763
36092
  for (const entry of input) {
@@ -35810,32 +36139,25 @@ function createTeamRouter(ocrDir) {
35810
36139
  return;
35811
36140
  }
35812
36141
  try {
35813
- const result = spawnSync("ocr", ["team", "set", "--stdin"], {
36142
+ execBinary("ocr", ["team", "set", "--stdin"], {
35814
36143
  input: JSON.stringify(body.team),
35815
36144
  encoding: "utf-8",
35816
- cwd: ocrDir.replace(/\/\.ocr$/, ""),
36145
+ // Run from the project root (parent of `.ocr`). `dirname` is
36146
+ // separator-correct on every platform — a prior `/\/\.ocr$/` regex
36147
+ // silently no-op'd on Windows (join builds the path with `\`), running
36148
+ // `ocr team set` inside the `.ocr` dir itself (blocker B2). Matches the
36149
+ // `dirname(ocrDir)` derivation used across the socket handlers.
36150
+ cwd: dirname10(ocrDir),
35817
36151
  timeout: 1e4
35818
36152
  });
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
36153
  res.json({ ok: true, team: body.team });
35834
36154
  } catch (err) {
35835
36155
  console.error("Failed to persist team:", err);
36156
+ const e = err;
35836
36157
  res.status(500).json({
35837
36158
  error: "Failed to persist team",
35838
- detail: err instanceof Error ? err.message : String(err)
36159
+ detail: err instanceof Error ? err.message : String(err),
36160
+ ...typeof e.stderr === "string" && e.stderr ? { stderr: e.stderr } : {}
35839
36161
  });
35840
36162
  }
35841
36163
  });
@@ -35952,6 +36274,7 @@ function recoverFromEventsJsonl(ocrDir, db, workflowId) {
35952
36274
  function createSessionCaptureService(deps) {
35953
36275
  const { db, ocrDir, aiCliService } = deps;
35954
36276
  const driftLoggedFor = /* @__PURE__ */ new Set();
36277
+ const rejectLoggedFor = /* @__PURE__ */ new Set();
35955
36278
  function readBoundSessionId(executionId) {
35956
36279
  const result = db.exec(
35957
36280
  "SELECT vendor_session_id FROM command_executions WHERE id = ?",
@@ -35962,6 +36285,15 @@ function createSessionCaptureService(deps) {
35962
36285
  }
35963
36286
  function recordSessionId(executionId, vendorSessionId) {
35964
36287
  try {
36288
+ if (!isSafeVendorSessionId(vendorSessionId)) {
36289
+ if (!rejectLoggedFor.has(executionId)) {
36290
+ rejectLoggedFor.add(executionId);
36291
+ console.warn(
36292
+ `[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.`
36293
+ );
36294
+ }
36295
+ return;
36296
+ }
35965
36297
  const existing = readBoundSessionId(executionId);
35966
36298
  if (existing === vendorSessionId) return;
35967
36299
  if (existing) {
@@ -36141,7 +36473,7 @@ function buildDiagnostics(input) {
36141
36473
 
36142
36474
  // src/server/services/filesystem-sync.ts
36143
36475
  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";
36476
+ import { join as join17, basename as basename3, dirname as dirname11, relative } from "node:path";
36145
36477
  import { watch as watch2 } from "chokidar";
36146
36478
 
36147
36479
  // src/server/services/parsers/reviewer-parser.ts
@@ -36236,21 +36568,12 @@ var VERDICT_RE = /^\*?\*?\s*(?:##\s*)?Verdict\s*\*?\*?\s*:?\s*\*?\*?\s*(.*)/im;
36236
36568
  var BLOCKERS_RE = /^\*\*Blockers?\*\*\s*:?\s*(\d+)/im;
36237
36569
  var SHOULD_FIX_RE = /^\*\*Should\s*Fix\*\*\s*:?\s*(\d+)/im;
36238
36570
  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) {
36571
+ function extractVerdictLabel(raw) {
36251
36572
  const cleaned = raw.trim().replace(/^\*+|\*+$/g, "").trim();
36573
+ const canonical = normalizeVerdict(cleaned);
36574
+ if (canonical) return canonical;
36252
36575
  const upper = cleaned.toUpperCase();
36253
- for (const verdict of KNOWN_VERDICTS) {
36576
+ for (const verdict of CANONICAL_VERDICTS) {
36254
36577
  if (upper.startsWith(verdict)) return verdict;
36255
36578
  }
36256
36579
  const truncated = cleaned.split(/\s+[—:.]\s+|\n/, 1)[0] ?? cleaned;
@@ -36262,7 +36585,7 @@ function parseFinalMd(content) {
36262
36585
  if (verdictMatch) {
36263
36586
  const captured = (verdictMatch[1] ?? "").trim();
36264
36587
  if (captured.length > 0) {
36265
- verdict = normalizeVerdict(captured);
36588
+ verdict = extractVerdictLabel(captured);
36266
36589
  }
36267
36590
  }
36268
36591
  const blockerMatch = content.match(BLOCKERS_RE);
@@ -36349,13 +36672,13 @@ var FilesystemSync = class {
36349
36672
  for (const entry of entries) {
36350
36673
  if (!entry.isDirectory()) continue;
36351
36674
  const sessionId = entry.name;
36352
- const sessionDir = join16(this.sessionsDir, sessionId);
36675
+ const sessionDir = join17(this.sessionsDir, sessionId);
36353
36676
  this.syncSession(sessionId, sessionDir);
36354
36677
  }
36355
36678
  }
36356
36679
  syncSession(sessionId, sessionDir) {
36357
36680
  this.ensureSessionRow(sessionId, sessionDir);
36358
- const roundsDir = join16(sessionDir, "rounds");
36681
+ const roundsDir = join17(sessionDir, "rounds");
36359
36682
  if (existsSync13(roundsDir)) {
36360
36683
  const rounds = readdirSync2(roundsDir, { withFileTypes: true });
36361
36684
  for (const roundEntry of rounds) {
@@ -36363,34 +36686,34 @@ var FilesystemSync = class {
36363
36686
  const roundMatch = roundEntry.name.match(/^round-(\d+)$/);
36364
36687
  if (!roundMatch) continue;
36365
36688
  const roundNumber = parseInt(roundMatch[1] ?? "0", 10);
36366
- const roundDir = join16(roundsDir, roundEntry.name);
36367
- const reviewsDir = join16(roundDir, "reviews");
36689
+ const roundDir = join17(roundsDir, roundEntry.name);
36690
+ const reviewsDir = join17(roundDir, "reviews");
36368
36691
  if (existsSync13(reviewsDir)) {
36369
36692
  const reviewFiles = readdirSync2(reviewsDir).filter((f) => f.endsWith(".md"));
36370
36693
  for (const reviewFile of reviewFiles) {
36371
- const filePath = join16(reviewsDir, reviewFile);
36694
+ const filePath = join17(reviewsDir, reviewFile);
36372
36695
  this.processReviewerOutput(sessionId, roundNumber, filePath, reviewFile);
36373
36696
  }
36374
36697
  }
36375
- const roundMetaPath = join16(roundDir, "round-meta.json");
36698
+ const roundMetaPath = join17(roundDir, "round-meta.json");
36376
36699
  if (existsSync13(roundMetaPath)) {
36377
36700
  this.processRoundMeta(sessionId, roundNumber, roundMetaPath);
36378
36701
  }
36379
- const finalPath = join16(roundDir, "final.md");
36702
+ const finalPath = join17(roundDir, "final.md");
36380
36703
  if (existsSync13(finalPath)) {
36381
36704
  this.processFinalMd(sessionId, roundNumber, finalPath);
36382
36705
  }
36383
- const finalHumanPath = join16(roundDir, "final-human.md");
36706
+ const finalHumanPath = join17(roundDir, "final-human.md");
36384
36707
  if (existsSync13(finalHumanPath)) {
36385
36708
  this.processGenericArtifact(sessionId, "final-human", finalHumanPath, roundNumber);
36386
36709
  }
36387
- const discoursePath = join16(roundDir, "discourse.md");
36710
+ const discoursePath = join17(roundDir, "discourse.md");
36388
36711
  if (existsSync13(discoursePath)) {
36389
36712
  this.processGenericArtifact(sessionId, "discourse", discoursePath, roundNumber);
36390
36713
  }
36391
36714
  }
36392
36715
  }
36393
- const mapDir = join16(sessionDir, "map", "runs");
36716
+ const mapDir = join17(sessionDir, "map", "runs");
36394
36717
  if (existsSync13(mapDir)) {
36395
36718
  const runs = readdirSync2(mapDir, { withFileTypes: true });
36396
36719
  for (const runEntry of runs) {
@@ -36398,12 +36721,12 @@ var FilesystemSync = class {
36398
36721
  const runMatch = runEntry.name.match(/^run-(\d+)$/);
36399
36722
  if (!runMatch) continue;
36400
36723
  const runNumber = parseInt(runMatch[1] ?? "0", 10);
36401
- const runDir = join16(mapDir, runEntry.name);
36402
- const mapMetaPath = join16(runDir, "map-meta.json");
36724
+ const runDir = join17(mapDir, runEntry.name);
36725
+ const mapMetaPath = join17(runDir, "map-meta.json");
36403
36726
  if (existsSync13(mapMetaPath)) {
36404
36727
  this.processMapMeta(sessionId, runNumber, mapMetaPath);
36405
36728
  }
36406
- const mapPath = join16(runDir, "map.md");
36729
+ const mapPath = join17(runDir, "map.md");
36407
36730
  if (existsSync13(mapPath)) {
36408
36731
  this.processMapMd(sessionId, runNumber, mapPath);
36409
36732
  }
@@ -36413,7 +36736,7 @@ var FilesystemSync = class {
36413
36736
  ["requirements-mapping.md", "requirements-mapping"]
36414
36737
  ];
36415
36738
  for (const [fileName, artifactType] of mapArtifacts) {
36416
- const filePath = join16(runDir, fileName);
36739
+ const filePath = join17(runDir, fileName);
36417
36740
  if (existsSync13(filePath)) {
36418
36741
  this.processGenericArtifact(sessionId, artifactType, filePath, void 0, runNumber);
36419
36742
  }
@@ -36425,68 +36748,113 @@ var FilesystemSync = class {
36425
36748
  ["discovered-standards.md", "discovered-standards"]
36426
36749
  ];
36427
36750
  for (const [fileName, artifactType] of sessionArtifacts) {
36428
- const filePath = join16(sessionDir, fileName);
36751
+ const filePath = join17(sessionDir, fileName);
36429
36752
  if (existsSync13(filePath)) {
36430
36753
  this.processGenericArtifact(sessionId, artifactType, filePath);
36431
36754
  }
36432
36755
  }
36433
36756
  }
36757
+ // ── Terminal-completion evidence (defect D1) ──
36758
+ //
36759
+ // The dashboard read/sync path NEVER originates terminal workflow completion.
36760
+ // A `final.md` / `map.md` artifact on disk is evidence of the **synthesis**
36761
+ // phase only; terminal completion is the CLI's to declare and is recognized
36762
+ // solely from the CLI-produced evidence — a `round_completed` / `map_completed`
36763
+ // orchestration event. Closing on artifact presence alone is the fabrication
36764
+ // these helpers exist to prevent.
36765
+ /** Whether the CLI has recorded a `round_completed` event for this round. */
36766
+ hasRoundCompletedEvent(sessionId, round) {
36767
+ return queryFirst(
36768
+ this.db,
36769
+ `SELECT 1 FROM orchestration_events
36770
+ WHERE session_id = ? AND event_type = 'round_completed' AND round = ? LIMIT 1`,
36771
+ [sessionId, round]
36772
+ ) != null;
36773
+ }
36774
+ /** Whether the CLI has recorded a `map_completed` event for this map run. */
36775
+ hasMapCompletedEvent(sessionId, mapRun) {
36776
+ return queryFirst(
36777
+ this.db,
36778
+ `SELECT 1 FROM orchestration_events
36779
+ WHERE session_id = ? AND event_type = 'map_completed' AND round = ? LIMIT 1`,
36780
+ [sessionId, mapRun]
36781
+ ) != null;
36782
+ }
36783
+ /**
36784
+ * Full CLI terminal evidence for a review round: a `round_completed` event AND
36785
+ * a validated `round-meta.json` on disk. Used by the backfill reconciler to
36786
+ * decide whether a discovered-on-disk session is genuinely complete.
36787
+ */
36788
+ hasTerminalRoundEvidence(sessionId, round, roundDir) {
36789
+ return existsSync13(join17(roundDir, "round-meta.json")) && this.hasRoundCompletedEvent(sessionId, round);
36790
+ }
36791
+ /** Full CLI terminal evidence for a map run: a `map_completed` event AND a
36792
+ * validated `map-meta.json` on disk. */
36793
+ hasTerminalMapEvidence(sessionId, mapRun, runDir) {
36794
+ return existsSync13(join17(runDir, "map-meta.json")) && this.hasMapCompletedEvent(sessionId, mapRun);
36795
+ }
36434
36796
  // ── Session Backfill ──
36435
36797
  ensureSessionRow(sessionId, sessionDir) {
36436
36798
  const branchMatch = sessionId.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
36437
36799
  const branch = branchMatch?.[1] ?? "unknown";
36438
- const hasRoundsDir = existsSync13(join16(sessionDir, "rounds"));
36439
- const hasMapDir = existsSync13(join16(sessionDir, "map"));
36800
+ const hasRoundsDir = existsSync13(join17(sessionDir, "rounds"));
36801
+ const hasMapDir = existsSync13(join17(sessionDir, "map"));
36440
36802
  const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
36441
36803
  let currentRound = 1;
36442
36804
  if (hasRoundsDir) {
36443
- const roundDirs = readdirSync2(join16(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
36805
+ const roundDirs = readdirSync2(join17(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
36444
36806
  currentRound = Math.max(1, roundDirs.length);
36445
36807
  }
36446
36808
  let currentMapRun = 1;
36447
- const mapRunsDir = join16(sessionDir, "map", "runs");
36809
+ const mapRunsDir = join17(sessionDir, "map", "runs");
36448
36810
  if (existsSync13(mapRunsDir)) {
36449
36811
  const runDirs = readdirSync2(mapRunsDir).filter((d) => d.match(/^run-\d+$/));
36450
36812
  currentMapRun = Math.max(1, runDirs.length);
36451
36813
  }
36452
36814
  let phase = "context";
36453
36815
  let phaseNumber = 1;
36454
- let status = "closed";
36816
+ let status = "active";
36455
36817
  if (workflowType === "review" && hasRoundsDir) {
36456
- const roundDir = join16(sessionDir, "rounds", `round-${currentRound}`);
36457
- if (existsSync13(join16(roundDir, "final.md"))) {
36818
+ const roundDir = join17(sessionDir, "rounds", `round-${currentRound}`);
36819
+ if (existsSync13(join17(roundDir, "final.md")) && this.hasTerminalRoundEvidence(sessionId, currentRound, roundDir)) {
36458
36820
  phase = "complete";
36459
36821
  phaseNumber = 8;
36460
36822
  status = "closed";
36461
- } else if (existsSync13(join16(roundDir, "discourse.md"))) {
36823
+ } else if (existsSync13(join17(roundDir, "final.md"))) {
36824
+ phase = "synthesis";
36825
+ phaseNumber = 7;
36826
+ } else if (existsSync13(join17(roundDir, "discourse.md"))) {
36462
36827
  phase = "synthesis";
36463
36828
  phaseNumber = 7;
36464
- } else if (existsSync13(join16(roundDir, "reviews")) && readdirSync2(join16(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
36829
+ } else if (existsSync13(join17(roundDir, "reviews")) && readdirSync2(join17(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
36465
36830
  phase = "reviews";
36466
36831
  phaseNumber = 4;
36467
- } else if (existsSync13(join16(sessionDir, "context.md"))) {
36832
+ } else if (existsSync13(join17(sessionDir, "context.md"))) {
36468
36833
  phase = "analysis";
36469
36834
  phaseNumber = 3;
36470
- } else if (existsSync13(join16(sessionDir, "discovered-standards.md"))) {
36835
+ } else if (existsSync13(join17(sessionDir, "discovered-standards.md"))) {
36471
36836
  phase = "change-context";
36472
36837
  phaseNumber = 2;
36473
36838
  }
36474
36839
  } else if (workflowType === "map" && hasMapDir) {
36475
- const runDir = join16(mapRunsDir, `run-${currentMapRun}`);
36476
- if (existsSync13(join16(runDir, "map.md"))) {
36840
+ const runDir = join17(mapRunsDir, `run-${currentMapRun}`);
36841
+ if (existsSync13(join17(runDir, "map.md")) && this.hasTerminalMapEvidence(sessionId, currentMapRun, runDir)) {
36477
36842
  phase = "complete";
36478
36843
  phaseNumber = 6;
36479
36844
  status = "closed";
36480
- } else if (existsSync13(join16(runDir, "requirements-mapping.md"))) {
36845
+ } else if (existsSync13(join17(runDir, "map.md"))) {
36846
+ phase = "synthesis";
36847
+ phaseNumber = 5;
36848
+ } else if (existsSync13(join17(runDir, "requirements-mapping.md"))) {
36481
36849
  phase = "synthesis";
36482
36850
  phaseNumber = 5;
36483
- } else if (existsSync13(join16(runDir, "flow-analysis.md"))) {
36851
+ } else if (existsSync13(join17(runDir, "flow-analysis.md"))) {
36484
36852
  phase = "requirements-mapping";
36485
36853
  phaseNumber = 4;
36486
- } else if (existsSync13(join16(runDir, "topology.md"))) {
36854
+ } else if (existsSync13(join17(runDir, "topology.md"))) {
36487
36855
  phase = "flow-analysis";
36488
36856
  phaseNumber = 3;
36489
- } else if (existsSync13(join16(sessionDir, "discovered-standards.md"))) {
36857
+ } else if (existsSync13(join17(sessionDir, "discovered-standards.md"))) {
36490
36858
  phase = "topology";
36491
36859
  phaseNumber = 2;
36492
36860
  }
@@ -36541,7 +36909,7 @@ var FilesystemSync = class {
36541
36909
  try {
36542
36910
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
36543
36911
  if (entry.isDirectory()) {
36544
- if (this.hasArtifacts(join16(dir, entry.name))) return true;
36912
+ if (this.hasArtifacts(join17(dir, entry.name))) return true;
36545
36913
  } else if (/\.(md|json)$/.test(entry.name)) {
36546
36914
  return true;
36547
36915
  }
@@ -36687,7 +37055,7 @@ var FilesystemSync = class {
36687
37055
  "SELECT current_phase, phase_number, workflow_type FROM sessions WHERE id = ?",
36688
37056
  [sessionId]
36689
37057
  );
36690
- if (session && session["workflow_type"] === "map" && (session["current_phase"] !== "complete" || session["phase_number"] < 6)) {
37058
+ if (session && session["workflow_type"] === "map" && this.hasMapCompletedEvent(sessionId, runNumber) && (session["current_phase"] !== "complete" || session["phase_number"] < 6)) {
36691
37059
  commitReasonClose(
36692
37060
  this.db,
36693
37061
  sessionId,
@@ -36845,13 +37213,8 @@ var FilesystemSync = class {
36845
37213
  console.error(`[FilesystemSync] Invalid round-meta.json at ${filePath}`);
36846
37214
  return;
36847
37215
  }
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;
37216
+ const normalizedVerdict = normalizeVerdict(meta.verdict) ?? meta.verdict;
37217
+ const { blockerCount, shouldFixCount, suggestionCount, reviewerCount, totalFindingCount } = resolveRoundCounts(meta);
36855
37218
  this.db.run("BEGIN TRANSACTION");
36856
37219
  try {
36857
37220
  this.db.run(
@@ -36860,7 +37223,7 @@ var FilesystemSync = class {
36860
37223
  reviewer_count = ?, total_finding_count = ?, source = 'orchestrator', parsed_at = ?
36861
37224
  WHERE session_id = ? AND round_number = ?`,
36862
37225
  [
36863
- meta.verdict,
37226
+ normalizedVerdict,
36864
37227
  blockerCount,
36865
37228
  suggestionCount,
36866
37229
  shouldFixCount,
@@ -36881,12 +37244,12 @@ var FilesystemSync = class {
36881
37244
  this.db.run("COMMIT");
36882
37245
  return;
36883
37246
  }
36884
- const roundDir = dirname10(filePath);
37247
+ const roundDir = dirname11(filePath);
36885
37248
  for (const reviewer of meta.reviewers) {
36886
37249
  const reviewerType = reviewer.type ?? "unknown";
36887
37250
  const instanceNumber = reviewer.instance ?? 1;
36888
37251
  const findings = reviewer.findings ?? [];
36889
- const reviewerMdPath = join16(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
37252
+ const reviewerMdPath = join17(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
36890
37253
  this.db.run(
36891
37254
  `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
36892
37255
  VALUES (?, ?, ?, ?, ?, ?)`,
@@ -36960,7 +37323,7 @@ var FilesystemSync = class {
36960
37323
  this.io?.to(`session:${sessionId}`).emit("round:updated", {
36961
37324
  sessionId,
36962
37325
  roundNumber,
36963
- verdict: meta.verdict,
37326
+ verdict: normalizedVerdict,
36964
37327
  blockerCount,
36965
37328
  shouldFixCount,
36966
37329
  suggestionCount,
@@ -37121,11 +37484,12 @@ var FilesystemSync = class {
37121
37484
  );
37122
37485
  } else {
37123
37486
  const parsed = parseFinalMd(content);
37487
+ const parsedVerdict = parsed.verdict ? normalizeVerdict(parsed.verdict) ?? parsed.verdict : parsed.verdict;
37124
37488
  this.db.run(
37125
37489
  `UPDATE review_rounds SET verdict = ?, blocker_count = ?, suggestion_count = ?, should_fix_count = ?, final_md_path = ?, parsed_at = ?, source = 'parser'
37126
37490
  WHERE session_id = ? AND round_number = ?`,
37127
37491
  [
37128
- parsed.verdict,
37492
+ parsedVerdict,
37129
37493
  parsed.blockerCount,
37130
37494
  parsed.suggestionCount,
37131
37495
  parsed.shouldFixCount,
@@ -37155,7 +37519,7 @@ var FilesystemSync = class {
37155
37519
  "SELECT current_phase, phase_number, status FROM sessions WHERE id = ?",
37156
37520
  [sessionId]
37157
37521
  );
37158
- if (session && (session["current_phase"] !== "complete" || session["phase_number"] < 8)) {
37522
+ if (session && this.hasRoundCompletedEvent(sessionId, roundNumber) && (session["current_phase"] !== "complete" || session["phase_number"] < 8)) {
37159
37523
  commitReasonClose(
37160
37524
  this.db,
37161
37525
  sessionId,
@@ -37249,7 +37613,7 @@ var FilesystemSync = class {
37249
37613
  const parts = relFromSessions.split("/");
37250
37614
  const sessionId = parts[0];
37251
37615
  if (!sessionId) return;
37252
- const sessionDir = join16(this.sessionsDir, sessionId);
37616
+ const sessionDir = join17(this.sessionsDir, sessionId);
37253
37617
  this.ensureSessionRow(sessionId, sessionDir);
37254
37618
  const fileName = basename3(filePath);
37255
37619
  const reviewerMatch = relFromSessions.match(/rounds\/round-(\d+)\/reviews\/(.+\.md)$/);
@@ -37322,7 +37686,7 @@ var FilesystemSync = class {
37322
37686
 
37323
37687
  // src/server/services/db-sync-watcher.ts
37324
37688
  import { existsSync as existsSync14 } from "node:fs";
37325
- import { dirname as dirname11, basename as basename4 } from "node:path";
37689
+ import { dirname as dirname12, basename as basename4 } from "node:path";
37326
37690
  import { watch as watch3 } from "chokidar";
37327
37691
  function col(row, key) {
37328
37692
  return row[key] ?? null;
@@ -37364,7 +37728,7 @@ var DbSyncWatcher = class {
37364
37728
  /** Start watching the DB file (and its WAL sidecar) for external writes. */
37365
37729
  startWatching() {
37366
37730
  if (!existsSync14(this.dbFilePath)) return;
37367
- const watchDir = dirname11(this.dbFilePath);
37731
+ const watchDir = dirname12(this.dbFilePath);
37368
37732
  const dbFile = basename4(this.dbFilePath);
37369
37733
  const walFile = `${dbFile}-wal`;
37370
37734
  this.watcher = watch3(watchDir, {
@@ -37626,20 +37990,20 @@ function commandFingerprint(row) {
37626
37990
  }
37627
37991
 
37628
37992
  // src/server/socket/chat-handler.ts
37629
- import { dirname as dirname12 } from "node:path";
37993
+ import { dirname as dirname13 } from "node:path";
37630
37994
 
37631
37995
  // src/server/services/chat-context.ts
37632
37996
  import { readFileSync as readFileSync10, readdirSync as readdirSync3, existsSync as existsSync15 } from "node:fs";
37633
- import { join as join17 } from "node:path";
37997
+ import { join as join18 } from "node:path";
37634
37998
  function buildChatContext(ocrDir, target) {
37635
- const sessionsDir = join17(ocrDir, "sessions");
37999
+ const sessionsDir = join18(ocrDir, "sessions");
37636
38000
  if (target.type === "map_run") {
37637
38001
  return buildMapRunContext(sessionsDir, target.sessionId, target.runNumber);
37638
38002
  }
37639
38003
  return buildReviewRoundContext(sessionsDir, target.sessionId, target.roundNumber);
37640
38004
  }
37641
38005
  function buildMapRunContext(sessionsDir, sessionId, runNumber) {
37642
- const mapPath = join17(
38006
+ const mapPath = join18(
37643
38007
  sessionsDir,
37644
38008
  sessionId,
37645
38009
  "map",
@@ -37666,9 +38030,9 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
37666
38030
  return parts.join("\n");
37667
38031
  }
37668
38032
  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");
38033
+ const roundDir = join18(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
38034
+ const finalPath = join18(roundDir, "final.md");
38035
+ const reviewersDir = join18(roundDir, "reviews");
37672
38036
  const parts = [
37673
38037
  `You are an expert code reviewer assisting with a code review session.`,
37674
38038
  `You are looking at review round #${roundNumber} for session "${sessionId}".`,
@@ -37685,7 +38049,7 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
37685
38049
  if (existsSync15(reviewersDir)) {
37686
38050
  const files = readdirSync3(reviewersDir).filter((f) => f.endsWith(".md")).sort();
37687
38051
  for (const file of files) {
37688
- const content = readFileSync10(join17(reviewersDir, file), "utf-8");
38052
+ const content = readFileSync10(join18(reviewersDir, file), "utf-8");
37689
38053
  const reviewerName = file.replace(/\.md$/, "");
37690
38054
  parts.push("");
37691
38055
  parts.push(`<reviewer name="${reviewerName}">`);
@@ -37749,7 +38113,7 @@ function startTrackedExecution(io2, db, ocrDir, command, args = []) {
37749
38113
  db.run(
37750
38114
  `UPDATE command_executions
37751
38115
  SET exit_code = ?, finished_at = ?, output = ?, pid = NULL
37752
- WHERE id = ?`,
38116
+ WHERE id = ? AND finished_at IS NULL`,
37753
38117
  [exitCode, finishedAt, outputBuffer, executionId]
37754
38118
  );
37755
38119
  appendCommandLog(ocrDir, {
@@ -37839,7 +38203,7 @@ User: ${message}`;
37839
38203
  });
37840
38204
  return;
37841
38205
  }
37842
- const repoRoot = dirname12(ocrDir);
38206
+ const repoRoot = dirname13(ocrDir);
37843
38207
  const spawnResult = adapter.spawn({
37844
38208
  prompt,
37845
38209
  cwd: repoRoot,
@@ -38015,13 +38379,13 @@ function cleanupAllChats() {
38015
38379
  }
38016
38380
 
38017
38381
  // 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";
38382
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
38019
38383
  import { tmpdir as tmpdir2 } from "node:os";
38020
- import { join as join18, dirname as dirname13, isAbsolute as isAbsolute2 } from "node:path";
38384
+ import { join as join19, dirname as dirname14, isAbsolute as isAbsolute2 } from "node:path";
38021
38385
  import { randomUUID as randomUUID2 } from "node:crypto";
38022
38386
  function resolveSessionDir2(sessionDir, ocrDir) {
38023
38387
  if (isAbsolute2(sessionDir)) return sessionDir;
38024
- return join18(dirname13(ocrDir), sessionDir);
38388
+ return join19(dirname14(ocrDir), sessionDir);
38025
38389
  }
38026
38390
  var BRANCH_PREFIXES = [
38027
38391
  "feat",
@@ -38090,7 +38454,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
38090
38454
  return;
38091
38455
  }
38092
38456
  const branch = session.branch;
38093
- const repoRoot = dirname13(ocrDir);
38457
+ const repoRoot = dirname14(ocrDir);
38094
38458
  try {
38095
38459
  await execBinaryAsync("gh", ["auth", "status"], { env: cleanEnv(), cwd: repoRoot, encoding: "utf-8" });
38096
38460
  } catch {
@@ -38191,16 +38555,16 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
38191
38555
  socket.emit("post:error", { error: "Session not found" });
38192
38556
  return;
38193
38557
  }
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");
38558
+ const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join19(ocrDir, "sessions", sessionId);
38559
+ const roundDir = join19(sessionDir, "rounds", `round-${roundNumber}`);
38560
+ const finalPath = join19(roundDir, "final.md");
38197
38561
  if (!existsSync16(finalPath)) {
38198
38562
  socket.emit("post:error", { error: "final.md not found for this round" });
38199
38563
  return;
38200
38564
  }
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");
38565
+ const humanReviewPath = join19(roundDir, "final-human.md");
38566
+ const repoRoot = dirname14(ocrDir);
38567
+ const commandMdPath = join19(ocrDir, "commands", "translate-review-to-single-human.md");
38204
38568
  let commandContent;
38205
38569
  try {
38206
38570
  commandContent = readFileSync11(commandMdPath, "utf-8");
@@ -38360,10 +38724,10 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
38360
38724
  socket.emit("post:save-result", { success: false, error: "Session not found" });
38361
38725
  return;
38362
38726
  }
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");
38727
+ const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join19(ocrDir, "sessions", sessionId);
38728
+ const roundDir = join19(sessionDir, "rounds", `round-${roundNumber}`);
38729
+ mkdirSync7(roundDir, { recursive: true });
38730
+ const filePath = join19(roundDir, "final-human.md");
38367
38731
  writeFileSync5(filePath, content, { mode: 420 });
38368
38732
  socket.emit("post:save-result", { success: true });
38369
38733
  } catch (err) {
@@ -38390,14 +38754,14 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
38390
38754
  );
38391
38755
  tracker.appendOutput(`\u25B8 Posting review to PR #${prNumber}...
38392
38756
  `);
38393
- const tmpDir = join18(tmpdir2(), "ocr-post-comments");
38757
+ const tmpDir = join19(tmpdir2(), "ocr-post-comments");
38394
38758
  try {
38395
- mkdirSync6(tmpDir, { recursive: true, mode: 448 });
38759
+ mkdirSync7(tmpDir, { recursive: true, mode: 448 });
38396
38760
  } catch {
38397
38761
  }
38398
- const tmpFile = join18(tmpDir, `${randomUUID2()}.md`);
38762
+ const tmpFile = join19(tmpDir, `${randomUUID2()}.md`);
38399
38763
  writeFileSync5(tmpFile, content, { mode: 384 });
38400
- const repoRoot = dirname13(ocrDir);
38764
+ const repoRoot = dirname14(ocrDir);
38401
38765
  try {
38402
38766
  const { stdout } = await execBinaryAsync(
38403
38767
  "gh",
@@ -38440,9 +38804,67 @@ function cleanupAllPostGenerations() {
38440
38804
  }
38441
38805
  }
38442
38806
 
38807
+ // src/server/services/forward-resume-sweep.ts
38808
+ function hasPositiveDeathEvidence(db, sessionId, isAlive) {
38809
+ const instances = listAgentSessionsForWorkflow(db, sessionId);
38810
+ if (instances.length === 0) return false;
38811
+ return instances.every(
38812
+ (s) => s.ended_at != null || s.pid != null && !isAlive(s.pid)
38813
+ );
38814
+ }
38815
+ function planForwardResume(db, cfg) {
38816
+ const isAlive = cfg.isAlive ?? defaultIsAlive;
38817
+ const plan = [];
38818
+ for (const session of getAllSessions(db)) {
38819
+ if (session.status !== "active") continue;
38820
+ const events = getEventsForSession(db, session.id);
38821
+ const workflowType = session.workflow_type === "map" ? "map" : "review";
38822
+ if (hasTerminalArtifactEvent2(events, workflowType, session.current_round)) {
38823
+ continue;
38824
+ }
38825
+ if (!hasPositiveDeathEvidence(db, session.id, isAlive)) continue;
38826
+ const stranded = strandedActionByCap(db, session, cfg.maxAttempts);
38827
+ if (stranded.action === "abort_or_fresh") {
38828
+ plan.push({ sessionId: session.id, action: "cap_close" });
38829
+ continue;
38830
+ }
38831
+ const latest = getLatestAgentSessionWithVendorId(db, session.id);
38832
+ plan.push({
38833
+ sessionId: session.id,
38834
+ action: latest?.vendor_session_id ? "resume" : "handoff"
38835
+ });
38836
+ }
38837
+ return plan;
38838
+ }
38839
+ function runForwardResumeSweep(deps) {
38840
+ const plan = planForwardResume(deps.db, deps.config);
38841
+ for (const item of plan) {
38842
+ try {
38843
+ if (item.action === "cap_close") {
38844
+ closeForwardResumeExhausted(deps.db, item.sessionId, deps.maxAttempts);
38845
+ deps.log?.(
38846
+ `[ForwardResume] ${item.sessionId}: attempts exhausted \u2192 closed non-success`
38847
+ );
38848
+ } else if (item.action === "resume") {
38849
+ deps.spawnResume(item.sessionId);
38850
+ deps.log?.(`[ForwardResume] ${item.sessionId}: auto-resuming (ocr review --resume)`);
38851
+ } else {
38852
+ deps.log?.(
38853
+ `[ForwardResume] ${item.sessionId}: stranded, no resume adapter \u2014 pick up in terminal`
38854
+ );
38855
+ }
38856
+ } catch (err) {
38857
+ deps.log?.(
38858
+ `[ForwardResume] ${item.sessionId}: ${err instanceof Error ? err.message : String(err)}`
38859
+ );
38860
+ }
38861
+ }
38862
+ return plan;
38863
+ }
38864
+
38443
38865
  // src/server/index.ts
38444
38866
  import { homedir } from "node:os";
38445
- var __dirname3 = dirname14(fileURLToPath3(import.meta.url));
38867
+ var __dirname3 = dirname15(fileURLToPath3(import.meta.url));
38446
38868
  function shortenPath(p) {
38447
38869
  const home = homedir();
38448
38870
  return p.startsWith(home) ? "~" + p.slice(home.length) : p;
@@ -38509,7 +38931,7 @@ if (process.env.NODE_ENV !== "production") {
38509
38931
  function isOcrDashboardProcess(pid) {
38510
38932
  if (process.platform === "win32") return false;
38511
38933
  try {
38512
- const cmd = execFileSync3("ps", ["-p", String(pid), "-o", "command="], {
38934
+ const cmd = execBinary("ps", ["-p", String(pid), "-o", "command="], {
38513
38935
  encoding: "utf-8",
38514
38936
  timeout: 3e3
38515
38937
  }).trim();
@@ -38524,23 +38946,23 @@ async function startServer(options = {}) {
38524
38946
  process.title = "ocr-dashboard";
38525
38947
  const ocrDir = resolveOcrDir();
38526
38948
  const aiCliService = new AiCliService(ocrDir);
38527
- const dbPathForCheckpoint = join19(ocrDir, "data", "ocr.db");
38949
+ const dbPathForCheckpoint = join20(ocrDir, "data", "ocr.db");
38528
38950
  const walResult = walCheckpointTruncate(dbPathForCheckpoint);
38529
38951
  if (walResult === "checkpointed") {
38530
38952
  console.log(" WAL checkpoint: truncated stale write-ahead-log file");
38531
38953
  }
38532
- for (const reaped of reapOrphanDbFiles(join19(ocrDir, "data"))) {
38954
+ for (const reaped of reapOrphanDbFiles(join20(ocrDir, "data"))) {
38533
38955
  console.log(` Orphan reap: removed stale ${reaped}`);
38534
38956
  }
38535
- const staleLogs = reapStaleExecLogs(join19(ocrDir, "data", "exec-logs"));
38957
+ const staleLogs = reapStaleExecLogs(join20(ocrDir, "data", "exec-logs"));
38536
38958
  if (staleLogs.length > 0) {
38537
38959
  console.log(` Exec-log reap: removed ${staleLogs.length} stale agent log(s)`);
38538
38960
  }
38539
38961
  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 });
38962
+ const dataDir = join20(ocrDir, "data");
38963
+ const pidFilePath = join20(dataDir, "dashboard.pid");
38964
+ const portFilePath = join20(dataDir, "server-port");
38965
+ mkdirSync8(dataDir, { recursive: true });
38544
38966
  try {
38545
38967
  unlinkSync5(portFilePath);
38546
38968
  } catch {
@@ -38638,12 +39060,39 @@ async function startServer(options = {}) {
38638
39060
  }
38639
39061
  };
38640
39062
  await reconcileCompleted();
39063
+ const forwardResumeMaxAttempts = getForwardResumeMaxAttempts(ocrDir);
39064
+ const spawnResume = (sessionId) => {
39065
+ const child = spawnBinary("ocr", ["review", "--resume", sessionId], {
39066
+ cwd: ocrDir.replace(/\.ocr$/, "") || process.cwd(),
39067
+ stdio: "ignore",
39068
+ detached: true
39069
+ });
39070
+ child.on("error", (err) => {
39071
+ console.error(`[ForwardResume] spawn failed for ${sessionId}:`, err.message);
39072
+ });
39073
+ child.unref();
39074
+ };
39075
+ const runForwardResume = () => {
39076
+ try {
39077
+ runForwardResumeSweep({
39078
+ db,
39079
+ config: { maxAttempts: forwardResumeMaxAttempts, heartbeatMs: heartbeatSeconds * 1e3 },
39080
+ maxAttempts: forwardResumeMaxAttempts,
39081
+ spawnResume,
39082
+ log: (m) => console.log(` ${m}`)
39083
+ });
39084
+ } catch (err) {
39085
+ console.error("[ForwardResume] sweep failed:", err);
39086
+ }
39087
+ };
39088
+ runForwardResume();
38641
39089
  const SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
38642
39090
  const sweepTimer = setInterval(() => {
38643
39091
  try {
38644
39092
  logAgentSweep(sweepStaleAgentSessions(db, heartbeatSeconds, defaultIsAlive));
38645
39093
  sweepStaleSessions(db, STALE_SESSION_THRESHOLD_SECONDS);
38646
39094
  void reconcileCompleted();
39095
+ runForwardResume();
38647
39096
  } catch (err) {
38648
39097
  console.error("[sweep] periodic sweep failed:", err);
38649
39098
  }
@@ -38679,10 +39128,10 @@ async function startServer(options = {}) {
38679
39128
  app.use("/api/agent-sessions", createAgentSessionsRouter(db, () => pullSync()));
38680
39129
  app.use("/api/sessions", createHandoffRouter(sessionCapture, ocrDir, () => pullSync()));
38681
39130
  app.use("/api/team", createTeamRouter(ocrDir));
38682
- const clientDir = join19(__dirname3, "client");
39131
+ const clientDir = join20(__dirname3, "client");
38683
39132
  if (process.env.NODE_ENV === "production" && existsSync17(clientDir)) {
38684
39133
  app.use(import_express15.default.static(clientDir, { index: false }));
38685
- const indexHtmlPath = join19(clientDir, "index.html");
39134
+ const indexHtmlPath = join20(clientDir, "index.html");
38686
39135
  const rawIndexHtml = existsSync17(indexHtmlPath) ? readFileSync12(indexHtmlPath, "utf-8") : "";
38687
39136
  const tokenScript = `<script>window.__OCR_TOKEN__=${JSON.stringify(AUTH_TOKEN)};</script>`;
38688
39137
  const injectedIndexHtml = rawIndexHtml.replace(
@@ -38702,7 +39151,7 @@ async function startServer(options = {}) {
38702
39151
  registerChatHandlers(io, socket, db, ocrDir, aiCliService);
38703
39152
  registerPostHandlers(io, socket, db, ocrDir, aiCliService);
38704
39153
  });
38705
- const dbFilePath = join19(ocrDir, "data", "ocr.db");
39154
+ const dbFilePath = join20(ocrDir, "data", "ocr.db");
38706
39155
  const dbSyncWatcher = new DbSyncWatcher(
38707
39156
  db,
38708
39157
  dbFilePath,
@@ -38718,7 +39167,7 @@ async function startServer(options = {}) {
38718
39167
  dbSyncWatcher.startWatching();
38719
39168
  pullSync = () => dbSyncWatcher.syncFromDisk();
38720
39169
  console.log(` Watching DB: ${shortenPath(dbFilePath)}`);
38721
- const sessionsDir = join19(ocrDir, "sessions");
39170
+ const sessionsDir = join20(ocrDir, "sessions");
38722
39171
  const fsSync = new FilesystemSync(db, sessionsDir, io);
38723
39172
  await fsSync.fullScan();
38724
39173
  fsSync.startWatching();
@@ -38782,7 +39231,7 @@ async function startServer(options = {}) {
38782
39231
  unlinkSync5(portFilePath);
38783
39232
  } catch {
38784
39233
  }
38785
- clearSpawnMarker(ocrDir);
39234
+ clearAllSpawnMarkers(ocrDir);
38786
39235
  try {
38787
39236
  const activeResult = db.exec(
38788
39237
  "SELECT id, pid FROM command_executions WHERE pid IS NOT NULL AND finished_at IS NULL"