@runfusion/fusion 0.8.2 → 0.8.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 (48) hide show
  1. package/dist/bin.js +1714 -840
  2. package/dist/client/assets/{AgentDetailView-B6wfIQ9j.js → AgentDetailView-C_DD03d6.js} +1 -1
  3. package/dist/client/assets/{AgentsView-Fcym0XCw.js → AgentsView-CsEX_Fsi.js} +3 -3
  4. package/dist/client/assets/ChatView-DhudD-jT.js +1 -0
  5. package/dist/client/assets/{DevServerView-CmMS4D6V.js → DevServerView-CNItMoga.js} +1 -1
  6. package/dist/client/assets/DirectoryPicker-BqcVbDZX.js +1 -0
  7. package/dist/client/assets/{DocumentsView-C4HDkN_0.js → DocumentsView-zpHkjZdf.js} +1 -1
  8. package/dist/client/assets/{InsightsView-Egu71gmh.css → InsightsView-6LHF7OdE.css} +1 -1
  9. package/dist/client/assets/InsightsView-CgguV1au.js +11 -0
  10. package/dist/client/assets/{MemoryView-DPYGW09y.js → MemoryView-BWXP8uGT.js} +1 -1
  11. package/dist/client/assets/{NodesView-DsM-c8RF.js → NodesView-DP_O4ae0.js} +1 -1
  12. package/dist/client/assets/{PiExtensionsManager-DHgMjjRE.js → PiExtensionsManager-DGEPbF0y.js} +3 -3
  13. package/dist/client/assets/{PluginManager-N-7Sw_tE.js → PluginManager-4TehPpcf.js} +1 -1
  14. package/dist/client/assets/{RoadmapsView-BgX79uYM.js → RoadmapsView-DNaToPqN.js} +2 -2
  15. package/dist/client/assets/SettingsModal-BluRnKGd.js +31 -0
  16. package/dist/client/assets/{SettingsModal-BIKEMPwb.js → SettingsModal-C-RjolQ5.js} +1 -1
  17. package/dist/client/assets/SettingsModal-C7gPLBaR.css +1 -0
  18. package/dist/client/assets/{SetupWizardModal-D2m0i9Io.js → SetupWizardModal-BQoo_AvX.js} +1 -1
  19. package/dist/client/assets/{SkillsView-Eb1Mngnt.js → SkillsView-AS8Cr_Md.js} +1 -1
  20. package/dist/client/assets/{TodoView-BN8FQYyp.js → TodoView-nWOpOg3R.js} +2 -2
  21. package/dist/client/assets/{folder-open-BqZBHfoZ.js → folder-open-D0LfE0ZP.js} +1 -1
  22. package/dist/client/assets/index-D6xr8Oa2.css +1 -0
  23. package/dist/client/assets/index-heCcln3Z.js +656 -0
  24. package/dist/client/assets/{list-checks-D2EURsP0.js → list-checks-D1faMe1o.js} +1 -1
  25. package/dist/client/assets/{star-CNQlAD8p.js → star-B_uA5YGG.js} +1 -1
  26. package/dist/client/assets/{upload-uoxlYkig.js → upload-DPQ3hWf0.js} +1 -1
  27. package/dist/client/assets/{users-BLvidusm.js → users-CEcYlrZO.js} +1 -1
  28. package/dist/client/index.html +2 -2
  29. package/dist/client/version.json +1 -1
  30. package/dist/extension.js +1033 -372
  31. package/dist/pi-claude-cli/package.json +1 -1
  32. package/dist/pi-claude-cli/src/__tests__/control-handler.test.ts +39 -66
  33. package/dist/pi-claude-cli/src/__tests__/process-manager.test.ts +7 -9
  34. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +73 -148
  35. package/dist/pi-claude-cli/src/__tests__/setup-test-isolation.test.ts +2 -0
  36. package/dist/pi-claude-cli/src/__tests__/setup-test-isolation.ts +8 -0
  37. package/dist/pi-claude-cli/src/control-handler.ts +22 -8
  38. package/dist/pi-claude-cli/src/process-manager.ts +10 -3
  39. package/dist/pi-claude-cli/src/prompt-builder.ts +2 -1
  40. package/dist/pi-claude-cli/src/provider.ts +10 -2
  41. package/package.json +1 -1
  42. package/dist/client/assets/ChatView-DEckS3f3.js +0 -1
  43. package/dist/client/assets/DirectoryPicker-CQtE-YyA.js +0 -1
  44. package/dist/client/assets/InsightsView-oNQ7h5e8.js +0 -11
  45. package/dist/client/assets/SettingsModal-C4EwtN6K.js +0 -31
  46. package/dist/client/assets/SettingsModal-D5hLoLXp.css +0 -1
  47. package/dist/client/assets/index-D2fXOwWF.css +0 -1
  48. package/dist/client/assets/index-DlHPhpDu.js +0 -656
