@runfusion/fusion 0.9.3 → 0.9.4

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 (37) hide show
  1. package/dist/bin.js +2876 -915
  2. package/dist/client/assets/{AgentDetailView-D9UWpTYr.js → AgentDetailView-5W1q48YS.js} +3 -3
  3. package/dist/client/assets/{AgentsView-DeCfRupM.js → AgentsView-DcEnemu0.js} +3 -3
  4. package/dist/client/assets/{ChatView-ChlqnJfu.js → ChatView-CTc6mP8y.js} +1 -1
  5. package/dist/client/assets/{DevServerView-B7EjWlgc.js → DevServerView-LOrDrAYm.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-crtmkC00.js → DirectoryPicker-Bgp6PCbu.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-BLxVoopL.js → DocumentsView-CNbnZ7Q3.js} +1 -1
  8. package/dist/client/assets/{InsightsView-CcdTychV.js → InsightsView-CmJwV-ZC.js} +1 -1
  9. package/dist/client/assets/{MemoryView-rSwx9Md8.js → MemoryView-Bwi5p79s.js} +1 -1
  10. package/dist/client/assets/{NodesView-Bwz0cHKV.js → NodesView-1pZii99I.js} +3 -3
  11. package/dist/client/assets/{PiExtensionsManager-Uo3E8Ae7.js → PiExtensionsManager-CokhM-MB.js} +3 -3
  12. package/dist/client/assets/{PluginManager-HtW8xVY8.js → PluginManager-cHaGKMgY.js} +1 -1
  13. package/dist/client/assets/ResearchView-BVJFgfat.css +1 -0
  14. package/dist/client/assets/ResearchView-CQDI2y7Q.js +1 -0
  15. package/dist/client/assets/{RoadmapsView-C2j64cbz.js → RoadmapsView-BTo3BT0I.js} +2 -2
  16. package/dist/client/assets/{SettingsModal-YjpwLH2V.css → SettingsModal-9HS8MnmW.css} +1 -1
  17. package/dist/client/assets/{SettingsModal-CVd9kNk7.js → SettingsModal-D5slUUsC.js} +1 -1
  18. package/dist/client/assets/SettingsModal-EEQwF0Ql.js +31 -0
  19. package/dist/client/assets/{SetupWizardModal-Dy-vQpTm.js → SetupWizardModal-1qSn8Yl0.js} +1 -1
  20. package/dist/client/assets/{SkillsView-BQwTyjxc.js → SkillsView-CY3I5OYc.js} +1 -1
  21. package/dist/client/assets/{TodoView-Dce4DrzU.js → TodoView-B1GDwwhR.js} +2 -2
  22. package/dist/client/assets/{folder-open-DWUflP4Q.js → folder-open-DPESt6bg.js} +1 -1
  23. package/dist/client/assets/{index-C3-q81dV.css → index-2_pvFDiN.css} +1 -1
  24. package/dist/client/assets/index-DNIrnlpO.js +656 -0
  25. package/dist/client/assets/{list-checks-CusZ_RMn.js → list-checks-D7D9kx7Y.js} +1 -1
  26. package/dist/client/assets/{star-C-NXZn1F.js → star-C59_6aNu.js} +1 -1
  27. package/dist/client/assets/{upload-CXJ5L6L4.js → upload-1I0eQddJ.js} +1 -1
  28. package/dist/client/assets/{users-YaA3x5mt.js → users-DH50eBCX.js} +1 -1
  29. package/dist/client/index.html +2 -2
  30. package/dist/client/version.json +1 -1
  31. package/dist/extension.js +2200 -380
  32. package/dist/pi-claude-cli/package.json +1 -1
  33. package/package.json +1 -1
  34. package/dist/client/assets/ResearchView-BV-iy9g8.js +0 -1
  35. package/dist/client/assets/ResearchView-aQkoD9QR.css +0 -1
  36. package/dist/client/assets/SettingsModal-ClnT1Lcv.js +0 -31
  37. package/dist/client/assets/index-Bs3RZu5I.js +0 -656
