@open-code-review/cli 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +67 -121
  2. package/dist/dashboard/client/assets/{_basePickBy-BJKCdvle.js → _basePickBy-DbLJVCA4.js} +1 -1
  3. package/dist/dashboard/client/assets/{_baseUniq-L_sxIO0r.js → _baseUniq-IXEG0cJJ.js} +1 -1
  4. package/dist/dashboard/client/assets/{arc-tqAEcLt5.js → arc-lsKxmOJY.js} +1 -1
  5. package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-CrKQo6Ye.js → architectureDiagram-VXUJARFQ-DfMlzFJX.js} +1 -1
  6. package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-DXOc89nw.js → blockDiagram-VD42YOAC-bSpnd26J.js} +1 -1
  7. package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-Ba-jYbw0.js → c4Diagram-YG6GDRKO-DPYmVhCZ.js} +1 -1
  8. package/dist/dashboard/client/assets/channel-C--wY_Wd.js +1 -0
  9. package/dist/dashboard/client/assets/{chunk-4BX2VUAB-D1G3HCqL.js → chunk-4BX2VUAB-CI9zC4lV.js} +1 -1
  10. package/dist/dashboard/client/assets/{chunk-55IACEB6-FI7g4AjR.js → chunk-55IACEB6-BqUdJdx5.js} +1 -1
  11. package/dist/dashboard/client/assets/{chunk-B4BG7PRW-DhEGFGWs.js → chunk-B4BG7PRW-DymQrTp-.js} +1 -1
  12. package/dist/dashboard/client/assets/{chunk-DI55MBZ5-Da3-6ZE4.js → chunk-DI55MBZ5-lZ_9LKGJ.js} +1 -1
  13. package/dist/dashboard/client/assets/{chunk-FMBD7UC4-D0QLOjiy.js → chunk-FMBD7UC4-DC5rgLNm.js} +1 -1
  14. package/dist/dashboard/client/assets/{chunk-QN33PNHL-WkfgpbLo.js → chunk-QN33PNHL-BrygpHrX.js} +1 -1
  15. package/dist/dashboard/client/assets/{chunk-QZHKN3VN-Bqn0IO1w.js → chunk-QZHKN3VN-CWJqBdNg.js} +1 -1
  16. package/dist/dashboard/client/assets/{chunk-TZMSLE5B-CC_K_BeL.js → chunk-TZMSLE5B-BACgM5pG.js} +1 -1
  17. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-DoxmMlnf.js +1 -0
  18. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-DoxmMlnf.js +1 -0
  19. package/dist/dashboard/client/assets/clone-BgvweD4v.js +1 -0
  20. package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-D8urqxIF.js → cose-bilkent-S5V4N54A-BYvGIfo0.js} +1 -1
  21. package/dist/dashboard/client/assets/{dagre-6UL2VRFP-w2xS0ztU.js → dagre-6UL2VRFP-B1rZyiLJ.js} +1 -1
  22. package/dist/dashboard/client/assets/{diagram-PSM6KHXK-DlOtv6zO.js → diagram-PSM6KHXK-Dvl5dQMd.js} +1 -1
  23. package/dist/dashboard/client/assets/{diagram-QEK2KX5R-EpxsVLZY.js → diagram-QEK2KX5R-Cmntmhht.js} +1 -1
  24. package/dist/dashboard/client/assets/{diagram-S2PKOQOG-kmITzl42.js → diagram-S2PKOQOG-BqZcpG85.js} +1 -1
  25. package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-Bvyepu_Z.js → erDiagram-Q2GNP2WA-Cw7BALso.js} +1 -1
  26. package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-BokLAZN0.js → flowDiagram-NV44I4VS-B_amTHzQ.js} +1 -1
  27. package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-i5ZSGuTN.js → ganttDiagram-JELNMOA3-B1j2-sTo.js} +1 -1
  28. package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-CIayQ8P9.js → gitGraphDiagram-V2S2FVAM-D5BkfAMt.js} +1 -1
  29. package/dist/dashboard/client/assets/{graph-C3ouLF2F.js → graph-B_v15DHv.js} +1 -1
  30. package/dist/dashboard/client/assets/index-UkJZZdYD.js +548 -0
  31. package/dist/dashboard/client/assets/index-Zl---B_3.css +1 -0
  32. package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-wxe8NO00.js → infoDiagram-HS3SLOUP-C4dtIkj3.js} +1 -1
  33. package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-BeHCbOFN.js → journeyDiagram-XKPGCS4Q-hha4Am8v.js} +1 -1
  34. package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-DxUlb4wo.js → kanban-definition-3W4ZIXB7-1EY8l7Ng.js} +1 -1
  35. package/dist/dashboard/client/assets/{layout-CYsQ5kjv.js → layout-7SmAbjFT.js} +1 -1
  36. package/dist/dashboard/client/assets/{linear-ByuMiLUn.js → linear-BfjSBezh.js} +1 -1
  37. package/dist/dashboard/client/assets/{mermaid-renderer-cx-n1jFM.js → mermaid-renderer-PPIt-kY4.js} +4 -4
  38. package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-CI5zvW3G.js → mindmap-definition-VGOIOE7T-BFpjN9LY.js} +1 -1
  39. package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-lC7QV-4L.js → pieDiagram-ADFJNKIX-GBbQtDBQ.js} +1 -1
  40. package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-DI7Bn_fF.js → quadrantDiagram-AYHSOK5B-Dm0vOhOw.js} +1 -1
  41. package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-BVuFGUp6.js → requirementDiagram-UZGBJVZJ-BrKONIV8.js} +1 -1
  42. package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-C-3hBPRk.js → sankeyDiagram-TZEHDZUN-IOobtmDc.js} +1 -1
  43. package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-CLS6xCbv.js → sequenceDiagram-WL72ISMW-Dnb0bOW5.js} +1 -1
  44. package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-XOLrkoEE.js → stateDiagram-FKZM4ZOC-C9-bf7bn.js} +1 -1
  45. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-C8Gr4khP.js +1 -0
  46. package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-N9m6IkH5.js → timeline-definition-IT6M3QCI-tJogDEHB.js} +1 -1
  47. package/dist/dashboard/client/assets/{treemap-GDKQZRPO-ayvdfxB1.js → treemap-GDKQZRPO-DQY6HADq.js} +1 -1
  48. package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-CUmVEVIH.js → xychartDiagram-PRI3JC2R-DfxeQmTO.js} +1 -1
  49. package/dist/dashboard/client/index.html +2 -2
  50. package/dist/dashboard/server.js +810 -206
  51. package/dist/index.js +810 -15
  52. package/dist/lib/db/index.js +522 -0
  53. package/package.json +2 -2
  54. package/dist/dashboard/client/assets/channel-OmrThJE3.js +0 -1
  55. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-Dg5ffKNR.js +0 -1
  56. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-Dg5ffKNR.js +0 -1
  57. package/dist/dashboard/client/assets/clone-CKI4Qu1i.js +0 -1
  58. package/dist/dashboard/client/assets/index-CPEavIIM.css +0 -1
  59. package/dist/dashboard/client/assets/index-icxlpW-l.js +0 -456
  60. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-Cy33HZ1p.js +0 -1
@@ -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 dirname10 = path2.dirname;
18413
+ var dirname11 = path2.dirname;
18414
18414
  var basename3 = path2.basename;
18415
18415
  var extname = path2.extname;
