@open-code-review/cli 2.2.0 → 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 (65) hide show
  1. package/README.md +9 -0
  2. package/dist/dashboard/client/assets/{_basePickBy-BBPb8BJA.js → _basePickBy-CyrHyeyN.js} +1 -1
  3. package/dist/dashboard/client/assets/{_baseUniq-CFHdos6T.js → _baseUniq-Bg7NJSGS.js} +1 -1
  4. package/dist/dashboard/client/assets/{arc-BKGGWA2F.js → arc-zDGAKMur.js} +1 -1
  5. package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-B_ovNjX1.js → architectureDiagram-VXUJARFQ-BxlGxm0Q.js} +1 -1
  6. package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-C2M-avVp.js → blockDiagram-VD42YOAC-BskTNyX5.js} +1 -1
  7. package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-BtOBpAzH.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-Cz2EbHPl.js → chunk-4BX2VUAB-xq9xoCTv.js} +1 -1
  10. package/dist/dashboard/client/assets/{chunk-55IACEB6-C8xpXw9G.js → chunk-55IACEB6-DYdXYVh5.js} +1 -1
  11. package/dist/dashboard/client/assets/{chunk-B4BG7PRW-BSRfOovX.js → chunk-B4BG7PRW-BGAyFRFS.js} +1 -1
  12. package/dist/dashboard/client/assets/{chunk-DI55MBZ5-CEUbYQWn.js → chunk-DI55MBZ5-C5ul9stk.js} +1 -1
  13. package/dist/dashboard/client/assets/{chunk-FMBD7UC4-5xWP6GRj.js → chunk-FMBD7UC4-BSaPo2xa.js} +1 -1
  14. package/dist/dashboard/client/assets/{chunk-QN33PNHL-DfNCVcy8.js → chunk-QN33PNHL-CyzabUv0.js} +1 -1
  15. package/dist/dashboard/client/assets/{chunk-QZHKN3VN--OdToKKu.js → chunk-QZHKN3VN-CceRbxt_.js} +1 -1
  16. package/dist/dashboard/client/assets/{chunk-TZMSLE5B-B_0K0Qso.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-Cc_Dmnxz.js → cose-bilkent-S5V4N54A-DEdXBrCt.js} +1 -1
  21. package/dist/dashboard/client/assets/{dagre-6UL2VRFP-DaAfvUXU.js → dagre-6UL2VRFP-DRdIiP58.js} +1 -1
  22. package/dist/dashboard/client/assets/{diagram-PSM6KHXK-7idwN0rC.js → diagram-PSM6KHXK-Bo7Q2VlK.js} +1 -1
  23. package/dist/dashboard/client/assets/{diagram-QEK2KX5R-D9j9H13n.js → diagram-QEK2KX5R-2Fmc2o5x.js} +1 -1
  24. package/dist/dashboard/client/assets/{diagram-S2PKOQOG-SMF5SB0K.js → diagram-S2PKOQOG-5WE8f0p7.js} +1 -1
  25. package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-EVJ4Qa2F.js → erDiagram-Q2GNP2WA-DD-iXWd_.js} +1 -1
  26. package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-tZ7SFE77.js → flowDiagram-NV44I4VS-CCWo8Ue9.js} +1 -1
  27. package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-DFSqguY7.js → ganttDiagram-JELNMOA3-CNY4d5UK.js} +1 -1
  28. package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-CqHdP3HE.js → gitGraphDiagram-V2S2FVAM-Dq5SBEJJ.js} +1 -1
  29. package/dist/dashboard/client/assets/{graph-C0XnkNkk.js → graph-BTt9lokK.js} +1 -1
  30. package/dist/dashboard/client/assets/index-B0k81q2b.js +581 -0
  31. package/dist/dashboard/client/assets/index-Czwdh6UA.css +1 -0
  32. package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-DlXZo9U2.js → infoDiagram-HS3SLOUP-AnKZja-G.js} +1 -1
  33. package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CgC8_7eN.js → journeyDiagram-XKPGCS4Q-nC-_WjPN.js} +1 -1
  34. package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-BMAw_jNp.js → kanban-definition-3W4ZIXB7-BEY73sWU.js} +1 -1
  35. package/dist/dashboard/client/assets/{layout-XjM3Q-ka.js → layout-D4DfNpzH.js} +1 -1
  36. package/dist/dashboard/client/assets/{linear-CMUrrr1X.js → linear-ZpGvKjeP.js} +1 -1
  37. package/dist/dashboard/client/assets/{mermaid-renderer-D2jYNs7K.js → mermaid-renderer-BCDxmS9g.js} +4 -4
  38. package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-CL4hv-vg.js → mindmap-definition-VGOIOE7T-MzAaKESA.js} +1 -1
  39. package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-DTqv-1h1.js → pieDiagram-ADFJNKIX-B_X1kySF.js} +1 -1
  40. package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-BpFlSW9N.js → quadrantDiagram-AYHSOK5B-CMoIEMLN.js} +1 -1
  41. package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-BqYqqXL4.js → requirementDiagram-UZGBJVZJ-v4CRsn1w.js} +1 -1
  42. package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-kEI9kntR.js → sankeyDiagram-TZEHDZUN-CPcyN8Jj.js} +1 -1
  43. package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-Cnu_1j-N.js → sequenceDiagram-WL72ISMW-CTg0Vx1H.js} +1 -1
  44. package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-BoC-rqoG.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-CXMWuzDL.js → timeline-definition-IT6M3QCI-B8xFcSGb.js} +1 -1
  47. package/dist/dashboard/client/assets/{treemap-GDKQZRPO-o9ZFgpbJ.js → treemap-GDKQZRPO-HQQuGl9w.js} +1 -1
  48. package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-CfIuUpeA.js → xychartDiagram-PRI3JC2R-Drz0SW3I.js} +1 -1
  49. package/dist/dashboard/client/index.html +2 -2
  50. package/dist/dashboard/server.js +1085 -632
  51. package/dist/index.js +1395 -383
  52. package/package.json +6 -39
  53. package/dist/dashboard/client/assets/channel-rgw7C1e7.js +0 -1
  54. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-DTGi7d9X.js +0 -1
  55. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-DTGi7d9X.js +0 -1
  56. package/dist/dashboard/client/assets/clone-Cz7hswqi.js +0 -1
  57. package/dist/dashboard/client/assets/index-C3NEq704.js +0 -571
  58. package/dist/dashboard/client/assets/index-CzxeSSaQ.css +0 -1
  59. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-COR3QD3v.js +0 -1
  60. package/dist/lib/db/index.js +0 -2177
  61. package/dist/lib/models.js +0 -85
  62. package/dist/lib/runtime-config.js +0 -55
  63. package/dist/lib/state/index.js +0 -2196
  64. package/dist/lib/team-config.js +0 -175
  65. 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);