package/dist/extension.js CHANGED
@@ -101,7 +101,23 @@ var init_settings_schema = __esm({
101
101
  dashboardCurrentProjectIdByNode: void 0,
102
102
  // Dashboard TUI memory guard
103
103
  vitestAutoKillEnabled: true,
104
- vitestKillThresholdPct: 90
104
+ vitestKillThresholdPct: 90,
105
+ researchGlobalEnabled: true,
106
+ researchGlobalMaxConcurrentRuns: 3,
107
+ researchGlobalDefaultTimeout: 3e5,
108
+ researchGlobalMaxSourcesPerRun: 20,
109
+ researchGlobalMaxSynthesisRounds: 2,
110
+ researchWebSearchProvider: "none",
111
+ researchSearxngUrl: void 0,
112
+ researchBraveApiKey: void 0,
113
+ researchGoogleSearchApiKey: void 0,
114
+ researchGoogleSearchCx: void 0,
115
+ researchTavilyApiKey: void 0,
116
+ researchGitHubEnabled: false,
117
+ researchLocalDocsEnabled: true,
118
+ researchMaxSearchResults: 10,
119
+ researchFetchTimeoutMs: 3e4,
120
+ researchUserAgent: "FusionResearchBot/1.0"
105
121
  };
106
122
  DEFAULT_PROJECT_SETTINGS = {
107
123
  globalPause: false,
@@ -184,7 +200,7 @@ var init_settings_schema = __esm({
184
200
  autoBackupRetention: 7,
185
201
  autoBackupDir: ".fusion/backups",
186
202
  autoSummarizeTitles: false,
187
- useAiMergeCommitSummary: false,
203
+ useAiMergeCommitSummary: true,
188
204
  titleSummarizerProvider: void 0,
189
205
  titleSummarizerModelId: void 0,
190
206
  titleSummarizerFallbackProvider: void 0,
@@ -248,6 +264,11 @@ var init_settings_schema = __esm({
248
264
  reflectionAfterTask: true,
249
265
  reviewHandoffPolicy: "disabled",
250
266
  showQuickChatFAB: false,
267
+ researchEnabled: true,
268
+ researchMaxConcurrentRuns: 3,
269
+ researchDefaultTimeout: 3e5,
270
+ researchMaxSourcesPerRun: 20,
271
+ researchMaxSynthesisRounds: 2,
251
272
  experimentalFeatures: {}
252
273
  };
253
274
  DEFAULT_SETTINGS = {
@@ -6754,9 +6775,9 @@ var init_global_settings = __esm({
6754
6775
  * Serialize operations via promise chain to prevent lost-update races.
6755
6776
  */
6756
6777
  withLock(fn) {
6757
- let resolve18;
6778
+ let resolve19;
6758
6779
  const next = new Promise((r) => {
6759
- resolve18 = r;
6780
+ resolve19 = r;
6760
6781
  });
6761
6782
  const prev = this.lock;
6762
6783
  this.lock = next;
@@ -6764,7 +6785,7 @@ var init_global_settings = __esm({
6764
6785
  try {
6765
6786
  return await fn();
6766
6787
  } finally {
6767
- resolve18();
6788
+ resolve19();
6768
6789
  }
6769
6790
  });
6770
6791
  }
@@ -11989,7 +12010,7 @@ var init_research_store = __esm({
11989
12010
  }
11990
12011
  return deleted;
11991
12012
  }
11992
- appendEvent(runId, event) {
12013
+ addEvent(runId, event) {
11993
12014
  const run = this.getRun(runId);
11994
12015
  if (!run) throw new Error(`Research run not found: ${runId}`);
11995
12016
  const created = {
@@ -12000,13 +12021,18 @@ var init_research_store = __esm({
12000
12021
  metadata: event.metadata
12001
12022
  };
12002
12023
  this.updateRun(runId, { events: [...run.events, created] });
12024
+ this.emit("event:added", { runId, event: created });
12003
12025
  return created;
12004
12026
  }
12027
+ appendEvent(runId, event) {
12028
+ return this.addEvent(runId, event);
12029
+ }
12005
12030
  addSource(runId, source) {
12006
12031
  const run = this.getRun(runId);
12007
12032
  if (!run) throw new Error(`Research run not found: ${runId}`);
12008
12033
  const created = { ...source, id: generateId("RSRC") };
12009
12034
  this.updateRun(runId, { sources: [...run.sources, created] });
12035
+ this.emit("source:added", { runId, source: created });
12010
12036
  return created;
12011
12037
  }
12012
12038
  updateSource(runId, sourceId, updates) {
@@ -28419,9 +28445,9 @@ var init_automation_store = __esm({
28419
28445
  */
28420
28446
  withScheduleLock(id, fn) {
28421
28447
  const prev = this.scheduleLocks.get(id) ?? Promise.resolve();
28422
- let resolve18;
28448
+ let resolve19;
28423
28449
  const next = new Promise((r) => {
28424
- resolve18 = r;
28450
+ resolve19 = r;
28425
28451
  });
28426
28452
  this.scheduleLocks.set(id, next);
28427
28453
  return prev.then(async () => {
@@ -28431,7 +28457,7 @@ var init_automation_store = __esm({
28431
28457
  if (this.scheduleLocks.get(id) === next) {
28432
28458
  this.scheduleLocks.delete(id);
28433
28459
  }
28434
- resolve18();
28460
+ resolve19();
28435
28461
  }
28436
28462
  });
28437
28463
  }
@@ -30220,7 +30246,7 @@ var init_project_memory = __esm({
30220
30246
  // ../core/src/run-command.ts
30221
30247
  import { spawn } from "node:child_process";
30222
30248
  function runCommandAsync(command, options = {}) {
30223
- return new Promise((resolve18) => {
30249
+ return new Promise((resolve19) => {
30224
30250
  const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
30225
30251
  let stdout = "";
30226
30252
  let stderr = "";
@@ -30279,7 +30305,7 @@ function runCommandAsync(command, options = {}) {
30279
30305
  clearTimeout(forceKillTimer);
30280
30306
  forceKillTimer = null;
30281
30307
  }
30282
- resolve18({
30308
+ resolve19({
30283
30309
  stdout,
30284
30310
  stderr,
30285
30311
  exitCode: null,
@@ -30297,7 +30323,7 @@ function runCommandAsync(command, options = {}) {
30297
30323
  }
30298
30324
  signalProcessGroup("SIGTERM");
30299
30325
  scheduleForceKill(NORMAL_CLEANUP_FORCE_KILL_DELAY_MS);
30300
- resolve18({
30326
+ resolve19({
30301
30327
  stdout,
30302
30328
  stderr,
30303
30329
  exitCode: code,
@@ -31035,10 +31061,10 @@ ${recentText}` : void 0
31035
31061
  * report the same total-execution figure that the task detail Stats panel
31036
31062
  * computes from the full log.
31037
31063
  */
31038
- computeTimedExecutionMs(log9) {
31039
- if (!log9 || log9.length === 0) return 0;
31064
+ computeTimedExecutionMs(log17) {
31065
+ if (!log17 || log17.length === 0) return 0;
31040
31066
  let total = 0;
31041
- for (const entry of log9) {
31067
+ for (const entry of log17) {
31042
31068
  const action = typeof entry.action === "string" ? entry.action : "";
31043
31069
  const outcome = typeof entry.outcome === "string" ? entry.outcome : "";
31044
31070
  if (!action.includes("[timing]") && !outcome.includes("[timing]")) continue;
@@ -31512,9 +31538,9 @@ ${outcome}`;
31512
31538
  * lost-update races on the nextId counter.
31513
31539
  */
31514
31540
  withConfigLock(fn) {
31515
- let resolve18;
31541
+ let resolve19;
31516
31542
  const next = new Promise((r) => {
31517
- resolve18 = r;
31543
+ resolve19 = r;
31518
31544
  });
31519
31545
  const prev = this.configLock;
31520
31546
  this.configLock = next;
@@ -31522,7 +31548,7 @@ ${outcome}`;
31522
31548
  try {
31523
31549
  return await fn();
31524
31550
  } finally {
31525
- resolve18();
31551
+ resolve19();
31526
31552
  }
31527
31553
  });
31528
31554
  }
@@ -31532,9 +31558,9 @@ ${outcome}`;
31532
31558
  */
31533
31559
  withTaskLock(id, fn) {
31534
31560
  const prev = this.taskLocks.get(id) ?? Promise.resolve();
31535
- let resolve18;
31561
+ let resolve19;
31536
31562
  const next = new Promise((r) => {
31537
- resolve18 = r;
31563
+ resolve19 = r;
31538
31564
  });
31539
31565
  this.taskLocks.set(id, next);
31540
31566
  return prev.then(async () => {
@@ -31544,7 +31570,7 @@ ${outcome}`;
31544
31570
  if (this.taskLocks.get(id) === next) {
31545
31571
  this.taskLocks.delete(id);
31546
31572
  }
31547
- resolve18();
31573
+ resolve19();
31548
31574
  }
31549
31575
  });
31550
31576
  }
@@ -33025,13 +33051,13 @@ ${task.description}
33025
33051
  if (row.column === "archived") {
33026
33052
  throw new Error(`Task ${id} is archived \u2014 logging is read-only`);
33027
33053
  }
33028
- const log9 = fromJson(row.log) || [];
33029
- log9.push(entry);
33030
- if (log9.length > TASK_ACTIVITY_LOG_ENTRY_LIMIT) {
33031
- log9.splice(0, log9.length - TASK_ACTIVITY_LOG_ENTRY_LIMIT);
33054
+ const log17 = fromJson(row.log) || [];
33055
+ log17.push(entry);
33056
+ if (log17.length > TASK_ACTIVITY_LOG_ENTRY_LIMIT) {
33057
+ log17.splice(0, log17.length - TASK_ACTIVITY_LOG_ENTRY_LIMIT);
33032
33058
  }
33033
33059
  const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
33034
- this.db.prepare("UPDATE tasks SET log = ?, updatedAt = ? WHERE id = ?").run(toJson(log9), updatedAt, id);
33060
+ this.db.prepare("UPDATE tasks SET log = ?, updatedAt = ? WHERE id = ?").run(toJson(log17), updatedAt, id);
33035
33061
  this.db.bumpLastModified();
33036
33062
  const current = this.readTaskFromDb(id);
33037
33063
  if (current) {
@@ -33042,7 +33068,7 @@ ${task.description}
33042
33068
  this.emit("task:updated", current);
33043
33069
  return current;
33044
33070
  }
33045
- const emittedTask = { id, log: log9, updatedAt };
33071
+ const emittedTask = { id, log: log17, updatedAt };
33046
33072
  this.emit("task:updated", emittedTask);
33047
33073
  return emittedTask;
33048
33074
  });
@@ -33796,7 +33822,7 @@ ${task.description}
33796
33822
  }
33797
33823
  }
33798
33824
  }
33799
- await new Promise((resolve18) => setImmediate(resolve18));
33825
+ await new Promise((resolve19) => setImmediate(resolve19));
33800
33826
  const selectClause = this.getTaskSelectClause(true);
33801
33827
  const changedRows = this.lastPollTime ? this.db.prepare(`SELECT ${selectClause} FROM tasks WHERE updatedAt > ? OR columnMovedAt > ?`).all(this.lastPollTime, this.lastPollTime) : this.db.prepare(`SELECT ${selectClause} FROM tasks`).all();
33802
33828
  this.lastPollTime = (/* @__PURE__ */ new Date()).toISOString();
@@ -33816,7 +33842,7 @@ ${task.description}
33816
33842
  this.emit("task:updated", task);
33817
33843
  }
33818
33844
  if (i > 0 && i % 50 === 0) {
33819
- await new Promise((resolve18) => setImmediate(resolve18));
33845
+ await new Promise((resolve19) => setImmediate(resolve19));
33820
33846
  }
33821
33847
  }
33822
33848
  const elapsed = Date.now() - startTime;
@@ -35672,7 +35698,7 @@ function runGh(args, cwd) {
35672
35698
  }
35673
35699
  function runGhAsync(args, cwdOrOptions) {
35674
35700
  const { cwd, signal: externalSignal, timeoutMs = DEFAULT_GH_TIMEOUT_MS } = normalizeRunGhOptions(cwdOrOptions);
35675
- return new Promise((resolve18, reject) => {
35701
+ return new Promise((resolve19, reject) => {
35676
35702
  if (externalSignal?.aborted) {
35677
35703
  reject(makeGhError(`gh command aborted: ${describeAbortReason(externalSignal.reason)}`, "ABORT_ERR"));
35678
35704
  return;
@@ -35723,7 +35749,7 @@ function runGhAsync(args, cwdOrOptions) {
35723
35749
  ghError.stderr = stderr ?? "";
35724
35750
  reject(ghError);
35725
35751
  } else {
35726
- resolve18(stdout ?? "");
35752
+ resolve19(stdout ?? "");
35727
35753
  }
35728
35754
  }
35729
35755
  );
@@ -36011,9 +36037,9 @@ var init_routine_store = __esm({
36011
36037
  */
36012
36038
  withRoutineLock(id, fn) {
36013
36039
  const prev = this.routineLocks.get(id) ?? Promise.resolve();
36014
- let resolve18;
36040
+ let resolve19;
36015
36041
  const next = new Promise((r) => {
36016
- resolve18 = r;
36042
+ resolve19 = r;
36017
36043
  });
36018
36044
  this.routineLocks.set(id, next);
36019
36045
  return prev.then(async () => {
@@ -36023,7 +36049,7 @@ var init_routine_store = __esm({
36023
36049
  if (this.routineLocks.get(id) === next) {
36024
36050
  this.routineLocks.delete(id);
36025
36051
  }
36026
- resolve18();
36052
+ resolve19();
36027
36053
  }
36028
36054
  });
36029
36055
  }
@@ -36622,13 +36648,13 @@ var init_plugin_loader = __esm({
36622
36648
  * Execute a promise with a timeout.
36623
36649
  */
36624
36650
  withTimeout(promise, ms, timeoutMessage) {
36625
- return new Promise((resolve18, reject) => {
36651
+ return new Promise((resolve19, reject) => {
36626
36652
  const timer = setTimeout(() => {
36627
36653
  reject(new Error(timeoutMessage));
36628
36654
  }, ms);
36629
36655
  promise.then((result) => {
36630
36656
  clearTimeout(timer);
36631
- resolve18(result);
36657
+ resolve19(result);
36632
36658
  }).catch((err) => {
36633
36659
  clearTimeout(timer);
36634
36660
  reject(err);
@@ -39051,7 +39077,7 @@ var init_memory_insights = __esm({
39051
39077
  });
39052
39078
 
39053
39079
  // ../core/src/research-types.ts
39054
- var RESEARCH_RUN_STATUSES, RESEARCH_SOURCE_STATUSES, RESEARCH_EXPORT_FORMATS, RESEARCH_SOURCE_TYPES, RESEARCH_EVENT_TYPES;
39080
+ var RESEARCH_RUN_STATUSES, RESEARCH_SOURCE_STATUSES, RESEARCH_EXPORT_FORMATS, RESEARCH_SOURCE_TYPES, RESEARCH_EVENT_TYPES, RESEARCH_ORCHESTRATION_PHASES, RESEARCH_ORCHESTRATION_STEP_STATUSES;
39055
39081
  var init_research_types = __esm({
39056
39082
  "../core/src/research-types.ts"() {
39057
39083
  "use strict";
@@ -39078,6 +39104,17 @@ var init_research_types = __esm({
39078
39104
  "result_updated",
39079
39105
  "progress"
39080
39106
  ];
39107
+ RESEARCH_ORCHESTRATION_PHASES = [
39108
+ "planning",
39109
+ "searching",
39110
+ "fetching",
39111
+ "synthesizing",
39112
+ "finalizing",
39113
+ "completed",
39114
+ "failed",
39115
+ "cancelled"
39116
+ ];
39117
+ RESEARCH_ORCHESTRATION_STEP_STATUSES = ["pending", "running", "completed", "failed", "skipped"];
39081
39118
  }
39082
39119
  });
39083
39120
 
@@ -39669,7 +39706,7 @@ var require_node = __commonJS({
39669
39706
  var tty = __require("tty");
39670
39707
  var util = __require("util");
39671
39708
  exports.init = init;
39672
- exports.log = log9;
39709
+ exports.log = log17;
39673
39710
  exports.formatArgs = formatArgs;
39674
39711
  exports.save = save;
39675
39712
  exports.load = load;
@@ -39804,7 +39841,7 @@ var require_node = __commonJS({
39804
39841
  }
39805
39842
  return (/* @__PURE__ */ new Date()).toISOString() + " ";
39806
39843
  }
39807
- function log9(...args) {
39844
+ function log17(...args) {
39808
39845
  return process.stderr.write(util.formatWithOptions(exports.inspectOpts, ...args) + "\n");
39809
39846
  }
39810
39847
  function save(namespaces) {
@@ -40015,9 +40052,9 @@ var require_pump = __commonJS({
40015
40052
  "use strict";
40016
40053
  var once = require_once();
40017
40054
  var eos = require_end_of_stream();
40018
- var fs;
40055
+ var fs2;
40019
40056
  try {
40020
- fs = __require("fs");
40057
+ fs2 = __require("fs");
40021
40058
  } catch (e) {
40022
40059
  }
40023
40060
  var noop = function() {
@@ -40028,8 +40065,8 @@ var require_pump = __commonJS({
40028
40065
  };
40029
40066
  var isFS = function(stream) {
40030
40067
  if (!ancient) return false;
40031
- if (!fs) return false;
40032
- return (stream instanceof (fs.ReadStream || noop) || stream instanceof (fs.WriteStream || noop)) && isFn(stream.close);
40068
+ if (!fs2) return false;
40069
+ return (stream instanceof (fs2.ReadStream || noop) || stream instanceof (fs2.WriteStream || noop)) && isFn(stream.close);
40033
40070
  };
40034
40071
  var isRequest = function(stream) {
40035
40072
  return stream.setHeader && isFn(stream.abort);
@@ -40153,7 +40190,7 @@ var require_get_stream = __commonJS({
40153
40190
  };
40154
40191
  const { maxBuffer } = options;
40155
40192
  let stream;
40156
- await new Promise((resolve18, reject) => {
40193
+ await new Promise((resolve19, reject) => {
40157
40194
  const rejectPromise = (error) => {
40158
40195
  if (error && stream.getBufferedLength() <= BufferConstants.MAX_LENGTH) {
40159
40196
  error.bufferedData = stream.getBufferedValue();
@@ -40165,7 +40202,7 @@ var require_get_stream = __commonJS({
40165
40202
  rejectPromise(error);
40166
40203
  return;
40167
40204
  }
40168
- resolve18();
40205
+ resolve19();
40169
40206
  });
40170
40207
  stream.on("data", () => {
40171
40208
  if (stream.getBufferedLength() > maxBuffer) {
@@ -40243,7 +40280,7 @@ var require_pend = __commonJS({
40243
40280
  var require_fd_slicer = __commonJS({
40244
40281
  "../../node_modules/.pnpm/fd-slicer@1.1.0/node_modules/fd-slicer/index.js"(exports) {
40245
40282
  "use strict";
40246
- var fs = __require("fs");
40283
+ var fs2 = __require("fs");
40247
40284
  var util = __require("util");
40248
40285
  var stream = __require("stream");
40249
40286
  var Readable = stream.Readable;
@@ -40268,7 +40305,7 @@ var require_fd_slicer = __commonJS({
40268
40305
  FdSlicer.prototype.read = function(buffer, offset, length, position, callback) {
40269
40306
  var self = this;
40270
40307
  self.pend.go(function(cb) {
40271
- fs.read(self.fd, buffer, offset, length, position, function(err, bytesRead, buffer2) {
40308
+ fs2.read(self.fd, buffer, offset, length, position, function(err, bytesRead, buffer2) {
40272
40309
  cb();
40273
40310
  callback(err, bytesRead, buffer2);
40274
40311
  });
@@ -40277,7 +40314,7 @@ var require_fd_slicer = __commonJS({
40277
40314
  FdSlicer.prototype.write = function(buffer, offset, length, position, callback) {
40278
40315
  var self = this;
40279
40316
  self.pend.go(function(cb) {
40280
- fs.write(self.fd, buffer, offset, length, position, function(err, written, buffer2) {
40317
+ fs2.write(self.fd, buffer, offset, length, position, function(err, written, buffer2) {
40281
40318
  cb();
40282
40319
  callback(err, written, buffer2);
40283
40320
  });
@@ -40298,7 +40335,7 @@ var require_fd_slicer = __commonJS({
40298
40335
  if (self.refCount > 0) return;
40299
40336
  if (self.refCount < 0) throw new Error("invalid unref");
40300
40337
  if (self.autoClose) {
40301
- fs.close(self.fd, onCloseDone);
40338
+ fs2.close(self.fd, onCloseDone);
40302
40339
  }
40303
40340
  function onCloseDone(err) {
40304
40341
  if (err) {
@@ -40335,7 +40372,7 @@ var require_fd_slicer = __commonJS({
40335
40372
  self.context.pend.go(function(cb) {
40336
40373
  if (self.destroyed) return cb();
40337
40374
  var buffer = new Buffer(toRead);
40338
- fs.read(self.context.fd, buffer, 0, toRead, self.pos, function(err, bytesRead) {
40375
+ fs2.read(self.context.fd, buffer, 0, toRead, self.pos, function(err, bytesRead) {
40339
40376
  if (err) {
40340
40377
  self.destroy(err);
40341
40378
  } else if (bytesRead === 0) {
@@ -40382,7 +40419,7 @@ var require_fd_slicer = __commonJS({
40382
40419
  }
40383
40420
  self.context.pend.go(function(cb) {
40384
40421
  if (self.destroyed) return cb();
40385
- fs.write(self.context.fd, buffer, 0, buffer.length, self.pos, function(err2, bytes) {
40422
+ fs2.write(self.context.fd, buffer, 0, buffer.length, self.pos, function(err2, bytes) {
40386
40423
  if (err2) {
40387
40424
  self.destroy();
40388
40425
  cb();
@@ -40811,7 +40848,7 @@ var require_buffer_crc32 = __commonJS({
40811
40848
  var require_yauzl = __commonJS({
40812
40849
  "../../node_modules/.pnpm/yauzl@2.10.0/node_modules/yauzl/index.js"(exports) {
40813
40850
  "use strict";
40814
- var fs = __require("fs");
40851
+ var fs2 = __require("fs");
40815
40852
  var zlib = __require("zlib");
40816
40853
  var fd_slicer = require_fd_slicer();
40817
40854
  var crc32 = require_buffer_crc32();
@@ -40841,10 +40878,10 @@ var require_yauzl = __commonJS({
40841
40878
  if (options.validateEntrySizes == null) options.validateEntrySizes = true;
40842
40879
  if (options.strictFileNames == null) options.strictFileNames = false;
40843
40880
  if (callback == null) callback = defaultCallback;
40844
- fs.open(path2, "r", function(err, fd) {
40881
+ fs2.open(path2, "r", function(err, fd) {
40845
40882
  if (err) return callback(err);
40846
40883
  fromFd(fd, options, function(err2, zipfile) {
40847
- if (err2) fs.close(fd, defaultCallback);
40884
+ if (err2) fs2.close(fd, defaultCallback);
40848
40885
  callback(err2, zipfile);
40849
40886
  });
40850
40887
  });
@@ -40861,7 +40898,7 @@ var require_yauzl = __commonJS({
40861
40898
  if (options.validateEntrySizes == null) options.validateEntrySizes = true;
40862
40899
  if (options.strictFileNames == null) options.strictFileNames = false;
40863
40900
  if (callback == null) callback = defaultCallback;
40864
- fs.fstat(fd, function(err, stats) {
40901
+ fs2.fstat(fd, function(err, stats) {
40865
40902
  if (err) return callback(err);
40866
40903
  var reader = fd_slicer.createFromFd(fd, { autoClose: true });
40867
40904
  fromRandomAccessReader(reader, stats.size, options, callback);
@@ -41442,7 +41479,7 @@ var require_extract_zip = __commonJS({
41442
41479
  "../../node_modules/.pnpm/extract-zip@2.0.1/node_modules/extract-zip/index.js"(exports, module) {
41443
41480
  "use strict";
41444
41481
  var debug = require_src()("extract-zip");
41445
- var { createWriteStream, promises: fs } = __require("fs");
41482
+ var { createWriteStream, promises: fs2 } = __require("fs");
41446
41483
  var getStream = require_get_stream();
41447
41484
  var path2 = __require("path");
41448
41485
  var { promisify: promisify13 } = __require("util");
@@ -41459,7 +41496,7 @@ var require_extract_zip = __commonJS({
41459
41496
  debug("opening", this.zipPath, "with opts", this.opts);
41460
41497
  this.zipfile = await openZip(this.zipPath, { lazyEntries: true });
41461
41498
  this.canceled = false;
41462
- return new Promise((resolve18, reject) => {
41499
+ return new Promise((resolve19, reject) => {
41463
41500
  this.zipfile.on("error", (err) => {
41464
41501
  this.canceled = true;
41465
41502
  reject(err);
@@ -41468,7 +41505,7 @@ var require_extract_zip = __commonJS({
41468
41505
  this.zipfile.on("close", () => {
41469
41506
  if (!this.canceled) {
41470
41507
  debug("zip extraction complete");
41471
- resolve18();
41508
+ resolve19();
41472
41509
  }
41473
41510
  });
41474
41511
  this.zipfile.on("entry", async (entry) => {
@@ -41483,8 +41520,8 @@ var require_extract_zip = __commonJS({
41483
41520
  }
41484
41521
  const destDir = path2.dirname(path2.join(this.opts.dir, entry.fileName));
41485
41522
  try {
41486
- await fs.mkdir(destDir, { recursive: true });
41487
- const canonicalDestDir = await fs.realpath(destDir);
41523
+ await fs2.mkdir(destDir, { recursive: true });
41524
+ const canonicalDestDir = await fs2.realpath(destDir);
41488
41525
  const relativeDestDir = path2.relative(this.opts.dir, canonicalDestDir);
41489
41526
  if (relativeDestDir.split(path2.sep).includes("..")) {
41490
41527
  throw new Error(`Out of bound path "${canonicalDestDir}" found while processing file ${entry.fileName}`);
@@ -41528,14 +41565,14 @@ var require_extract_zip = __commonJS({
41528
41565
  mkdirOptions.mode = procMode;
41529
41566
  }
41530
41567
  debug("mkdir", { dir: destDir, ...mkdirOptions });
41531
- await fs.mkdir(destDir, mkdirOptions);
41568
+ await fs2.mkdir(destDir, mkdirOptions);
41532
41569
  if (isDir) return;
41533
41570
  debug("opening read stream", dest);
41534
41571
  const readStream = await promisify13(this.zipfile.openReadStream.bind(this.zipfile))(entry);
41535
41572
  if (symlink) {
41536
41573
  const link = await getStream(readStream);
41537
41574
  debug("creating symlink", link, dest);
41538
- await fs.symlink(link, dest);
41575
+ await fs2.symlink(link, dest);
41539
41576
  } else {
41540
41577
  await pipeline(readStream, createWriteStream(dest, { mode: procMode }));
41541
41578
  }
@@ -41567,8 +41604,8 @@ var require_extract_zip = __commonJS({
41567
41604
  if (!path2.isAbsolute(opts.dir)) {
41568
41605
  throw new Error("Target directory is expected to be absolute");
41569
41606
  }
41570
- await fs.mkdir(opts.dir, { recursive: true });
41571
- opts.dir = await fs.realpath(opts.dir);
41607
+ await fs2.mkdir(opts.dir, { recursive: true });
41608
+ opts.dir = await fs2.realpath(opts.dir);
41572
41609
  return new Extractor(zipPath, opts).extract();
41573
41610
  };
41574
41611
  }
@@ -43289,7 +43326,7 @@ var require_merge = __commonJS({
43289
43326
  var require_addPairToJSMap = __commonJS({
43290
43327
  "../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/addPairToJSMap.js"(exports) {
43291
43328
  "use strict";
43292
- var log9 = require_log();
43329
+ var log17 = require_log();
43293
43330
  var merge = require_merge();
43294
43331
  var stringify = require_stringify();
43295
43332
  var identity = require_identity();
@@ -43338,7 +43375,7 @@ var require_addPairToJSMap = __commonJS({
43338
43375
  let jsonStr = JSON.stringify(strKey);
43339
43376
  if (jsonStr.length > 40)
43340
43377
  jsonStr = jsonStr.substring(0, 36) + '..."';
43341
- log9.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
43378
+ log17.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
43342
43379
  ctx.mapKeyWarned = true;
43343
43380
  }
43344
43381
  return strKey;
@@ -48427,14 +48464,14 @@ var require_parser = __commonJS({
48427
48464
  case "scalar":
48428
48465
  case "single-quoted-scalar":
48429
48466
  case "double-quoted-scalar": {
48430
- const fs = this.flowScalar(this.type);
48467
+ const fs2 = this.flowScalar(this.type);
48431
48468
  if (atNextItem || it.value) {
48432
- map.items.push({ start, key: fs, sep: [] });
48469
+ map.items.push({ start, key: fs2, sep: [] });
48433
48470
  this.onKeyLine = true;
48434
48471
  } else if (it.sep) {
48435
- this.stack.push(fs);
48472
+ this.stack.push(fs2);
48436
48473
  } else {
48437
- Object.assign(it, { key: fs, sep: [] });
48474
+ Object.assign(it, { key: fs2, sep: [] });
48438
48475
  this.onKeyLine = true;
48439
48476
  }
48440
48477
  return;
@@ -48562,13 +48599,13 @@ var require_parser = __commonJS({
48562
48599
  case "scalar":
48563
48600
  case "single-quoted-scalar":
48564
48601
  case "double-quoted-scalar": {
48565
- const fs = this.flowScalar(this.type);
48602
+ const fs2 = this.flowScalar(this.type);
48566
48603
  if (!it || it.value)
48567
- fc.items.push({ start: [], key: fs, sep: [] });
48604
+ fc.items.push({ start: [], key: fs2, sep: [] });
48568
48605
  else if (it.sep)
48569
- this.stack.push(fs);
48606
+ this.stack.push(fs2);
48570
48607
  else
48571
- Object.assign(it, { key: fs, sep: [] });
48608
+ Object.assign(it, { key: fs2, sep: [] });
48572
48609
  return;
48573
48610
  }
48574
48611
  case "flow-map-end":
@@ -48734,7 +48771,7 @@ var require_public_api = __commonJS({
48734
48771
  var composer = require_composer();
48735
48772
  var Document = require_Document();
48736
48773
  var errors = require_errors();
48737
- var log9 = require_log();
48774
+ var log17 = require_log();
48738
48775
  var identity = require_identity();
48739
48776
  var lineCounter = require_line_counter();
48740
48777
  var parser = require_parser();
@@ -48786,7 +48823,7 @@ var require_public_api = __commonJS({
48786
48823
  const doc = parseDocument(src, options);
48787
48824
  if (!doc)
48788
48825
  return null;
48789
- doc.warnings.forEach((warning) => log9.warn(doc.options.logLevel, warning));
48826
+ doc.warnings.forEach((warning) => log17.warn(doc.options.logLevel, warning));
48790
48827
  if (doc.errors.length > 0) {
48791
48828
  if (doc.options.logLevel !== "silent")
48792
48829
  throw doc.errors[0];
@@ -50098,6 +50135,8 @@ __export(src_exports, {
50098
50135
  QmdMemoryBackend: () => QmdMemoryBackend,
50099
50136
  RESEARCH_EVENT_TYPES: () => RESEARCH_EVENT_TYPES,
50100
50137
  RESEARCH_EXPORT_FORMATS: () => RESEARCH_EXPORT_FORMATS,
50138
+ RESEARCH_ORCHESTRATION_PHASES: () => RESEARCH_ORCHESTRATION_PHASES,
50139
+ RESEARCH_ORCHESTRATION_STEP_STATUSES: () => RESEARCH_ORCHESTRATION_STEP_STATUSES,
50101
50140
  RESEARCH_RUN_STATUSES: () => RESEARCH_RUN_STATUSES,
50102
50141
  RESEARCH_SOURCE_STATUSES: () => RESEARCH_SOURCE_STATUSES,
50103
50142
  RESEARCH_SOURCE_TYPES: () => RESEARCH_SOURCE_TYPES,
@@ -51568,12 +51607,12 @@ var init_concurrency = __esm({
51568
51607
  this._active++;
51569
51608
  return Promise.resolve();
51570
51609
  }
51571
- return new Promise((resolve18) => {
51610
+ return new Promise((resolve19) => {
51572
51611
  this._waiters.push({
51573
51612
  priority,
51574
51613
  resolve: () => {
51575
51614
  this._active++;
51576
- resolve18();
51615
+ resolve19();
51577
51616
  }
51578
51617
  });
51579
51618
  });
@@ -54195,20 +54234,20 @@ async function withRateLimitRetry(fn, options = {}) {
54195
54234
  throw lastError ?? new Error("withRateLimitRetry: unexpected state");
54196
54235
  }
54197
54236
  function sleep(ms, signal) {
54198
- return new Promise((resolve18, reject) => {
54237
+ return new Promise((resolve19, reject) => {
54199
54238
  if (signal?.aborted) {
54200
54239
  reject(signal.reason ?? new Error("Aborted"));
54201
54240
  return;
54202
54241
  }
54203
- const timer = setTimeout(resolve18, ms);
54242
+ const timer = setTimeout(resolve19, ms);
54204
54243
  if (signal) {
54205
54244
  const onAbort = () => {
54206
54245
  clearTimeout(timer);
54207
54246
  reject(signal.reason ?? new Error("Aborted"));
54208
54247
  };
54209
54248
  signal.addEventListener("abort", onAbort, { once: true });
54210
- const origResolve = resolve18;
54211
- resolve18 = () => {
54249
+ const origResolve = resolve19;
54250
+ resolve19 = () => {
54212
54251
  signal.removeEventListener("abort", onAbort);
54213
54252
  origResolve();
54214
54253
  };
@@ -54288,9 +54327,9 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
54288
54327
  return { attachmentContents, imageContents };
54289
54328
  }
54290
54329
  const { readFile: readFile19 } = await import("node:fs/promises");
54291
- const { join: join40 } = await import("node:path");
54330
+ const { join: join41 } = await import("node:path");
54292
54331
  for (const att of attachments) {
54293
- const filePath = join40(
54332
+ const filePath = join41(
54294
54333
  rootDir,
54295
54334
  ".fusion",
54296
54335
  "tasks",
@@ -55828,9 +55867,9 @@ Remove or replace these ids and call fn_task_create again.`
55828
55867
  }
55829
55868
  try {
55830
55869
  const { readFile: readFile19 } = await import("node:fs/promises");
55831
- const { join: join40 } = await import("node:path");
55870
+ const { join: join41 } = await import("node:path");
55832
55871
  const promptContent = await readFile19(
55833
- join40(rootDir, promptPath),
55872
+ join41(rootDir, promptPath),
55834
55873
  "utf-8"
55835
55874
  ).catch((err) => {
55836
55875
  const msg = err instanceof Error ? err.message : String(err);
@@ -56201,7 +56240,7 @@ import { existsSync as existsSync22 } from "node:fs";
56201
56240
  import { join as join28 } from "node:path";
56202
56241
  import { Type as Type3 } from "typebox";
56203
56242
  async function execWithProcessGroup(command, options) {
56204
- return new Promise((resolve18, reject) => {
56243
+ return new Promise((resolve19, reject) => {
56205
56244
  if (options.signal?.aborted) {
56206
56245
  reject(Object.assign(
56207
56246
  new Error(`Command aborted before start: ${command}`),
@@ -56294,7 +56333,7 @@ async function execWithProcessGroup(command, options) {
56294
56333
  return;
56295
56334
  }
56296
56335
  if (code === 0) {
56297
- resolve18({ stdout, stderr, bufferOverflow: stdoutOverflow || stderrOverflow });
56336
+ resolve19({ stdout, stderr, bufferOverflow: stdoutOverflow || stderrOverflow });
56298
56337
  return;
56299
56338
  }
56300
56339
  reject(Object.assign(
@@ -56885,10 +56924,27 @@ async function generateAiMergeSummary(commitLog, diffStat, settings, rootDir) {
56885
56924
  return null;
56886
56925
  }
56887
56926
  }
56927
+ async function generateAiMergeSubject(commitLog, diffStat, settings, rootDir, branch, taskId, signal) {
56928
+ try {
56929
+ const resolved = resolveTitleSummarizerSettingsModel(settings);
56930
+ return await summarizeCommitSubject(
56931
+ diffStat,
56932
+ rootDir,
56933
+ resolved.provider,
56934
+ resolved.modelId,
56935
+ { branch, taskId, commitLog, signal }
56936
+ );
56937
+ } catch (err) {
56938
+ const message = err instanceof Error ? err.message : String(err);
56939
+ mergerLog.warn(`AI merge subject failed; using deterministic fallback (${message})`);
56940
+ return null;
56941
+ }
56942
+ }
56888
56943
  async function buildDeterministicMergeMessage(params) {
56889
- const { taskId, branch, commitLog, diffStat, includeTaskId, aiSummary } = params;
56944
+ const { taskId, branch, commitLog, diffStat, includeTaskId, aiSummary, aiSubject } = params;
56890
56945
  const prefix = includeTaskId ? `feat(${taskId})` : "feat";
56891
- const subject = `${prefix}: merge ${branch}`;
56946
+ const subjectSummary = aiSubject?.trim().length ? aiSubject.trim() : `merge ${branch}`;
56947
+ const subject = `${prefix}: ${subjectSummary}`;
56892
56948
  const trimmedCommitLog = commitLog?.trim() ?? "";
56893
56949
  const trimmedDiffStat = diffStat?.trim() ?? "";
56894
56950
  const commitsSection = trimmedCommitLog.length > 0 ? trimmedCommitLog : `- merge ${branch}`;
@@ -56904,7 +56960,7 @@ ${trimmedDiffStat}` : ""
56904
56960
  bodyArg: `-m "${escape(body)}"`
56905
56961
  };
56906
56962
  }
56907
- async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, includeTaskId, preAttemptHeadSha, authorArg, diffStat, settings, signal, aiSummary) {
56963
+ async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, includeTaskId, preAttemptHeadSha, authorArg, diffStat, settings, signal, aiSummary, aiSubject) {
56908
56964
  try {
56909
56965
  const { stdout: unstagedFiles } = await execAsync2("git diff --name-only", {
56910
56966
  cwd: rootDir,
@@ -56959,7 +57015,8 @@ async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, i
56959
57015
  commitLog: messageCommitLog,
56960
57016
  diffStat: messageDiffStat,
56961
57017
  includeTaskId,
56962
- aiSummary
57018
+ aiSummary,
57019
+ aiSubject
56963
57020
  });
56964
57021
  const trailerArg = buildTaskIdTrailerArg(taskId);
56965
57022
  if (!headMoved) {
@@ -58200,6 +58257,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
58200
58257
  diffStat = "(unable to read diff)";
58201
58258
  }
58202
58259
  const aiMergeSummary = settings.useAiMergeCommitSummary ? await generateAiMergeSummary(commitLog, diffStat, settings, rootDir) : null;
58260
+ const aiMergeSubject = settings.useAiMergeCommitSummary ? await generateAiMergeSubject(commitLog, diffStat, settings, rootDir, branch, taskId, options.signal) : null;
58203
58261
  try {
58204
58262
  const scopeResult = await validateDiffScope(store, taskId, diffStat, settings.strictScopeEnforcement);
58205
58263
  for (const warning of scopeResult.warnings) {
@@ -58261,6 +58319,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
58261
58319
  commitLog,
58262
58320
  diffStat,
58263
58321
  aiSummary: aiMergeSummary,
58322
+ aiSubject: aiMergeSubject,
58264
58323
  includeTaskId,
58265
58324
  sourceIssueRef,
58266
58325
  smartConflictResolution,
@@ -58383,7 +58442,8 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
58383
58442
  diffStat,
58384
58443
  settings,
58385
58444
  options.signal,
58386
- aiMergeSummary
58445
+ aiMergeSummary,
58446
+ aiMergeSubject
58387
58447
  );
58388
58448
  if (!finalized) {
58389
58449
  resetMergeWithWarn(rootDir, taskId, "verification-fix finalize");
@@ -58478,7 +58538,8 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
58478
58538
  diffStat,
58479
58539
  settings,
58480
58540
  options.signal,
58481
- aiMergeSummary
58541
+ aiMergeSummary,
58542
+ aiMergeSubject
58482
58543
  );
58483
58544
  if (!finalized) {
58484
58545
  resetMergeWithWarn(rootDir, taskId, "build-verification fix finalize");
@@ -58857,6 +58918,7 @@ async function executeMergeAttempt(params, aiTracker) {
58857
58918
  commitLog,
58858
58919
  diffStat,
58859
58920
  aiSummary,
58921
+ aiSubject,
58860
58922
  includeTaskId,
58861
58923
  sourceIssueRef,
58862
58924
  smartConflictResolution,
@@ -59102,7 +59164,8 @@ async function executeMergeAttempt(params, aiTracker) {
59102
59164
  commitLog: actualContext.commitLog || commitLog,
59103
59165
  diffStat: actualContext.diffStat || diffStat,
59104
59166
  includeTaskId,
59105
- aiSummary
59167
+ aiSummary,
59168
+ aiSubject
59106
59169
  });
59107
59170
  const trailerArg = buildTaskIdTrailerArg(taskId);
59108
59171
  await execAsync2(
@@ -60684,7 +60747,7 @@ function resolveExecutorModelPair(taskModelProvider, taskModelId, settings) {
60684
60747
  return { provider: void 0, modelId: void 0 };
60685
60748
  }
60686
60749
  function sleep2(ms) {
60687
- return new Promise((resolve18) => setTimeout(resolve18, ms));
60750
+ return new Promise((resolve19) => setTimeout(resolve19, ms));
60688
60751
  }
60689
60752
  var execAsync4, stepExecLog, MAX_STEP_RETRIES, RETRY_DELAYS_MS, NOOP_TASK_STORE, StepSessionExecutor;
60690
60753
  var init_step_session_executor = __esm({
@@ -64909,7 +64972,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
64909
64972
  );
64910
64973
  }
64911
64974
  const delay2 = this.WORKTREE_RETRY_DELAYS[attempt] || 1e3;
64912
- await new Promise((resolve18) => setTimeout(resolve18, delay2));
64975
+ await new Promise((resolve19) => setTimeout(resolve19, delay2));
64913
64976
  }
64914
64977
  }
64915
64978
  throw new Error("Unexpected exit from worktree creation retry loop");
@@ -65552,8 +65615,8 @@ Review the work done in this worktree and evaluate it against the criteria in yo
65552
65615
  executorLog.warn(`${taskId}: recoverApprovedStepsOnResume getTask failed: ${err instanceof Error ? err.message : String(err)}`);
65553
65616
  return;
65554
65617
  }
65555
- const log9 = detail.log ?? [];
65556
- if (log9.length === 0) return;
65618
+ const log17 = detail.log ?? [];
65619
+ if (log17.length === 0) return;
65557
65620
  let recovered = 0;
65558
65621
  for (let i = 0; i < detail.steps.length; i++) {
65559
65622
  if (detail.steps[i].status !== "in-progress") continue;
@@ -65562,8 +65625,8 @@ Review the work done in this worktree and evaluate it against the criteria in yo
65562
65625
  const stepName = detail.steps[i].name;
65563
65626
  const transitionPrefix = `Step ${i} (${stepName}) \u2192 `;
65564
65627
  const approvePrefix = `code review Step ${i}:`;
65565
- for (let j = 0; j < log9.length; j++) {
65566
- const action = log9[j].action || "";
65628
+ for (let j = 0; j < log17.length; j++) {
65629
+ const action = log17[j].action || "";
65567
65630
  if (action.startsWith(transitionPrefix)) {
65568
65631
  const status = action.slice(transitionPrefix.length).trim();
65569
65632
  if (status === "pending") lastPendingAt = j;
@@ -70492,6 +70555,1651 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
70492
70555
  }
70493
70556
  });
70494
70557
 
70558
+ // ../engine/src/research-orchestrator.ts
70559
+ var log6, ResearchOrchestrator;
70560
+ var init_research_orchestrator = __esm({
70561
+ "../engine/src/research-orchestrator.ts"() {
70562
+ "use strict";
70563
+ init_concurrency();
70564
+ init_logger2();
70565
+ log6 = createLogger2("research-orchestrator");
70566
+ ResearchOrchestrator = class {
70567
+ store;
70568
+ stepRunner;
70569
+ semaphore;
70570
+ activeRuns = /* @__PURE__ */ new Map();
70571
+ cancellation = /* @__PURE__ */ new Map();
70572
+ constructor(options) {
70573
+ this.store = options.store;
70574
+ this.stepRunner = options.stepRunner;
70575
+ this.semaphore = new AgentSemaphore(options.maxConcurrentRuns ?? 3);
70576
+ }
70577
+ createRun(config) {
70578
+ const run = this.store.createRun({
70579
+ query: "",
70580
+ providerConfig: config,
70581
+ metadata: {
70582
+ orchestration: {
70583
+ phase: "planning",
70584
+ stepIndex: 0,
70585
+ totalSteps: this.computeTotalSteps(config)
70586
+ }
70587
+ }
70588
+ });
70589
+ return run.id;
70590
+ }
70591
+ async startRun(runId, query, options = {}) {
70592
+ const run = this.store.getRun(runId);
70593
+ if (!run) throw new Error(`Research run not found: ${runId}`);
70594
+ const config = run.providerConfig ?? {};
70595
+ const controller = new AbortController();
70596
+ if (options.abortSignal) {
70597
+ options.abortSignal.addEventListener("abort", () => controller.abort(options.abortSignal?.reason), { once: true });
70598
+ }
70599
+ const totalSteps = this.computeTotalSteps(config);
70600
+ this.activeRuns.set(runId, {
70601
+ controller,
70602
+ phase: "planning",
70603
+ stepIndex: 0,
70604
+ totalSteps,
70605
+ config
70606
+ });
70607
+ await this.semaphore.run(async () => {
70608
+ this.store.updateRun(runId, { query, status: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString(), error: null });
70609
+ await this.runPhases(runId, query, config, controller.signal);
70610
+ });
70611
+ const updated = this.store.getRun(runId);
70612
+ if (!updated) throw new Error(`Research run not found after start: ${runId}`);
70613
+ return updated;
70614
+ }
70615
+ cancelRun(runId) {
70616
+ const active = this.activeRuns.get(runId);
70617
+ if (!active) return false;
70618
+ const state = {
70619
+ runId,
70620
+ controller: active.controller,
70621
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
70622
+ gracefulShutdown: true,
70623
+ reason: "Cancelled by user"
70624
+ };
70625
+ this.cancellation.set(runId, state);
70626
+ active.controller.abort(new Error("Research run cancelled"));
70627
+ return true;
70628
+ }
70629
+ retryRun(runId) {
70630
+ const run = this.store.getRun(runId);
70631
+ if (!run) throw new Error(`Research run not found: ${runId}`);
70632
+ if (run.status !== "failed" && run.status !== "cancelled") {
70633
+ throw new Error(`Research run ${runId} is not retryable (status=${run.status})`);
70634
+ }
70635
+ const next = this.store.createRun({
70636
+ query: run.query,
70637
+ topic: run.topic,
70638
+ providerConfig: run.providerConfig,
70639
+ tags: [...run.tags],
70640
+ metadata: {
70641
+ ...run.metadata ?? {},
70642
+ retryOfRunId: run.id
70643
+ }
70644
+ });
70645
+ this.store.addEvent(next.id, {
70646
+ type: "info",
70647
+ message: `Retry run created from ${run.id}`,
70648
+ metadata: { retryOfRunId: run.id }
70649
+ });
70650
+ return next.id;
70651
+ }
70652
+ getRunStatus(runId) {
70653
+ const run = this.store.getRun(runId);
70654
+ if (!run) throw new Error(`Research run not found: ${runId}`);
70655
+ const active = this.activeRuns.get(runId);
70656
+ const metadata = run.metadata?.orchestration ?? {};
70657
+ const phase = active?.phase ?? metadata.phase ?? this.statusToPhase(run.status);
70658
+ const stepIndex = active?.stepIndex ?? Number(metadata.stepIndex ?? 0);
70659
+ const totalSteps = active?.totalSteps ?? Number(metadata.totalSteps ?? 0);
70660
+ return {
70661
+ runId,
70662
+ status: run.status,
70663
+ phase,
70664
+ stepIndex,
70665
+ totalSteps,
70666
+ progress: totalSteps > 0 ? Math.min(1, stepIndex / totalSteps) : 0,
70667
+ active: this.activeRuns.has(runId)
70668
+ };
70669
+ }
70670
+ async runPhases(runId, query, config, signal) {
70671
+ try {
70672
+ await this.runPlanning(runId, query, config, signal);
70673
+ const sources = await this.runSearching(runId, query, config, signal);
70674
+ const fetchedSources = await this.runFetching(runId, sources, config, signal);
70675
+ const synthesis = await this.runSynthesis(runId, query, fetchedSources, config, signal);
70676
+ await this.runFinalizing(runId, synthesis.output, synthesis.citations, synthesis.confidence, signal);
70677
+ this.store.updateStatus(runId, "completed");
70678
+ this.transitionPhase(runId, "completed", "Research run completed");
70679
+ } catch (err) {
70680
+ if (signal.aborted) {
70681
+ this.onCancelled(runId);
70682
+ } else {
70683
+ const { message, detail } = formatError(err);
70684
+ this.store.addEvent(runId, {
70685
+ type: "error",
70686
+ message: `Research run failed: ${message}`,
70687
+ metadata: { detail }
70688
+ });
70689
+ this.store.updateStatus(runId, "failed", { error: message });
70690
+ this.transitionPhase(runId, "failed", "Research run failed", { error: message });
70691
+ }
70692
+ } finally {
70693
+ this.activeRuns.delete(runId);
70694
+ this.cancellation.delete(runId);
70695
+ }
70696
+ }
70697
+ async runPlanning(runId, query, config, _signal) {
70698
+ this.transitionPhase(runId, "planning", "Planning research execution");
70699
+ this.stepStarted(runId, {
70700
+ id: `${runId}-planning`,
70701
+ type: "synthesis-pass",
70702
+ phase: "planning",
70703
+ status: "running",
70704
+ order: 0,
70705
+ name: "Create plan",
70706
+ input: { query, providerCount: config.providers.length },
70707
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
70708
+ });
70709
+ this.stepCompleted(runId, `${runId}-planning`, { query });
70710
+ }
70711
+ async runSearching(runId, query, config, signal) {
70712
+ this.throwIfAborted(signal);
70713
+ this.transitionPhase(runId, "searching", "Searching sources");
70714
+ const allSources = [];
70715
+ for (const provider of config.providers) {
70716
+ this.throwIfAborted(signal);
70717
+ const step = this.createStep(runId, "source-query", "searching", `Search with ${provider.type}`, {
70718
+ query,
70719
+ provider: provider.type
70720
+ });
70721
+ this.stepStarted(runId, step);
70722
+ const result = await this.stepRunner.runSourceQuery(query, provider.type, provider.config, signal);
70723
+ if (!result.ok || !result.data) {
70724
+ this.stepFailed(runId, step.id, result.error?.message ?? `Provider ${provider.type} returned no data`, result.error);
70725
+ continue;
70726
+ }
70727
+ for (const source of result.data.slice(0, Math.max(0, config.maxSources - allSources.length))) {
70728
+ const saved = this.store.addSource(runId, source);
70729
+ allSources.push(saved);
70730
+ this.store.addEvent(runId, {
70731
+ type: "source_added",
70732
+ message: `Source found: ${saved.reference}`,
70733
+ metadata: { sourceId: saved.id, provider: provider.type }
70734
+ });
70735
+ }
70736
+ this.stepCompleted(runId, step.id, { sourceCount: result.data.length });
70737
+ if (allSources.length >= config.maxSources) break;
70738
+ }
70739
+ if (allSources.length === 0) {
70740
+ throw new Error("No sources discovered during search phase");
70741
+ }
70742
+ return allSources;
70743
+ }
70744
+ async runFetching(runId, sources, config, signal) {
70745
+ this.throwIfAborted(signal);
70746
+ this.transitionPhase(runId, "fetching", "Fetching source content");
70747
+ const fetched = [];
70748
+ const provider = config.providers[0];
70749
+ for (const source of sources.slice(0, config.maxSources)) {
70750
+ this.throwIfAborted(signal);
70751
+ const step = this.createStep(runId, "content-fetch", "fetching", `Fetch ${source.reference}`, {
70752
+ sourceId: source.id
70753
+ });
70754
+ this.stepStarted(runId, step);
70755
+ const result = await this.stepRunner.runContentFetch(source.reference, provider?.config, signal);
70756
+ if (!result.ok || !result.data) {
70757
+ this.stepFailed(runId, step.id, result.error?.message ?? "Failed to fetch source content", result.error);
70758
+ continue;
70759
+ }
70760
+ const updated = {
70761
+ ...source,
70762
+ content: result.data.content,
70763
+ metadata: {
70764
+ ...source.metadata ?? {},
70765
+ ...result.data.metadata ?? {}
70766
+ },
70767
+ status: "completed",
70768
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
70769
+ };
70770
+ this.store.updateSource(runId, source.id, updated);
70771
+ fetched.push(updated);
70772
+ this.stepCompleted(runId, step.id, { fetched: true });
70773
+ }
70774
+ if (fetched.length === 0) {
70775
+ throw new Error("No source content fetched");
70776
+ }
70777
+ return fetched;
70778
+ }
70779
+ async runSynthesis(runId, query, sources, config, signal) {
70780
+ this.throwIfAborted(signal);
70781
+ this.transitionPhase(runId, "synthesizing", "Synthesizing findings");
70782
+ let final;
70783
+ for (let round = 1; round <= Math.max(1, config.maxSynthesisRounds); round++) {
70784
+ this.throwIfAborted(signal);
70785
+ const step = this.createStep(runId, "synthesis-pass", "synthesizing", `Synthesis round ${round}`, {
70786
+ round
70787
+ });
70788
+ this.stepStarted(runId, step);
70789
+ const request2 = {
70790
+ query,
70791
+ sources,
70792
+ round,
70793
+ desiredFormat: "markdown"
70794
+ };
70795
+ const result = await this.stepRunner.runSynthesis(request2, config.synthesisModel, signal);
70796
+ if (!result.ok || !result.data) {
70797
+ this.stepFailed(runId, step.id, result.error?.message ?? "Synthesis failed", result.error);
70798
+ continue;
70799
+ }
70800
+ final = result.data;
70801
+ this.store.addEvent(runId, {
70802
+ type: "progress",
70803
+ message: `Synthesis round ${round} completed`,
70804
+ metadata: { round, confidence: result.data.confidence }
70805
+ });
70806
+ this.stepCompleted(runId, step.id, { round, citations: result.data.citations.length });
70807
+ }
70808
+ if (!final) {
70809
+ throw new Error("All synthesis rounds failed");
70810
+ }
70811
+ return final;
70812
+ }
70813
+ async runFinalizing(runId, output, citations, confidence, signal) {
70814
+ this.throwIfAborted(signal);
70815
+ this.transitionPhase(runId, "finalizing", "Finalizing research results");
70816
+ this.store.setResults(runId, {
70817
+ summary: output,
70818
+ findings: [
70819
+ {
70820
+ heading: "Synthesis",
70821
+ content: output,
70822
+ sources: citations,
70823
+ confidence
70824
+ }
70825
+ ],
70826
+ citations,
70827
+ synthesizedOutput: output
70828
+ });
70829
+ }
70830
+ onCancelled(runId) {
70831
+ const cancellation = this.cancellation.get(runId);
70832
+ this.store.addEvent(runId, {
70833
+ type: "warning",
70834
+ message: "Research run cancelled",
70835
+ metadata: {
70836
+ requestedAt: cancellation?.requestedAt,
70837
+ reason: cancellation?.reason
70838
+ }
70839
+ });
70840
+ this.store.updateStatus(runId, "cancelled", {
70841
+ cancelledAt: (/* @__PURE__ */ new Date()).toISOString(),
70842
+ error: cancellation?.reason
70843
+ });
70844
+ this.transitionPhase(runId, "cancelled", "Research run cancelled");
70845
+ }
70846
+ transitionPhase(runId, phase, message, metadata) {
70847
+ const active = this.activeRuns.get(runId);
70848
+ if (active) {
70849
+ active.phase = phase;
70850
+ }
70851
+ this.store.updateRun(runId, {
70852
+ metadata: {
70853
+ orchestration: {
70854
+ phase,
70855
+ stepIndex: active?.stepIndex ?? 0,
70856
+ totalSteps: active?.totalSteps ?? 0
70857
+ }
70858
+ }
70859
+ });
70860
+ this.store.addEvent(runId, {
70861
+ type: "progress",
70862
+ message,
70863
+ metadata: {
70864
+ orchestrationEventType: "phase-changed",
70865
+ phase,
70866
+ ...metadata ?? {}
70867
+ }
70868
+ });
70869
+ log6.log(`${runId}: phase changed -> ${phase}`);
70870
+ }
70871
+ stepStarted(runId, step) {
70872
+ this.bumpStep(runId, step.order);
70873
+ this.store.addEvent(runId, {
70874
+ type: "progress",
70875
+ message: `${step.name} started`,
70876
+ metadata: {
70877
+ orchestrationEventType: "step-started",
70878
+ step
70879
+ }
70880
+ });
70881
+ }
70882
+ stepCompleted(runId, stepId, output) {
70883
+ this.store.addEvent(runId, {
70884
+ type: "progress",
70885
+ message: `${stepId} completed`,
70886
+ metadata: {
70887
+ orchestrationEventType: "step-completed",
70888
+ stepId,
70889
+ output
70890
+ }
70891
+ });
70892
+ }
70893
+ stepFailed(runId, stepId, errorMessage, errorMeta) {
70894
+ this.store.addEvent(runId, {
70895
+ type: "error",
70896
+ message: `${stepId} failed: ${errorMessage}`,
70897
+ metadata: {
70898
+ orchestrationEventType: "step-failed",
70899
+ stepId,
70900
+ ...errorMeta ?? {}
70901
+ }
70902
+ });
70903
+ }
70904
+ bumpStep(runId, stepIndex) {
70905
+ const active = this.activeRuns.get(runId);
70906
+ if (!active) return;
70907
+ active.stepIndex = stepIndex;
70908
+ this.store.updateRun(runId, {
70909
+ metadata: {
70910
+ orchestration: {
70911
+ phase: active.phase,
70912
+ stepIndex: active.stepIndex,
70913
+ totalSteps: active.totalSteps
70914
+ }
70915
+ }
70916
+ });
70917
+ }
70918
+ createStep(runId, type, phase, name, input) {
70919
+ const active = this.activeRuns.get(runId);
70920
+ const order = (active?.stepIndex ?? 0) + 1;
70921
+ return {
70922
+ id: `${runId}-${phase}-${order}`,
70923
+ type,
70924
+ phase,
70925
+ status: "running",
70926
+ order,
70927
+ name,
70928
+ input,
70929
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
70930
+ };
70931
+ }
70932
+ computeTotalSteps(config) {
70933
+ const providers = Math.max(1, config.providers.length);
70934
+ return 1 + providers + Math.max(1, config.maxSources) + Math.max(1, config.maxSynthesisRounds) + 1;
70935
+ }
70936
+ statusToPhase(status) {
70937
+ if (status === "completed") return "completed";
70938
+ if (status === "failed") return "failed";
70939
+ if (status === "cancelled") return "cancelled";
70940
+ return "planning";
70941
+ }
70942
+ throwIfAborted(signal) {
70943
+ if (signal.aborted) {
70944
+ throw signal.reason ?? new Error("Research run aborted");
70945
+ }
70946
+ }
70947
+ };
70948
+ }
70949
+ });
70950
+
70951
+ // ../engine/src/research-step-runner.ts
70952
+ var log7, DEFAULT_QUERY_TIMEOUT_MS, DEFAULT_FETCH_TIMEOUT_MS, DEFAULT_SYNTHESIS_TIMEOUT_MS, ResearchStepTimeoutError, ResearchStepAbortError, ResearchStepProviderError, ResearchStepRunner;
70953
+ var init_research_step_runner = __esm({
70954
+ "../engine/src/research-step-runner.ts"() {
70955
+ "use strict";
70956
+ init_logger2();
70957
+ log7 = createLogger2("research-step-runner");
70958
+ DEFAULT_QUERY_TIMEOUT_MS = 3e4;
70959
+ DEFAULT_FETCH_TIMEOUT_MS = 6e4;
70960
+ DEFAULT_SYNTHESIS_TIMEOUT_MS = 12e4;
70961
+ ResearchStepTimeoutError = class extends Error {
70962
+ constructor(step, timeoutMs) {
70963
+ super(`${step} timed out after ${timeoutMs}ms`);
70964
+ this.name = "ResearchStepTimeoutError";
70965
+ }
70966
+ };
70967
+ ResearchStepAbortError = class extends Error {
70968
+ constructor(step) {
70969
+ super(`${step} aborted`);
70970
+ this.name = "ResearchStepAbortError";
70971
+ }
70972
+ };
70973
+ ResearchStepProviderError = class extends Error {
70974
+ constructor(step, message) {
70975
+ super(`${step} provider error: ${message}`);
70976
+ this.name = "ResearchStepProviderError";
70977
+ }
70978
+ };
70979
+ ResearchStepRunner = class {
70980
+ providers;
70981
+ synthesisRunner;
70982
+ constructor(options = {}) {
70983
+ this.providers = new Map((options.providers ?? []).map((provider) => [provider.type, provider]));
70984
+ this.synthesisRunner = options.synthesisRunner;
70985
+ }
70986
+ async runSourceQuery(query, providerType, config = {}, signal) {
70987
+ const provider = this.providers.get(providerType);
70988
+ if (!provider || !provider.isConfigured()) {
70989
+ return this.unconfigured(`provider ${providerType} is not configured`);
70990
+ }
70991
+ try {
70992
+ const data = await this.withTimeout(
70993
+ `source-query:${providerType}`,
70994
+ provider.search(query, config, signal),
70995
+ config.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
70996
+ signal
70997
+ );
70998
+ return { ok: true, data };
70999
+ } catch (error) {
71000
+ return this.classifyError("source-query", error);
71001
+ }
71002
+ }
71003
+ async runContentFetch(url, config = {}, signal) {
71004
+ const provider = this.findFirstConfiguredProvider();
71005
+ if (!provider) {
71006
+ return this.unconfigured("no configured provider available for content fetch");
71007
+ }
71008
+ try {
71009
+ const data = await this.withTimeout(
71010
+ `content-fetch:${provider.type}`,
71011
+ provider.fetchContent(url, config, signal),
71012
+ config.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS,
71013
+ signal
71014
+ );
71015
+ return { ok: true, data };
71016
+ } catch (error) {
71017
+ return this.classifyError("content-fetch", error);
71018
+ }
71019
+ }
71020
+ async runSynthesis(request2, modelSettings = {}, signal) {
71021
+ if (!this.synthesisRunner) {
71022
+ return this.unconfigured("synthesis provider is not configured");
71023
+ }
71024
+ try {
71025
+ const timeoutMs = modelSettings.timeoutMs ?? DEFAULT_SYNTHESIS_TIMEOUT_MS;
71026
+ const data = await this.withTimeout(
71027
+ "synthesis",
71028
+ this.synthesisRunner(request2, modelSettings, signal),
71029
+ timeoutMs,
71030
+ signal
71031
+ );
71032
+ return { ok: true, data };
71033
+ } catch (error) {
71034
+ return this.classifyError("synthesis", error);
71035
+ }
71036
+ }
71037
+ findFirstConfiguredProvider() {
71038
+ for (const provider of this.providers.values()) {
71039
+ if (provider.isConfigured()) return provider;
71040
+ }
71041
+ return void 0;
71042
+ }
71043
+ classifyError(step, error) {
71044
+ if (error instanceof ResearchStepTimeoutError) {
71045
+ return { ok: false, error: { code: "timeout", message: error.message, retryable: true } };
71046
+ }
71047
+ if (error instanceof ResearchStepAbortError) {
71048
+ return { ok: false, error: { code: "aborted", message: error.message, retryable: false } };
71049
+ }
71050
+ const { message, detail } = formatError(error);
71051
+ log7.warn(`${step} failed`, detail);
71052
+ return {
71053
+ ok: false,
71054
+ error: {
71055
+ code: "provider_error",
71056
+ message,
71057
+ retryable: true
71058
+ }
71059
+ };
71060
+ }
71061
+ unconfigured(message) {
71062
+ return {
71063
+ ok: false,
71064
+ error: {
71065
+ code: "provider_not_configured",
71066
+ message,
71067
+ retryable: false
71068
+ }
71069
+ };
71070
+ }
71071
+ async withTimeout(step, promise, timeoutMs, signal) {
71072
+ if (signal?.aborted) {
71073
+ throw new ResearchStepAbortError(step);
71074
+ }
71075
+ let timeoutId;
71076
+ let abortListener;
71077
+ const abortPromise = new Promise((_, reject) => {
71078
+ if (!signal) return;
71079
+ abortListener = () => reject(new ResearchStepAbortError(step));
71080
+ signal.addEventListener("abort", abortListener, { once: true });
71081
+ });
71082
+ const timeoutPromise = new Promise((_, reject) => {
71083
+ timeoutId = setTimeout(() => reject(new ResearchStepTimeoutError(step, timeoutMs)), timeoutMs);
71084
+ });
71085
+ try {
71086
+ return await Promise.race([promise, timeoutPromise, abortPromise]);
71087
+ } finally {
71088
+ if (timeoutId) clearTimeout(timeoutId);
71089
+ if (signal && abortListener) {
71090
+ signal.removeEventListener("abort", abortListener);
71091
+ }
71092
+ }
71093
+ }
71094
+ };
71095
+ }
71096
+ });
71097
+
71098
+ // ../engine/src/research/types.ts
71099
+ var ResearchProviderError;
71100
+ var init_types2 = __esm({
71101
+ "../engine/src/research/types.ts"() {
71102
+ "use strict";
71103
+ ResearchProviderError = class extends Error {
71104
+ providerType;
71105
+ code;
71106
+ retryable;
71107
+ constructor(options) {
71108
+ super(options.message, options.cause ? { cause: options.cause } : void 0);
71109
+ this.name = "ResearchProviderError";
71110
+ this.providerType = options.providerType;
71111
+ this.code = options.code;
71112
+ this.retryable = options.retryable ?? false;
71113
+ }
71114
+ };
71115
+ }
71116
+ });
71117
+
71118
+ // ../engine/src/research/providers/github-provider.ts
71119
+ function isGhError(value) {
71120
+ return value instanceof Error && "stderr" in value && "stdout" in value && "code" in value;
71121
+ }
71122
+ function parseGitHubUrl(url) {
71123
+ let parsedUrl;
71124
+ try {
71125
+ parsedUrl = new URL(url);
71126
+ } catch {
71127
+ return null;
71128
+ }
71129
+ if (!/(^|\.)github\.com$/i.test(parsedUrl.hostname)) {
71130
+ return null;
71131
+ }
71132
+ const segments = parsedUrl.pathname.split("/").filter(Boolean);
71133
+ if (segments.length < 2) return null;
71134
+ const [owner, repo, section, ...rest] = segments;
71135
+ if (!section) {
71136
+ return { owner, repo, kind: "repo" };
71137
+ }
71138
+ if (section === "issues" && rest[0]) {
71139
+ return { owner, repo, kind: "issue", number: rest[0] };
71140
+ }
71141
+ if (section === "pull" && rest[0]) {
71142
+ return { owner, repo, kind: "pr", number: rest[0] };
71143
+ }
71144
+ if (section === "blob" && rest.length >= 2) {
71145
+ return { owner, repo, kind: "file", ref: rest[0], filePath: rest.slice(1).join("/") };
71146
+ }
71147
+ return null;
71148
+ }
71149
+ var log8, DEFAULT_TIMEOUT_MS, GitHubProvider;
71150
+ var init_github_provider = __esm({
71151
+ "../engine/src/research/providers/github-provider.ts"() {
71152
+ "use strict";
71153
+ init_src();
71154
+ init_logger2();
71155
+ init_types2();
71156
+ log8 = createLogger2("research:github");
71157
+ DEFAULT_TIMEOUT_MS = 3e4;
71158
+ GitHubProvider = class {
71159
+ type = "github";
71160
+ async search(query, config = {}, signal) {
71161
+ const timeoutMs = Number(config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
71162
+ const maxResults = Number(config.maxResults ?? 10);
71163
+ const searchType = config.metadata?.searchType ?? "both";
71164
+ const sources = [];
71165
+ try {
71166
+ if (searchType === "repos" || searchType === "both") {
71167
+ const repos = await runGhJsonAsync(
71168
+ [
71169
+ "search",
71170
+ "repos",
71171
+ query,
71172
+ "--json",
71173
+ "fullName,description,htmlUrl,stargazersCount,language,updatedAt",
71174
+ "--limit",
71175
+ String(maxResults)
71176
+ ],
71177
+ { signal, timeoutMs }
71178
+ );
71179
+ sources.push(
71180
+ ...repos.slice(0, maxResults).map((repo, idx) => ({
71181
+ id: `github-repo-${idx}-${repo.htmlUrl}`,
71182
+ type: "github",
71183
+ reference: repo.htmlUrl,
71184
+ title: repo.fullName,
71185
+ excerpt: repo.description ?? "",
71186
+ status: "completed",
71187
+ metadata: {
71188
+ resultType: "repo",
71189
+ stars: repo.stargazersCount ?? 0,
71190
+ language: repo.language,
71191
+ updatedAt: repo.updatedAt,
71192
+ rank: idx + 1
71193
+ }
71194
+ }))
71195
+ );
71196
+ }
71197
+ if (searchType === "issues" || searchType === "both") {
71198
+ const issues = await runGhJsonAsync(
71199
+ [
71200
+ "search",
71201
+ "issues",
71202
+ query,
71203
+ "--json",
71204
+ "title,body,htmlUrl,state,labels",
71205
+ "--limit",
71206
+ String(maxResults)
71207
+ ],
71208
+ { signal, timeoutMs }
71209
+ );
71210
+ sources.push(
71211
+ ...issues.slice(0, maxResults).map((issue, idx) => ({
71212
+ id: `github-issue-${idx}-${issue.htmlUrl}`,
71213
+ type: "github",
71214
+ reference: issue.htmlUrl,
71215
+ title: issue.title,
71216
+ excerpt: issue.body?.slice(0, 280) ?? "",
71217
+ status: "completed",
71218
+ metadata: {
71219
+ resultType: "issue",
71220
+ state: issue.state,
71221
+ labels: (issue.labels ?? []).map((label) => label.name).filter(Boolean),
71222
+ rank: idx + 1
71223
+ }
71224
+ }))
71225
+ );
71226
+ }
71227
+ return sources;
71228
+ } catch (error) {
71229
+ throw this.mapGhError(error);
71230
+ }
71231
+ }
71232
+ async fetchContent(url, config = {}, signal) {
71233
+ const parsed = parseGitHubUrl(url);
71234
+ if (!parsed) {
71235
+ throw new ResearchProviderError({
71236
+ providerType: "github",
71237
+ code: "provider-unavailable",
71238
+ message: "Unsupported GitHub URL"
71239
+ });
71240
+ }
71241
+ const timeoutMs = Number(config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
71242
+ try {
71243
+ if (parsed.kind === "repo") {
71244
+ const readme = await runGhJsonAsync(
71245
+ ["api", `repos/${parsed.owner}/${parsed.repo}/readme`],
71246
+ { signal, timeoutMs }
71247
+ );
71248
+ if (readme.encoding !== "base64" || !readme.content) {
71249
+ throw new ResearchProviderError({
71250
+ providerType: "github",
71251
+ code: "provider-unavailable",
71252
+ message: "Unsupported README encoding"
71253
+ });
71254
+ }
71255
+ const content2 = Buffer.from(readme.content.replace(/\n/g, ""), "base64").toString("utf-8");
71256
+ return {
71257
+ content: content2,
71258
+ metadata: {
71259
+ url,
71260
+ owner: parsed.owner,
71261
+ repo: parsed.repo,
71262
+ kind: "repo-readme",
71263
+ name: readme.name
71264
+ },
71265
+ mimeType: "text/markdown"
71266
+ };
71267
+ }
71268
+ if (parsed.kind === "issue") {
71269
+ const issue = await runGhAsync(["issue", "view", parsed.number ?? "", "--repo", `${parsed.owner}/${parsed.repo}`, "--comments"], {
71270
+ signal,
71271
+ timeoutMs
71272
+ });
71273
+ return {
71274
+ content: issue,
71275
+ metadata: { url, owner: parsed.owner, repo: parsed.repo, kind: "issue", number: parsed.number },
71276
+ mimeType: "text/plain"
71277
+ };
71278
+ }
71279
+ if (parsed.kind === "pr") {
71280
+ const pr = await runGhAsync(["pr", "view", parsed.number ?? "", "--repo", `${parsed.owner}/${parsed.repo}`, "--comments"], {
71281
+ signal,
71282
+ timeoutMs
71283
+ });
71284
+ return {
71285
+ content: pr,
71286
+ metadata: { url, owner: parsed.owner, repo: parsed.repo, kind: "pr", number: parsed.number },
71287
+ mimeType: "text/plain"
71288
+ };
71289
+ }
71290
+ const apiPath = `repos/${parsed.owner}/${parsed.repo}/contents/${parsed.filePath ?? ""}${parsed.ref ? `?ref=${encodeURIComponent(parsed.ref)}` : ""}`;
71291
+ const file = await runGhJsonAsync(["api", apiPath], {
71292
+ signal,
71293
+ timeoutMs
71294
+ });
71295
+ const content = file.encoding === "base64" && file.content ? Buffer.from(file.content.replace(/\n/g, ""), "base64").toString("utf-8") : file.content ?? "";
71296
+ return {
71297
+ content,
71298
+ metadata: {
71299
+ url,
71300
+ owner: parsed.owner,
71301
+ repo: parsed.repo,
71302
+ kind: "file",
71303
+ path: parsed.filePath,
71304
+ ref: parsed.ref,
71305
+ name: file.name
71306
+ },
71307
+ mimeType: "text/plain"
71308
+ };
71309
+ } catch (error) {
71310
+ throw this.mapGhError(error);
71311
+ }
71312
+ }
71313
+ isConfigured() {
71314
+ return isGhAvailable() && isGhAuthenticated();
71315
+ }
71316
+ mapGhError(error) {
71317
+ if (error instanceof ResearchProviderError) return error;
71318
+ if (isGhError(error)) {
71319
+ const message = `${error.message}${error.stderr ? `: ${error.stderr}` : ""}`;
71320
+ const lowered = message.toLowerCase();
71321
+ if (error.code === "ABORT_ERR" || lowered.includes("aborted")) {
71322
+ return new ResearchProviderError({ providerType: "github", code: "abort", message, cause: error });
71323
+ }
71324
+ if (lowered.includes("timed out")) {
71325
+ return new ResearchProviderError({
71326
+ providerType: "github",
71327
+ code: "timeout",
71328
+ message,
71329
+ retryable: true,
71330
+ cause: error
71331
+ });
71332
+ }
71333
+ if (error.code === 403 || lowered.includes("rate limit")) {
71334
+ return new ResearchProviderError({
71335
+ providerType: "github",
71336
+ code: "rate-limited",
71337
+ message,
71338
+ retryable: true,
71339
+ cause: error
71340
+ });
71341
+ }
71342
+ if (error.code === 401 || lowered.includes("not logged") || lowered.includes("authentication")) {
71343
+ return new ResearchProviderError({
71344
+ providerType: "github",
71345
+ code: "auth-failed",
71346
+ message,
71347
+ cause: error
71348
+ });
71349
+ }
71350
+ if (error.code === 404 || lowered.includes("not found")) {
71351
+ return new ResearchProviderError({
71352
+ providerType: "github",
71353
+ code: "provider-unavailable",
71354
+ message,
71355
+ cause: error
71356
+ });
71357
+ }
71358
+ return new ResearchProviderError({
71359
+ providerType: "github",
71360
+ code: "network-error",
71361
+ message,
71362
+ retryable: true,
71363
+ cause: error
71364
+ });
71365
+ }
71366
+ log8.warn("github provider error", { error });
71367
+ return new ResearchProviderError({
71368
+ providerType: "github",
71369
+ code: "network-error",
71370
+ message: error instanceof Error ? error.message : "GitHub provider failed",
71371
+ retryable: true,
71372
+ cause: error
71373
+ });
71374
+ }
71375
+ };
71376
+ }
71377
+ });
71378
+
71379
+ // ../engine/src/research/providers/llm-synthesis-provider.ts
71380
+ function isLargeModel(modelId) {
71381
+ const id = modelId?.toLowerCase() ?? "";
71382
+ return id.includes("gpt-5") || id.includes("claude-opus") || id.includes("gemini-2.5-pro") || id.includes("sonnet");
71383
+ }
71384
+ function scoreSource(source) {
71385
+ const confidence = typeof source.metadata?.confidence === "number" ? source.metadata.confidence : 0;
71386
+ const hasContent = source.content ? 1 : 0;
71387
+ return confidence * 10 + hasContent;
71388
+ }
71389
+ function buildSynthesisPrompt(request2, sources) {
71390
+ const format = request2.desiredFormat ?? "markdown";
71391
+ const renderedSources = sources.map(
71392
+ (source, index) => `Source [${index + 1}]
71393
+ Title: ${source.title ?? source.reference}
71394
+ Reference: ${source.reference}
71395
+ Excerpt: ${source.excerpt ?? ""}
71396
+ Content: ${source.content ?? ""}`
71397
+ ).join("\n\n");
71398
+ return [
71399
+ "You are a research synthesis assistant.",
71400
+ `Query: ${request2.query}`,
71401
+ `Round: ${request2.round}`,
71402
+ `Desired format: ${format}`,
71403
+ "Analyze the provided sources and produce: summary, key findings, contradictions, confidence, and follow-up queries.",
71404
+ "Cite source references inline using [n] notation where n is source number.",
71405
+ request2.instructions ? `Additional instructions: ${request2.instructions}` : "",
71406
+ "Sources:",
71407
+ renderedSources,
71408
+ 'Return valid JSON: {"summary": string, "findings": [{"statement": string, "citations": string[]}], "confidence": number, "followUps": string[]}'
71409
+ ].filter(Boolean).join("\n\n");
71410
+ }
71411
+ function extractAssistantText(session) {
71412
+ const messages = session.state?.messages;
71413
+ if (!Array.isArray(messages)) return void 0;
71414
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
71415
+ const msg = messages[i];
71416
+ if (msg.role !== "assistant") continue;
71417
+ if (typeof msg.content === "string" && msg.content.trim()) return msg.content;
71418
+ if (Array.isArray(msg.content)) {
71419
+ for (const part of msg.content) {
71420
+ if (typeof part === "object" && part && "text" in part && typeof part.text === "string") {
71421
+ return part.text;
71422
+ }
71423
+ }
71424
+ }
71425
+ }
71426
+ return void 0;
71427
+ }
71428
+ function extractCitations(text, sources) {
71429
+ const matches = text.match(/\[(\d+)\]/g) ?? [];
71430
+ const refs = /* @__PURE__ */ new Set();
71431
+ for (const match of matches) {
71432
+ const idx = Number.parseInt(match.replace(/\D/g, ""), 10) - 1;
71433
+ if (Number.isFinite(idx) && idx >= 0 && idx < sources.length) {
71434
+ refs.add(sources[idx].reference);
71435
+ }
71436
+ }
71437
+ return [...refs];
71438
+ }
71439
+ function extractConfidence(text) {
71440
+ const jsonBlock = text.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1] ?? text;
71441
+ try {
71442
+ const parsed = JSON.parse(jsonBlock);
71443
+ if (typeof parsed.confidence === "number") return parsed.confidence;
71444
+ } catch {
71445
+ const match = text.match(/"confidence"\s*:\s*([0-9]*\.?[0-9]+)/i);
71446
+ if (match) {
71447
+ const parsed = Number.parseFloat(match[1]);
71448
+ if (Number.isFinite(parsed)) return parsed;
71449
+ }
71450
+ }
71451
+ return void 0;
71452
+ }
71453
+ var log9, DEFAULT_TIMEOUT_MS2, LARGE_MODEL_CONTEXT_CHARS, SMALL_MODEL_CONTEXT_CHARS, LLMSynthesisProvider;
71454
+ var init_llm_synthesis_provider = __esm({
71455
+ "../engine/src/research/providers/llm-synthesis-provider.ts"() {
71456
+ "use strict";
71457
+ init_logger2();
71458
+ init_pi();
71459
+ init_types2();
71460
+ log9 = createLogger2("research:llm-synthesis");
71461
+ DEFAULT_TIMEOUT_MS2 = 12e4;
71462
+ LARGE_MODEL_CONTEXT_CHARS = 1e5;
71463
+ SMALL_MODEL_CONTEXT_CHARS = 3e4;
71464
+ LLMSynthesisProvider = class {
71465
+ constructor(options) {
71466
+ this.options = options;
71467
+ }
71468
+ type = "llm-synthesis";
71469
+ isConfigured() {
71470
+ return true;
71471
+ }
71472
+ async search(_query, _options = {}, _signal) {
71473
+ return [];
71474
+ }
71475
+ async fetchContent(_url, _options = {}, _signal) {
71476
+ return { content: "", metadata: {} };
71477
+ }
71478
+ async synthesize(request2, modelSelection, signal) {
71479
+ if (!modelSelection?.provider || !modelSelection?.modelId) {
71480
+ throw new ResearchProviderError({ providerType: "llm-synthesis", code: "provider-unavailable", message: "Synthesis model is not configured" });
71481
+ }
71482
+ const timeoutSignal = AbortSignal.timeout(this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS2);
71483
+ const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
71484
+ try {
71485
+ const cappedSources = this.applySourceBudget(request2.sources, modelSelection);
71486
+ const prompt = buildSynthesisPrompt(request2, cappedSources);
71487
+ const { session } = await createFnAgent2({
71488
+ cwd: this.options.projectRoot,
71489
+ tools: "readonly",
71490
+ systemPrompt: "You synthesize research findings into concise, cited outputs.",
71491
+ defaultProvider: modelSelection.provider,
71492
+ defaultModelId: modelSelection.modelId
71493
+ });
71494
+ try {
71495
+ await Promise.race([
71496
+ promptWithFallback(session, prompt),
71497
+ new Promise((_, reject) => {
71498
+ requestSignal.addEventListener(
71499
+ "abort",
71500
+ () => reject(new ResearchProviderError({
71501
+ providerType: "llm-synthesis",
71502
+ code: signal?.aborted ? "abort" : "timeout",
71503
+ message: signal?.aborted ? "Synthesis aborted" : "Synthesis timed out",
71504
+ retryable: !signal?.aborted
71505
+ })),
71506
+ { once: true }
71507
+ );
71508
+ })
71509
+ ]);
71510
+ if (requestSignal.aborted) {
71511
+ throw new ResearchProviderError({ providerType: "llm-synthesis", code: signal?.aborted ? "abort" : "timeout", message: signal?.aborted ? "Synthesis aborted" : "Synthesis timed out", retryable: !signal?.aborted });
71512
+ }
71513
+ const responseText = extractAssistantText(session);
71514
+ if (!responseText) {
71515
+ throw new ResearchProviderError({ providerType: "llm-synthesis", code: "provider-unavailable", message: "No synthesis response received" });
71516
+ }
71517
+ return {
71518
+ output: responseText,
71519
+ citations: extractCitations(responseText, cappedSources),
71520
+ confidence: extractConfidence(responseText),
71521
+ metadata: {
71522
+ sourceCount: cappedSources.length,
71523
+ truncated: cappedSources.length < request2.sources.length
71524
+ }
71525
+ };
71526
+ } finally {
71527
+ session.dispose();
71528
+ }
71529
+ } catch (error) {
71530
+ if (error instanceof ResearchProviderError) throw error;
71531
+ if (error instanceof DOMException && error.name === "AbortError") {
71532
+ throw new ResearchProviderError({ providerType: "llm-synthesis", code: "abort", message: "Synthesis aborted", cause: error });
71533
+ }
71534
+ log9.warn("llm synthesis failed", { error });
71535
+ throw new ResearchProviderError({
71536
+ providerType: "llm-synthesis",
71537
+ code: "provider-unavailable",
71538
+ message: error instanceof Error ? error.message : "Synthesis failed",
71539
+ retryable: true,
71540
+ cause: error
71541
+ });
71542
+ }
71543
+ }
71544
+ applySourceBudget(sources, modelSelection) {
71545
+ const contextChars = isLargeModel(modelSelection.modelId) ? LARGE_MODEL_CONTEXT_CHARS : SMALL_MODEL_CONTEXT_CHARS;
71546
+ const budget = Math.floor(contextChars * 0.8);
71547
+ const ordered = [...sources].sort((a, b) => scoreSource(b) - scoreSource(a));
71548
+ let total = 0;
71549
+ const kept = [];
71550
+ for (const source of ordered) {
71551
+ const chunk = `${source.title ?? ""}
71552
+ ${source.excerpt ?? ""}
71553
+ ${source.content ?? ""}`;
71554
+ if (total + chunk.length > budget) continue;
71555
+ total += chunk.length;
71556
+ kept.push(source);
71557
+ }
71558
+ return kept.length > 0 ? kept : ordered.slice(0, 1);
71559
+ }
71560
+ };
71561
+ }
71562
+ });
71563
+
71564
+ // ../engine/src/research/providers/local-docs-provider.ts
71565
+ import { promises as fs } from "node:fs";
71566
+ import { extname as extname2, join as join35, relative as relative7, resolve as resolve15 } from "node:path";
71567
+ function buildExcerpt(content, terms) {
71568
+ const lower = content.toLowerCase();
71569
+ const first = terms.find((term) => lower.includes(term));
71570
+ if (!first) return content.slice(0, 220).replace(/\s+/g, " ").trim();
71571
+ const idx = lower.indexOf(first);
71572
+ const start = Math.max(0, idx - 80);
71573
+ const end = Math.min(content.length, idx + 140);
71574
+ return content.slice(start, end).replace(/\s+/g, " ").trim();
71575
+ }
71576
+ function matchesGitignore(relPath, patterns) {
71577
+ return patterns.some((pattern) => {
71578
+ if (pattern.endsWith("/")) return relPath.startsWith(pattern.slice(0, -1));
71579
+ if (pattern.includes("*")) {
71580
+ const regex = new RegExp(`^${pattern.split("*").map(escapeRegex2).join(".*")}$`);
71581
+ return regex.test(relPath);
71582
+ }
71583
+ return relPath === pattern || relPath.startsWith(`${pattern}/`);
71584
+ });
71585
+ }
71586
+ function escapeRegex2(value) {
71587
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
71588
+ }
71589
+ var log10, DEFAULT_MAX_RESULTS, DEFAULT_TIMEOUT_MS3, MAX_FILE_SIZE_BYTES, BINARY_SNIFF_BYTES, LocalDocsProvider;
71590
+ var init_local_docs_provider = __esm({
71591
+ "../engine/src/research/providers/local-docs-provider.ts"() {
71592
+ "use strict";
71593
+ init_logger2();
71594
+ init_types2();
71595
+ log10 = createLogger2("research:local-docs");
71596
+ DEFAULT_MAX_RESULTS = 10;
71597
+ DEFAULT_TIMEOUT_MS3 = 3e4;
71598
+ MAX_FILE_SIZE_BYTES = 1024 * 1024;
71599
+ BINARY_SNIFF_BYTES = 8 * 1024;
71600
+ LocalDocsProvider = class {
71601
+ constructor(options) {
71602
+ this.options = options;
71603
+ this.projectRoot = resolve15(options.projectRoot);
71604
+ this.scanPaths = options.scanPaths ?? ["docs", "README.md", "AGENTS.md", ".fusion/memory"];
71605
+ }
71606
+ type = "local-docs";
71607
+ projectRoot;
71608
+ scanPaths;
71609
+ isConfigured() {
71610
+ return true;
71611
+ }
71612
+ async search(query, config = {}, signal) {
71613
+ const timeoutMs = Number(config.timeoutMs ?? this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS3);
71614
+ const maxResults = Number(config.maxResults ?? this.options.maxResults ?? DEFAULT_MAX_RESULTS);
71615
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
71616
+ const files = await this.withTimeout(this.collectCandidateFiles(signal), timeoutMs, signal);
71617
+ const results = [];
71618
+ for (const file of files) {
71619
+ this.throwIfAborted(signal);
71620
+ const content = await this.safeReadText(file, signal);
71621
+ if (!content) continue;
71622
+ const lower = content.toLowerCase();
71623
+ let score = 0;
71624
+ for (const term of terms) {
71625
+ const matches = lower.match(new RegExp(escapeRegex2(term), "g"));
71626
+ score += matches?.length ?? 0;
71627
+ }
71628
+ if (score <= 0) continue;
71629
+ const relPath = relative7(this.projectRoot, file);
71630
+ results.push({
71631
+ score,
71632
+ source: {
71633
+ id: `local-docs-${relPath}`,
71634
+ type: "local",
71635
+ reference: relPath,
71636
+ title: relPath,
71637
+ excerpt: buildExcerpt(content, terms),
71638
+ status: "completed",
71639
+ metadata: { score, path: relPath }
71640
+ }
71641
+ });
71642
+ }
71643
+ return results.sort((a, b) => b.score - a.score).slice(0, maxResults).map((item) => item.source);
71644
+ }
71645
+ async fetchContent(filePath, config = {}, signal) {
71646
+ const timeoutMs = Number(config.timeoutMs ?? this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS3);
71647
+ const resolvedPath = resolve15(this.projectRoot, filePath);
71648
+ if (!resolvedPath.startsWith(this.projectRoot)) {
71649
+ throw new ResearchProviderError({
71650
+ providerType: "local-docs",
71651
+ code: "provider-unavailable",
71652
+ message: "Path traversal is not allowed"
71653
+ });
71654
+ }
71655
+ const stat8 = await this.withTimeout(fs.stat(resolvedPath), timeoutMs, signal);
71656
+ if (!stat8.isFile()) {
71657
+ throw new ResearchProviderError({ providerType: "local-docs", code: "provider-unavailable", message: "Path is not a file" });
71658
+ }
71659
+ const content = await this.withTimeout(fs.readFile(resolvedPath), timeoutMs, signal);
71660
+ const sniff = content.subarray(0, BINARY_SNIFF_BYTES);
71661
+ if (sniff.includes(0)) {
71662
+ throw new ResearchProviderError({ providerType: "local-docs", code: "provider-unavailable", message: "Binary file is not supported" });
71663
+ }
71664
+ const text = content.toString("utf-8");
71665
+ return {
71666
+ content: text.length > MAX_FILE_SIZE_BYTES ? text.slice(0, MAX_FILE_SIZE_BYTES) : text,
71667
+ metadata: {
71668
+ path: relative7(this.projectRoot, resolvedPath),
71669
+ size: stat8.size,
71670
+ modifiedAt: stat8.mtime.toISOString(),
71671
+ extension: extname2(resolvedPath)
71672
+ },
71673
+ mimeType: "text/plain"
71674
+ };
71675
+ }
71676
+ async collectCandidateFiles(signal) {
71677
+ const ignorePatterns = await this.readGitignore();
71678
+ const files = [];
71679
+ for (const pathEntry of this.scanPaths) {
71680
+ const target = resolve15(this.projectRoot, pathEntry);
71681
+ if (!target.startsWith(this.projectRoot)) continue;
71682
+ try {
71683
+ const stat8 = await fs.stat(target);
71684
+ if (stat8.isDirectory()) {
71685
+ await this.walk(target, files, ignorePatterns, signal);
71686
+ } else if (stat8.isFile()) {
71687
+ files.push(target);
71688
+ }
71689
+ } catch {
71690
+ }
71691
+ }
71692
+ const rootEntries = await fs.readdir(this.projectRoot, { withFileTypes: true });
71693
+ for (const entry of rootEntries) {
71694
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
71695
+ files.push(join35(this.projectRoot, entry.name));
71696
+ }
71697
+ }
71698
+ return [...new Set(files)];
71699
+ }
71700
+ async walk(dir, out, ignorePatterns, signal) {
71701
+ this.throwIfAborted(signal);
71702
+ const entries = await fs.readdir(dir, { withFileTypes: true });
71703
+ for (const entry of entries) {
71704
+ this.throwIfAborted(signal);
71705
+ const fullPath = join35(dir, entry.name);
71706
+ const relPath = relative7(this.projectRoot, fullPath).replace(/\\/g, "/");
71707
+ if (matchesGitignore(relPath, ignorePatterns)) continue;
71708
+ if (entry.isDirectory()) {
71709
+ await this.walk(fullPath, out, ignorePatterns, signal);
71710
+ } else if (entry.isFile()) {
71711
+ out.push(fullPath);
71712
+ }
71713
+ }
71714
+ }
71715
+ async safeReadText(filePath, signal) {
71716
+ try {
71717
+ const stat8 = await fs.stat(filePath);
71718
+ if (stat8.size > MAX_FILE_SIZE_BYTES) return void 0;
71719
+ const content = await fs.readFile(filePath);
71720
+ if (content.subarray(0, BINARY_SNIFF_BYTES).includes(0)) return void 0;
71721
+ this.throwIfAborted(signal);
71722
+ return content.toString("utf-8");
71723
+ } catch (error) {
71724
+ log10.warn("failed to read local docs file", { filePath, error });
71725
+ return void 0;
71726
+ }
71727
+ }
71728
+ async readGitignore() {
71729
+ try {
71730
+ const content = await fs.readFile(join35(this.projectRoot, ".gitignore"), "utf-8");
71731
+ return content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
71732
+ } catch {
71733
+ return [];
71734
+ }
71735
+ }
71736
+ throwIfAborted(signal) {
71737
+ if (signal?.aborted) {
71738
+ throw new ResearchProviderError({ providerType: "local-docs", code: "abort", message: "Local docs scan aborted" });
71739
+ }
71740
+ }
71741
+ async withTimeout(promise, timeoutMs, signal) {
71742
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
71743
+ const combined = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
71744
+ return await Promise.race([
71745
+ promise,
71746
+ new Promise((_, reject) => {
71747
+ combined.addEventListener(
71748
+ "abort",
71749
+ () => {
71750
+ reject(
71751
+ new ResearchProviderError({
71752
+ providerType: "local-docs",
71753
+ code: signal?.aborted ? "abort" : "timeout",
71754
+ message: signal?.aborted ? "Local docs operation aborted" : `Local docs operation timed out after ${timeoutMs}ms`,
71755
+ retryable: !signal?.aborted
71756
+ })
71757
+ );
71758
+ },
71759
+ { once: true }
71760
+ );
71761
+ })
71762
+ ]);
71763
+ }
71764
+ };
71765
+ }
71766
+ });
71767
+
71768
+ // ../engine/src/research/providers/page-fetch-provider.ts
71769
+ function extractHtml(html) {
71770
+ const title = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1]?.trim();
71771
+ const description = html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["'][^>]*>/i)?.[1]?.trim();
71772
+ const stripped = html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<(nav|footer|header)[\s\S]*?<\/\1>/gi, " ");
71773
+ const main = stripped.match(/<(main|article)[^>]*>([\s\S]*?)<\/\1>/i)?.[2] ?? stripped.match(/<body[^>]*>([\s\S]*?)<\/body>/i)?.[1] ?? stripped;
71774
+ const text = main.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
71775
+ return { title, description, content: text };
71776
+ }
71777
+ function truncate(value) {
71778
+ return value.length > MAX_CONTENT_CHARS ? value.slice(0, MAX_CONTENT_CHARS) : value;
71779
+ }
71780
+ function looksLikeJson(value) {
71781
+ const trimmed = value.trim();
71782
+ return trimmed.startsWith("{") || trimmed.startsWith("[");
71783
+ }
71784
+ var log11, DEFAULT_TIMEOUT_MS4, DEFAULT_USER_AGENT, MAX_CONTENT_CHARS, PageFetchProvider;
71785
+ var init_page_fetch_provider = __esm({
71786
+ "../engine/src/research/providers/page-fetch-provider.ts"() {
71787
+ "use strict";
71788
+ init_logger2();
71789
+ init_types2();
71790
+ log11 = createLogger2("research:page-fetch");
71791
+ DEFAULT_TIMEOUT_MS4 = 3e4;
71792
+ DEFAULT_USER_AGENT = "FusionResearchBot/1.0";
71793
+ MAX_CONTENT_CHARS = 500 * 1024;
71794
+ PageFetchProvider = class {
71795
+ constructor(options = {}) {
71796
+ this.options = options;
71797
+ }
71798
+ type = "page-fetch";
71799
+ isConfigured() {
71800
+ return true;
71801
+ }
71802
+ async search(_query, _config = {}, _signal) {
71803
+ return [];
71804
+ }
71805
+ async fetchContent(url, config = {}, signal) {
71806
+ const timeoutMs = Number(config.timeoutMs ?? this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS4);
71807
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
71808
+ const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
71809
+ try {
71810
+ const response = await fetch(url, {
71811
+ method: "GET",
71812
+ redirect: "follow",
71813
+ headers: {
71814
+ "User-Agent": config.metadata?.userAgent ?? this.options.userAgent ?? DEFAULT_USER_AGENT
71815
+ },
71816
+ signal: requestSignal
71817
+ });
71818
+ if (!response.ok) {
71819
+ throw new ResearchProviderError({
71820
+ providerType: "page-fetch",
71821
+ code: response.status >= 500 ? "provider-unavailable" : "network-error",
71822
+ message: `fetch failed with status ${response.status}`,
71823
+ retryable: response.status >= 500
71824
+ });
71825
+ }
71826
+ const contentType = response.headers.get("content-type") ?? "application/octet-stream";
71827
+ const mimeType = contentType.split(";")[0].trim().toLowerCase();
71828
+ const raw = await response.text();
71829
+ const metadata = {
71830
+ url,
71831
+ contentType,
71832
+ contentLength: raw.length
71833
+ };
71834
+ if (mimeType.includes("text/html")) {
71835
+ const extracted = extractHtml(raw);
71836
+ metadata.title = extracted.title;
71837
+ metadata.description = extracted.description;
71838
+ metadata.contentLength = extracted.content.length;
71839
+ return { content: truncate(extracted.content), metadata, mimeType };
71840
+ }
71841
+ if (mimeType.includes("application/json") || looksLikeJson(raw)) {
71842
+ const pretty = JSON.stringify(JSON.parse(raw), null, 2);
71843
+ return { content: truncate(pretty), metadata, mimeType };
71844
+ }
71845
+ if (mimeType.includes("text/") || mimeType.includes("markdown")) {
71846
+ return { content: truncate(raw), metadata, mimeType };
71847
+ }
71848
+ throw new ResearchProviderError({
71849
+ providerType: "page-fetch",
71850
+ code: "provider-unavailable",
71851
+ message: `unsupported mime type: ${mimeType}`
71852
+ });
71853
+ } catch (error) {
71854
+ if (error instanceof ResearchProviderError) throw error;
71855
+ if (error instanceof DOMException && error.name === "AbortError") {
71856
+ throw new ResearchProviderError({ providerType: "page-fetch", code: "abort", message: "Fetch aborted", cause: error });
71857
+ }
71858
+ if (error instanceof Error && error.name === "TimeoutError") {
71859
+ throw new ResearchProviderError({ providerType: "page-fetch", code: "timeout", message: error.message, retryable: true, cause: error });
71860
+ }
71861
+ log11.warn("page fetch failed", { error });
71862
+ throw new ResearchProviderError({
71863
+ providerType: "page-fetch",
71864
+ code: "network-error",
71865
+ message: error instanceof Error ? error.message : "fetch failed",
71866
+ retryable: true,
71867
+ cause: error
71868
+ });
71869
+ }
71870
+ }
71871
+ };
71872
+ }
71873
+ });
71874
+
71875
+ // ../engine/src/research/providers/web-search-provider.ts
71876
+ async function sleep3(ms, signal) {
71877
+ await new Promise((resolve19, reject) => {
71878
+ const timer = setTimeout(resolve19, ms);
71879
+ const onAbort = () => {
71880
+ clearTimeout(timer);
71881
+ reject(new ResearchProviderError({ providerType: "web-search", code: "abort", message: "Search aborted" }));
71882
+ };
71883
+ signal.addEventListener("abort", onAbort, { once: true });
71884
+ });
71885
+ }
71886
+ var log12, DEFAULT_MAX_RESULTS2, DEFAULT_TIMEOUT_MS5, RETRY_BASE_DELAY_MS2, RETRY_MAX_DELAY_MS, RETRY_MAX_ATTEMPTS, WebSearchProvider;
71887
+ var init_web_search_provider = __esm({
71888
+ "../engine/src/research/providers/web-search-provider.ts"() {
71889
+ "use strict";
71890
+ init_logger2();
71891
+ init_types2();
71892
+ log12 = createLogger2("research:web-search");
71893
+ DEFAULT_MAX_RESULTS2 = 10;
71894
+ DEFAULT_TIMEOUT_MS5 = 3e4;
71895
+ RETRY_BASE_DELAY_MS2 = 1e3;
71896
+ RETRY_MAX_DELAY_MS = 1e4;
71897
+ RETRY_MAX_ATTEMPTS = 3;
71898
+ WebSearchProvider = class {
71899
+ constructor(options = {}) {
71900
+ this.options = options;
71901
+ }
71902
+ type = "web-search";
71903
+ isConfigured() {
71904
+ const backend = this.options.backend ?? "none";
71905
+ if (backend === "none") return false;
71906
+ if (backend === "searxng") return Boolean(this.options.searxngUrl);
71907
+ if (backend === "brave") return Boolean(this.options.braveApiKey);
71908
+ if (backend === "google") return Boolean(this.options.googleApiKey && this.options.googleCx);
71909
+ if (backend === "tavily") return Boolean(this.options.tavilyApiKey);
71910
+ return false;
71911
+ }
71912
+ async search(query, config = {}, signal) {
71913
+ const backend = this.options.backend ?? "none";
71914
+ if (!this.isConfigured()) return [];
71915
+ const maxResults = Number(config.maxResults ?? this.options.maxResults ?? DEFAULT_MAX_RESULTS2);
71916
+ const timeoutMs = Number(config.timeoutMs ?? this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS5);
71917
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
71918
+ const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
71919
+ const results = await this.withHttpRetry(async () => {
71920
+ if (backend === "searxng") return this.searchSearxng(query, maxResults, requestSignal);
71921
+ if (backend === "brave") return this.searchBrave(query, maxResults, requestSignal);
71922
+ if (backend === "google") return this.searchGoogle(query, maxResults, requestSignal);
71923
+ if (backend === "tavily") return this.searchTavily(query, maxResults, requestSignal);
71924
+ return [];
71925
+ }, requestSignal, backend);
71926
+ return results.slice(0, maxResults).map((result, index) => ({
71927
+ id: `${backend}-${index}-${result.url}`,
71928
+ type: "web",
71929
+ reference: result.url,
71930
+ title: result.title,
71931
+ excerpt: result.snippet,
71932
+ status: "completed",
71933
+ metadata: {
71934
+ backend,
71935
+ rank: index + 1
71936
+ }
71937
+ }));
71938
+ }
71939
+ async fetchContent(_url, _options = {}, _signal) {
71940
+ return { content: "", metadata: {} };
71941
+ }
71942
+ async searchSearxng(query, maxResults, signal) {
71943
+ const base = this.options.searxngUrl?.replace(/\/$/, "") ?? "";
71944
+ const url = new URL(`${base}/search`);
71945
+ url.searchParams.set("q", query);
71946
+ url.searchParams.set("format", "json");
71947
+ const response = await this.requestJson(
71948
+ url.toString(),
71949
+ {
71950
+ method: "GET",
71951
+ signal
71952
+ },
71953
+ "searxng"
71954
+ );
71955
+ return (response.results ?? []).slice(0, maxResults).map((item) => ({
71956
+ url: item.url,
71957
+ title: item.title ?? item.url,
71958
+ snippet: item.content ?? ""
71959
+ }));
71960
+ }
71961
+ async searchBrave(query, maxResults, signal) {
71962
+ const url = new URL("https://api.search.brave.com/res/v1/web/search");
71963
+ url.searchParams.set("q", query);
71964
+ url.searchParams.set("count", String(maxResults));
71965
+ const response = await this.requestJson(
71966
+ url.toString(),
71967
+ {
71968
+ method: "GET",
71969
+ headers: {
71970
+ "X-Subscription-Token": this.options.braveApiKey ?? "",
71971
+ "User-Agent": this.options.userAgent ?? "FusionResearchBot/1.0"
71972
+ },
71973
+ signal
71974
+ },
71975
+ "brave"
71976
+ );
71977
+ return (response.web?.results ?? []).slice(0, maxResults).map((item) => ({
71978
+ url: item.url,
71979
+ title: item.title ?? item.url,
71980
+ snippet: item.description ?? ""
71981
+ }));
71982
+ }
71983
+ async searchGoogle(query, maxResults, signal) {
71984
+ const url = new URL("https://www.googleapis.com/customsearch/v1");
71985
+ url.searchParams.set("q", query);
71986
+ url.searchParams.set("num", String(maxResults));
71987
+ url.searchParams.set("key", this.options.googleApiKey ?? "");
71988
+ url.searchParams.set("cx", this.options.googleCx ?? "");
71989
+ const response = await this.requestJson(
71990
+ url.toString(),
71991
+ {
71992
+ method: "GET",
71993
+ signal
71994
+ },
71995
+ "google"
71996
+ );
71997
+ return (response.items ?? []).slice(0, maxResults).map((item) => ({
71998
+ url: item.link,
71999
+ title: item.title ?? item.link,
72000
+ snippet: item.snippet ?? ""
72001
+ }));
72002
+ }
72003
+ async searchTavily(query, maxResults, signal) {
72004
+ const response = await this.requestJson(
72005
+ "https://api.tavily.com/search",
72006
+ {
72007
+ method: "POST",
72008
+ headers: {
72009
+ "Content-Type": "application/json",
72010
+ "User-Agent": this.options.userAgent ?? "FusionResearchBot/1.0"
72011
+ },
72012
+ body: JSON.stringify({
72013
+ api_key: this.options.tavilyApiKey,
72014
+ query,
72015
+ max_results: maxResults
72016
+ }),
72017
+ signal
72018
+ },
72019
+ "tavily"
72020
+ );
72021
+ return (response.results ?? []).slice(0, maxResults).map((item) => ({
72022
+ url: item.url,
72023
+ title: item.title ?? item.url,
72024
+ snippet: item.content ?? ""
72025
+ }));
72026
+ }
72027
+ async withHttpRetry(operation, signal, backend) {
72028
+ let attempt = 0;
72029
+ let lastError;
72030
+ while (attempt < RETRY_MAX_ATTEMPTS) {
72031
+ if (signal.aborted) {
72032
+ throw new ResearchProviderError({ providerType: "web-search", code: "abort", message: "Search aborted" });
72033
+ }
72034
+ try {
72035
+ return await operation();
72036
+ } catch (error) {
72037
+ lastError = error;
72038
+ const isRetryableHttp = error instanceof ResearchProviderError && (error.code === "rate-limited" || error.code === "network-error" || error.code === "provider-unavailable") && error.retryable;
72039
+ attempt += 1;
72040
+ if (!isRetryableHttp || attempt >= RETRY_MAX_ATTEMPTS) {
72041
+ break;
72042
+ }
72043
+ const delay2 = Math.min(RETRY_BASE_DELAY_MS2 * 2 ** (attempt - 1), RETRY_MAX_DELAY_MS);
72044
+ const jitter = delay2 * (Math.random() * 0.2 - 0.1);
72045
+ const totalDelay = Math.max(0, delay2 + jitter);
72046
+ log12.warn(`retrying ${backend} search`, { attempt, totalDelay });
72047
+ await sleep3(totalDelay, signal);
72048
+ }
72049
+ }
72050
+ throw lastError;
72051
+ }
72052
+ async requestJson(url, init, backend) {
72053
+ try {
72054
+ const response = await fetch(url, init);
72055
+ if (!response.ok) {
72056
+ const message = `${backend} request failed with status ${response.status}`;
72057
+ if (response.status === 401 || response.status === 403) {
72058
+ throw new ResearchProviderError({ providerType: "web-search", code: "auth-failed", message });
72059
+ }
72060
+ if (response.status === 429) {
72061
+ throw new ResearchProviderError({
72062
+ providerType: "web-search",
72063
+ code: "rate-limited",
72064
+ message,
72065
+ retryable: true
72066
+ });
72067
+ }
72068
+ if (response.status >= 500) {
72069
+ throw new ResearchProviderError({
72070
+ providerType: "web-search",
72071
+ code: "provider-unavailable",
72072
+ message,
72073
+ retryable: true
72074
+ });
72075
+ }
72076
+ throw new ResearchProviderError({ providerType: "web-search", code: "network-error", message });
72077
+ }
72078
+ return await response.json();
72079
+ } catch (error) {
72080
+ if (error instanceof ResearchProviderError) throw error;
72081
+ if (error instanceof DOMException && error.name === "AbortError") {
72082
+ throw new ResearchProviderError({ providerType: "web-search", code: "abort", message: "Search aborted", cause: error });
72083
+ }
72084
+ if (error instanceof Error && error.name === "TimeoutError") {
72085
+ throw new ResearchProviderError({ providerType: "web-search", code: "timeout", message: error.message, retryable: true, cause: error });
72086
+ }
72087
+ throw new ResearchProviderError({
72088
+ providerType: "web-search",
72089
+ code: "network-error",
72090
+ message: error instanceof Error ? error.message : "Unexpected network error",
72091
+ retryable: true,
72092
+ cause: error
72093
+ });
72094
+ }
72095
+ }
72096
+ };
72097
+ }
72098
+ });
72099
+
72100
+ // ../engine/src/research/provider-registry.ts
72101
+ var log13, ResearchProviderRegistry, DisabledProvider;
72102
+ var init_provider_registry = __esm({
72103
+ "../engine/src/research/provider-registry.ts"() {
72104
+ "use strict";
72105
+ init_logger2();
72106
+ init_github_provider();
72107
+ init_llm_synthesis_provider();
72108
+ init_local_docs_provider();
72109
+ init_page_fetch_provider();
72110
+ init_web_search_provider();
72111
+ log13 = createLogger2("research:provider-registry");
72112
+ ResearchProviderRegistry = class {
72113
+ constructor(settings, projectRoot) {
72114
+ this.settings = settings;
72115
+ this.projectRoot = projectRoot;
72116
+ this.instantiateProviders();
72117
+ }
72118
+ providers = /* @__PURE__ */ new Map();
72119
+ getProvider(type) {
72120
+ return this.providers.get(type);
72121
+ }
72122
+ getAvailableProviders() {
72123
+ return [...this.providers.entries()].filter(([, provider]) => provider.isConfigured()).map(([type]) => type);
72124
+ }
72125
+ isProviderAvailable(type) {
72126
+ const provider = this.providers.get(type);
72127
+ return Boolean(provider?.isConfigured());
72128
+ }
72129
+ refreshSettings(settings) {
72130
+ this.settings = settings;
72131
+ this.instantiateProviders();
72132
+ }
72133
+ instantiateProviders() {
72134
+ const backend = this.resolveSearchBackend();
72135
+ const maxResults = Number(this.settings.researchMaxSearchResults ?? 10);
72136
+ const fetchTimeoutMs = Number(this.settings.researchFetchTimeoutMs ?? 3e4);
72137
+ const userAgent = this.settings.researchUserAgent ?? "FusionResearchBot/1.0";
72138
+ this.providers = /* @__PURE__ */ new Map([
72139
+ [
72140
+ "web-search",
72141
+ new WebSearchProvider({
72142
+ backend,
72143
+ searxngUrl: this.settings.researchSearxngUrl,
72144
+ braveApiKey: this.settings.researchBraveApiKey,
72145
+ googleApiKey: this.settings.researchGoogleSearchApiKey,
72146
+ googleCx: this.settings.researchGoogleSearchCx,
72147
+ tavilyApiKey: this.settings.researchTavilyApiKey,
72148
+ maxResults,
72149
+ timeoutMs: fetchTimeoutMs,
72150
+ userAgent
72151
+ })
72152
+ ],
72153
+ ["page-fetch", new PageFetchProvider({ timeoutMs: fetchTimeoutMs, userAgent })],
72154
+ ["github", this.settings.researchGitHubEnabled ? new GitHubProvider() : new DisabledProvider("github")],
72155
+ [
72156
+ "local-docs",
72157
+ this.settings.researchLocalDocsEnabled === false ? new DisabledProvider("local-docs") : new LocalDocsProvider({ projectRoot: this.projectRoot, timeoutMs: fetchTimeoutMs, maxResults })
72158
+ ],
72159
+ ["llm-synthesis", new LLMSynthesisProvider({ projectRoot: this.projectRoot })]
72160
+ ]);
72161
+ log13.log("providers refreshed", { available: this.getAvailableProviders(), backend });
72162
+ }
72163
+ resolveSearchBackend() {
72164
+ const explicit = this.settings.researchWebSearchProvider;
72165
+ if (explicit && explicit !== "none") return explicit;
72166
+ if (this.settings.researchSearxngUrl) return "searxng";
72167
+ if (this.settings.researchTavilyApiKey) return "tavily";
72168
+ if (this.settings.researchBraveApiKey) return "brave";
72169
+ if (this.settings.researchGoogleSearchApiKey && this.settings.researchGoogleSearchCx) return "google";
72170
+ return "none";
72171
+ }
72172
+ };
72173
+ DisabledProvider = class {
72174
+ type;
72175
+ constructor(type) {
72176
+ this.type = type;
72177
+ }
72178
+ isConfigured() {
72179
+ return false;
72180
+ }
72181
+ async search() {
72182
+ return [];
72183
+ }
72184
+ async fetchContent() {
72185
+ return { content: "", metadata: {} };
72186
+ }
72187
+ };
72188
+ }
72189
+ });
72190
+
72191
+ // ../engine/src/research/providers/index.ts
72192
+ var init_providers = __esm({
72193
+ "../engine/src/research/providers/index.ts"() {
72194
+ "use strict";
72195
+ init_web_search_provider();
72196
+ init_page_fetch_provider();
72197
+ init_github_provider();
72198
+ init_local_docs_provider();
72199
+ init_llm_synthesis_provider();
72200
+ }
72201
+ });
72202
+
70495
72203
  // ../engine/src/pr-monitor-gh.ts
70496
72204
  function createDefaultPrMonitorGhClient() {
70497
72205
  return {
@@ -71409,13 +73117,14 @@ async function sendNtfyNotification({
71409
73117
  schedulerLog.log(`Failed to send ntfy notification: ${err}`);
71410
73118
  }
71411
73119
  }
71412
- var DEFAULT_NTFY_BASE_URL, DEFAULT_NTFY_EVENTS, NtfyNotifier;
73120
+ var DEFAULT_NTFY_BASE_URL, GRIDLOCK_NOTIFICATION_COOLDOWN_MS, DEFAULT_NTFY_EVENTS, NtfyNotifier;
71413
73121
  var init_notifier = __esm({
71414
73122
  "../engine/src/notifier.ts"() {
71415
73123
  "use strict";
71416
73124
  init_logger2();
71417
73125
  init_notification();
71418
73126
  DEFAULT_NTFY_BASE_URL = "https://ntfy.sh";
73127
+ GRIDLOCK_NOTIFICATION_COOLDOWN_MS = 15 * 60 * 1e3;
71419
73128
  DEFAULT_NTFY_EVENTS = [
71420
73129
  "in-review",
71421
73130
  "merged",
@@ -71446,8 +73155,8 @@ var init_notifier = __esm({
71446
73155
  ntfyBaseUrl;
71447
73156
  defaultNtfyBaseUrl;
71448
73157
  projectId;
71449
- notifiedEvents = /* @__PURE__ */ new Set();
71450
73158
  abortController = null;
73159
+ lastGridlockNotificationAt = null;
71451
73160
  async start() {
71452
73161
  this.abortController = new AbortController();
71453
73162
  const settings = await this.store.getSettings();
@@ -71480,8 +73189,16 @@ var init_notifier = __esm({
71480
73189
  this.ntfyBaseUrl = resolveNtfyBaseUrl(settings.ntfyBaseUrl, this.defaultNtfyBaseUrl);
71481
73190
  }
71482
73191
  notifyGridlock(event) {
73192
+ if (event === null) {
73193
+ this.lastGridlockNotificationAt = null;
73194
+ return;
73195
+ }
71483
73196
  if (!this.config.enabled || !this.config.topic || !this.isEventEnabled("gridlock")) return;
71484
- const blockedTasks = event.blockedTaskIds.sort();
73197
+ const now = Date.now();
73198
+ if (this.lastGridlockNotificationAt !== null && now - this.lastGridlockNotificationAt < GRIDLOCK_NOTIFICATION_COOLDOWN_MS) {
73199
+ return;
73200
+ }
73201
+ const blockedTasks = [...event.blockedTaskIds].sort();
71485
73202
  const reasonSummary = Object.values(event.reasons).reduce((acc, reason) => {
71486
73203
  acc[reason] = (acc[reason] ?? 0) + 1;
71487
73204
  return acc;
@@ -71493,31 +73210,21 @@ var init_notifier = __esm({
71493
73210
  dashboardHost: this.config.dashboardHost,
71494
73211
  projectId: this.projectId
71495
73212
  });
71496
- const dedupKey = `gridlock:${blockedTasks.join(",")}`;
71497
- this.maybeNotifyByKey(
71498
- dedupKey,
71499
- () => sendNtfyNotification({
71500
- ntfyBaseUrl: this.ntfyBaseUrl,
71501
- topic: this.config.topic,
71502
- title: "Pipeline gridlocked",
71503
- message: `${event.blockedTaskCount} todo tasks are blocked (${reasons.join(", ")}). Blocked: ${blockedTasks.join(", ")}. Blocking: ${event.blockingTaskIds.join(", ") || "none"}.`,
71504
- priority: "high",
71505
- clickUrl,
71506
- signal: this.abortController?.signal
71507
- })
71508
- );
73213
+ this.lastGridlockNotificationAt = now;
73214
+ sendNtfyNotification({
73215
+ ntfyBaseUrl: this.ntfyBaseUrl,
73216
+ topic: this.config.topic,
73217
+ title: "Pipeline gridlocked",
73218
+ message: `${event.blockedTaskCount} todo tasks are blocked (${reasons.join(", ")}). Blocked: ${blockedTasks.join(", ")}. Blocking: ${event.blockingTaskIds.join(", ") || "none"}.`,
73219
+ priority: "high",
73220
+ clickUrl,
73221
+ signal: this.abortController?.signal
73222
+ }).catch(() => {
73223
+ });
71509
73224
  }
71510
73225
  isEventEnabled(event) {
71511
73226
  return isNtfyEventEnabled(this.config.events, event);
71512
73227
  }
71513
- maybeNotifyByKey(key, notifyFn) {
71514
- if (this.notifiedEvents.has(key)) {
71515
- return;
71516
- }
71517
- this.notifiedEvents.add(key);
71518
- notifyFn().catch(() => {
71519
- });
71520
- }
71521
73228
  getConfig() {
71522
73229
  return { ...this.config, events: [...this.config.events] };
71523
73230
  }
@@ -71574,7 +73281,7 @@ function truncateOutput(stdout, stderr) {
71574
73281
  }
71575
73282
  return combined;
71576
73283
  }
71577
- var execAsync6, log6, DEFAULT_TIMEOUT_MS, MAX_BUFFER, MAX_OUTPUT_LENGTH, DEFAULT_POLL_INTERVAL_MS, MIN_POLL_INTERVAL_MS, CronRunner, AI_AUTOMATION_SYSTEM_PROMPT;
73284
+ var execAsync6, log14, DEFAULT_TIMEOUT_MS6, MAX_BUFFER, MAX_OUTPUT_LENGTH, DEFAULT_POLL_INTERVAL_MS, MIN_POLL_INTERVAL_MS, CronRunner, AI_AUTOMATION_SYSTEM_PROMPT;
71578
73285
  var init_cron_runner = __esm({
71579
73286
  "../engine/src/cron-runner.ts"() {
71580
73287
  "use strict";
@@ -71583,8 +73290,8 @@ var init_cron_runner = __esm({
71583
73290
  init_shell_utils();
71584
73291
  init_pi();
71585
73292
  execAsync6 = promisify7(exec6);
71586
- log6 = createLogger2("cron-runner");
71587
- DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
73293
+ log14 = createLogger2("cron-runner");
73294
+ DEFAULT_TIMEOUT_MS6 = 5 * 60 * 1e3;
71588
73295
  MAX_BUFFER = 1024 * 1024;
71589
73296
  MAX_OUTPUT_LENGTH = 10 * 1024;
71590
73297
  DEFAULT_POLL_INTERVAL_MS = 60 * 1e3;
@@ -71616,7 +73323,7 @@ var init_cron_runner = __esm({
71616
73323
  start() {
71617
73324
  if (this.running) return;
71618
73325
  this.running = true;
71619
- log6.log(`Started (poll every ${this.pollIntervalMs / 1e3}s, scope: ${this.scope})`);
73326
+ log14.log(`Started (poll every ${this.pollIntervalMs / 1e3}s, scope: ${this.scope})`);
71620
73327
  void this.tick();
71621
73328
  this.pollInterval = setInterval(() => {
71622
73329
  void this.tick();
@@ -71630,7 +73337,7 @@ var init_cron_runner = __esm({
71630
73337
  clearInterval(this.pollInterval);
71631
73338
  this.pollInterval = null;
71632
73339
  }
71633
- log6.log("Stopped");
73340
+ log14.log("Stopped");
71634
73341
  }
71635
73342
  /**
71636
73343
  * Single poll cycle: find due schedules and execute them.
@@ -71654,29 +73361,29 @@ var init_cron_runner = __esm({
71654
73361
  const executedIds = /* @__PURE__ */ new Set();
71655
73362
  for (const schedule of dueSchedules) {
71656
73363
  if (this.inFlight.has(schedule.id)) {
71657
- log6.warn(`Skipping ${schedule.name} (${schedule.id}) \u2014 still running from previous tick`);
73364
+ log14.warn(`Skipping ${schedule.name} (${schedule.id}) \u2014 still running from previous tick`);
71658
73365
  continue;
71659
73366
  }
71660
73367
  if (executedIds.has(schedule.id)) {
71661
- log6.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 already executed from another scope this tick`);
73368
+ log14.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 already executed from another scope this tick`);
71662
73369
  continue;
71663
73370
  }
71664
73371
  executedIds.add(schedule.id);
71665
73372
  const scheduleScope = schedule.scope ?? "project";
71666
73373
  if (scheduleScope !== this.scope && this.scope !== "all") {
71667
- log6.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 belongs to ${scheduleScope} scope, not polling`);
73374
+ log14.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 belongs to ${scheduleScope} scope, not polling`);
71668
73375
  continue;
71669
73376
  }
71670
- log6.log(`Executing ${schedule.name} (${schedule.id}) [scope: ${scheduleScope}]`);
73377
+ log14.log(`Executing ${schedule.name} (${schedule.id}) [scope: ${scheduleScope}]`);
71671
73378
  const currentSettings = await this.store.getSettings();
71672
73379
  if (currentSettings.globalPause || currentSettings.enginePaused) {
71673
- log6.log("Pause detected mid-tick \u2014 stopping schedule execution");
73380
+ log14.log("Pause detected mid-tick \u2014 stopping schedule execution");
71674
73381
  break;
71675
73382
  }
71676
73383
  await this.executeSchedule(schedule);
71677
73384
  }
71678
73385
  } catch (err) {
71679
- log6.error(`Tick error: ${err.message}`);
73386
+ log14.error(`Tick error: ${err.message}`);
71680
73387
  } finally {
71681
73388
  this.ticking = false;
71682
73389
  }
@@ -71706,13 +73413,13 @@ var init_cron_runner = __esm({
71706
73413
  try {
71707
73414
  await this.automationStore.recordRun(schedule.id, result);
71708
73415
  } catch (recordErr) {
71709
- log6.error(`Failed to record run for ${schedule.id}: ${recordErr.message}`);
73416
+ log14.error(`Failed to record run for ${schedule.id}: ${recordErr.message}`);
71710
73417
  }
71711
73418
  if (this.onScheduleRunProcessed) {
71712
73419
  try {
71713
73420
  await this.onScheduleRunProcessed(schedule, result);
71714
73421
  } catch (callbackErr) {
71715
- log6.error(
73422
+ log14.error(
71716
73423
  `Post-run callback failed for ${schedule.name} (${schedule.id}): ${callbackErr.message}`
71717
73424
  );
71718
73425
  }
@@ -71723,16 +73430,16 @@ var init_cron_runner = __esm({
71723
73430
  * Execute a legacy single-command schedule.
71724
73431
  */
71725
73432
  async executeLegacyCommand(schedule, startedAt) {
71726
- log6.log(`Executing ${schedule.name} (${schedule.id}): ${schedule.command}`);
73433
+ log14.log(`Executing ${schedule.name} (${schedule.id}): ${schedule.command}`);
71727
73434
  try {
71728
- const timeoutMs = schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS;
73435
+ const timeoutMs = schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS6;
71729
73436
  const { stdout, stderr } = await execAsync6(schedule.command, {
71730
73437
  timeout: timeoutMs,
71731
73438
  maxBuffer: MAX_BUFFER,
71732
73439
  shell: defaultShell
71733
73440
  });
71734
73441
  const output = truncateOutput(stdout, stderr);
71735
- log6.log(`\u2713 ${schedule.name} completed (${output.length} bytes output)`);
73442
+ log14.log(`\u2713 ${schedule.name} completed (${output.length} bytes output)`);
71736
73443
  return {
71737
73444
  success: true,
71738
73445
  output,
@@ -71743,8 +73450,8 @@ var init_cron_runner = __esm({
71743
73450
  const stdout = err.stdout ?? "";
71744
73451
  const stderr = err.stderr ?? "";
71745
73452
  const output = truncateOutput(stdout, stderr);
71746
- const errorMessage = err.killed ? `Command timed out after ${(schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS) / 1e3}s` : err.message ?? String(err);
71747
- log6.warn(`\u2717 ${schedule.name} failed: ${errorMessage}`);
73453
+ const errorMessage = err.killed ? `Command timed out after ${(schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS6) / 1e3}s` : err.message ?? String(err);
73454
+ log14.warn(`\u2717 ${schedule.name} failed: ${errorMessage}`);
71748
73455
  return {
71749
73456
  success: false,
71750
73457
  output,
@@ -71760,25 +73467,25 @@ var init_cron_runner = __esm({
71760
73467
  */
71761
73468
  async executeSteps(schedule, startedAt) {
71762
73469
  const steps = schedule.steps;
71763
- log6.log(`Executing ${schedule.name} (${schedule.id}): ${steps.length} steps`);
73470
+ log14.log(`Executing ${schedule.name} (${schedule.id}): ${steps.length} steps`);
71764
73471
  const stepResults = [];
71765
73472
  let overallSuccess = true;
71766
73473
  let stoppedEarly = false;
71767
73474
  for (let i = 0; i < steps.length; i++) {
71768
73475
  const step = steps[i];
71769
- log6.log(` Step ${i + 1}/${steps.length}: ${step.name} (${step.type})`);
73476
+ log14.log(` Step ${i + 1}/${steps.length}: ${step.name} (${step.type})`);
71770
73477
  const stepResult = await this.executeStep(schedule, step, i);
71771
73478
  stepResults.push(stepResult);
71772
73479
  if (!stepResult.success) {
71773
73480
  overallSuccess = false;
71774
73481
  if (!step.continueOnFailure) {
71775
- log6.warn(` Step "${step.name}" failed \u2014 stopping execution`);
73482
+ log14.warn(` Step "${step.name}" failed \u2014 stopping execution`);
71776
73483
  stoppedEarly = true;
71777
73484
  break;
71778
73485
  }
71779
- log6.warn(` Step "${step.name}" failed \u2014 continuing (continueOnFailure=true)`);
73486
+ log14.warn(` Step "${step.name}" failed \u2014 continuing (continueOnFailure=true)`);
71780
73487
  } else {
71781
- log6.log(` \u2713 Step "${step.name}" completed`);
73488
+ log14.log(` \u2713 Step "${step.name}" completed`);
71782
73489
  }
71783
73490
  }
71784
73491
  const outputParts = [];
@@ -71791,7 +73498,7 @@ var init_cron_runner = __esm({
71791
73498
  const failedSteps = stepResults.filter((sr) => !sr.success);
71792
73499
  const error = failedSteps.length > 0 ? `${failedSteps.length} step(s) failed: ${failedSteps.map((s) => s.stepName).join(", ")}${stoppedEarly ? " (execution stopped)" : ""}` : void 0;
71793
73500
  const status = overallSuccess ? "\u2713" : "\u2717";
71794
- log6.log(`${status} ${schedule.name}: ${stepResults.length}/${steps.length} steps executed, ${failedSteps.length} failed`);
73501
+ log14.log(`${status} ${schedule.name}: ${stepResults.length}/${steps.length} steps executed, ${failedSteps.length} failed`);
71795
73502
  return {
71796
73503
  success: overallSuccess,
71797
73504
  output,
@@ -71806,7 +73513,7 @@ var init_cron_runner = __esm({
71806
73513
  */
71807
73514
  async executeStep(schedule, step, stepIndex) {
71808
73515
  const stepStartedAt = (/* @__PURE__ */ new Date()).toISOString();
71809
- const timeoutMs = step.timeoutMs ?? schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS;
73516
+ const timeoutMs = step.timeoutMs ?? schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS6;
71810
73517
  if (step.type === "command") {
71811
73518
  return this.executeCommandStep(step, stepIndex, timeoutMs, stepStartedAt);
71812
73519
  } else if (step.type === "ai-prompt") {
@@ -71908,8 +73615,8 @@ var init_cron_runner = __esm({
71908
73615
  const modelProvider = step.modelProvider?.trim() || defaultModel.provider;
71909
73616
  const modelId = step.modelId?.trim() || defaultModel.modelId;
71910
73617
  const model = modelProvider && modelId ? `${modelProvider}/${modelId}` : "default";
71911
- log6.log(` AI prompt step "${step.name}" using model: ${model}`);
71912
- log6.log(` Prompt: ${step.prompt.slice(0, 100)}${step.prompt.length > 100 ? "\u2026" : ""}`);
73618
+ log14.log(` AI prompt step "${step.name}" using model: ${model}`);
73619
+ log14.log(` Prompt: ${step.prompt.slice(0, 100)}${step.prompt.length > 100 ? "\u2026" : ""}`);
71913
73620
  try {
71914
73621
  const resultPromise = this.aiPromptExecutor(step.prompt, modelProvider, modelId);
71915
73622
  const timeoutPromise = new Promise((_resolve, reject) => {
@@ -71917,7 +73624,7 @@ var init_cron_runner = __esm({
71917
73624
  });
71918
73625
  const response = await Promise.race([resultPromise, timeoutPromise]);
71919
73626
  const output = response.length > MAX_OUTPUT_LENGTH ? response.slice(0, MAX_OUTPUT_LENGTH) + "\n[output truncated]" : response;
71920
- log6.log(` \u2713 AI prompt step "${step.name}" completed (${response.length} chars)`);
73627
+ log14.log(` \u2713 AI prompt step "${step.name}" completed (${response.length} chars)`);
71921
73628
  return {
71922
73629
  stepId: step.id,
71923
73630
  stepName: step.name,
@@ -71929,7 +73636,7 @@ var init_cron_runner = __esm({
71929
73636
  };
71930
73637
  } catch (err) {
71931
73638
  const errorMessage = err.message ?? String(err);
71932
- log6.warn(` \u2717 AI prompt step "${step.name}" failed: ${errorMessage}`);
73639
+ log14.warn(` \u2717 AI prompt step "${step.name}" failed: ${errorMessage}`);
71933
73640
  return {
71934
73641
  stepId: step.id,
71935
73642
  stepName: step.name,
@@ -71973,7 +73680,7 @@ var init_cron_runner = __esm({
71973
73680
  try {
71974
73681
  const task = await this.store.createTask(taskInput);
71975
73682
  const output = `Created task ${task.id}: ${task.title || task.description.slice(0, 80)}`;
71976
- log6.log(` \u2713 Create-task step "${step.name}" created task ${task.id}`);
73683
+ log14.log(` \u2713 Create-task step "${step.name}" created task ${task.id}`);
71977
73684
  return {
71978
73685
  stepId: step.id,
71979
73686
  stepName: step.name,
@@ -71985,7 +73692,7 @@ var init_cron_runner = __esm({
71985
73692
  };
71986
73693
  } catch (err) {
71987
73694
  const errorMessage = err.message ?? String(err);
71988
- log6.warn(` \u2717 Create-task step "${step.name}" failed: ${errorMessage}`);
73695
+ log14.warn(` \u2717 Create-task step "${step.name}" failed: ${errorMessage}`);
71989
73696
  return {
71990
73697
  stepId: step.id,
71991
73698
  stepName: step.name,
@@ -72027,16 +73734,16 @@ function truncateOutput2(stdout, stderr) {
72027
73734
  }
72028
73735
  return output;
72029
73736
  }
72030
- var import_cron_parser4, log7, execAsync7, DEFAULT_TIMEOUT_MS2, MAX_BUFFER2, MAX_OUTPUT_LENGTH2, MAX_CATCH_UP_INTERVALS, RoutineRunner;
73737
+ var import_cron_parser4, log15, execAsync7, DEFAULT_TIMEOUT_MS7, MAX_BUFFER2, MAX_OUTPUT_LENGTH2, MAX_CATCH_UP_INTERVALS, RoutineRunner;
72031
73738
  var init_routine_runner = __esm({
72032
73739
  "../engine/src/routine-runner.ts"() {
72033
73740
  "use strict";
72034
73741
  import_cron_parser4 = __toESM(require_dist2(), 1);
72035
73742
  init_logger2();
72036
73743
  init_shell_utils();
72037
- log7 = createLogger2("routine-runner");
73744
+ log15 = createLogger2("routine-runner");
72038
73745
  execAsync7 = promisify8(exec7);
72039
- DEFAULT_TIMEOUT_MS2 = 5 * 60 * 1e3;
73746
+ DEFAULT_TIMEOUT_MS7 = 5 * 60 * 1e3;
72040
73747
  MAX_BUFFER2 = 1024 * 1024;
72041
73748
  MAX_OUTPUT_LENGTH2 = 10 * 1024;
72042
73749
  MAX_CATCH_UP_INTERVALS = 10;
@@ -72071,7 +73778,7 @@ var init_routine_runner = __esm({
72071
73778
  }
72072
73779
  const concurrency = routine.executionPolicy ?? "queue";
72073
73780
  if (concurrency === "reject" && this.inFlightExecutions.has(routineId)) {
72074
- log7.log(`Routine ${routineId} rejected \u2014 already running`);
73781
+ log15.log(`Routine ${routineId} rejected \u2014 already running`);
72075
73782
  return {
72076
73783
  routineId,
72077
73784
  success: false,
@@ -72082,7 +73789,7 @@ var init_routine_runner = __esm({
72082
73789
  };
72083
73790
  }
72084
73791
  if (concurrency === "queue" && this.inFlightExecutions.has(routineId)) {
72085
- log7.log(`Routine ${routineId} queued \u2014 waiting for existing execution`);
73792
+ log15.log(`Routine ${routineId} queued \u2014 waiting for existing execution`);
72086
73793
  const existingResult = await this.inFlightExecutions.get(routineId);
72087
73794
  if (existingResult) {
72088
73795
  await existingResult;
@@ -72130,7 +73837,7 @@ var init_routine_runner = __esm({
72130
73837
  };
72131
73838
  } catch (err) {
72132
73839
  const errorMessage = err instanceof Error ? err.message : String(err);
72133
- log7.error(`Routine ${routineId} execution failed: ${errorMessage}`);
73840
+ log15.error(`Routine ${routineId} execution failed: ${errorMessage}`);
72134
73841
  try {
72135
73842
  await this.options.routineStore.completeRoutineExecution(routineId, {
72136
73843
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -72138,7 +73845,7 @@ var init_routine_runner = __esm({
72138
73845
  error: errorMessage
72139
73846
  });
72140
73847
  } catch (persistError) {
72141
- log7.error(`[${routineId}] Failed to persist error state: ${persistError}`);
73848
+ log15.error(`[${routineId}] Failed to persist error state: ${persistError}`);
72142
73849
  }
72143
73850
  return {
72144
73851
  routineId,
@@ -72191,7 +73898,7 @@ var init_routine_runner = __esm({
72191
73898
  async executeCommand(command, timeoutMs, startedAt) {
72192
73899
  try {
72193
73900
  const { stdout, stderr } = await execAsync7(command, {
72194
- timeout: timeoutMs ?? DEFAULT_TIMEOUT_MS2,
73901
+ timeout: timeoutMs ?? DEFAULT_TIMEOUT_MS7,
72195
73902
  maxBuffer: MAX_BUFFER2,
72196
73903
  shell: defaultShell
72197
73904
  });
@@ -72205,7 +73912,7 @@ var init_routine_runner = __esm({
72205
73912
  const errObj = err;
72206
73913
  const stdout = typeof errObj.stdout === "string" ? errObj.stdout : "";
72207
73914
  const stderr = typeof errObj.stderr === "string" ? errObj.stderr : "";
72208
- const error = errObj.killed === true ? `Command timed out after ${(timeoutMs ?? DEFAULT_TIMEOUT_MS2) / 1e3}s` : (err instanceof Error ? err.message : null) ?? String(err);
73915
+ const error = errObj.killed === true ? `Command timed out after ${(timeoutMs ?? DEFAULT_TIMEOUT_MS7) / 1e3}s` : (err instanceof Error ? err.message : null) ?? String(err);
72209
73916
  return {
72210
73917
  success: false,
72211
73918
  output: truncateOutput2(stdout, stderr),
@@ -72250,7 +73957,7 @@ var init_routine_runner = __esm({
72250
73957
  }
72251
73958
  async executeStep(routine, step, stepIndex) {
72252
73959
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
72253
- const timeoutMs = step.timeoutMs ?? routine.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
73960
+ const timeoutMs = step.timeoutMs ?? routine.timeoutMs ?? DEFAULT_TIMEOUT_MS7;
72254
73961
  if (step.type === "command") {
72255
73962
  const result = await this.executeCommand(step.command ?? "", timeoutMs, startedAt);
72256
73963
  return {
@@ -72352,7 +74059,7 @@ var init_routine_runner = __esm({
72352
74059
  if (missedIntervals.length === 0) {
72353
74060
  return;
72354
74061
  }
72355
- log7.log(`[${routine.id}] Running ${missedIntervals.length} catch-up executions`);
74062
+ log15.log(`[${routine.id}] Running ${missedIntervals.length} catch-up executions`);
72356
74063
  for (const missedInterval of missedIntervals) {
72357
74064
  try {
72358
74065
  await this.executeRoutine(routine.id, "cron", {
@@ -72360,11 +74067,11 @@ var init_routine_runner = __esm({
72360
74067
  missedInterval: missedInterval.toISOString()
72361
74068
  });
72362
74069
  } catch (err) {
72363
- log7.error(`[${routine.id}] Catch-up execution failed: ${err}`);
74070
+ log15.error(`[${routine.id}] Catch-up execution failed: ${err}`);
72364
74071
  }
72365
74072
  }
72366
74073
  } catch (err) {
72367
- log7.error(`[${routine.id}] Error calculating catch-up intervals: ${err}`);
74074
+ log15.error(`[${routine.id}] Error calculating catch-up intervals: ${err}`);
72368
74075
  }
72369
74076
  }
72370
74077
  /**
@@ -72891,7 +74598,7 @@ var init_stuck_task_detector = __esm({
72891
74598
  import { exec as exec8 } from "node:child_process";
72892
74599
  import { promisify as promisify9 } from "node:util";
72893
74600
  import { existsSync as existsSync28, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
72894
- import { isAbsolute as isAbsolute12, join as join35, relative as relative7, resolve as resolve15 } from "node:path";
74601
+ import { isAbsolute as isAbsolute12, join as join36, relative as relative8, resolve as resolve16 } from "node:path";
72895
74602
  function shellQuote(value) {
72896
74603
  return `'${value.replace(/'/g, "'\\''")}'`;
72897
74604
  }
@@ -72925,19 +74632,25 @@ function isNoTaskDoneFailure(task) {
72925
74632
  function hasStepProgress(task) {
72926
74633
  return task.steps.some((step) => step.status !== "pending");
72927
74634
  }
72928
- var log8, execAsync8, APPROVED_TRIAGE_RECOVERY_GRACE_MS, ORPHANED_EXECUTION_RECOVERY_GRACE_MS, ACTIVE_MERGE_STATUSES, NON_TERMINAL_STEP_STATUSES2, ORPHANED_WITH_WORKTREE_GRACE_MS, MAX_TASK_DONE_RETRIES, SelfHealingManager;
74635
+ var log16, execAsync8, APPROVED_TRIAGE_RECOVERY_GRACE_MS, ORPHANED_EXECUTION_RECOVERY_GRACE_MS, ACTIVE_MERGE_STATUSES, NON_TERMINAL_STEP_STATUSES2, GHOST_REVIEW_PRESERVED_STATUSES, ORPHANED_WITH_WORKTREE_GRACE_MS, MAX_TASK_DONE_RETRIES, SelfHealingManager;
72929
74636
  var init_self_healing = __esm({
72930
74637
  "../engine/src/self-healing.ts"() {
72931
74638
  "use strict";
72932
74639
  init_src();
72933
74640
  init_logger2();
72934
74641
  init_worktree_pool();
72935
- log8 = createLogger2("self-healing");
74642
+ log16 = createLogger2("self-healing");
72936
74643
  execAsync8 = promisify9(exec8);
72937
74644
  APPROVED_TRIAGE_RECOVERY_GRACE_MS = 6e4;
72938
74645
  ORPHANED_EXECUTION_RECOVERY_GRACE_MS = 6e4;
72939
74646
  ACTIVE_MERGE_STATUSES = /* @__PURE__ */ new Set(["merging", "merging-pr"]);
72940
74647
  NON_TERMINAL_STEP_STATUSES2 = /* @__PURE__ */ new Set(["pending", "in-progress"]);
74648
+ GHOST_REVIEW_PRESERVED_STATUSES = /* @__PURE__ */ new Set([
74649
+ "awaiting-user-review",
74650
+ "awaiting-approval",
74651
+ "merging",
74652
+ "merging-pr"
74653
+ ]);
72941
74654
  ORPHANED_WITH_WORKTREE_GRACE_MS = 3e5;
72942
74655
  MAX_TASK_DONE_RETRIES = 3;
72943
74656
  SelfHealingManager = class _SelfHealingManager {
@@ -72962,7 +74675,7 @@ var init_self_healing = __esm({
72962
74675
  };
72963
74676
  this.store.on("settings:updated", this.settingsListener);
72964
74677
  this.startMaintenance();
72965
- log8.log("Started");
74678
+ log16.log("Started");
72966
74679
  }
72967
74680
  /**
72968
74681
  * Run only the recovery subset needed at runtime startup, after the executor
@@ -72987,10 +74700,10 @@ var init_self_healing = __esm({
72987
74700
  for (const step of steps) {
72988
74701
  try {
72989
74702
  await step.fn();
72990
- log8.log(`Startup recovery step "${step.name}" completed`);
74703
+ log16.log(`Startup recovery step "${step.name}" completed`);
72991
74704
  } catch (stepErr) {
72992
74705
  const stepErrMessage = stepErr instanceof Error ? stepErr.message : String(stepErr);
72993
- log8.error(`Startup recovery step "${step.name}" failed: ${stepErrMessage} \u2014 continuing with remaining steps`);
74706
+ log16.error(`Startup recovery step "${step.name}" failed: ${stepErrMessage} \u2014 continuing with remaining steps`);
72994
74707
  }
72995
74708
  }
72996
74709
  }
@@ -73000,7 +74713,7 @@ var init_self_healing = __esm({
73000
74713
  this.store.removeListener("settings:updated", this.settingsListener);
73001
74714
  } catch (err) {
73002
74715
  const errorMessage = err instanceof Error ? err.message : String(err);
73003
- log8.warn(`Failed to remove settings:updated listener during stop(): ${errorMessage}`);
74716
+ log16.warn(`Failed to remove settings:updated listener during stop(): ${errorMessage}`);
73004
74717
  }
73005
74718
  this.settingsListener = null;
73006
74719
  }
@@ -73009,22 +74722,22 @@ var init_self_healing = __esm({
73009
74722
  clearInterval(this.maintenanceInterval);
73010
74723
  this.maintenanceInterval = null;
73011
74724
  }
73012
- log8.log("Stopped");
74725
+ log16.log("Stopped");
73013
74726
  }
73014
74727
  // ── Auto-unpause ───────────────────────────────────────────────────
73015
74728
  onSettingsUpdated(settings, previous) {
73016
74729
  if (!previous.globalPause && settings.globalPause) {
73017
74730
  if (!settings.autoUnpauseEnabled) {
73018
- log8.log("Global pause activated \u2014 auto-unpause disabled, requires manual intervention");
74731
+ log16.log("Global pause activated \u2014 auto-unpause disabled, requires manual intervention");
73019
74732
  return;
73020
74733
  }
73021
74734
  if (settings.globalPauseReason === "manual") {
73022
- log8.log("Global pause activated manually \u2014 auto-unpause skipped, requires manual intervention");
74735
+ log16.log("Global pause activated manually \u2014 auto-unpause skipped, requires manual intervention");
73023
74736
  return;
73024
74737
  }
73025
74738
  if (this.lastUnpauseAt && Date.now() - this.lastUnpauseAt < 6e4) {
73026
74739
  this.unpauseAttempt++;
73027
- log8.warn(`Global pause re-triggered within 60s \u2014 escalating to attempt ${this.unpauseAttempt}`);
74740
+ log16.warn(`Global pause re-triggered within 60s \u2014 escalating to attempt ${this.unpauseAttempt}`);
73028
74741
  }
73029
74742
  this.lastPauseTriggeredAt = Date.now();
73030
74743
  const baseDelay = settings.autoUnpauseBaseDelayMs ?? 3e5;
@@ -73044,7 +74757,7 @@ var init_self_healing = __esm({
73044
74757
  const delaySec = Math.round(delayMs / 1e3);
73045
74758
  const delayMin = Math.round(delaySec / 60);
73046
74759
  const display = delayMin >= 1 ? `${delayMin}m` : `${delaySec}s`;
73047
- log8.warn(`Auto-unpause scheduled in ${display} (attempt ${this.unpauseAttempt + 1})`);
74760
+ log16.warn(`Auto-unpause scheduled in ${display} (attempt ${this.unpauseAttempt + 1})`);
73048
74761
  this.unpauseTimer = setTimeout(() => {
73049
74762
  this.unpauseTimer = null;
73050
74763
  void this.attemptUnpause();
@@ -73054,16 +74767,16 @@ var init_self_healing = __esm({
73054
74767
  try {
73055
74768
  const settings = await this.store.getSettings();
73056
74769
  if (!settings.globalPause) {
73057
- log8.log("Auto-unpause: already unpaused \u2014 no action needed");
74770
+ log16.log("Auto-unpause: already unpaused \u2014 no action needed");
73058
74771
  this.unpauseAttempt = 0;
73059
74772
  return;
73060
74773
  }
73061
- log8.warn("Auto-unpause: clearing globalPause");
74774
+ log16.warn("Auto-unpause: clearing globalPause");
73062
74775
  this.lastUnpauseAt = Date.now();
73063
74776
  await this.store.updateSettings({ globalPause: false, globalPauseReason: void 0 });
73064
74777
  } catch (err) {
73065
74778
  const errorMessage = err instanceof Error ? err.message : String(err);
73066
- log8.error(`Auto-unpause failed: ${errorMessage}`);
74779
+ log16.error(`Auto-unpause failed: ${errorMessage}`);
73067
74780
  }
73068
74781
  }
73069
74782
  cancelUnpauseTimer() {
@@ -73087,7 +74800,7 @@ var init_self_healing = __esm({
73087
74800
  const task = await this.store.getTask(taskId);
73088
74801
  const newCount = (task.stuckKillCount ?? 0) + 1;
73089
74802
  if (newCount > maxKills) {
73090
- log8.warn(`${taskId} exceeded stuck kill budget (${newCount}/${maxKills}) \u2014 marking failed`);
74803
+ log16.warn(`${taskId} exceeded stuck kill budget (${newCount}/${maxKills}) \u2014 marking failed`);
73091
74804
  await this.store.updateTask(taskId, {
73092
74805
  stuckKillCount: newCount,
73093
74806
  status: "failed",
@@ -73097,7 +74810,7 @@ var init_self_healing = __esm({
73097
74810
  await this.store.moveTask(taskId, "in-review");
73098
74811
  } catch (moveErr) {
73099
74812
  const moveErrMessage = moveErr instanceof Error ? moveErr.message : String(moveErr);
73100
- log8.warn(`${taskId} moveTask("in-review") failed (${moveErrMessage}) \u2014 task already marked failed, not re-queuing`);
74813
+ log16.warn(`${taskId} moveTask("in-review") failed (${moveErrMessage}) \u2014 task already marked failed, not re-queuing`);
73101
74814
  }
73102
74815
  await this.store.logEntry(
73103
74816
  taskId,
@@ -73105,7 +74818,7 @@ var init_self_healing = __esm({
73105
74818
  );
73106
74819
  return false;
73107
74820
  }
73108
- log8.log(`${taskId} stuck kill ${newCount}/${maxKills} \u2014 will re-queue`);
74821
+ log16.log(`${taskId} stuck kill ${newCount}/${maxKills} \u2014 will re-queue`);
73109
74822
  await this.store.updateTask(taskId, { stuckKillCount: newCount });
73110
74823
  await this.store.logEntry(
73111
74824
  taskId,
@@ -73114,7 +74827,7 @@ var init_self_healing = __esm({
73114
74827
  return true;
73115
74828
  } catch (err) {
73116
74829
  const errorMessage = err instanceof Error ? err.message : String(err);
73117
- log8.error(`checkStuckBudget failed for ${taskId}: ${errorMessage}`);
74830
+ log16.error(`checkStuckBudget failed for ${taskId}: ${errorMessage}`);
73118
74831
  return true;
73119
74832
  }
73120
74833
  }
@@ -73143,7 +74856,7 @@ var init_self_healing = __esm({
73143
74856
  );
73144
74857
  const branchHead = branchHeadOut.trim();
73145
74858
  if (mergeBase === branchHead) {
73146
- log8.warn(
74859
+ log16.warn(
73147
74860
  `${task.id} branch has no unique commits \u2014 resetting ${completedSteps.length} step(s) to pending`
73148
74861
  );
73149
74862
  for (let i = 0; i < task.steps.length; i++) {
@@ -73158,7 +74871,7 @@ var init_self_healing = __esm({
73158
74871
  }
73159
74872
  } catch (err) {
73160
74873
  const errorMessage = err instanceof Error ? err.message : String(err);
73161
- log8.warn(
74874
+ log16.warn(
73162
74875
  `Failed to reset steps for ${task.id} after branch/worktree loss (${branchName}): ${errorMessage} \u2014 non-fatal`
73163
74876
  );
73164
74877
  }
@@ -73168,10 +74881,10 @@ var init_self_healing = __esm({
73168
74881
  const settings = await this.store.getSettings();
73169
74882
  const intervalMs = settings.maintenanceIntervalMs ?? 9e5;
73170
74883
  if (intervalMs <= 0) {
73171
- log8.log("Periodic maintenance disabled (maintenanceIntervalMs <= 0)");
74884
+ log16.log("Periodic maintenance disabled (maintenanceIntervalMs <= 0)");
73172
74885
  return;
73173
74886
  }
73174
- log8.log(`Periodic maintenance every ${Math.round(intervalMs / 6e4)}m`);
74887
+ log16.log(`Periodic maintenance every ${Math.round(intervalMs / 6e4)}m`);
73175
74888
  this.maintenanceInterval = setInterval(() => {
73176
74889
  void this.runMaintenance();
73177
74890
  }, intervalMs);
@@ -73234,7 +74947,7 @@ var init_self_healing = __esm({
73234
74947
  out = r.stdout;
73235
74948
  } catch (err) {
73236
74949
  const errorMessage = err instanceof Error ? err.message : String(err);
73237
- log8.warn(
74950
+ log16.warn(
73238
74951
  `Failed to read git log for landed commit lookup (${task.id}): ${errorMessage} \u2014 retrying with HEAD range`
73239
74952
  );
73240
74953
  if (!task.baseCommitSha) return "";
@@ -73265,7 +74978,7 @@ var init_self_healing = __esm({
73265
74978
  Object.assign(commit, parseShortstat(stats.stdout));
73266
74979
  } catch (err) {
73267
74980
  const errorMessage = err instanceof Error ? err.message : String(err);
73268
- log8.warn(
74981
+ log16.warn(
73269
74982
  `Failed to read shortstat for landed commit ${sha} (${task.id}): ${errorMessage} \u2014 continuing without stats`
73270
74983
  );
73271
74984
  }
@@ -73280,7 +74993,7 @@ var init_self_healing = __esm({
73280
74993
  });
73281
74994
  } catch (err) {
73282
74995
  const errorMessage = err instanceof Error ? err.message : String(err);
73283
- log8.warn(
74996
+ log16.warn(
73284
74997
  `Failed to remove interrupted-merge worktree ${task.worktree} for ${task.id}: ${errorMessage} \u2014 non-fatal, cleanup can retry later`
73285
74998
  );
73286
74999
  }
@@ -73293,19 +75006,19 @@ var init_self_healing = __esm({
73293
75006
  });
73294
75007
  } catch (err) {
73295
75008
  const errorMessage = err instanceof Error ? err.message : String(err);
73296
- log8.warn(
75009
+ log16.warn(
73297
75010
  `Failed to delete interrupted-merge branch ${branch} for ${task.id}: ${errorMessage} \u2014 non-fatal`
73298
75011
  );
73299
75012
  }
73300
75013
  }
73301
75014
  async runMaintenance() {
73302
75015
  if (this.maintenanceRunning) {
73303
- log8.log("Maintenance cycle skipped \u2014 previous cycle still running");
75016
+ log16.log("Maintenance cycle skipped \u2014 previous cycle still running");
73304
75017
  return;
73305
75018
  }
73306
75019
  this.maintenanceRunning = true;
73307
75020
  const startMs = Date.now();
73308
- log8.log("Maintenance cycle starting");
75021
+ log16.log("Maintenance cycle starting");
73309
75022
  try {
73310
75023
  const batch1Fns = [
73311
75024
  { name: "prune-worktrees", fn: () => this.pruneWorktrees() },
@@ -73317,9 +75030,9 @@ var init_self_healing = __esm({
73317
75030
  for (const fn of batch1Fns) {
73318
75031
  try {
73319
75032
  await fn.fn();
73320
- log8.log(`Maintenance batch 1 step "${fn.name}" succeeded`);
75033
+ log16.log(`Maintenance batch 1 step "${fn.name}" succeeded`);
73321
75034
  } catch (stepErr) {
73322
- log8.error(`Maintenance batch 1 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
75035
+ log16.error(`Maintenance batch 1 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
73323
75036
  }
73324
75037
  }
73325
75038
  const batch2Fns = [
@@ -73334,14 +75047,15 @@ var init_self_healing = __esm({
73334
75047
  { name: "recover-partial-progress-no-task-done", fn: () => this.recoverPartialProgressNoTaskDoneFailures() },
73335
75048
  { name: "recover-orphaned-executions", fn: () => this.recoverOrphanedExecutions() },
73336
75049
  { name: "recover-approved-triage", fn: () => this.recoverApprovedTriageTasks() },
73337
- { name: "recover-orphaned-planning", fn: () => this.recoverOrphanedPlanningTasks() }
75050
+ { name: "recover-orphaned-planning", fn: () => this.recoverOrphanedPlanningTasks() },
75051
+ { name: "recover-ghost-review", fn: () => this.recoverGhostReviewTasks() }
73338
75052
  ];
73339
75053
  for (const fn of batch2Fns) {
73340
75054
  try {
73341
75055
  await fn.fn();
73342
- log8.log(`Maintenance batch 2 step "${fn.name}" succeeded`);
75056
+ log16.log(`Maintenance batch 2 step "${fn.name}" succeeded`);
73343
75057
  } catch (stepErr) {
73344
- log8.error(`Maintenance batch 2 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
75058
+ log16.error(`Maintenance batch 2 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
73345
75059
  }
73346
75060
  }
73347
75061
  const batch3Fns = [
@@ -73350,13 +75064,13 @@ var init_self_healing = __esm({
73350
75064
  for (const fn of batch3Fns) {
73351
75065
  try {
73352
75066
  await fn.fn();
73353
- log8.log(`Maintenance batch 3 step "${fn.name}" succeeded`);
75067
+ log16.log(`Maintenance batch 3 step "${fn.name}" succeeded`);
73354
75068
  } catch (stepErr) {
73355
- log8.error(`Maintenance batch 3 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
75069
+ log16.error(`Maintenance batch 3 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
73356
75070
  }
73357
75071
  }
73358
75072
  const elapsedMs = Date.now() - startMs;
73359
- log8.log(`Maintenance cycle completed in ${elapsedMs}ms`);
75073
+ log16.log(`Maintenance cycle completed in ${elapsedMs}ms`);
73360
75074
  } finally {
73361
75075
  this.maintenanceRunning = false;
73362
75076
  }
@@ -73389,7 +75103,7 @@ var init_self_healing = __esm({
73389
75103
  return movedAt < cutoff;
73390
75104
  });
73391
75105
  if (stale.length === 0) return 0;
73392
- log8.log(`Auto-archiving ${stale.length} done task(s) older than ${archiveAfterMs}ms`);
75106
+ log16.log(`Auto-archiving ${stale.length} done task(s) older than ${archiveAfterMs}ms`);
73393
75107
  let archived = 0;
73394
75108
  for (const task of stale) {
73395
75109
  try {
@@ -73397,16 +75111,16 @@ var init_self_healing = __esm({
73397
75111
  archived++;
73398
75112
  } catch (err) {
73399
75113
  const errorMessage = err instanceof Error ? err.message : String(err);
73400
- log8.error(`Failed to auto-archive ${task.id}: ${errorMessage}`);
75114
+ log16.error(`Failed to auto-archive ${task.id}: ${errorMessage}`);
73401
75115
  }
73402
75116
  }
73403
75117
  if (archived > 0) {
73404
- log8.log(`Auto-archived ${archived} stale done task(s)`);
75118
+ log16.log(`Auto-archived ${archived} stale done task(s)`);
73405
75119
  }
73406
75120
  return archived;
73407
75121
  } catch (err) {
73408
75122
  const errorMessage = err instanceof Error ? err.message : String(err);
73409
- log8.error(`Auto-archive sweep failed: ${errorMessage}`);
75123
+ log16.error(`Auto-archive sweep failed: ${errorMessage}`);
73410
75124
  return 0;
73411
75125
  }
73412
75126
  }
@@ -73431,25 +75145,25 @@ var init_self_healing = __esm({
73431
75145
  (t) => t.column === "in-progress" && !t.paused && !executingIds.has(t.id) && t.steps.length > 0 && t.steps.every((s) => s.status === "done" || s.status === "skipped")
73432
75146
  );
73433
75147
  if (stuckCompleted.length === 0) return 0;
73434
- log8.warn(`Found ${stuckCompleted.length} completed task(s) stuck in in-progress`);
75148
+ log16.warn(`Found ${stuckCompleted.length} completed task(s) stuck in in-progress`);
73435
75149
  let recovered = 0;
73436
75150
  for (const task of stuckCompleted) {
73437
75151
  const latestExecutingIds = this.options.getExecutingTaskIds?.() ?? /* @__PURE__ */ new Set();
73438
75152
  if (latestExecutingIds.has(task.id)) {
73439
- log8.log(`${task.id} started executing concurrently \u2014 skipping recovery this cycle`);
75153
+ log16.log(`${task.id} started executing concurrently \u2014 skipping recovery this cycle`);
73440
75154
  continue;
73441
75155
  }
73442
- log8.log(`Recovering completed task ${task.id}: ${task.title || task.description?.slice(0, 60) || "(untitled)"}`);
75156
+ log16.log(`Recovering completed task ${task.id}: ${task.title || task.description?.slice(0, 60) || "(untitled)"}`);
73443
75157
  const success = await recoverFn(task);
73444
75158
  if (success) recovered++;
73445
75159
  }
73446
75160
  if (recovered > 0) {
73447
- log8.log(`Recovered ${recovered} completed task(s) \u2192 in-review`);
75161
+ log16.log(`Recovered ${recovered} completed task(s) \u2192 in-review`);
73448
75162
  }
73449
75163
  return recovered;
73450
75164
  } catch (err) {
73451
75165
  const errorMessage = err instanceof Error ? err.message : String(err);
73452
- log8.error(`Completed task recovery failed: ${errorMessage}`);
75166
+ log16.error(`Completed task recovery failed: ${errorMessage}`);
73453
75167
  return 0;
73454
75168
  }
73455
75169
  }
@@ -73472,7 +75186,7 @@ var init_self_healing = __esm({
73472
75186
  (t) => t.column === "in-review" && !t.paused && Boolean(t.worktree) && t.mergeDetails?.mergeConfirmed !== true && getTaskMergeBlocker(t) === void 0
73473
75187
  );
73474
75188
  if (mergeable.length === 0) return 0;
73475
- log8.warn(`Found ${mergeable.length} mergeable review task(s) stuck in in-review`);
75189
+ log16.warn(`Found ${mergeable.length} mergeable review task(s) stuck in in-review`);
73476
75190
  const enqueueMerge = this.options.enqueueMerge;
73477
75191
  let recovered = 0;
73478
75192
  for (const task of mergeable) {
@@ -73486,20 +75200,20 @@ var init_self_healing = __esm({
73486
75200
  task.id,
73487
75201
  enqueueMerge ? "Auto-recovered: eligible in-review task re-enqueued for merge" : "Auto-recovered: eligible in-review task was merged and moved to done"
73488
75202
  );
73489
- log8.log(`Recovered mergeable review task ${task.id}`);
75203
+ log16.log(`Recovered mergeable review task ${task.id}`);
73490
75204
  recovered++;
73491
75205
  } catch (err) {
73492
75206
  const errorMessage = err instanceof Error ? err.message : String(err);
73493
- log8.error(`Failed to recover mergeable review task ${task.id}: ${errorMessage}`);
75207
+ log16.error(`Failed to recover mergeable review task ${task.id}: ${errorMessage}`);
73494
75208
  }
73495
75209
  }
73496
75210
  if (recovered > 0) {
73497
- log8.log(`Recovered ${recovered} mergeable review task(s) \u2192 done`);
75211
+ log16.log(`Recovered ${recovered} mergeable review task(s) \u2192 done`);
73498
75212
  }
73499
75213
  return recovered;
73500
75214
  } catch (err) {
73501
75215
  const errorMessage = err instanceof Error ? err.message : String(err);
73502
- log8.error(`Mergeable review recovery failed: ${errorMessage}`);
75216
+ log16.error(`Mergeable review recovery failed: ${errorMessage}`);
73503
75217
  return 0;
73504
75218
  }
73505
75219
  }
@@ -73547,7 +75261,7 @@ var init_self_healing = __esm({
73547
75261
  return true;
73548
75262
  });
73549
75263
  if (candidates.length === 0) return 0;
73550
- log8.warn(`Found ${candidates.length} in-review task(s) with failed pre-merge workflow steps \u2014 auto-reviving`);
75264
+ log16.warn(`Found ${candidates.length} in-review task(s) with failed pre-merge workflow steps \u2014 auto-reviving`);
73551
75265
  let recovered = 0;
73552
75266
  for (const task of candidates) {
73553
75267
  const nextCount = (task.postReviewFixCount ?? 0) + 1;
@@ -73559,23 +75273,23 @@ var init_self_healing = __esm({
73559
75273
  );
73560
75274
  const sentBack = await recoverFn(task);
73561
75275
  if (sentBack) {
73562
- log8.log(`Revived ${task.id}: sent back for fix (${nextCount}/${maxFixes})`);
75276
+ log16.log(`Revived ${task.id}: sent back for fix (${nextCount}/${maxFixes})`);
73563
75277
  recovered++;
73564
75278
  } else {
73565
- log8.warn(`Revival of ${task.id} was skipped by executor \u2014 budget already consumed`);
75279
+ log16.warn(`Revival of ${task.id} was skipped by executor \u2014 budget already consumed`);
73566
75280
  }
73567
75281
  } catch (err) {
73568
75282
  const errorMessage = err instanceof Error ? err.message : String(err);
73569
- log8.error(`Failed to revive ${task.id}: ${errorMessage}`);
75283
+ log16.error(`Failed to revive ${task.id}: ${errorMessage}`);
73570
75284
  }
73571
75285
  }
73572
75286
  if (recovered > 0) {
73573
- log8.log(`Auto-revived ${recovered} in-review task(s) for pre-merge workflow step fix`);
75287
+ log16.log(`Auto-revived ${recovered} in-review task(s) for pre-merge workflow step fix`);
73574
75288
  }
73575
75289
  return recovered;
73576
75290
  } catch (err) {
73577
75291
  const errorMessage = err instanceof Error ? err.message : String(err);
73578
- log8.error(`Failed pre-merge workflow step revival failed: ${errorMessage}`);
75292
+ log16.error(`Failed pre-merge workflow step revival failed: ${errorMessage}`);
73579
75293
  return 0;
73580
75294
  }
73581
75295
  }
@@ -73599,7 +75313,7 @@ var init_self_healing = __esm({
73599
75313
  (task) => task.column === "in-review" && !task.paused && !task.status && task.steps.length > 0 && task.steps.some((step) => NON_TERMINAL_STEP_STATUSES2.has(step.status)) && now - new Date(task.updatedAt).getTime() >= timeoutMs
73600
75314
  );
73601
75315
  if (staleIncomplete.length === 0) return 0;
73602
- log8.warn(`Found ${staleIncomplete.length} stale in-review task(s) with incomplete steps`);
75316
+ log16.warn(`Found ${staleIncomplete.length} stale in-review task(s) with incomplete steps`);
73603
75317
  let recovered = 0;
73604
75318
  for (const task of staleIncomplete) {
73605
75319
  try {
@@ -73608,17 +75322,82 @@ var init_self_healing = __esm({
73608
75322
  "Auto-recovered: in-review task still had incomplete steps \u2014 moved back to todo for retry"
73609
75323
  );
73610
75324
  await this.store.moveTask(task.id, "todo");
73611
- log8.log(`Recovered stale incomplete review task ${task.id}: moved back to todo`);
75325
+ log16.log(`Recovered stale incomplete review task ${task.id}: moved back to todo`);
73612
75326
  recovered++;
73613
75327
  } catch (err) {
73614
75328
  const errorMessage = err instanceof Error ? err.message : String(err);
73615
- log8.error(`Failed to recover stale incomplete review task ${task.id}: ${errorMessage}`);
75329
+ log16.error(`Failed to recover stale incomplete review task ${task.id}: ${errorMessage}`);
73616
75330
  }
73617
75331
  }
73618
75332
  return recovered;
73619
75333
  } catch (err) {
73620
75334
  const errorMessage = err instanceof Error ? err.message : String(err);
73621
- log8.error(`Stale incomplete review recovery failed: ${errorMessage}`);
75335
+ log16.error(`Stale incomplete review recovery failed: ${errorMessage}`);
75336
+ return 0;
75337
+ }
75338
+ }
75339
+ /**
75340
+ * Final-fallback recovery for `in-review` tasks that fell through every other
75341
+ * scan and have sat untouched longer than `taskStuckTimeoutMs`.
75342
+ *
75343
+ * The other review-recovery scans each require a specific shape (failed
75344
+ * pre-merge step, incomplete steps, mergeable + worktree present, confirmed
75345
+ * merge, transient merge status). A task whose state doesn't match any of
75346
+ * those shapes — e.g. `status: "failed"` with no failed pre-merge step, or
75347
+ * any other unanticipated combination — has no recovery path and stays
75348
+ * silent in review forever.
75349
+ *
75350
+ * This catch-all kicks any such task back to `todo`, clearing transient
75351
+ * `status` so the scheduler can pick it up. Worktree state is intentionally
75352
+ * not considered: the executor will recreate one if needed.
75353
+ *
75354
+ * Preserved statuses (skipped):
75355
+ * - `awaiting-user-review`, `awaiting-approval`: explicit human handoff
75356
+ * - `merging`, `merging-pr`: handled by `recoverInterruptedMergingTasks`
75357
+ *
75358
+ * Rate-limiting comes from the `updatedAt >= taskStuckTimeoutMs` gate —
75359
+ * each kick refreshes `updatedAt`, so a task that re-enters review and gets
75360
+ * stuck again can only be kicked once per `taskStuckTimeoutMs` window.
75361
+ *
75362
+ * @returns Number of tasks kicked back to todo
75363
+ */
75364
+ async recoverGhostReviewTasks() {
75365
+ try {
75366
+ const settings = await this.store.getSettings();
75367
+ const timeoutMs = settings.taskStuckTimeoutMs;
75368
+ if (!timeoutMs || timeoutMs <= 0) return 0;
75369
+ if (settings.globalPause || settings.enginePaused) return 0;
75370
+ const now = Date.now();
75371
+ const executingIds = this.options.getExecutingTaskIds?.() ?? /* @__PURE__ */ new Set();
75372
+ const tasks = await this.store.listTasks({ column: "in-review", slim: true });
75373
+ const ghosts = tasks.filter(
75374
+ (task) => task.column === "in-review" && !task.paused && !executingIds.has(task.id) && !(task.status && GHOST_REVIEW_PRESERVED_STATUSES.has(task.status)) && // Confirmed merges belong in `done` (handled by `recoverMergedReviewTasks`).
75375
+ task.mergeDetails?.mergeConfirmed !== true && now - new Date(task.updatedAt).getTime() >= timeoutMs
75376
+ );
75377
+ if (ghosts.length === 0) return 0;
75378
+ log16.warn(`Found ${ghosts.length} ghost in-review task(s) \u2014 kicking back to todo`);
75379
+ let recovered = 0;
75380
+ for (const task of ghosts) {
75381
+ try {
75382
+ if (task.status) {
75383
+ await this.store.updateTask(task.id, { status: null, error: null });
75384
+ }
75385
+ await this.store.logEntry(
75386
+ task.id,
75387
+ "Auto-recovered: in-review task idle past stuck-task timeout \u2014 kicked back to todo"
75388
+ );
75389
+ await this.store.moveTask(task.id, "todo");
75390
+ log16.log(`Kicked ghost review task ${task.id} back to todo`);
75391
+ recovered++;
75392
+ } catch (err) {
75393
+ const errorMessage = err instanceof Error ? err.message : String(err);
75394
+ log16.error(`Failed to kick ghost review task ${task.id}: ${errorMessage}`);
75395
+ }
75396
+ }
75397
+ return recovered;
75398
+ } catch (err) {
75399
+ const errorMessage = err instanceof Error ? err.message : String(err);
75400
+ log16.error(`Ghost review recovery failed: ${errorMessage}`);
73622
75401
  return 0;
73623
75402
  }
73624
75403
  }
@@ -73646,7 +75425,7 @@ var init_self_healing = __esm({
73646
75425
  (task) => task.column === "in-review" && !task.paused && Boolean(task.status && ACTIVE_MERGE_STATUSES.has(task.status)) && this.isPastInterruptedMergeGrace(task, timeoutMs)
73647
75426
  );
73648
75427
  if (candidates.length === 0) return 0;
73649
- log8.warn(`Found ${candidates.length} stale merging task(s) in in-review`);
75428
+ log16.warn(`Found ${candidates.length} stale merging task(s) in in-review`);
73650
75429
  let recovered = 0;
73651
75430
  for (const task of candidates) {
73652
75431
  try {
@@ -73674,7 +75453,7 @@ var init_self_healing = __esm({
73674
75453
  task.id,
73675
75454
  `Auto-recovered: stale merge status finalized from landed commit ${landedCommit.sha.slice(0, 8)}`
73676
75455
  );
73677
- log8.log(`Recovered interrupted merge ${task.id}: finalized landed commit ${landedCommit.sha.slice(0, 8)}`);
75456
+ log16.log(`Recovered interrupted merge ${task.id}: finalized landed commit ${landedCommit.sha.slice(0, 8)}`);
73678
75457
  recovered++;
73679
75458
  continue;
73680
75459
  }
@@ -73683,27 +75462,27 @@ var init_self_healing = __esm({
73683
75462
  task.id,
73684
75463
  "Auto-recovered: stale merge status cleared; merge will be retried"
73685
75464
  );
73686
- log8.log(`Recovered interrupted merge ${task.id}: cleared stale status for retry`);
75465
+ log16.log(`Recovered interrupted merge ${task.id}: cleared stale status for retry`);
73687
75466
  try {
73688
75467
  this.options.enqueueMerge?.(task.id);
73689
75468
  } catch (enqueueErr) {
73690
- log8.warn(
75469
+ log16.warn(
73691
75470
  `Failed to re-enqueue ${task.id} after stale-merge recovery (will rely on polling sweep): ${enqueueErr instanceof Error ? enqueueErr.message : String(enqueueErr)}`
73692
75471
  );
73693
75472
  }
73694
75473
  recovered++;
73695
75474
  } catch (err) {
73696
75475
  const errorMessage = err instanceof Error ? err.message : String(err);
73697
- log8.error(`Failed to recover interrupted merge ${task.id}: ${errorMessage}`);
75476
+ log16.error(`Failed to recover interrupted merge ${task.id}: ${errorMessage}`);
73698
75477
  }
73699
75478
  }
73700
75479
  if (recovered > 0) {
73701
- log8.log(`Recovered ${recovered} interrupted merge task(s)`);
75480
+ log16.log(`Recovered ${recovered} interrupted merge task(s)`);
73702
75481
  }
73703
75482
  return recovered;
73704
75483
  } catch (err) {
73705
75484
  const errorMessage = err instanceof Error ? err.message : String(err);
73706
- log8.error(`Interrupted merge recovery failed: ${errorMessage}`);
75485
+ log16.error(`Interrupted merge recovery failed: ${errorMessage}`);
73707
75486
  return 0;
73708
75487
  }
73709
75488
  }
@@ -73724,7 +75503,7 @@ var init_self_healing = __esm({
73724
75503
  (t) => t.column === "in-review" && !t.paused && t.mergeDetails?.mergeConfirmed === true
73725
75504
  );
73726
75505
  if (mergedButNotDone.length === 0) return 0;
73727
- log8.warn(`Found ${mergedButNotDone.length} merged task(s) stuck in in-review`);
75506
+ log16.warn(`Found ${mergedButNotDone.length} merged task(s) stuck in in-review`);
73728
75507
  let recovered = 0;
73729
75508
  for (const task of mergedButNotDone) {
73730
75509
  try {
@@ -73738,20 +75517,20 @@ var init_self_healing = __esm({
73738
75517
  task.id,
73739
75518
  "Auto-recovered: merge already confirmed \u2014 moved from in-review to done"
73740
75519
  );
73741
- log8.log(`Recovered merged task ${task.id}: moved to done`);
75520
+ log16.log(`Recovered merged task ${task.id}: moved to done`);
73742
75521
  recovered++;
73743
75522
  } catch (err) {
73744
75523
  const errorMessage = err instanceof Error ? err.message : String(err);
73745
- log8.error(`Failed to recover merged task ${task.id}: ${errorMessage}`);
75524
+ log16.error(`Failed to recover merged task ${task.id}: ${errorMessage}`);
73746
75525
  }
73747
75526
  }
73748
75527
  if (recovered > 0) {
73749
- log8.log(`Recovered ${recovered} merged task(s) \u2192 done`);
75528
+ log16.log(`Recovered ${recovered} merged task(s) \u2192 done`);
73750
75529
  }
73751
75530
  return recovered;
73752
75531
  } catch (err) {
73753
75532
  const errorMessage = err instanceof Error ? err.message : String(err);
73754
- log8.error(`Merged review recovery failed: ${errorMessage}`);
75533
+ log16.error(`Merged review recovery failed: ${errorMessage}`);
73755
75534
  return 0;
73756
75535
  }
73757
75536
  }
@@ -73772,7 +75551,7 @@ var init_self_healing = __esm({
73772
75551
  (t) => t.column === "in-review" && !t.paused && t.status === "failed" && t.error?.includes("without calling task_done") && t.steps.length > 0 && t.steps.every((s) => s.status === "done" || s.status === "skipped")
73773
75552
  );
73774
75553
  if (misclassified.length === 0) return 0;
73775
- log8.warn(`Found ${misclassified.length} misclassified failure(s) with all steps done`);
75554
+ log16.warn(`Found ${misclassified.length} misclassified failure(s) with all steps done`);
73776
75555
  let recovered = 0;
73777
75556
  for (const task of misclassified) {
73778
75557
  try {
@@ -73784,20 +75563,20 @@ var init_self_healing = __esm({
73784
75563
  task.id,
73785
75564
  "Auto-recovered: all steps complete despite 'no task_done' failure \u2014 cleared error for normal review"
73786
75565
  );
73787
- log8.log(`Recovered misclassified failure ${task.id}: ${task.title || task.description?.slice(0, 60) || "(untitled)"}`);
75566
+ log16.log(`Recovered misclassified failure ${task.id}: ${task.title || task.description?.slice(0, 60) || "(untitled)"}`);
73788
75567
  recovered++;
73789
75568
  } catch (err) {
73790
75569
  const errorMessage = err instanceof Error ? err.message : String(err);
73791
- log8.error(`Failed to recover misclassified failure ${task.id}: ${errorMessage}`);
75570
+ log16.error(`Failed to recover misclassified failure ${task.id}: ${errorMessage}`);
73792
75571
  }
73793
75572
  }
73794
75573
  if (recovered > 0) {
73795
- log8.log(`Recovered ${recovered} misclassified failure(s) \u2192 cleared for review`);
75574
+ log16.log(`Recovered ${recovered} misclassified failure(s) \u2192 cleared for review`);
73796
75575
  }
73797
75576
  return recovered;
73798
75577
  } catch (err) {
73799
75578
  const errorMessage = err instanceof Error ? err.message : String(err);
73800
- log8.error(`Misclassified failure recovery failed: ${errorMessage}`);
75579
+ log16.error(`Misclassified failure recovery failed: ${errorMessage}`);
73801
75580
  return 0;
73802
75581
  }
73803
75582
  }
@@ -73821,7 +75600,7 @@ var init_self_healing = __esm({
73821
75600
  return staleness >= graceMs;
73822
75601
  });
73823
75602
  if (orphaned.length === 0) return 0;
73824
- log8.warn(`Found ${orphaned.length} orphaned executor task(s) stuck in in-progress`);
75603
+ log16.warn(`Found ${orphaned.length} orphaned executor task(s) stuck in in-progress`);
73825
75604
  let recovered = 0;
73826
75605
  for (const task of orphaned) {
73827
75606
  try {
@@ -73841,16 +75620,16 @@ var init_self_healing = __esm({
73841
75620
  recovered++;
73842
75621
  } catch (err) {
73843
75622
  const errorMessage = err instanceof Error ? err.message : String(err);
73844
- log8.error(`Failed to recover orphaned executor task ${task.id}: ${errorMessage}`);
75623
+ log16.error(`Failed to recover orphaned executor task ${task.id}: ${errorMessage}`);
73845
75624
  }
73846
75625
  }
73847
75626
  if (recovered > 0) {
73848
- log8.log(`Recovered ${recovered} orphaned executor task(s) \u2192 todo`);
75627
+ log16.log(`Recovered ${recovered} orphaned executor task(s) \u2192 todo`);
73849
75628
  }
73850
75629
  return recovered;
73851
75630
  } catch (err) {
73852
75631
  const errorMessage = err instanceof Error ? err.message : String(err);
73853
- log8.error(`Orphaned executor recovery failed: ${errorMessage}`);
75632
+ log16.error(`Orphaned executor recovery failed: ${errorMessage}`);
73854
75633
  return 0;
73855
75634
  }
73856
75635
  }
@@ -73871,12 +75650,12 @@ var init_self_healing = __esm({
73871
75650
  (task) => task.column === "in-progress" && task.status === "failed" && isNoTaskDoneFailure(task) && !task.paused && !executingIds.has(task.id) && !isTaskWorkComplete(task) && !hasStepProgress(task)
73872
75651
  );
73873
75652
  if (candidates.length === 0) return 0;
73874
- log8.warn(`Found ${candidates.length} no-progress no-task_done failure(s) in in-progress`);
75653
+ log16.warn(`Found ${candidates.length} no-progress no-task_done failure(s) in in-progress`);
73875
75654
  let recovered = 0;
73876
75655
  for (const task of candidates) {
73877
75656
  try {
73878
75657
  if (await this.hasRecoverableGitWork(task)) {
73879
- log8.log(`${task.id} has recoverable git work \u2014 leaving in-progress for inspection`);
75658
+ log16.log(`${task.id} has recoverable git work \u2014 leaving in-progress for inspection`);
73880
75659
  continue;
73881
75660
  }
73882
75661
  await this.store.updateTask(task.id, {
@@ -73892,16 +75671,16 @@ var init_self_healing = __esm({
73892
75671
  recovered++;
73893
75672
  } catch (err) {
73894
75673
  const errorMessage = err instanceof Error ? err.message : String(err);
73895
- log8.error(`Failed to recover no-progress no-task_done failure ${task.id}: ${errorMessage}`);
75674
+ log16.error(`Failed to recover no-progress no-task_done failure ${task.id}: ${errorMessage}`);
73896
75675
  }
73897
75676
  }
73898
75677
  if (recovered > 0) {
73899
- log8.log(`Recovered ${recovered} no-progress no-task_done failure(s) \u2192 todo`);
75678
+ log16.log(`Recovered ${recovered} no-progress no-task_done failure(s) \u2192 todo`);
73900
75679
  }
73901
75680
  return recovered;
73902
75681
  } catch (err) {
73903
75682
  const errorMessage = err instanceof Error ? err.message : String(err);
73904
- log8.error(`No-progress no-task_done recovery failed: ${errorMessage}`);
75683
+ log16.error(`No-progress no-task_done recovery failed: ${errorMessage}`);
73905
75684
  return 0;
73906
75685
  }
73907
75686
  }
@@ -73932,7 +75711,7 @@ var init_self_healing = __esm({
73932
75711
  (task) => task.column === "in-review" && task.status === "failed" && isNoTaskDoneFailure(task) && !task.paused && !isTaskWorkComplete(task) && hasStepProgress(task) && (task.taskDoneRetryCount ?? 0) < MAX_TASK_DONE_RETRIES
73933
75712
  );
73934
75713
  if (candidates.length === 0) return 0;
73935
- log8.warn(
75714
+ log16.warn(
73936
75715
  `Found ${candidates.length} partial-progress no-task_done failure(s) eligible for auto-retry`
73937
75716
  );
73938
75717
  let recovered = 0;
@@ -73953,20 +75732,20 @@ var init_self_healing = __esm({
73953
75732
  recovered++;
73954
75733
  } catch (err) {
73955
75734
  const errorMessage = err instanceof Error ? err.message : String(err);
73956
- log8.error(
75735
+ log16.error(
73957
75736
  `Failed to auto-retry partial-progress no-task_done failure ${task.id}: ${errorMessage}`
73958
75737
  );
73959
75738
  }
73960
75739
  }
73961
75740
  if (recovered > 0) {
73962
- log8.log(
75741
+ log16.log(
73963
75742
  `Auto-retried ${recovered} partial-progress no-task_done failure(s) \u2192 todo`
73964
75743
  );
73965
75744
  }
73966
75745
  return recovered;
73967
75746
  } catch (err) {
73968
75747
  const errorMessage = err instanceof Error ? err.message : String(err);
73969
- log8.error(`Partial-progress no-task_done recovery failed: ${errorMessage}`);
75748
+ log16.error(`Partial-progress no-task_done recovery failed: ${errorMessage}`);
73970
75749
  return 0;
73971
75750
  }
73972
75751
  }
@@ -73980,7 +75759,7 @@ var init_self_healing = __esm({
73980
75759
  if (status.trim().length > 0) return true;
73981
75760
  } catch (err) {
73982
75761
  const errorMessage = err instanceof Error ? err.message : String(err);
73983
- log8.warn(
75762
+ log16.warn(
73984
75763
  `Failed to inspect worktree status for ${task.id} at ${task.worktree}: ${errorMessage} \u2014 preserving worktree`
73985
75764
  );
73986
75765
  return true;
@@ -74003,7 +75782,7 @@ var init_self_healing = __esm({
74003
75782
  return Number.parseInt(uniqueCommits.trim(), 10) > 0;
74004
75783
  } catch (err) {
74005
75784
  const errorMessage = err instanceof Error ? err.message : String(err);
74006
- log8.warn(
75785
+ log16.warn(
74007
75786
  `Failed to compare branch ${branchName} against HEAD for ${task.id}: ${errorMessage} \u2014 preserving branch`
74008
75787
  );
74009
75788
  return true;
@@ -74028,20 +75807,20 @@ var init_self_healing = __esm({
74028
75807
  (t) => t.column === "triage" && t.status === "planning" && !t.paused && !planningIds.has(t.id) && now - new Date(t.updatedAt).getTime() >= APPROVED_TRIAGE_RECOVERY_GRACE_MS && hasLatestSpecReviewApproval2(t)
74029
75808
  );
74030
75809
  if (orphanedApproved.length === 0) return 0;
74031
- log8.warn(`Found ${orphanedApproved.length} approved triage task(s) stuck in planning`);
75810
+ log16.warn(`Found ${orphanedApproved.length} approved triage task(s) stuck in planning`);
74032
75811
  let recovered = 0;
74033
75812
  for (const task of orphanedApproved) {
74034
- log8.log(`Recovering approved triage task ${task.id}: ${task.title || task.description?.slice(0, 60) || "(untitled)"}`);
75813
+ log16.log(`Recovering approved triage task ${task.id}: ${task.title || task.description?.slice(0, 60) || "(untitled)"}`);
74035
75814
  const success = await recoverFn(task);
74036
75815
  if (success) recovered++;
74037
75816
  }
74038
75817
  if (recovered > 0) {
74039
- log8.log(`Recovered ${recovered} approved triage task(s) out of planning`);
75818
+ log16.log(`Recovered ${recovered} approved triage task(s) out of planning`);
74040
75819
  }
74041
75820
  return recovered;
74042
75821
  } catch (err) {
74043
75822
  const errorMessage = err instanceof Error ? err.message : String(err);
74044
- log8.error(`Approved triage recovery failed: ${errorMessage}`);
75823
+ log16.error(`Approved triage recovery failed: ${errorMessage}`);
74045
75824
  return 0;
74046
75825
  }
74047
75826
  }
@@ -74067,11 +75846,11 @@ var init_self_healing = __esm({
74067
75846
  (t) => t.column === "triage" && t.status === "planning" && !t.paused && !planningIds.has(t.id) && now - new Date(t.updatedAt).getTime() >= APPROVED_TRIAGE_RECOVERY_GRACE_MS && !hasLatestSpecReviewApproval2(t)
74068
75847
  );
74069
75848
  if (orphaned.length === 0) return 0;
74070
- log8.warn(`Found ${orphaned.length} orphaned planning triage task(s) without approval`);
75849
+ log16.warn(`Found ${orphaned.length} orphaned planning triage task(s) without approval`);
74071
75850
  let recovered = 0;
74072
75851
  for (const task of orphaned) {
74073
75852
  try {
74074
- log8.log(`Recovering orphaned planning task ${task.id}: ${task.title || task.description?.slice(0, 60) || "(untitled)"}`);
75853
+ log16.log(`Recovering orphaned planning task ${task.id}: ${task.title || task.description?.slice(0, 60) || "(untitled)"}`);
74075
75854
  await this.store.updateTask(task.id, { status: null });
74076
75855
  await this.store.logEntry(
74077
75856
  task.id,
@@ -74080,16 +75859,16 @@ var init_self_healing = __esm({
74080
75859
  recovered++;
74081
75860
  } catch (err) {
74082
75861
  const errorMessage = err instanceof Error ? err.message : String(err);
74083
- log8.error(`Failed to recover orphaned planning task ${task.id}: ${errorMessage}`);
75862
+ log16.error(`Failed to recover orphaned planning task ${task.id}: ${errorMessage}`);
74084
75863
  }
74085
75864
  }
74086
75865
  if (recovered > 0) {
74087
- log8.log(`Recovered ${recovered} orphaned planning task(s) \u2014 cleared for re-planning`);
75866
+ log16.log(`Recovered ${recovered} orphaned planning task(s) \u2014 cleared for re-planning`);
74088
75867
  }
74089
75868
  return recovered;
74090
75869
  } catch (err) {
74091
75870
  const errorMessage = err instanceof Error ? err.message : String(err);
74092
- log8.error(`Orphaned planning task recovery failed: ${errorMessage}`);
75871
+ log16.error(`Orphaned planning task recovery failed: ${errorMessage}`);
74093
75872
  return 0;
74094
75873
  }
74095
75874
  }
@@ -74100,10 +75879,10 @@ var init_self_healing = __esm({
74100
75879
  cwd: this.options.rootDir,
74101
75880
  timeout: 3e4
74102
75881
  });
74103
- log8.log("Worktree prune completed");
75882
+ log16.log("Worktree prune completed");
74104
75883
  } catch (err) {
74105
75884
  const errorMessage = err instanceof Error ? err.message : String(err);
74106
- log8.error(`Worktree prune failed: ${errorMessage}`);
75885
+ log16.error(`Worktree prune failed: ${errorMessage}`);
74107
75886
  }
74108
75887
  }
74109
75888
  /**
@@ -74136,16 +75915,16 @@ var init_self_healing = __esm({
74136
75915
  cleaned++;
74137
75916
  } catch (err) {
74138
75917
  const errorMessage = err instanceof Error ? err.message : String(err);
74139
- log8.warn(`Failed to remove orphaned worktree ${worktreePath}: ${errorMessage} \u2014 non-fatal`);
75918
+ log16.warn(`Failed to remove orphaned worktree ${worktreePath}: ${errorMessage} \u2014 non-fatal`);
74140
75919
  }
74141
75920
  }
74142
75921
  if (cleaned > 0) {
74143
- log8.log(`Cleaned ${cleaned} orphaned worktree(s)`);
75922
+ log16.log(`Cleaned ${cleaned} orphaned worktree(s)`);
74144
75923
  }
74145
75924
  return cleaned;
74146
75925
  } catch (err) {
74147
75926
  const errorMessage = err instanceof Error ? err.message : String(err);
74148
- log8.error(`Orphan cleanup failed: ${errorMessage}`);
75927
+ log16.error(`Orphan cleanup failed: ${errorMessage}`);
74149
75928
  return 0;
74150
75929
  }
74151
75930
  }
@@ -74156,35 +75935,35 @@ var init_self_healing = __esm({
74156
75935
  * tracks registered idle worktrees, never these orphans.
74157
75936
  */
74158
75937
  async reapUnregisteredOrphans() {
74159
- const worktreesDir = join35(this.options.rootDir, ".worktrees");
75938
+ const worktreesDir = join36(this.options.rootDir, ".worktrees");
74160
75939
  if (!existsSync28(worktreesDir)) return 0;
74161
75940
  let dirs;
74162
75941
  try {
74163
- dirs = readdirSync5(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join35(worktreesDir, e.name));
75942
+ dirs = readdirSync5(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join36(worktreesDir, e.name));
74164
75943
  } catch (err) {
74165
- log8.warn(`Failed to read .worktrees/ for unregistered orphan reap: ${err instanceof Error ? err.message : String(err)}`);
75944
+ log16.warn(`Failed to read .worktrees/ for unregistered orphan reap: ${err instanceof Error ? err.message : String(err)}`);
74166
75945
  return 0;
74167
75946
  }
74168
75947
  if (dirs.length === 0) return 0;
74169
75948
  const registered = await getRegisteredWorktreePaths(this.options.rootDir);
74170
- const unregistered = dirs.filter((d) => !registered.has(resolve15(d)));
75949
+ const unregistered = dirs.filter((d) => !registered.has(resolve16(d)));
74171
75950
  let cleaned = 0;
74172
75951
  for (const path2 of unregistered) {
74173
- const rel = relative7(worktreesDir, path2);
75952
+ const rel = relative8(worktreesDir, path2);
74174
75953
  if (rel === "" || rel.startsWith("..") || isAbsolute12(rel)) {
74175
- log8.warn(`Refusing to remove path outside .worktrees: ${path2}`);
75954
+ log16.warn(`Refusing to remove path outside .worktrees: ${path2}`);
74176
75955
  continue;
74177
75956
  }
74178
75957
  try {
74179
75958
  rmSync3(path2, { recursive: true, force: true });
74180
- log8.log(`Cleaned unregistered worktree dir: ${path2}`);
75959
+ log16.log(`Cleaned unregistered worktree dir: ${path2}`);
74181
75960
  cleaned++;
74182
75961
  } catch (err) {
74183
- log8.warn(`Failed to remove unregistered worktree dir ${path2}: ${err instanceof Error ? err.message : String(err)}`);
75962
+ log16.warn(`Failed to remove unregistered worktree dir ${path2}: ${err instanceof Error ? err.message : String(err)}`);
74184
75963
  }
74185
75964
  }
74186
75965
  if (cleaned > 0) {
74187
- log8.log(`Cleaned ${cleaned} unregistered worktree dir(s) (recycle mode preserves registered idle worktrees)`);
75966
+ log16.log(`Cleaned ${cleaned} unregistered worktree dir(s) (recycle mode preserves registered idle worktrees)`);
74188
75967
  }
74189
75968
  return cleaned;
74190
75969
  }
@@ -74213,12 +75992,12 @@ var init_self_healing = __esm({
74213
75992
  cwd: this.options.rootDir,
74214
75993
  timeout: 3e4
74215
75994
  });
74216
- log8.log(`Deleted branch: ${branch}`);
75995
+ log16.log(`Deleted branch: ${branch}`);
74217
75996
  cleaned++;
74218
75997
  deletedBranches.push(branch);
74219
75998
  } catch (err) {
74220
75999
  const errorMessage = err instanceof Error ? err.message : String(err);
74221
- log8.warn(
76000
+ log16.warn(
74222
76001
  `Safe delete failed for orphaned branch ${branch}: ${errorMessage} \u2014 attempting force delete`
74223
76002
  );
74224
76003
  try {
@@ -74226,28 +76005,28 @@ var init_self_healing = __esm({
74226
76005
  cwd: this.options.rootDir,
74227
76006
  timeout: 3e4
74228
76007
  });
74229
- log8.log(`Force-deleted branch: ${branch}`);
76008
+ log16.log(`Force-deleted branch: ${branch}`);
74230
76009
  cleaned++;
74231
76010
  deletedBranches.push(branch);
74232
76011
  } catch (forceErr) {
74233
76012
  const forceErrorMessage = forceErr instanceof Error ? forceErr.message : String(forceErr);
74234
- log8.warn(`Failed to force-delete orphaned branch ${branch}: ${forceErrorMessage} \u2014 non-fatal`);
76013
+ log16.warn(`Failed to force-delete orphaned branch ${branch}: ${forceErrorMessage} \u2014 non-fatal`);
74235
76014
  }
74236
76015
  }
74237
76016
  }
74238
76017
  if (deletedBranches.length > 0) {
74239
76018
  const cleared = this.store.clearStaleBaseBranchReferences(deletedBranches);
74240
76019
  if (cleared.length > 0) {
74241
- log8.log(`Cleared stale baseBranch on ${cleared.length} task(s): ${cleared.join(", ")}`);
76020
+ log16.log(`Cleared stale baseBranch on ${cleared.length} task(s): ${cleared.join(", ")}`);
74242
76021
  }
74243
76022
  }
74244
76023
  if (cleaned > 0) {
74245
- log8.log(`Cleaned ${cleaned} orphaned branch(es)`);
76024
+ log16.log(`Cleaned ${cleaned} orphaned branch(es)`);
74246
76025
  }
74247
76026
  return cleaned;
74248
76027
  } catch (err) {
74249
76028
  const errorMessage = err instanceof Error ? err.message : String(err);
74250
- log8.error(`Orphaned branch cleanup failed: ${errorMessage}`);
76029
+ log16.error(`Orphaned branch cleanup failed: ${errorMessage}`);
74251
76030
  return 0;
74252
76031
  }
74253
76032
  }
@@ -74256,16 +76035,16 @@ var init_self_healing = __esm({
74256
76035
  try {
74257
76036
  const result = this.store.walCheckpoint();
74258
76037
  if (result.log > 0) {
74259
- log8.log(`WAL checkpoint: ${result.checkpointed}/${result.log} pages checkpointed` + (result.busy > 0 ? ` (${result.busy} busy)` : ""));
76038
+ log16.log(`WAL checkpoint: ${result.checkpointed}/${result.log} pages checkpointed` + (result.busy > 0 ? ` (${result.busy} busy)` : ""));
74260
76039
  }
74261
76040
  } catch (err) {
74262
76041
  const errorMessage = err instanceof Error ? err.message : String(err);
74263
- log8.error(`WAL checkpoint failed: ${errorMessage}`);
76042
+ log16.error(`WAL checkpoint failed: ${errorMessage}`);
74264
76043
  }
74265
76044
  }
74266
76045
  /** Remove oldest idle worktrees if total count exceeds 2× maxWorktrees. */
74267
76046
  async enforceWorktreeCap() {
74268
- const worktreesDir = join35(this.options.rootDir, ".worktrees");
76047
+ const worktreesDir = join36(this.options.rootDir, ".worktrees");
74269
76048
  if (!existsSync28(worktreesDir)) return;
74270
76049
  try {
74271
76050
  const settings = await this.store.getSettings();
@@ -74280,7 +76059,7 @@ var init_self_healing = __esm({
74280
76059
  return { path: p, mtime: statSync5(p).mtimeMs };
74281
76060
  } catch (err) {
74282
76061
  const errorMessage = err instanceof Error ? err.message : String(err);
74283
- log8.warn(`Failed to read mtime for worktree ${p}: ${errorMessage} \u2014 defaulting mtime to 0`);
76062
+ log16.warn(`Failed to read mtime for worktree ${p}: ${errorMessage} \u2014 defaulting mtime to 0`);
74284
76063
  return { path: p, mtime: 0 };
74285
76064
  }
74286
76065
  });
@@ -74297,15 +76076,15 @@ var init_self_healing = __esm({
74297
76076
  removed++;
74298
76077
  } catch (err) {
74299
76078
  const errorMessage = err instanceof Error ? err.message : String(err);
74300
- log8.warn(`Failed to remove idle worktree ${worktreePath} during cap enforcement: ${errorMessage} \u2014 non-fatal`);
76079
+ log16.warn(`Failed to remove idle worktree ${worktreePath} during cap enforcement: ${errorMessage} \u2014 non-fatal`);
74301
76080
  }
74302
76081
  }
74303
76082
  if (removed > 0) {
74304
- log8.warn(`Worktree cap: removed ${removed} idle worktree(s) (was ${dirs.length}, cap ${cap})`);
76083
+ log16.warn(`Worktree cap: removed ${removed} idle worktree(s) (was ${dirs.length}, cap ${cap})`);
74305
76084
  }
74306
76085
  } catch (err) {
74307
76086
  const errorMessage = err instanceof Error ? err.message : String(err);
74308
- log8.error(`Worktree cap enforcement failed: ${errorMessage}`);
76087
+ log16.error(`Worktree cap enforcement failed: ${errorMessage}`);
74309
76088
  }
74310
76089
  }
74311
76090
  };
@@ -74804,13 +76583,13 @@ var init_plugin_runner = __esm({
74804
76583
  * Returns the result on success, throws on timeout.
74805
76584
  */
74806
76585
  withTimeout(promise, ms, timeoutMessage) {
74807
- return new Promise((resolve18, reject) => {
76586
+ return new Promise((resolve19, reject) => {
74808
76587
  const timer = setTimeout(() => {
74809
76588
  reject(new Error(timeoutMessage));
74810
76589
  }, ms);
74811
76590
  promise.then((result) => {
74812
76591
  clearTimeout(timer);
74813
- resolve18(result);
76592
+ resolve19(result);
74814
76593
  }).catch((err) => {
74815
76594
  clearTimeout(timer);
74816
76595
  reject(err);
@@ -75275,7 +77054,7 @@ var init_in_process_runtime = __esm({
75275
77054
  try {
75276
77055
  const { RoutineStore: RoutineStoreClass } = await Promise.resolve().then(() => (init_src(), src_exports));
75277
77056
  if (typeof RoutineStoreClass.prototype.getDueRoutines === "function") {
75278
- const routineStore = new RoutineStoreClass(this.taskStore.getFusionDir());
77057
+ const routineStore = new RoutineStoreClass(this.config.workingDirectory);
75279
77058
  await routineStore.init();
75280
77059
  this.routineStore = routineStore;
75281
77060
  if (this.heartbeatMonitor) {
@@ -75446,7 +77225,7 @@ var init_in_process_runtime = __esm({
75446
77225
  runtimeLog.log(
75447
77226
  `Waiting for ${metrics.inFlightTasks} in-flight tasks to complete...`
75448
77227
  );
75449
- await new Promise((resolve18) => setTimeout(resolve18, 1e3));
77228
+ await new Promise((resolve19) => setTimeout(resolve19, 1e3));
75450
77229
  }
75451
77230
  const finalMetrics = this.getMetrics();
75452
77231
  if (finalMetrics.inFlightTasks > 0) {
@@ -75843,13 +77622,13 @@ var init_ipc_host = __esm({
75843
77622
  }
75844
77623
  const id = generateCorrelationId();
75845
77624
  const message = { type, id, payload };
75846
- return new Promise((resolve18, reject) => {
77625
+ return new Promise((resolve19, reject) => {
75847
77626
  const timeout = setTimeout(() => {
75848
77627
  this.pendingCommands.delete(id);
75849
77628
  reject(new Error(`Command ${type} timed out after ${timeoutMs ?? this.commandTimeoutMs}ms`));
75850
77629
  }, timeoutMs ?? this.commandTimeoutMs);
75851
77630
  this.pendingCommands.set(id, {
75852
- resolve: resolve18,
77631
+ resolve: resolve19,
75853
77632
  reject,
75854
77633
  timeout,
75855
77634
  type
@@ -75927,7 +77706,7 @@ var init_ipc_host = __esm({
75927
77706
  import { EventEmitter as EventEmitter20 } from "node:events";
75928
77707
  import { fork } from "node:child_process";
75929
77708
  import { fileURLToPath as fileURLToPath3 } from "node:url";
75930
- import { dirname as dirname10, join as join36 } from "node:path";
77709
+ import { dirname as dirname10, join as join37 } from "node:path";
75931
77710
  var HealthMonitor, ChildProcessRuntime;
75932
77711
  var init_child_process_runtime = __esm({
75933
77712
  "../engine/src/runtimes/child-process-runtime.ts"() {
@@ -76089,7 +77868,7 @@ var init_child_process_runtime = __esm({
76089
77868
  const isCompiled = !import.meta.url.endsWith(".ts");
76090
77869
  const currentDir = dirname10(fileURLToPath3(import.meta.url));
76091
77870
  const workerFile = isCompiled ? "child-process-worker.js" : "child-process-worker.ts";
76092
- return join36(currentDir, workerFile);
77871
+ return join37(currentDir, workerFile);
76093
77872
  }
76094
77873
  /**
76095
77874
  * Set up event forwarding from IPC host to runtime listeners.
@@ -76320,7 +78099,7 @@ var init_child_process_runtime = __esm({
76320
78099
  });
76321
78100
 
76322
78101
  // ../engine/src/runtimes/remote-node-client.ts
76323
- var RemoteNodeRequestError, RETRY_BASE_DELAY_MS2, DEFAULT_MAX_RETRIES, RemoteNodeClient;
78102
+ var RemoteNodeRequestError, RETRY_BASE_DELAY_MS3, DEFAULT_MAX_RETRIES, RemoteNodeClient;
76324
78103
  var init_remote_node_client = __esm({
76325
78104
  "../engine/src/runtimes/remote-node-client.ts"() {
76326
78105
  "use strict";
@@ -76333,7 +78112,7 @@ var init_remote_node_client = __esm({
76333
78112
  this.name = "RemoteNodeRequestError";
76334
78113
  }
76335
78114
  };
76336
- RETRY_BASE_DELAY_MS2 = 1e3;
78115
+ RETRY_BASE_DELAY_MS3 = 1e3;
76337
78116
  DEFAULT_MAX_RETRIES = 3;
76338
78117
  RemoteNodeClient = class {
76339
78118
  baseUrl;
@@ -76638,7 +78417,7 @@ var init_remote_node_client = __esm({
76638
78417
  if (!isRetryable || attempt >= maxRetries) {
76639
78418
  throw error;
76640
78419
  }
76641
- const delayMs = RETRY_BASE_DELAY_MS2 * 2 ** attempt;
78420
+ const delayMs = RETRY_BASE_DELAY_MS3 * 2 ** attempt;
76642
78421
  attempt += 1;
76643
78422
  remoteNodeLog.warn(
76644
78423
  `Request failed, retrying in ${delayMs}ms (attempt ${attempt}/${maxRetries})`,
@@ -76658,8 +78437,8 @@ var init_remote_node_client = __esm({
76658
78437
  return error instanceof TypeError;
76659
78438
  }
76660
78439
  async sleep(ms) {
76661
- await new Promise((resolve18) => {
76662
- setTimeout(resolve18, ms);
78440
+ await new Promise((resolve19) => {
78441
+ setTimeout(resolve19, ms);
76663
78442
  });
76664
78443
  }
76665
78444
  };
@@ -76923,14 +78702,14 @@ var init_remote_node_runtime = __esm({
76923
78702
  return error instanceof Error ? error : new Error(String(error));
76924
78703
  }
76925
78704
  async sleep(ms, signal) {
76926
- await new Promise((resolve18) => {
78705
+ await new Promise((resolve19) => {
76927
78706
  const timeout = setTimeout(() => {
76928
78707
  cleanup();
76929
- resolve18();
78708
+ resolve19();
76930
78709
  }, ms);
76931
78710
  const onAbort = () => {
76932
78711
  cleanup();
76933
- resolve18();
78712
+ resolve19();
76934
78713
  };
76935
78714
  const cleanup = () => {
76936
78715
  clearTimeout(timeout);
@@ -77271,11 +79050,13 @@ var init_gridlock_detector = __esm({
77271
79050
  this.pollIntervalMs = options.pollIntervalMs ?? 3e4;
77272
79051
  this.missionStore = options.missionStore;
77273
79052
  this.onGridlock = options.onGridlock;
79053
+ this.onGridlockCleared = options.onGridlockCleared;
77274
79054
  }
77275
79055
  interval = null;
77276
79056
  pollIntervalMs;
77277
79057
  missionStore;
77278
79058
  onGridlock;
79059
+ onGridlockCleared;
77279
79060
  lastGridlockKey = null;
77280
79061
  start() {
77281
79062
  if (this.interval) return;
@@ -77305,12 +79086,12 @@ var init_gridlock_detector = __esm({
77305
79086
  return true;
77306
79087
  });
77307
79088
  if (schedulable.length === 0) {
77308
- this.lastGridlockKey = null;
79089
+ this.clearGridlockState();
77309
79090
  return null;
77310
79091
  }
77311
79092
  const active = tasks.filter((task) => task.column === "in-progress" || task.column === "in-review" && Boolean(task.worktree));
77312
79093
  if (active.length === 0) {
77313
- this.lastGridlockKey = null;
79094
+ this.clearGridlockState();
77314
79095
  return null;
77315
79096
  }
77316
79097
  const overlapIgnorePaths = settings.overlapIgnorePaths ?? [];
@@ -77348,7 +79129,7 @@ var init_gridlock_detector = __esm({
77348
79129
  }
77349
79130
  const blockedTaskIds = Object.keys(reasons).sort();
77350
79131
  if (blockedTaskIds.length !== schedulable.length) {
77351
- this.lastGridlockKey = null;
79132
+ this.clearGridlockState();
77352
79133
  return null;
77353
79134
  }
77354
79135
  const gridlockKey = blockedTaskIds.join(",");
@@ -77365,6 +79146,12 @@ var init_gridlock_detector = __esm({
77365
79146
  }
77366
79147
  return event;
77367
79148
  }
79149
+ clearGridlockState() {
79150
+ if (this.lastGridlockKey !== null) {
79151
+ this.lastGridlockKey = null;
79152
+ this.onGridlockCleared?.();
79153
+ }
79154
+ }
77368
79155
  isMissionBlocked(task) {
77369
79156
  if (!this.missionStore || !task.sliceId) return false;
77370
79157
  try {
@@ -77825,10 +79612,10 @@ var init_tunnel_process_manager = __esm({
77825
79612
  lastError: null
77826
79613
  });
77827
79614
  this.emitLog("info", "manager", `Stopping ${currentHandle.provider} tunnel (pid=${currentHandle.child.pid ?? "n/a"})`);
77828
- this.activeStopPromise = new Promise((resolve18) => {
79615
+ this.activeStopPromise = new Promise((resolve19) => {
77829
79616
  const onClose = () => {
77830
79617
  currentHandle.child.removeListener("close", onClose);
77831
- resolve18();
79618
+ resolve19();
77832
79619
  };
77833
79620
  currentHandle.child.once("close", onClose);
77834
79621
  killManagedProcess(currentHandle.child, "SIGTERM");
@@ -77979,6 +79766,8 @@ var init_project_engine = __esm({
77979
79766
  init_merger();
77980
79767
  init_concurrency();
77981
79768
  init_logger2();
79769
+ init_research_orchestrator();
79770
+ init_research_step_runner();
77982
79771
  init_tunnel_process_manager();
77983
79772
  execFileAsync = promisify10(execFile3);
77984
79773
  MERGE_HANDOFF_GRACE_MS = 300;
@@ -78009,6 +79798,7 @@ var init_project_engine = __esm({
78009
79798
  gridlockDetector;
78010
79799
  cronRunner;
78011
79800
  automationStore;
79801
+ researchOrchestrator;
78012
79802
  remoteTunnelManager;
78013
79803
  remoteTunnelRestoreDiagnostics = {
78014
79804
  outcome: "skipped",
@@ -78063,6 +79853,14 @@ var init_project_engine = __esm({
78063
79853
  await this.runtime.start();
78064
79854
  const store = this.runtime.getTaskStore();
78065
79855
  const cwd = this.config.workingDirectory;
79856
+ const settings = await store.getSettings();
79857
+ if (typeof store.getResearchStore === "function") {
79858
+ this.researchOrchestrator = new ResearchOrchestrator({
79859
+ store: store.getResearchStore(),
79860
+ stepRunner: new ResearchStepRunner(),
79861
+ maxConcurrentRuns: settings.researchMaxConcurrentRuns ?? 3
79862
+ });
79863
+ }
78066
79864
  this.remoteTunnelManager = new TunnelProcessManager();
78067
79865
  try {
78068
79866
  await this.restoreRemoteTunnelIfNeeded(store);
@@ -78093,7 +79891,8 @@ var init_project_engine = __esm({
78093
79891
  await this.notifier.start();
78094
79892
  }
78095
79893
  this.gridlockDetector = new GridlockDetector(store, {
78096
- onGridlock: (event) => this.notifier?.notifyGridlock(event)
79894
+ onGridlock: (event) => this.notifier?.notifyGridlock(event),
79895
+ onGridlockCleared: () => this.notifier?.notifyGridlock(null)
78097
79896
  });
78098
79897
  this.gridlockDetector.start();
78099
79898
  this.setAutomationSubsystemHealth(
@@ -78112,11 +79911,11 @@ var init_project_engine = __esm({
78112
79911
  scope: "project"
78113
79912
  // Project-scoped execution — global schedules run separately
78114
79913
  });
78115
- const settings = await store.getSettings();
79914
+ const settings2 = await store.getSettings();
78116
79915
  const startupSyncFailures = [];
78117
79916
  if (typeof coreAutomationModule.syncInsightExtractionAutomation === "function") {
78118
79917
  try {
78119
- await coreAutomationModule.syncInsightExtractionAutomation(this.automationStore, settings);
79918
+ await coreAutomationModule.syncInsightExtractionAutomation(this.automationStore, settings2);
78120
79919
  } catch (err) {
78121
79920
  const { message, detail } = formatErrorDetails(err);
78122
79921
  startupSyncFailures.push(`insight extraction: ${message}`);
@@ -78128,7 +79927,7 @@ ${detail}`);
78128
79927
  }
78129
79928
  if (typeof coreAutomationModule.syncAutoSummarizeAutomation === "function") {
78130
79929
  try {
78131
- await coreAutomationModule.syncAutoSummarizeAutomation(this.automationStore, settings);
79930
+ await coreAutomationModule.syncAutoSummarizeAutomation(this.automationStore, settings2);
78132
79931
  } catch (err) {
78133
79932
  const { message, detail } = formatErrorDetails(err);
78134
79933
  startupSyncFailures.push(`auto-summarize: ${message}`);
@@ -78140,7 +79939,7 @@ ${detail}`);
78140
79939
  }
78141
79940
  if (typeof coreAutomationModule.syncMemoryDreamsAutomation === "function") {
78142
79941
  try {
78143
- await coreAutomationModule.syncMemoryDreamsAutomation(this.automationStore, settings);
79942
+ await coreAutomationModule.syncMemoryDreamsAutomation(this.automationStore, settings2);
78144
79943
  } catch (err) {
78145
79944
  const { message, detail } = formatErrorDetails(err);
78146
79945
  startupSyncFailures.push(`memory dreams: ${message}`);
@@ -78305,6 +80104,10 @@ ${detail}`
78305
80104
  getRoutineStore() {
78306
80105
  return this.runtime.getRoutineStore();
78307
80106
  }
80107
+ /** Get the ResearchOrchestrator (if initialized). Returns undefined before start(). */
80108
+ getResearchOrchestrator() {
80109
+ return this.researchOrchestrator;
80110
+ }
78308
80111
  /** Get the remote tunnel manager (available after start()). */
78309
80112
  getRemoteTunnelManager() {
78310
80113
  return this.remoteTunnelManager;
@@ -78395,12 +80198,12 @@ ${detail}`
78395
80198
  */
78396
80199
  async onMerge(taskId) {
78397
80200
  if (this.mergeActive.has(taskId)) {
78398
- return new Promise((resolve18, reject) => {
78399
- this.manualMergeResolvers.set(taskId, { resolve: resolve18, reject });
80201
+ return new Promise((resolve19, reject) => {
80202
+ this.manualMergeResolvers.set(taskId, { resolve: resolve19, reject });
78400
80203
  });
78401
80204
  }
78402
- return new Promise((resolve18, reject) => {
78403
- this.manualMergeResolvers.set(taskId, { resolve: resolve18, reject });
80205
+ return new Promise((resolve19, reject) => {
80206
+ this.manualMergeResolvers.set(taskId, { resolve: resolve19, reject });
78404
80207
  this.internalEnqueueMerge(taskId);
78405
80208
  });
78406
80209
  }
@@ -80138,11 +81941,14 @@ __export(src_exports2, {
80138
81941
  AgentSemaphore: () => AgentSemaphore,
80139
81942
  CronRunner: () => CronRunner,
80140
81943
  DEFAULT_NTFY_EVENTS: () => DEFAULT_NTFY_EVENTS,
81944
+ GitHubProvider: () => GitHubProvider,
80141
81945
  HEARTBEAT_NO_TASK_SYSTEM_PROMPT: () => HEARTBEAT_NO_TASK_SYSTEM_PROMPT,
80142
81946
  HEARTBEAT_PROCEDURE: () => HEARTBEAT_PROCEDURE,
80143
81947
  HEARTBEAT_SYSTEM_PROMPT: () => HEARTBEAT_SYSTEM_PROMPT,
80144
81948
  HeartbeatMonitor: () => HeartbeatMonitor,
80145
81949
  HeartbeatTriggerScheduler: () => HeartbeatTriggerScheduler,
81950
+ LLMSynthesisProvider: () => LLMSynthesisProvider,
81951
+ LocalDocsProvider: () => LocalDocsProvider,
80146
81952
  MissionAutopilot: () => MissionAutopilot,
80147
81953
  MissionExecutionLoop: () => MissionExecutionLoop,
80148
81954
  NodeHealthMonitor: () => NodeHealthMonitor,
@@ -80152,6 +81958,7 @@ __export(src_exports2, {
80152
81958
  PRIORITY_EXECUTE: () => PRIORITY_EXECUTE,
80153
81959
  PRIORITY_MERGE: () => PRIORITY_MERGE,
80154
81960
  PRIORITY_SPECIFY: () => PRIORITY_SPECIFY,
81961
+ PageFetchProvider: () => PageFetchProvider,
80155
81962
  PeerExchangeService: () => PeerExchangeService,
80156
81963
  PluginRunner: () => PluginRunner,
80157
81964
  PrCommentHandler: () => PrCommentHandler,
@@ -80161,6 +81968,13 @@ __export(src_exports2, {
80161
81968
  ProjectManager: () => ProjectManager,
80162
81969
  RemoteNodeClient: () => RemoteNodeClient,
80163
81970
  RemoteNodeRuntime: () => RemoteNodeRuntime,
81971
+ ResearchOrchestrator: () => ResearchOrchestrator,
81972
+ ResearchProviderError: () => ResearchProviderError,
81973
+ ResearchProviderRegistry: () => ResearchProviderRegistry,
81974
+ ResearchStepAbortError: () => ResearchStepAbortError,
81975
+ ResearchStepProviderError: () => ResearchStepProviderError,
81976
+ ResearchStepRunner: () => ResearchStepRunner,
81977
+ ResearchStepTimeoutError: () => ResearchStepTimeoutError,
80164
81978
  RoutineRunner: () => RoutineRunner,
80165
81979
  RoutineScheduler: () => RoutineScheduler,
80166
81980
  Scheduler: () => Scheduler,
@@ -80172,6 +81986,7 @@ __export(src_exports2, {
80172
81986
  TriageProcessor: () => TriageProcessor,
80173
81987
  TunnelProcessManager: () => TunnelProcessManager,
80174
81988
  UsageLimitPauser: () => UsageLimitPauser,
81989
+ WebSearchProvider: () => WebSearchProvider,
80175
81990
  WebhookNotificationProvider: () => WebhookNotificationProvider,
80176
81991
  WorktreePool: () => WorktreePool,
80177
81992
  aiMergeTask: () => aiMergeTask,
@@ -80244,6 +82059,11 @@ var init_src2 = __esm({
80244
82059
  init_logger2();
80245
82060
  init_usage_limit_detector();
80246
82061
  init_rate_limit_retry();
82062
+ init_research_orchestrator();
82063
+ init_research_step_runner();
82064
+ init_provider_registry();
82065
+ init_types2();
82066
+ init_providers();
80247
82067
  init_pr_monitor();
80248
82068
  init_pr_comment_handler();
80249
82069
  init_notifier();
@@ -82028,7 +83848,7 @@ function buildHermesArgs(prompt, settings, resumeSessionId) {
82028
83848
  async function invokeHermesCli(prompt, settings, resumeSessionId, signal) {
82029
83849
  const args = buildHermesArgs(prompt, settings, resumeSessionId);
82030
83850
  const binary = resolveBinaryForSpawn(settings.binaryPath);
82031
- return new Promise((resolve18, reject) => {
83851
+ return new Promise((resolve19, reject) => {
82032
83852
  let settled = false;
82033
83853
  const spawnEnv = { ...process.env, PYTHONUNBUFFERED: "1" };
82034
83854
  if (settings.profile) {
@@ -82096,7 +83916,7 @@ ${combined}`));
82096
83916
  return;
82097
83917
  }
82098
83918
  try {
82099
- resolve18(parseHermesOutput(stdout, stderr));
83919
+ resolve19(parseHermesOutput(stdout, stderr));
82100
83920
  } catch (parseErr) {
82101
83921
  reject(parseErr);
82102
83922
  }
@@ -82322,7 +84142,7 @@ async function promptCli(session, message, config, callbacks, signal) {
82322
84142
  const args = buildOpenClawArgs(config, session.sessionId, message);
82323
84143
  const cb = { ...session.callbacks, ...callbacks };
82324
84144
  cb.onToolStart?.("openclaw.agent", { sessionId: session.sessionId });
82325
- return new Promise((resolve18, reject) => {
84145
+ return new Promise((resolve19, reject) => {
82326
84146
  let settled = false;
82327
84147
  const child = spawn5(config.binaryPath, args, {
82328
84148
  stdio: ["ignore", "pipe", "pipe"]
@@ -82415,7 +84235,7 @@ async function promptCli(session, message, config, callbacks, signal) {
82415
84235
  ...metaError ? { error: metaError } : {},
82416
84236
  ...errorText.length > 0 ? { toolErrors: errorText } : {}
82417
84237
  });
82418
- resolve18();
84238
+ resolve19();
82419
84239
  });
82420
84240
  });
82421
84241
  }
@@ -83468,7 +85288,7 @@ var require_galois_field = __commonJS({
83468
85288
  EXP_TABLE[i] = EXP_TABLE[i - 255];
83469
85289
  }
83470
85290
  })();
83471
- exports.log = function log9(n) {
85291
+ exports.log = function log17(n) {
83472
85292
  if (n < 1) throw new Error("log(" + n + ")");
83473
85293
  return LOG_TABLE[n];
83474
85294
  };
@@ -86690,7 +88510,7 @@ var require_utils2 = __commonJS({
86690
88510
  var require_png2 = __commonJS({
86691
88511
  "../../node_modules/.pnpm/qrcode@1.5.4/node_modules/qrcode/lib/renderer/png.js"(exports) {
86692
88512
  "use strict";
86693
- var fs = __require("fs");
88513
+ var fs2 = __require("fs");
86694
88514
  var PNG = require_png().PNG;
86695
88515
  var Utils = require_utils2();
86696
88516
  exports.render = function render(qrData, options) {
@@ -86742,7 +88562,7 @@ var require_png2 = __commonJS({
86742
88562
  called = true;
86743
88563
  cb.apply(null, args);
86744
88564
  };
86745
- const stream = fs.createWriteStream(path2);
88565
+ const stream = fs2.createWriteStream(path2);
86746
88566
  stream.on("error", done);
86747
88567
  stream.on("close", done);
86748
88568
  exports.renderToFileStream(stream, qrData, options);
@@ -86810,9 +88630,9 @@ var require_utf8 = __commonJS({
86810
88630
  cb = options;
86811
88631
  options = void 0;
86812
88632
  }
86813
- const fs = __require("fs");
88633
+ const fs2 = __require("fs");
86814
88634
  const utf8 = exports.render(qrData, options);
86815
- fs.writeFile(path2, utf8, cb);
88635
+ fs2.writeFile(path2, utf8, cb);
86816
88636
  };
86817
88637
  }
86818
88638
  });
@@ -86991,10 +88811,10 @@ var require_svg = __commonJS({
86991
88811
  cb = options;
86992
88812
  options = void 0;
86993
88813
  }
86994
- const fs = __require("fs");
88814
+ const fs2 = __require("fs");
86995
88815
  const svgTag = exports.render(qrData, options);
86996
88816
  const xmlStr = '<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' + svgTag;
86997
- fs.writeFile(path2, xmlStr, cb);
88817
+ fs2.writeFile(path2, xmlStr, cb);
86998
88818
  };
86999
88819
  }
87000
88820
  });
@@ -87099,10 +88919,10 @@ var require_browser3 = __commonJS({
87099
88919
  text = canvas;
87100
88920
  canvas = void 0;
87101
88921
  }
87102
- return new Promise(function(resolve18, reject) {
88922
+ return new Promise(function(resolve19, reject) {
87103
88923
  try {
87104
88924
  const data = QRCode2.create(text, opts);
87105
- resolve18(renderFunc(data, canvas, opts));
88925
+ resolve19(renderFunc(data, canvas, opts));
87106
88926
  } catch (e) {
87107
88927
  reject(e);
87108
88928
  }
@@ -87184,11 +89004,11 @@ var require_server = __commonJS({
87184
89004
  }
87185
89005
  function render(renderFunc, text, params) {
87186
89006
  if (!params.cb) {
87187
- return new Promise(function(resolve18, reject) {
89007
+ return new Promise(function(resolve19, reject) {
87188
89008
  try {
87189
89009
  const data = QRCode2.create(text, params.opts);
87190
89010
  return renderFunc(data, params.opts, function(err, data2) {
87191
- return err ? reject(err) : resolve18(data2);
89011
+ return err ? reject(err) : resolve19(data2);
87192
89012
  });
87193
89013
  } catch (e) {
87194
89014
  reject(e);
@@ -87297,7 +89117,7 @@ var init_register_messaging_scripts = __esm({
87297
89117
 
87298
89118
  // ../dashboard/src/github.ts
87299
89119
  function delay(ms) {
87300
- return new Promise((resolve18) => setTimeout(resolve18, ms));
89120
+ return new Promise((resolve19) => setTimeout(resolve19, ms));
87301
89121
  }
87302
89122
  function normalizeCheckState(state) {
87303
89123
  switch ((state ?? "").toLowerCase()) {
@@ -89848,13 +91668,13 @@ function resolvePaperclipConfig(settings) {
89848
91668
  };
89849
91669
  }
89850
91670
  async function discoverPaperclipCliConfig(opts = {}) {
89851
- const fs = await import("node:fs/promises");
91671
+ const fs2 = await import("node:fs/promises");
89852
91672
  const path2 = await import("node:path");
89853
91673
  const os3 = await import("node:os");
89854
91674
  const configPath = opts.configPath ?? path2.join(os3.homedir(), ".paperclip", "instances", "default", "config.json");
89855
91675
  let raw;
89856
91676
  try {
89857
- raw = await fs.readFile(configPath, "utf-8");
91677
+ raw = await fs2.readFile(configPath, "utf-8");
89858
91678
  } catch (error) {
89859
91679
  const code = error.code;
89860
91680
  return {
@@ -90017,7 +91837,7 @@ async function spawnPaperclipCliJson(args, opts) {
90017
91837
  }
90018
91838
  const timeoutMs = opts.cliTimeoutMs ?? 15e3;
90019
91839
  const label = ["paperclipai", ...args].join(" ");
90020
- return new Promise((resolve18, reject) => {
91840
+ return new Promise((resolve19, reject) => {
90021
91841
  let child;
90022
91842
  try {
90023
91843
  child = spawn10(bin, fullArgs, { stdio: ["ignore", "pipe", "pipe"] });
@@ -90063,7 +91883,7 @@ async function spawnPaperclipCliJson(args, opts) {
90063
91883
  return;
90064
91884
  }
90065
91885
  try {
90066
- resolve18(JSON.parse(cleaned));
91886
+ resolve19(JSON.parse(cleaned));
90067
91887
  } catch {
90068
91888
  reject(new Error(`${label} returned non-JSON output: ${cleaned.slice(0, 200)}`));
90069
91889
  }
@@ -90132,8 +91952,8 @@ var init_paperclip_client = __esm({
90132
91952
 
90133
91953
  // ../../plugins/fusion-plugin-paperclip-runtime/dist/runtime-adapter.js
90134
91954
  import { randomUUID as randomUUID14 } from "node:crypto";
90135
- function sleep3(ms) {
90136
- return new Promise((resolve18) => setTimeout(resolve18, ms));
91955
+ function sleep4(ms) {
91956
+ return new Promise((resolve19) => setTimeout(resolve19, ms));
90137
91957
  }
90138
91958
  function asString2(value) {
90139
91959
  return typeof value === "string" ? value : void 0;
@@ -90435,7 +92255,7 @@ var init_runtime_adapter3 = __esm({
90435
92255
  this.logger.warn(`Paperclip run ${runId} exceeded local runTimeoutMs=${session.runTimeoutMs}; abandoning poll. Run continues server-side.`);
90436
92256
  break;
90437
92257
  }
90438
- await sleep3(interval);
92258
+ await sleep4(interval);
90439
92259
  interval = Math.min(interval * 2, session.pollIntervalMaxMs);
90440
92260
  }
90441
92261
  return { text: textBuf, thinking: thinkBuf, runStatus, timedOutLocally };
@@ -94593,7 +96413,7 @@ var init_auth_middleware = __esm({
94593
96413
 
94594
96414
  // ../dashboard/src/server.ts
94595
96415
  import express from "express";
94596
- import { join as join37, dirname as dirname11 } from "node:path";
96416
+ import { join as join38, dirname as dirname11 } from "node:path";
94597
96417
  import { fileURLToPath as fileURLToPath4 } from "node:url";
94598
96418
  function clearAiSessionCleanupInterval() {
94599
96419
  if (!aiSessionCleanupIntervalHandle) {
@@ -94701,7 +96521,7 @@ var init_src4 = __esm({
94701
96521
  });
94702
96522
 
94703
96523
  // src/project-context.ts
94704
- import { resolve as resolve16, dirname as dirname12, basename as basename9 } from "node:path";
96524
+ import { resolve as resolve17, dirname as dirname12, basename as basename9 } from "node:path";
94705
96525
  async function resolveProject(projectNameFlag, cwd = process.cwd(), globalDir) {
94706
96526
  const central = new CentralCore(globalDir);
94707
96527
  await central.init();
@@ -94777,9 +96597,9 @@ async function clearDefaultProject(globalDir) {
94777
96597
  await globalStore.updateSettings(rest);
94778
96598
  }
94779
96599
  async function detectProjectFromCwd(cwd, central) {
94780
- let currentDir = resolve16(cwd);
96600
+ let currentDir = resolve17(cwd);
94781
96601
  while (true) {
94782
- const kbPath = resolve16(currentDir, ".fusion", "fusion.db");
96602
+ const kbPath = resolve17(currentDir, ".fusion", "fusion.db");
94783
96603
  if (isValidSqliteDatabaseFile(kbPath)) {
94784
96604
  const project = await central.getProjectByPath(currentDir);
94785
96605
  if (project) {
@@ -94883,7 +96703,7 @@ __export(task_exports, {
94883
96703
  });
94884
96704
  import { createInterface as createInterface2 } from "node:readline/promises";
94885
96705
  import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync29, readFileSync as readFileSync8 } from "node:fs";
94886
- import { basename as basename10, join as join38 } from "node:path";
96706
+ import { basename as basename10, join as join39 } from "node:path";
94887
96707
  function getGitHubIssueUrl(sourceMetadata) {
94888
96708
  if (!sourceMetadata || typeof sourceMetadata !== "object") return void 0;
94889
96709
  const issueUrl = sourceMetadata.issueUrl;
@@ -95047,11 +96867,11 @@ async function runTaskCreate(descriptionArg, attachFiles, depends, projectName,
95047
96867
  console.log(` Path: .fusion/tasks/${task.id}/`);
95048
96868
  if (attachFiles && attachFiles.length > 0) {
95049
96869
  const { readFile: readFile19 } = await import("node:fs/promises");
95050
- const { basename: basename12, extname: extname3, resolve: resolve18 } = await import("node:path");
96870
+ const { basename: basename12, extname: extname4, resolve: resolve19 } = await import("node:path");
95051
96871
  for (const filePath of attachFiles) {
95052
- const resolvedPath = resolve18(filePath);
96872
+ const resolvedPath = resolve19(filePath);
95053
96873
  const filename = basename12(resolvedPath);
95054
- const ext = extname3(filename).toLowerCase();
96874
+ const ext = extname4(filename).toLowerCase();
95055
96875
  const mimeType = MIME_TYPES[ext];
95056
96876
  if (!mimeType) {
95057
96877
  console.error(` \u2717 Unsupported file type: ${ext} (${filename})`);
@@ -95183,7 +97003,7 @@ async function runTaskLogs(id, options = {}, projectName) {
95183
97003
  printEntries(filteredEntries);
95184
97004
  if (options.follow) {
95185
97005
  const projectPath = projectContext?.projectPath ?? process.cwd();
95186
- const logPath = join38(projectPath, ".fusion", "tasks", id, "agent.log");
97006
+ const logPath = join39(projectPath, ".fusion", "tasks", id, "agent.log");
95187
97007
  if (!existsSync29(logPath)) {
95188
97008
  console.log(`
95189
97009
  Waiting for log file to be created...`);
@@ -95346,11 +97166,11 @@ async function runTaskMerge(id, projectName) {
95346
97166
  }
95347
97167
  async function runTaskAttach(id, filePath, projectName) {
95348
97168
  const { readFile: readFile19 } = await import("node:fs/promises");
95349
- const { basename: basename12, extname: extname3 } = await import("node:path");
95350
- const { resolve: resolve18 } = await import("node:path");
95351
- const resolvedPath = resolve18(filePath);
97169
+ const { basename: basename12, extname: extname4 } = await import("node:path");
97170
+ const { resolve: resolve19 } = await import("node:path");
97171
+ const resolvedPath = resolve19(filePath);
95352
97172
  const filename = basename12(resolvedPath);
95353
- const ext = extname3(filename).toLowerCase();
97173
+ const ext = extname4(filename).toLowerCase();
95354
97174
  const mimeType = MIME_TYPES[ext];
95355
97175
  if (!mimeType) {
95356
97176
  console.error(`Unsupported file type: ${ext}`);
@@ -95887,12 +97707,12 @@ async function promptText(question) {
95887
97707
  console.log(" (Enter your response. Type DONE on its own line when finished):\n");
95888
97708
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
95889
97709
  const lines = [];
95890
- return new Promise((resolve18) => {
97710
+ return new Promise((resolve19) => {
95891
97711
  const askLine = () => {
95892
97712
  rl.question(" ").then((line) => {
95893
97713
  if (line.trim() === "DONE") {
95894
97714
  rl.close();
95895
- resolve18(lines.join("\n"));
97715
+ resolve19(lines.join("\n"));
95896
97716
  } else {
95897
97717
  lines.push(line);
95898
97718
  askLine();
@@ -96297,9 +98117,9 @@ async function runSkillsInstall(args, options) {
96297
98117
  stdio: "inherit",
96298
98118
  shell: true
96299
98119
  });
96300
- const exitCode = await new Promise((resolve18, reject) => {
98120
+ const exitCode = await new Promise((resolve19, reject) => {
96301
98121
  child.on("exit", (code) => {
96302
- resolve18(code ?? 1);
98122
+ resolve19(code ?? 1);
96303
98123
  });
96304
98124
  child.on("error", (err) => {
96305
98125
  reject(err);
@@ -96326,7 +98146,7 @@ init_src();
96326
98146
  init_gh_cli();
96327
98147
  import { Type as Type7 } from "typebox";
96328
98148
  import { StringEnum } from "@mariozechner/pi-ai";
96329
- import { resolve as resolve17, basename as basename11, extname as extname2, join as join39 } from "node:path";
98149
+ import { resolve as resolve18, basename as basename11, extname as extname3, join as join40 } from "node:path";
96330
98150
  import { readFile as readFile18 } from "node:fs/promises";
96331
98151
  import { existsSync as existsSync30 } from "node:fs";
96332
98152
  import { spawn as spawn9 } from "node:child_process";
@@ -96346,14 +98166,14 @@ var MIME_TYPES2 = {
96346
98166
  ".xml": "application/xml"
96347
98167
  };
96348
98168
  function resolveProjectRoot(cwd) {
96349
- let current = resolve17(cwd);
98169
+ let current = resolve18(cwd);
96350
98170
  while (true) {
96351
- if (existsSync30(join39(current, ".fusion"))) {
98171
+ if (existsSync30(join40(current, ".fusion"))) {
96352
98172
  return current;
96353
98173
  }
96354
- const parent = resolve17(current, "..");
98174
+ const parent = resolve18(current, "..");
96355
98175
  if (parent === current) {
96356
- return resolve17(cwd);
98176
+ return resolve18(cwd);
96357
98177
  }
96358
98178
  current = parent;
96359
98179
  }
@@ -96369,7 +98189,7 @@ async function getStore2(cwd) {
96369
98189
  return store;
96370
98190
  }
96371
98191
  function getFusionDir(cwd) {
96372
- return join39(resolveProjectRoot(cwd), ".fusion");
98192
+ return join40(resolveProjectRoot(cwd), ".fusion");
96373
98193
  }
96374
98194
  async function validateAssignableAgentId(cwd, agentId) {
96375
98195
  const { AgentStore: AgentStore2, isEphemeralAgent: isEphemeralAgent2 } = await Promise.resolve().then(() => (init_src(), src_exports));
@@ -96698,9 +98518,9 @@ Column: triage
96698
98518
  path: Type7.String({ description: "Path to the file to attach" })
96699
98519
  }),
96700
98520
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
96701
- const filePath = resolve17(ctx.cwd, params.path.replace(/^@/, ""));
98521
+ const filePath = resolve18(ctx.cwd, params.path.replace(/^@/, ""));
96702
98522
  const filename = basename11(filePath);
96703
- const ext = extname2(filename).toLowerCase();
98523
+ const ext = extname3(filename).toLowerCase();
96704
98524
  const mimeType = MIME_TYPES2[ext];
96705
98525
  if (!mimeType) {
96706
98526
  throw new Error(
@@ -97815,12 +99635,12 @@ Status: ${updated.status}`
97815
99635
  child.stderr?.on("data", (data) => {
97816
99636
  stderr += data.toString();
97817
99637
  });
97818
- const exitCode = await new Promise((resolve18) => {
99638
+ const exitCode = await new Promise((resolve19) => {
97819
99639
  child.on("exit", (code) => {
97820
- resolve18(code ?? 1);
99640
+ resolve19(code ?? 1);
97821
99641
  });
97822
99642
  child.on("error", () => {
97823
- resolve18(1);
99643
+ resolve19(1);
97824
99644
  });
97825
99645
  });
97826
99646
  try {