18416
- var join13 = path2.join;
18416
+ var join14 = 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 = dirname10(loc);
18452
+ var dir = dirname11(loc);
18453
18453
  var file = basename3(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 = join13(dir, file);
18464
+ var path3 = join14(dir, file);
18465
18465
  var stat = tryStat(path3);
18466
18466
  if (stat && stat.isFile()) {
18467
18467
  return path3;
18468
18468
  }
18469
- path3 = join13(dir, basename3(file, ext), "index" + ext);
18469
+ path3 = join14(dir, basename3(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 join13 = path2.join;
19102
+ var join14 = 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(join13(root, path3));
19321
+ path3 = normalize(join14(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 = join13(path3, self._index[i]);
19456
+ var p = join14(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);
@@ -20592,7 +20592,7 @@ var require_application = __commonJS({
20592
20592
  "../../node_modules/.pnpm/express@4.22.1/node_modules/express/lib/application.js"(exports, module) {
20593
20593
  "use strict";
20594
20594
  var finalhandler = require_finalhandler();
20595
- var Router11 = require_router();
20595
+ var Router12 = require_router();
20596
20596
  var methods = require_methods();
20597
20597
  var middleware = require_init();
20598
20598
  var query = require_query();
@@ -20657,7 +20657,7 @@ var require_application = __commonJS({
20657
20657
  };
20658
20658
  app2.lazyrouter = function lazyrouter() {
20659
20659
  if (!this._router) {
20660
- this._router = new Router11({
20660
+ this._router = new Router12({
20661
20661
  caseSensitive: this.enabled("case sensitive routing"),
20662
20662
  strict: this.enabled("strict routing")
20663
20663
  });
@@ -22521,7 +22521,7 @@ var require_express = __commonJS({
22521
22521
  var mixin = require_merge_descriptors();
22522
22522
  var proto = require_application();
22523
22523
  var Route = require_route();
22524
- var Router11 = require_router();
22524
+ var Router12 = require_router();
22525
22525
  var req = require_request();
22526
22526
  var res = require_response();
22527
22527
  exports = module.exports = createApplication;
@@ -22544,7 +22544,7 @@ var require_express = __commonJS({
22544
22544
  exports.request = req;
22545
22545
  exports.response = res;
22546
22546
  exports.Route = Route;
22547
- exports.Router = Router11;
22547
+ exports.Router = Router12;
22548
22548
  exports.json = bodyParser.json;
22549
22549
  exports.query = require_query();
22550
22550
  exports.raw = bodyParser.raw;
@@ -23179,10 +23179,10 @@ var init_open = __esm({
23179
23179
  });
23180
23180
 
23181
23181
  // src/server/index.ts
23182
- var import_express11 = __toESM(require_express2(), 1);
23182
+ var import_express12 = __toESM(require_express2(), 1);
23183
23183
  import { createServer } from "node:http";
23184
- import { existsSync as existsSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3 } from "node:fs";
23185
- import { join as join12, dirname as dirname9, resolve as resolve2 } from "node:path";
23184
+ import { existsSync as existsSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3 } from "node:fs";
23185
+ import { join as join13, dirname as dirname10, resolve as resolve2 } from "node:path";
23186
23186
  import { fileURLToPath as fileURLToPath3 } from "node:url";
23187
23187
  import { randomBytes } from "node:crypto";
23188
23188
  import { Server as SocketIOServer } from "socket.io";
@@ -23440,6 +23440,30 @@ var MIGRATIONS = [
23440
23440
  CREATE INDEX idx_events_session ON orchestration_events(session_id);
23441
23441
  CREATE INDEX idx_events_type ON orchestration_events(event_type);
23442
23442
  `
23443
+ },
23444
+ {
23445
+ version: 6,
23446
+ description: "Add orchestrator-first columns to review_rounds for round-meta.json support",
23447
+ sql: `
23448
+ ALTER TABLE review_rounds ADD COLUMN source TEXT DEFAULT NULL;
23449
+ ALTER TABLE review_rounds ADD COLUMN reviewer_count INTEGER DEFAULT 0;
23450
+ ALTER TABLE review_rounds ADD COLUMN total_finding_count INTEGER DEFAULT 0;
23451
+ `
23452
+ },
23453
+ {
23454
+ version: 7,
23455
+ description: "Add category column to review_findings for blocker/should_fix/suggestion classification",
23456
+ sql: `
23457
+ ALTER TABLE review_findings ADD COLUMN category TEXT DEFAULT NULL;
23458
+ `
23459
+ },
23460
+ {
23461
+ version: 8,
23462
+ description: "Add orchestrator-first columns to map_runs for map-meta.json support",
23463
+ sql: `
23464
+ ALTER TABLE map_runs ADD COLUMN source TEXT DEFAULT NULL;
23465
+ ALTER TABLE map_runs ADD COLUMN section_count INTEGER DEFAULT 0;
23466
+ `
23443
23467
  }
23444
23468
  ];
23445
23469
  function ensureSchemaVersionTable(db) {
@@ -23984,24 +24008,24 @@ function enrichSession(db, session) {
23984
24008
  let reviewPhaseNumber = 0;
23985
24009
  let reviewPhase = "";
23986
24010
  if (hasReview) {
24011
+ const derived = deriveReviewPhase(db, session.id);
23987
24012
  if (session.workflow_type === "review") {
23988
- reviewPhaseNumber = session.phase_number;
23989
- reviewPhase = session.current_phase;
24013
+ reviewPhaseNumber = Math.max(session.phase_number, derived);
23990
24014
  } else {
23991
- reviewPhaseNumber = deriveReviewPhase(db, session.id);
23992
- reviewPhase = REVIEW_PHASE_NAMES[reviewPhaseNumber - 1] ?? "context";
24015
+ reviewPhaseNumber = derived;
23993
24016
  }
24017
+ reviewPhase = REVIEW_PHASE_NAMES[reviewPhaseNumber - 1] ?? "context";
23994
24018
  }
23995
24019
  let mapPhaseNumber = 0;
23996
24020
  let mapPhase = "";
23997
24021
  if (hasMap) {
24022
+ const derived = deriveMapPhase(db, session.id);
23998
24023
  if (session.workflow_type === "map") {
23999
- mapPhaseNumber = session.phase_number;
24000
- mapPhase = session.current_phase;
24024
+ mapPhaseNumber = Math.max(session.phase_number, derived);
24001
24025
  } else {
24002
- mapPhaseNumber = deriveMapPhase(db, session.id);
24003
- mapPhase = MAP_PHASE_NAMES[mapPhaseNumber - 1] ?? "map-context";
24026
+ mapPhaseNumber = derived;
24004
24027
  }
24028
+ mapPhase = MAP_PHASE_NAMES[mapPhaseNumber - 1] ?? "map-context";
24005
24029
  }
24006
24030
  const latestRound = rounds.length > 0 ? rounds[rounds.length - 1] : null;
24007
24031
  const latestVerdict = latestRound?.verdict ?? null;
@@ -25166,11 +25190,37 @@ function resolveLocalCli() {
25166
25190
  }
25167
25191
 
25168
25192
  // src/server/socket/command-runner.ts
25193
+ function shellSplit(str) {
25194
+ const tokens = [];
25195
+ let current = "";
25196
+ let quote = null;
25197
+ for (let i = 0; i < str.length; i++) {
25198
+ const ch = str[i];
25199
+ if (quote) {
25200
+ if (ch === quote) {
25201
+ quote = null;
25202
+ } else {
25203
+ current += ch;
25204
+ }
25205
+ } else if (ch === '"' || ch === "'") {
25206
+ quote = ch;
25207
+ } else if (/\s/.test(ch)) {
25208
+ if (current) {
25209
+ tokens.push(current);
25210
+ current = "";
25211
+ }
25212
+ } else {
25213
+ current += ch;
25214
+ }
25215
+ }
25216
+ if (current) tokens.push(current);
25217
+ return tokens;
25218
+ }
25169
25219
  var ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
25170
25220
  "progress",
25171
25221
  "state"
25172
25222
  ]);
25173
- var AI_COMMANDS = /* @__PURE__ */ new Set(["map", "review", "translate-review-to-single-human", "address"]);
25223
+ var AI_COMMANDS = /* @__PURE__ */ new Set(["map", "review", "translate-review-to-single-human", "address", "create-reviewer", "sync-reviewers"]);
25174
25224
  var MAX_CONCURRENT = 3;
25175
25225
  var activeCommands = /* @__PURE__ */ new Map();
25176
25226
  function getActiveCommands() {
@@ -25192,7 +25242,7 @@ function registerCommandHandlers(io2, socket, db, ocrDir, aiCliService) {
25192
25242
  }
25193
25243
  const { command } = payload;
25194
25244
  const normalized = command.replace(/^ocr\s+/, "");
25195
- const parts = normalized.split(/\s+/);
25245
+ const parts = shellSplit(normalized);
25196
25246
  const baseCommand = parts[0] ?? "";
25197
25247
  const subArgs = parts.slice(1);
25198
25248
  if (!ALLOWED_COMMANDS.has(baseCommand) && !AI_COMMANDS.has(baseCommand)) {
@@ -25349,34 +25399,68 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
25349
25399
  finishExecution(io2, db, ocrDir, executionId, 1, content);
25350
25400
  return;
25351
25401
  }
25352
- let target = "staged changes";
25353
- let requirements = "";
25354
- const options = [];
25355
- let i = 0;
25356
- while (i < subArgs.length) {
25357
- const arg = subArgs[i] ?? "";
25358
- if (arg === "--fresh") {
25359
- options.push("--fresh");
25360
- i++;
25361
- } else if (arg === "--requirements" && i + 1 < subArgs.length) {
25362
- requirements = subArgs.slice(i + 1).join(" ");
25363
- break;
25364
- } else if (!arg.startsWith("--")) {
25365
- target = arg;
25366
- i++;
25367
- } else {
25368
- i++;
25402
+ const promptLines = [];
25403
+ if (baseCommand === "create-reviewer" || baseCommand === "sync-reviewers") {
25404
+ const argsStr = subArgs.length > 0 ? subArgs.join(" ") : "";
25405
+ promptLines.push(
25406
+ `Follow the instructions below to run the OCR ${baseCommand} workflow.`,
25407
+ "",
25408
+ `Arguments: ${argsStr || "none"}`
25409
+ );
25410
+ } else {
25411
+ let target = "staged changes";
25412
+ let requirements = "";
25413
+ let team = "";
25414
+ const reviewerDescriptions = [];
25415
+ const options = [];
25416
+ let i = 0;
25417
+ while (i < subArgs.length) {
25418
+ const arg = subArgs[i] ?? "";
25419
+ if (arg === "--fresh") {
25420
+ options.push("--fresh");
25421
+ i++;
25422
+ } else if (arg === "--requirements" && i + 1 < subArgs.length) {
25423
+ requirements = subArgs.slice(i + 1).join(" ");
25424
+ break;
25425
+ } else if (arg === "--team" && i + 1 < subArgs.length) {
25426
+ team = subArgs[i + 1] ?? "";
25427
+ i += 2;
25428
+ } else if (arg === "--reviewer" && i + 1 < subArgs.length) {
25429
+ const raw = subArgs[i + 1] ?? "";
25430
+ const countMatch = raw.match(/^(\d+):(.+)$/);
25431
+ if (countMatch) {
25432
+ reviewerDescriptions.push({ description: countMatch[2], count: parseInt(countMatch[1], 10) });
25433
+ } else {
25434
+ reviewerDescriptions.push({ description: raw, count: 1 });
25435
+ }
25436
+ i += 2;
25437
+ } else if (!arg.startsWith("--")) {
25438
+ target = arg;
25439
+ i++;
25440
+ } else {
25441
+ i++;
25442
+ }
25443
+ }
25444
+ const optionsStr = options.length > 0 ? options.join(" ") : "none";
25445
+ promptLines.push(
25446
+ `Follow the instructions below to run the OCR ${baseCommand} workflow.`,
25447
+ "",
25448
+ `Target: ${target}`,
25449
+ `Options: ${optionsStr}`
25450
+ );
25451
+ if (team) {
25452
+ promptLines.push(`Team: ${team}`);
25453
+ }
25454
+ for (const { description, count } of reviewerDescriptions) {
25455
+ if (count > 1) {
25456
+ promptLines.push(`Reviewer (x${count}): ${description}`);
25457
+ } else {
25458
+ promptLines.push(`Reviewer: ${description}`);
25459
+ }
25460
+ }
25461
+ if (requirements) {
25462
+ promptLines.push(`Requirements: ${requirements}`);
25369
25463
  }
25370
- }
25371
- const optionsStr = options.length > 0 ? options.join(" ") : "none";
25372
- const promptLines = [
25373
- `Follow the instructions below to run the OCR ${baseCommand} workflow.`,
25374
- "",
25375
- `Target: ${target}`,
25376
- `Options: ${optionsStr}`
25377
- ];
25378
- if (requirements) {
25379
- promptLines.push(`Requirements: ${requirements}`);
25380
25464
  }
25381
25465
  const localCli = resolveLocalCli();
25382
25466
  if (localCli) {
@@ -25744,10 +25828,77 @@ function createChatRouter(db, ocrDir) {
25744
25828
  return router;
25745
25829
  }
25746
25830
 
25831
+ // src/server/routes/reviewers.ts
25832
+ var import_express11 = __toESM(require_express2(), 1);
25833
+ import { readFileSync as readFileSync5, existsSync as existsSync4, watch } from "node:fs";
25834
+ import { join as join9 } from "node:path";
25835
+ function readReviewersMeta(ocrDir) {
25836
+ const metaPath = join9(ocrDir, "reviewers-meta.json");
25837
+ if (!existsSync4(metaPath)) {
25838
+ return { reviewers: [], defaults: [] };
25839
+ }
25840
+ try {
25841
+ const raw = readFileSync5(metaPath, "utf-8");
25842
+ const meta = JSON.parse(raw);
25843
+ const reviewers = meta.reviewers ?? [];
25844
+ const defaults = reviewers.filter((r) => r.is_default).map((r) => r.id);
25845
+ return { reviewers, defaults };
25846
+ } catch {
25847
+ return { reviewers: [], defaults: [] };
25848
+ }
25849
+ }
25850
+ var VALID_ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
25851
+ function createReviewersRouter(ocrDir) {
25852
+ const router = (0, import_express11.Router)();
25853
+ router.get("/", (_req, res) => {
25854
+ res.json(readReviewersMeta(ocrDir));
25855
+ });
25856
+ router.get("/:id/prompt", (req, res) => {
25857
+ const { id } = req.params;
25858
+ if (!id || !VALID_ID_RE.test(id)) {
25859
+ res.status(400).json({ error: "Invalid reviewer ID" });
25860
+ return;
25861
+ }
25862
+ const filePath = join9(ocrDir, "skills", "references", "reviewers", `${id}.md`);
25863
+ if (!existsSync4(filePath)) {
25864
+ res.status(404).json({ error: "Reviewer not found", id });
25865
+ return;
25866
+ }
25867
+ try {
25868
+ const content = readFileSync5(filePath, "utf-8");
25869
+ res.json({ id, content });
25870
+ } catch {
25871
+ res.status(500).json({ error: "Failed to read reviewer file", id });
25872
+ }
25873
+ });
25874
+ return router;
25875
+ }
25876
+ function watchReviewersMeta(ocrDir, io2) {
25877
+ const metaPath = join9(ocrDir, "reviewers-meta.json");
25878
+ let watcher = null;
25879
+ let debounce;
25880
+ try {
25881
+ watcher = watch(metaPath, () => {
25882
+ clearTimeout(debounce);
25883
+ debounce = setTimeout(() => {
25884
+ const data = readReviewersMeta(ocrDir);
25885
+ io2.emit("reviewers:updated", data);
25886
+ }, 200);
25887
+ });
25888
+ watcher.on("error", () => {
25889
+ });
25890
+ } catch {
25891
+ }
25892
+ return () => {
25893
+ clearTimeout(debounce);
25894
+ watcher?.close();
25895
+ };
25896
+ }
25897
+
25747
25898
  // src/server/services/filesystem-sync.ts
25748
- import { readdirSync, readFileSync as readFileSync5, statSync, existsSync as existsSync4 } from "node:fs";
25749
- import { join as join9, basename as basename2, relative } from "node:path";
25750
- import { watch } from "chokidar";
25899
+ import { readdirSync, readFileSync as readFileSync6, statSync, existsSync as existsSync5 } from "node:fs";
25900
+ import { join as join10, basename as basename2, dirname as dirname7, relative } from "node:path";
25901
+ import { watch as watch2 } from "chokidar";
25751
25902
 
25752
25903
  // src/server/services/parsers/reviewer-parser.ts
25753
25904
  var FINDING_HEADING_RE = /^#{2,3}\s+(?:Finding|Issue|Suggestion)\s*(?:\d+)?\s*[:\s]*\s*(.*)/i;
@@ -25838,9 +25989,9 @@ function finalizeFinding(partial, summaryLines) {
25838
25989
 
25839
25990
  // src/server/services/parsers/final-parser.ts
25840
25991
  var VERDICT_RE = /^\*?\*?\s*(?:##\s*)?Verdict\s*\*?\*?\s*:?\s*\*?\*?\s*(.*)/im;
25841
- var BLOCKERS_RE = /\*?\*?Blockers?\*?\*?\s*:?\s*(\d+)/i;
25842
- var SHOULD_FIX_RE = /\*?\*?Should\s*Fix\*?\*?\s*:?\s*(\d+)/i;
25843
- var SUGGESTIONS_RE = /\*?\*?Suggestions?\*?\*?\s*:?\s*(\d+)/i;
25992
+ var BLOCKERS_RE = /^\*\*Blockers?\*\*\s*:?\s*(\d+)/im;
25993
+ var SHOULD_FIX_RE = /^\*\*Should\s*Fix\*\*\s*:?\s*(\d+)/im;
25994
+ var SUGGESTIONS_RE = /^\*\*Suggestions?\*\*\s*:?\s*(\d+)/im;
25844
25995
  function parseFinalMd(content) {
25845
25996
  let verdict = null;
25846
25997
  const verdictMatch = content.match(VERDICT_RE);
@@ -25854,25 +26005,39 @@ function parseFinalMd(content) {
25854
26005
  let shouldFixCount = shouldFixMatch ? parseInt(shouldFixMatch[1] ?? "0", 10) : 0;
25855
26006
  let suggestionCount = suggestionsMatch ? parseInt(suggestionsMatch[1] ?? "0", 10) : 0;
25856
26007
  if (!blockerMatch) {
25857
- blockerCount = countSectionHeaders(content, /^##\s+Blockers?\b/im);
26008
+ blockerCount = countSectionItems(content, /^##\s+Blockers?\b/im);
25858
26009
  }
25859
26010
  if (!shouldFixMatch) {
25860
- shouldFixCount = countSectionHeaders(content, /^##\s+Should\s*Fix\b/im);
26011
+ shouldFixCount = countSectionItems(content, /^##\s+Should\s*Fix\b/im);
25861
26012
  }
25862
26013
  if (!suggestionsMatch) {
25863
- suggestionCount = countSectionHeaders(content, /^##\s+Suggestions?\b/im);
26014
+ suggestionCount = countSectionItems(content, /^##\s+Suggestions?\b/im);
25864
26015
  }
25865
26016
  return { verdict, blockerCount, shouldFixCount, suggestionCount };
25866
26017
  }
25867
- function countSectionHeaders(content, sectionRe) {
26018
+ function countSectionItems(content, sectionRe) {
25868
26019
  const match = content.match(sectionRe);
25869
- if (!match?.index) return 0;
26020
+ if (!match?.index && match?.index !== 0) return 0;
25870
26021
  const afterSection = content.slice(match.index + (match[0]?.length ?? 0));
25871
26022
  const lines = afterSection.split("\n");
25872
26023
  let count = 0;
25873
26024
  for (const line of lines) {
25874
- if (line.match(/^##\s+[^#]/) && !line.match(/^###/)) break;
25875
- if (line.match(/^###\s+\d+\./)) count++;
26025
+ const trimmed = line.trim();
26026
+ if (/^##\s+[^#]/.test(trimmed)) break;
26027
+ if (/^---+\s*$/.test(trimmed)) break;
26028
+ if (/^###\s+\d+\./.test(trimmed)) {
26029
+ count++;
26030
+ continue;
26031
+ }
26032
+ if (/^###\s+[^\w\s#]/.test(trimmed)) {
26033
+ count++;
26034
+ continue;
26035
+ }
26036
+ if (/^-\s+\S/.test(trimmed)) {
26037
+ if (/^-\s+(?:none\b|no\s|n\/a\b)/i.test(trimmed)) continue;
26038
+ count++;
26039
+ continue;
26040
+ }
25876
26041
  }
25877
26042
  return count;
25878
26043
  }
@@ -25913,60 +26078,68 @@ var FilesystemSync = class {
25913
26078
  debounceTimers = /* @__PURE__ */ new Map();
25914
26079
  onSync;
25915
26080
  // ── 6.1: Full Scan ──
25916
- fullScan() {
25917
- if (!existsSync4(this.sessionsDir)) return;
26081
+ async fullScan() {
26082
+ if (!existsSync5(this.sessionsDir)) return;
25918
26083
  const entries = readdirSync(this.sessionsDir, { withFileTypes: true });
25919
26084
  for (const entry of entries) {
25920
26085
  if (!entry.isDirectory()) continue;
25921
26086
  const sessionId = entry.name;
25922
- const sessionDir = join9(this.sessionsDir, sessionId);
26087
+ const sessionDir = join10(this.sessionsDir, sessionId);
25923
26088
  this.syncSession(sessionId, sessionDir);
25924
26089
  }
25925
26090
  }
25926
26091
  syncSession(sessionId, sessionDir) {
25927
26092
  this.ensureSessionRow(sessionId, sessionDir);
25928
- const roundsDir = join9(sessionDir, "rounds");
25929
- if (existsSync4(roundsDir)) {
26093
+ const roundsDir = join10(sessionDir, "rounds");
26094
+ if (existsSync5(roundsDir)) {
25930
26095
  const rounds = readdirSync(roundsDir, { withFileTypes: true });
25931
26096
  for (const roundEntry of rounds) {
25932
26097
  if (!roundEntry.isDirectory()) continue;
25933
26098
  const roundMatch = roundEntry.name.match(/^round-(\d+)$/);
25934
26099
  if (!roundMatch) continue;
25935
26100
  const roundNumber = parseInt(roundMatch[1] ?? "0", 10);
25936
- const roundDir = join9(roundsDir, roundEntry.name);
25937
- const reviewsDir = join9(roundDir, "reviews");
25938
- if (existsSync4(reviewsDir)) {
26101
+ const roundDir = join10(roundsDir, roundEntry.name);
26102
+ const reviewsDir = join10(roundDir, "reviews");
26103
+ if (existsSync5(reviewsDir)) {
25939
26104
  const reviewFiles = readdirSync(reviewsDir).filter((f) => f.endsWith(".md"));
25940
26105
  for (const reviewFile of reviewFiles) {
25941
- const filePath = join9(reviewsDir, reviewFile);
26106
+ const filePath = join10(reviewsDir, reviewFile);
25942
26107
  this.processReviewerOutput(sessionId, roundNumber, filePath, reviewFile);
25943
26108
  }
25944
26109
  }
25945
- const finalPath = join9(roundDir, "final.md");
25946
- if (existsSync4(finalPath)) {
26110
+ const roundMetaPath = join10(roundDir, "round-meta.json");
26111
+ if (existsSync5(roundMetaPath)) {
26112
+ this.processRoundMeta(sessionId, roundNumber, roundMetaPath);
26113
+ }
26114
+ const finalPath = join10(roundDir, "final.md");
26115
+ if (existsSync5(finalPath)) {
25947
26116
  this.processFinalMd(sessionId, roundNumber, finalPath);
25948
26117
  }
25949
- const finalHumanPath = join9(roundDir, "final-human.md");
25950
- if (existsSync4(finalHumanPath)) {
26118
+ const finalHumanPath = join10(roundDir, "final-human.md");
26119
+ if (existsSync5(finalHumanPath)) {
25951
26120
  this.processGenericArtifact(sessionId, "final-human", finalHumanPath, roundNumber);
25952
26121
  }
25953
- const discoursePath = join9(roundDir, "discourse.md");
25954
- if (existsSync4(discoursePath)) {
26122
+ const discoursePath = join10(roundDir, "discourse.md");
26123
+ if (existsSync5(discoursePath)) {
25955
26124
  this.processGenericArtifact(sessionId, "discourse", discoursePath, roundNumber);
25956
26125
  }
25957
26126
  }
25958
26127
  }
25959
- const mapDir = join9(sessionDir, "map", "runs");
25960
- if (existsSync4(mapDir)) {
26128
+ const mapDir = join10(sessionDir, "map", "runs");
26129
+ if (existsSync5(mapDir)) {
25961
26130
  const runs = readdirSync(mapDir, { withFileTypes: true });
25962
26131
  for (const runEntry of runs) {
25963
26132
  if (!runEntry.isDirectory()) continue;
25964
26133
  const runMatch = runEntry.name.match(/^run-(\d+)$/);
25965
26134
  if (!runMatch) continue;
25966
26135
  const runNumber = parseInt(runMatch[1] ?? "0", 10);
25967
- const runDir = join9(mapDir, runEntry.name);
25968
- const mapPath = join9(runDir, "map.md");
25969
- if (existsSync4(mapPath)) {
26136
+ const runDir = join10(mapDir, runEntry.name);
26137
+ const mapMetaPath = join10(runDir, "map-meta.json");
26138
+ if (existsSync5(mapMetaPath)) {
26139
+ this.processMapMeta(sessionId, runNumber, mapMetaPath);
26140
+ }
26141
+ const mapPath = join10(runDir, "map.md");
26142
+ if (existsSync5(mapPath)) {
25970
26143
  this.processMapMd(sessionId, runNumber, mapPath);
25971
26144
  }
25972
26145
  const mapArtifacts = [
@@ -25975,8 +26148,8 @@ var FilesystemSync = class {
25975
26148
  ["requirements-mapping.md", "requirements-mapping"]
25976
26149
  ];
25977
26150
  for (const [fileName, artifactType] of mapArtifacts) {
25978
- const filePath = join9(runDir, fileName);
25979
- if (existsSync4(filePath)) {
26151
+ const filePath = join10(runDir, fileName);
26152
+ if (existsSync5(filePath)) {
25980
26153
  this.processGenericArtifact(sessionId, artifactType, filePath, void 0, runNumber);
25981
26154
  }
25982
26155
  }
@@ -25987,8 +26160,8 @@ var FilesystemSync = class {
25987
26160
  ["discovered-standards.md", "discovered-standards"]
25988
26161
  ];
25989
26162
  for (const [fileName, artifactType] of sessionArtifacts) {
25990
- const filePath = join9(sessionDir, fileName);
25991
- if (existsSync4(filePath)) {
26163
+ const filePath = join10(sessionDir, fileName);
26164
+ if (existsSync5(filePath)) {
25992
26165
  this.processGenericArtifact(sessionId, artifactType, filePath);
25993
26166
  }
25994
26167
  }
@@ -25997,17 +26170,17 @@ var FilesystemSync = class {
25997
26170
  ensureSessionRow(sessionId, sessionDir) {
25998
26171
  const branchMatch = sessionId.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
25999
26172
  const branch = branchMatch?.[1] ?? "unknown";
26000
- const hasRoundsDir = existsSync4(join9(sessionDir, "rounds"));
26001
- const hasMapDir = existsSync4(join9(sessionDir, "map"));
26173
+ const hasRoundsDir = existsSync5(join10(sessionDir, "rounds"));
26174
+ const hasMapDir = existsSync5(join10(sessionDir, "map"));
26002
26175
  const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
26003
26176
  let currentRound = 1;
26004
26177
  if (hasRoundsDir) {
26005
- const roundDirs = readdirSync(join9(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
26178
+ const roundDirs = readdirSync(join10(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
26006
26179
  currentRound = Math.max(1, roundDirs.length);
26007
26180
  }
26008
26181
  let currentMapRun = 1;
26009
- const mapRunsDir = join9(sessionDir, "map", "runs");
26010
- if (existsSync4(mapRunsDir)) {
26182
+ const mapRunsDir = join10(sessionDir, "map", "runs");
26183
+ if (existsSync5(mapRunsDir)) {
26011
26184
  const runDirs = readdirSync(mapRunsDir).filter((d) => d.match(/^run-\d+$/));
26012
26185
  currentMapRun = Math.max(1, runDirs.length);
26013
26186
  }
@@ -26015,40 +26188,40 @@ var FilesystemSync = class {
26015
26188
  let phaseNumber = 1;
26016
26189
  let status = "active";
26017
26190
  if (workflowType === "review" && hasRoundsDir) {
26018
- const roundDir = join9(sessionDir, "rounds", `round-${currentRound}`);
26019
- if (existsSync4(join9(roundDir, "final.md"))) {
26191
+ const roundDir = join10(sessionDir, "rounds", `round-${currentRound}`);
26192
+ if (existsSync5(join10(roundDir, "final.md"))) {
26020
26193
  phase = "complete";
26021
26194
  phaseNumber = 8;
26022
26195
  status = "closed";
26023
- } else if (existsSync4(join9(roundDir, "discourse.md"))) {
26196
+ } else if (existsSync5(join10(roundDir, "discourse.md"))) {
26024
26197
  phase = "synthesis";
26025
26198
  phaseNumber = 7;
26026
- } else if (existsSync4(join9(roundDir, "reviews")) && readdirSync(join9(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
26199
+ } else if (existsSync5(join10(roundDir, "reviews")) && readdirSync(join10(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
26027
26200
  phase = "reviews";
26028
26201
  phaseNumber = 4;
26029
- } else if (existsSync4(join9(sessionDir, "context.md"))) {
26202
+ } else if (existsSync5(join10(sessionDir, "context.md"))) {
26030
26203
  phase = "analysis";
26031
26204
  phaseNumber = 3;
26032
- } else if (existsSync4(join9(sessionDir, "discovered-standards.md"))) {
26205
+ } else if (existsSync5(join10(sessionDir, "discovered-standards.md"))) {
26033
26206
  phase = "change-context";
26034
26207
  phaseNumber = 2;
26035
26208
  }
26036
26209
  } else if (workflowType === "map" && hasMapDir) {
26037
- const runDir = join9(mapRunsDir, `run-${currentMapRun}`);
26038
- if (existsSync4(join9(runDir, "map.md"))) {
26210
+ const runDir = join10(mapRunsDir, `run-${currentMapRun}`);
26211
+ if (existsSync5(join10(runDir, "map.md"))) {
26039
26212
  phase = "complete";
26040
26213
  phaseNumber = 6;
26041
26214
  status = "closed";
26042
- } else if (existsSync4(join9(runDir, "requirements-mapping.md"))) {
26215
+ } else if (existsSync5(join10(runDir, "requirements-mapping.md"))) {
26043
26216
  phase = "synthesis";
26044
26217
  phaseNumber = 5;
26045
- } else if (existsSync4(join9(runDir, "flow-analysis.md"))) {
26218
+ } else if (existsSync5(join10(runDir, "flow-analysis.md"))) {
26046
26219
  phase = "requirements-mapping";
26047
26220
  phaseNumber = 4;
26048
- } else if (existsSync4(join9(runDir, "topology.md"))) {
26221
+ } else if (existsSync5(join10(runDir, "topology.md"))) {
26049
26222
  phase = "flow-analysis";
26050
26223
  phaseNumber = 3;
26051
- } else if (existsSync4(join9(sessionDir, "discovered-standards.md"))) {
26224
+ } else if (existsSync5(join10(sessionDir, "discovered-standards.md"))) {
26052
26225
  phase = "topology";
26053
26226
  phaseNumber = 2;
26054
26227
  }
@@ -26106,15 +26279,21 @@ var FilesystemSync = class {
26106
26279
  processMapMd(sessionId, runNumber, filePath) {
26107
26280
  const existingRun = queryFirst(
26108
26281
  this.db,
26109
- "SELECT id, parsed_at FROM map_runs WHERE session_id = ? AND run_number = ?",
26282
+ "SELECT id, parsed_at, source FROM map_runs WHERE session_id = ? AND run_number = ?",
26110
26283
  [sessionId, runNumber]
26111
26284
  );
26112
26285
  if (existingRun && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
26113
- const content = readFileSync5(filePath, "utf-8");
26286
+ if (existingRun?.["source"] === "orchestrator") {
26287
+ const content2 = readFileSync6(filePath, "utf-8");
26288
+ const action2 = this.upsertMarkdownArtifact(sessionId, "map", filePath, content2, void 0);
26289
+ this.emitArtifactEvent(action2, { sessionId, artifactType: "map", filePath });
26290
+ return;
26291
+ }
26292
+ const content = readFileSync6(filePath, "utf-8");
26114
26293
  const parsed = parseMapMd(content);
26115
26294
  this.db.run(
26116
- `INSERT OR REPLACE INTO map_runs (session_id, run_number, file_count, map_md_path, parsed_at)
26117
- VALUES (?, ?, ?, ?, ?)`,
26295
+ `INSERT OR REPLACE INTO map_runs (session_id, run_number, file_count, map_md_path, parsed_at, source)
26296
+ VALUES (?, ?, ?, ?, ?, 'parser')`,
26118
26297
  [sessionId, runNumber, parsed.sections.reduce((sum, s) => sum + s.files.length, 0), filePath, sqlNow()]
26119
26298
  );
26120
26299
  const runRow = queryFirst(
@@ -26192,10 +26371,10 @@ var FilesystemSync = class {
26192
26371
  }
26193
26372
  const session = queryFirst(
26194
26373
  this.db,
26195
- "SELECT current_phase, workflow_type FROM sessions WHERE id = ?",
26374
+ "SELECT current_phase, phase_number, workflow_type FROM sessions WHERE id = ?",
26196
26375
  [sessionId]
26197
26376
  );
26198
- if (session && session["workflow_type"] === "map" && session["current_phase"] !== "complete") {
26377
+ if (session && session["workflow_type"] === "map" && (session["current_phase"] !== "complete" || session["phase_number"] < 6)) {
26199
26378
  this.db.run(
26200
26379
  `UPDATE sessions SET current_phase = 'complete', phase_number = 6, status = 'closed', updated_at = datetime('now')
26201
26380
  WHERE id = ?`,
@@ -26224,11 +26403,22 @@ var FilesystemSync = class {
26224
26403
  );
26225
26404
  const roundRow = queryFirst(
26226
26405
  this.db,
26227
- "SELECT id FROM review_rounds WHERE session_id = ? AND round_number = ?",
26406
+ "SELECT id, source FROM review_rounds WHERE session_id = ? AND round_number = ?",
26228
26407
  [sessionId, roundNumber]
26229
26408
  );
26230
26409
  const roundId = roundRow?.["id"];
26231
26410
  if (!roundId) return;
26411
+ if (roundRow?.["source"] === "orchestrator") {
26412
+ const content2 = readFileSync6(filePath, "utf-8");
26413
+ const action2 = this.upsertMarkdownArtifact(sessionId, "reviewer-output", filePath, content2, roundNumber);
26414
+ this.emitArtifactEvent(action2, {
26415
+ sessionId,
26416
+ artifactType: "reviewer-output",
26417
+ roundNumber,
26418
+ filePath
26419
+ });
26420
+ return;
26421
+ }
26232
26422
  const nameMatch = fileName.replace(/\.md$/, "").match(/^(.+?)-(\d+)$/);
26233
26423
  const reviewerType = nameMatch?.[1] ?? fileName.replace(/\.md$/, "");
26234
26424
  const instanceNumber = nameMatch?.[2] ? parseInt(nameMatch[2], 10) : 1;
@@ -26238,7 +26428,7 @@ var FilesystemSync = class {
26238
26428
  [roundId, reviewerType, instanceNumber]
26239
26429
  );
26240
26430
  if (existingOutput && this.shouldSkip(filePath, existingOutput["parsed_at"] ?? null)) return;
26241
- const content = readFileSync5(filePath, "utf-8");
26431
+ const content = readFileSync6(filePath, "utf-8");
26242
26432
  const parsed = parseReviewerOutput(content);
26243
26433
  this.db.run(
26244
26434
  `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
@@ -26311,8 +26501,8 @@ var FilesystemSync = class {
26311
26501
  filePath
26312
26502
  });
26313
26503
  }
26314
- // ── 6.4: Final.md Integration ──
26315
- processFinalMd(sessionId, roundNumber, filePath) {
26504
+ // ── 6.3b: Round Meta (Orchestrator-First) ──
26505
+ processRoundMeta(sessionId, roundNumber, filePath) {
26316
26506
  this.db.run(
26317
26507
  `INSERT OR IGNORE INTO review_rounds (session_id, round_number)
26318
26508
  VALUES (?, ?)`,
@@ -26320,46 +26510,332 @@ var FilesystemSync = class {
26320
26510
  );
26321
26511
  const existingRound = queryFirst(
26322
26512
  this.db,
26323
- "SELECT parsed_at FROM review_rounds WHERE session_id = ? AND round_number = ?",
26513
+ "SELECT parsed_at, source FROM review_rounds WHERE session_id = ? AND round_number = ?",
26324
26514
  [sessionId, roundNumber]
26325
26515
  );
26326
- if (existingRound && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
26327
- const content = readFileSync5(filePath, "utf-8");
26328
- const parsed = parseFinalMd(content);
26516
+ if (existingRound?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
26517
+ let raw;
26518
+ try {
26519
+ raw = JSON.parse(readFileSync6(filePath, "utf-8"));
26520
+ } catch {
26521
+ console.error(`[FilesystemSync] Failed to parse ${filePath}`);
26522
+ return;
26523
+ }
26524
+ const meta = raw;
26525
+ if (meta.schema_version !== 1 || !meta.verdict || !Array.isArray(meta.reviewers)) {
26526
+ console.error(`[FilesystemSync] Invalid round-meta.json at ${filePath}`);
26527
+ return;
26528
+ }
26529
+ const allFindings = meta.reviewers.flatMap((r) => r.findings ?? []);
26530
+ const blockerCount = allFindings.filter((f) => f.category === "blocker").length;
26531
+ const shouldFixCount = allFindings.filter((f) => f.category === "should_fix").length;
26532
+ const suggestionCount = allFindings.filter((f) => f.category === "suggestion").length;
26533
+ const reviewerCount = meta.reviewers.length;
26534
+ const totalFindingCount = allFindings.length;
26535
+ this.db.run("BEGIN TRANSACTION");
26536
+ try {
26537
+ this.db.run(
26538
+ `UPDATE review_rounds
26539
+ SET verdict = ?, blocker_count = ?, suggestion_count = ?, should_fix_count = ?,
26540
+ reviewer_count = ?, total_finding_count = ?, source = 'orchestrator', parsed_at = ?
26541
+ WHERE session_id = ? AND round_number = ?`,
26542
+ [
26543
+ meta.verdict,
26544
+ blockerCount,
26545
+ suggestionCount,
26546
+ shouldFixCount,
26547
+ reviewerCount,
26548
+ totalFindingCount,
26549
+ sqlNow(),
26550
+ sessionId,
26551
+ roundNumber
26552
+ ]
26553
+ );
26554
+ const roundRow = queryFirst(
26555
+ this.db,
26556
+ "SELECT id FROM review_rounds WHERE session_id = ? AND round_number = ?",
26557
+ [sessionId, roundNumber]
26558
+ );
26559
+ const roundId = roundRow?.["id"];
26560
+ if (!roundId) {
26561
+ this.db.run("COMMIT");
26562
+ return;
26563
+ }
26564
+ const roundDir = dirname7(filePath);
26565
+ for (const reviewer of meta.reviewers) {
26566
+ const reviewerType = reviewer.type ?? "unknown";
26567
+ const instanceNumber = reviewer.instance ?? 1;
26568
+ const findings = reviewer.findings ?? [];
26569
+ const reviewerMdPath = join10(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
26570
+ this.db.run(
26571
+ `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
26572
+ VALUES (?, ?, ?, ?, ?, ?)`,
26573
+ [roundId, reviewerType, instanceNumber, reviewerMdPath, findings.length, sqlNow()]
26574
+ );
26575
+ const outputRow = queryFirst(
26576
+ this.db,
26577
+ "SELECT id FROM reviewer_outputs WHERE round_id = ? AND reviewer_type = ? AND instance_number = ?",
26578
+ [roundId, reviewerType, instanceNumber]
26579
+ );
26580
+ const outputId = outputRow?.["id"];
26581
+ if (!outputId) continue;
26582
+ const stashedFindingProgress = /* @__PURE__ */ new Map();
26583
+ const findingProgressResult = this.db.exec(
26584
+ `SELECT rf.title, rf.severity, rf.file_path, ufp.status, ufp.updated_at
26585
+ FROM user_finding_progress ufp
26586
+ JOIN review_findings rf ON rf.id = ufp.finding_id
26587
+ WHERE rf.reviewer_output_id = ?`,
26588
+ [outputId]
26589
+ );
26590
+ if (findingProgressResult[0]) {
26591
+ for (const row of findingProgressResult[0].values) {
26592
+ const key = `${row[0]}|${row[1]}|${row[2]}`;
26593
+ stashedFindingProgress.set(key, {
26594
+ status: row[3],
26595
+ updatedAt: row[4]
26596
+ });
26597
+ }
26598
+ }
26599
+ this.db.run("DELETE FROM review_findings WHERE reviewer_output_id = ?", [outputId]);
26600
+ for (const finding of findings) {
26601
+ this.db.run(
26602
+ `INSERT INTO review_findings (reviewer_output_id, title, severity, file_path, line_start, line_end, summary, is_blocker, parsed_at)
26603
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
26604
+ [
26605
+ outputId,
26606
+ finding.title ?? "",
26607
+ finding.severity ?? "info",
26608
+ finding.file_path ?? null,
26609
+ finding.line_start ?? null,
26610
+ finding.line_end ?? null,
26611
+ finding.summary ?? null,
26612
+ finding.category === "blocker" ? 1 : 0,
26613
+ sqlNow()
26614
+ ]
26615
+ );
26616
+ const key = `${finding.title ?? ""}|${finding.severity ?? "info"}|${finding.file_path ?? ""}`;
26617
+ const stashed = stashedFindingProgress.get(key);
26618
+ if (stashed) {
26619
+ const newFindingRow = queryFirst(
26620
+ this.db,
26621
+ "SELECT id FROM review_findings WHERE reviewer_output_id = ? AND title = ? AND severity = ? AND file_path IS ?",
26622
+ [outputId, finding.title ?? "", finding.severity ?? "info", finding.file_path ?? null]
26623
+ );
26624
+ if (newFindingRow) {
26625
+ this.db.run(
26626
+ `INSERT OR REPLACE INTO user_finding_progress (finding_id, status, updated_at)
26627
+ VALUES (?, ?, ?)`,
26628
+ [newFindingRow["id"], stashed.status, stashed.updatedAt]
26629
+ );
26630
+ }
26631
+ }
26632
+ }
26633
+ }
26634
+ this.db.run("COMMIT");
26635
+ } catch (err) {
26636
+ this.db.run("ROLLBACK");
26637
+ console.error(`[FilesystemSync] Error in processRoundMeta for ${filePath}:`, err);
26638
+ return;
26639
+ }
26640
+ this.io?.to(`session:${sessionId}`).emit("round:updated", {
26641
+ sessionId,
26642
+ roundNumber,
26643
+ verdict: meta.verdict,
26644
+ blockerCount,
26645
+ shouldFixCount,
26646
+ suggestionCount,
26647
+ reviewerCount,
26648
+ totalFindingCount,
26649
+ source: "orchestrator"
26650
+ });
26651
+ }
26652
+ // ── 6.2b: Map-meta.json Integration ──
26653
+ processMapMeta(sessionId, runNumber, filePath) {
26329
26654
  this.db.run(
26330
- `UPDATE review_rounds SET verdict = ?, blocker_count = ?, suggestion_count = ?, should_fix_count = ?, final_md_path = ?, parsed_at = ?
26331
- WHERE session_id = ? AND round_number = ?`,
26332
- [
26333
- parsed.verdict,
26334
- parsed.blockerCount,
26335
- parsed.suggestionCount,
26336
- parsed.shouldFixCount,
26337
- filePath,
26338
- sqlNow(),
26339
- sessionId,
26340
- roundNumber
26341
- ]
26655
+ `INSERT OR IGNORE INTO map_runs (session_id, run_number)
26656
+ VALUES (?, ?)`,
26657
+ [sessionId, runNumber]
26342
26658
  );
26343
- const actualBlockers = queryScalar(
26659
+ const existingRun = queryFirst(
26344
26660
  this.db,
26345
- `SELECT COUNT(*) FROM review_findings rf
26346
- JOIN reviewer_outputs ro ON rf.reviewer_output_id = ro.id
26347
- WHERE ro.round_id = (SELECT id FROM review_rounds WHERE session_id = ? AND round_number = ?)
26348
- AND rf.is_blocker = 1`,
26661
+ "SELECT id, parsed_at, source FROM map_runs WHERE session_id = ? AND run_number = ?",
26662
+ [sessionId, runNumber]
26663
+ );
26664
+ if (existingRun?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
26665
+ let raw;
26666
+ try {
26667
+ raw = JSON.parse(readFileSync6(filePath, "utf-8"));
26668
+ } catch {
26669
+ console.error(`[FilesystemSync] Failed to parse ${filePath}`);
26670
+ return;
26671
+ }
26672
+ const meta = raw;
26673
+ if (meta.schema_version !== 1 || !Array.isArray(meta.sections)) {
26674
+ console.error(`[FilesystemSync] Invalid map-meta.json at ${filePath}`);
26675
+ return;
26676
+ }
26677
+ const sectionCount = meta.sections.length;
26678
+ const fileCount = meta.sections.reduce((sum, s) => sum + (s.files?.length ?? 0), 0);
26679
+ this.db.run("BEGIN TRANSACTION");
26680
+ try {
26681
+ this.db.run(
26682
+ `UPDATE map_runs
26683
+ SET file_count = ?, section_count = ?, source = 'orchestrator', parsed_at = ?
26684
+ WHERE session_id = ? AND run_number = ?`,
26685
+ [fileCount, sectionCount, sqlNow(), sessionId, runNumber]
26686
+ );
26687
+ const runRow = queryFirst(
26688
+ this.db,
26689
+ "SELECT id FROM map_runs WHERE session_id = ? AND run_number = ?",
26690
+ [sessionId, runNumber]
26691
+ );
26692
+ const mapRunId = runRow?.["id"];
26693
+ if (!mapRunId) {
26694
+ this.db.run("COMMIT");
26695
+ return;
26696
+ }
26697
+ const stashedFileProgress = /* @__PURE__ */ new Map();
26698
+ const progressResult = this.db.exec(
26699
+ `SELECT mf.file_path, ufp.is_reviewed, ufp.reviewed_at
26700
+ FROM user_file_progress ufp
26701
+ JOIN map_files mf ON mf.id = ufp.map_file_id
26702
+ JOIN map_sections ms ON ms.id = mf.section_id
26703
+ WHERE ms.map_run_id = ?`,
26704
+ [mapRunId]
26705
+ );
26706
+ if (progressResult[0]) {
26707
+ for (const row of progressResult[0].values) {
26708
+ const fp = row[0];
26709
+ stashedFileProgress.set(fp, {
26710
+ isReviewed: row[1],
26711
+ reviewedAt: row[2]
26712
+ });
26713
+ }
26714
+ }
26715
+ const oldSections = this.db.exec(
26716
+ "SELECT id FROM map_sections WHERE map_run_id = ?",
26717
+ [mapRunId]
26718
+ );
26719
+ if (oldSections[0]) {
26720
+ for (const row of oldSections[0].values) {
26721
+ this.db.run("DELETE FROM map_files WHERE section_id = ?", [row[0]]);
26722
+ }
26723
+ }
26724
+ this.db.run("DELETE FROM map_sections WHERE map_run_id = ?", [mapRunId]);
26725
+ for (const section of meta.sections) {
26726
+ const sectionNumber = section.section_number ?? 0;
26727
+ const title = section.title ?? "Untitled";
26728
+ const description = section.description ?? null;
26729
+ const files = section.files ?? [];
26730
+ this.db.run(
26731
+ `INSERT OR REPLACE INTO map_sections (map_run_id, section_number, title, description, file_count, display_order)
26732
+ VALUES (?, ?, ?, ?, ?, ?)`,
26733
+ [mapRunId, sectionNumber, title, description, files.length, sectionNumber]
26734
+ );
26735
+ const sectionRow = queryFirst(
26736
+ this.db,
26737
+ "SELECT id FROM map_sections WHERE map_run_id = ? AND section_number = ?",
26738
+ [mapRunId, sectionNumber]
26739
+ );
26740
+ const sectionId = sectionRow?.["id"];
26741
+ if (!sectionId) continue;
26742
+ for (let fi = 0; fi < files.length; fi++) {
26743
+ const file = files[fi];
26744
+ if (!file) continue;
26745
+ this.db.run(
26746
+ `INSERT OR REPLACE INTO map_files (section_id, file_path, role, lines_added, lines_deleted, display_order)
26747
+ VALUES (?, ?, ?, ?, ?, ?)`,
26748
+ [sectionId, file.file_path ?? "", file.role ?? null, file.lines_added ?? 0, file.lines_deleted ?? 0, fi]
26749
+ );
26750
+ const stashed = stashedFileProgress.get(file.file_path ?? "");
26751
+ if (stashed) {
26752
+ const newFileRow = queryFirst(
26753
+ this.db,
26754
+ "SELECT id FROM map_files WHERE section_id = ? AND file_path = ?",
26755
+ [sectionId, file.file_path ?? ""]
26756
+ );
26757
+ if (newFileRow) {
26758
+ this.db.run(
26759
+ `INSERT OR REPLACE INTO user_file_progress (map_file_id, is_reviewed, reviewed_at)
26760
+ VALUES (?, ?, ?)`,
26761
+ [newFileRow["id"], stashed.isReviewed, stashed.reviewedAt]
26762
+ );
26763
+ }
26764
+ }
26765
+ }
26766
+ }
26767
+ this.db.run("COMMIT");
26768
+ } catch (err) {
26769
+ this.db.run("ROLLBACK");
26770
+ console.error(`[FilesystemSync] Error in processMapMeta for ${filePath}:`, err);
26771
+ return;
26772
+ }
26773
+ this.io?.to(`session:${sessionId}`).emit("map:updated", {
26774
+ sessionId,
26775
+ runNumber,
26776
+ fileCount,
26777
+ sectionCount,
26778
+ source: "orchestrator"
26779
+ });
26780
+ }
26781
+ // ── 6.4: Final.md Integration ──
26782
+ processFinalMd(sessionId, roundNumber, filePath) {
26783
+ this.db.run(
26784
+ `INSERT OR IGNORE INTO review_rounds (session_id, round_number)
26785
+ VALUES (?, ?)`,
26786
+ [sessionId, roundNumber]
26787
+ );
26788
+ const existingRound = queryFirst(
26789
+ this.db,
26790
+ "SELECT parsed_at, source FROM review_rounds WHERE session_id = ? AND round_number = ?",
26349
26791
  [sessionId, roundNumber]
26350
26792
  );
26351
- if (actualBlockers !== null && actualBlockers !== parsed.blockerCount) {
26793
+ const isOrchestratorSource = existingRound?.["source"] === "orchestrator";
26794
+ if (!isOrchestratorSource && existingRound && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
26795
+ const content = readFileSync6(filePath, "utf-8");
26796
+ if (isOrchestratorSource) {
26352
26797
  this.db.run(
26353
- "UPDATE review_rounds SET blocker_count = ? WHERE session_id = ? AND round_number = ?",
26354
- [actualBlockers, sessionId, roundNumber]
26798
+ `UPDATE review_rounds SET final_md_path = ?, parsed_at = ?
26799
+ WHERE session_id = ? AND round_number = ?`,
26800
+ [filePath, sqlNow(), sessionId, roundNumber]
26801
+ );
26802
+ } else {
26803
+ const parsed = parseFinalMd(content);
26804
+ this.db.run(
26805
+ `UPDATE review_rounds SET verdict = ?, blocker_count = ?, suggestion_count = ?, should_fix_count = ?, final_md_path = ?, parsed_at = ?, source = 'parser'
26806
+ WHERE session_id = ? AND round_number = ?`,
26807
+ [
26808
+ parsed.verdict,
26809
+ parsed.blockerCount,
26810
+ parsed.suggestionCount,
26811
+ parsed.shouldFixCount,
26812
+ filePath,
26813
+ sqlNow(),
26814
+ sessionId,
26815
+ roundNumber
26816
+ ]
26817
+ );
26818
+ const actualBlockers = queryScalar(
26819
+ this.db,
26820
+ `SELECT COUNT(*) FROM review_findings rf
26821
+ JOIN reviewer_outputs ro ON rf.reviewer_output_id = ro.id
26822
+ WHERE ro.round_id = (SELECT id FROM review_rounds WHERE session_id = ? AND round_number = ?)
26823
+ AND rf.is_blocker = 1`,
26824
+ [sessionId, roundNumber]
26355
26825
  );
26826
+ if (actualBlockers !== null && actualBlockers !== parsed.blockerCount) {
26827
+ this.db.run(
26828
+ "UPDATE review_rounds SET blocker_count = ? WHERE session_id = ? AND round_number = ?",
26829
+ [actualBlockers, sessionId, roundNumber]
26830
+ );
26831
+ }
26356
26832
  }
26357
26833
  const session = queryFirst(
26358
26834
  this.db,
26359
26835
  "SELECT current_phase, phase_number, status FROM sessions WHERE id = ?",
26360
26836
  [sessionId]
26361
26837
  );
26362
- if (session && session["current_phase"] !== "complete") {
26838
+ if (session && (session["current_phase"] !== "complete" || session["phase_number"] < 8)) {
26363
26839
  this.db.run(
26364
26840
  `UPDATE sessions SET current_phase = 'complete', phase_number = 8, status = 'closed', updated_at = datetime('now')
26365
26841
  WHERE id = ?`,
@@ -26389,7 +26865,7 @@ var FilesystemSync = class {
26389
26865
  [sessionId, artifactType, relPath]
26390
26866
  );
26391
26867
  if (existing && this.shouldSkip(filePath, existing["parsed_at"] ?? null)) return;
26392
- const content = readFileSync5(filePath, "utf-8");
26868
+ const content = readFileSync6(filePath, "utf-8");
26393
26869
  const action = this.upsertMarkdownArtifact(sessionId, artifactType, filePath, content, roundNumber);
26394
26870
  this.emitArtifactEvent(action, {
26395
26871
  sessionId,
@@ -26401,7 +26877,7 @@ var FilesystemSync = class {
26401
26877
  // ── 6.6: Chokidar Watcher ──
26402
26878
  startWatching() {
26403
26879
  if (this.watcher) return;
26404
- this.watcher = watch(this.sessionsDir, {
26880
+ this.watcher = watch2(this.sessionsDir, {
26405
26881
  persistent: true,
26406
26882
  ignoreInitial: true,
26407
26883
  depth: 10,
@@ -26427,7 +26903,7 @@ var FilesystemSync = class {
26427
26903
  this.debounceTimers.clear();
26428
26904
  }
26429
26905
  handleFileChange(filePath) {
26430
- if (!filePath.endsWith(".md")) return;
26906
+ if (!filePath.endsWith(".md") && !filePath.endsWith(".json")) return;
26431
26907
  const existing = this.debounceTimers.get(filePath);
26432
26908
  if (existing) clearTimeout(existing);
26433
26909
  this.debounceTimers.set(
@@ -26448,7 +26924,7 @@ var FilesystemSync = class {
26448
26924
  const parts = relFromSessions.split("/");
26449
26925
  const sessionId = parts[0];
26450
26926
  if (!sessionId) return;
26451
- const sessionDir = join9(this.sessionsDir, sessionId);
26927
+ const sessionDir = join10(this.sessionsDir, sessionId);
26452
26928
  this.ensureSessionRow(sessionId, sessionDir);
26453
26929
  const fileName = basename2(filePath);
26454
26930
  const reviewerMatch = relFromSessions.match(/rounds\/round-(\d+)\/reviews\/(.+\.md)$/);
@@ -26457,6 +26933,12 @@ var FilesystemSync = class {
26457
26933
  this.processReviewerOutput(sessionId, roundNumber, filePath, reviewerMatch[2] ?? "");
26458
26934
  return;
26459
26935
  }
26936
+ const roundMetaMatch = relFromSessions.match(/rounds\/round-(\d+)\/round-meta\.json$/);
26937
+ if (roundMetaMatch) {
26938
+ const roundNumber = parseInt(roundMetaMatch[1] ?? "0", 10);
26939
+ this.processRoundMeta(sessionId, roundNumber, filePath);
26940
+ return;
26941
+ }
26460
26942
  const finalMatch = relFromSessions.match(/rounds\/round-(\d+)\/final\.md$/);
26461
26943
  if (finalMatch) {
26462
26944
  const roundNumber = parseInt(finalMatch[1] ?? "0", 10);
@@ -26475,6 +26957,12 @@ var FilesystemSync = class {
26475
26957
  this.processGenericArtifact(sessionId, "discourse", filePath, roundNumber);
26476
26958
  return;
26477
26959
  }
26960
+ const mapMetaMatch = relFromSessions.match(/map\/runs\/run-(\d+)\/map-meta\.json$/);
26961
+ if (mapMetaMatch) {
26962
+ const runNumber = parseInt(mapMetaMatch[1] ?? "0", 10);
26963
+ this.processMapMeta(sessionId, runNumber, filePath);
26964
+ return;
26965
+ }
26478
26966
  const mapMatch = relFromSessions.match(/map\/runs\/run-(\d+)\/map\.md$/);
26479
26967
  if (mapMatch) {
26480
26968
  const runNumber = parseInt(mapMatch[1] ?? "0", 10);
@@ -26508,8 +26996,8 @@ var FilesystemSync = class {
26508
26996
  };
26509
26997
 
26510
26998
  // src/server/services/db-sync-watcher.ts
26511
- import { existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
26512
- import { watch as watch2 } from "chokidar";
26999
+ import { existsSync as existsSync6, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
27000
+ import { watch as watch3 } from "chokidar";
26513
27001
  import initSqlJs3 from "sql.js";
26514
27002
  function col(row, key) {
26515
27003
  return row[key] ?? null;
@@ -26530,7 +27018,7 @@ var DbSyncWatcher = class {
26530
27018
  * Initialize the WASM runtime (called once at startup).
26531
27019
  */
26532
27020
  async init() {
26533
- const wasmBuffer = readFileSync6(locateWasm());
27021
+ const wasmBuffer = readFileSync7(locateWasm());
26534
27022
  this.wasmBinary = wasmBuffer.buffer.slice(
26535
27023
  wasmBuffer.byteOffset,
26536
27024
  wasmBuffer.byteOffset + wasmBuffer.byteLength
@@ -26541,12 +27029,12 @@ var DbSyncWatcher = class {
26541
27029
  * Start watching the DB file for external changes.
26542
27030
  */
26543
27031
  startWatching() {
26544
- if (!existsSync5(this.dbFilePath)) return;
27032
+ if (!existsSync6(this.dbFilePath)) return;
26545
27033
  try {
26546
27034
  this.lastMtime = statSync2(this.dbFilePath).mtimeMs;
26547
27035
  } catch {
26548
27036
  }
26549
- this.watcher = watch2(this.dbFilePath, {
27037
+ this.watcher = watch3(this.dbFilePath, {
26550
27038
  // Also watch WAL/SHM files that SQLite may create
26551
27039
  persistent: true,
26552
27040
  ignoreInitial: true,
@@ -26592,7 +27080,7 @@ var DbSyncWatcher = class {
26592
27080
  * to avoid overwriting CLI changes.
26593
27081
  */
26594
27082
  syncFromDisk() {
26595
- if (!this.SQL || !existsSync5(this.dbFilePath)) return;
27083
+ if (!this.SQL || !existsSync6(this.dbFilePath)) return;
26596
27084
  let currentMtime;
26597
27085
  try {
26598
27086
  currentMtime = statSync2(this.dbFilePath).mtimeMs;
@@ -26603,7 +27091,7 @@ var DbSyncWatcher = class {
26603
27091
  this.lastMtime = currentMtime;
26604
27092
  let diskDb = null;
26605
27093
  try {
26606
- const fileBuffer = readFileSync6(this.dbFilePath);
27094
+ const fileBuffer = readFileSync7(this.dbFilePath);
26607
27095
  diskDb = new this.SQL.Database(fileBuffer);
26608
27096
  this.syncSessions(diskDb);
26609
27097
  this.syncEvents(diskDb);
@@ -26698,7 +27186,7 @@ var DbSyncWatcher = class {
26698
27186
  const diskEvents = resultToRows(
26699
27187
  diskDb.exec("SELECT * FROM orchestration_events ORDER BY id ASC")
26700
27188
  );
26701
- let insertedCount = 0;
27189
+ const newEvents = [];
26702
27190
  const affectedSessions = /* @__PURE__ */ new Set();
26703
27191
  for (const row of diskEvents) {
26704
27192
  const eventId = col(row, "id");
@@ -26722,15 +27210,128 @@ var DbSyncWatcher = class {
26722
27210
  col(row, "created_at")
26723
27211
  ]
26724
27212
  );
26725
- insertedCount++;
27213
+ newEvents.push(row);
26726
27214
  affectedSessions.add(sessionId);
26727
27215
  }
26728
- if (insertedCount > 0) {
27216
+ for (const row of newEvents) {
27217
+ const eventType = col(row, "event_type");
27218
+ const sessionId = col(row, "session_id");
27219
+ const metadataStr = col(row, "metadata");
27220
+ if (eventType === "round_completed") {
27221
+ const roundNumber = col(row, "round");
27222
+ if (sessionId && roundNumber && metadataStr) {
27223
+ this.processRoundCompletedEvent(sessionId, roundNumber, metadataStr);
27224
+ }
27225
+ } else if (eventType === "map_completed") {
27226
+ const runNumber = col(row, "round");
27227
+ if (sessionId && runNumber && metadataStr) {
27228
+ this.processMapCompletedEvent(sessionId, runNumber, metadataStr);
27229
+ }
27230
+ }
27231
+ }
27232
+ if (newEvents.length > 0) {
26729
27233
  for (const sessionId of affectedSessions) {
26730
- this.io.emit("session:events", { session_id: sessionId });
27234
+ this.io.to(`session:${sessionId}`).emit("session:events", { session_id: sessionId });
26731
27235
  }
26732
27236
  }
26733
27237
  }
27238
+ /**
27239
+ * Process a `round_completed` orchestration event.
27240
+ * Upserts review_rounds with orchestrator data for real-time dashboard updates.
27241
+ * Idempotent — skips if round already has source='orchestrator'.
27242
+ */
27243
+ processRoundCompletedEvent(sessionId, roundNumber, metadataStr) {
27244
+ let metadata;
27245
+ try {
27246
+ metadata = JSON.parse(metadataStr);
27247
+ } catch {
27248
+ return;
27249
+ }
27250
+ const existing = this.db.exec(
27251
+ "SELECT source FROM review_rounds WHERE session_id = ? AND round_number = ?",
27252
+ [sessionId, roundNumber]
27253
+ );
27254
+ const rows = resultToRows(existing);
27255
+ if (rows.length > 0 && col(rows[0], "source") === "orchestrator") {
27256
+ return;
27257
+ }
27258
+ this.db.run(
27259
+ `INSERT OR IGNORE INTO review_rounds (session_id, round_number)
27260
+ VALUES (?, ?)`,
27261
+ [sessionId, roundNumber]
27262
+ );
27263
+ this.db.run(
27264
+ `UPDATE review_rounds
27265
+ SET verdict = ?, blocker_count = ?, suggestion_count = ?, should_fix_count = ?,
27266
+ reviewer_count = ?, total_finding_count = ?, source = 'orchestrator',
27267
+ parsed_at = datetime('now')
27268
+ WHERE session_id = ? AND round_number = ?`,
27269
+ [
27270
+ metadata.verdict ?? null,
27271
+ metadata.blocker_count ?? 0,
27272
+ metadata.suggestion_count ?? 0,
27273
+ metadata.should_fix_count ?? 0,
27274
+ metadata.reviewer_count ?? 0,
27275
+ metadata.total_finding_count ?? 0,
27276
+ sessionId,
27277
+ roundNumber
27278
+ ]
27279
+ );
27280
+ this.io.to(`session:${sessionId}`).emit("round:updated", {
27281
+ sessionId,
27282
+ roundNumber,
27283
+ verdict: metadata.verdict,
27284
+ blockerCount: metadata.blocker_count,
27285
+ shouldFixCount: metadata.should_fix_count,
27286
+ suggestionCount: metadata.suggestion_count,
27287
+ source: "orchestrator"
27288
+ });
27289
+ }
27290
+ /**
27291
+ * Process a `map_completed` orchestration event.
27292
+ * Upserts map_runs with orchestrator data for real-time dashboard updates.
27293
+ * Idempotent — skips if run already has source='orchestrator'.
27294
+ */
27295
+ processMapCompletedEvent(sessionId, runNumber, metadataStr) {
27296
+ let metadata;
27297
+ try {
27298
+ metadata = JSON.parse(metadataStr);
27299
+ } catch {
27300
+ return;
27301
+ }
27302
+ const existing = this.db.exec(
27303
+ "SELECT source FROM map_runs WHERE session_id = ? AND run_number = ?",
27304
+ [sessionId, runNumber]
27305
+ );
27306
+ const rows = resultToRows(existing);
27307
+ if (rows.length > 0 && col(rows[0], "source") === "orchestrator") {
27308
+ return;
27309
+ }
27310
+ this.db.run(
27311
+ `INSERT OR IGNORE INTO map_runs (session_id, run_number)
27312
+ VALUES (?, ?)`,
27313
+ [sessionId, runNumber]
27314
+ );
27315
+ this.db.run(
27316
+ `UPDATE map_runs
27317
+ SET file_count = ?, section_count = ?, source = 'orchestrator',
27318
+ parsed_at = datetime('now')
27319
+ WHERE session_id = ? AND run_number = ?`,
27320
+ [
27321
+ metadata.file_count ?? 0,
27322
+ metadata.section_count ?? 0,
27323
+ sessionId,
27324
+ runNumber
27325
+ ]
27326
+ );
27327
+ this.io.to(`session:${sessionId}`).emit("map:updated", {
27328
+ sessionId,
27329
+ runNumber,
27330
+ fileCount: metadata.file_count,
27331
+ sectionCount: metadata.section_count,
27332
+ source: "orchestrator"
27333
+ });
27334
+ }
26734
27335
  /**
26735
27336
  * Record current mtime after the dashboard writes to disk.
26736
27337
  * Called automatically via registered save hooks after saveDb().
@@ -26744,20 +27345,20 @@ var DbSyncWatcher = class {
26744
27345
  };
26745
27346
 
26746
27347
  // src/server/socket/chat-handler.ts
26747
- import { dirname as dirname7 } from "node:path";
27348
+ import { dirname as dirname8 } from "node:path";
26748
27349
 
26749
27350
  // src/server/services/chat-context.ts
26750
- import { readFileSync as readFileSync7, readdirSync as readdirSync2, existsSync as existsSync6 } from "node:fs";
26751
- import { join as join10 } from "node:path";
27351
+ import { readFileSync as readFileSync8, readdirSync as readdirSync2, existsSync as existsSync7 } from "node:fs";
27352
+ import { join as join11 } from "node:path";
26752
27353
  function buildChatContext(ocrDir, target) {
26753
- const sessionsDir = join10(ocrDir, "sessions");
27354
+ const sessionsDir = join11(ocrDir, "sessions");
26754
27355
  if (target.type === "map_run") {
26755
27356
  return buildMapRunContext(sessionsDir, target.sessionId, target.runNumber);
26756
27357
  }
26757
27358
  return buildReviewRoundContext(sessionsDir, target.sessionId, target.roundNumber);
26758
27359
  }
26759
27360
  function buildMapRunContext(sessionsDir, sessionId, runNumber) {
26760
- const mapPath = join10(
27361
+ const mapPath = join11(
26761
27362
  sessionsDir,
26762
27363
  sessionId,
26763
27364
  "map",
@@ -26771,8 +27372,8 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
26771
27372
  "",
26772
27373
  `Below is the Code Review Map that organizes the changeset into reviewable sections:`
26773
27374
  ];
26774
- if (existsSync6(mapPath)) {
26775
- const content = readFileSync7(mapPath, "utf-8");
27375
+ if (existsSync7(mapPath)) {
27376
+ const content = readFileSync8(mapPath, "utf-8");
26776
27377
  parts.push("");
26777
27378
  parts.push("<map>");
26778
27379
  parts.push(content);
@@ -26784,26 +27385,26 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
26784
27385
  return parts.join("\n");
26785
27386
  }
26786
27387
  function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
26787
- const roundDir = join10(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
26788
- const finalPath = join10(roundDir, "final.md");
26789
- const reviewersDir = join10(roundDir, "reviews");
27388
+ const roundDir = join11(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
27389
+ const finalPath = join11(roundDir, "final.md");
27390
+ const reviewersDir = join11(roundDir, "reviews");
26790
27391
  const parts = [
26791
27392
  `You are an expert code reviewer assisting with a code review session.`,
26792
27393
  `You are looking at review round #${roundNumber} for session "${sessionId}".`,
26793
27394
  "",
26794
27395
  `Below are the review artifacts for this round:`
26795
27396
  ];
26796
- if (existsSync6(finalPath)) {
26797
- const content = readFileSync7(finalPath, "utf-8");
27397
+ if (existsSync7(finalPath)) {
27398
+ const content = readFileSync8(finalPath, "utf-8");
26798
27399
  parts.push("");
26799
27400
  parts.push("<final-synthesis>");
26800
27401
  parts.push(content);
26801
27402
  parts.push("</final-synthesis>");
26802
27403
  }
26803
- if (existsSync6(reviewersDir)) {
27404
+ if (existsSync7(reviewersDir)) {
26804
27405
  const files = readdirSync2(reviewersDir).filter((f) => f.endsWith(".md")).sort();
26805
27406
  for (const file of files) {
26806
- const content = readFileSync7(join10(reviewersDir, file), "utf-8");
27407
+ const content = readFileSync8(join11(reviewersDir, file), "utf-8");
26807
27408
  const reviewerName = file.replace(/\.md$/, "");
26808
27409
  parts.push("");
26809
27410
  parts.push(`<reviewer name="${reviewerName}">`);
@@ -26811,7 +27412,7 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
26811
27412
  parts.push("</reviewer>");
26812
27413
  }
26813
27414
  }
26814
- if (!existsSync6(finalPath) && !existsSync6(reviewersDir)) {
27415
+ if (!existsSync7(finalPath) && !existsSync7(reviewersDir)) {
26815
27416
  parts.push("");
26816
27417
  parts.push("(No review artifacts found on disk for this round.)");
26817
27418
  }
@@ -26939,7 +27540,7 @@ User: ${message}`;
26939
27540
  });
26940
27541
  return;
26941
27542
  }
26942
- const repoRoot = dirname7(ocrDir);
27543
+ const repoRoot = dirname8(ocrDir);
26943
27544
  const spawnResult = adapter.spawn({
26944
27545
  prompt,
26945
27546
  cwd: repoRoot,
@@ -27116,15 +27717,15 @@ function cleanupAllChats() {
27116
27717
 
27117
27718
  // src/server/socket/post-handler.ts
27118
27719
  import { execFile } from "node:child_process";
27119
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
27720
+ import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
27120
27721
  import { tmpdir as tmpdir2 } from "node:os";
27121
- import { join as join11, dirname as dirname8, isAbsolute } from "node:path";
27722
+ import { join as join12, dirname as dirname9, isAbsolute } from "node:path";
27122
27723
  import { randomUUID } from "node:crypto";
27123
27724
  import { promisify } from "node:util";
27124
27725
  var execFileAsync = promisify(execFile);
27125
27726
  function resolveSessionDir(sessionDir, ocrDir) {
27126
27727
  if (isAbsolute(sessionDir)) return sessionDir;
27127
- return join11(dirname8(ocrDir), sessionDir);
27728
+ return join12(dirname9(ocrDir), sessionDir);
27128
27729
  }
27129
27730
  var BRANCH_PREFIXES = [
27130
27731
  "feat",
@@ -27193,7 +27794,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
27193
27794
  return;
27194
27795
  }
27195
27796
  const branch = session.branch;
27196
- const repoRoot = dirname8(ocrDir);
27797
+ const repoRoot = dirname9(ocrDir);
27197
27798
  try {
27198
27799
  await execFileAsync("gh", ["auth", "status"], { env: cleanEnv(), cwd: repoRoot });
27199
27800
  } catch {
@@ -27294,19 +27895,19 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
27294
27895
  socket.emit("post:error", { error: "Session not found" });
27295
27896
  return;
27296
27897
  }
27297
- const sessionDir = session.session_dir ? resolveSessionDir(session.session_dir, ocrDir) : join11(ocrDir, "sessions", sessionId);
27298
- const roundDir = join11(sessionDir, "rounds", `round-${roundNumber}`);
27299
- const finalPath = join11(roundDir, "final.md");
27300
- if (!existsSync7(finalPath)) {
27898
+ const sessionDir = session.session_dir ? resolveSessionDir(session.session_dir, ocrDir) : join12(ocrDir, "sessions", sessionId);
27899
+ const roundDir = join12(sessionDir, "rounds", `round-${roundNumber}`);
27900
+ const finalPath = join12(roundDir, "final.md");
27901
+ if (!existsSync8(finalPath)) {
27301
27902
  socket.emit("post:error", { error: "final.md not found for this round" });
27302
27903
  return;
27303
27904
  }
27304
- const humanReviewPath = join11(roundDir, "final-human.md");
27305
- const repoRoot = dirname8(ocrDir);
27306
- const commandMdPath = join11(ocrDir, "commands", "translate-review-to-single-human.md");
27905
+ const humanReviewPath = join12(roundDir, "final-human.md");
27906
+ const repoRoot = dirname9(ocrDir);
27907
+ const commandMdPath = join12(ocrDir, "commands", "translate-review-to-single-human.md");
27307
27908
  let commandContent;
27308
27909
  try {
27309
- commandContent = readFileSync8(commandMdPath, "utf-8");
27910
+ commandContent = readFileSync9(commandMdPath, "utf-8");
27310
27911
  } catch {
27311
27912
  socket.emit("post:error", {
27312
27913
  error: `Command file not found: ${commandMdPath}. Run \`ocr init\` to set up.`
@@ -27382,9 +27983,9 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
27382
27983
  }
27383
27984
  }
27384
27985
  let generatedContent = "";
27385
- if (existsSync7(humanReviewPath)) {
27986
+ if (existsSync8(humanReviewPath)) {
27386
27987
  try {
27387
- generatedContent = readFileSync8(humanReviewPath, "utf-8").trim();
27988
+ generatedContent = readFileSync9(humanReviewPath, "utf-8").trim();
27388
27989
  } catch {
27389
27990
  }
27390
27991
  }
@@ -27459,10 +28060,10 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
27459
28060
  socket.emit("post:save-result", { success: false, error: "Session not found" });
27460
28061
  return;
27461
28062
  }
27462
- const sessionDir = session.session_dir ? resolveSessionDir(session.session_dir, ocrDir) : join11(ocrDir, "sessions", sessionId);
27463
- const roundDir = join11(sessionDir, "rounds", `round-${roundNumber}`);
28063
+ const sessionDir = session.session_dir ? resolveSessionDir(session.session_dir, ocrDir) : join12(ocrDir, "sessions", sessionId);
28064
+ const roundDir = join12(sessionDir, "rounds", `round-${roundNumber}`);
27464
28065
  mkdirSync2(roundDir, { recursive: true });
27465
- const filePath = join11(roundDir, "final-human.md");
28066
+ const filePath = join12(roundDir, "final-human.md");
27466
28067
  writeFileSync3(filePath, content, { mode: 420 });
27467
28068
  saveDb(db, ocrDir);
27468
28069
  socket.emit("post:save-result", { success: true });
@@ -27490,14 +28091,14 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
27490
28091
  );
27491
28092
  tracker.appendOutput(`\u25B8 Posting review to PR #${prNumber}...
27492
28093
  `);
27493
- const tmpDir = join11(tmpdir2(), "ocr-post-comments");
28094
+ const tmpDir = join12(tmpdir2(), "ocr-post-comments");
27494
28095
  try {
27495
28096
  mkdirSync2(tmpDir, { recursive: true, mode: 448 });
27496
28097
  } catch {
27497
28098
  }
27498
- const tmpFile = join11(tmpDir, `${randomUUID()}.md`);
28099
+ const tmpFile = join12(tmpDir, `${randomUUID()}.md`);
27499
28100
  writeFileSync3(tmpFile, content, { mode: 384 });
27500
- const repoRoot = dirname8(ocrDir);
28101
+ const repoRoot = dirname9(ocrDir);
27501
28102
  try {
27502
28103
  const { stdout } = await execFileAsync(
27503
28104
  "gh",
@@ -27541,9 +28142,9 @@ function cleanupAllPostGenerations() {
27541
28142
  }
27542
28143
 
27543
28144
  // src/server/index.ts
27544
- var __dirname3 = dirname9(fileURLToPath3(import.meta.url));
28145
+ var __dirname3 = dirname10(fileURLToPath3(import.meta.url));
27545
28146
  var AUTH_TOKEN = randomBytes(32).toString("hex");
27546
- var app = (0, import_express11.default)();
28147
+ var app = (0, import_express12.default)();
27547
28148
  var httpServer = createServer(app);
27548
28149
  var io = new SocketIOServer(httpServer, {
27549
28150
  cors: {
@@ -27552,7 +28153,7 @@ var io = new SocketIOServer(httpServer, {
27552
28153
  maxHttpBufferSize: 1e6
27553
28154
  // 1 MB — explicit default; review if large payloads are needed
27554
28155
  });
27555
- app.use(import_express11.default.json());
28156
+ app.use(import_express12.default.json());
27556
28157
  if (process.env.NODE_ENV !== "production") {
27557
28158
  app.use((_req, res, next) => {
27558
28159
  const origin = _req.headers.origin;
@@ -27604,12 +28205,12 @@ async function startServer(options = {}) {
27604
28205
  const ocrDir = resolveOcrDir();
27605
28206
  const aiCliService = new AiCliService(ocrDir);
27606
28207
  const db = await openDb(ocrDir);
27607
- const dataDir = join12(ocrDir, "data");
27608
- const pidFilePath = join12(dataDir, "dashboard.pid");
28208
+ const dataDir = join13(ocrDir, "data");
28209
+ const pidFilePath = join13(dataDir, "dashboard.pid");
27609
28210
  mkdirSync3(dataDir, { recursive: true });
27610
- if (existsSync8(pidFilePath)) {
28211
+ if (existsSync9(pidFilePath)) {
27611
28212
  try {
27612
- const oldPid = parseInt(readFileSync9(pidFilePath, "utf-8").trim(), 10);
28213
+ const oldPid = parseInt(readFileSync10(pidFilePath, "utf-8").trim(), 10);
27613
28214
  if (!isNaN(oldPid)) {
27614
28215
  try {
27615
28216
  process.kill(oldPid, 0);
@@ -27709,11 +28310,12 @@ async function startServer(options = {}) {
27709
28310
  app.use("/api/commands", createCommandsRouter(db));
27710
28311
  app.use("/api/config", createConfigRouter(ocrDir, aiCliService));
27711
28312
  app.use("/api/sessions", createChatRouter(db, ocrDir));
27712
- const clientDir = join12(__dirname3, "client");
27713
- if (process.env.NODE_ENV === "production" && existsSync8(clientDir)) {
27714
- app.use(import_express11.default.static(clientDir, { index: false }));
27715
- const indexHtmlPath = join12(clientDir, "index.html");
27716
- const rawIndexHtml = existsSync8(indexHtmlPath) ? readFileSync9(indexHtmlPath, "utf-8") : "";
28313
+ app.use("/api/reviewers", createReviewersRouter(ocrDir));
28314
+ const clientDir = join13(__dirname3, "client");
28315
+ if (process.env.NODE_ENV === "production" && existsSync9(clientDir)) {
28316
+ app.use(import_express12.default.static(clientDir, { index: false }));
28317
+ const indexHtmlPath = join13(clientDir, "index.html");
28318
+ const rawIndexHtml = existsSync9(indexHtmlPath) ? readFileSync10(indexHtmlPath, "utf-8") : "";
27717
28319
  const tokenScript = `<script>window.__OCR_TOKEN__=${JSON.stringify(AUTH_TOKEN)};</script>`;
27718
28320
  const injectedIndexHtml = rawIndexHtml.replace(
27719
28321
  "</head>",
@@ -27736,7 +28338,7 @@ async function startServer(options = {}) {
27736
28338
  console.log("Client disconnected:", socket.id);
27737
28339
  });
27738
28340
  });
27739
- const dbFilePath = join12(ocrDir, "data", "ocr.db");
28341
+ const dbFilePath = join13(ocrDir, "data", "ocr.db");
27740
28342
  const dbSyncWatcher = new DbSyncWatcher(db, dbFilePath, io, () => {
27741
28343
  saveDb(db, ocrDir);
27742
28344
  });
@@ -27747,12 +28349,13 @@ async function startServer(options = {}) {
27747
28349
  () => dbSyncWatcher.syncFromDisk(),
27748
28350
  () => dbSyncWatcher.markOwnWrite()
27749
28351
  );
27750
- const sessionsDir = join12(ocrDir, "sessions");
28352
+ const sessionsDir = join13(ocrDir, "sessions");
27751
28353
  const fsSync = new FilesystemSync(db, sessionsDir, io, () => saveDb(db, ocrDir));
27752
28354
  await fsSync.fullScan();
27753
28355
  saveDb(db, ocrDir);
27754
28356
  fsSync.startWatching();
27755
28357
  console.log(`Watching sessions: ${sessionsDir}`);
28358
+ const stopReviewersWatch = watchReviewersMeta(ocrDir, io);
27756
28359
  await new Promise((resolve3, reject) => {
27757
28360
  httpServer.once("error", (err) => {
27758
28361
  if (err.code === "EADDRINUSE") {
@@ -27836,6 +28439,7 @@ async function startServer(options = {}) {
27836
28439
  }
27837
28440
  dbSyncWatcher.stopWatching();
27838
28441
  fsSync.stopWatching();
28442
+ stopReviewersWatch();
27839
28443
  io.close();
27840
28444
  httpServer.close(() => {
27841
28445
  try {