package/dist/extension.js CHANGED
@@ -2432,7 +2432,7 @@ var init_db = __esm({
2432
2432
  "use strict";
2433
2433
  init_sqlite_adapter();
2434
2434
  init_types();
2435
- SCHEMA_VERSION = 51;
2435
+ SCHEMA_VERSION = 52;
2436
2436
  SCHEMA_SQL = `
2437
2437
  -- Tasks table with JSON columns for nested data
2438
2438
  CREATE TABLE IF NOT EXISTS tasks (
@@ -2949,7 +2949,12 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
2949
2949
  if (!inMemory && !existsSync(fusionDir)) {
2950
2950
  mkdirSync(fusionDir, { recursive: true });
2951
2951
  }
2952
- this.db = new DatabaseSync(this.dbPath);
2952
+ try {
2953
+ this.db = new DatabaseSync(this.dbPath);
2954
+ } catch (error) {
2955
+ const message = error instanceof Error ? error.message : String(error);
2956
+ throw new Error(`Failed to open Fusion database at ${this.dbPath}: ${message}`);
2957
+ }
2953
2958
  if (!inMemory) {
2954
2959
  this.db.exec("PRAGMA journal_mode = WAL");
2955
2960
  }
@@ -3972,6 +3977,11 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
3972
3977
  }
3973
3978
  });
3974
3979
  }
3980
+ if (version < 52) {
3981
+ this.applyMigration(52, () => {
3982
+ this.addColumnIfMissing("tasks", "mergeConflictBounceCount", "INTEGER DEFAULT 0");
3983
+ });
3984
+ }
3975
3985
  }
3976
3986
  /**
3977
3987
  * Run a single migration step inside a transaction and bump the version.
@@ -6305,14 +6315,17 @@ import { homedir } from "node:os";
6305
6315
  import { dirname, join as join4 } from "node:path";
6306
6316
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3, rename as rename2, chmod } from "node:fs/promises";
6307
6317
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, renameSync } from "node:fs";
6318
+ function getHomeDir() {
6319
+ return process.env.HOME || process.env.USERPROFILE || homedir();
6320
+ }
6308
6321
  function legacyGlobalDir() {
6309
- return join4(homedir(), ".pi", "fusion");
6322
+ return join4(getHomeDir(), ".pi", "fusion");
6310
6323
  }
6311
6324
  function legacyGlobalDirOriginal() {
6312
- return join4(homedir(), ".pi", "kb");
6325
+ return join4(getHomeDir(), ".pi", "kb");
6313
6326
  }
6314
6327
  function defaultGlobalDir() {
6315
- return join4(homedir(), ".fusion");
6328
+ return join4(getHomeDir(), ".fusion");
6316
6329
  }
6317
6330
  function resolveGlobalDir(dir) {
6318
6331
  const hasExplicitDir = typeof dir === "string" && dir.length > 0;
@@ -6484,9 +6497,9 @@ var init_global_settings = __esm({
6484
6497
  * Serialize operations via promise chain to prevent lost-update races.
6485
6498
  */
6486
6499
  withLock(fn) {
6487
- let resolve19;
6500
+ let resolve17;
6488
6501
  const next = new Promise((r) => {
6489
- resolve19 = r;
6502
+ resolve17 = r;
6490
6503
  });
6491
6504
  const prev = this.lock;
6492
6505
  this.lock = next;
@@ -6494,7 +6507,7 @@ var init_global_settings = __esm({
6494
6507
  try {
6495
6508
  return await fn();
6496
6509
  } finally {
6497
- resolve19();
6510
+ resolve17();
6498
6511
  }
6499
6512
  });
6500
6513
  }
@@ -12033,7 +12046,12 @@ CREATE INDEX IF NOT EXISTS idxSettingsSyncNode ON settingsSyncState(nodeId);
12033
12046
  if (!existsSync6(this.globalDir)) {
12034
12047
  mkdirSync4(this.globalDir, { recursive: true });
12035
12048
  }
12036
- this.db = new DatabaseSync(this.dbPath);
12049
+ try {
12050
+ this.db = new DatabaseSync(this.dbPath);
12051
+ } catch (error) {
12052
+ const message = error instanceof Error ? error.message : String(error);
12053
+ throw new Error(`Failed to open Fusion central database at ${this.dbPath}: ${message}`);
12054
+ }
12037
12055
  this.db.exec("PRAGMA journal_mode = WAL");
12038
12056
  this.db.exec("PRAGMA busy_timeout = 5000");
12039
12057
  this.db.exec("PRAGMA foreign_keys = ON");
@@ -17957,6 +17975,40 @@ var init_central_core = __esm({
17957
17975
  }
17958
17976
  });
17959
17977
 
17978
+ // ../core/src/sqlite-validation.ts
17979
+ import { existsSync as existsSync8, statSync as statSync2 } from "node:fs";
17980
+ function isValidSqliteDatabaseFile(dbPath) {
17981
+ if (!existsSync8(dbPath)) {
17982
+ return false;
17983
+ }
17984
+ try {
17985
+ if (!statSync2(dbPath).isFile()) {
17986
+ return false;
17987
+ }
17988
+ } catch {
17989
+ return false;
17990
+ }
17991
+ let db = null;
17992
+ try {
17993
+ db = new DatabaseSync(dbPath);
17994
+ db.prepare("PRAGMA schema_version").get();
17995
+ return true;
17996
+ } catch {
17997
+ return false;
17998
+ } finally {
17999
+ try {
18000
+ db?.close();
18001
+ } catch {
18002
+ }
18003
+ }
18004
+ }
18005
+ var init_sqlite_validation = __esm({
18006
+ "../core/src/sqlite-validation.ts"() {
18007
+ "use strict";
18008
+ init_sqlite_adapter();
18009
+ }
18010
+ });
18011
+
17960
18012
  // ../core/src/migration.ts
17961
18013
  var migration_exports = {};
17962
18014
  __export(migration_exports, {
@@ -17965,26 +18017,25 @@ __export(migration_exports, {
17965
18017
  MigrationCoordinator: () => MigrationCoordinator,
17966
18018
  ProjectRequiredError: () => ProjectRequiredError
17967
18019
  });
17968
- import { existsSync as existsSync8, statSync as statSync2 } from "node:fs";
18020
+ import { existsSync as existsSync9 } from "node:fs";
17969
18021
  import { homedir as homedir2, tmpdir } from "node:os";
17970
18022
  import { isAbsolute as isAbsolute3, join as join11, resolve as resolve4, basename as basename3, dirname as dirname3 } from "node:path";
18023
+ function getHomeDir2() {
18024
+ return process.env.HOME || process.env.USERPROFILE || homedir2();
18025
+ }
17971
18026
  function hasProjectDbFile(dir, folderName, dbName) {
17972
18027
  const projectDir = join11(dir, folderName);
17973
18028
  const dbPath = join11(projectDir, dbName);
17974
- if (!existsSync8(projectDir)) return false;
17975
- if (!existsSync8(dbPath)) return false;
17976
- try {
17977
- const stat8 = statSync2(dbPath);
17978
- return stat8.isFile() && stat8.size > 0;
17979
- } catch {
17980
- return false;
17981
- }
18029
+ if (!existsSync9(projectDir)) return false;
18030
+ return isValidSqliteDatabaseFile(dbPath);
17982
18031
  }
17983
18032
  var ProjectRequiredError, FirstRunDetector, MigrationCoordinator, BackwardCompat;
17984
18033
  var init_migration = __esm({
17985
18034
  "../core/src/migration.ts"() {
17986
18035
  "use strict";
17987
18036
  init_central_core();
18037
+ init_global_settings();
18038
+ init_sqlite_validation();
17988
18039
  ProjectRequiredError = class extends Error {
17989
18040
  constructor(message, availableProjects) {
17990
18041
  super(message);
@@ -18045,7 +18096,7 @@ var init_migration = __esm({
18045
18096
  */
18046
18097
  hasCentralDb() {
18047
18098
  const centralDbPath = join11(this.globalDir, "fusion-central.db");
18048
- return existsSync8(centralDbPath);
18099
+ return existsSync9(centralDbPath);
18049
18100
  }
18050
18101
  /**
18051
18102
  * Get the path to the central database.
@@ -18067,7 +18118,7 @@ var init_migration = __esm({
18067
18118
  const projects = [];
18068
18119
  const visited = /* @__PURE__ */ new Set();
18069
18120
  let current = resolve4(startDir);
18070
- const home = homedir2();
18121
+ const home = getHomeDir2();
18071
18122
  const root = dirname3(current) === current ? current : "/";
18072
18123
  const systemTmp = resolve4(tmpdir());
18073
18124
  while (current !== home && current !== root && current !== systemTmp) {
@@ -18099,7 +18150,7 @@ var init_migration = __esm({
18099
18150
  * @returns Generated name
18100
18151
  */
18101
18152
  async generateProjectName(projectPath) {
18102
- if (!existsSync8(join11(projectPath, ".git"))) {
18153
+ if (!existsSync9(join11(projectPath, ".git"))) {
18103
18154
  return basename3(projectPath);
18104
18155
  }
18105
18156
  try {
@@ -18148,7 +18199,7 @@ var init_migration = __esm({
18148
18199
  return hasProjectDbFile(dir, ".fusion", "fusion.db");
18149
18200
  }
18150
18201
  getDefaultGlobalDir() {
18151
- return join11(homedir2(), ".pi", "kb");
18202
+ return resolveGlobalDir();
18152
18203
  }
18153
18204
  };
18154
18205
  MigrationCoordinator = class {
@@ -27572,8 +27623,8 @@ var require_CronFileParser = __commonJS({
27572
27623
  * @throws If file cannot be read
27573
27624
  */
27574
27625
  static parseFileSync(filePath) {
27575
- const { readFileSync: readFileSync10 } = __require("fs");
27576
- const data = readFileSync10(filePath, "utf8");
27626
+ const { readFileSync: readFileSync8 } = __require("fs");
27627
+ const data = readFileSync8(filePath, "utf8");
27577
27628
  return _CronFileParser.#parseContent(data);
27578
27629
  }
27579
27630
  /**
@@ -27790,9 +27841,9 @@ var init_automation_store = __esm({
27790
27841
  */
27791
27842
  withScheduleLock(id, fn) {
27792
27843
  const prev = this.scheduleLocks.get(id) ?? Promise.resolve();
27793
- let resolve19;
27844
+ let resolve17;
27794
27845
  const next = new Promise((r) => {
27795
- resolve19 = r;
27846
+ resolve17 = r;
27796
27847
  });
27797
27848
  this.scheduleLocks.set(id, next);
27798
27849
  return prev.then(async () => {
@@ -27802,7 +27853,7 @@ var init_automation_store = __esm({
27802
27853
  if (this.scheduleLocks.get(id) === next) {
27803
27854
  this.scheduleLocks.delete(id);
27804
27855
  }
27805
- resolve19();
27856
+ resolve17();
27806
27857
  }
27807
27858
  });
27808
27859
  }
@@ -28061,7 +28112,7 @@ __export(memory_dreams_exports, {
28061
28112
  syncMemoryDreamsAutomation: () => syncMemoryDreamsAutomation
28062
28113
  });
28063
28114
  import { appendFile, mkdir as mkdir5, readFile as readFile5, readdir as readdir3, stat, writeFile as writeFile4 } from "node:fs/promises";
28064
- import { existsSync as existsSync9 } from "node:fs";
28115
+ import { existsSync as existsSync10 } from "node:fs";
28065
28116
  import { join as join13 } from "node:path";
28066
28117
  function agentMemoryWorkspacePath(rootDir, agentId) {
28067
28118
  const safeAgentId = agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
@@ -28080,7 +28131,7 @@ async function ensureAgentMemoryFiles(rootDir, agent, date = /* @__PURE__ */ new
28080
28131
  const workspacePath = agentMemoryWorkspacePath(rootDir, agent.id);
28081
28132
  await mkdir5(workspacePath, { recursive: true });
28082
28133
  const longTermPath = agentMemoryLongTermPath(rootDir, agent.id);
28083
- if (!existsSync9(longTermPath)) {
28134
+ if (!existsSync10(longTermPath)) {
28084
28135
  const title = agent.name?.trim() ? `# Agent Memory: ${agent.name.trim()}` : "# Agent Memory";
28085
28136
  await writeFile4(
28086
28137
  longTermPath,
@@ -28094,11 +28145,11 @@ ${agent.memory?.trim() ?? ""}
28094
28145
  );
28095
28146
  }
28096
28147
  const dreamsPath = agentMemoryDreamsPath(rootDir, agent.id);
28097
- if (!existsSync9(dreamsPath)) {
28148
+ if (!existsSync10(dreamsPath)) {
28098
28149
  await writeFile4(dreamsPath, "# Agent Memory Dreams\n\n<!-- Synthesized patterns from this agent's daily notes. -->\n", "utf-8");
28099
28150
  }
28100
28151
  const dailyPath = agentDailyMemoryPath(rootDir, agent.id, date);
28101
- if (!existsSync9(dailyPath)) {
28152
+ if (!existsSync10(dailyPath)) {
28102
28153
  await writeFile4(dailyPath, `# Agent Daily Memory ${date.toISOString().slice(0, 10)}
28103
28154
 
28104
28155
  <!-- Running observations for this agent. -->
@@ -28146,7 +28197,7 @@ function extractDreamProcessorResult(output) {
28146
28197
  };
28147
28198
  }
28148
28199
  async function readIfExists(path2) {
28149
- if (!existsSync9(path2)) {
28200
+ if (!existsSync10(path2)) {
28150
28201
  return "";
28151
28202
  }
28152
28203
  return readFile5(path2, "utf-8");
@@ -28188,7 +28239,7 @@ async function readAgentDailyNotes(rootDir, agentId, date) {
28188
28239
  const workspacePath = agentMemoryWorkspacePath(rootDir, agentId);
28189
28240
  const dateKey = date.toISOString().slice(0, 10);
28190
28241
  const dailyPath = agentDailyMemoryPath(rootDir, agentId, date);
28191
- if (existsSync9(dailyPath)) {
28242
+ if (existsSync10(dailyPath)) {
28192
28243
  return readFile5(dailyPath, "utf-8");
28193
28244
  }
28194
28245
  const files = await readdir3(workspacePath).catch(() => []);
@@ -28383,7 +28434,7 @@ __export(memory_backend_exports, {
28383
28434
  writeProjectMemoryFile: () => writeProjectMemoryFile
28384
28435
  });
28385
28436
  import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir6, access as access2, constants, readdir as readdir4, stat as stat2 } from "node:fs/promises";
28386
- import { existsSync as existsSync10 } from "node:fs";
28437
+ import { existsSync as existsSync11 } from "node:fs";
28387
28438
  import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute4, join as join14, normalize as normalize2, relative, resolve as resolve5, sep as sep2 } from "node:path";
28388
28439
  import { createHash as createHash3 } from "node:crypto";
28389
28440
  function shouldSkipBackgroundQmdRefresh() {
@@ -28468,18 +28519,18 @@ async function ensureOpenClawMemoryFiles(rootDir, date = /* @__PURE__ */ new Dat
28468
28519
  await mkdir6(workspacePath, { recursive: true });
28469
28520
  const longTermPath = memoryLongTermPath(rootDir);
28470
28521
  let longTermCreated = false;
28471
- if (!existsSync10(longTermPath)) {
28522
+ if (!existsSync11(longTermPath)) {
28472
28523
  await writeFile5(longTermPath, getDefaultLongTermMemoryScaffold(), "utf-8");
28473
28524
  longTermCreated = true;
28474
28525
  }
28475
28526
  const todayPath = dailyMemoryPath(rootDir, date);
28476
28527
  let dailyCreated = false;
28477
- if (!existsSync10(todayPath)) {
28528
+ if (!existsSync11(todayPath)) {
28478
28529
  await writeFile5(todayPath, getDefaultDailyMemoryScaffold(date), "utf-8");
28479
28530
  dailyCreated = true;
28480
28531
  }
28481
28532
  const dreamsPath = memoryDreamsPath(rootDir);
28482
- if (!existsSync10(dreamsPath)) {
28533
+ if (!existsSync11(dreamsPath)) {
28483
28534
  await writeFile5(dreamsPath, getDefaultDreamsScaffold(), "utf-8");
28484
28535
  }
28485
28536
  return { longTermCreated, dailyCreated };
@@ -28558,7 +28609,7 @@ async function listAgentMemoryFiles(rootDir, agentId, date = /* @__PURE__ */ new
28558
28609
  const workspaceDisplayPath = relative(rootDir, workspacePath).replace(/\\/g, "/");
28559
28610
  const files = [];
28560
28611
  const longTermPath = join14(workspacePath, MEMORY_LONG_TERM_FILENAME);
28561
- if (existsSync10(longTermPath)) {
28612
+ if (existsSync11(longTermPath)) {
28562
28613
  files.push({
28563
28614
  absPath: longTermPath,
28564
28615
  displayPath: `${workspaceDisplayPath}/${MEMORY_LONG_TERM_FILENAME}`,
@@ -28566,14 +28617,14 @@ async function listAgentMemoryFiles(rootDir, agentId, date = /* @__PURE__ */ new
28566
28617
  });
28567
28618
  }
28568
28619
  const dreamsPath = join14(workspacePath, MEMORY_DREAMS_FILENAME);
28569
- if (existsSync10(dreamsPath)) {
28620
+ if (existsSync11(dreamsPath)) {
28570
28621
  files.push({
28571
28622
  absPath: dreamsPath,
28572
28623
  displayPath: `${workspaceDisplayPath}/${MEMORY_DREAMS_FILENAME}`,
28573
28624
  fileName: MEMORY_DREAMS_FILENAME
28574
28625
  });
28575
28626
  }
28576
- if (existsSync10(workspacePath)) {
28627
+ if (existsSync11(workspacePath)) {
28577
28628
  for (const entry of await readdir4(workspacePath)) {
28578
28629
  if (!DAILY_MEMORY_RE.test(entry)) continue;
28579
28630
  const absPath = join14(workspacePath, entry);
@@ -28739,14 +28790,14 @@ async function listMemoryFiles(rootDir) {
28739
28790
  const files = [];
28740
28791
  const workspacePath = memoryWorkspacePath(rootDir);
28741
28792
  const longTerm = memoryLongTermPath(rootDir);
28742
- if (existsSync10(longTerm)) {
28793
+ if (existsSync11(longTerm)) {
28743
28794
  files.push({ absPath: longTerm, displayPath: `${MEMORY_WORKSPACE_PATH}/${MEMORY_LONG_TERM_FILENAME}` });
28744
28795
  }
28745
28796
  const dreams = memoryDreamsPath(rootDir);
28746
- if (existsSync10(dreams)) {
28797
+ if (existsSync11(dreams)) {
28747
28798
  files.push({ absPath: dreams, displayPath: `${MEMORY_WORKSPACE_PATH}/${MEMORY_DREAMS_FILENAME}` });
28748
28799
  }
28749
- if (existsSync10(workspacePath)) {
28800
+ if (existsSync11(workspacePath)) {
28750
28801
  for (const entry of await readdir4(workspacePath)) {
28751
28802
  if (!DAILY_MEMORY_RE.test(entry)) continue;
28752
28803
  const absPath = join14(workspacePath, entry);
@@ -29112,7 +29163,7 @@ var init_memory_backend = __esm({
29112
29163
  const filePath = this.getLongTermPath(rootDir);
29113
29164
  const dir = join14(rootDir, MEMORY_WORKSPACE_PATH);
29114
29165
  try {
29115
- if (!existsSync10(dir)) {
29166
+ if (!existsSync11(dir)) {
29116
29167
  await mkdir6(dir, { recursive: true });
29117
29168
  }
29118
29169
  const tmpPath = filePath + ".tmp";
@@ -29255,7 +29306,7 @@ var init_memory_backend = __esm({
29255
29306
 
29256
29307
  // ../core/src/project-memory.ts
29257
29308
  import { readFile as readFile7 } from "node:fs/promises";
29258
- import { existsSync as existsSync11 } from "node:fs";
29309
+ import { existsSync as existsSync12 } from "node:fs";
29259
29310
  function getDefaultMemoryScaffold() {
29260
29311
  return `# Project Memory
29261
29312
 
@@ -29280,7 +29331,7 @@ function getDefaultMemoryScaffold() {
29280
29331
  }
29281
29332
  async function ensureMemoryFile(rootDir) {
29282
29333
  const longTermPath = memoryLongTermPath(rootDir);
29283
- if (existsSync11(longTermPath)) {
29334
+ if (existsSync12(longTermPath)) {
29284
29335
  return false;
29285
29336
  }
29286
29337
  const { longTermCreated } = await ensureOpenClawMemoryFiles(rootDir);
@@ -29575,7 +29626,7 @@ This project has a memory system that stores durable project learnings.
29575
29626
  }
29576
29627
  async function readProjectMemory(rootDir) {
29577
29628
  const longTermPath = memoryLongTermPath(rootDir);
29578
- if (!existsSync11(longTermPath)) {
29629
+ if (!existsSync12(longTermPath)) {
29579
29630
  return "";
29580
29631
  }
29581
29632
  return readFile7(longTermPath, "utf-8");
@@ -29590,7 +29641,7 @@ var init_project_memory = __esm({
29590
29641
  // ../core/src/run-command.ts
29591
29642
  import { spawn } from "node:child_process";
29592
29643
  function runCommandAsync(command, options = {}) {
29593
- return new Promise((resolve19) => {
29644
+ return new Promise((resolve17) => {
29594
29645
  const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
29595
29646
  let stdout = "";
29596
29647
  let stderr = "";
@@ -29649,7 +29700,7 @@ function runCommandAsync(command, options = {}) {
29649
29700
  clearTimeout(forceKillTimer);
29650
29701
  forceKillTimer = null;
29651
29702
  }
29652
- resolve19({
29703
+ resolve17({
29653
29704
  stdout,
29654
29705
  stderr,
29655
29706
  exitCode: null,
@@ -29667,7 +29718,7 @@ function runCommandAsync(command, options = {}) {
29667
29718
  }
29668
29719
  signalProcessGroup("SIGTERM");
29669
29720
  scheduleForceKill(NORMAL_CLEANUP_FORCE_KILL_DELAY_MS);
29670
- resolve19({
29721
+ resolve17({
29671
29722
  stdout,
29672
29723
  stderr,
29673
29724
  exitCode: code,
@@ -29740,15 +29791,15 @@ import { EventEmitter as EventEmitter12 } from "node:events";
29740
29791
  import { randomUUID as randomUUID6 } from "node:crypto";
29741
29792
  import { mkdir as mkdir7, readdir as readdir5, readFile as readFile8, writeFile as writeFile6, rename as rename4, unlink as unlink3 } from "node:fs/promises";
29742
29793
  import { join as join15 } from "node:path";
29743
- import { existsSync as existsSync12, watch } from "node:fs";
29794
+ import { existsSync as existsSync13, watch } from "node:fs";
29744
29795
  function assertSafeGitBranchName(name) {
29745
29796
  if (!name || name.length > 255 || name.startsWith("-") || name.startsWith(".") || name.startsWith("/") || name.endsWith("/") || name.endsWith(".") || name.endsWith(".lock") || name.includes("..") || name.includes("@{") || !/^[A-Za-z0-9._/+-]+$/.test(name)) {
29746
29797
  throw new Error(`Unsafe git branch name: ${JSON.stringify(name)}`);
29747
29798
  }
29748
29799
  }
29749
29800
  function assertSafeAbsolutePath(path2) {
29750
- const isAbsolute12 = path2.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path2);
29751
- if (!path2 || path2.length > 4096 || !isAbsolute12 || path2.startsWith("-") || // Reject shell metacharacters, quotes, control chars, and NULs.
29801
+ const isAbsolute13 = path2.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path2);
29802
+ if (!path2 || path2.length > 4096 || !isAbsolute13 || path2.startsWith("-") || // Reject shell metacharacters, quotes, control chars, and NULs.
29752
29803
  /["'`$\n\r\t;&|<>()*?[\]{}\\\0]/.test(
29753
29804
  path2.replace(/^[A-Za-z]:/, "")
29754
29805
  // ignore the drive-letter colon on Windows
@@ -29972,7 +30023,7 @@ var init_store = __esm({
29972
30023
  }
29973
30024
  await this.migrateActiveArchivedTasksToArchiveDb();
29974
30025
  await this.importLegacyAgentLogsOnce();
29975
- if (!existsSync12(this.configPath)) {
30026
+ if (!existsSync13(this.configPath)) {
29976
30027
  const config = await this.readConfig();
29977
30028
  try {
29978
30029
  await writeFile6(this.configPath, JSON.stringify(config, null, 2));
@@ -30034,6 +30085,7 @@ var init_store = __esm({
30034
30085
  recoveryRetryCount: row.recoveryRetryCount ?? void 0,
30035
30086
  taskDoneRetryCount: row.taskDoneRetryCount ?? void 0,
30036
30087
  verificationFailureCount: row.verificationFailureCount ?? void 0,
30088
+ mergeConflictBounceCount: row.mergeConflictBounceCount ?? void 0,
30037
30089
  nextRecoveryAt: row.nextRecoveryAt || void 0,
30038
30090
  error: row.error || void 0,
30039
30091
  summary: row.summary || void 0,
@@ -30181,7 +30233,7 @@ ${recentText}` : void 0
30181
30233
  }
30182
30234
  async readPromptForArchive(taskId) {
30183
30235
  const promptPath = join15(this.taskDir(taskId), "PROMPT.md");
30184
- if (!existsSync12(promptPath)) {
30236
+ if (!existsSync13(promptPath)) {
30185
30237
  return void 0;
30186
30238
  }
30187
30239
  return readFile8(promptPath, "utf-8");
@@ -30323,6 +30375,7 @@ ${recentText}` : void 0
30323
30375
  "recoveryRetryCount",
30324
30376
  "taskDoneRetryCount",
30325
30377
  "verificationFailureCount",
30378
+ "mergeConflictBounceCount",
30326
30379
  "nextRecoveryAt",
30327
30380
  "error",
30328
30381
  "summary",
@@ -30425,6 +30478,7 @@ ${outcome}`;
30425
30478
  "recoveryRetryCount",
30426
30479
  "taskDoneRetryCount",
30427
30480
  "verificationFailureCount",
30481
+ "mergeConflictBounceCount",
30428
30482
  "nextRecoveryAt",
30429
30483
  "error",
30430
30484
  "summary",
@@ -30495,7 +30549,7 @@ ${outcome}`;
30495
30549
  id, title, description, priority, "column", status, size, reviewLevel, currentStep,
30496
30550
  worktree, blockedBy, paused, baseBranch, branch, baseCommitSha, modelPresetId, modelProvider,
30497
30551
  modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
30498
- workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, nextRecoveryAt, error,
30552
+ workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, nextRecoveryAt, error,
30499
30553
  summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
30500
30554
  tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
30501
30555
  dependencies, steps, log, attachments, steeringComments,
@@ -30503,7 +30557,7 @@ ${outcome}`;
30503
30557
  sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
30504
30558
  mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, checkedOutBy, checkedOutAt
30505
30559
  ) VALUES (
30506
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
30560
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
30507
30561
  )
30508
30562
  ON CONFLICT(id) DO UPDATE SET
30509
30563
  title = excluded.title,
@@ -30534,6 +30588,7 @@ ${outcome}`;
30534
30588
  recoveryRetryCount = excluded.recoveryRetryCount,
30535
30589
  taskDoneRetryCount = excluded.taskDoneRetryCount,
30536
30590
  verificationFailureCount = excluded.verificationFailureCount,
30591
+ mergeConflictBounceCount = excluded.mergeConflictBounceCount,
30537
30592
  nextRecoveryAt = excluded.nextRecoveryAt,
30538
30593
  error = excluded.error,
30539
30594
  summary = excluded.summary,
@@ -30605,6 +30660,7 @@ ${outcome}`;
30605
30660
  task.recoveryRetryCount ?? null,
30606
30661
  task.taskDoneRetryCount ?? 0,
30607
30662
  task.verificationFailureCount ?? 0,
30663
+ task.mergeConflictBounceCount ?? 0,
30608
30664
  task.nextRecoveryAt ?? null,
30609
30665
  task.error ?? null,
30610
30666
  task.summary ?? null,
@@ -30817,9 +30873,9 @@ ${outcome}`;
30817
30873
  * lost-update races on the nextId counter.
30818
30874
  */
30819
30875
  withConfigLock(fn) {
30820
- let resolve19;
30876
+ let resolve17;
30821
30877
  const next = new Promise((r) => {
30822
- resolve19 = r;
30878
+ resolve17 = r;
30823
30879
  });
30824
30880
  const prev = this.configLock;
30825
30881
  this.configLock = next;
@@ -30827,7 +30883,7 @@ ${outcome}`;
30827
30883
  try {
30828
30884
  return await fn();
30829
30885
  } finally {
30830
- resolve19();
30886
+ resolve17();
30831
30887
  }
30832
30888
  });
30833
30889
  }
@@ -30837,9 +30893,9 @@ ${outcome}`;
30837
30893
  */
30838
30894
  withTaskLock(id, fn) {
30839
30895
  const prev = this.taskLocks.get(id) ?? Promise.resolve();
30840
- let resolve19;
30896
+ let resolve17;
30841
30897
  const next = new Promise((r) => {
30842
- resolve19 = r;
30898
+ resolve17 = r;
30843
30899
  });
30844
30900
  this.taskLocks.set(id, next);
30845
30901
  return prev.then(async () => {
@@ -30849,7 +30905,7 @@ ${outcome}`;
30849
30905
  if (this.taskLocks.get(id) === next) {
30850
30906
  this.taskLocks.delete(id);
30851
30907
  }
30852
- resolve19();
30908
+ resolve17();
30853
30909
  }
30854
30910
  });
30855
30911
  }
@@ -31567,7 +31623,7 @@ ${newTask.description}
31567
31623
  for (const attachment of sourceTask.attachments) {
31568
31624
  const sourcePath = join15(sourceAttachDir, attachment.filename);
31569
31625
  const targetPath = join15(targetAttachDir, attachment.filename);
31570
- if (existsSync12(sourcePath)) {
31626
+ if (existsSync13(sourcePath)) {
31571
31627
  const content = await readFile8(sourcePath);
31572
31628
  await writeFile6(targetPath, content);
31573
31629
  }
@@ -31599,7 +31655,7 @@ ${newTask.description}
31599
31655
  }
31600
31656
  let prompt = "";
31601
31657
  const promptPath = join15(this.taskDir(id), "PROMPT.md");
31602
- if (existsSync12(promptPath)) {
31658
+ if (existsSync13(promptPath)) {
31603
31659
  prompt = await readFile8(promptPath, "utf-8");
31604
31660
  }
31605
31661
  return { ...task, prompt };
@@ -32002,6 +32058,11 @@ ${newTask.description}
32002
32058
  } else if (updates.verificationFailureCount !== void 0) {
32003
32059
  task.verificationFailureCount = updates.verificationFailureCount;
32004
32060
  }
32061
+ if (updates.mergeConflictBounceCount === null) {
32062
+ task.mergeConflictBounceCount = void 0;
32063
+ } else if (updates.mergeConflictBounceCount !== void 0) {
32064
+ task.mergeConflictBounceCount = updates.mergeConflictBounceCount;
32065
+ }
32005
32066
  if (updates.nextRecoveryAt === null) {
32006
32067
  task.nextRecoveryAt = void 0;
32007
32068
  } else if (updates.nextRecoveryAt !== void 0) {
@@ -32121,7 +32182,7 @@ ${newTask.description}
32121
32182
  }
32122
32183
  if (updates.prompt === void 0 && (updates.title !== void 0 || updates.description !== void 0)) {
32123
32184
  const promptPath = join15(dir, "PROMPT.md");
32124
- if (existsSync12(promptPath)) {
32185
+ if (existsSync13(promptPath)) {
32125
32186
  const existingPrompt = await readFile8(promptPath, "utf-8");
32126
32187
  let newPrompt;
32127
32188
  if (task.column === "triage") {
@@ -32422,7 +32483,7 @@ ${task.description}
32422
32483
  async parseStepsFromPrompt(id) {
32423
32484
  const dir = this.taskDir(id);
32424
32485
  const promptPath = join15(dir, "PROMPT.md");
32425
- if (!existsSync12(promptPath)) return [];
32486
+ if (!existsSync13(promptPath)) return [];
32426
32487
  const content = await readFile8(promptPath, "utf-8");
32427
32488
  const steps = [];
32428
32489
  const stepRegex = /^###\s+Step\s+\d+[^:]*:\s*(.+)$/gm;
@@ -32445,7 +32506,7 @@ ${task.description}
32445
32506
  async parseDependenciesFromPrompt(id) {
32446
32507
  const dir = this.taskDir(id);
32447
32508
  const promptPath = join15(dir, "PROMPT.md");
32448
- if (!existsSync12(promptPath)) return [];
32509
+ if (!existsSync13(promptPath)) return [];
32449
32510
  const content = await readFile8(promptPath, "utf-8");
32450
32511
  const headingMatch = content.match(/^##\s+Dependencies\s*$/m);
32451
32512
  if (!headingMatch) return [];
@@ -32469,7 +32530,7 @@ ${task.description}
32469
32530
  async parseFileScopeFromPrompt(id) {
32470
32531
  const dir = this.taskDir(id);
32471
32532
  const promptPath = join15(dir, "PROMPT.md");
32472
- if (!existsSync12(promptPath)) return [];
32533
+ if (!existsSync13(promptPath)) return [];
32473
32534
  const content = await readFile8(promptPath, "utf-8");
32474
32535
  const headingMatch = content.match(/^##\s+File\s+Scope\s*$/m);
32475
32536
  if (!headingMatch) return [];
@@ -32506,7 +32567,7 @@ ${task.description}
32506
32567
  const rewrittenDependents = this.rewriteDependentsAndDeleteTask(id, dependentIds);
32507
32568
  if (this.isWatching) this.taskCache.delete(id);
32508
32569
  const dir = this.taskDir(id);
32509
- if (existsSync12(dir)) {
32570
+ if (existsSync13(dir)) {
32510
32571
  const { rm: rm4 } = await import("node:fs/promises");
32511
32572
  await rm4(dir, { recursive: true });
32512
32573
  }
@@ -32698,7 +32759,7 @@ ${task.description}
32698
32759
  };
32699
32760
  const worktreePath2 = task.worktree;
32700
32761
  const changed = this.clearDoneTransientFields(task);
32701
- if (worktreePath2 && existsSync12(worktreePath2)) {
32762
+ if (worktreePath2 && existsSync13(worktreePath2)) {
32702
32763
  assertSafeAbsolutePath(worktreePath2);
32703
32764
  const removeWorktree = await this.runGitCommand(`git worktree remove "${worktreePath2}" --force`, 12e4);
32704
32765
  if (removeWorktree.exitCode === 0) {
@@ -32765,7 +32826,7 @@ ${task.description}
32765
32826
  # resolve conflicts, then: fn task move ${id} done`
32766
32827
  );
32767
32828
  }
32768
- if (worktreePath && existsSync12(worktreePath)) {
32829
+ if (worktreePath && existsSync13(worktreePath)) {
32769
32830
  assertSafeAbsolutePath(worktreePath);
32770
32831
  const removeWorktree = await this.runGitCommand(`git worktree remove "${worktreePath}" --force`, 12e4);
32771
32832
  if (removeWorktree.exitCode === 0) {
@@ -33004,7 +33065,7 @@ ${task.description}
33004
33065
  }
33005
33066
  }
33006
33067
  }
33007
- await new Promise((resolve19) => setImmediate(resolve19));
33068
+ await new Promise((resolve17) => setImmediate(resolve17));
33008
33069
  const selectClause = this.getTaskSelectClause(true);
33009
33070
  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();
33010
33071
  this.lastPollTime = (/* @__PURE__ */ new Date()).toISOString();
@@ -33024,7 +33085,7 @@ ${task.description}
33024
33085
  this.emit("task:updated", task);
33025
33086
  }
33026
33087
  if (i > 0 && i % 50 === 0) {
33027
- await new Promise((resolve19) => setImmediate(resolve19));
33088
+ await new Promise((resolve17) => setImmediate(resolve17));
33028
33089
  }
33029
33090
  }
33030
33091
  const elapsed = Date.now() - startTime;
@@ -33716,7 +33777,7 @@ ${task.description}
33716
33777
  return rows.map((row) => this.mapAgentLogRow(row));
33717
33778
  }
33718
33779
  async importLegacyAgentLogs() {
33719
- if (!existsSync12(this.tasksDir)) return 0;
33780
+ if (!existsSync13(this.tasksDir)) return 0;
33720
33781
  const entries = await readdir5(this.tasksDir, { withFileTypes: true });
33721
33782
  let imported = 0;
33722
33783
  const insertStmt = this.db.prepare(`
@@ -33726,7 +33787,7 @@ ${task.description}
33726
33787
  for (const entry of entries) {
33727
33788
  if (!entry.isDirectory()) continue;
33728
33789
  const logPath = join15(this.tasksDir, entry.name, "agent.log");
33729
- if (!existsSync12(logPath)) continue;
33790
+ if (!existsSync13(logPath)) continue;
33730
33791
  try {
33731
33792
  const content = await readFile8(logPath, "utf-8");
33732
33793
  for (const line of content.split("\n")) {
@@ -33830,7 +33891,7 @@ ${task.description}
33830
33891
  const cleanedUpIds = [];
33831
33892
  for (const task of archivedTasks) {
33832
33893
  const dir = this.taskDir(task.id);
33833
- if (!existsSync12(dir)) {
33894
+ if (!existsSync13(dir)) {
33834
33895
  continue;
33835
33896
  }
33836
33897
  const entry = await this.taskToArchiveEntry(task, (/* @__PURE__ */ new Date()).toISOString());
@@ -34564,17 +34625,17 @@ var init_daemon_token = __esm({
34564
34625
  });
34565
34626
 
34566
34627
  // ../core/src/pi-extensions.ts
34567
- import { existsSync as existsSync13, mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync2, statSync as statSync3, writeFileSync } from "node:fs";
34628
+ import { existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync2, statSync as statSync3, writeFileSync } from "node:fs";
34568
34629
  import { homedir as homedir3 } from "node:os";
34569
- import { basename as basename5, join as join16, relative as relative2, resolve as resolve6, sep as sep3 } from "node:path";
34570
- function getHomeDir(home) {
34630
+ import { basename as basename5, isAbsolute as isAbsolute5, join as join16, relative as relative2, resolve as resolve6, sep as sep3, win32 } from "node:path";
34631
+ function getHomeDir3(home) {
34571
34632
  return home ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir3();
34572
34633
  }
34573
34634
  function getFusionAgentDir(home) {
34574
- return join16(getHomeDir(home), ".fusion", "agent");
34635
+ return join16(getHomeDir3(home), ".fusion", "agent");
34575
34636
  }
34576
34637
  function getLegacyPiAgentDir(home) {
34577
- return join16(getHomeDir(home), ".pi", "agent");
34638
+ return join16(getHomeDir3(home), ".pi", "agent");
34578
34639
  }
34579
34640
  function getFusionAgentSettingsPath(home) {
34580
34641
  return join16(getFusionAgentDir(home), "settings.json");
@@ -34582,7 +34643,7 @@ function getFusionAgentSettingsPath(home) {
34582
34643
  function resolvePiExtensionProjectRoot(cwd) {
34583
34644
  let current = resolve6(cwd);
34584
34645
  while (true) {
34585
- if (existsSync13(join16(current, ".fusion"))) {
34646
+ if (existsSync14(join16(current, ".fusion"))) {
34586
34647
  return current;
34587
34648
  }
34588
34649
  const parent = resolve6(current, "..");
@@ -34619,21 +34680,21 @@ function readPiManifest(packageJsonPath) {
34619
34680
  }
34620
34681
  function resolveExtensionEntries(dir) {
34621
34682
  const packageJsonPath = join16(dir, "package.json");
34622
- if (existsSync13(packageJsonPath)) {
34683
+ if (existsSync14(packageJsonPath)) {
34623
34684
  const manifest = readPiManifest(packageJsonPath);
34624
34685
  if (manifest?.extensions?.length) {
34625
- const entries = manifest.extensions.map((entry) => resolve6(dir, entry)).filter((entry) => existsSync13(entry));
34686
+ const entries = manifest.extensions.map((entry) => resolve6(dir, entry)).filter((entry) => existsSync14(entry));
34626
34687
  if (entries.length > 0) return entries;
34627
34688
  }
34628
34689
  }
34629
34690
  const indexTs = join16(dir, "index.ts");
34630
- if (existsSync13(indexTs)) return [indexTs];
34691
+ if (existsSync14(indexTs)) return [indexTs];
34631
34692
  const indexJs = join16(dir, "index.js");
34632
- if (existsSync13(indexJs)) return [indexJs];
34693
+ if (existsSync14(indexJs)) return [indexJs];
34633
34694
  return null;
34634
34695
  }
34635
34696
  function discoverExtensionsInDir(dir, cwd, home) {
34636
- if (!existsSync13(dir)) return [];
34697
+ if (!existsSync14(dir)) return [];
34637
34698
  const discovered = [];
34638
34699
  try {
34639
34700
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -34753,10 +34814,24 @@ function reconcileClaudeCliPaths(paths, vendoredPath) {
34753
34814
  }
34754
34815
  return filtered;
34755
34816
  }
34817
+ function getDisplayPathWithinRoot(root, targetPath) {
34818
+ const usesWindowsPaths = /^[A-Za-z]:[\\/]/.test(root) || /^[A-Za-z]:[\\/]/.test(targetPath) || root.includes("\\") || targetPath.includes("\\");
34819
+ const pathApi = usesWindowsPaths ? win32 : { relative: relative2, isAbsolute: isAbsolute5, sep: sep3 };
34820
+ const rel = pathApi.relative(root, targetPath);
34821
+ if (rel === "") {
34822
+ return "";
34823
+ }
34824
+ if (!rel || rel === ".." || rel.startsWith(`..${pathApi.sep}`) || pathApi.isAbsolute(rel)) {
34825
+ return null;
34826
+ }
34827
+ return rel.split(pathApi.sep).join("/");
34828
+ }
34756
34829
  function formatPiExtensionSource(source, extensionPath, cwd, home) {
34757
- const homeDir = getHomeDir(home);
34830
+ const homeDir = getHomeDir3(home);
34758
34831
  const projectRoot = resolvePiExtensionProjectRoot(cwd);
34759
- const relativePath = extensionPath.startsWith(homeDir) ? `~${extensionPath.slice(homeDir.length)}` : extensionPath.startsWith(projectRoot) ? relative2(projectRoot, extensionPath).split(sep3).join("/") : extensionPath;
34832
+ const relativeToHome = getDisplayPathWithinRoot(homeDir, extensionPath);
34833
+ const relativeToProject = getDisplayPathWithinRoot(projectRoot, extensionPath);
34834
+ const relativePath = relativeToHome !== null ? relativeToHome.length > 0 ? `~/${relativeToHome}` : "~" : relativeToProject !== null ? relativeToProject || "." : extensionPath;
34760
34835
  return `${source}: ${relativePath}`;
34761
34836
  }
34762
34837
  var FUSION_DISABLED_EXTENSIONS_KEY;
@@ -34850,7 +34925,7 @@ function runGh(args, cwd) {
34850
34925
  }
34851
34926
  function runGhAsync(args, cwdOrOptions) {
34852
34927
  const { cwd, signal: externalSignal, timeoutMs = DEFAULT_GH_TIMEOUT_MS } = normalizeRunGhOptions(cwdOrOptions);
34853
- return new Promise((resolve19, reject) => {
34928
+ return new Promise((resolve17, reject) => {
34854
34929
  if (externalSignal?.aborted) {
34855
34930
  reject(makeGhError(`gh command aborted: ${describeAbortReason(externalSignal.reason)}`, "ABORT_ERR"));
34856
34931
  return;
@@ -34901,7 +34976,7 @@ function runGhAsync(args, cwdOrOptions) {
34901
34976
  ghError.stderr = stderr ?? "";
34902
34977
  reject(ghError);
34903
34978
  } else {
34904
- resolve19(stdout ?? "");
34979
+ resolve17(stdout ?? "");
34905
34980
  }
34906
34981
  }
34907
34982
  );
@@ -35189,9 +35264,9 @@ var init_routine_store = __esm({
35189
35264
  */
35190
35265
  withRoutineLock(id, fn) {
35191
35266
  const prev = this.routineLocks.get(id) ?? Promise.resolve();
35192
- let resolve19;
35267
+ let resolve17;
35193
35268
  const next = new Promise((r) => {
35194
- resolve19 = r;
35269
+ resolve17 = r;
35195
35270
  });
35196
35271
  this.routineLocks.set(id, next);
35197
35272
  return prev.then(async () => {
@@ -35201,7 +35276,7 @@ var init_routine_store = __esm({
35201
35276
  if (this.routineLocks.get(id) === next) {
35202
35277
  this.routineLocks.delete(id);
35203
35278
  }
35204
- resolve19();
35279
+ resolve17();
35205
35280
  }
35206
35281
  });
35207
35282
  }
@@ -35534,7 +35609,7 @@ var init_dispatcher = __esm({
35534
35609
  });
35535
35610
 
35536
35611
  // ../core/src/plugin-loader.ts
35537
- import { basename as basename6, dirname as dirname5, extname, isAbsolute as isAbsolute5, resolve as resolve7 } from "node:path";
35612
+ import { basename as basename6, dirname as dirname5, extname, isAbsolute as isAbsolute6, resolve as resolve7 } from "node:path";
35538
35613
  import { copyFile, rm } from "node:fs/promises";
35539
35614
  import { pathToFileURL } from "node:url";
35540
35615
  import { EventEmitter as EventEmitter14 } from "node:events";
@@ -35673,7 +35748,7 @@ var init_plugin_loader = __esm({
35673
35748
  }
35674
35749
  }
35675
35750
  resolvePluginPath(path2) {
35676
- if (isAbsolute5(path2)) {
35751
+ if (isAbsolute6(path2)) {
35677
35752
  return path2;
35678
35753
  }
35679
35754
  if (path2.startsWith("@") || path2.includes("/")) {
@@ -35800,13 +35875,13 @@ var init_plugin_loader = __esm({
35800
35875
  * Execute a promise with a timeout.
35801
35876
  */
35802
35877
  withTimeout(promise, ms, timeoutMessage) {
35803
- return new Promise((resolve19, reject) => {
35878
+ return new Promise((resolve17, reject) => {
35804
35879
  const timer = setTimeout(() => {
35805
35880
  reject(new Error(timeoutMessage));
35806
35881
  }, ms);
35807
35882
  promise.then((result) => {
35808
35883
  clearTimeout(timer);
35809
- resolve19(result);
35884
+ resolve17(result);
35810
35885
  }).catch((err) => {
35811
35886
  clearTimeout(timer);
35812
35887
  reject(err);
@@ -36092,7 +36167,7 @@ var init_plugin_loader = __esm({
36092
36167
 
36093
36168
  // ../core/src/backup.ts
36094
36169
  import { cp, mkdir as mkdir8, readdir as readdir6, stat as stat3, unlink as unlink4 } from "node:fs/promises";
36095
- import { existsSync as existsSync14 } from "node:fs";
36170
+ import { existsSync as existsSync15 } from "node:fs";
36096
36171
  import { join as join17 } from "node:path";
36097
36172
  function generateBackupFilename() {
36098
36173
  return `fusion-${formatTimestamp(/* @__PURE__ */ new Date())}.db`;
@@ -36276,7 +36351,7 @@ var init_backup = __esm({
36276
36351
  let filename = generateBackupFilename();
36277
36352
  let targetPath = join17(backupDirPath, filename);
36278
36353
  let counter = 1;
36279
- while (existsSync14(targetPath)) {
36354
+ while (existsSync15(targetPath)) {
36280
36355
  const baseName = filename.replace(/\.db$/, "");
36281
36356
  filename = `${baseName}-${counter}.db`;
36282
36357
  targetPath = join17(backupDirPath, filename);
@@ -36653,10 +36728,88 @@ async function summarizeTitle(description, rootDir, provider, modelId) {
36653
36728
  }
36654
36729
  }
36655
36730
  }
36731
+ async function summarizeCommitBody(diffStat, rootDir, provider, modelId, opts) {
36732
+ const trimmedStat = (diffStat ?? "").trim();
36733
+ if (trimmedStat.length === 0) {
36734
+ return null;
36735
+ }
36736
+ const truncatedStat = trimmedStat.length > MAX_COMMIT_BODY_INPUT_LENGTH ? trimmedStat.slice(0, MAX_COMMIT_BODY_INPUT_LENGTH) + "\n\u2026(truncated)" : trimmedStat;
36737
+ const userPromptParts = [];
36738
+ if (opts?.branch) userPromptParts.push(`Branch: ${opts.branch}`);
36739
+ if (opts?.taskId) userPromptParts.push(`Task: ${opts.taskId}`);
36740
+ if (userPromptParts.length > 0) userPromptParts.push("");
36741
+ userPromptParts.push("Files changed (`git diff --stat`):");
36742
+ userPromptParts.push(truncatedStat);
36743
+ userPromptParts.push("");
36744
+ userPromptParts.push("Write the commit body now.");
36745
+ const userPrompt = userPromptParts.join("\n");
36746
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_COMMIT_BODY_TIMEOUT_MS;
36747
+ const aborter = new AbortController();
36748
+ const timer = setTimeout(() => aborter.abort(), timeoutMs);
36749
+ if (opts?.signal) {
36750
+ if (opts.signal.aborted) aborter.abort();
36751
+ else opts.signal.addEventListener("abort", () => aborter.abort(), { once: true });
36752
+ }
36753
+ let session;
36754
+ try {
36755
+ const createFnAgent5 = await getFnAgent();
36756
+ if (!createFnAgent5) {
36757
+ if (DEBUG) console.log("[ai-summarize] AI engine not available for commit body");
36758
+ return null;
36759
+ }
36760
+ const agentOptions = {
36761
+ cwd: rootDir,
36762
+ systemPrompt: COMMIT_BODY_SYSTEM_PROMPT,
36763
+ tools: "readonly"
36764
+ };
36765
+ if (provider && modelId) {
36766
+ agentOptions.defaultProvider = provider;
36767
+ agentOptions.defaultModelId = modelId;
36768
+ }
36769
+ const agentResult = await createFnAgent5(agentOptions);
36770
+ if (!agentResult?.session) return null;
36771
+ session = agentResult.session;
36772
+ await session.prompt(userPrompt);
36773
+ if (aborter.signal.aborted) return null;
36774
+ if (session.state?.error) {
36775
+ if (DEBUG) console.log(`[ai-summarize] Commit-body session error: ${session.state.error}`);
36776
+ return null;
36777
+ }
36778
+ const messages = session.state?.messages ?? [];
36779
+ const assistant = messages.filter((m) => m.role === "assistant").pop();
36780
+ if (!assistant?.content) return null;
36781
+ let body = "";
36782
+ if (typeof assistant.content === "string") {
36783
+ body = assistant.content;
36784
+ } else if (Array.isArray(assistant.content)) {
36785
+ body = assistant.content.filter(
36786
+ (c) => c.type === "text" && typeof c.text === "string"
36787
+ ).map((c) => c.text).join("");
36788
+ }
36789
+ body = body.trim();
36790
+ if (!body) return null;
36791
+ if (body.length > MAX_COMMIT_BODY_LENGTH) {
36792
+ body = body.slice(0, MAX_COMMIT_BODY_LENGTH).trim();
36793
+ }
36794
+ return body;
36795
+ } catch (err) {
36796
+ if (DEBUG) {
36797
+ const message = err instanceof Error ? err.message : String(err);
36798
+ console.log(`[ai-summarize] Commit-body generation failed: ${message}`);
36799
+ }
36800
+ return null;
36801
+ } finally {
36802
+ clearTimeout(timer);
36803
+ try {
36804
+ session?.dispose?.();
36805
+ } catch {
36806
+ }
36807
+ }
36808
+ }
36656
36809
  function __resetSummarizeState() {
36657
36810
  rateLimits.clear();
36658
36811
  }
36659
- var SUMMARIZE_SYSTEM_PROMPT, MAX_DESCRIPTION_LENGTH, MIN_DESCRIPTION_LENGTH, MAX_TITLE_LENGTH, MAX_REQUESTS_PER_HOUR, RATE_LIMIT_WINDOW_MS, CLEANUP_INTERVAL_MS, rateLimits, cleanupInterval, ValidationError, RateLimitError, AiServiceError, DEBUG;
36812
+ var SUMMARIZE_SYSTEM_PROMPT, MAX_DESCRIPTION_LENGTH, MIN_DESCRIPTION_LENGTH, MAX_TITLE_LENGTH, MAX_REQUESTS_PER_HOUR, RATE_LIMIT_WINDOW_MS, CLEANUP_INTERVAL_MS, rateLimits, cleanupInterval, ValidationError, RateLimitError, AiServiceError, DEBUG, COMMIT_BODY_SYSTEM_PROMPT, MAX_COMMIT_BODY_INPUT_LENGTH, MAX_COMMIT_BODY_LENGTH, DEFAULT_COMMIT_BODY_TIMEOUT_MS;
36660
36813
  var init_ai_summarize = __esm({
36661
36814
  "../core/src/ai-summarize.ts"() {
36662
36815
  "use strict";
@@ -36701,6 +36854,19 @@ Your job is to create a concise title (max 60 characters) that summarizes the gi
36701
36854
  }
36702
36855
  };
36703
36856
  DEBUG = process.env.FUSION_DEBUG_AI === "true";
36857
+ COMMIT_BODY_SYSTEM_PROMPT = `You write concise commit message bodies for merge commits.
36858
+
36859
+ Your job is to summarize the changes described in a \`git diff --stat\` into a short, useful body.
36860
+
36861
+ ## Guidelines
36862
+ - Output ONLY the body text \u2014 no code fences, no preamble, no subject line
36863
+ - 2\u20136 short bullet points starting with "- "
36864
+ - Be specific about what changed; reference filenames where helpful
36865
+ - Keep total output under 600 characters
36866
+ - Do not invent details that aren't in the input \u2014 if uncertain, stay general`;
36867
+ MAX_COMMIT_BODY_INPUT_LENGTH = 4e3;
36868
+ MAX_COMMIT_BODY_LENGTH = 2e3;
36869
+ DEFAULT_COMMIT_BODY_TIMEOUT_MS = 3e4;
36704
36870
  }
36705
36871
  });
36706
36872
 
@@ -37028,18 +37194,18 @@ var init_mission_types = __esm({
37028
37194
 
37029
37195
  // ../core/src/memory-insights.ts
37030
37196
  import { readFile as readFile10, writeFile as writeFile8, mkdir as mkdir9 } from "node:fs/promises";
37031
- import { existsSync as existsSync15 } from "node:fs";
37197
+ import { existsSync as existsSync16 } from "node:fs";
37032
37198
  import { dirname as dirname6, join as join18 } from "node:path";
37033
37199
  async function readWorkingMemory(rootDir) {
37034
37200
  const filePath = join18(rootDir, MEMORY_WORKING_PATH);
37035
- if (!existsSync15(filePath)) {
37201
+ if (!existsSync16(filePath)) {
37036
37202
  return "";
37037
37203
  }
37038
37204
  return readFile10(filePath, "utf-8");
37039
37205
  }
37040
37206
  async function readInsightsMemory(rootDir) {
37041
37207
  const filePath = join18(rootDir, MEMORY_INSIGHTS_PATH);
37042
- if (!existsSync15(filePath)) {
37208
+ if (!existsSync16(filePath)) {
37043
37209
  return null;
37044
37210
  }
37045
37211
  return readFile10(filePath, "utf-8");
@@ -37047,7 +37213,7 @@ async function readInsightsMemory(rootDir) {
37047
37213
  async function writeInsightsMemory(rootDir, content) {
37048
37214
  const filePath = join18(rootDir, MEMORY_INSIGHTS_PATH);
37049
37215
  const dir = join18(rootDir, ".fusion");
37050
- if (!existsSync15(dir)) {
37216
+ if (!existsSync16(dir)) {
37051
37217
  await mkdir9(dir, { recursive: true });
37052
37218
  }
37053
37219
  await writeFile8(filePath, content, "utf-8");
@@ -37055,14 +37221,14 @@ async function writeInsightsMemory(rootDir, content) {
37055
37221
  async function writeWorkingMemory(rootDir, content) {
37056
37222
  const filePath = join18(rootDir, MEMORY_WORKING_PATH);
37057
37223
  const dir = dirname6(filePath);
37058
- if (!existsSync15(dir)) {
37224
+ if (!existsSync16(dir)) {
37059
37225
  await mkdir9(dir, { recursive: true });
37060
37226
  }
37061
37227
  await writeFile8(filePath, content, "utf-8");
37062
37228
  }
37063
37229
  async function readMemoryAudit(rootDir) {
37064
37230
  const filePath = join18(rootDir, MEMORY_AUDIT_PATH);
37065
- if (!existsSync15(filePath)) {
37231
+ if (!existsSync16(filePath)) {
37066
37232
  return null;
37067
37233
  }
37068
37234
  return readFile10(filePath, "utf-8");
@@ -37070,14 +37236,14 @@ async function readMemoryAudit(rootDir) {
37070
37236
  async function writeMemoryAudit(rootDir, content) {
37071
37237
  const filePath = join18(rootDir, MEMORY_AUDIT_PATH);
37072
37238
  const dir = join18(rootDir, ".fusion");
37073
- if (!existsSync15(dir)) {
37239
+ if (!existsSync16(dir)) {
37074
37240
  await mkdir9(dir, { recursive: true });
37075
37241
  }
37076
37242
  await writeFile8(filePath, content, "utf-8");
37077
37243
  }
37078
37244
  async function readMemoryAuditState(rootDir) {
37079
37245
  const filePath = join18(rootDir, MEMORY_AUDIT_STATE_PATH);
37080
- if (!existsSync15(filePath)) {
37246
+ if (!existsSync16(filePath)) {
37081
37247
  return null;
37082
37248
  }
37083
37249
  try {
@@ -37097,7 +37263,7 @@ async function readMemoryAuditState(rootDir) {
37097
37263
  async function writeMemoryAuditState(rootDir, state) {
37098
37264
  const filePath = join18(rootDir, MEMORY_AUDIT_STATE_PATH);
37099
37265
  const dir = join18(rootDir, ".fusion");
37100
- if (!existsSync15(dir)) {
37266
+ if (!existsSync16(dir)) {
37101
37267
  await mkdir9(dir, { recursive: true });
37102
37268
  }
37103
37269
  await writeFile8(filePath, JSON.stringify(state, null, 2), "utf-8");
@@ -37574,7 +37740,7 @@ async function generateMemoryAudit(rootDir, lastExtraction, pruningOutcome) {
37574
37740
  const effectiveExtraction = lastExtraction ?? persistedState?.extraction;
37575
37741
  const effectivePruning = pruningOutcome ?? persistedState?.pruning;
37576
37742
  const workingMemoryPath = join18(rootDir, MEMORY_WORKING_PATH);
37577
- const workingMemoryExists = existsSync15(workingMemoryPath);
37743
+ const workingMemoryExists = existsSync16(workingMemoryPath);
37578
37744
  let workingMemorySize = 0;
37579
37745
  let workingMemorySectionCount = 0;
37580
37746
  let workingMemoryContent = "";
@@ -37629,7 +37795,7 @@ async function generateMemoryAudit(rootDir, lastExtraction, pruningOutcome) {
37629
37795
  });
37630
37796
  }
37631
37797
  const insightsMemoryPath = join18(rootDir, MEMORY_INSIGHTS_PATH);
37632
- const insightsMemoryExists = existsSync15(insightsMemoryPath);
37798
+ const insightsMemoryExists = existsSync16(insightsMemoryPath);
37633
37799
  let insightsMemorySize = 0;
37634
37800
  let insightsMemoryContent = "";
37635
37801
  const categoryCounts = {
@@ -39013,7 +39179,7 @@ var require_get_stream = __commonJS({
39013
39179
  };
39014
39180
  const { maxBuffer } = options;
39015
39181
  let stream;
39016
- await new Promise((resolve19, reject) => {
39182
+ await new Promise((resolve17, reject) => {
39017
39183
  const rejectPromise = (error) => {
39018
39184
  if (error && stream.getBufferedLength() <= BufferConstants.MAX_LENGTH) {
39019
39185
  error.bufferedData = stream.getBufferedValue();
@@ -39025,7 +39191,7 @@ var require_get_stream = __commonJS({
39025
39191
  rejectPromise(error);
39026
39192
  return;
39027
39193
  }
39028
- resolve19();
39194
+ resolve17();
39029
39195
  });
39030
39196
  stream.on("data", () => {
39031
39197
  if (stream.getBufferedLength() > maxBuffer) {
@@ -40319,7 +40485,7 @@ var require_extract_zip = __commonJS({
40319
40485
  debug("opening", this.zipPath, "with opts", this.opts);
40320
40486
  this.zipfile = await openZip(this.zipPath, { lazyEntries: true });
40321
40487
  this.canceled = false;
40322
- return new Promise((resolve19, reject) => {
40488
+ return new Promise((resolve17, reject) => {
40323
40489
  this.zipfile.on("error", (err) => {
40324
40490
  this.canceled = true;
40325
40491
  reject(err);
@@ -40328,7 +40494,7 @@ var require_extract_zip = __commonJS({
40328
40494
  this.zipfile.on("close", () => {
40329
40495
  if (!this.canceled) {
40330
40496
  debug("zip extraction complete");
40331
- resolve19();
40497
+ resolve17();
40332
40498
  }
40333
40499
  });
40334
40500
  this.zipfile.on("entry", async (entry) => {
@@ -47737,7 +47903,7 @@ var require_dist3 = __commonJS({
47737
47903
  });
47738
47904
 
47739
47905
  // ../core/src/agent-companies-parser.ts
47740
- import { existsSync as existsSync16, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync3, rmSync, statSync as statSync4 } from "node:fs";
47906
+ import { existsSync as existsSync17, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync3, rmSync, statSync as statSync4 } from "node:fs";
47741
47907
  import { tmpdir as tmpdir2 } from "node:os";
47742
47908
  import { join as join19, resolve as resolve8 } from "node:path";
47743
47909
  function slugifyAgentReference(value) {
@@ -47778,10 +47944,10 @@ function pushAlias(aliases, value) {
47778
47944
  if (pathRef !== normalized && pathRef.length > 0) {
47779
47945
  aliases.add(pathRef);
47780
47946
  }
47781
- const basename9 = extractPathBasename(value);
47782
- if (basename9) {
47783
- aliases.add(basename9);
47784
- const basenameSlug = slugifyAgentReference(basename9);
47947
+ const basename12 = extractPathBasename(value);
47948
+ if (basename12) {
47949
+ aliases.add(basename12);
47950
+ const basenameSlug = slugifyAgentReference(basename12);
47785
47951
  if (basenameSlug.length > 0) {
47786
47952
  aliases.add(basenameSlug);
47787
47953
  }
@@ -47965,14 +48131,14 @@ function parseManifestFile(filePath, parser) {
47965
48131
  }
47966
48132
  function parseManifestSubdirectories(rootDir, sectionDir, filename, parser) {
47967
48133
  const sectionPath = join19(rootDir, sectionDir);
47968
- if (!existsSync16(sectionPath)) {
48134
+ if (!existsSync17(sectionPath)) {
47969
48135
  return [];
47970
48136
  }
47971
48137
  const manifests = [];
47972
48138
  const entries = readdirSync2(sectionPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
47973
48139
  for (const entry of entries) {
47974
48140
  const manifestPath = join19(sectionPath, entry.name, filename);
47975
- if (!existsSync16(manifestPath)) {
48141
+ if (!existsSync17(manifestPath)) {
47976
48142
  continue;
47977
48143
  }
47978
48144
  manifests.push(parseManifestFile(manifestPath, parser));
@@ -48010,7 +48176,7 @@ function walkTeamIncludes(teams) {
48010
48176
  }
48011
48177
  function parseCompanyDirectory(dirPath) {
48012
48178
  const resolvedPath = resolve8(dirPath);
48013
- if (!existsSync16(resolvedPath)) {
48179
+ if (!existsSync17(resolvedPath)) {
48014
48180
  throw new AgentCompaniesParseError(`Company directory does not exist: ${resolvedPath}`);
48015
48181
  }
48016
48182
  if (!statSync4(resolvedPath).isDirectory()) {
@@ -48020,7 +48186,7 @@ function parseCompanyDirectory(dirPath) {
48020
48186
  const teams = parseManifestSubdirectories(resolvedPath, "teams", "TEAM.md", parseTeamManifest);
48021
48187
  walkTeamIncludes(teams);
48022
48188
  return {
48023
- company: existsSync16(companyPath) ? parseManifestFile(companyPath, parseCompanyManifest) : void 0,
48189
+ company: existsSync17(companyPath) ? parseManifestFile(companyPath, parseCompanyManifest) : void 0,
48024
48190
  agents: parseManifestSubdirectories(resolvedPath, "agents", "AGENTS.md", parseAgentManifest),
48025
48191
  teams,
48026
48192
  projects: parseManifestSubdirectories(
@@ -48034,7 +48200,7 @@ function parseCompanyDirectory(dirPath) {
48034
48200
  };
48035
48201
  }
48036
48202
  function resolveExtractionRoot(tempDir) {
48037
- if (existsSync16(join19(tempDir, "COMPANY.md"))) {
48203
+ if (existsSync17(join19(tempDir, "COMPANY.md"))) {
48038
48204
  return tempDir;
48039
48205
  }
48040
48206
  const directories = readdirSync2(tempDir, { withFileTypes: true }).filter(
@@ -48042,7 +48208,7 @@ function resolveExtractionRoot(tempDir) {
48042
48208
  );
48043
48209
  for (const directory of directories) {
48044
48210
  const candidate = join19(tempDir, directory.name);
48045
- if (existsSync16(join19(candidate, "COMPANY.md"))) {
48211
+ if (existsSync17(join19(candidate, "COMPANY.md"))) {
48046
48212
  return candidate;
48047
48213
  }
48048
48214
  }
@@ -48880,6 +49046,7 @@ __export(src_exports, {
48880
49046
  COLUMNS: () => COLUMNS,
48881
49047
  COLUMN_DESCRIPTIONS: () => COLUMN_DESCRIPTIONS,
48882
49048
  COLUMN_LABELS: () => COLUMN_LABELS,
49049
+ COMMIT_BODY_SYSTEM_PROMPT: () => COMMIT_BODY_SYSTEM_PROMPT,
48883
49050
  COMPACT_MEMORY_SYSTEM_PROMPT: () => COMPACT_MEMORY_SYSTEM_PROMPT,
48884
49051
  CentralCore: () => CentralCore,
48885
49052
  CentralDatabase: () => CentralDatabase,
@@ -48889,6 +49056,7 @@ __export(src_exports, {
48889
49056
  DAEMON_TOKEN_PREFIX: () => DAEMON_TOKEN_PREFIX,
48890
49057
  DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS: () => DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS,
48891
49058
  DEFAULT_AUTO_SUMMARIZE_SCHEDULE: () => DEFAULT_AUTO_SUMMARIZE_SCHEDULE,
49059
+ DEFAULT_COMMIT_BODY_TIMEOUT_MS: () => DEFAULT_COMMIT_BODY_TIMEOUT_MS,
48892
49060
  DEFAULT_EXECUTION_MODE: () => DEFAULT_EXECUTION_MODE,
48893
49061
  DEFAULT_GLOBAL_SETTINGS: () => DEFAULT_GLOBAL_SETTINGS,
48894
49062
  DEFAULT_INSIGHT_SCHEDULE: () => DEFAULT_INSIGHT_SCHEDULE,
@@ -48910,6 +49078,8 @@ __export(src_exports, {
48910
49078
  INSIGHT_EXTRACTION_SCHEDULE_NAME: () => INSIGHT_EXTRACTION_SCHEDULE_NAME,
48911
49079
  INTERVIEW_STATES: () => INTERVIEW_STATES,
48912
49080
  InsightStore: () => InsightStore,
49081
+ MAX_COMMIT_BODY_INPUT_LENGTH: () => MAX_COMMIT_BODY_INPUT_LENGTH,
49082
+ MAX_COMMIT_BODY_LENGTH: () => MAX_COMMIT_BODY_LENGTH,
48913
49083
  MAX_DESCRIPTION_LENGTH: () => MAX_DESCRIPTION_LENGTH,
48914
49084
  MAX_REQUESTS_PER_HOUR: () => MAX_REQUESTS_PER_HOUR,
48915
49085
  MAX_ROUTINE_RUN_HISTORY: () => MAX_ROUTINE_RUN_HISTORY,
@@ -49069,6 +49239,7 @@ __export(src_exports, {
49069
49239
  isValidPermission: () => isValidPermission,
49070
49240
  isValidPromptKey: () => isValidPromptKey,
49071
49241
  isValidPromptOverrideMap: () => isValidPromptOverrideMap,
49242
+ isValidSqliteDatabaseFile: () => isValidSqliteDatabaseFile,
49072
49243
  isWebhookTrigger: () => isWebhookTrigger,
49073
49244
  listAgentMemoryFiles: () => listAgentMemoryFiles,
49074
49245
  listMemoryBackendTypes: () => listMemoryBackendTypes,
@@ -49153,6 +49324,7 @@ __export(src_exports, {
49153
49324
  shouldTriggerExtraction: () => shouldTriggerExtraction,
49154
49325
  slugify: () => slugify,
49155
49326
  sortTasksByPriorityThenAgeAndId: () => sortTasksByPriorityThenAgeAndId,
49327
+ summarizeCommitBody: () => summarizeCommitBody,
49156
49328
  summarizeTitle: () => summarizeTitle,
49157
49329
  syncAutoSummarizeAutomation: () => syncAutoSummarizeAutomation,
49158
49330
  syncBackupAutomation: () => syncBackupAutomation,
@@ -49195,6 +49367,7 @@ var init_src = __esm({
49195
49367
  init_archive_db();
49196
49368
  init_db_migrate();
49197
49369
  init_global_settings();
49370
+ init_sqlite_validation();
49198
49371
  init_daemon_token();
49199
49372
  init_pi_extensions();
49200
49373
  init_board();
@@ -49487,7 +49660,7 @@ var init_agent_logger = __esm({
49487
49660
 
49488
49661
  // ../engine/src/agent-tools.ts
49489
49662
  import { appendFile as appendFile2, mkdir as mkdir11, readFile as readFile11, readdir as readdir7, stat as stat4, writeFile as writeFile10 } from "node:fs/promises";
49490
- import { existsSync as existsSync17 } from "node:fs";
49663
+ import { existsSync as existsSync18 } from "node:fs";
49491
49664
  import { createHash as createHash4 } from "node:crypto";
49492
49665
  import { join as join21 } from "node:path";
49493
49666
  import { Type } from "@mariozechner/pi-ai";
@@ -49546,7 +49719,7 @@ async function syncAgentMemoryFile(rootDir, agentMemory) {
49546
49719
  const dir = agentMemoryDirectory(rootDir, agentMemory.agentId);
49547
49720
  await mkdir11(dir, { recursive: true });
49548
49721
  const longTermPath = agentMemoryFilePath(rootDir, agentMemory.agentId);
49549
- if (!existsSync17(longTermPath)) {
49722
+ if (!existsSync18(longTermPath)) {
49550
49723
  const title = agentMemory.agentName?.trim() ? `# Agent Memory: ${agentMemory.agentName.trim()}` : "# Agent Memory";
49551
49724
  const fileContent = `${title}
49552
49725
 
@@ -49557,11 +49730,11 @@ ${content || ""}
49557
49730
  await writeFile10(longTermPath, fileContent, "utf-8");
49558
49731
  }
49559
49732
  const dreamsPath = agentDreamsFilePath(rootDir, agentMemory.agentId);
49560
- if (!existsSync17(dreamsPath)) {
49733
+ if (!existsSync18(dreamsPath)) {
49561
49734
  await writeFile10(dreamsPath, "# Agent Memory Dreams\n\n<!-- Synthesized patterns from this agent's daily notes. -->\n", "utf-8");
49562
49735
  }
49563
49736
  const dailyPath = agentDailyFilePath(rootDir, agentMemory.agentId);
49564
- if (!existsSync17(dailyPath)) {
49737
+ if (!existsSync18(dailyPath)) {
49565
49738
  await writeFile10(dailyPath, `# Agent Daily Memory ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
49566
49739
 
49567
49740
  <!-- Running observations for this agent. -->
@@ -50377,12 +50550,12 @@ var init_concurrency = __esm({
50377
50550
  this._active++;
50378
50551
  return Promise.resolve();
50379
50552
  }
50380
- return new Promise((resolve19) => {
50553
+ return new Promise((resolve17) => {
50381
50554
  this._waiters.push({
50382
50555
  priority,
50383
50556
  resolve: () => {
50384
50557
  this._active++;
50385
- resolve19();
50558
+ resolve17();
50386
50559
  }
50387
50560
  });
50388
50561
  });
@@ -50446,10 +50619,10 @@ var init_concurrency = __esm({
50446
50619
  });
50447
50620
 
50448
50621
  // ../engine/src/skill-resolver.ts
50449
- import { existsSync as existsSync18, readFileSync as readFileSync4 } from "node:fs";
50622
+ import { existsSync as existsSync19, readFileSync as readFileSync4 } from "node:fs";
50450
50623
  import { join as join22 } from "node:path";
50451
50624
  function readJsonObject(path2) {
50452
- if (!existsSync18(path2)) {
50625
+ if (!existsSync19(path2)) {
50453
50626
  return {};
50454
50627
  }
50455
50628
  try {
@@ -50461,7 +50634,7 @@ function readJsonObject(path2) {
50461
50634
  }
50462
50635
  function readProjectSettings(projectRootDir) {
50463
50636
  const fusionSettings = join22(projectRootDir, ".fusion", "settings.json");
50464
- if (existsSync18(fusionSettings)) {
50637
+ if (existsSync19(fusionSettings)) {
50465
50638
  const parsed = readJsonObject(fusionSettings);
50466
50639
  return {
50467
50640
  skills: Array.isArray(parsed.skills) ? parsed.skills : void 0,
@@ -50682,43 +50855,43 @@ var init_context_limit_detector = __esm({
50682
50855
  });
50683
50856
 
50684
50857
  // ../engine/src/auth-storage.ts
50685
- import { existsSync as existsSync19, readFileSync as readFileSync5 } from "node:fs";
50858
+ import { existsSync as existsSync20, readFileSync as readFileSync5 } from "node:fs";
50686
50859
  import { homedir as homedir4 } from "node:os";
50687
50860
  import { join as join23 } from "node:path";
50688
50861
  import { AuthStorage } from "@mariozechner/pi-coding-agent";
50689
50862
  import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
50690
- function getHomeDir2() {
50863
+ function getHomeDir4() {
50691
50864
  return process.env.HOME || process.env.USERPROFILE || homedir4();
50692
50865
  }
50693
- function getFusionAuthPath(home = getHomeDir2()) {
50866
+ function getFusionAuthPath(home = getHomeDir4()) {
50694
50867
  return join23(home, ".fusion", "agent", "auth.json");
50695
50868
  }
50696
- function getFusionModelsPath(home = getHomeDir2()) {
50869
+ function getFusionModelsPath(home = getHomeDir4()) {
50697
50870
  return join23(home, ".fusion", "agent", "models.json");
50698
50871
  }
50699
- function getLegacyAuthPaths(home = getHomeDir2()) {
50872
+ function getLegacyAuthPaths(home = getHomeDir4()) {
50700
50873
  return [
50701
50874
  join23(home, ".pi", "agent", "auth.json"),
50702
50875
  join23(home, ".pi", "auth.json")
50703
50876
  ];
50704
50877
  }
50705
- function getLegacyModelsPaths(home = getHomeDir2()) {
50878
+ function getLegacyModelsPaths(home = getHomeDir4()) {
50706
50879
  return [
50707
50880
  join23(home, ".pi", "agent", "models.json"),
50708
50881
  join23(home, ".pi", "models.json")
50709
50882
  ];
50710
50883
  }
50711
- function getModelRegistryModelsPath(home = getHomeDir2()) {
50884
+ function getModelRegistryModelsPath(home = getHomeDir4()) {
50712
50885
  const fusionModelsPath = getFusionModelsPath(home);
50713
- if (existsSync19(fusionModelsPath)) {
50886
+ if (existsSync20(fusionModelsPath)) {
50714
50887
  return fusionModelsPath;
50715
50888
  }
50716
- return getLegacyModelsPaths(home).find((modelsPath) => existsSync19(modelsPath)) ?? fusionModelsPath;
50889
+ return getLegacyModelsPaths(home).find((modelsPath) => existsSync20(modelsPath)) ?? fusionModelsPath;
50717
50890
  }
50718
50891
  function readLegacyCredentials(authPaths = getLegacyAuthPaths()) {
50719
50892
  const credentials = {};
50720
50893
  for (const authPath of authPaths) {
50721
- if (!existsSync19(authPath)) {
50894
+ if (!existsSync20(authPath)) {
50722
50895
  continue;
50723
50896
  }
50724
50897
  try {
@@ -50794,11 +50967,11 @@ var init_auth_storage = __esm({
50794
50967
  });
50795
50968
 
50796
50969
  // ../engine/src/pi.ts
50797
- import { existsSync as existsSync20, readFileSync as readFileSync6 } from "node:fs";
50970
+ import { existsSync as existsSync21, readFileSync as readFileSync6 } from "node:fs";
50798
50971
  import { exec } from "node:child_process";
50799
50972
  import { promisify as promisify2 } from "node:util";
50800
50973
  import { createRequire as createRequire2 } from "node:module";
50801
- import { basename as basename7, dirname as dirname7, join as join24, relative as relative3, isAbsolute as isAbsolute6, resolve as resolve10 } from "node:path";
50974
+ import { basename as basename7, dirname as dirname7, join as join24, relative as relative3, isAbsolute as isAbsolute7, resolve as resolve10 } from "node:path";
50802
50975
  import {
50803
50976
  createAgentSession,
50804
50977
  createBashTool,
@@ -51052,7 +51225,7 @@ function isRetryableModelSelectionError(message) {
51052
51225
  return normalized.includes("rate limit") || normalized.includes("too many requests") || normalized.includes("429") || normalized.includes("401") || normalized.includes("403") || normalized.includes("unauthorized") || normalized.includes("forbidden") || normalized.includes("authentication") || normalized.includes("invalid api key") || normalized.includes("invalid key") || normalized.includes("api key") || normalized.includes("overloaded") || normalized.includes("quota") || normalized.includes("capacity") || normalized.includes("temporarily unavailable") || normalized.includes("invalid temperature");
51053
51226
  }
51054
51227
  function readJsonObject2(path2) {
51055
- if (!existsSync20(path2)) {
51228
+ if (!existsSync21(path2)) {
51056
51229
  return {};
51057
51230
  }
51058
51231
  try {
@@ -51216,13 +51389,13 @@ function getPackageManagerAgentDir() {
51216
51389
  const legacyAgentDir = getLegacyPiAgentDir();
51217
51390
  const fusionSettings = readJsonObject2(join24(fusionAgentDir, "settings.json"));
51218
51391
  const legacySettings = readJsonObject2(join24(legacyAgentDir, "settings.json"));
51219
- if (hasPackageManagerSettings(fusionSettings) || !existsSync20(legacyAgentDir)) {
51392
+ if (hasPackageManagerSettings(fusionSettings) || !existsSync21(legacyAgentDir)) {
51220
51393
  return fusionAgentDir;
51221
51394
  }
51222
51395
  if (hasPackageManagerSettings(legacySettings)) {
51223
51396
  return legacyAgentDir;
51224
51397
  }
51225
- return existsSync20(fusionAgentDir) ? fusionAgentDir : legacyAgentDir;
51398
+ return existsSync21(fusionAgentDir) ? fusionAgentDir : legacyAgentDir;
51226
51399
  }
51227
51400
  function resolveVendoredClaudeCliEntry() {
51228
51401
  try {
@@ -51234,7 +51407,7 @@ function resolveVendoredClaudeCliEntry() {
51234
51407
  const entry = extensions[0];
51235
51408
  if (typeof entry !== "string" || entry.length === 0) return null;
51236
51409
  const path2 = resolve10(dirname7(pkgJsonPath), entry);
51237
- return existsSync20(path2) ? path2 : null;
51410
+ return existsSync21(path2) ? path2 : null;
51238
51411
  } catch {
51239
51412
  return null;
51240
51413
  }
@@ -51280,7 +51453,7 @@ async function registerExtensionProviders(cwd, modelRegistry) {
51280
51453
  }
51281
51454
  }
51282
51455
  function getProjectRootFromWorktree(cwd) {
51283
- const match = cwd.match(/^(.+?)\/\.worktrees\/[^/]+/);
51456
+ const match = cwd.match(/^(.+?)[\\/]\.worktrees[\\/][^\\/]+(?:[\\/]|$)/);
51284
51457
  if (match) {
51285
51458
  return match[1];
51286
51459
  }
@@ -51312,10 +51485,10 @@ async function isCompleteGitWorktree(worktreePath) {
51312
51485
  }
51313
51486
  }
51314
51487
  async function assertValidWorktreeSession(cwd, projectRoot) {
51315
- if (!existsSync20(cwd)) {
51488
+ if (!existsSync21(cwd)) {
51316
51489
  throw new Error(`Refusing to start coding agent in missing worktree: ${cwd}`);
51317
51490
  }
51318
- if (!existsSync20(join24(cwd, ".git")) || !await isCompleteGitWorktree(cwd)) {
51491
+ if (!existsSync21(join24(cwd, ".git")) || !await isCompleteGitWorktree(cwd)) {
51319
51492
  throw new Error(`Refusing to start coding agent in incomplete worktree: ${cwd}`);
51320
51493
  }
51321
51494
  if (!await isRegisteredGitWorktree(projectRoot, cwd)) {
@@ -51325,9 +51498,9 @@ async function assertValidWorktreeSession(cwd, projectRoot) {
51325
51498
  function isWorktreeAllowedPath(worktreePath, projectRoot, requestedPath, toolName) {
51326
51499
  const worktreeResolved = resolve10(worktreePath);
51327
51500
  const projectRootResolved = resolve10(projectRoot);
51328
- const requestedResolved = isAbsolute6(requestedPath) ? resolve10(requestedPath) : resolve10(worktreeResolved, requestedPath);
51501
+ const requestedResolved = isAbsolute7(requestedPath) ? resolve10(requestedPath) : resolve10(worktreeResolved, requestedPath);
51329
51502
  const relToWorktree = relative3(worktreeResolved, requestedResolved);
51330
- if (!relToWorktree.startsWith("..") && !isAbsolute6(relToWorktree)) {
51503
+ if (!relToWorktree.startsWith("..") && !isAbsolute7(relToWorktree)) {
51331
51504
  return true;
51332
51505
  }
51333
51506
  const relToProjectRoot = relative3(projectRootResolved, requestedResolved).replace(/\\/g, "/");
@@ -52057,7 +52230,7 @@ var init_usage_limit_detector = __esm({
52057
52230
 
52058
52231
  // ../engine/src/agent-instructions.ts
52059
52232
  import { readFile as readFile12 } from "node:fs/promises";
52060
- import { isAbsolute as isAbsolute7, resolve as resolve11, relative as relative4, normalize as normalize3, sep as sep4 } from "node:path";
52233
+ import { isAbsolute as isAbsolute8, resolve as resolve11, relative as relative4, normalize as normalize3, sep as sep4 } from "node:path";
52061
52234
  function trimAndClamp(value, maxLength, label, agentId) {
52062
52235
  const trimmed = value.trim();
52063
52236
  if (!trimmed) {
@@ -52087,7 +52260,7 @@ function resolveValidatedInstructionsPath(rawPath, rootDir, agentId) {
52087
52260
  log4.warn(`instructionsPath must end in .md for agent ${agentId}: ${trimmed}`);
52088
52261
  return null;
52089
52262
  }
52090
- if (isAbsolute7(trimmed)) {
52263
+ if (isAbsolute8(trimmed)) {
52091
52264
  log4.warn(`instructionsPath must be project-relative for agent ${agentId}: ${trimmed}`);
52092
52265
  return null;
52093
52266
  }
@@ -52098,7 +52271,7 @@ function resolveValidatedInstructionsPath(rawPath, rootDir, agentId) {
52098
52271
  }
52099
52272
  const resolvedPath = resolve11(rootDir, normalized);
52100
52273
  const rel = relative4(rootDir, resolvedPath);
52101
- if (!rel || rel.startsWith(`..${sep4}`) || rel === ".." || isAbsolute7(rel)) {
52274
+ if (!rel || rel.startsWith(`..${sep4}`) || rel === ".." || isAbsolute8(rel)) {
52102
52275
  log4.warn(`instructionsPath escapes project root for agent ${agentId}: ${trimmed}`);
52103
52276
  return null;
52104
52277
  }
@@ -52803,20 +52976,20 @@ async function withRateLimitRetry(fn, options = {}) {
52803
52976
  throw lastError ?? new Error("withRateLimitRetry: unexpected state");
52804
52977
  }
52805
52978
  function sleep(ms, signal) {
52806
- return new Promise((resolve19, reject) => {
52979
+ return new Promise((resolve17, reject) => {
52807
52980
  if (signal?.aborted) {
52808
52981
  reject(signal.reason ?? new Error("Aborted"));
52809
52982
  return;
52810
52983
  }
52811
- const timer = setTimeout(resolve19, ms);
52984
+ const timer = setTimeout(resolve17, ms);
52812
52985
  if (signal) {
52813
52986
  const onAbort = () => {
52814
52987
  clearTimeout(timer);
52815
52988
  reject(signal.reason ?? new Error("Aborted"));
52816
52989
  };
52817
52990
  signal.addEventListener("abort", onAbort, { once: true });
52818
- const origResolve = resolve19;
52819
- resolve19 = () => {
52991
+ const origResolve = resolve17;
52992
+ resolve17 = () => {
52820
52993
  signal.removeEventListener("abort", onAbort);
52821
52994
  origResolve();
52822
52995
  };
@@ -54779,11 +54952,11 @@ var init_run_audit = __esm({
54779
54952
  // ../engine/src/merger.ts
54780
54953
  import { execSync, exec as exec2, spawn as spawn2 } from "node:child_process";
54781
54954
  import { promisify as promisify3 } from "node:util";
54782
- import { existsSync as existsSync21 } from "node:fs";
54955
+ import { existsSync as existsSync22 } from "node:fs";
54783
54956
  import { join as join26 } from "node:path";
54784
54957
  import { Type as Type3 } from "typebox";
54785
54958
  async function execWithProcessGroup(command, options) {
54786
- return new Promise((resolve19, reject) => {
54959
+ return new Promise((resolve17, reject) => {
54787
54960
  if (options.signal?.aborted) {
54788
54961
  reject(Object.assign(
54789
54962
  new Error(`Command aborted before start: ${command}`),
@@ -54876,7 +55049,7 @@ async function execWithProcessGroup(command, options) {
54876
55049
  return;
54877
55050
  }
54878
55051
  if (code === 0) {
54879
- resolve19({ stdout, stderr, bufferOverflow: stdoutOverflow || stderrOverflow });
55052
+ resolve17({ stdout, stderr, bufferOverflow: stdoutOverflow || stderrOverflow });
54880
55053
  return;
54881
55054
  }
54882
55055
  reject(Object.assign(
@@ -55041,7 +55214,7 @@ async function getStagedFiles(cwd) {
55041
55214
  }
55042
55215
  }
55043
55216
  function hasInstallState(rootDir) {
55044
- return existsSync21(join26(rootDir, "node_modules")) || existsSync21(join26(rootDir, ".pnp.cjs"));
55217
+ return existsSync22(join26(rootDir, "node_modules")) || existsSync22(join26(rootDir, ".pnp.cjs"));
55045
55218
  }
55046
55219
  function shouldSyncDependenciesForMerge(stagedFiles, installStatePresent) {
55047
55220
  if (!installStatePresent) return true;
@@ -55050,10 +55223,10 @@ function shouldSyncDependenciesForMerge(stagedFiles, installStatePresent) {
55050
55223
  );
55051
55224
  }
55052
55225
  function getDependencySyncCommand(rootDir) {
55053
- if (existsSync21(join26(rootDir, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
55054
- if (existsSync21(join26(rootDir, "package-lock.json"))) return "npm install";
55055
- if (existsSync21(join26(rootDir, "yarn.lock"))) return "yarn install --frozen-lockfile";
55056
- if (existsSync21(join26(rootDir, "bun.lock")) || existsSync21(join26(rootDir, "bun.lockb"))) {
55226
+ if (existsSync22(join26(rootDir, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
55227
+ if (existsSync22(join26(rootDir, "package-lock.json"))) return "npm install";
55228
+ if (existsSync22(join26(rootDir, "yarn.lock"))) return "yarn install --frozen-lockfile";
55229
+ if (existsSync22(join26(rootDir, "bun.lock")) || existsSync22(join26(rootDir, "bun.lockb"))) {
55057
55230
  return "bun install --frozen-lockfile";
55058
55231
  }
55059
55232
  return null;
@@ -55086,8 +55259,8 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
55086
55259
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
55087
55260
  };
55088
55261
  }
55089
- if (existsSync21(join26(rootDir, "pnpm-lock.yaml"))) {
55090
- if (existsSync21(join26(rootDir, "pnpm-workspace.yaml"))) {
55262
+ if (existsSync22(join26(rootDir, "pnpm-lock.yaml"))) {
55263
+ if (existsSync22(join26(rootDir, "pnpm-workspace.yaml"))) {
55091
55264
  mergerLog.warn(
55092
55265
  `Inferred test command "pnpm test" in a pnpm workspace (${rootDir}). This runs the full monorepo suite on every merge. Consider setting an explicit scoped testCommand in project settings, e.g. \`pnpm -r --filter "...[main]" test\`.`
55093
55266
  );
@@ -55098,21 +55271,21 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
55098
55271
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
55099
55272
  };
55100
55273
  }
55101
- if (existsSync21(join26(rootDir, "yarn.lock"))) {
55274
+ if (existsSync22(join26(rootDir, "yarn.lock"))) {
55102
55275
  return {
55103
55276
  command: "yarn test",
55104
55277
  testSource: "inferred",
55105
55278
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
55106
55279
  };
55107
55280
  }
55108
- if (existsSync21(join26(rootDir, "bun.lock")) || existsSync21(join26(rootDir, "bun.lockb"))) {
55281
+ if (existsSync22(join26(rootDir, "bun.lock")) || existsSync22(join26(rootDir, "bun.lockb"))) {
55109
55282
  return {
55110
55283
  command: "bun test",
55111
55284
  testSource: "inferred",
55112
55285
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
55113
55286
  };
55114
55287
  }
55115
- if (existsSync21(join26(rootDir, "package-lock.json"))) {
55288
+ if (existsSync22(join26(rootDir, "package-lock.json"))) {
55116
55289
  return {
55117
55290
  command: "npm test",
55118
55291
  testSource: "inferred",
@@ -55901,6 +56074,68 @@ async function findWorktreeUser(store, worktreePath, excludeTaskId) {
55901
56074
  function quoteArg(value) {
55902
56075
  return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
55903
56076
  }
56077
+ async function resolveSafeCommitBody(opts) {
56078
+ const cleanLog = opts.commitLog.trim();
56079
+ if (cleanLog.length > 0) return cleanLog;
56080
+ const cleanStat = opts.diffStat.trim();
56081
+ if (cleanStat.length > 0) {
56082
+ const useTitleSummarizer = !!opts.settings.titleSummarizerProvider && !!opts.settings.titleSummarizerModelId;
56083
+ const provider = useTitleSummarizer ? opts.settings.titleSummarizerProvider : opts.settings.defaultProviderOverride && opts.settings.defaultModelIdOverride ? opts.settings.defaultProviderOverride : opts.settings.defaultProvider;
56084
+ const modelId = useTitleSummarizer ? opts.settings.titleSummarizerModelId : opts.settings.defaultProviderOverride && opts.settings.defaultModelIdOverride ? opts.settings.defaultModelIdOverride : opts.settings.defaultModelId;
56085
+ const ai = await summarizeCommitBody(cleanStat, opts.rootDir, provider, modelId, {
56086
+ branch: opts.branch,
56087
+ taskId: opts.taskId,
56088
+ signal: opts.signal,
56089
+ timeoutMs: opts.aiTimeoutMs
56090
+ }).catch(() => null);
56091
+ if (ai && ai.trim().length > 0) return ai.trim();
56092
+ return `Files changed:
56093
+
56094
+ ${cleanStat}`;
56095
+ }
56096
+ return `- merge ${opts.branch}`;
56097
+ }
56098
+ async function commitPatchId(rootDir, sha) {
56099
+ try {
56100
+ const { stdout } = await execAsync2(
56101
+ `git diff-tree -p ${quoteArg(sha)} | git patch-id --stable`,
56102
+ { cwd: rootDir, encoding: "utf-8" }
56103
+ );
56104
+ const line = stdout.trim();
56105
+ if (!line) return void 0;
56106
+ const [pid] = line.split(/\s+/, 1);
56107
+ return pid || void 0;
56108
+ } catch {
56109
+ return void 0;
56110
+ }
56111
+ }
56112
+ async function collectPatchIds(rootDir, target, windowSize) {
56113
+ const ids = /* @__PURE__ */ new Set();
56114
+ try {
56115
+ const { stdout } = await execAsync2(
56116
+ `git log -n ${Math.max(1, windowSize)} --format=%H ${quoteArg(target)}`,
56117
+ { cwd: rootDir, encoding: "utf-8" }
56118
+ );
56119
+ const shas = stdout.trim().split("\n").filter(Boolean);
56120
+ for (const sha of shas) {
56121
+ const pid = await commitPatchId(rootDir, sha);
56122
+ if (pid) ids.add(pid);
56123
+ }
56124
+ } catch {
56125
+ }
56126
+ return ids;
56127
+ }
56128
+ async function listBranchCommits(rootDir, target, branch) {
56129
+ try {
56130
+ const { stdout } = await execAsync2(
56131
+ `git log --reverse --format=%H ${quoteArg(target)}..${quoteArg(branch)}`,
56132
+ { cwd: rootDir, encoding: "utf-8" }
56133
+ );
56134
+ return stdout.trim().split("\n").filter(Boolean);
56135
+ } catch {
56136
+ return [];
56137
+ }
56138
+ }
55904
56139
  function getCommandErrorMessage(error) {
55905
56140
  if (error instanceof Error) {
55906
56141
  const stderr = error.stderr;
@@ -56417,11 +56652,182 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
56417
56652
  settings.worktreeRebaseBeforeMerge === false ? "remote rebase disabled" : ""
56418
56653
  );
56419
56654
  }
56655
+ let preMergeRebaseFallthrough;
56656
+ if (preferMainRebaseFailureMessage && worktreePath) {
56657
+ let rebaseTarget = "";
56658
+ try {
56659
+ const { stdout } = await execAsync2("git rev-parse HEAD", {
56660
+ cwd: rootDir,
56661
+ encoding: "utf-8"
56662
+ });
56663
+ rebaseTarget = stdout.trim();
56664
+ } catch {
56665
+ rebaseTarget = "";
56666
+ }
56667
+ if (rebaseTarget && task.baseBranch && task.baseBranch !== "main") {
56668
+ let depTip;
56669
+ try {
56670
+ const { stdout } = await execAsync2(
56671
+ `git rev-parse --verify "${task.baseBranch}^{commit}"`,
56672
+ { cwd: rootDir, encoding: "utf-8" }
56673
+ );
56674
+ depTip = stdout.trim() || void 0;
56675
+ } catch {
56676
+ depTip = void 0;
56677
+ }
56678
+ if (!depTip && task.baseCommitSha) {
56679
+ try {
56680
+ const { stdout } = await execAsync2(
56681
+ `git rev-parse --verify "${task.baseCommitSha}^{commit}"`,
56682
+ { cwd: rootDir, encoding: "utf-8" }
56683
+ );
56684
+ depTip = stdout.trim() || void 0;
56685
+ } catch {
56686
+ depTip = void 0;
56687
+ }
56688
+ }
56689
+ if (depTip && depTip !== rebaseTarget) {
56690
+ try {
56691
+ throwIfAborted(options.signal, taskId);
56692
+ await execAsync2("git rebase --abort", { cwd: worktreePath }).catch(
56693
+ () => void 0
56694
+ );
56695
+ await execAsync2(
56696
+ `git rebase --onto "${rebaseTarget}" "${depTip}" "${branch}"`,
56697
+ { cwd: worktreePath }
56698
+ );
56699
+ preferMainRebaseFailureMessage = void 0;
56700
+ rebaseHappened = true;
56701
+ mergerLog.log(
56702
+ `${taskId}: Layer 1 recovery \u2014 rebased ${branch} --onto ${rebaseTarget.slice(0, 8)} dropping commits up to dep tip ${depTip.slice(0, 8)} (baseBranch=${task.baseBranch})`
56703
+ );
56704
+ await store.logEntry(
56705
+ taskId,
56706
+ `Pre-merge recovery (Layer 1): dropped dependency commits from ${task.baseBranch} via rebase --onto ${rebaseTarget.slice(0, 8)} ${depTip.slice(0, 8)} ${branch}; the merge will proceed against the cleaned branch`
56707
+ );
56708
+ } catch (layer1Err) {
56709
+ rethrowIfMergeAborted(layer1Err);
56710
+ mergerLog.warn(
56711
+ `${taskId}: Layer 1 (dep-drop) recovery failed: ${layer1Err instanceof Error ? layer1Err.message : String(layer1Err)}`
56712
+ );
56713
+ await execAsync2("git rebase --abort", { cwd: worktreePath }).catch(
56714
+ () => void 0
56715
+ );
56716
+ }
56717
+ }
56718
+ }
56719
+ if (preferMainRebaseFailureMessage && rebaseTarget && worktreePath) {
56720
+ try {
56721
+ throwIfAborted(options.signal, taskId);
56722
+ const mainPatchIds = await collectPatchIds(rootDir, rebaseTarget, 500);
56723
+ const branchCommits = await listBranchCommits(rootDir, rebaseTarget, branch);
56724
+ if (branchCommits.length === 0) {
56725
+ rebaseHappened = true;
56726
+ preferMainRebaseFailureMessage = void 0;
56727
+ } else {
56728
+ const surviving = [];
56729
+ let dropped = 0;
56730
+ for (const sha of branchCommits) {
56731
+ const pid = await commitPatchId(rootDir, sha);
56732
+ if (pid && mainPatchIds.has(pid)) {
56733
+ dropped += 1;
56734
+ } else {
56735
+ surviving.push(sha);
56736
+ }
56737
+ }
56738
+ if (dropped > 0 && surviving.length === branchCommits.length) {
56739
+ mergerLog.warn(`${taskId}: Layer 2 internal accounting mismatch \u2014 skipping`);
56740
+ } else if (dropped > 0) {
56741
+ let originalBranchSha = "";
56742
+ try {
56743
+ const { stdout } = await execAsync2(
56744
+ `git rev-parse --verify "${branch}^{commit}"`,
56745
+ { cwd: worktreePath, encoding: "utf-8" }
56746
+ );
56747
+ originalBranchSha = stdout.trim();
56748
+ } catch {
56749
+ originalBranchSha = "";
56750
+ }
56751
+ const restoreOriginalBranch = async () => {
56752
+ if (!originalBranchSha) return;
56753
+ await execAsync2(`git reset --hard "${originalBranchSha}"`, {
56754
+ cwd: worktreePath
56755
+ }).catch(() => void 0);
56756
+ await execAsync2(`git checkout -f "${branch}"`, { cwd: worktreePath }).catch(
56757
+ () => void 0
56758
+ );
56759
+ await execAsync2(`git reset --hard "${originalBranchSha}"`, {
56760
+ cwd: worktreePath
56761
+ }).catch(() => void 0);
56762
+ };
56763
+ try {
56764
+ await execAsync2("git rebase --abort", { cwd: worktreePath }).catch(
56765
+ () => void 0
56766
+ );
56767
+ await execAsync2(`git checkout "${branch}"`, { cwd: worktreePath });
56768
+ await execAsync2(`git reset --hard "${rebaseTarget}"`, {
56769
+ cwd: worktreePath
56770
+ });
56771
+ for (const sha of surviving) {
56772
+ throwIfAborted(options.signal, taskId);
56773
+ try {
56774
+ await execAsync2(`git cherry-pick --allow-empty "${sha}"`, {
56775
+ cwd: worktreePath
56776
+ });
56777
+ } catch (pickErr) {
56778
+ rethrowIfMergeAborted(pickErr);
56779
+ await execAsync2("git cherry-pick --abort", { cwd: worktreePath }).catch(
56780
+ () => void 0
56781
+ );
56782
+ await restoreOriginalBranch();
56783
+ throw pickErr;
56784
+ }
56785
+ }
56786
+ preferMainRebaseFailureMessage = void 0;
56787
+ rebaseHappened = true;
56788
+ mergerLog.log(
56789
+ `${taskId}: Layer 2 recovery \u2014 patch-id stripped ${dropped} duplicate commit(s); replayed ${surviving.length} survivor(s) onto ${rebaseTarget.slice(0, 8)}`
56790
+ );
56791
+ await store.logEntry(
56792
+ taskId,
56793
+ `Pre-merge recovery (Layer 2): patch-id matched ${dropped} branch commit(s) against the last 500 main commits and dropped them as duplicates; cherry-picked ${surviving.length} unique commit(s) onto ${rebaseTarget.slice(0, 8)}`
56794
+ );
56795
+ } catch (replayErr) {
56796
+ await restoreOriginalBranch();
56797
+ throw replayErr;
56798
+ }
56799
+ } else {
56800
+ mergerLog.log(
56801
+ `${taskId}: Layer 2 found no duplicate-content commits to drop (window=500)`
56802
+ );
56803
+ }
56804
+ }
56805
+ } catch (layer2Err) {
56806
+ rethrowIfMergeAborted(layer2Err);
56807
+ mergerLog.warn(
56808
+ `${taskId}: Layer 2 (patch-id strip) recovery failed: ${layer2Err instanceof Error ? layer2Err.message : String(layer2Err)}`
56809
+ );
56810
+ }
56811
+ }
56812
+ if (preferMainRebaseFailureMessage) {
56813
+ preMergeRebaseFallthrough = preferMainRebaseFailureMessage;
56814
+ preferMainRebaseFailureMessage = void 0;
56815
+ mergerLog.warn(
56816
+ `${taskId}: Layers 1 & 2 could not unblock the prefer-main rebase \u2014 falling through to AI arbitration (Layer 3). Deterministic verification will gate the result.`
56817
+ );
56818
+ await store.logEntry(
56819
+ taskId,
56820
+ `Pre-merge recovery (Layer 3): both surgical and patch-id recovery failed; AI arbiter takes over. SAFETY CONSTRAINT for the AI: do NOT re-introduce content that current main has deleted. If hunks are ambiguous, prefer main's version. Post-merge test/build verification will reject any resolution that breaks main's intent.`,
56821
+ "PreMergeRebaseFallthrough"
56822
+ );
56823
+ }
56824
+ }
56420
56825
  if (preferMainRebaseFailureMessage) {
56421
56826
  throw new Error(
56422
- `${preferMainRebaseFailureMessage} for ${taskId}. Strategy "smart-prefer-main" requires a successful rebase to preserve main's deletions; falling through to a -X ours merge would silently re-introduce branch-only content. Resolve the rebase conflict manually, or switch mergeConflictStrategy to "smart-prefer-branch" / "ai-only".`
56827
+ `${preferMainRebaseFailureMessage} for ${taskId}. Strategy "smart-prefer-main" requires a successful rebase to preserve main's deletions; recovery layers 1\u20133 require a worktree path which is missing for this task. Resolve the rebase conflict manually, or switch mergeConflictStrategy to "smart-prefer-branch" / "ai-only".`
56423
56828
  );
56424
56829
  }
56830
+ void preMergeRebaseFallthrough;
56425
56831
  if (mergeConflictStrategy === "smart-prefer-main" && !rebaseHappened) {
56426
56832
  mergerLog.warn(
56427
56833
  `${taskId}: smart-prefer-main ran without a successful pre-merge rebase (${worktreePath ? "no remote resolvable or rebase disabled" : "no worktreePath"}). Main's deletions may not be preserved if the branch re-introduces them.`
@@ -56525,7 +56931,8 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
56525
56931
  testCommand: effectiveTestCommand,
56526
56932
  buildCommand: effectiveBuildCommand,
56527
56933
  testSource: effectiveTestSource,
56528
- buildSource: effectiveBuildSource
56934
+ buildSource: effectiveBuildSource,
56935
+ preMergeRebaseFallthrough
56529
56936
  }, aiTracker);
56530
56937
  if (success) {
56531
56938
  result.attemptsMade = attemptNum;
@@ -56777,8 +57184,14 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
56777
57184
  if (!merged && smartConflictResolution && mergeConflictStrategy !== "abort") {
56778
57185
  merged = await mergeAttempt(2);
56779
57186
  }
56780
- if (!merged && smartConflictResolution && mergeConflictStrategy !== "ai-only" && mergeConflictStrategy !== "abort") {
57187
+ if (!merged && smartConflictResolution && mergeConflictStrategy !== "ai-only" && mergeConflictStrategy !== "abort" && !preMergeRebaseFallthrough) {
56781
57188
  merged = await mergeAttempt(3);
57189
+ } else if (!merged && preMergeRebaseFallthrough) {
57190
+ await store.logEntry(
57191
+ taskId,
57192
+ `Attempt 3 (-X ours fallback) suppressed: pre-merge rebase recovery layers 1+2 failed under smart-prefer-main, so the unsafe ours-side fallback is skipped to honor the strategy's safety contract. Verification-gated AI Attempts 1+2 already exhausted; merge cannot complete safely without manual intervention.`,
57193
+ "PreMergeRebaseFallthrough"
57194
+ );
56782
57195
  }
56783
57196
  if (aiTracker.mergeWasEmpty) {
56784
57197
  mergeWasEmpty = true;
@@ -56916,7 +57329,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
56916
57329
  }
56917
57330
  }
56918
57331
  throwIfAborted(options.signal, taskId);
56919
- if (worktreePath && existsSync21(worktreePath)) {
57332
+ if (worktreePath && existsSync22(worktreePath)) {
56920
57333
  const otherUser = await findWorktreeUser(store, worktreePath, taskId);
56921
57334
  if (otherUser) {
56922
57335
  mergerLog.log(`Worktree retained \u2014 still needed by ${otherUser}`);
@@ -57174,7 +57587,16 @@ async function executeMergeAttempt(params, aiTracker) {
57174
57587
  }).trim();
57175
57588
  if (staged !== "0") {
57176
57589
  throwIfAborted(options.signal, taskId);
57177
- const escapedLog = commitLog.replace(/"/g, '\\"');
57590
+ const safeBody = await resolveSafeCommitBody({
57591
+ rootDir,
57592
+ taskId,
57593
+ branch,
57594
+ commitLog,
57595
+ diffStat,
57596
+ settings,
57597
+ signal: options.signal
57598
+ });
57599
+ const escapedLog = safeBody.replace(/"/g, '\\"');
57178
57600
  const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
57179
57601
  const authorArg = getCommitAuthorArg(settings);
57180
57602
  const trailerArg = buildTaskIdTrailerArg(taskId);
@@ -57287,7 +57709,8 @@ async function executeMergeAttempt(params, aiTracker) {
57287
57709
  options,
57288
57710
  testCommand,
57289
57711
  buildCommand: buildCommand2,
57290
- sourceIssueRef
57712
+ sourceIssueRef,
57713
+ preMergeRebaseFallthrough: params.preMergeRebaseFallthrough
57291
57714
  });
57292
57715
  if (!agentResult.success) {
57293
57716
  const errorMessage = agentResult.error || "Build verification failed";
@@ -57347,7 +57770,7 @@ async function executeMergeAttempt(params, aiTracker) {
57347
57770
  }
57348
57771
  }
57349
57772
  async function attemptWithSideStrategy(params, side = "theirs", aiTracker) {
57350
- const { rootDir, branch, commitLog, includeTaskId, sourceIssueRef, taskId, store, settings, testCommand, buildCommand: buildCommand2, testSource, buildSource } = params;
57773
+ const { rootDir, branch, commitLog, diffStat, includeTaskId, sourceIssueRef, taskId, store, settings, testCommand, buildCommand: buildCommand2, testSource, buildSource } = params;
57351
57774
  mergerLog.log(`${taskId}: attempting merge with -X ${side} strategy`);
57352
57775
  try {
57353
57776
  throwIfAborted(params.options.signal, taskId);
@@ -57384,7 +57807,16 @@ async function attemptWithSideStrategy(params, side = "theirs", aiTracker) {
57384
57807
  return true;
57385
57808
  }
57386
57809
  throwIfAborted(params.options.signal, taskId);
57387
- const escapedLog = commitLog.replace(/"/g, '\\"');
57810
+ const safeBody = await resolveSafeCommitBody({
57811
+ rootDir,
57812
+ taskId,
57813
+ branch,
57814
+ commitLog,
57815
+ diffStat,
57816
+ settings,
57817
+ signal: params.options.signal
57818
+ });
57819
+ const escapedLog = safeBody.replace(/"/g, '\\"');
57388
57820
  const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
57389
57821
  const authorArg = getCommitAuthorArg(settings);
57390
57822
  const trailerArg = buildTaskIdTrailerArg(taskId);
@@ -57430,7 +57862,8 @@ async function runAiAgentForCommit(params) {
57430
57862
  sourceIssueRef,
57431
57863
  options,
57432
57864
  testCommand,
57433
- buildCommand: buildCommand2
57865
+ buildCommand: buildCommand2,
57866
+ preMergeRebaseFallthrough
57434
57867
  } = params;
57435
57868
  const settings = await store.getSettings();
57436
57869
  let buildFailed = false;
@@ -57527,7 +57960,8 @@ async function runAiAgentForCommit(params) {
57527
57960
  testCommand,
57528
57961
  buildCommand: buildCommand2,
57529
57962
  authorArg,
57530
- sourceIssueRef
57963
+ sourceIssueRef,
57964
+ preMergeRebaseFallthrough
57531
57965
  });
57532
57966
  mergerLog.log(`${taskId}: starting fresh merge agent session`);
57533
57967
  try {
@@ -57560,7 +57994,8 @@ async function runAiAgentForCommit(params) {
57560
57994
  testCommand,
57561
57995
  buildCommand: buildCommand2,
57562
57996
  authorArg,
57563
- sourceIssueRef
57997
+ sourceIssueRef,
57998
+ preMergeRebaseFallthrough
57564
57999
  });
57565
58000
  try {
57566
58001
  await withRateLimitRetry(async () => {
@@ -57598,7 +58033,16 @@ async function runAiAgentForCommit(params) {
57598
58033
  if (!buildCommand2) {
57599
58034
  throwIfAborted(options.signal, taskId);
57600
58035
  mergerLog.log("Agent didn't commit \u2014 committing with fallback message");
57601
- const escapedLog = commitLog.replace(/"/g, '\\"');
58036
+ const safeBody = await resolveSafeCommitBody({
58037
+ rootDir,
58038
+ taskId,
58039
+ branch,
58040
+ commitLog,
58041
+ diffStat,
58042
+ settings,
58043
+ signal: options.signal
58044
+ });
58045
+ const escapedLog = safeBody.replace(/"/g, '\\"');
57602
58046
  const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
57603
58047
  const authorArg2 = getCommitAuthorArg(settings);
57604
58048
  const trailerArg = buildTaskIdTrailerArg(taskId);
@@ -57627,17 +58071,41 @@ async function runAiAgentForCommit(params) {
57627
58071
  }
57628
58072
  }
57629
58073
  function buildMergePrompt(params) {
57630
- const { taskId, branch, commitLog, diffStat, hasConflicts, simplifiedContext, sourceIssueRef, testCommand, buildCommand: buildCommand2, authorArg } = params;
58074
+ const { taskId, branch, commitLog, diffStat, hasConflicts, simplifiedContext, sourceIssueRef, testCommand, buildCommand: buildCommand2, authorArg, preMergeRebaseFallthrough } = params;
57631
58075
  const truncatedCommitLog = truncateWithEllipsis(commitLog, MERGE_COMMIT_LOG_MAX_CHARS);
57632
58076
  const truncatedDiffStat = truncateWithEllipsis(diffStat, MERGE_DIFF_STAT_MAX_CHARS);
57633
- const parts = [
58077
+ const parts = [];
58078
+ if (preMergeRebaseFallthrough) {
58079
+ parts.push(
58080
+ "## \u26A0\uFE0F Pre-merge rebase recovery exhausted \u2014 you are the final arbiter",
58081
+ "",
58082
+ "The pre-merge rebase against main aborted, and the surgical (Layer 1) and",
58083
+ "patch-id (Layer 2) recovery layers could not reconcile the branch. You are",
58084
+ "running under `smart-prefer-main` strategy, which means:",
58085
+ "",
58086
+ "**SAFETY CONSTRAINT \u2014 main's deletions are authoritative.**",
58087
+ "- If a hunk shows main has deleted lines that the branch re-adds, prefer",
58088
+ " main's deletion. Branch-only re-additions are likely orphan content from",
58089
+ " a squash-merged dependency and must NOT be re-introduced.",
58090
+ "- If a hunk is genuinely ambiguous, prefer main's version.",
58091
+ "- The merge result MUST pass `pnpm test` and `pnpm build`. If you can't",
58092
+ " produce a result that does, call `fn_report_build_failure` with concrete",
58093
+ " output rather than committing a regression.",
58094
+ "",
58095
+ `Original rebase failure for context: ${preMergeRebaseFallthrough.slice(0, 800)}`,
58096
+ "",
58097
+ "---",
58098
+ ""
58099
+ );
58100
+ }
58101
+ parts.push(
57634
58102
  `Finalize the merge of branch \`${branch}\` for task ${taskId}.`,
57635
58103
  "",
57636
58104
  "## Branch commits",
57637
58105
  "```",
57638
58106
  truncatedCommitLog,
57639
58107
  "```"
57640
- ];
58108
+ );
57641
58109
  if (!simplifiedContext) {
57642
58110
  parts.push(
57643
58111
  "",
@@ -58012,7 +58480,7 @@ var init_merger = __esm({
58012
58480
  // ../engine/src/worktree-names.ts
58013
58481
  import { readdirSync as readdirSync3 } from "node:fs";
58014
58482
  import { join as join27 } from "node:path";
58015
- import { existsSync as existsSync22 } from "node:fs";
58483
+ import { existsSync as existsSync23 } from "node:fs";
58016
58484
  function slugify2(str) {
58017
58485
  return str.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
58018
58486
  }
@@ -58038,7 +58506,7 @@ function generateReservedWorktreeName(rootDir, reservedNames = /* @__PURE__ */ n
58038
58506
  return `${baseName}-${suffix}`;
58039
58507
  }
58040
58508
  function getExistingWorktreeNames(worktreesDir) {
58041
- if (!existsSync22(worktreesDir)) {
58509
+ if (!existsSync23(worktreesDir)) {
58042
58510
  return /* @__PURE__ */ new Set();
58043
58511
  }
58044
58512
  try {
@@ -58175,8 +58643,8 @@ __export(worktree_pool_exports, {
58175
58643
  });
58176
58644
  import { exec as exec3 } from "node:child_process";
58177
58645
  import { promisify as promisify4 } from "node:util";
58178
- import { existsSync as existsSync23, lstatSync, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
58179
- import { join as join28, relative as relative5, resolve as resolve12, isAbsolute as isAbsolute8 } from "node:path";
58646
+ import { existsSync as existsSync24, lstatSync, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
58647
+ import { join as join28, relative as relative5, resolve as resolve12, isAbsolute as isAbsolute9 } from "node:path";
58180
58648
  async function isGitRepository(dir) {
58181
58649
  try {
58182
58650
  await execAsync3("git rev-parse --git-dir", {
@@ -58213,20 +58681,20 @@ async function isRegisteredGitWorktree2(rootDir, worktreePath) {
58213
58681
  return (await getRegisteredWorktreePaths(rootDir)).has(resolve12(worktreePath));
58214
58682
  }
58215
58683
  function hasRequiredWorktreeFiles(worktreePath) {
58216
- return existsSync23(join28(worktreePath, ".git")) && existsSync23(join28(worktreePath, "package.json"));
58684
+ return existsSync24(join28(worktreePath, ".git")) && existsSync24(join28(worktreePath, "package.json"));
58217
58685
  }
58218
58686
  async function isUsableTaskWorktree(rootDir, worktreePath) {
58219
- return existsSync23(worktreePath) && await isRegisteredGitWorktree2(rootDir, worktreePath) && hasRequiredWorktreeFiles(worktreePath);
58687
+ return existsSync24(worktreePath) && await isRegisteredGitWorktree2(rootDir, worktreePath) && hasRequiredWorktreeFiles(worktreePath);
58220
58688
  }
58221
58689
  function isInsideWorktreesDir(rootDir, worktreePath) {
58222
58690
  const worktreesDir = resolve12(rootDir, ".worktrees");
58223
58691
  const target = resolve12(worktreePath);
58224
58692
  const rel = relative5(worktreesDir, target);
58225
- return rel !== "" && !rel.startsWith("..") && !isAbsolute8(rel);
58693
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute9(rel);
58226
58694
  }
58227
58695
  async function scanIdleWorktrees(rootDir, store) {
58228
58696
  const worktreesDir = join28(rootDir, ".worktrees");
58229
- if (!existsSync23(worktreesDir)) {
58697
+ if (!existsSync24(worktreesDir)) {
58230
58698
  return [];
58231
58699
  }
58232
58700
  let dirs;
@@ -58256,13 +58724,13 @@ async function scanIdleWorktrees(rootDir, store) {
58256
58724
  }
58257
58725
  async function cleanupOrphanedWorktrees(rootDir, store) {
58258
58726
  const worktreesDir = join28(rootDir, ".worktrees");
58259
- if (!existsSync23(worktreesDir)) {
58727
+ if (!existsSync24(worktreesDir)) {
58260
58728
  return 0;
58261
58729
  }
58262
58730
  const orphaned = await scanIdleWorktrees(rootDir, store);
58263
58731
  const registeredWorktrees = await getRegisteredWorktreePaths(rootDir);
58264
58732
  let dirs = [];
58265
- if (existsSync23(worktreesDir)) {
58733
+ if (existsSync24(worktreesDir)) {
58266
58734
  try {
58267
58735
  dirs = readdirSync4(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join28(worktreesDir, e.name));
58268
58736
  } catch (err) {
@@ -58297,7 +58765,7 @@ async function cleanupOrphanedWorktrees(rootDir, store) {
58297
58765
  }
58298
58766
  async function reapOrphanWorktrees(projectRoot) {
58299
58767
  const worktreesDir = join28(projectRoot, ".worktrees");
58300
- if (!existsSync23(worktreesDir)) {
58768
+ if (!existsSync24(worktreesDir)) {
58301
58769
  return 0;
58302
58770
  }
58303
58771
  let entries;
@@ -58321,7 +58789,7 @@ async function reapOrphanWorktrees(projectRoot) {
58321
58789
  for (const { name, fullPath } of entries) {
58322
58790
  const resolvedFull = resolve12(fullPath);
58323
58791
  const rel = relative5(resolve12(worktreesDir), resolvedFull);
58324
- if (!rel || rel.startsWith("..") || isAbsolute8(rel)) {
58792
+ if (!rel || rel.startsWith("..") || isAbsolute9(rel)) {
58325
58793
  worktreePoolLog.warn(`reapOrphanWorktrees: skipping out-of-bounds path ${fullPath}`);
58326
58794
  continue;
58327
58795
  }
@@ -58329,7 +58797,7 @@ async function reapOrphanWorktrees(projectRoot) {
58329
58797
  continue;
58330
58798
  }
58331
58799
  const dotGit = join28(resolvedFull, ".git");
58332
- if (existsSync23(dotGit)) {
58800
+ if (existsSync24(dotGit)) {
58333
58801
  worktreePoolLog.log(`reapOrphanWorktrees: skipping ${name} (has .git entry but not in registered list \u2014 may be partially registered)`);
58334
58802
  continue;
58335
58803
  }
@@ -58388,7 +58856,7 @@ var init_worktree_pool = __esm({
58388
58856
  acquire() {
58389
58857
  for (const path2 of this.idle) {
58390
58858
  this.idle.delete(path2);
58391
- if (existsSync23(path2)) {
58859
+ if (existsSync24(path2)) {
58392
58860
  return path2;
58393
58861
  }
58394
58862
  worktreePoolLog.log(`Pruned stale entry: ${path2}`);
@@ -58435,7 +58903,7 @@ var init_worktree_pool = __esm({
58435
58903
  */
58436
58904
  rehydrate(idlePaths) {
58437
58905
  for (const path2 of idlePaths) {
58438
- if (existsSync23(path2)) {
58906
+ if (existsSync24(path2)) {
58439
58907
  this.idle.add(path2);
58440
58908
  } else {
58441
58909
  worktreePoolLog.log(`Rehydrate skipped (not on disk): ${path2}`);
@@ -58491,7 +58959,7 @@ var init_worktree_pool = __esm({
58491
58959
  throw err;
58492
58960
  }
58493
58961
  const conflictingPath = match[1];
58494
- if (!existsSync23(conflictingPath)) {
58962
+ if (!existsSync24(conflictingPath)) {
58495
58963
  await execAsync3("git worktree prune", { cwd: worktreePath });
58496
58964
  await execAsync3(checkoutCmd, { cwd: worktreePath });
58497
58965
  return branchName;
@@ -58567,7 +59035,7 @@ var init_token_cap_detector = __esm({
58567
59035
  // ../engine/src/step-session-executor.ts
58568
59036
  import { exec as exec4 } from "node:child_process";
58569
59037
  import { promisify as promisify5 } from "node:util";
58570
- import { existsSync as existsSync24 } from "node:fs";
59038
+ import { existsSync as existsSync25 } from "node:fs";
58571
59039
  import { join as join29 } from "node:path";
58572
59040
  function parseStepFileScopes(prompt) {
58573
59041
  const result = /* @__PURE__ */ new Map();
@@ -58845,7 +59313,7 @@ function resolveExecutorModelPair(taskModelProvider, taskModelId, settings) {
58845
59313
  return { provider: void 0, modelId: void 0 };
58846
59314
  }
58847
59315
  function sleep2(ms) {
58848
- return new Promise((resolve19) => setTimeout(resolve19, ms));
59316
+ return new Promise((resolve17) => setTimeout(resolve17, ms));
58849
59317
  }
58850
59318
  var execAsync4, stepExecLog, MAX_STEP_RETRIES, RETRY_DELAYS_MS, NOOP_TASK_STORE, StepSessionExecutor;
58851
59319
  var init_step_session_executor = __esm({
@@ -58971,7 +59439,7 @@ var init_step_session_executor = __esm({
58971
59439
  }
58972
59440
  for (const [stepIdx, worktreePath] of this.parallelWorktrees) {
58973
59441
  try {
58974
- if (existsSync24(worktreePath)) {
59442
+ if (existsSync25(worktreePath)) {
58975
59443
  await execAsync4(`git worktree remove "${worktreePath}" --force`, {
58976
59444
  cwd: this.options.rootDir
58977
59445
  });
@@ -59303,7 +59771,7 @@ var init_step_session_executor = __esm({
59303
59771
  for (const [stepIdx, worktreePath] of worktreePaths) {
59304
59772
  if (worktreePath !== this.options.worktreePath) {
59305
59773
  try {
59306
- if (existsSync24(worktreePath)) {
59774
+ if (existsSync25(worktreePath)) {
59307
59775
  await execAsync4(`git worktree remove "${worktreePath}" --force`, {
59308
59776
  cwd: this.options.rootDir
59309
59777
  });
@@ -59480,8 +59948,8 @@ var init_task_completion = __esm({
59480
59948
  // ../engine/src/executor.ts
59481
59949
  import { exec as exec5 } from "node:child_process";
59482
59950
  import { promisify as promisify6 } from "node:util";
59483
- import { isAbsolute as isAbsolute9, join as join31, relative as relative6, resolve as resolvePath } from "node:path";
59484
- import { existsSync as existsSync25 } from "node:fs";
59951
+ import { isAbsolute as isAbsolute10, join as join31, relative as relative6, resolve as resolvePath } from "node:path";
59952
+ import { existsSync as existsSync26 } from "node:fs";
59485
59953
  import { readFile as readFile14, writeFile as writeFile11 } from "node:fs/promises";
59486
59954
  import { Type as Type4 } from "@mariozechner/pi-ai";
59487
59955
  import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
@@ -60566,7 +61034,7 @@ Lint, tests, and typecheck are also hard quality gates:
60566
61034
  async recoverCompletedTask(task) {
60567
61035
  try {
60568
61036
  const settings = await this.store.getSettings();
60569
- if (task.worktree && existsSync25(task.worktree)) {
61037
+ if (task.worktree && existsSync26(task.worktree)) {
60570
61038
  const modifiedFiles = await this.captureModifiedFiles(task.worktree, task.baseCommitSha);
60571
61039
  if (modifiedFiles.length > 0) {
60572
61040
  await this.store.updateTask(task.id, { modifiedFiles });
@@ -60727,7 +61195,7 @@ Lint, tests, and typecheck are also hard quality gates:
60727
61195
  if (task.dependencies.length === 0) return null;
60728
61196
  for (const depId of task.dependencies) {
60729
61197
  const dep = allTasks.find((t) => t.id === depId);
60730
- if (dep && dep.worktree && (dep.column === "done" || dep.column === "in-review") && existsSync25(dep.worktree)) {
61198
+ if (dep && dep.worktree && (dep.column === "done" || dep.column === "in-review") && existsSync26(dep.worktree)) {
60731
61199
  return dep.worktree;
60732
61200
  }
60733
61201
  }
@@ -60849,7 +61317,7 @@ Lint, tests, and typecheck are also hard quality gates:
60849
61317
  );
60850
61318
  }
60851
61319
  const branchName = `fusion/${task.id.toLowerCase()}`;
60852
- let isResume = existsSync25(worktreePath);
61320
+ let isResume = existsSync26(worktreePath);
60853
61321
  let acquiredFromPool = false;
60854
61322
  const baseBranch = task.baseBranch || null;
60855
61323
  if (task.worktree && isResume && !await isUsableTaskWorktree(this.rootDir, worktreePath)) {
@@ -60863,7 +61331,7 @@ Lint, tests, and typecheck are also hard quality gates:
60863
61331
  );
60864
61332
  await this.store.updateTask(task.id, { worktree: null, branch: null });
60865
61333
  worktreePath = join31(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
60866
- isResume = existsSync25(worktreePath);
61334
+ isResume = existsSync26(worktreePath);
60867
61335
  }
60868
61336
  if (!isResume) {
60869
61337
  if (this.options.pool && settings.recycleWorktrees) {
@@ -61157,7 +61625,7 @@ Lint, tests, and typecheck are also hard quality gates:
61157
61625
  executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay2}: ${errorMessage}`);
61158
61626
  await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay2}): ${errorMessage}`, void 0, this.currentRunContext);
61159
61627
  }
61160
- if (worktreePath && existsSync25(worktreePath)) {
61628
+ if (worktreePath && existsSync26(worktreePath)) {
61161
61629
  try {
61162
61630
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
61163
61631
  await audit.git({ type: "worktree:remove", target: worktreePath });
@@ -61216,7 +61684,7 @@ Lint, tests, and typecheck are also hard quality gates:
61216
61684
  try {
61217
61685
  const latestTask = await this.store.getTask(task.id);
61218
61686
  await this.resetStepsIfWorkLost(latestTask);
61219
- if (worktreePath && existsSync25(worktreePath)) {
61687
+ if (worktreePath && existsSync26(worktreePath)) {
61220
61688
  try {
61221
61689
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
61222
61690
  } catch (wtErr) {
@@ -61309,7 +61777,7 @@ Lint, tests, and typecheck are also hard quality gates:
61309
61777
  const executorFallbackProvider = settings.fallbackProvider;
61310
61778
  const executorFallbackModelId = settings.fallbackModelId;
61311
61779
  const executorThinkingLevel = detail.thinkingLevel ?? settings.defaultThinkingLevel;
61312
- const isResuming = !!task.sessionFile && existsSync25(task.sessionFile);
61780
+ const isResuming = !!task.sessionFile && existsSync26(task.sessionFile);
61313
61781
  const sessionManager = isResuming ? SessionManager2.open(task.sessionFile) : SessionManager2.create(worktreePath);
61314
61782
  executorLog.log(`${task.id}: creating agent session (provider=${executorProvider ?? "default"}, model=${executorModelId ?? "default"}, resuming=${isResuming})`);
61315
61783
  const executorInstructions = await this.resolveInstructionsForRole("executor");
@@ -61692,7 +62160,7 @@ Lint, tests, and typecheck are also hard quality gates:
61692
62160
  this.options.onComplete?.(task);
61693
62161
  } else {
61694
62162
  executorLog.log(`${task.id} paused \u2014 moving to todo`);
61695
- if (worktreePath && existsSync25(worktreePath)) {
62163
+ if (worktreePath && existsSync26(worktreePath)) {
61696
62164
  try {
61697
62165
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
61698
62166
  executorLog.log(`Removed old worktree for paused task: ${worktreePath}`);
@@ -61787,7 +62255,7 @@ Lint, tests, and typecheck are also hard quality gates:
61787
62255
  executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay2}: ${errorMessage}`);
61788
62256
  await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay2}): ${errorMessage}`, void 0, this.currentRunContext);
61789
62257
  }
61790
- if (worktreePath && existsSync25(worktreePath)) {
62258
+ if (worktreePath && existsSync26(worktreePath)) {
61791
62259
  try {
61792
62260
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
61793
62261
  executorLog.log(`Removed old worktree for transient retry: ${worktreePath}`);
@@ -61842,7 +62310,7 @@ Lint, tests, and typecheck are also hard quality gates:
61842
62310
  try {
61843
62311
  const latestTask = await this.store.getTask(task.id);
61844
62312
  await this.resetStepsIfWorkLost(latestTask);
61845
- if (worktreePath && existsSync25(worktreePath)) {
62313
+ if (worktreePath && existsSync26(worktreePath)) {
61846
62314
  try {
61847
62315
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
61848
62316
  executorLog.log(`Removed old worktree for stuck-killed retry: ${worktreePath}`);
@@ -62942,9 +63410,23 @@ Review the work done in this worktree and evaluate it against the criteria in yo
62942
63410
  resolvedStartPoint = resolved;
62943
63411
  }
62944
63412
  }
63413
+ const squashImport = resolvedStartPoint ? await this.planSquashImportFromDep(taskId, resolvedStartPoint, startPoint) : null;
63414
+ const initialStartPoint = squashImport ? squashImport.mainBase : resolvedStartPoint;
62945
63415
  for (let attempt = 0; attempt < this.MAX_WORKTREE_RETRIES; attempt++) {
62946
63416
  try {
62947
- const result = await this.tryCreateWorktree(branch, currentPath, taskId, resolvedStartPoint, attempt);
63417
+ const result = await this.tryCreateWorktree(branch, currentPath, taskId, initialStartPoint, attempt);
63418
+ if (squashImport) {
63419
+ await this.squashImportDepIntoWorktree(
63420
+ result.path,
63421
+ taskId,
63422
+ squashImport.depTip,
63423
+ squashImport.label
63424
+ ).catch((importErr) => {
63425
+ executorLog.warn(
63426
+ `Squash-import of ${squashImport.label} into ${result.branch} failed for ${taskId} (continuing without): ${importErr instanceof Error ? importErr.message : String(importErr)}`
63427
+ );
63428
+ });
63429
+ }
62948
63430
  await this.rebaseNewWorktreeOntoRemote(result.path, result.branch, taskId).catch((err) => {
62949
63431
  executorLog.warn(
62950
63432
  `Post-create worktree rebase failed for ${taskId} (continuing): ${err instanceof Error ? err.message : String(err)}`
@@ -62966,7 +63448,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
62966
63448
  );
62967
63449
  }
62968
63450
  const delay2 = this.WORKTREE_RETRY_DELAYS[attempt] || 1e3;
62969
- await new Promise((resolve19) => setTimeout(resolve19, delay2));
63451
+ await new Promise((resolve17) => setTimeout(resolve17, delay2));
62970
63452
  }
62971
63453
  }
62972
63454
  throw new Error("Unexpected exit from worktree creation retry loop");
@@ -62974,6 +63456,136 @@ Review the work done in this worktree and evaluate it against the criteria in yo
62974
63456
  quoteShellArg(value) {
62975
63457
  return `'${value.replace(/'/g, "'\\''")}'`;
62976
63458
  }
63459
+ /**
63460
+ * Decide whether a task's declared dep base should be squash-imported
63461
+ * (instead of forked from). Returns the planned operation's data when the
63462
+ * dep tip differs from the resolvable main base; returns null when no
63463
+ * import is needed (dep is already at main) or when no main base is
63464
+ * resolvable (caller falls back to legacy fork-from-dep).
63465
+ *
63466
+ * `originalStartPoint` is the user-facing label (typically the branch name
63467
+ * like `fusion/fn-2729`) used purely for log messages. `depTip` is the
63468
+ * resolved SHA of the dep's tip — that's what gets squash-merged.
63469
+ */
63470
+ async planSquashImportFromDep(taskId, depTip, originalStartPoint) {
63471
+ let settings;
63472
+ try {
63473
+ settings = await this.store.getSettings();
63474
+ } catch {
63475
+ return null;
63476
+ }
63477
+ let mainBase = "";
63478
+ if (settings.worktreeRebaseBeforeMerge !== false) {
63479
+ let remote = settings.worktreeRebaseRemote?.trim() || "";
63480
+ if (!remote) {
63481
+ try {
63482
+ const { stdout } = await execAsync5("git remote", { cwd: this.rootDir });
63483
+ const remotes = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
63484
+ if (remotes.includes("origin")) remote = "origin";
63485
+ else if (remotes.length === 1) remote = remotes[0];
63486
+ } catch {
63487
+ }
63488
+ }
63489
+ if (remote) {
63490
+ let defaultBranch = "";
63491
+ try {
63492
+ const { stdout } = await execAsync5(
63493
+ `git rev-parse --abbrev-ref ${this.quoteShellArg(remote)}/HEAD`,
63494
+ { cwd: this.rootDir }
63495
+ );
63496
+ defaultBranch = stdout.trim().replace(new RegExp(`^${remote}/`), "");
63497
+ } catch {
63498
+ }
63499
+ if (defaultBranch && defaultBranch !== "HEAD") {
63500
+ await execAsync5(
63501
+ `git fetch ${this.quoteShellArg(remote)} ${this.quoteShellArg(defaultBranch)}`,
63502
+ { cwd: this.rootDir }
63503
+ ).catch(() => void 0);
63504
+ try {
63505
+ const { stdout } = await execAsync5(
63506
+ `git rev-parse --verify "${remote}/${defaultBranch}^{commit}"`,
63507
+ { cwd: this.rootDir, encoding: "utf-8" }
63508
+ );
63509
+ mainBase = stdout.trim();
63510
+ } catch {
63511
+ }
63512
+ }
63513
+ }
63514
+ }
63515
+ if (!mainBase) {
63516
+ try {
63517
+ const { stdout } = await execAsync5("git rev-parse HEAD", {
63518
+ cwd: this.rootDir,
63519
+ encoding: "utf-8"
63520
+ });
63521
+ mainBase = stdout.trim();
63522
+ } catch {
63523
+ return null;
63524
+ }
63525
+ }
63526
+ if (!mainBase) return null;
63527
+ try {
63528
+ await execAsync5(
63529
+ `git merge-base --is-ancestor ${this.quoteShellArg(depTip)} ${this.quoteShellArg(mainBase)}`,
63530
+ { cwd: this.rootDir }
63531
+ );
63532
+ if (depTip === mainBase) return null;
63533
+ return { depTip: mainBase, mainBase, label: originalStartPoint || depTip.slice(0, 8) };
63534
+ } catch {
63535
+ }
63536
+ return { depTip, mainBase, label: originalStartPoint || depTip.slice(0, 8) };
63537
+ }
63538
+ /**
63539
+ * Squash-merge the dep's content into a worktree that's already branched
63540
+ * off main. Produces one commit on the worktree branch carrying the dep's
63541
+ * content, instead of inheriting the dep's individual commits. Best-effort:
63542
+ * any failure (conflict, hooks, IO) leaves the worktree at main and the
63543
+ * caller proceeds — the dependent task will then need to import the dep's
63544
+ * content itself, but the worktree itself is still usable.
63545
+ */
63546
+ async squashImportDepIntoWorktree(worktreePath, taskId, depTip, label) {
63547
+ try {
63548
+ await execAsync5(
63549
+ `git merge-base --is-ancestor ${this.quoteShellArg(depTip)} HEAD`,
63550
+ { cwd: worktreePath }
63551
+ );
63552
+ return;
63553
+ } catch {
63554
+ }
63555
+ try {
63556
+ await execAsync5(
63557
+ `git merge --squash --allow-unrelated-histories ${this.quoteShellArg(depTip)}`,
63558
+ { cwd: worktreePath }
63559
+ );
63560
+ } catch (err) {
63561
+ await execAsync5("git reset --hard HEAD", { cwd: worktreePath }).catch(
63562
+ () => void 0
63563
+ );
63564
+ throw err;
63565
+ }
63566
+ try {
63567
+ await execAsync5("git diff --cached --quiet", { cwd: worktreePath });
63568
+ return;
63569
+ } catch {
63570
+ }
63571
+ const subject = `chore(${taskId}): import dependency content from ${label}`;
63572
+ const body = `Squash-imported the working tree of ${label} as a single commit so this branch carries the dep's content without inheriting its individual commits. If the dep is later squash-merged to main, this commit's patch-id should match the merge and rebase cleanly.`;
63573
+ try {
63574
+ await execAsync5(
63575
+ `git commit -m ${this.quoteShellArg(subject)} -m ${this.quoteShellArg(body)}`,
63576
+ { cwd: worktreePath }
63577
+ );
63578
+ } catch (commitErr) {
63579
+ await execAsync5("git reset --hard HEAD", { cwd: worktreePath }).catch(
63580
+ () => void 0
63581
+ );
63582
+ throw commitErr;
63583
+ }
63584
+ await this.store.logEntry(
63585
+ taskId,
63586
+ `Squash-imported dependency content from ${label} into worktree (single import commit instead of inheriting raw commits)`
63587
+ );
63588
+ }
62977
63589
  /**
62978
63590
  * After creating a fresh task worktree, fetch the configured remote and
62979
63591
  * rebase the task branch onto `<remote>/<defaultBranch>`. The result is a
@@ -63058,7 +63670,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
63058
63670
  * rather than fail the task permanently.
63059
63671
  */
63060
63672
  async resolveWorktreeStartPoint(startPoint, taskId) {
63061
- const command = isAbsolute9(startPoint) && existsSync25(startPoint) ? `git -C "${startPoint}" rev-parse --verify HEAD^{commit}` : `git rev-parse --verify "${startPoint}^{commit}"`;
63673
+ const command = isAbsolute10(startPoint) && existsSync26(startPoint) ? `git -C "${startPoint}" rev-parse --verify HEAD^{commit}` : `git rev-parse --verify "${startPoint}^{commit}"`;
63062
63674
  try {
63063
63675
  const { stdout } = await execAsync5(command, { cwd: this.rootDir });
63064
63676
  return stdout.trim() || startPoint;
@@ -63078,7 +63690,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
63078
63690
  */
63079
63691
  async tryCreateWorktree(branch, path2, taskId, startPoint, attemptNumber = 0, recoveryDepth = 0) {
63080
63692
  await this.assertWorktreePathNotNested(path2, taskId);
63081
- if (existsSync25(path2)) {
63693
+ if (existsSync26(path2)) {
63082
63694
  const isRegistered = await this.isRegisteredWorktree(path2);
63083
63695
  if (!isRegistered) {
63084
63696
  await this.store.logEntry(
@@ -63276,7 +63888,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
63276
63888
  if (wt === rootResolved) continue;
63277
63889
  if (wt === target) continue;
63278
63890
  const rel = relative6(wt, target);
63279
- if (rel && !rel.startsWith("..") && !isAbsolute9(rel)) {
63891
+ if (rel && !rel.startsWith("..") && !isAbsolute10(rel)) {
63280
63892
  await this.store.logEntry(
63281
63893
  taskId,
63282
63894
  `Refusing to create nested worktree`,
@@ -63419,6 +64031,10 @@ Review the work done in this worktree and evaluate it against the criteria in yo
63419
64031
  if (alreadyUsedMatch) {
63420
64032
  return { type: "already-used", path: alreadyUsedMatch[1], message: output };
63421
64033
  }
64034
+ const alreadyCheckedOutMatch = output.match(/is already checked out at '([^']+)'/);
64035
+ if (alreadyCheckedOutMatch) {
64036
+ return { type: "already-used", path: alreadyCheckedOutMatch[1], message: output };
64037
+ }
63422
64038
  if (output.match(/invalid reference/i) || output.match(/unable to resolve reference/i) || output.match(/stale file handle/i) || output.match(/not a valid ref/i) || output.match(/unable to delete.*ref/i)) {
63423
64039
  return { type: "invalid-reference", message: output };
63424
64040
  }
@@ -63905,9 +64521,9 @@ var init_effective_node = __esm({
63905
64521
  });
63906
64522
 
63907
64523
  // ../engine/src/scheduler.ts
63908
- import { existsSync as existsSync26 } from "node:fs";
64524
+ import { existsSync as existsSync27 } from "node:fs";
63909
64525
  import { readFile as readFile15 } from "node:fs/promises";
63910
- import { join as join32 } from "node:path";
64526
+ import { basename as basename8, join as join32 } from "node:path";
63911
64527
  function pathsOverlap2(a, b) {
63912
64528
  for (const pa of a) {
63913
64529
  const prefixA = pa.endsWith("/*") ? pa.slice(0, -1) : null;
@@ -64075,11 +64691,11 @@ var init_scheduler = __esm({
64075
64691
  */
64076
64692
  async validateTaskFilesystem(id) {
64077
64693
  const taskDir = join32(this.store.getTasksDir(), id);
64078
- if (!existsSync26(taskDir)) {
64694
+ if (!existsSync27(taskDir)) {
64079
64695
  return { valid: false, reason: "missing directory" };
64080
64696
  }
64081
64697
  const promptPath = join32(taskDir, "PROMPT.md");
64082
- if (!existsSync26(promptPath)) {
64698
+ if (!existsSync27(promptPath)) {
64083
64699
  return { valid: false, reason: "missing or empty PROMPT.md" };
64084
64700
  }
64085
64701
  try {
@@ -64182,7 +64798,7 @@ var init_scheduler = __esm({
64182
64798
  */
64183
64799
  planWorktreePath(task, naming, reservedNames) {
64184
64800
  if (task.worktree) {
64185
- const existingName = task.worktree.split("/").pop();
64801
+ const existingName = basename8(task.worktree);
64186
64802
  if (existingName) reservedNames.add(existingName);
64187
64803
  return task.worktree;
64188
64804
  }
@@ -64319,7 +64935,7 @@ var init_scheduler = __esm({
64319
64935
  const ordered = resolveDependencyOrder(todo);
64320
64936
  let started = 0;
64321
64937
  const reservedWorktreeNames = new Set(
64322
- tasks.map((task) => task.worktree?.split("/").pop()).filter((name) => Boolean(name))
64938
+ tasks.map((task) => task.worktree ? basename8(task.worktree) : void 0).filter((name) => Boolean(name))
64323
64939
  );
64324
64940
  for (const taskId of ordered) {
64325
64941
  const task = tasks.find((t) => t.id === taskId);
@@ -66118,7 +66734,7 @@ __export(agent_reflection_exports, {
66118
66734
  AgentReflectionService: () => AgentReflectionService
66119
66735
  });
66120
66736
  import { readFile as readFile16 } from "node:fs/promises";
66121
- import { isAbsolute as isAbsolute10, resolve as resolve13 } from "node:path";
66737
+ import { isAbsolute as isAbsolute11, resolve as resolve13 } from "node:path";
66122
66738
  var reflectionLog, REFLECTION_SYSTEM_PROMPT, DEFAULT_OUTCOME_LIMIT, AgentReflectionService;
66123
66739
  var init_agent_reflection = __esm({
66124
66740
  "../engine/src/agent-reflection.ts"() {
@@ -66424,7 +67040,7 @@ Rules:
66424
67040
  pieces.push(agent.instructionsText.trim());
66425
67041
  }
66426
67042
  if (agent.instructionsPath?.trim()) {
66427
- const resolvedPath = isAbsolute10(agent.instructionsPath) ? agent.instructionsPath : resolve13(this.rootDir, agent.instructionsPath);
67043
+ const resolvedPath = isAbsolute11(agent.instructionsPath) ? agent.instructionsPath : resolve13(this.rootDir, agent.instructionsPath);
66428
67044
  try {
66429
67045
  const content = await readFile16(resolvedPath, "utf-8");
66430
67046
  if (content.trim()) {
@@ -70389,8 +71005,8 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
70389
71005
  // ../engine/src/self-healing.ts
70390
71006
  import { exec as exec8 } from "node:child_process";
70391
71007
  import { promisify as promisify9 } from "node:util";
70392
- import { existsSync as existsSync27, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
70393
- import { isAbsolute as isAbsolute11, join as join33, relative as relative7, resolve as resolve14 } from "node:path";
71008
+ import { existsSync as existsSync28, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
71009
+ import { isAbsolute as isAbsolute12, join as join33, relative as relative7, resolve as resolve14 } from "node:path";
70394
71010
  function shellQuote(value) {
70395
71011
  return `'${value.replace(/'/g, "'\\''")}'`;
70396
71012
  }
@@ -70771,7 +71387,7 @@ var init_self_healing = __esm({
70771
71387
  return commit;
70772
71388
  }
70773
71389
  async cleanupInterruptedMergeArtifacts(task) {
70774
- if (task.worktree && existsSync27(task.worktree)) {
71390
+ if (task.worktree && existsSync28(task.worktree)) {
70775
71391
  try {
70776
71392
  await execAsync8(`git worktree remove ${shellQuote(task.worktree)} --force`, {
70777
71393
  cwd: this.options.rootDir,
@@ -71302,7 +71918,7 @@ var init_self_healing = __esm({
71302
71918
  return false;
71303
71919
  }
71304
71920
  const staleness = now - new Date(t.updatedAt).getTime();
71305
- const hasWorktree = t.worktree && existsSync27(t.worktree);
71921
+ const hasWorktree = t.worktree && existsSync28(t.worktree);
71306
71922
  const graceMs = hasWorktree ? ORPHANED_WITH_WORKTREE_GRACE_MS : ORPHANED_EXECUTION_RECOVERY_GRACE_MS;
71307
71923
  return staleness >= graceMs;
71308
71924
  });
@@ -71311,7 +71927,7 @@ var init_self_healing = __esm({
71311
71927
  let recovered = 0;
71312
71928
  for (const task of orphaned) {
71313
71929
  try {
71314
- const hadWorktree = task.worktree && existsSync27(task.worktree);
71930
+ const hadWorktree = task.worktree && existsSync28(task.worktree);
71315
71931
  const reason = hadWorktree ? "worktree exists but no active session" : "missing worktree/session";
71316
71932
  await this.resetStepsIfWorkLost(task);
71317
71933
  await this.store.updateTask(task.id, {
@@ -71457,7 +72073,7 @@ var init_self_healing = __esm({
71457
72073
  }
71458
72074
  }
71459
72075
  async hasRecoverableGitWork(task) {
71460
- if (task.worktree && existsSync27(task.worktree)) {
72076
+ if (task.worktree && existsSync28(task.worktree)) {
71461
72077
  try {
71462
72078
  const { stdout: status } = await execAsync8("git status --porcelain", {
71463
72079
  cwd: task.worktree,
@@ -71643,7 +72259,7 @@ var init_self_healing = __esm({
71643
72259
  */
71644
72260
  async reapUnregisteredOrphans() {
71645
72261
  const worktreesDir = join33(this.options.rootDir, ".worktrees");
71646
- if (!existsSync27(worktreesDir)) return 0;
72262
+ if (!existsSync28(worktreesDir)) return 0;
71647
72263
  let dirs;
71648
72264
  try {
71649
72265
  dirs = readdirSync5(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join33(worktreesDir, e.name));
@@ -71657,7 +72273,7 @@ var init_self_healing = __esm({
71657
72273
  let cleaned = 0;
71658
72274
  for (const path2 of unregistered) {
71659
72275
  const rel = relative7(worktreesDir, path2);
71660
- if (rel === "" || rel.startsWith("..") || isAbsolute11(rel)) {
72276
+ if (rel === "" || rel.startsWith("..") || isAbsolute12(rel)) {
71661
72277
  log8.warn(`Refusing to remove path outside .worktrees: ${path2}`);
71662
72278
  continue;
71663
72279
  }
@@ -71752,7 +72368,7 @@ var init_self_healing = __esm({
71752
72368
  /** Remove oldest idle worktrees if total count exceeds 2× maxWorktrees. */
71753
72369
  async enforceWorktreeCap() {
71754
72370
  const worktreesDir = join33(this.options.rootDir, ".worktrees");
71755
- if (!existsSync27(worktreesDir)) return;
72371
+ if (!existsSync28(worktreesDir)) return;
71756
72372
  try {
71757
72373
  const settings = await this.store.getSettings();
71758
72374
  const cap = (settings.maxWorktrees ?? 4) * 2;
@@ -72290,13 +72906,13 @@ var init_plugin_runner = __esm({
72290
72906
  * Returns the result on success, throws on timeout.
72291
72907
  */
72292
72908
  withTimeout(promise, ms, timeoutMessage) {
72293
- return new Promise((resolve19, reject) => {
72909
+ return new Promise((resolve17, reject) => {
72294
72910
  const timer = setTimeout(() => {
72295
72911
  reject(new Error(timeoutMessage));
72296
72912
  }, ms);
72297
72913
  promise.then((result) => {
72298
72914
  clearTimeout(timer);
72299
- resolve19(result);
72915
+ resolve17(result);
72300
72916
  }).catch((err) => {
72301
72917
  clearTimeout(timer);
72302
72918
  reject(err);
@@ -72965,7 +73581,7 @@ var init_in_process_runtime = __esm({
72965
73581
  runtimeLog.log(
72966
73582
  `Waiting for ${metrics.inFlightTasks} in-flight tasks to complete...`
72967
73583
  );
72968
- await new Promise((resolve19) => setTimeout(resolve19, 1e3));
73584
+ await new Promise((resolve17) => setTimeout(resolve17, 1e3));
72969
73585
  }
72970
73586
  const finalMetrics = this.getMetrics();
72971
73587
  if (finalMetrics.inFlightTasks > 0) {
@@ -73362,13 +73978,13 @@ var init_ipc_host = __esm({
73362
73978
  }
73363
73979
  const id = generateCorrelationId();
73364
73980
  const message = { type, id, payload };
73365
- return new Promise((resolve19, reject) => {
73981
+ return new Promise((resolve17, reject) => {
73366
73982
  const timeout = setTimeout(() => {
73367
73983
  this.pendingCommands.delete(id);
73368
73984
  reject(new Error(`Command ${type} timed out after ${timeoutMs ?? this.commandTimeoutMs}ms`));
73369
73985
  }, timeoutMs ?? this.commandTimeoutMs);
73370
73986
  this.pendingCommands.set(id, {
73371
- resolve: resolve19,
73987
+ resolve: resolve17,
73372
73988
  reject,
73373
73989
  timeout,
73374
73990
  type
@@ -74177,8 +74793,8 @@ var init_remote_node_client = __esm({
74177
74793
  return error instanceof TypeError;
74178
74794
  }
74179
74795
  async sleep(ms) {
74180
- await new Promise((resolve19) => {
74181
- setTimeout(resolve19, ms);
74796
+ await new Promise((resolve17) => {
74797
+ setTimeout(resolve17, ms);
74182
74798
  });
74183
74799
  }
74184
74800
  };
@@ -74442,14 +75058,14 @@ var init_remote_node_runtime = __esm({
74442
75058
  return error instanceof Error ? error : new Error(String(error));
74443
75059
  }
74444
75060
  async sleep(ms, signal) {
74445
- await new Promise((resolve19) => {
75061
+ await new Promise((resolve17) => {
74446
75062
  const timeout = setTimeout(() => {
74447
75063
  cleanup();
74448
- resolve19();
75064
+ resolve17();
74449
75065
  }, ms);
74450
75066
  const onAbort = () => {
74451
75067
  cleanup();
74452
- resolve19();
75068
+ resolve17();
74453
75069
  };
74454
75070
  const cleanup = () => {
74455
75071
  clearTimeout(timeout);
@@ -75336,10 +75952,10 @@ var init_tunnel_process_manager = __esm({
75336
75952
  lastError: null
75337
75953
  });
75338
75954
  this.emitLog("info", "manager", `Stopping ${currentHandle.provider} tunnel (pid=${currentHandle.child.pid ?? "n/a"})`);
75339
- this.activeStopPromise = new Promise((resolve19) => {
75955
+ this.activeStopPromise = new Promise((resolve17) => {
75340
75956
  const onClose = () => {
75341
75957
  currentHandle.child.removeListener("close", onClose);
75342
- resolve19();
75958
+ resolve17();
75343
75959
  };
75344
75960
  currentHandle.child.once("close", onClose);
75345
75961
  killManagedProcess(currentHandle.child, "SIGTERM");
@@ -75554,6 +76170,12 @@ var init_project_engine = __esm({
75554
76170
  * a follow-up triage task so a fresh agent (or human) can investigate
75555
76171
  * the underlying flake/regression instead of looping forever. */
75556
76172
  static MAX_VERIFICATION_FAILURE_BOUNCES = 3;
76173
+ /** Cap on outer in-review→in-progress bounces caused by auto-merge conflict
76174
+ * retries being exhausted. After this many bounces the task is parked in
76175
+ * in-review with status=failed and a follow-up task is created, so the
76176
+ * 30-minute cooldown sweep cannot loop forever on a merge that requires
76177
+ * human intervention. */
76178
+ static MAX_MERGE_CONFLICT_BOUNCES = 2;
75557
76179
  /** 30-minute cooldown before a retry-exhausted task gets another sweep attempt */
75558
76180
  static AUTO_MERGE_COOLDOWN_MS = 30 * 60 * 1e3;
75559
76181
  // Event handler references for cleanup
@@ -75895,12 +76517,12 @@ ${detail}`
75895
76517
  */
75896
76518
  async onMerge(taskId) {
75897
76519
  if (this.mergeActive.has(taskId)) {
75898
- return new Promise((resolve19, reject) => {
75899
- this.manualMergeResolvers.set(taskId, { resolve: resolve19, reject });
76520
+ return new Promise((resolve17, reject) => {
76521
+ this.manualMergeResolvers.set(taskId, { resolve: resolve17, reject });
75900
76522
  });
75901
76523
  }
75902
- return new Promise((resolve19, reject) => {
75903
- this.manualMergeResolvers.set(taskId, { resolve: resolve19, reject });
76524
+ return new Promise((resolve17, reject) => {
76525
+ this.manualMergeResolvers.set(taskId, { resolve: resolve17, reject });
75904
76526
  this.internalEnqueueMerge(taskId);
75905
76527
  });
75906
76528
  }
@@ -76120,6 +76742,7 @@ ${detail}`
76120
76742
  canMergeTask(task) {
76121
76743
  if (task.mergeDetails?.mergeConfirmed) return true;
76122
76744
  if (this.options.getTaskMergeBlocker?.(task)) return false;
76745
+ if (task.status === "failed") return false;
76123
76746
  return (task.mergeRetries ?? 0) < _ProjectEngine.MAX_AUTO_MERGE_RETRIES || this.hasAutoHealableVerificationBufferFailure(task) || this.isRetryCooldownElapsed(task);
76124
76747
  }
76125
76748
  internalEnqueueMerge(taskId) {
@@ -76283,6 +76906,15 @@ ${detail}`
76283
76906
  continue;
76284
76907
  }
76285
76908
  runtimeLog.error(`${manualResolver ? "Manual" : "Auto"}-merge failed for ${taskId}: ${errorMsg}`);
76909
+ await store.logEntry(
76910
+ taskId,
76911
+ `${manualResolver ? "Manual" : "Auto"}-merge failed: ${errorMsg}`,
76912
+ err instanceof Error ? err.name : void 0
76913
+ ).catch((logErr) => {
76914
+ runtimeLog.warn(
76915
+ `Auto-merge: failed to log merge-failure entry on ${taskId}: ${logErr instanceof Error ? logErr.message : String(logErr)}`
76916
+ );
76917
+ });
76286
76918
  if (manualResolver) {
76287
76919
  this.manualMergeResolvers.delete(taskId);
76288
76920
  manualResolver.reject(err instanceof Error ? err : new Error(errorMsg));
@@ -76361,7 +76993,7 @@ ${detail}`
76361
76993
  const isConflictError = errorMsg.includes("conflict") || errorMsg.includes("Conflict");
76362
76994
  if (taskOnErr && isConflictError) {
76363
76995
  const currentRetries = taskOnErr.mergeRetries ?? 0;
76364
- if (settingsOnErr.autoResolveConflicts !== false && currentRetries < _ProjectEngine.MAX_AUTO_MERGE_RETRIES) {
76996
+ if (settingsOnErr.autoResolveConflicts !== false && currentRetries + 1 < _ProjectEngine.MAX_AUTO_MERGE_RETRIES) {
76365
76997
  const newRetryCount = currentRetries + 1;
76366
76998
  await store.updateTask(taskId, { mergeRetries: newRetryCount, status: null });
76367
76999
  const delayMs = 5e3 * Math.pow(2, currentRetries);
@@ -76372,21 +77004,92 @@ ${detail}`
76372
77004
  if (!this.shuttingDown) this.internalEnqueueMerge(taskId);
76373
77005
  }, delayMs);
76374
77006
  } else {
76375
- try {
76376
- await store.updateTask(taskId, { status: null });
76377
- } catch (recoveryErr) {
76378
- runtimeLog.error(
76379
- `Auto-merge: failed to clear status on ${taskId} after max retries exceeded: ${recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr)}`
76380
- );
77007
+ const previousBounces = taskOnErr.mergeConflictBounceCount ?? 0;
77008
+ const nextBounces = previousBounces + 1;
77009
+ const bounceCap = _ProjectEngine.MAX_MERGE_CONFLICT_BOUNCES;
77010
+ const autoResolveDisabled = settingsOnErr.autoResolveConflicts === false;
77011
+ if (autoResolveDisabled || nextBounces > bounceCap) {
77012
+ const reason = autoResolveDisabled ? "autoResolveConflicts is disabled" : `merge-conflict bounce cap reached (${nextBounces - 1}/${bounceCap})`;
77013
+ try {
77014
+ await store.updateTask(taskId, {
77015
+ status: "failed",
77016
+ mergeRetries: _ProjectEngine.MAX_AUTO_MERGE_RETRIES,
77017
+ error: `Auto-merge gave up: ${reason}. ${errorMsg}`
77018
+ });
77019
+ await store.addTaskComment(
77020
+ taskId,
77021
+ `Auto-merge gave up after ${_ProjectEngine.MAX_AUTO_MERGE_RETRIES} conflict-resolution retries (${reason}). Resolve the conflict on branch \`${taskOnErr.branch ?? "?"}\` manually, then unpause/retry.`,
77022
+ "agent"
77023
+ );
77024
+ await store.logEntry(
77025
+ taskId,
77026
+ `Auto-merge gave up after conflict retries exhausted (${reason}); task parked for human intervention`,
77027
+ "MergeConflictGiveUp"
77028
+ );
77029
+ if (!autoResolveDisabled) {
77030
+ try {
77031
+ const followUp = await store.createTask({
77032
+ description: `Resolve auto-merge conflict on ${taskId} (${taskOnErr.title || "untitled"}). Auto-merge attempted to rebase + resolve ${nextBounces - 1} times against main and exhausted retries each pass. Branch: \`${taskOnErr.branch ?? "?"}\`. Worktree: \`${taskOnErr.worktree ?? "?"}\`. Last merge error: ${errorMsg}`,
77033
+ column: "triage",
77034
+ priority: "high"
77035
+ });
77036
+ await store.addTaskComment(
77037
+ taskId,
77038
+ `Created follow-up ${followUp.id} to track manual conflict resolution.`,
77039
+ "agent"
77040
+ );
77041
+ } catch (followUpErr) {
77042
+ runtimeLog.warn(
77043
+ `Auto-merge: failed to create follow-up for ${taskId}: ${followUpErr instanceof Error ? followUpErr.message : String(followUpErr)}`
77044
+ );
77045
+ }
77046
+ }
77047
+ } catch (recoveryErr) {
77048
+ runtimeLog.error(
77049
+ `Auto-merge: failed to park ${taskId} after conflict-bounce cap: ${recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr)}`
77050
+ );
77051
+ }
77052
+ } else {
77053
+ try {
77054
+ await store.addTaskComment(
77055
+ taskId,
77056
+ `Auto-merge could not resolve conflicts within ${_ProjectEngine.MAX_AUTO_MERGE_RETRIES} retries (bounce ${nextBounces}/${bounceCap}). Bouncing back to in-progress for a fresh rebase against main; the executor will re-run quality gates and re-attempt the merge.`,
77057
+ "agent"
77058
+ );
77059
+ await store.updateTask(taskId, {
77060
+ status: null,
77061
+ mergeRetries: 0,
77062
+ error: null,
77063
+ mergeConflictBounceCount: nextBounces
77064
+ });
77065
+ await store.moveTask(taskId, "in-progress");
77066
+ await store.logEntry(
77067
+ taskId,
77068
+ `Auto-merge conflicts unresolved (${_ProjectEngine.MAX_AUTO_MERGE_RETRIES}/${_ProjectEngine.MAX_AUTO_MERGE_RETRIES}) \u2014 bounced to in-progress for re-rebase (bounce ${nextBounces}/${bounceCap})`,
77069
+ "MergeConflictBounce"
77070
+ );
77071
+ runtimeLog.log(
77072
+ `Auto-merge: ${taskId} conflict retries exhausted \u2014 bounced to in-progress (${nextBounces}/${bounceCap})`
77073
+ );
77074
+ } catch (recoveryErr) {
77075
+ runtimeLog.error(
77076
+ `Auto-merge: failed to bounce ${taskId} after conflict exhaustion: ${recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr)}`
77077
+ );
77078
+ }
76381
77079
  }
76382
77080
  }
76383
77081
  } else {
76384
77082
  try {
76385
77083
  await store.updateTask(taskId, {
76386
- status: null,
77084
+ status: "failed",
76387
77085
  mergeRetries: _ProjectEngine.MAX_AUTO_MERGE_RETRIES,
76388
77086
  error: errorMsg
76389
77087
  });
77088
+ await store.addTaskComment(
77089
+ taskId,
77090
+ `Auto-merge failed with a non-conflict error and stopped retrying: ${errorMsg}`,
77091
+ "agent"
77092
+ );
76390
77093
  } catch (recoveryErr) {
76391
77094
  runtimeLog.error(
76392
77095
  `Auto-merge: failed to update ${taskId} after non-conflict error: ${recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr)}`
@@ -76396,7 +77099,7 @@ ${detail}`
76396
77099
  } else {
76397
77100
  try {
76398
77101
  await store.updateTask(taskId, {
76399
- status: null,
77102
+ status: "failed",
76400
77103
  mergeRetries: _ProjectEngine.MAX_AUTO_MERGE_RETRIES,
76401
77104
  error: errorMsg
76402
77105
  });
@@ -79424,7 +80127,7 @@ function buildHermesArgs(prompt, settings, resumeSessionId) {
79424
80127
  async function invokeHermesCli(prompt, settings, resumeSessionId, signal) {
79425
80128
  const args = buildHermesArgs(prompt, settings, resumeSessionId);
79426
80129
  const binary = resolveBinaryForSpawn(settings.binaryPath);
79427
- return new Promise((resolve19, reject) => {
80130
+ return new Promise((resolve17, reject) => {
79428
80131
  let settled = false;
79429
80132
  const spawnEnv = { ...process.env, PYTHONUNBUFFERED: "1" };
79430
80133
  if (settings.profile) {
@@ -79492,7 +80195,7 @@ ${combined}`));
79492
80195
  return;
79493
80196
  }
79494
80197
  try {
79495
- resolve19(parseHermesOutput(stdout, stderr));
80198
+ resolve17(parseHermesOutput(stdout, stderr));
79496
80199
  } catch (parseErr) {
79497
80200
  reject(parseErr);
79498
80201
  }
@@ -79718,7 +80421,7 @@ async function promptCli(session, message, config, callbacks, signal) {
79718
80421
  const args = buildOpenClawArgs(config, session.sessionId, message);
79719
80422
  const cb = { ...session.callbacks, ...callbacks };
79720
80423
  cb.onToolStart?.("openclaw.agent", { sessionId: session.sessionId });
79721
- return new Promise((resolve19, reject) => {
80424
+ return new Promise((resolve17, reject) => {
79722
80425
  let settled = false;
79723
80426
  const child = spawn5(config.binaryPath, args, {
79724
80427
  stdio: ["ignore", "pipe", "pipe"]
@@ -79811,7 +80514,7 @@ async function promptCli(session, message, config, callbacks, signal) {
79811
80514
  ...metaError ? { error: metaError } : {},
79812
80515
  ...errorText.length > 0 ? { toolErrors: errorText } : {}
79813
80516
  });
79814
- resolve19();
80517
+ resolve17();
79815
80518
  });
79816
80519
  });
79817
80520
  }
@@ -80093,6 +80796,7 @@ var init_register_settings_memory_routes = __esm({
80093
80796
  "../dashboard/src/routes/register-settings-memory-routes.ts"() {
80094
80797
  "use strict";
80095
80798
  init_src();
80799
+ init_src2();
80096
80800
  init_api_error();
80097
80801
  init_remote_auth();
80098
80802
  init_project_store_resolver();
@@ -80122,7 +80826,7 @@ var init_register_messaging_scripts = __esm({
80122
80826
 
80123
80827
  // ../dashboard/src/github.ts
80124
80828
  function delay(ms) {
80125
- return new Promise((resolve19) => setTimeout(resolve19, ms));
80829
+ return new Promise((resolve17) => setTimeout(resolve17, ms));
80126
80830
  }
80127
80831
  function normalizeCheckState(state) {
80128
80832
  switch ((state ?? "").toLowerCase()) {
@@ -82833,7 +83537,7 @@ async function spawnPaperclipCliJson(args, opts) {
82833
83537
  }
82834
83538
  const timeoutMs = opts.cliTimeoutMs ?? 15e3;
82835
83539
  const label = ["paperclipai", ...args].join(" ");
82836
- return new Promise((resolve19, reject) => {
83540
+ return new Promise((resolve17, reject) => {
82837
83541
  let child;
82838
83542
  try {
82839
83543
  child = spawn10(bin, fullArgs, { stdio: ["ignore", "pipe", "pipe"] });
@@ -82879,7 +83583,7 @@ async function spawnPaperclipCliJson(args, opts) {
82879
83583
  return;
82880
83584
  }
82881
83585
  try {
82882
- resolve19(JSON.parse(cleaned));
83586
+ resolve17(JSON.parse(cleaned));
82883
83587
  } catch {
82884
83588
  reject(new Error(`${label} returned non-JSON output: ${cleaned.slice(0, 200)}`));
82885
83589
  }
@@ -82949,7 +83653,7 @@ var init_paperclip_client = __esm({
82949
83653
  // ../../plugins/fusion-plugin-paperclip-runtime/dist/runtime-adapter.js
82950
83654
  import { randomUUID as randomUUID13 } from "node:crypto";
82951
83655
  function sleep3(ms) {
82952
- return new Promise((resolve19) => setTimeout(resolve19, ms));
83656
+ return new Promise((resolve17) => setTimeout(resolve17, ms));
82953
83657
  }
82954
83658
  function asString2(value) {
82955
83659
  return typeof value === "string" ? value : void 0;
@@ -83357,35 +84061,20 @@ var init_update_check = __esm({
83357
84061
  }
83358
84062
  });
83359
84063
 
84064
+ // ../dashboard/src/cli-package-version.ts
84065
+ var init_cli_package_version = __esm({
84066
+ "../dashboard/src/cli-package-version.ts"() {
84067
+ "use strict";
84068
+ }
84069
+ });
84070
+
83360
84071
  // ../dashboard/src/routes/register-update-check-routes.ts
83361
- import { existsSync as existsSync28, readFileSync as readFileSync7 } from "node:fs";
83362
- import { dirname as dirname9, resolve as resolve15 } from "node:path";
83363
- import { fileURLToPath as fileURLToPath3 } from "node:url";
83364
- var CLI_PACKAGE_VERSION;
83365
84072
  var init_register_update_check_routes = __esm({
83366
84073
  "../dashboard/src/routes/register-update-check-routes.ts"() {
83367
84074
  "use strict";
83368
84075
  init_src();
83369
84076
  init_update_check();
83370
- CLI_PACKAGE_VERSION = (() => {
83371
- try {
83372
- let cur = dirname9(fileURLToPath3(import.meta.url));
83373
- for (let i = 0; i < 8; i++) {
83374
- const pkgPath = resolve15(cur, "package.json");
83375
- if (existsSync28(pkgPath)) {
83376
- const parsed = JSON.parse(readFileSync7(pkgPath, "utf-8"));
83377
- if (parsed.name === "@runfusion/fusion" && typeof parsed.version === "string" && parsed.version.length > 0) {
83378
- return parsed.version;
83379
- }
83380
- }
83381
- const parent = resolve15(cur, "..");
83382
- if (parent === cur) break;
83383
- cur = parent;
83384
- }
83385
- } catch {
83386
- }
83387
- return process.env.npm_package_version ?? "0.0.0";
83388
- })();
84077
+ init_cli_package_version();
83389
84078
  }
83390
84079
  });
83391
84080
 
@@ -87412,9 +88101,8 @@ var init_auth_middleware = __esm({
87412
88101
 
87413
88102
  // ../dashboard/src/server.ts
87414
88103
  import express from "express";
87415
- import { join as join35, dirname as dirname10, resolve as resolve16 } from "node:path";
87416
- import { existsSync as existsSync29, readFileSync as readFileSync8 } from "node:fs";
87417
- import { fileURLToPath as fileURLToPath4 } from "node:url";
88104
+ import { join as join35, dirname as dirname9 } from "node:path";
88105
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
87418
88106
  function clearAiSessionCleanupInterval() {
87419
88107
  if (!aiSessionCleanupIntervalHandle) {
87420
88108
  return;
@@ -87422,7 +88110,7 @@ function clearAiSessionCleanupInterval() {
87422
88110
  clearInterval(aiSessionCleanupIntervalHandle);
87423
88111
  aiSessionCleanupIntervalHandle = void 0;
87424
88112
  }
87425
- var __dirname, PACKAGE_VERSION, CLI_PACKAGE_VERSION2, MIN_AI_SESSION_TTL_MS, MAX_AI_SESSION_TTL_MS, MIN_AI_SESSION_CLEANUP_INTERVAL_MS, MAX_AI_SESSION_CLEANUP_INTERVAL_MS, aiSessionCleanupIntervalHandle;
88113
+ var __dirname, MIN_AI_SESSION_TTL_MS, MAX_AI_SESSION_TTL_MS, MIN_AI_SESSION_CLEANUP_INTERVAL_MS, MAX_AI_SESSION_CLEANUP_INTERVAL_MS, aiSessionCleanupIntervalHandle;
87426
88114
  var init_server = __esm({
87427
88115
  "../dashboard/src/server.ts"() {
87428
88116
  "use strict";
@@ -87448,37 +88136,8 @@ var init_server = __esm({
87448
88136
  init_dev_server_routes();
87449
88137
  init_auth_middleware();
87450
88138
  init_remote_auth();
87451
- __dirname = dirname10(fileURLToPath4(import.meta.url));
87452
- PACKAGE_VERSION = (() => {
87453
- try {
87454
- const packageJsonPath = join35(__dirname, "..", "package.json");
87455
- const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf-8"));
87456
- if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
87457
- return packageJson.version;
87458
- }
87459
- } catch {
87460
- }
87461
- return process.env.npm_package_version ?? "0.0.0";
87462
- })();
87463
- CLI_PACKAGE_VERSION2 = (() => {
87464
- try {
87465
- let cur = __dirname;
87466
- for (let i = 0; i < 8; i++) {
87467
- const pkgPath = resolve16(cur, "package.json");
87468
- if (existsSync29(pkgPath)) {
87469
- const parsed = JSON.parse(readFileSync8(pkgPath, "utf-8"));
87470
- if (parsed.name === "@runfusion/fusion" && typeof parsed.version === "string" && parsed.version.length > 0) {
87471
- return parsed.version;
87472
- }
87473
- }
87474
- const parent = resolve16(cur, "..");
87475
- if (parent === cur) break;
87476
- cur = parent;
87477
- }
87478
- } catch {
87479
- }
87480
- return process.env.npm_package_version ?? "0.0.0";
87481
- })();
88139
+ init_cli_package_version();
88140
+ __dirname = dirname9(fileURLToPath3(import.meta.url));
87482
88141
  MIN_AI_SESSION_TTL_MS = 10 * 60 * 1e3;
87483
88142
  MAX_AI_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
87484
88143
  MIN_AI_SESSION_CLEANUP_INTERVAL_MS = 60 * 1e3;
@@ -87542,6 +88201,7 @@ var init_src4 = __esm({
87542
88201
  init_rate_limit();
87543
88202
  init_github_poll();
87544
88203
  init_github_issue_comment();
88204
+ init_cli_package_version();
87545
88205
  init_api_error();
87546
88206
  init_badge_pubsub();
87547
88207
  init_plugins();
@@ -87549,8 +88209,7 @@ var init_src4 = __esm({
87549
88209
  });
87550
88210
 
87551
88211
  // src/project-context.ts
87552
- import { resolve as resolve17, dirname as dirname11 } from "node:path";
87553
- import { existsSync as existsSync30 } from "node:fs";
88212
+ import { resolve as resolve15, dirname as dirname10, basename as basename9 } from "node:path";
87554
88213
  async function resolveProject(projectNameFlag, cwd = process.cwd(), globalDir) {
87555
88214
  const central = new CentralCore(globalDir);
87556
88215
  await central.init();
@@ -87626,21 +88285,21 @@ async function clearDefaultProject(globalDir) {
87626
88285
  await globalStore.updateSettings(rest);
87627
88286
  }
87628
88287
  async function detectProjectFromCwd(cwd, central) {
87629
- let currentDir = resolve17(cwd);
88288
+ let currentDir = resolve15(cwd);
87630
88289
  while (true) {
87631
- const kbPath = resolve17(currentDir, ".fusion", "fusion.db");
87632
- if (existsSync30(kbPath)) {
88290
+ const kbPath = resolve15(currentDir, ".fusion", "fusion.db");
88291
+ if (isValidSqliteDatabaseFile(kbPath)) {
87633
88292
  const project = await central.getProjectByPath(currentDir);
87634
88293
  if (project) {
87635
88294
  return project;
87636
88295
  }
87637
88296
  return {
87638
88297
  id: "",
87639
- name: currentDir.split("/").filter(Boolean).at(-1) ?? "current-project",
88298
+ name: basename9(currentDir) || "current-project",
87640
88299
  path: currentDir
87641
88300
  };
87642
88301
  }
87643
- const parentDir = dirname11(currentDir);
88302
+ const parentDir = dirname10(currentDir);
87644
88303
  if (parentDir === currentDir) {
87645
88304
  break;
87646
88305
  }
@@ -87690,10 +88349,12 @@ async function findNodeByNameOrId(central, nameOrId) {
87690
88349
  }
87691
88350
  return central.getNodeByName(nameOrId);
87692
88351
  }
88352
+ var ANSI_ESCAPE_PATTERN;
87693
88353
  var init_node = __esm({
87694
88354
  "src/commands/node.ts"() {
87695
88355
  "use strict";
87696
88356
  init_src();
88357
+ ANSI_ESCAPE_PATTERN = new RegExp("\\u001b\\[[0-9;]*m", "g");
87697
88358
  }
87698
88359
  });
87699
88360
 
@@ -87729,14 +88390,14 @@ __export(task_exports, {
87729
88390
  runTaskUpdate: () => runTaskUpdate
87730
88391
  });
87731
88392
  import { createInterface as createInterface2 } from "node:readline/promises";
87732
- import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync31, readFileSync as readFileSync9 } from "node:fs";
87733
- import { join as join36 } from "node:path";
88393
+ import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync29, readFileSync as readFileSync7 } from "node:fs";
88394
+ import { basename as basename10, join as join36 } from "node:path";
87734
88395
  function asLocalProjectContext(store) {
87735
88396
  const cwd = process.cwd();
87736
88397
  return {
87737
88398
  projectId: cwd,
87738
88399
  projectPath: cwd,
87739
- projectName: cwd.split("/").filter(Boolean).at(-1) ?? "current-project",
88400
+ projectName: basename10(cwd) || "current-project",
87740
88401
  isRegistered: false,
87741
88402
  store
87742
88403
  };
@@ -87850,10 +88511,10 @@ async function runTaskCreate(descriptionArg, attachFiles, depends, projectName,
87850
88511
  console.log(` Path: .fusion/tasks/${task.id}/`);
87851
88512
  if (attachFiles && attachFiles.length > 0) {
87852
88513
  const { readFile: readFile19 } = await import("node:fs/promises");
87853
- const { basename: basename9, extname: extname3, resolve: resolve19 } = await import("node:path");
88514
+ const { basename: basename12, extname: extname3, resolve: resolve17 } = await import("node:path");
87854
88515
  for (const filePath of attachFiles) {
87855
- const resolvedPath = resolve19(filePath);
87856
- const filename = basename9(resolvedPath);
88516
+ const resolvedPath = resolve17(filePath);
88517
+ const filename = basename12(resolvedPath);
87857
88518
  const ext = extname3(filename).toLowerCase();
87858
88519
  const mimeType = MIME_TYPES[ext];
87859
88520
  if (!mimeType) {
@@ -87987,7 +88648,7 @@ async function runTaskLogs(id, options = {}, projectName) {
87987
88648
  if (options.follow) {
87988
88649
  const projectPath = projectContext?.projectPath ?? process.cwd();
87989
88650
  const logPath = join36(projectPath, ".fusion", "tasks", id, "agent.log");
87990
- if (!existsSync31(logPath)) {
88651
+ if (!existsSync29(logPath)) {
87991
88652
  console.log(`
87992
88653
  Waiting for log file to be created...`);
87993
88654
  }
@@ -88016,7 +88677,7 @@ async function runTaskLogs(id, options = {}, projectName) {
88016
88677
  lastPosition = 0;
88017
88678
  }
88018
88679
  if (stats.size > lastPosition) {
88019
- const content = readFileSync9(logPath, "utf-8");
88680
+ const content = readFileSync7(logPath, "utf-8");
88020
88681
  const lines = content.slice(lastPosition).split("\n");
88021
88682
  for (const line of lines) {
88022
88683
  if (!line.trim()) continue;
@@ -88145,10 +88806,10 @@ async function runTaskMerge(id, projectName) {
88145
88806
  }
88146
88807
  async function runTaskAttach(id, filePath, projectName) {
88147
88808
  const { readFile: readFile19 } = await import("node:fs/promises");
88148
- const { basename: basename9, extname: extname3 } = await import("node:path");
88149
- const { resolve: resolve19 } = await import("node:path");
88150
- const resolvedPath = resolve19(filePath);
88151
- const filename = basename9(resolvedPath);
88809
+ const { basename: basename12, extname: extname3 } = await import("node:path");
88810
+ const { resolve: resolve17 } = await import("node:path");
88811
+ const resolvedPath = resolve17(filePath);
88812
+ const filename = basename12(resolvedPath);
88152
88813
  const ext = extname3(filename).toLowerCase();
88153
88814
  const mimeType = MIME_TYPES[ext];
88154
88815
  if (!mimeType) {
@@ -88678,12 +89339,12 @@ async function promptText(question) {
88678
89339
  console.log(" (Enter your response. Type DONE on its own line when finished):\n");
88679
89340
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
88680
89341
  const lines = [];
88681
- return new Promise((resolve19) => {
89342
+ return new Promise((resolve17) => {
88682
89343
  const askLine = () => {
88683
89344
  rl.question(" ").then((line) => {
88684
89345
  if (line.trim() === "DONE") {
88685
89346
  rl.close();
88686
- resolve19(lines.join("\n"));
89347
+ resolve17(lines.join("\n"));
88687
89348
  } else {
88688
89349
  lines.push(line);
88689
89350
  askLine();
@@ -89087,9 +89748,9 @@ async function runSkillsInstall(args, options) {
89087
89748
  stdio: "inherit",
89088
89749
  shell: true
89089
89750
  });
89090
- const exitCode = await new Promise((resolve19, reject) => {
89751
+ const exitCode = await new Promise((resolve17, reject) => {
89091
89752
  child.on("exit", (code) => {
89092
- resolve19(code ?? 1);
89753
+ resolve17(code ?? 1);
89093
89754
  });
89094
89755
  child.on("error", (err) => {
89095
89756
  reject(err);
@@ -89116,9 +89777,9 @@ init_src();
89116
89777
  init_gh_cli();
89117
89778
  import { Type as Type7 } from "typebox";
89118
89779
  import { StringEnum } from "@mariozechner/pi-ai";
89119
- import { resolve as resolve18, basename as basename8, extname as extname2, join as join37 } from "node:path";
89780
+ import { resolve as resolve16, basename as basename11, extname as extname2, join as join37 } from "node:path";
89120
89781
  import { readFile as readFile18 } from "node:fs/promises";
89121
- import { existsSync as existsSync32 } from "node:fs";
89782
+ import { existsSync as existsSync30 } from "node:fs";
89122
89783
  import { spawn as spawn9 } from "node:child_process";
89123
89784
  var MIME_TYPES2 = {
89124
89785
  ".png": "image/png",
@@ -89136,14 +89797,14 @@ var MIME_TYPES2 = {
89136
89797
  ".xml": "application/xml"
89137
89798
  };
89138
89799
  function resolveProjectRoot(cwd) {
89139
- let current = resolve18(cwd);
89800
+ let current = resolve16(cwd);
89140
89801
  while (true) {
89141
- if (existsSync32(join37(current, ".fusion"))) {
89802
+ if (existsSync30(join37(current, ".fusion"))) {
89142
89803
  return current;
89143
89804
  }
89144
- const parent = resolve18(current, "..");
89805
+ const parent = resolve16(current, "..");
89145
89806
  if (parent === current) {
89146
- return resolve18(cwd);
89807
+ return resolve16(cwd);
89147
89808
  }
89148
89809
  current = parent;
89149
89810
  }
@@ -89487,8 +90148,8 @@ Column: triage
89487
90148
  path: Type7.String({ description: "Path to the file to attach" })
89488
90149
  }),
89489
90150
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
89490
- const filePath = resolve18(ctx.cwd, params.path.replace(/^@/, ""));
89491
- const filename = basename8(filePath);
90151
+ const filePath = resolve16(ctx.cwd, params.path.replace(/^@/, ""));
90152
+ const filename = basename11(filePath);
89492
90153
  const ext = extname2(filename).toLowerCase();
89493
90154
  const mimeType = MIME_TYPES2[ext];
89494
90155
  if (!mimeType) {
@@ -90596,12 +91257,12 @@ Status: ${updated.status}`
90596
91257
  child.stderr?.on("data", (data) => {
90597
91258
  stderr += data.toString();
90598
91259
  });
90599
- const exitCode = await new Promise((resolve19) => {
91260
+ const exitCode = await new Promise((resolve17) => {
90600
91261
  child.on("exit", (code) => {
90601
- resolve19(code ?? 1);
91262
+ resolve17(code ?? 1);
90602
91263
  });
90603
91264
  child.on("error", () => {
90604
- resolve19(1);
91265
+ resolve17(1);
90605
91266
  });
90606
91267
  });
90607
91268
  try {