@@ -20806,7 +20806,7 @@ var require_application = __commonJS({
20806
20806
  };
20807
20807
  app2.del = deprecate.function(app2.delete, "app.del: Use app.delete instead");
20808
20808
  app2.render = function render(name, options, callback) {
20809
- var cache = this.cache;
20809
+ var cache2 = this.cache;
20810
20810
  var done = callback;
20811
20811
  var engines = this.engines;
20812
20812
  var opts = options;
@@ -20825,7 +20825,7 @@ var require_application = __commonJS({
20825
20825
  renderOptions.cache = this.enabled("view cache");
20826
20826
  }
20827
20827
  if (renderOptions.cache) {
20828
- view = cache[name];
20828
+ view = cache2[name];
20829
20829
  }
20830
20830
  if (!view) {
20831
20831
  var View2 = this.get("view");
@@ -20841,7 +20841,7 @@ var require_application = __commonJS({
20841
20841
  return done(err);
20842
20842
  }
20843
20843
  if (renderOptions.cache) {
20844
- cache[name] = view;
20844
+ cache2[name] = view;
20845
20845
  }
20846
20846
  }
20847
20847
  tryRender(view, renderOptions, done);
@@ -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"
@@ -33489,11 +33635,6 @@ function buildResumeCommand(vendor, vendorSessionId) {
33489
33635
  // src/server/services/ai-cli/claude-adapter.ts
33490
33636
  var WORKFLOW_TOOLS = ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "TodoWrite", "TodoRead", "Task"];
33491
33637
  var QUERY_TOOLS = ["Read", "Grep", "Glob"];
33492
- var BUNDLED_CLAUDE_MODELS = [
33493
- { id: "claude-opus-4-7", displayName: "Claude Opus 4.7" },
33494
- { id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" },
33495
- { id: "claude-haiku-4-5-20251001", displayName: "Claude Haiku 4.5" }
33496
- ];
33497
33638
  var ClaudeCodeAdapter = class {
33498
33639
  name = "Claude Code";
33499
33640
  binary = "claude";
@@ -33522,6 +33663,7 @@ var ClaudeCodeAdapter = class {
33522
33663
  }
33523
33664
  }
33524
33665
  spawn(opts) {
33666
+ assertNonEmptyPrompt(opts.prompt);
33525
33667
  const isWorkflow = opts.mode === "workflow";
33526
33668
  const maxTurns = opts.maxTurns ?? (isWorkflow ? 500 : 1);
33527
33669
  const tools = opts.allowedTools ?? (isWorkflow ? WORKFLOW_TOOLS : QUERY_TOOLS);
@@ -33543,7 +33685,6 @@ var ClaudeCodeAdapter = class {
33543
33685
  flags.push("--model", opts.model);
33544
33686
  }
33545
33687
  const { stdio, logFd, logPath } = buildFileStdio(
33546
- "pipe",
33547
33688
  isWorkflow ? opts.logFile : void 0
33548
33689
  );
33549
33690
  const proc = spawnBinary("claude", flags, {
@@ -33554,46 +33695,13 @@ var ClaudeCodeAdapter = class {
33554
33695
  });
33555
33696
  closeFileStdio(logFd);
33556
33697
  if (isWorkflow) proc.unref();
33557
- proc.stdin?.write(opts.prompt);
33558
- proc.stdin?.end();
33698
+ deliverPrompt(proc, opts.prompt);
33559
33699
  return {
33560
33700
  process: proc,
33561
33701
  detached: isWorkflow,
33562
33702
  ...logPath ? { logPath } : {}
33563
33703
  };
33564
33704
  }
33565
- async listModels() {
33566
- try {
33567
- const output = execBinary("claude", ["models", "--json"], {
33568
- encoding: "utf-8",
33569
- timeout: 5e3,
33570
- stdio: ["ignore", "pipe", "ignore"]
33571
- });
33572
- const parsed = JSON.parse(output);
33573
- if (Array.isArray(parsed)) {
33574
- const models = [];
33575
- for (const item of parsed) {
33576
- if (typeof item === "string") {
33577
- models.push({ id: item });
33578
- } else if (typeof item === "object" && item !== null && "id" in item && typeof item.id === "string") {
33579
- const obj = item;
33580
- const desc = { id: obj.id };
33581
- if (typeof obj.displayName === "string") desc.displayName = obj.displayName;
33582
- if (typeof obj.provider === "string") desc.provider = obj.provider;
33583
- if (Array.isArray(obj.tags)) {
33584
- desc.tags = obj.tags.filter((t) => typeof t === "string");
33585
- }
33586
- models.push(desc);
33587
- }
33588
- }
33589
- if (models.length > 0) {
33590
- return models;
33591
- }
33592
- }
33593
- } catch {
33594
- }
33595
- return BUNDLED_CLAUDE_MODELS;
33596
- }
33597
33705
  createParser() {
33598
33706
  return new ClaudeLineParser();
33599
33707
  }
@@ -33747,11 +33855,6 @@ function extractToolResultOutput(content) {
33747
33855
  function capitalize(s) {
33748
33856
  return s.charAt(0).toUpperCase() + s.slice(1);
33749
33857
  }
33750
- var BUNDLED_OPENCODE_MODELS = [
33751
- { id: "anthropic/claude-opus-4-7", provider: "anthropic" },
33752
- { id: "anthropic/claude-sonnet-4-6", provider: "anthropic" },
33753
- { id: "anthropic/claude-haiku-4-5-20251001", provider: "anthropic" }
33754
- ];
33755
33858
  var OpenCodeAdapter = class {
33756
33859
  name = "OpenCode";
33757
33860
  binary = "opencode";
@@ -33784,11 +33887,11 @@ var OpenCodeAdapter = class {
33784
33887
  }
33785
33888
  }
33786
33889
  spawn(opts) {
33890
+ assertNonEmptyPrompt(opts.prompt);
33787
33891
  const isWorkflow = opts.mode === "workflow";
33788
33892
  const agent = opts.allowedTools ? void 0 : isWorkflow ? "build" : "plan";
33789
33893
  const args = [
33790
33894
  "run",
33791
- opts.prompt,
33792
33895
  "--format",
33793
33896
  "json"
33794
33897
  ];
@@ -33802,7 +33905,6 @@ var OpenCodeAdapter = class {
33802
33905
  args.push("--model", opts.model);
33803
33906
  }
33804
33907
  const { stdio, logFd, logPath } = buildFileStdio(
33805
- "ignore",
33806
33908
  isWorkflow ? opts.logFile : void 0
33807
33909
  );
33808
33910
  const proc = spawnBinary("opencode", args, {
@@ -33813,44 +33915,13 @@ var OpenCodeAdapter = class {
33813
33915
  });
33814
33916
  closeFileStdio(logFd);
33815
33917
  if (isWorkflow) proc.unref();
33918
+ deliverPrompt(proc, opts.prompt);
33816
33919
  return {
33817
33920
  process: proc,
33818
33921
  detached: isWorkflow,
33819
33922
  ...logPath ? { logPath } : {}
33820
33923
  };
33821
33924
  }
33822
- async listModels() {
33823
- try {
33824
- const output = execBinary("opencode", ["models", "--json"], {
33825
- encoding: "utf-8",
33826
- timeout: 5e3,
33827
- stdio: ["ignore", "pipe", "ignore"]
33828
- });
33829
- const parsed = JSON.parse(output);
33830
- if (Array.isArray(parsed)) {
33831
- const models = [];
33832
- for (const item of parsed) {
33833
- if (typeof item === "string") {
33834
- models.push({ id: item });
33835
- } else if (typeof item === "object" && item !== null && "id" in item && typeof item.id === "string") {
33836
- const obj = item;
33837
- const desc = { id: obj.id };
33838
- if (typeof obj.displayName === "string") desc.displayName = obj.displayName;
33839
- if (typeof obj.provider === "string") desc.provider = obj.provider;
33840
- if (Array.isArray(obj.tags)) {
33841
- desc.tags = obj.tags.filter((t) => typeof t === "string");
33842
- }
33843
- models.push(desc);
33844
- }
33845
- }
33846
- if (models.length > 0) {
33847
- return models;
33848
- }
33849
- }
33850
- } catch {
33851
- }
33852
- return BUNDLED_OPENCODE_MODELS;
33853
- }
33854
33925
  /**
33855
33926
  * OpenCode emits each event with all its content already resolved (tool
33856
33927
  * results arrive in the same event as the call), so the parser is
@@ -34019,6 +34090,9 @@ function readEventJournal(ocrDir, executionId) {
34019
34090
  }
34020
34091
 
34021
34092
  // src/server/services/ai-cli/index.ts
34093
+ function createRegisteredAdapters() {
34094
+ return [new ClaudeCodeAdapter(), new OpenCodeAdapter()];
34095
+ }
34022
34096
  function readAiCliPreference(ocrDir) {
34023
34097
  try {
34024
34098
  const configPath = join9(ocrDir, "config.yaml");
@@ -34038,10 +34112,7 @@ var AiCliService = class {
34038
34112
  status;
34039
34113
  constructor(ocrDir) {
34040
34114
  this.preference = readAiCliPreference(ocrDir);
34041
- const adapters = [
34042
- new ClaudeCodeAdapter(),
34043
- new OpenCodeAdapter()
34044
- ];
34115
+ const adapters = createRegisteredAdapters();
34045
34116
  this.entries = adapters.map((adapter) => ({
34046
34117
  adapter,
34047
34118
  detection: adapter.detect()
@@ -34118,8 +34189,8 @@ var AiCliService = class {
34118
34189
  const available = this.entries.filter((e) => e.detection.found);
34119
34190
  if (available.length === 0) return null;
34120
34191
  if (this.preference !== "auto") {
34121
- const preferred = available.find((e) => e.adapter.binary === this.preference);
34122
- if (preferred) return preferred.adapter;
34192
+ const preferred2 = available.find((e) => e.adapter.binary === this.preference);
34193
+ if (preferred2) return preferred2.adapter;
34123
34194
  console.warn(
34124
34195
  ` AI CLI: Preferred "${this.preference}" not found, falling back to auto-detection`
34125
34196
  );
@@ -34166,10 +34237,12 @@ var FileTailer = class {
34166
34237
  /** Read everything currently available from `offset` to EOF. */
34167
34238
  poll() {
34168
34239
  if (!this.ensureOpen()) return;
34240
+ const fd = this.fd;
34241
+ if (fd === null) return;
34169
34242
  let bytes;
34170
34243
  do {
34171
34244
  try {
34172
- bytes = readSync(this.fd, this.buf, 0, this.buf.length, this.offset);
34245
+ bytes = readSync(fd, this.buf, 0, this.buf.length, this.offset);
34173
34246
  } catch {
34174
34247
  return;
34175
34248
  }
@@ -34230,100 +34303,12 @@ function resolveLocalCli() {
34230
34303
  return null;
34231
34304
  }
34232
34305
 
34233
- // ../cli/src/lib/state/projection.ts
34234
- var REASON_EVENT_TYPES = [
34235
- "session_aborted",
34236
- "session_auto_closed_stale",
34237
- "session_synced",
34238
- "session_legacy_import"
34239
- ];
34240
- var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
34241
- "session_closed",
34242
- ...REASON_EVENT_TYPES
34243
- ]);
34244
- function hasCompletionInvariant(db, session) {
34245
- const eventType = session.workflow_type === "map" ? "map_completed" : "round_completed";
34246
- const round = session.workflow_type === "map" ? session.current_map_run : session.current_round;
34247
- const r = db.exec(
34248
- `SELECT 1 FROM orchestration_events
34249
- WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
34250
- [session.id, eventType, round]
34251
- );
34252
- return (r[0]?.values.length ?? 0) > 0;
34253
- }
34254
-
34255
- // ../cli/src/lib/state/index.ts
34256
- async function stateClose(params) {
34257
- const { sessionId, ocrDir, abort } = params;
34258
- const db = await ensureDatabase(ocrDir);
34259
- const existing = getSession(db, sessionId);
34260
- if (!existing) {
34261
- throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
34262
- }
34263
- if (existing.status === "closed") {
34264
- console.error(`[ocr] Session already closed: ${sessionId}`);
34265
- return;
34266
- }
34267
- if (!abort && !hasCompletionInvariant(db, existing)) {
34268
- const what = existing.workflow_type === "map" ? `map run ${existing.current_map_run} has no map_completed event` : `round ${existing.current_round} has no round_completed event`;
34269
- throw new StateError(
34270
- STATE_EXIT.INVARIANT_UNMET,
34271
- `Cannot close session ${sessionId}: ${what}. Run 'ocr state complete-round' to finalize it, or pass --abort to record an abandoned session.`
34272
- );
34273
- }
34274
- const note = "closed by parent workflow close";
34275
- db.transaction(() => {
34276
- if (abort) {
34277
- insertEvent(db, {
34278
- session_id: sessionId,
34279
- event_type: "session_aborted",
34280
- phase: existing.current_phase,
34281
- phase_number: existing.phase_number,
34282
- round: existing.current_round
34283
- });
34284
- }
34285
- updateSession(db, sessionId, {
34286
- status: "closed",
34287
- current_phase: "complete"
34288
- });
34289
- if (!abort) {
34290
- insertEvent(db, {
34291
- session_id: sessionId,
34292
- event_type: "session_closed",
34293
- phase: "complete",
34294
- phase_number: existing.phase_number,
34295
- round: existing.current_round
34296
- });
34297
- }
34298
- cascadeTerminateExecutions(db, sessionId, CASCADE_CLOSE_EXIT_CODE, note);
34299
- });
34300
- }
34301
- async function reconcileWorkflowOnExit(ocrDir, sessionId, db) {
34302
- db ??= await ensureDatabase(ocrDir);
34303
- const existing = getSession(db, sessionId);
34304
- if (!existing) return "not-found";
34305
- if (existing.status === "closed") return "already-closed";
34306
- if (!hasCompletionInvariant(db, existing)) return "incomplete";
34307
- if (hasInFlightDependents(db, sessionId)) return "in-flight";
34308
- await stateClose({ sessionId, ocrDir, abort: false });
34309
- return "closed";
34310
- }
34311
- async function reconcileCompletedSessions(ocrDir) {
34312
- const db = await ensureDatabase(ocrDir);
34313
- const closed = [];
34314
- for (const s of getAllSessions(db)) {
34315
- if (s.status !== "active") continue;
34316
- const outcome = await reconcileWorkflowOnExit(ocrDir, s.id, db);
34317
- if (outcome === "closed") closed.push(s.id);
34318
- }
34319
- return closed;
34320
- }
34321
-
34322
- // ../cli/src/lib/runtime-config.ts
34306
+ // ../shared/config/src/runtime-config.ts
34323
34307
  import { existsSync as existsSync9, readFileSync as readFileSync4 } from "node:fs";
34324
34308
  import { join as join11 } from "node:path";
34325
34309
  var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
34326
34310
  var DEFAULT_WORKFLOW_HARD_DEADLINE_MINUTES = 60;
34311
+ var DEFAULT_FORWARD_RESUME_MAX_ATTEMPTS = 2;
34327
34312
  function readRuntimePositiveInt(ocrDir, key, defaultValue) {
34328
34313
  const configPath = join11(ocrDir, "config.yaml");
34329
34314
  if (!existsSync9(configPath)) return defaultValue;
@@ -34368,8 +34353,15 @@ function getWorkflowHardDeadlineMs(ocrDir) {
34368
34353
  DEFAULT_WORKFLOW_HARD_DEADLINE_MINUTES
34369
34354
  ) * 60 * 1e3;
34370
34355
  }
34356
+ function getForwardResumeMaxAttempts(ocrDir) {
34357
+ return readRuntimePositiveInt(
34358
+ ocrDir,
34359
+ "forward_resume_max_attempts",
34360
+ DEFAULT_FORWARD_RESUME_MAX_ATTEMPTS
34361
+ );
34362
+ }
34371
34363
 
34372
- // src/server/socket/command-runner.ts
34364
+ // src/server/socket/prompt-builder.ts
34373
34365
  function shellSplit(str) {
34374
34366
  const tokens = [];
34375
34367
  let current = "";
@@ -34396,11 +34388,6 @@ function shellSplit(str) {
34396
34388
  if (current) tokens.push(current);
34397
34389
  return tokens;
34398
34390
  }
34399
- var ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
34400
- "progress",
34401
- "state"
34402
- ]);
34403
- var AI_COMMANDS = /* @__PURE__ */ new Set(["map", "review", "translate-review-to-single-human", "address", "create-reviewer", "sync-reviewers"]);
34404
34391
  function escapeUserHeaders(value) {
34405
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");
34406
34393
  }
@@ -34425,8 +34412,8 @@ function buildPrompt(opts) {
34425
34412
  options.push("--fresh");
34426
34413
  i++;
34427
34414
  } else if (arg === "--requirements" && i + 1 < subArgs.length) {
34428
- requirements = subArgs.slice(i + 1).join(" ");
34429
- break;
34415
+ requirements = subArgs[i + 1] ?? "";
34416
+ i += 2;
34430
34417
  } else if (arg === "--team" && i + 1 < subArgs.length) {
34431
34418
  team = subArgs[i + 1] ?? "";
34432
34419
  i += 2;
@@ -34436,8 +34423,9 @@ function buildPrompt(opts) {
34436
34423
  } else if (arg === "--reviewer" && i + 1 < subArgs.length) {
34437
34424
  const raw = subArgs[i + 1] ?? "";
34438
34425
  const countMatch = raw.match(/^(\d+):(.+)$/);
34439
- if (countMatch) {
34440
- 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) });
34441
34429
  } else {
34442
34430
  reviewerDescriptions.push({ description: raw, count: 1 });
34443
34431
  }
@@ -34548,57 +34536,372 @@ function extractPerInstanceModels(subArgs) {
34548
34536
  }
34549
34537
  return [...models];
34550
34538
  }
34539
+
34540
+ // src/server/socket/process-registry.ts
34551
34541
  var MAX_CONCURRENT = 3;
34552
- var WATCHDOG_TICK_MS = 1e4;
34553
- var POST_RESULT_GRACE_MS = 3e4;
34554
- var HEARTBEAT_THROTTLE_MS = 5e3;
34555
- function decideWatchdogTick(i) {
34556
- if (i.resultSeenAt !== void 0 && i.nowMs - i.resultSeenAt > i.postResultGraceMs) {
34557
- return {
34558
- action: "finalize",
34559
- reap: !i.exited,
34560
- exitCode: i.resultIsError ? 1 : 0,
34561
- reason: "result-grace"
34562
- };
34563
- }
34564
- if (i.nowMs - i.startedAtMs > i.hardDeadlineMs) {
34565
- return {
34566
- action: "finalize",
34567
- reap: !i.exited,
34568
- exitCode: WATCHDOG_DEADLINE_EXIT_CODE,
34569
- reason: "hard-deadline"
34570
- };
34571
- }
34572
- return i.exited ? { action: "wait" } : { action: "beat" };
34573
- }
34574
34542
  var activeCommands = /* @__PURE__ */ new Map();
34575
- function spawnMarkerPath(ocrDir) {
34576
- return join12(ocrDir, "data", "dashboard-active-spawn.json");
34577
- }
34578
- function writeSpawnMarker(ocrDir, executionUid, pid) {
34579
- const dataDir = join12(ocrDir, "data");
34580
- if (!existsSync10(dataDir)) mkdirSync5(dataDir, { recursive: true });
34581
- const payload = JSON.stringify({
34582
- execution_uid: executionUid,
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,
34583
34570
  pid,
34584
34571
  started_at: (/* @__PURE__ */ new Date()).toISOString()
34585
34572
  });
34586
- writeFileSync3(spawnMarkerPath(ocrDir), payload, { mode: 384 });
34573
+ writeFileSync3(spawnMarkerPath(ocrDir, executionUid), payload, { mode: 384 });
34587
34574
  }
34588
- function clearSpawnMarker(ocrDir) {
34575
+ function clearSpawnMarker(ocrDir, executionUid) {
34589
34576
  try {
34590
- unlinkSync3(spawnMarkerPath(ocrDir));
34577
+ unlinkSync3(spawnMarkerPath(ocrDir, executionUid));
34591
34578
  } catch {
34592
34579
  }
34593
34580
  }
34594
- function getActiveCommands() {
34595
- return Array.from(activeCommands.values()).map((entry) => ({
34596
- execution_id: entry.executionId,
34597
- command: entry.commandStr,
34598
- started_at: entry.startedAt,
34599
- output: entry.outputBuffer
34600
- }));
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
34593
+ var WATCHDOG_TICK_MS = 1e4;
34594
+ var POST_RESULT_GRACE_MS = 3e4;
34595
+ var HEARTBEAT_THROTTLE_MS = 5e3;
34596
+ function decideWatchdogTick(i) {
34597
+ if (i.resultSeenAt !== void 0 && i.nowMs - i.resultSeenAt > i.postResultGraceMs) {
34598
+ return {
34599
+ action: "finalize",
34600
+ reap: !i.exited,
34601
+ exitCode: i.resultIsError ? 1 : 0,
34602
+ reason: "result-grace"
34603
+ };
34604
+ }
34605
+ if (i.nowMs - i.startedAtMs > i.hardDeadlineMs) {
34606
+ return {
34607
+ action: "finalize",
34608
+ reap: !i.exited,
34609
+ exitCode: WATCHDOG_DEADLINE_EXIT_CODE,
34610
+ reason: "hard-deadline"
34611
+ };
34612
+ }
34613
+ return i.exited ? { action: "wait" } : { action: "beat" };
34614
+ }
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
+ };
34630
+ }
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;
34657
+ try {
34658
+ return JSON.parse(e.metadata);
34659
+ } catch {
34660
+ return null;
34661
+ }
34662
+ }
34663
+ function remainingPhasesAfter(workflowType, currentPhase) {
34664
+ const numbers = workflowType === "map" ? MAP_PHASE_NUMBERS : REVIEW_PHASE_NUMBERS;
34665
+ const cur = numbers[currentPhase];
34666
+ if (cur === void 0) return [];
34667
+ return Object.entries(numbers).filter(([, n]) => n > cur).sort((a, b) => a[1] - b[1]).map(([phase]) => phase);
34668
+ }
34669
+ function hasTerminalArtifactEvent2(events, workflowType, round) {
34670
+ const terminal = workflowType === "map" ? "map_completed" : "round_completed";
34671
+ return events.some((e) => e.event_type === terminal && e.round === round);
34672
+ }
34673
+ function countForwardResumeLeases(events, round) {
34674
+ return events.filter((e) => parseLeaseMetadata(e)?.kind === FORWARD_RESUME_KIND && parseLeaseMetadata(e)?.round === round).length;
34675
+ }
34676
+ function strandedActionByCap(db, session, maxAttempts) {
34677
+ const events = getEventsForSession(db, session.id);
34678
+ const leaseCount = countForwardResumeLeases(events, session.current_round);
34679
+ const workflowType = session.workflow_type === "map" ? "map" : "review";
34680
+ return {
34681
+ action: leaseCount >= maxAttempts ? "abort_or_fresh" : "forward_resume",
34682
+ remainingPhases: remainingPhasesAfter(workflowType, session.current_phase),
34683
+ attemptsRemaining: Math.max(0, maxAttempts - leaseCount)
34684
+ };
34685
+ }
34686
+ function closeForwardResumeExhausted(db, sessionId, attempts) {
34687
+ commitReasonClose(
34688
+ db,
34689
+ sessionId,
34690
+ {
34691
+ event_type: "session_auto_closed_stale",
34692
+ phase: "complete",
34693
+ metadata: JSON.stringify({
34694
+ reason: FORWARD_RESUME_EXHAUSTED_REASON,
34695
+ attempts
34696
+ })
34697
+ },
34698
+ { status: "closed", current_phase: "complete" }
34699
+ );
34700
+ }
34701
+
34702
+ // ../shared/persistence/src/state/projection.ts
34703
+ var REASON_EVENT_TYPES = [
34704
+ "session_aborted",
34705
+ "session_auto_closed_stale",
34706
+ "session_synced",
34707
+ "session_legacy_import"
34708
+ ];
34709
+ var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
34710
+ "session_closed",
34711
+ ...REASON_EVENT_TYPES
34712
+ ]);
34713
+ function hasCompletionInvariant(db, session) {
34714
+ const eventType = session.workflow_type === "map" ? "map_completed" : "round_completed";
34715
+ const round = session.workflow_type === "map" ? session.current_map_run : session.current_round;
34716
+ const r = db.exec(
34717
+ `SELECT 1 FROM orchestration_events
34718
+ WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
34719
+ [session.id, eventType, round]
34720
+ );
34721
+ return (r[0]?.values.length ?? 0) > 0;
34722
+ }
34723
+
34724
+ // ../shared/persistence/src/state/index.ts
34725
+ async function stateClose(params) {
34726
+ const { sessionId, ocrDir, abort } = params;
34727
+ const db = await ensureDatabase(ocrDir);
34728
+ const existing = getSession(db, sessionId);
34729
+ if (!existing) {
34730
+ throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
34731
+ }
34732
+ if (existing.status === "closed") {
34733
+ console.error(`[ocr] Session already closed: ${sessionId}`);
34734
+ return;
34735
+ }
34736
+ if (!abort && !hasCompletionInvariant(db, existing)) {
34737
+ const what = existing.workflow_type === "map" ? `map run ${existing.current_map_run} has no map_completed event` : `round ${existing.current_round} has no round_completed event`;
34738
+ throw new StateError(
34739
+ STATE_EXIT.INVARIANT_UNMET,
34740
+ `Cannot close session ${sessionId}: ${what}. Run 'ocr state complete-round' to finalize it, or pass --abort to record an abandoned session.`
34741
+ );
34742
+ }
34743
+ const note = "closed by parent workflow close";
34744
+ db.transaction(() => {
34745
+ if (abort) {
34746
+ insertEvent(db, {
34747
+ session_id: sessionId,
34748
+ event_type: "session_aborted",
34749
+ phase: existing.current_phase,
34750
+ phase_number: existing.phase_number,
34751
+ round: existing.current_round
34752
+ });
34753
+ }
34754
+ updateSession(db, sessionId, {
34755
+ status: "closed",
34756
+ current_phase: "complete"
34757
+ });
34758
+ if (!abort) {
34759
+ insertEvent(db, {
34760
+ session_id: sessionId,
34761
+ event_type: "session_closed",
34762
+ phase: "complete",
34763
+ phase_number: existing.phase_number,
34764
+ round: existing.current_round
34765
+ });
34766
+ }
34767
+ cascadeTerminateExecutions(db, sessionId, CASCADE_CLOSE_EXIT_CODE, note);
34768
+ });
34769
+ }
34770
+ async function reconcileWorkflowOnExit(ocrDir, sessionId, db) {
34771
+ db ??= await ensureDatabase(ocrDir);
34772
+ const existing = getSession(db, sessionId);
34773
+ if (!existing) return "not-found";
34774
+ if (existing.status === "closed") return "already-closed";
34775
+ if (!hasCompletionInvariant(db, existing)) return "incomplete";
34776
+ if (hasInFlightDependents(db, sessionId)) return "in-flight";
34777
+ await stateClose({ sessionId, ocrDir, abort: false });
34778
+ return "closed";
34779
+ }
34780
+ async function reconcileCompletedSessions(ocrDir) {
34781
+ const db = await ensureDatabase(ocrDir);
34782
+ const closed = [];
34783
+ for (const s of getAllSessions(db)) {
34784
+ if (s.status !== "active") continue;
34785
+ const outcome = await reconcileWorkflowOnExit(ocrDir, s.id, db);
34786
+ if (outcome === "closed") closed.push(s.id);
34787
+ }
34788
+ return closed;
34789
+ }
34790
+
34791
+ // src/server/services/command-outcome.ts
34792
+ function deriveCommandOutcome(exitCode, completeness) {
34793
+ if (exitCode === null) return null;
34794
+ if (exitCode === CANCELLED_EXIT_CODE || exitCode === CASCADE_CLOSE_EXIT_CODE) {
34795
+ return "cancelled";
34796
+ }
34797
+ if (exitCode === WATCHDOG_DEADLINE_EXIT_CODE) return "failed";
34798
+ if (exitCode !== 0) return "failed";
34799
+ if (completeness === null || completeness === "complete") return "success";
34800
+ return "incomplete";
34801
+ }
34802
+ function deriveCancellationReason(exitCode) {
34803
+ if (exitCode === CANCELLED_EXIT_CODE) return "user";
34804
+ if (exitCode === CASCADE_CLOSE_EXIT_CODE) return "cascade";
34805
+ return null;
34806
+ }
34807
+ function getWorkflowCompletenessForExecution(db, executionId) {
34808
+ const result = db.exec(
34809
+ `SELECT sc.completeness_state
34810
+ FROM command_executions ce
34811
+ LEFT JOIN session_completeness sc ON sc.session_id = ce.workflow_id
34812
+ WHERE ce.id = ?`,
34813
+ [executionId]
34814
+ );
34815
+ const row = result[0]?.values[0];
34816
+ if (!row) return null;
34817
+ const state = row[0];
34818
+ if (state === "complete" || state === "closed_without_artifact" || state === "in_flight" || state === "open_no_artifact") {
34819
+ return state;
34820
+ }
34821
+ return null;
34822
+ }
34823
+
34824
+ // src/server/socket/finalizer.ts
34825
+ function tryClaimFinalization(entry) {
34826
+ if (!entry) return true;
34827
+ if (entry.finalized) return false;
34828
+ entry.finalized = true;
34829
+ if (entry.watchdog) {
34830
+ clearInterval(entry.watchdog);
34831
+ entry.watchdog = void 0;
34832
+ }
34833
+ if (entry.tailer) {
34834
+ entry.tailer.stop();
34835
+ entry.tailer = void 0;
34836
+ }
34837
+ return true;
34601
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"]);
34602
34905
  function registerCommandHandlers(io2, socket, db, ocrDir, aiCliService, sessionCapture) {
34603
34906
  socket.on("command:run", (payload) => {
34604
34907
  try {
@@ -34771,15 +35074,14 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34771
35074
  finishExecution(io2, db, ocrDir, executionId, 1, content);
34772
35075
  return;
34773
35076
  }
35077
+ let capabilityWarning = null;
34774
35078
  if (adapter.supportsPerTaskModel === false) {
34775
35079
  const perInstanceModels = extractPerInstanceModels(subArgs);
34776
35080
  if (perInstanceModels.length > 0) {
34777
- 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.
34778
- `;
34779
- 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.`;
34780
35082
  }
34781
35083
  }
34782
- const commandMdPath = join12(ocrDir, "commands", `${baseCommand}.md`);
35084
+ const commandMdPath = join13(ocrDir, "commands", `${baseCommand}.md`);
34783
35085
  let commandContent;
34784
35086
  try {
34785
35087
  commandContent = readFileSync5(commandMdPath, "utf-8");
@@ -34830,9 +35132,9 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34830
35132
  let logFile;
34831
35133
  if (entry.uid) {
34832
35134
  try {
34833
- const logDir = join12(ocrDir, "data", "exec-logs");
34834
- mkdirSync5(logDir, { recursive: true });
34835
- 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`);
34836
35138
  } catch (err) {
34837
35139
  console.error(
34838
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):",
@@ -34888,20 +35190,7 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34888
35190
  }
34889
35191
  }, POLL_INTERVAL_MS);
34890
35192
  entry.linkPoll = linkPoll;
34891
- const bumpHeartbeat = () => {
34892
- if (entry.finalized) return;
34893
- const now = Date.now();
34894
- if (now - (entry.lastBeatWrite ?? 0) < HEARTBEAT_THROTTLE_MS) return;
34895
- entry.lastBeatWrite = now;
34896
- try {
34897
- db.run(
34898
- `UPDATE command_executions SET last_heartbeat_at = datetime('now') WHERE id = ? AND finished_at IS NULL`,
34899
- [executionId]
34900
- );
34901
- } catch (err) {
34902
- console.error("[command-runner] heartbeat bump failed:", err);
34903
- }
34904
- };
35193
+ const bumpHeartbeat = makeHeartbeatBumper(db, executionId, entry);
34905
35194
  const hardDeadlineMs = getWorkflowHardDeadlineMs(ocrDir);
34906
35195
  entry.watchdog = setInterval(() => {
34907
35196
  if (entry.finalized) return;
@@ -34933,6 +35222,12 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34933
35222
  `;
34934
35223
  entry.outputBuffer += notice;
34935
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
+ });
34936
35231
  } else {
34937
35232
  console.warn(`[watchdog] execution ${executionId}: result seen but no close after grace \u2014 finalizing${decision.reap ? " + reaping tree" : ""}`);
34938
35233
  }
@@ -34940,6 +35235,10 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34940
35235
  finishExecution(io2, db, ocrDir, executionId, decision.exitCode, entry.outputBuffer);
34941
35236
  return;
34942
35237
  }
35238
+ default: {
35239
+ const _exhaustive = decision;
35240
+ throw new Error(`unhandled watchdog action: ${JSON.stringify(_exhaustive)}`);
35241
+ }
34943
35242
  }
34944
35243
  }, WATCHDOG_TICK_MS);
34945
35244
  entry.watchdog.unref();
@@ -34967,6 +35266,16 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34967
35266
  journal.append(stream);
34968
35267
  io2.emit("command:event", stream);
34969
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
+ }
34970
35279
  function handleEvent(evt) {
34971
35280
  switch (evt.type) {
34972
35281
  case "text_delta":
@@ -35050,7 +35359,7 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
35050
35359
  clearInterval(entry.linkPoll);
35051
35360
  entry.linkPoll = void 0;
35052
35361
  }
35053
- clearSpawnMarker(ocrDir);
35362
+ clearSpawnMarker(ocrDir, entry.uid);
35054
35363
  if (entry.tailer) {
35055
35364
  entry.tailer.stop();
35056
35365
  entry.tailer = void 0;
@@ -35092,76 +35401,6 @@ ${stderrBuffer}`;
35092
35401
  finishExecution(io2, db, ocrDir, executionId, -1, entry.outputBuffer);
35093
35402
  });
35094
35403
  }
35095
- function finishExecution(io2, db, ocrDir, executionId, rawCode, output) {
35096
- const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
35097
- const entry = activeCommands.get(executionId);
35098
- const code = entry?.cancelled ? CANCELLED_EXIT_CODE : rawCode;
35099
- if (entry?.finalized) return;
35100
- if (entry) {
35101
- entry.finalized = true;
35102
- if (entry.watchdog) {
35103
- clearInterval(entry.watchdog);
35104
- entry.watchdog = void 0;
35105
- }
35106
- if (entry.tailer) {
35107
- entry.tailer.stop();
35108
- entry.tailer = void 0;
35109
- }
35110
- }
35111
- const res = db.prepare(
35112
- `UPDATE command_executions
35113
- SET exit_code = ?, finished_at = ?, output = ?, pid = NULL
35114
- WHERE id = ? AND finished_at IS NULL`
35115
- ).run(code, finishedAt, output, executionId);
35116
- if (Number(res.changes) === 0 && !entry) return;
35117
- const completeness = getWorkflowCompletenessForExecution(db, executionId);
35118
- const outcome = deriveCommandOutcome(code, completeness);
35119
- const cancellationReason = deriveCancellationReason(code);
35120
- if (entry?.uid) {
35121
- appendCommandLog(ocrDir, {
35122
- v: 1,
35123
- uid: entry.uid,
35124
- db_id: executionId,
35125
- command: entry.commandStr,
35126
- args: entry.argsJson ?? null,
35127
- exit_code: code,
35128
- started_at: entry.startedAt,
35129
- finished_at: finishedAt,
35130
- is_detached: entry.detached ? 1 : 0,
35131
- event: code === CANCELLED_EXIT_CODE ? "cancel" : "finish",
35132
- writer: "dashboard"
35133
- });
35134
- }
35135
- io2.emit("command:finished", {
35136
- execution_id: executionId,
35137
- exitCode: code,
35138
- finished_at: finishedAt,
35139
- outcome,
35140
- cancellation_reason: cancellationReason
35141
- });
35142
- activeCommands.delete(executionId);
35143
- const workflowRow = db.exec(
35144
- "SELECT workflow_id FROM command_executions WHERE id = ?",
35145
- [executionId]
35146
- );
35147
- const workflowId = workflowRow[0]?.values[0]?.[0];
35148
- if (typeof workflowId === "string" && workflowId.length > 0) {
35149
- void reconcileWorkflowOnExit(ocrDir, workflowId, db).then((outcome2) => {
35150
- if (outcome2 === "closed") {
35151
- console.log(`[command-runner] auto-finalized workflow ${workflowId}`);
35152
- } else if (outcome2 === "incomplete" || outcome2 === "in-flight") {
35153
- console.debug(
35154
- `[command-runner] workflow ${workflowId} not finalized: ${outcome2}`
35155
- );
35156
- }
35157
- }).catch((err) => {
35158
- console.error(
35159
- `[command-runner] reconcileWorkflowOnExit(${workflowId}) failed:`,
35160
- err instanceof Error ? err.message : err
35161
- );
35162
- });
35163
- }
35164
- }
35165
35404
 
35166
35405
  // src/server/routes/commands.ts
35167
35406
  var AVAILABLE_COMMANDS = [
@@ -35249,7 +35488,7 @@ function createCommandsRouter(db, ocrDir) {
35249
35488
  // src/server/routes/config.ts
35250
35489
  var import_express9 = __toESM(require_express2(), 1);
35251
35490
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
35252
- 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";
35253
35492
  var VALID_IDES = ["vscode", "cursor", "windsurf", "jetbrains", "sublime"];
35254
35493
  function detectIde() {
35255
35494
  const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? "";
@@ -35266,7 +35505,7 @@ function detectIde() {
35266
35505
  }
35267
35506
  function readIdeFromConfig(ocrDir) {
35268
35507
  try {
35269
- const configPath = join13(ocrDir, "config.yaml");
35508
+ const configPath = join14(ocrDir, "config.yaml");
35270
35509
  const content = readFileSync6(configPath, "utf-8");
35271
35510
  const match = content.match(/^\s*ide:\s*(\S+)/m);
35272
35511
  return match?.[1] ?? "auto";
@@ -35313,7 +35552,7 @@ function createConfigRouter(ocrDir, aiCliService) {
35313
35552
  return;
35314
35553
  }
35315
35554
  try {
35316
- const configPath = join13(ocrDir, "config.yaml");
35555
+ const configPath = join14(ocrDir, "config.yaml");
35317
35556
  let content = readFileSync6(configPath, "utf-8");
35318
35557
  if (content.match(/^\s*ide:\s*\S+/m)) {
35319
35558
  content = content.replace(/^(\s*ide:\s*)\S+/m, `$1${ide}`);
@@ -35403,9 +35642,9 @@ function createChatRouter(db) {
35403
35642
  // src/server/routes/reviewers.ts
35404
35643
  var import_express11 = __toESM(require_express2(), 1);
35405
35644
  import { readFileSync as readFileSync7, existsSync as existsSync11, watch } from "node:fs";
35406
- import { join as join14 } from "node:path";
35645
+ import { join as join15 } from "node:path";
35407
35646
  function readReviewersMeta(ocrDir) {
35408
- const metaPath = join14(ocrDir, "reviewers-meta.json");
35647
+ const metaPath = join15(ocrDir, "reviewers-meta.json");
35409
35648
  if (!existsSync11(metaPath)) {
35410
35649
  return { reviewers: [], defaults: [] };
35411
35650
  }
@@ -35434,7 +35673,7 @@ function createReviewersRouter(ocrDir) {
35434
35673
  res.status(400).json({ error: "Invalid reviewer ID" });
35435
35674
  return;
35436
35675
  }
35437
- const filePath = join14(ocrDir, "skills", "references", "reviewers", `${id}.md`);
35676
+ const filePath = join15(ocrDir, "skills", "references", "reviewers", `${id}.md`);
35438
35677
  if (!existsSync11(filePath)) {
35439
35678
  res.status(404).json({ error: "Reviewer not found", id });
35440
35679
  return;
@@ -35449,7 +35688,7 @@ function createReviewersRouter(ocrDir) {
35449
35688
  return router;
35450
35689
  }
35451
35690
  function watchReviewersMeta(ocrDir, io2) {
35452
- const metaPath = join14(ocrDir, "reviewers-meta.json");
35691
+ const metaPath = join15(ocrDir, "reviewers-meta.json");
35453
35692
  let watcher = null;
35454
35693
  let debounce;
35455
35694
  try {
@@ -35525,14 +35764,13 @@ function createHandoffRouter(sessionCapture, ocrDir, syncFromDisk = () => {
35525
35764
 
35526
35765
  // src/server/routes/team.ts
35527
35766
  var import_express14 = __toESM(require_express2(), 1);
35528
- import { spawnSync } from "node:child_process";
35529
35767
 
35530
- // ../cli/src/lib/team-config.ts
35768
+ // ../shared/config/src/team-config.ts
35531
35769
  var import_yaml = __toESM(require_dist(), 1);
35532
35770
  import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
35533
- import { join as join15 } from "node:path";
35771
+ import { join as join16 } from "node:path";
35534
35772
  function loadTeamConfig(ocrDir) {
35535
- const configPath = join15(ocrDir, "config.yaml");
35773
+ const configPath = join16(ocrDir, "config.yaml");
35536
35774
  if (!existsSync12(configPath)) {
35537
35775
  return { team: [], aliases: {}, defaultModel: null };
35538
35776
  }
@@ -35572,6 +35810,9 @@ function parseTeamConfigYaml(content) {
35572
35810
  aliases,
35573
35811
  defaultModel
35574
35812
  );
35813
+ if (resolvedModel !== null) {
35814
+ assertSafeModelId(resolvedModel, `default_team.${persona}[${i}]`);
35815
+ }
35575
35816
  team.push({
35576
35817
  persona,
35577
35818
  instance_index: i + 1,
@@ -35652,6 +35893,16 @@ function readOptionalString(obj, key, pathLabel) {
35652
35893
  }
35653
35894
  return value;
35654
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
+ }
35655
35906
  function readAliases(root) {
35656
35907
  const models = root["models"];
35657
35908
  if (!models || typeof models !== "object" || Array.isArray(models)) return {};
@@ -35693,29 +35944,71 @@ function resolveTeamComposition(team, override) {
35693
35944
  result.push(inst);
35694
35945
  }
35695
35946
  for (const inst of override) {
35947
+ if (inst.model !== null) {
35948
+ assertSafeModelId(inst.model, `override ${inst.persona}#${inst.instance_index}`);
35949
+ }
35696
35950
  result.push(inst);
35697
35951
  }
35698
35952
  return result;
35699
35953
  }
35700
35954
 
35701
- // ../cli/src/lib/models.ts
35702
- var BUNDLED_CLAUDE_MODELS2 = [
35703
- { id: "claude-opus-4-7", displayName: "Claude Opus 4.7" },
35704
- { id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" },
35705
- { id: "claude-haiku-4-5-20251001", displayName: "Claude Haiku 4.5" }
35706
- ];
35707
- var BUNDLED_OPENCODE_MODELS2 = [
35708
- { id: "anthropic/claude-opus-4-7", provider: "anthropic" },
35709
- { id: "anthropic/claude-sonnet-4-6", provider: "anthropic" },
35710
- { id: "anthropic/claude-haiku-4-5-20251001", provider: "anthropic" }
35711
- ];
35712
- function detectActiveVendor() {
35713
- for (const vendor of ["claude", "opencode"]) {
35955
+ // ../shared/config/src/models.ts
35956
+ function parseOpenCodeModelList(stdout) {
35957
+ const models = [];
35958
+ for (const rawLine of stdout.split(/\r?\n/)) {
35959
+ const line = rawLine.trim();
35960
+ if (!/^[^\s:]+\/\S+$/.test(line)) continue;
35961
+ const provider = line.slice(0, line.indexOf("/"));
35962
+ models.push({ id: line, provider });
35963
+ }
35964
+ return models.length > 0 ? models : null;
35965
+ }
35966
+ var VENDOR_MODEL_STRATEGIES = {
35967
+ claude: {
35968
+ displayName: "Claude Code",
35969
+ native: {
35970
+ // Verified against Claude Code 2.1.x: the CLI has no model-listing
35971
+ // subcommand (`claude models --json` → "unknown option"). Revisit if
35972
+ // a future release adds one.
35973
+ unavailableReason: "Claude Code does not provide a model-listing command; showing its documented model aliases instead"
35974
+ },
35975
+ // Vendor-documented aliases that always track the latest generation —
35976
+ // dated ids here would go stale by construction (the exact bug class of
35977
+ // issue #39). Pinned dated ids remain available via free-text entry.
35978
+ bundled: [
35979
+ { id: "opus", displayName: "Claude Opus (latest)" },
35980
+ { id: "sonnet", displayName: "Claude Sonnet (latest)" },
35981
+ { id: "haiku", displayName: "Claude Haiku (latest)" }
35982
+ ]
35983
+ },
35984
+ opencode: {
35985
+ displayName: "OpenCode",
35986
+ native: {
35987
+ // Plain `opencode models` — newline-delimited ids. (`--json` is not a
35988
+ // real flag, and `--verbose` interleaves JSON metadata blocks that
35989
+ // defeat line parsing.)
35990
+ args: ["models"],
35991
+ parse: parseOpenCodeModelList
35992
+ },
35993
+ bundled: [
35994
+ { id: "anthropic/claude-opus-4-8", provider: "anthropic" },
35995
+ { id: "anthropic/claude-sonnet-4-6", provider: "anthropic" },
35996
+ { id: "anthropic/claude-haiku-4-5", provider: "anthropic" }
35997
+ ]
35998
+ }
35999
+ };
36000
+ var SUPPORTED_VENDORS = Object.keys(
36001
+ VENDOR_MODEL_STRATEGIES
36002
+ );
36003
+ function isModelVendor(value) {
36004
+ return Object.hasOwn(VENDOR_MODEL_STRATEGIES, value);
36005
+ }
36006
+ async function detectActiveVendor() {
36007
+ for (const vendor of SUPPORTED_VENDORS) {
35714
36008
  try {
35715
- execBinary(vendor, ["--version"], {
36009
+ await execBinaryAsync(vendor, ["--version"], {
35716
36010
  encoding: "utf-8",
35717
- timeout: 3e3,
35718
- stdio: ["ignore", "pipe", "ignore"]
36011
+ timeout: 3e3
35719
36012
  });
35720
36013
  return vendor;
35721
36014
  } catch {
@@ -35723,48 +36016,77 @@ function detectActiveVendor() {
35723
36016
  }
35724
36017
  return null;
35725
36018
  }
35726
- function tryNativeEnumeration(vendor) {
36019
+ function describeProbeFailure(vendor, args, err) {
36020
+ const command = `${vendor} ${args.join(" ")}`;
36021
+ const e = err;
36022
+ if (e.code === "ENOENT") {
36023
+ return `\`${vendor}\` is not installed or not on PATH`;
36024
+ }
36025
+ if (e.killed) {
36026
+ return `\`${command}\` timed out or exceeded output limits`;
36027
+ }
36028
+ const stderr = typeof e.stderr === "string" ? e.stderr.trim() : "";
36029
+ const firstLine = (stderr.split(/\r?\n/)[0] ?? "").replace(/\u001b\[[0-9;]*[A-Za-z]/g, "").replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
36030
+ const detail = firstLine ? `: ${firstLine}` : "";
36031
+ const exit = typeof e.code === "number" ? ` with exit code ${e.code}` : "";
36032
+ return `\`${command}\` failed${exit}${detail}`;
36033
+ }
36034
+ async function tryNativeEnumeration(vendor, probe) {
36035
+ let stdout;
35727
36036
  try {
35728
- const output = execBinary(vendor, ["models", "--json"], {
36037
+ const result = await execBinaryAsync(vendor, probe.args, {
35729
36038
  encoding: "utf-8",
35730
- timeout: 5e3,
35731
- stdio: ["ignore", "pipe", "ignore"]
36039
+ timeout: 5e3
35732
36040
  });
35733
- const parsed = JSON.parse(output);
35734
- if (!Array.isArray(parsed)) return null;
35735
- const models = [];
35736
- for (const item of parsed) {
35737
- if (typeof item === "string") {
35738
- models.push({ id: item });
35739
- } else if (typeof item === "object" && item !== null && "id" in item && typeof item.id === "string") {
35740
- const obj = item;
35741
- const desc = { id: obj.id };
35742
- if (typeof obj.displayName === "string") desc.displayName = obj.displayName;
35743
- if (typeof obj.provider === "string") desc.provider = obj.provider;
35744
- if (Array.isArray(obj.tags)) {
35745
- desc.tags = obj.tags.filter((t) => typeof t === "string");
35746
- }
35747
- models.push(desc);
35748
- }
35749
- }
35750
- return models.length > 0 ? models : null;
35751
- } catch {
35752
- return null;
36041
+ stdout = result.stdout;
36042
+ } catch (err) {
36043
+ return { models: null, reason: describeProbeFailure(vendor, probe.args, err) };
35753
36044
  }
36045
+ const models = probe.parse(stdout);
36046
+ if (!models) {
36047
+ return {
36048
+ models: null,
36049
+ reason: `\`${vendor} ${probe.args.join(" ")}\` output did not contain any model identifiers`
36050
+ };
36051
+ }
36052
+ return { models };
35754
36053
  }
35755
- function bundledForVendor(vendor) {
35756
- if (vendor === "claude") return BUNDLED_CLAUDE_MODELS2;
35757
- return BUNDLED_OPENCODE_MODELS2;
35758
- }
35759
- function listModelsForVendor(vendor) {
35760
- const native = tryNativeEnumeration(vendor);
35761
- if (native) {
35762
- return { vendor, source: "native", models: native };
36054
+ var SUCCESS_TTL_MS = 6e4;
36055
+ var FAILURE_TTL_MS = 1e4;
36056
+ var cache = /* @__PURE__ */ new Map();
36057
+ async function listModelsForVendor(vendor) {
36058
+ const cached = cache.get(vendor);
36059
+ if (cached && cached.expiresAt > Date.now()) {
36060
+ return cached.result;
36061
+ }
36062
+ const strategy = VENDOR_MODEL_STRATEGIES[vendor];
36063
+ if (!strategy) {
36064
+ throw new Error(`Unknown vendor: ${vendor}`);
36065
+ }
36066
+ let result;
36067
+ if ("unavailableReason" in strategy.native) {
36068
+ result = {
36069
+ vendor,
36070
+ source: "bundled",
36071
+ models: strategy.bundled,
36072
+ nativeUnavailableReason: strategy.native.unavailableReason
36073
+ };
36074
+ } else {
36075
+ const native = await tryNativeEnumeration(vendor, strategy.native);
36076
+ result = native.models ? { vendor, source: "native", models: native.models } : {
36077
+ vendor,
36078
+ source: "bundled",
36079
+ models: strategy.bundled,
36080
+ nativeUnavailableReason: native.reason
36081
+ };
35763
36082
  }
35764
- return { vendor, source: "bundled", models: bundledForVendor(vendor) };
36083
+ const ttl = result.source === "native" ? SUCCESS_TTL_MS : FAILURE_TTL_MS;
36084
+ cache.set(vendor, { result, expiresAt: Date.now() + ttl });
36085
+ return result;
35765
36086
  }
35766
36087
 
35767
36088
  // src/server/routes/team.ts
36089
+ import { dirname as dirname10 } from "node:path";
35768
36090
  function isReviewerInstanceArray(input) {
35769
36091
  if (!Array.isArray(input)) return false;
35770
36092
  for (const entry of input) {
@@ -35817,60 +36139,64 @@ function createTeamRouter(ocrDir) {
35817
36139
  return;
35818
36140
  }
35819
36141
  try {
35820
- const result = spawnSync("ocr", ["team", "set", "--stdin"], {
36142
+ execBinary("ocr", ["team", "set", "--stdin"], {
35821
36143
  input: JSON.stringify(body.team),
35822
36144
  encoding: "utf-8",
35823
- 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),
35824
36151
  timeout: 1e4
35825
36152
  });
35826
- if (result.error) {
35827
- res.status(500).json({
35828
- error: "Failed to invoke ocr team set",
35829
- detail: result.error.message
35830
- });
35831
- return;
35832
- }
35833
- if (result.status !== 0) {
35834
- res.status(500).json({
35835
- error: "ocr team set exited non-zero",
35836
- stderr: result.stderr
35837
- });
35838
- return;
35839
- }
35840
36153
  res.json({ ok: true, team: body.team });
35841
36154
  } catch (err) {
35842
36155
  console.error("Failed to persist team:", err);
36156
+ const e = err;
35843
36157
  res.status(500).json({
35844
36158
  error: "Failed to persist team",
35845
- 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 } : {}
35846
36161
  });
35847
36162
  }
35848
36163
  });
35849
36164
  router.get("/models", (req, res) => {
35850
- let vendor;
35851
- const requested = req.query["vendor"]?.toLowerCase();
35852
- if (requested === "claude" || requested === "opencode") {
35853
- vendor = requested;
35854
- } else if (!requested || requested === "auto") {
35855
- vendor = detectActiveVendor();
35856
- } else {
35857
- res.status(400).json({ error: `Unknown vendor: ${requested}` });
35858
- return;
35859
- }
35860
- if (!vendor) {
35861
- res.json({ vendor: null, source: null, models: [] });
35862
- return;
35863
- }
35864
- try {
35865
- const result = listModelsForVendor(vendor);
35866
- res.json(result);
35867
- } catch (err) {
35868
- console.error("Failed to list models:", err);
35869
- res.status(500).json({
35870
- error: "Failed to list models",
35871
- detail: err instanceof Error ? err.message : String(err)
35872
- });
35873
- }
36165
+ void (async () => {
36166
+ try {
36167
+ const raw = req.query["vendor"];
36168
+ if (raw !== void 0 && typeof raw !== "string") {
36169
+ res.status(400).json({ error: "vendor must be a single string" });
36170
+ return;
36171
+ }
36172
+ const requested = raw?.toLowerCase();
36173
+ let vendor;
36174
+ if (requested && isModelVendor(requested)) {
36175
+ vendor = requested;
36176
+ } else if (!requested || requested === "auto") {
36177
+ vendor = await detectActiveVendor();
36178
+ } else {
36179
+ res.status(400).json({
36180
+ error: `Unknown vendor: ${requested}. Supported: ${SUPPORTED_VENDORS.join(", ")}`
36181
+ });
36182
+ return;
36183
+ }
36184
+ if (!vendor) {
36185
+ res.json({ vendor: null, source: null, models: [] });
36186
+ return;
36187
+ }
36188
+ const result = await listModelsForVendor(vendor);
36189
+ res.json(result);
36190
+ } catch (err) {
36191
+ console.error("Failed to list models:", err);
36192
+ if (!res.headersSent) {
36193
+ res.status(500).json({
36194
+ error: "Failed to list models",
36195
+ detail: err instanceof Error ? err.message : String(err)
36196
+ });
36197
+ }
36198
+ }
36199
+ })();
35874
36200
  });
35875
36201
  return router;
35876
36202
  }
@@ -35948,6 +36274,7 @@ function recoverFromEventsJsonl(ocrDir, db, workflowId) {
35948
36274
  function createSessionCaptureService(deps) {
35949
36275
  const { db, ocrDir, aiCliService } = deps;
35950
36276
  const driftLoggedFor = /* @__PURE__ */ new Set();
36277
+ const rejectLoggedFor = /* @__PURE__ */ new Set();
35951
36278
  function readBoundSessionId(executionId) {
35952
36279
  const result = db.exec(
35953
36280
  "SELECT vendor_session_id FROM command_executions WHERE id = ?",
@@ -35958,6 +36285,15 @@ function createSessionCaptureService(deps) {
35958
36285
  }
35959
36286
  function recordSessionId(executionId, vendorSessionId) {
35960
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
+ }
35961
36297
  const existing = readBoundSessionId(executionId);
35962
36298
  if (existing === vendorSessionId) return;
35963
36299
  if (existing) {
@@ -36137,7 +36473,7 @@ function buildDiagnostics(input) {
36137
36473
 
36138
36474
  // src/server/services/filesystem-sync.ts
36139
36475
  import { readdirSync as readdirSync2, readFileSync as readFileSync9, statSync as statSync3, existsSync as existsSync13 } from "node:fs";
36140
- import { join as join16, basename as basename3, dirname as dirname10, relative } from "node:path";
36476
+ import { join as join17, basename as basename3, dirname as dirname11, relative } from "node:path";
36141
36477
  import { watch as watch2 } from "chokidar";
36142
36478
 
36143
36479
  // src/server/services/parsers/reviewer-parser.ts
@@ -36232,21 +36568,12 @@ var VERDICT_RE = /^\*?\*?\s*(?:##\s*)?Verdict\s*\*?\*?\s*:?\s*\*?\*?\s*(.*)/im;
36232
36568
  var BLOCKERS_RE = /^\*\*Blockers?\*\*\s*:?\s*(\d+)/im;
36233
36569
  var SHOULD_FIX_RE = /^\*\*Should\s*Fix\*\*\s*:?\s*(\d+)/im;
36234
36570
  var SUGGESTIONS_RE = /^\*\*Suggestions?\*\*\s*:?\s*(\d+)/im;
36235
- var KNOWN_VERDICTS = [
36236
- "REQUEST CHANGES",
36237
- "CHANGES REQUESTED",
36238
- "NEEDS DISCUSSION",
36239
- "NEEDS WORK",
36240
- "APPROVED",
36241
- "APPROVE",
36242
- "LGTM",
36243
- "BLOCK",
36244
- "REJECT"
36245
- ];
36246
- function normalizeVerdict(raw) {
36571
+ function extractVerdictLabel(raw) {
36247
36572
  const cleaned = raw.trim().replace(/^\*+|\*+$/g, "").trim();
36573
+ const canonical = normalizeVerdict(cleaned);
36574
+ if (canonical) return canonical;
36248
36575
  const upper = cleaned.toUpperCase();
36249
- for (const verdict of KNOWN_VERDICTS) {
36576
+ for (const verdict of CANONICAL_VERDICTS) {
36250
36577
  if (upper.startsWith(verdict)) return verdict;
36251
36578
  }
36252
36579
  const truncated = cleaned.split(/\s+[—:.]\s+|\n/, 1)[0] ?? cleaned;
@@ -36258,7 +36585,7 @@ function parseFinalMd(content) {
36258
36585
  if (verdictMatch) {
36259
36586
  const captured = (verdictMatch[1] ?? "").trim();
36260
36587
  if (captured.length > 0) {
36261
- verdict = normalizeVerdict(captured);
36588
+ verdict = extractVerdictLabel(captured);
36262
36589
  }
36263
36590
  }
36264
36591
  const blockerMatch = content.match(BLOCKERS_RE);
@@ -36345,13 +36672,13 @@ var FilesystemSync = class {
36345
36672
  for (const entry of entries) {
36346
36673
  if (!entry.isDirectory()) continue;
36347
36674
  const sessionId = entry.name;
36348
- const sessionDir = join16(this.sessionsDir, sessionId);
36675
+ const sessionDir = join17(this.sessionsDir, sessionId);
36349
36676
  this.syncSession(sessionId, sessionDir);
36350
36677
  }
36351
36678
  }
36352
36679
  syncSession(sessionId, sessionDir) {
36353
36680
  this.ensureSessionRow(sessionId, sessionDir);
36354
- const roundsDir = join16(sessionDir, "rounds");
36681
+ const roundsDir = join17(sessionDir, "rounds");
36355
36682
  if (existsSync13(roundsDir)) {
36356
36683
  const rounds = readdirSync2(roundsDir, { withFileTypes: true });
36357
36684
  for (const roundEntry of rounds) {
@@ -36359,34 +36686,34 @@ var FilesystemSync = class {
36359
36686
  const roundMatch = roundEntry.name.match(/^round-(\d+)$/);
36360
36687
  if (!roundMatch) continue;
36361
36688
  const roundNumber = parseInt(roundMatch[1] ?? "0", 10);
36362
- const roundDir = join16(roundsDir, roundEntry.name);
36363
- const reviewsDir = join16(roundDir, "reviews");
36689
+ const roundDir = join17(roundsDir, roundEntry.name);
36690
+ const reviewsDir = join17(roundDir, "reviews");
36364
36691
  if (existsSync13(reviewsDir)) {
36365
36692
  const reviewFiles = readdirSync2(reviewsDir).filter((f) => f.endsWith(".md"));
36366
36693
  for (const reviewFile of reviewFiles) {
36367
- const filePath = join16(reviewsDir, reviewFile);
36694
+ const filePath = join17(reviewsDir, reviewFile);
36368
36695
  this.processReviewerOutput(sessionId, roundNumber, filePath, reviewFile);
36369
36696
  }
36370
36697
  }
36371
- const roundMetaPath = join16(roundDir, "round-meta.json");
36698
+ const roundMetaPath = join17(roundDir, "round-meta.json");
36372
36699
  if (existsSync13(roundMetaPath)) {
36373
36700
  this.processRoundMeta(sessionId, roundNumber, roundMetaPath);
36374
36701
  }
36375
- const finalPath = join16(roundDir, "final.md");
36702
+ const finalPath = join17(roundDir, "final.md");
36376
36703
  if (existsSync13(finalPath)) {
36377
36704
  this.processFinalMd(sessionId, roundNumber, finalPath);
36378
36705
  }
36379
- const finalHumanPath = join16(roundDir, "final-human.md");
36706
+ const finalHumanPath = join17(roundDir, "final-human.md");
36380
36707
  if (existsSync13(finalHumanPath)) {
36381
36708
  this.processGenericArtifact(sessionId, "final-human", finalHumanPath, roundNumber);
36382
36709
  }
36383
- const discoursePath = join16(roundDir, "discourse.md");
36710
+ const discoursePath = join17(roundDir, "discourse.md");
36384
36711
  if (existsSync13(discoursePath)) {
36385
36712
  this.processGenericArtifact(sessionId, "discourse", discoursePath, roundNumber);
36386
36713
  }
36387
36714
  }
36388
36715
  }
36389
- const mapDir = join16(sessionDir, "map", "runs");
36716
+ const mapDir = join17(sessionDir, "map", "runs");
36390
36717
  if (existsSync13(mapDir)) {
36391
36718
  const runs = readdirSync2(mapDir, { withFileTypes: true });
36392
36719
  for (const runEntry of runs) {
@@ -36394,12 +36721,12 @@ var FilesystemSync = class {
36394
36721
  const runMatch = runEntry.name.match(/^run-(\d+)$/);
36395
36722
  if (!runMatch) continue;
36396
36723
  const runNumber = parseInt(runMatch[1] ?? "0", 10);
36397
- const runDir = join16(mapDir, runEntry.name);
36398
- const mapMetaPath = join16(runDir, "map-meta.json");
36724
+ const runDir = join17(mapDir, runEntry.name);
36725
+ const mapMetaPath = join17(runDir, "map-meta.json");
36399
36726
  if (existsSync13(mapMetaPath)) {
36400
36727
  this.processMapMeta(sessionId, runNumber, mapMetaPath);
36401
36728
  }
36402
- const mapPath = join16(runDir, "map.md");
36729
+ const mapPath = join17(runDir, "map.md");
36403
36730
  if (existsSync13(mapPath)) {
36404
36731
  this.processMapMd(sessionId, runNumber, mapPath);
36405
36732
  }
@@ -36409,7 +36736,7 @@ var FilesystemSync = class {
36409
36736
  ["requirements-mapping.md", "requirements-mapping"]
36410
36737
  ];
36411
36738
  for (const [fileName, artifactType] of mapArtifacts) {
36412
- const filePath = join16(runDir, fileName);
36739
+ const filePath = join17(runDir, fileName);
36413
36740
  if (existsSync13(filePath)) {
36414
36741
  this.processGenericArtifact(sessionId, artifactType, filePath, void 0, runNumber);
36415
36742
  }
@@ -36421,68 +36748,113 @@ var FilesystemSync = class {
36421
36748
  ["discovered-standards.md", "discovered-standards"]
36422
36749
  ];
36423
36750
  for (const [fileName, artifactType] of sessionArtifacts) {
36424
- const filePath = join16(sessionDir, fileName);
36751
+ const filePath = join17(sessionDir, fileName);
36425
36752
  if (existsSync13(filePath)) {
36426
36753
  this.processGenericArtifact(sessionId, artifactType, filePath);
36427
36754
  }
36428
36755
  }
36429
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
+ }
36430
36796
  // ── Session Backfill ──
36431
36797
  ensureSessionRow(sessionId, sessionDir) {
36432
36798
  const branchMatch = sessionId.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
36433
36799
  const branch = branchMatch?.[1] ?? "unknown";
36434
- const hasRoundsDir = existsSync13(join16(sessionDir, "rounds"));
36435
- const hasMapDir = existsSync13(join16(sessionDir, "map"));
36800
+ const hasRoundsDir = existsSync13(join17(sessionDir, "rounds"));
36801
+ const hasMapDir = existsSync13(join17(sessionDir, "map"));
36436
36802
  const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
36437
36803
  let currentRound = 1;
36438
36804
  if (hasRoundsDir) {
36439
- 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+$/));
36440
36806
  currentRound = Math.max(1, roundDirs.length);
36441
36807
  }
36442
36808
  let currentMapRun = 1;
36443
- const mapRunsDir = join16(sessionDir, "map", "runs");
36809
+ const mapRunsDir = join17(sessionDir, "map", "runs");
36444
36810
  if (existsSync13(mapRunsDir)) {
36445
36811
  const runDirs = readdirSync2(mapRunsDir).filter((d) => d.match(/^run-\d+$/));
36446
36812
  currentMapRun = Math.max(1, runDirs.length);
36447
36813
  }
36448
36814
  let phase = "context";
36449
36815
  let phaseNumber = 1;
36450
- let status = "closed";
36816
+ let status = "active";
36451
36817
  if (workflowType === "review" && hasRoundsDir) {
36452
- const roundDir = join16(sessionDir, "rounds", `round-${currentRound}`);
36453
- 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)) {
36454
36820
  phase = "complete";
36455
36821
  phaseNumber = 8;
36456
36822
  status = "closed";
36457
- } else if (existsSync13(join16(roundDir, "discourse.md"))) {
36823
+ } else if (existsSync13(join17(roundDir, "final.md"))) {
36458
36824
  phase = "synthesis";
36459
36825
  phaseNumber = 7;
36460
- } else if (existsSync13(join16(roundDir, "reviews")) && readdirSync2(join16(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
36826
+ } else if (existsSync13(join17(roundDir, "discourse.md"))) {
36827
+ phase = "synthesis";
36828
+ phaseNumber = 7;
36829
+ } else if (existsSync13(join17(roundDir, "reviews")) && readdirSync2(join17(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
36461
36830
  phase = "reviews";
36462
36831
  phaseNumber = 4;
36463
- } else if (existsSync13(join16(sessionDir, "context.md"))) {
36832
+ } else if (existsSync13(join17(sessionDir, "context.md"))) {
36464
36833
  phase = "analysis";
36465
36834
  phaseNumber = 3;
36466
- } else if (existsSync13(join16(sessionDir, "discovered-standards.md"))) {
36835
+ } else if (existsSync13(join17(sessionDir, "discovered-standards.md"))) {
36467
36836
  phase = "change-context";
36468
36837
  phaseNumber = 2;
36469
36838
  }
36470
36839
  } else if (workflowType === "map" && hasMapDir) {
36471
- const runDir = join16(mapRunsDir, `run-${currentMapRun}`);
36472
- 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)) {
36473
36842
  phase = "complete";
36474
36843
  phaseNumber = 6;
36475
36844
  status = "closed";
36476
- } 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"))) {
36477
36849
  phase = "synthesis";
36478
36850
  phaseNumber = 5;
36479
- } else if (existsSync13(join16(runDir, "flow-analysis.md"))) {
36851
+ } else if (existsSync13(join17(runDir, "flow-analysis.md"))) {
36480
36852
  phase = "requirements-mapping";
36481
36853
  phaseNumber = 4;
36482
- } else if (existsSync13(join16(runDir, "topology.md"))) {
36854
+ } else if (existsSync13(join17(runDir, "topology.md"))) {
36483
36855
  phase = "flow-analysis";
36484
36856
  phaseNumber = 3;
36485
- } else if (existsSync13(join16(sessionDir, "discovered-standards.md"))) {
36857
+ } else if (existsSync13(join17(sessionDir, "discovered-standards.md"))) {
36486
36858
  phase = "topology";
36487
36859
  phaseNumber = 2;
36488
36860
  }
@@ -36537,7 +36909,7 @@ var FilesystemSync = class {
36537
36909
  try {
36538
36910
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
36539
36911
  if (entry.isDirectory()) {
36540
- if (this.hasArtifacts(join16(dir, entry.name))) return true;
36912
+ if (this.hasArtifacts(join17(dir, entry.name))) return true;
36541
36913
  } else if (/\.(md|json)$/.test(entry.name)) {
36542
36914
  return true;
36543
36915
  }
@@ -36683,7 +37055,7 @@ var FilesystemSync = class {
36683
37055
  "SELECT current_phase, phase_number, workflow_type FROM sessions WHERE id = ?",
36684
37056
  [sessionId]
36685
37057
  );
36686
- 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)) {
36687
37059
  commitReasonClose(
36688
37060
  this.db,
36689
37061
  sessionId,
@@ -36841,13 +37213,8 @@ var FilesystemSync = class {
36841
37213
  console.error(`[FilesystemSync] Invalid round-meta.json at ${filePath}`);
36842
37214
  return;
36843
37215
  }
36844
- const allFindings = meta.reviewers.flatMap((r) => r.findings ?? []);
36845
- const sc = meta.synthesis_counts;
36846
- const blockerCount = sc?.blockers ?? allFindings.filter((f) => f.category === "blocker").length;
36847
- const shouldFixCount = sc?.should_fix ?? allFindings.filter((f) => f.category === "should_fix").length;
36848
- const suggestionCount = sc?.suggestions ?? allFindings.filter((f) => f.category === "suggestion").length;
36849
- const reviewerCount = meta.reviewers.length;
36850
- const totalFindingCount = allFindings.length;
37216
+ const normalizedVerdict = normalizeVerdict(meta.verdict) ?? meta.verdict;
37217
+ const { blockerCount, shouldFixCount, suggestionCount, reviewerCount, totalFindingCount } = resolveRoundCounts(meta);
36851
37218
  this.db.run("BEGIN TRANSACTION");
36852
37219
  try {
36853
37220
  this.db.run(
@@ -36856,7 +37223,7 @@ var FilesystemSync = class {
36856
37223
  reviewer_count = ?, total_finding_count = ?, source = 'orchestrator', parsed_at = ?
36857
37224
  WHERE session_id = ? AND round_number = ?`,
36858
37225
  [
36859
- meta.verdict,
37226
+ normalizedVerdict,
36860
37227
  blockerCount,
36861
37228
  suggestionCount,
36862
37229
  shouldFixCount,
@@ -36877,12 +37244,12 @@ var FilesystemSync = class {
36877
37244
  this.db.run("COMMIT");
36878
37245
  return;
36879
37246
  }
36880
- const roundDir = dirname10(filePath);
37247
+ const roundDir = dirname11(filePath);
36881
37248
  for (const reviewer of meta.reviewers) {
36882
37249
  const reviewerType = reviewer.type ?? "unknown";
36883
37250
  const instanceNumber = reviewer.instance ?? 1;
36884
37251
  const findings = reviewer.findings ?? [];
36885
- const reviewerMdPath = join16(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
37252
+ const reviewerMdPath = join17(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
36886
37253
  this.db.run(
36887
37254
  `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
36888
37255
  VALUES (?, ?, ?, ?, ?, ?)`,
@@ -36956,7 +37323,7 @@ var FilesystemSync = class {
36956
37323
  this.io?.to(`session:${sessionId}`).emit("round:updated", {
36957
37324
  sessionId,
36958
37325
  roundNumber,
36959
- verdict: meta.verdict,
37326
+ verdict: normalizedVerdict,
36960
37327
  blockerCount,
36961
37328
  shouldFixCount,
36962
37329
  suggestionCount,
@@ -37117,11 +37484,12 @@ var FilesystemSync = class {
37117
37484
  );
37118
37485
  } else {
37119
37486
  const parsed = parseFinalMd(content);
37487
+ const parsedVerdict = parsed.verdict ? normalizeVerdict(parsed.verdict) ?? parsed.verdict : parsed.verdict;
37120
37488
  this.db.run(
37121
37489
  `UPDATE review_rounds SET verdict = ?, blocker_count = ?, suggestion_count = ?, should_fix_count = ?, final_md_path = ?, parsed_at = ?, source = 'parser'
37122
37490
  WHERE session_id = ? AND round_number = ?`,
37123
37491
  [
37124
- parsed.verdict,
37492
+ parsedVerdict,
37125
37493
  parsed.blockerCount,
37126
37494
  parsed.suggestionCount,
37127
37495
  parsed.shouldFixCount,
@@ -37151,7 +37519,7 @@ var FilesystemSync = class {
37151
37519
  "SELECT current_phase, phase_number, status FROM sessions WHERE id = ?",
37152
37520
  [sessionId]
37153
37521
  );
37154
- 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)) {
37155
37523
  commitReasonClose(
37156
37524
  this.db,
37157
37525
  sessionId,
@@ -37245,7 +37613,7 @@ var FilesystemSync = class {
37245
37613
  const parts = relFromSessions.split("/");
37246
37614
  const sessionId = parts[0];
37247
37615
  if (!sessionId) return;
37248
- const sessionDir = join16(this.sessionsDir, sessionId);
37616
+ const sessionDir = join17(this.sessionsDir, sessionId);
37249
37617
  this.ensureSessionRow(sessionId, sessionDir);
37250
37618
  const fileName = basename3(filePath);
37251
37619
  const reviewerMatch = relFromSessions.match(/rounds\/round-(\d+)\/reviews\/(.+\.md)$/);
@@ -37318,7 +37686,7 @@ var FilesystemSync = class {
37318
37686
 
37319
37687
  // src/server/services/db-sync-watcher.ts
37320
37688
  import { existsSync as existsSync14 } from "node:fs";
37321
- import { dirname as dirname11, basename as basename4 } from "node:path";
37689
+ import { dirname as dirname12, basename as basename4 } from "node:path";
37322
37690
  import { watch as watch3 } from "chokidar";
37323
37691
  function col(row, key) {
37324
37692
  return row[key] ?? null;
@@ -37360,7 +37728,7 @@ var DbSyncWatcher = class {
37360
37728
  /** Start watching the DB file (and its WAL sidecar) for external writes. */
37361
37729
  startWatching() {
37362
37730
  if (!existsSync14(this.dbFilePath)) return;
37363
- const watchDir = dirname11(this.dbFilePath);
37731
+ const watchDir = dirname12(this.dbFilePath);
37364
37732
  const dbFile = basename4(this.dbFilePath);
37365
37733
  const walFile = `${dbFile}-wal`;
37366
37734
  this.watcher = watch3(watchDir, {
@@ -37622,20 +37990,20 @@ function commandFingerprint(row) {
37622
37990
  }
37623
37991
 
37624
37992
  // src/server/socket/chat-handler.ts
37625
- import { dirname as dirname12 } from "node:path";
37993
+ import { dirname as dirname13 } from "node:path";
37626
37994
 
37627
37995
  // src/server/services/chat-context.ts
37628
37996
  import { readFileSync as readFileSync10, readdirSync as readdirSync3, existsSync as existsSync15 } from "node:fs";
37629
- import { join as join17 } from "node:path";
37997
+ import { join as join18 } from "node:path";
37630
37998
  function buildChatContext(ocrDir, target) {
37631
- const sessionsDir = join17(ocrDir, "sessions");
37999
+ const sessionsDir = join18(ocrDir, "sessions");
37632
38000
  if (target.type === "map_run") {
37633
38001
  return buildMapRunContext(sessionsDir, target.sessionId, target.runNumber);
37634
38002
  }
37635
38003
  return buildReviewRoundContext(sessionsDir, target.sessionId, target.roundNumber);
37636
38004
  }
37637
38005
  function buildMapRunContext(sessionsDir, sessionId, runNumber) {
37638
- const mapPath = join17(
38006
+ const mapPath = join18(
37639
38007
  sessionsDir,
37640
38008
  sessionId,
37641
38009
  "map",
@@ -37662,9 +38030,9 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
37662
38030
  return parts.join("\n");
37663
38031
  }
37664
38032
  function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
37665
- const roundDir = join17(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
37666
- const finalPath = join17(roundDir, "final.md");
37667
- 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");
37668
38036
  const parts = [
37669
38037
  `You are an expert code reviewer assisting with a code review session.`,
37670
38038
  `You are looking at review round #${roundNumber} for session "${sessionId}".`,
@@ -37681,7 +38049,7 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
37681
38049
  if (existsSync15(reviewersDir)) {
37682
38050
  const files = readdirSync3(reviewersDir).filter((f) => f.endsWith(".md")).sort();
37683
38051
  for (const file of files) {
37684
- const content = readFileSync10(join17(reviewersDir, file), "utf-8");
38052
+ const content = readFileSync10(join18(reviewersDir, file), "utf-8");
37685
38053
  const reviewerName = file.replace(/\.md$/, "");
37686
38054
  parts.push("");
37687
38055
  parts.push(`<reviewer name="${reviewerName}">`);
@@ -37745,7 +38113,7 @@ function startTrackedExecution(io2, db, ocrDir, command, args = []) {
37745
38113
  db.run(
37746
38114
  `UPDATE command_executions
37747
38115
  SET exit_code = ?, finished_at = ?, output = ?, pid = NULL
37748
- WHERE id = ?`,
38116
+ WHERE id = ? AND finished_at IS NULL`,
37749
38117
  [exitCode, finishedAt, outputBuffer, executionId]
37750
38118
  );
37751
38119
  appendCommandLog(ocrDir, {
@@ -37835,7 +38203,7 @@ User: ${message}`;
37835
38203
  });
37836
38204
  return;
37837
38205
  }
37838
- const repoRoot = dirname12(ocrDir);
38206
+ const repoRoot = dirname13(ocrDir);
37839
38207
  const spawnResult = adapter.spawn({
37840
38208
  prompt,
37841
38209
  cwd: repoRoot,
@@ -38011,13 +38379,13 @@ function cleanupAllChats() {
38011
38379
  }
38012
38380
 
38013
38381
  // src/server/socket/post-handler.ts
38014
- 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";
38015
38383
  import { tmpdir as tmpdir2 } from "node:os";
38016
- 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";
38017
38385
  import { randomUUID as randomUUID2 } from "node:crypto";
38018
38386
  function resolveSessionDir2(sessionDir, ocrDir) {
38019
38387
  if (isAbsolute2(sessionDir)) return sessionDir;
38020
- return join18(dirname13(ocrDir), sessionDir);
38388
+ return join19(dirname14(ocrDir), sessionDir);
38021
38389
  }
38022
38390
  var BRANCH_PREFIXES = [
38023
38391
  "feat",
@@ -38086,7 +38454,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
38086
38454
  return;
38087
38455
  }
38088
38456
  const branch = session.branch;
38089
- const repoRoot = dirname13(ocrDir);
38457
+ const repoRoot = dirname14(ocrDir);
38090
38458
  try {
38091
38459
  await execBinaryAsync("gh", ["auth", "status"], { env: cleanEnv(), cwd: repoRoot, encoding: "utf-8" });
38092
38460
  } catch {
@@ -38187,16 +38555,16 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
38187
38555
  socket.emit("post:error", { error: "Session not found" });
38188
38556
  return;
38189
38557
  }
38190
- const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join18(ocrDir, "sessions", sessionId);
38191
- const roundDir = join18(sessionDir, "rounds", `round-${roundNumber}`);
38192
- const finalPath = join18(roundDir, "final.md");
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");
38193
38561
  if (!existsSync16(finalPath)) {
38194
38562
  socket.emit("post:error", { error: "final.md not found for this round" });
38195
38563
  return;
38196
38564
  }
38197
- const humanReviewPath = join18(roundDir, "final-human.md");
38198
- const repoRoot = dirname13(ocrDir);
38199
- const commandMdPath = join18(ocrDir, "commands", "translate-review-to-single-human.md");
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");
38200
38568
  let commandContent;
38201
38569
  try {
38202
38570
  commandContent = readFileSync11(commandMdPath, "utf-8");
@@ -38356,10 +38724,10 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
38356
38724
  socket.emit("post:save-result", { success: false, error: "Session not found" });
38357
38725
  return;
38358
38726
  }
38359
- const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join18(ocrDir, "sessions", sessionId);
38360
- const roundDir = join18(sessionDir, "rounds", `round-${roundNumber}`);
38361
- mkdirSync6(roundDir, { recursive: true });
38362
- const filePath = join18(roundDir, "final-human.md");
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");
38363
38731
  writeFileSync5(filePath, content, { mode: 420 });
38364
38732
  socket.emit("post:save-result", { success: true });
38365
38733
  } catch (err) {
@@ -38386,14 +38754,14 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
38386
38754
  );
38387
38755
  tracker.appendOutput(`\u25B8 Posting review to PR #${prNumber}...
38388
38756
  `);
38389
- const tmpDir = join18(tmpdir2(), "ocr-post-comments");
38757
+ const tmpDir = join19(tmpdir2(), "ocr-post-comments");
38390
38758
  try {
38391
- mkdirSync6(tmpDir, { recursive: true, mode: 448 });
38759
+ mkdirSync7(tmpDir, { recursive: true, mode: 448 });
38392
38760
  } catch {
38393
38761
  }
38394
- const tmpFile = join18(tmpDir, `${randomUUID2()}.md`);
38762
+ const tmpFile = join19(tmpDir, `${randomUUID2()}.md`);
38395
38763
  writeFileSync5(tmpFile, content, { mode: 384 });
38396
- const repoRoot = dirname13(ocrDir);
38764
+ const repoRoot = dirname14(ocrDir);
38397
38765
  try {
38398
38766
  const { stdout } = await execBinaryAsync(
38399
38767
  "gh",
@@ -38436,9 +38804,67 @@ function cleanupAllPostGenerations() {
38436
38804
  }
38437
38805
  }
38438
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
+
38439
38865
  // src/server/index.ts
38440
38866
  import { homedir } from "node:os";
38441
- var __dirname3 = dirname14(fileURLToPath3(import.meta.url));
38867
+ var __dirname3 = dirname15(fileURLToPath3(import.meta.url));
38442
38868
  function shortenPath(p) {
38443
38869
  const home = homedir();
38444
38870
  return p.startsWith(home) ? "~" + p.slice(home.length) : p;
@@ -38505,7 +38931,7 @@ if (process.env.NODE_ENV !== "production") {
38505
38931
  function isOcrDashboardProcess(pid) {
38506
38932
  if (process.platform === "win32") return false;
38507
38933
  try {
38508
- const cmd = execFileSync3("ps", ["-p", String(pid), "-o", "command="], {
38934
+ const cmd = execBinary("ps", ["-p", String(pid), "-o", "command="], {
38509
38935
  encoding: "utf-8",
38510
38936
  timeout: 3e3
38511
38937
  }).trim();
@@ -38520,23 +38946,23 @@ async function startServer(options = {}) {
38520
38946
  process.title = "ocr-dashboard";
38521
38947
  const ocrDir = resolveOcrDir();
38522
38948
  const aiCliService = new AiCliService(ocrDir);
38523
- const dbPathForCheckpoint = join19(ocrDir, "data", "ocr.db");
38949
+ const dbPathForCheckpoint = join20(ocrDir, "data", "ocr.db");
38524
38950
  const walResult = walCheckpointTruncate(dbPathForCheckpoint);
38525
38951
  if (walResult === "checkpointed") {
38526
38952
  console.log(" WAL checkpoint: truncated stale write-ahead-log file");
38527
38953
  }
38528
- for (const reaped of reapOrphanDbFiles(join19(ocrDir, "data"))) {
38954
+ for (const reaped of reapOrphanDbFiles(join20(ocrDir, "data"))) {
38529
38955
  console.log(` Orphan reap: removed stale ${reaped}`);
38530
38956
  }
38531
- const staleLogs = reapStaleExecLogs(join19(ocrDir, "data", "exec-logs"));
38957
+ const staleLogs = reapStaleExecLogs(join20(ocrDir, "data", "exec-logs"));
38532
38958
  if (staleLogs.length > 0) {
38533
38959
  console.log(` Exec-log reap: removed ${staleLogs.length} stale agent log(s)`);
38534
38960
  }
38535
38961
  const db = await openDb(ocrDir);
38536
- const dataDir = join19(ocrDir, "data");
38537
- const pidFilePath = join19(dataDir, "dashboard.pid");
38538
- const portFilePath = join19(dataDir, "server-port");
38539
- mkdirSync7(dataDir, { recursive: true });
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 });
38540
38966
  try {
38541
38967
  unlinkSync5(portFilePath);
38542
38968
  } catch {
@@ -38634,12 +39060,39 @@ async function startServer(options = {}) {
38634
39060
  }
38635
39061
  };
38636
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();
38637
39089
  const SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
38638
39090
  const sweepTimer = setInterval(() => {
38639
39091
  try {
38640
39092
  logAgentSweep(sweepStaleAgentSessions(db, heartbeatSeconds, defaultIsAlive));
38641
39093
  sweepStaleSessions(db, STALE_SESSION_THRESHOLD_SECONDS);
38642
39094
  void reconcileCompleted();
39095
+ runForwardResume();
38643
39096
  } catch (err) {
38644
39097
  console.error("[sweep] periodic sweep failed:", err);
38645
39098
  }
@@ -38675,10 +39128,10 @@ async function startServer(options = {}) {
38675
39128
  app.use("/api/agent-sessions", createAgentSessionsRouter(db, () => pullSync()));
38676
39129
  app.use("/api/sessions", createHandoffRouter(sessionCapture, ocrDir, () => pullSync()));
38677
39130
  app.use("/api/team", createTeamRouter(ocrDir));
38678
- const clientDir = join19(__dirname3, "client");
39131
+ const clientDir = join20(__dirname3, "client");
38679
39132
  if (process.env.NODE_ENV === "production" && existsSync17(clientDir)) {
38680
39133
  app.use(import_express15.default.static(clientDir, { index: false }));
38681
- const indexHtmlPath = join19(clientDir, "index.html");
39134
+ const indexHtmlPath = join20(clientDir, "index.html");
38682
39135
  const rawIndexHtml = existsSync17(indexHtmlPath) ? readFileSync12(indexHtmlPath, "utf-8") : "";
38683
39136
  const tokenScript = `<script>window.__OCR_TOKEN__=${JSON.stringify(AUTH_TOKEN)};</script>`;
38684
39137
  const injectedIndexHtml = rawIndexHtml.replace(
@@ -38698,7 +39151,7 @@ async function startServer(options = {}) {
38698
39151
  registerChatHandlers(io, socket, db, ocrDir, aiCliService);
38699
39152
  registerPostHandlers(io, socket, db, ocrDir, aiCliService);
38700
39153
  });
38701
- const dbFilePath = join19(ocrDir, "data", "ocr.db");
39154
+ const dbFilePath = join20(ocrDir, "data", "ocr.db");
38702
39155
  const dbSyncWatcher = new DbSyncWatcher(
38703
39156
  db,
38704
39157
  dbFilePath,
@@ -38714,7 +39167,7 @@ async function startServer(options = {}) {
38714
39167
  dbSyncWatcher.startWatching();
38715
39168
  pullSync = () => dbSyncWatcher.syncFromDisk();
38716
39169
  console.log(` Watching DB: ${shortenPath(dbFilePath)}`);
38717
- const sessionsDir = join19(ocrDir, "sessions");
39170
+ const sessionsDir = join20(ocrDir, "sessions");
38718
39171
  const fsSync = new FilesystemSync(db, sessionsDir, io);
38719
39172
  await fsSync.fullScan();
38720
39173
  fsSync.startWatching();
@@ -38778,7 +39231,7 @@ async function startServer(options = {}) {
38778
39231
  unlinkSync5(portFilePath);
38779
39232
  } catch {
38780
39233
  }
38781
- clearSpawnMarker(ocrDir);
39234
+ clearAllSpawnMarkers(ocrDir);
38782
39235
  try {
38783
39236
  const activeResult = db.exec(
38784
39237
  "SELECT id, pid FROM command_executions WHERE pid IS NOT NULL AND finished_at IS NULL"