@runfusion/fusion 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/bin.js +799 -430
  2. package/dist/client/assets/{AgentDetailView-CnedHKqd.js → AgentDetailView-PYfYi6rl.js} +1 -1
  3. package/dist/client/assets/{AgentsView-TCAMvB-N.js → AgentsView-CjqSko8c.js} +3 -3
  4. package/dist/client/assets/{ChatView-B7djAFGF.js → ChatView-CY7Mdd3y.js} +1 -1
  5. package/dist/client/assets/{DevServerView-B_zgnvEn.js → DevServerView-Jt4PTXWB.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-B3ejJOl0.js → DirectoryPicker-Bcoh2_ft.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-DzmbEKrl.js → DocumentsView-B2YRSmIS.js} +1 -1
  8. package/dist/client/assets/{InsightsView-CoRsf8DC.js → InsightsView-C4-vGe-H.js} +1 -1
  9. package/dist/client/assets/{MemoryView-oFUIaoM5.js → MemoryView-Ch7agzuP.js} +1 -1
  10. package/dist/client/assets/{NodesView-BXUaViVf.js → NodesView-D9rm5-_6.js} +1 -1
  11. package/dist/client/assets/{PiExtensionsManager-DJRC5zRv.js → PiExtensionsManager-hpV1-dOB.js} +1 -1
  12. package/dist/client/assets/{PluginManager-OQhiY2zP.js → PluginManager-DHHM1Xeg.js} +1 -1
  13. package/dist/client/assets/{RoadmapsView-DwdewIMm.js → RoadmapsView-BpqARpKn.js} +1 -1
  14. package/dist/client/assets/{SettingsModal-Bmz_YrW5.js → SettingsModal-B8Q5r_TT.js} +3 -3
  15. package/dist/client/assets/{SettingsModal-CMRJwSFd.js → SettingsModal-DFitE2kE.js} +1 -1
  16. package/dist/client/assets/{SetupWizardModal-BnPgyQho.js → SetupWizardModal-CjUbb4VZ.js} +1 -1
  17. package/dist/client/assets/{SkillsView-pHEyP-vg.js → SkillsView-CaQNaA67.js} +1 -1
  18. package/dist/client/assets/{TodoView-Cg-xDGeH.js → TodoView-Bj0DcDdf.js} +1 -1
  19. package/dist/client/assets/{folder-open-DJxjQmf1.js → folder-open-DDanUhRq.js} +1 -1
  20. package/dist/client/assets/{index-Cf4wCcWp.js → index-BRTEq_-7.js} +5 -5
  21. package/dist/client/assets/index-w9ci2GQ7.css +1 -0
  22. package/dist/client/assets/{list-checks-NjVTpQgK.js → list-checks-eKLuZO4Y.js} +1 -1
  23. package/dist/client/assets/{upload-BQA5FG0G.js → upload-DDJr7Sat.js} +1 -1
  24. package/dist/client/assets/{users-p7CqLoIz.js → users-zMb4VibZ.js} +1 -1
  25. package/dist/client/index.html +2 -2
  26. package/dist/client/version.json +1 -1
  27. package/dist/extension.js +352 -154
  28. package/dist/pi-claude-cli/package.json +1 -1
  29. package/package.json +5 -5
  30. package/dist/client/assets/index-B7TDbn3p.css +0 -1
package/dist/extension.js CHANGED
@@ -164,6 +164,8 @@ var init_settings_schema = __esm({
164
164
  archiveAgentLogMode: "compact",
165
165
  autoUpdatePrStatus: false,
166
166
  autoCreatePr: false,
167
+ githubCommentOnDone: false,
168
+ githubCommentTemplate: void 0,
167
169
  autoBackupEnabled: false,
168
170
  autoBackupSchedule: "0 2 * * *",
169
171
  autoBackupRetention: 7,
@@ -2274,8 +2276,8 @@ function normalizeTaskComments(steeringComments, comments) {
2274
2276
  comments: normalizedComments
2275
2277
  };
2276
2278
  }
2277
- function createDatabase(fusionDir) {
2278
- return new Database(fusionDir);
2279
+ function createDatabase(fusionDir, options) {
2280
+ return new Database(fusionDir, options);
2279
2281
  }
2280
2282
  var SCHEMA_VERSION, SCHEMA_SQL, Database;
2281
2283
  var init_db = __esm({
@@ -2283,7 +2285,7 @@ var init_db = __esm({
2283
2285
  "use strict";
2284
2286
  init_sqlite_adapter();
2285
2287
  init_types();
2286
- SCHEMA_VERSION = 47;
2288
+ SCHEMA_VERSION = 48;
2287
2289
  SCHEMA_SQL = `
2288
2290
  -- Tasks table with JSON columns for nested data
2289
2291
  CREATE TABLE IF NOT EXISTS tasks (
@@ -2791,16 +2793,19 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
2791
2793
  /** Tracks transaction nesting depth for savepoint-based nested transactions. */
2792
2794
  transactionDepth = 0;
2793
2795
  _fts5Available;
2794
- constructor(fusionDir) {
2795
- this.dbPath = join(fusionDir, "fusion.db");
2796
- if (!isAbsolute(fusionDir)) {
2796
+ constructor(fusionDir, options) {
2797
+ const inMemory = options?.inMemory === true;
2798
+ this.dbPath = inMemory ? ":memory:" : join(fusionDir, "fusion.db");
2799
+ if (!inMemory && !isAbsolute(fusionDir)) {
2797
2800
  throw new Error(`[fusion] Database constructor requires an absolute fusionDir path, got: ${fusionDir}`);
2798
2801
  }
2799
- if (!existsSync(fusionDir)) {
2802
+ if (!inMemory && !existsSync(fusionDir)) {
2800
2803
  mkdirSync(fusionDir, { recursive: true });
2801
2804
  }
2802
2805
  this.db = new DatabaseSync(this.dbPath);
2803
- this.db.exec("PRAGMA journal_mode = WAL");
2806
+ if (!inMemory) {
2807
+ this.db.exec("PRAGMA journal_mode = WAL");
2808
+ }
2804
2809
  this.db.exec("PRAGMA busy_timeout = 5000");
2805
2810
  this.db.exec("PRAGMA foreign_keys = ON");
2806
2811
  this._fts5Available = probeFts5(this.db);
@@ -3729,6 +3734,11 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
3729
3734
  }
3730
3735
  });
3731
3736
  }
3737
+ if (version < 48) {
3738
+ this.applyMigration(48, () => {
3739
+ this.addColumnIfMissing("tasks", "verificationFailureCount", "INTEGER DEFAULT 0");
3740
+ });
3741
+ }
3732
3742
  }
3733
3743
  /**
3734
3744
  * Run a single migration step inside a transaction and bump the version.
@@ -3918,6 +3928,7 @@ var init_agent_store = __esm({
3918
3928
  locks = /* @__PURE__ */ new Map();
3919
3929
  _db = null;
3920
3930
  taskStore;
3931
+ inMemoryDb;
3921
3932
  constructor(options = {}) {
3922
3933
  super();
3923
3934
  if (!options.rootDir && process.env.VITEST === "true") {
@@ -3928,10 +3939,11 @@ var init_agent_store = __esm({
3928
3939
  this.rootDir = options.rootDir ?? resolve(".fusion");
3929
3940
  this.agentsDir = join2(this.rootDir, "agents");
3930
3941
  this.taskStore = options.taskStore;
3942
+ this.inMemoryDb = options.inMemoryDb === true;
3931
3943
  }
3932
3944
  get db() {
3933
3945
  if (!this._db) {
3934
- this._db = new Database(this.rootDir);
3946
+ this._db = new Database(this.rootDir, { inMemory: this.inMemoryDb });
3935
3947
  this._db.init();
3936
3948
  }
3937
3949
  return this._db;
@@ -6231,9 +6243,9 @@ var init_global_settings = __esm({
6231
6243
  * Serialize operations via promise chain to prevent lost-update races.
6232
6244
  */
6233
6245
  withLock(fn) {
6234
- let resolve16;
6246
+ let resolve17;
6235
6247
  const next = new Promise((r) => {
6236
- resolve16 = r;
6248
+ resolve17 = r;
6237
6249
  });
6238
6250
  const prev = this.lock;
6239
6251
  this.lock = next;
@@ -6241,7 +6253,7 @@ var init_global_settings = __esm({
6241
6253
  try {
6242
6254
  return await fn();
6243
6255
  } finally {
6244
- resolve16();
6256
+ resolve17();
6245
6257
  }
6246
6258
  });
6247
6259
  }
@@ -6305,12 +6317,15 @@ END;
6305
6317
  ArchiveDatabase = class {
6306
6318
  db;
6307
6319
  _fts5Available;
6308
- constructor(fusionDir) {
6309
- if (!existsSync4(fusionDir)) {
6320
+ constructor(fusionDir, options) {
6321
+ const inMemory = options?.inMemory === true;
6322
+ if (!inMemory && !existsSync4(fusionDir)) {
6310
6323
  mkdirSync3(fusionDir, { recursive: true });
6311
6324
  }
6312
- this.db = new DatabaseSync(join5(fusionDir, "archive.db"));
6313
- this.db.exec("PRAGMA journal_mode = WAL");
6325
+ this.db = new DatabaseSync(inMemory ? ":memory:" : join5(fusionDir, "archive.db"));
6326
+ if (!inMemory) {
6327
+ this.db.exec("PRAGMA journal_mode = WAL");
6328
+ }
6314
6329
  this.db.exec("PRAGMA busy_timeout = 5000");
6315
6330
  this._fts5Available = probeFts5(this.db);
6316
6331
  }
@@ -9525,19 +9540,21 @@ var init_plugin_store = __esm({
9525
9540
  init_db();
9526
9541
  init_plugin_types();
9527
9542
  PluginStore = class extends EventEmitter5 {
9528
- constructor(rootDir) {
9543
+ constructor(rootDir, options) {
9529
9544
  super();
9530
9545
  this.rootDir = rootDir;
9546
+ this.inMemoryDb = options?.inMemoryDb === true;
9531
9547
  }
9532
9548
  /** SQLite database instance */
9533
9549
  _db = null;
9550
+ inMemoryDb;
9534
9551
  /**
9535
9552
  * Get the SQLite database, initializing it on first access.
9536
9553
  */
9537
9554
  get db() {
9538
9555
  if (!this._db) {
9539
9556
  const fusionDir = join7(this.rootDir, ".fusion");
9540
- this._db = new Database(fusionDir);
9557
+ this._db = new Database(fusionDir, { inMemory: this.inMemoryDb });
9541
9558
  this._db.init();
9542
9559
  }
9543
9560
  return this._db;
@@ -27410,21 +27427,23 @@ var init_automation_store = __esm({
27410
27427
  init_db();
27411
27428
  CRON_TIMEZONE = "UTC";
27412
27429
  AutomationStore = class _AutomationStore extends EventEmitter11 {
27413
- constructor(rootDir) {
27430
+ constructor(rootDir, options) {
27414
27431
  super();
27415
27432
  this.rootDir = rootDir;
27433
+ this.inMemoryDb = options?.inMemoryDb === true;
27416
27434
  }
27417
27435
  /** Per-schedule promise chain for serializing writes. */
27418
27436
  scheduleLocks = /* @__PURE__ */ new Map();
27419
27437
  /** SQLite database instance */
27420
27438
  _db = null;
27439
+ inMemoryDb;
27421
27440
  /**
27422
27441
  * Get the SQLite database, initializing it on first access.
27423
27442
  */
27424
27443
  get db() {
27425
27444
  if (!this._db) {
27426
27445
  const fusionDir = join12(this.rootDir, ".fusion");
27427
- this._db = new Database(fusionDir);
27446
+ this._db = new Database(fusionDir, { inMemory: this.inMemoryDb });
27428
27447
  this._db.init();
27429
27448
  }
27430
27449
  return this._db;
@@ -27489,9 +27508,9 @@ var init_automation_store = __esm({
27489
27508
  */
27490
27509
  withScheduleLock(id, fn) {
27491
27510
  const prev = this.scheduleLocks.get(id) ?? Promise.resolve();
27492
- let resolve16;
27511
+ let resolve17;
27493
27512
  const next = new Promise((r) => {
27494
- resolve16 = r;
27513
+ resolve17 = r;
27495
27514
  });
27496
27515
  this.scheduleLocks.set(id, next);
27497
27516
  return prev.then(async () => {
@@ -27501,7 +27520,7 @@ var init_automation_store = __esm({
27501
27520
  if (this.scheduleLocks.get(id) === next) {
27502
27521
  this.scheduleLocks.delete(id);
27503
27522
  }
27504
- resolve16();
27523
+ resolve17();
27505
27524
  }
27506
27525
  });
27507
27526
  }
@@ -29289,7 +29308,7 @@ var init_project_memory = __esm({
29289
29308
  // ../core/src/run-command.ts
29290
29309
  import { spawn } from "node:child_process";
29291
29310
  function runCommandAsync(command, options = {}) {
29292
- return new Promise((resolve16) => {
29311
+ return new Promise((resolve17) => {
29293
29312
  const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
29294
29313
  let stdout = "";
29295
29314
  let stderr = "";
@@ -29348,7 +29367,7 @@ function runCommandAsync(command, options = {}) {
29348
29367
  clearTimeout(forceKillTimer);
29349
29368
  forceKillTimer = null;
29350
29369
  }
29351
- resolve16({
29370
+ resolve17({
29352
29371
  stdout,
29353
29372
  stderr,
29354
29373
  exitCode: null,
@@ -29366,7 +29385,7 @@ function runCommandAsync(command, options = {}) {
29366
29385
  }
29367
29386
  signalProcessGroup("SIGTERM");
29368
29387
  scheduleForceKill(NORMAL_CLEANUP_FORCE_KILL_DELAY_MS);
29369
- resolve16({
29388
+ resolve17({
29370
29389
  stdout,
29371
29390
  stderr,
29372
29391
  exitCode: code,
@@ -29426,8 +29445,8 @@ function assertSafeGitBranchName(name) {
29426
29445
  }
29427
29446
  }
29428
29447
  function assertSafeAbsolutePath(path) {
29429
- const isAbsolute11 = path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path);
29430
- if (!path || path.length > 4096 || !isAbsolute11 || path.startsWith("-") || // Reject shell metacharacters, quotes, control chars, and NULs.
29448
+ const isAbsolute12 = path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path);
29449
+ if (!path || path.length > 4096 || !isAbsolute12 || path.startsWith("-") || // Reject shell metacharacters, quotes, control chars, and NULs.
29431
29450
  /["'`$\n\r\t;&|<>()*?[\]{}\\\0]/.test(
29432
29451
  path.replace(/^[A-Za-z]:/, "")
29433
29452
  // ignore the drive-letter colon on Windows
@@ -29519,13 +29538,14 @@ var init_store = __esm({
29519
29538
  }
29520
29539
  };
29521
29540
  TaskStore = class _TaskStore extends EventEmitter12 {
29522
- constructor(rootDir, globalSettingsDir) {
29541
+ constructor(rootDir, globalSettingsDir, options) {
29523
29542
  super();
29524
29543
  this.rootDir = rootDir;
29525
29544
  this.setMaxListeners(100);
29526
29545
  this.fusionDir = join15(rootDir, ".fusion");
29527
29546
  this.tasksDir = join15(this.fusionDir, "tasks");
29528
29547
  this.configPath = join15(this.fusionDir, "config.json");
29548
+ this.inMemoryDb = options?.inMemoryDb === true;
29529
29549
  const resolvedGlobalSettingsDir = globalSettingsDir ?? (process.env.VITEST === "true" ? join15(rootDir, ".fusion-global-settings") : void 0);
29530
29550
  this.globalSettingsStore = new GlobalSettingsStore(resolvedGlobalSettingsDir);
29531
29551
  }
@@ -29568,6 +29588,7 @@ var init_store = __esm({
29568
29588
  configPath;
29569
29589
  /** SQLite database for structured data storage */
29570
29590
  _db = null;
29591
+ activityListenersWired = false;
29571
29592
  /** Separate SQLite database for compact archived task snapshots. */
29572
29593
  _archiveDb = null;
29573
29594
  /** File-system watcher instance */
@@ -29610,13 +29631,19 @@ var init_store = __esm({
29610
29631
  insightStore = null;
29611
29632
  /** Cached TodoStore instance */
29612
29633
  todoStore = null;
29634
+ // Test-only: when true, both fusion.db and archive.db open as `:memory:`
29635
+ // SQLite connections instead of disk-backed files. Production code never
29636
+ // sets this; it's gated through an opt-in TaskStoreOptions field below.
29637
+ // Tests that need cross-instance persistence (open store A, close,
29638
+ // open store B on the same dir, expect data) must leave this false.
29639
+ inMemoryDb;
29613
29640
  /**
29614
29641
  * Get the SQLite database, initializing it on first access.
29615
29642
  * Also performs auto-migration from legacy file-based storage if needed.
29616
29643
  */
29617
29644
  get db() {
29618
29645
  if (!this._db) {
29619
- this._db = new Database(this.fusionDir);
29646
+ this._db = new Database(this.fusionDir, { inMemory: this.inMemoryDb });
29620
29647
  this._db.init();
29621
29648
  if (detectLegacyData(this.fusionDir)) {
29622
29649
  }
@@ -29625,7 +29652,7 @@ var init_store = __esm({
29625
29652
  }
29626
29653
  get archiveDb() {
29627
29654
  if (!this._archiveDb) {
29628
- this._archiveDb = new ArchiveDatabase(this.fusionDir);
29655
+ this._archiveDb = new ArchiveDatabase(this.fusionDir, { inMemory: this.inMemoryDb });
29629
29656
  this._archiveDb.init();
29630
29657
  this.migrateLegacyArchiveEntriesToArchiveDb();
29631
29658
  }
@@ -29634,7 +29661,7 @@ var init_store = __esm({
29634
29661
  async init() {
29635
29662
  await mkdir7(this.tasksDir, { recursive: true });
29636
29663
  if (!this._db) {
29637
- this._db = new Database(this.fusionDir);
29664
+ this._db = new Database(this.fusionDir, { inMemory: this.inMemoryDb });
29638
29665
  this._db.init();
29639
29666
  }
29640
29667
  if (detectLegacyData(this.fusionDir)) {
@@ -29703,6 +29730,7 @@ var init_store = __esm({
29703
29730
  postReviewFixCount: row.postReviewFixCount ?? void 0,
29704
29731
  recoveryRetryCount: row.recoveryRetryCount ?? void 0,
29705
29732
  taskDoneRetryCount: row.taskDoneRetryCount ?? void 0,
29733
+ verificationFailureCount: row.verificationFailureCount ?? void 0,
29706
29734
  nextRecoveryAt: row.nextRecoveryAt || void 0,
29707
29735
  error: row.error || void 0,
29708
29736
  summary: row.summary || void 0,
@@ -29987,6 +30015,7 @@ ${recentText}` : void 0
29987
30015
  "postReviewFixCount",
29988
30016
  "recoveryRetryCount",
29989
30017
  "taskDoneRetryCount",
30018
+ "verificationFailureCount",
29990
30019
  "nextRecoveryAt",
29991
30020
  "error",
29992
30021
  "summary",
@@ -30056,6 +30085,7 @@ ${recentText}` : void 0
30056
30085
  "postReviewFixCount",
30057
30086
  "recoveryRetryCount",
30058
30087
  "taskDoneRetryCount",
30088
+ "verificationFailureCount",
30059
30089
  "nextRecoveryAt",
30060
30090
  "error",
30061
30091
  "summary",
@@ -30123,7 +30153,7 @@ ${recentText}` : void 0
30123
30153
  id, title, description, priority, "column", status, size, reviewLevel, currentStep,
30124
30154
  worktree, blockedBy, paused, baseBranch, branch, baseCommitSha, modelPresetId, modelProvider,
30125
30155
  modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
30126
- workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, nextRecoveryAt, error,
30156
+ workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, nextRecoveryAt, error,
30127
30157
  summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
30128
30158
  tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
30129
30159
  dependencies, steps, log, attachments, steeringComments,
@@ -30131,7 +30161,7 @@ ${recentText}` : void 0
30131
30161
  sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
30132
30162
  mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, checkedOutBy, checkedOutAt
30133
30163
  ) VALUES (
30134
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
30164
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
30135
30165
  )
30136
30166
  ON CONFLICT(id) DO UPDATE SET
30137
30167
  title = excluded.title,
@@ -30161,6 +30191,7 @@ ${recentText}` : void 0
30161
30191
  postReviewFixCount = excluded.postReviewFixCount,
30162
30192
  recoveryRetryCount = excluded.recoveryRetryCount,
30163
30193
  taskDoneRetryCount = excluded.taskDoneRetryCount,
30194
+ verificationFailureCount = excluded.verificationFailureCount,
30164
30195
  nextRecoveryAt = excluded.nextRecoveryAt,
30165
30196
  error = excluded.error,
30166
30197
  summary = excluded.summary,
@@ -30228,6 +30259,7 @@ ${recentText}` : void 0
30228
30259
  task.postReviewFixCount ?? 0,
30229
30260
  task.recoveryRetryCount ?? null,
30230
30261
  task.taskDoneRetryCount ?? 0,
30262
+ task.verificationFailureCount ?? 0,
30231
30263
  task.nextRecoveryAt ?? null,
30232
30264
  task.error ?? null,
30233
30265
  task.summary ?? null,
@@ -30303,8 +30335,14 @@ ${recentText}` : void 0
30303
30335
  /**
30304
30336
  * Set up event listeners for activity logging.
30305
30337
  * Call after init() to record task lifecycle events.
30338
+ *
30339
+ * Idempotent — repeated calls are no-ops. Without this guard, each duplicate
30340
+ * call double-registers handlers, causing the activity log to record every
30341
+ * `task:created` / `task:moved` event N times where N = number of init() calls.
30306
30342
  */
30307
30343
  setupActivityLogListeners() {
30344
+ if (this.activityListenersWired) return;
30345
+ this.activityListenersWired = true;
30308
30346
  this.on("task:created", (task) => {
30309
30347
  this.recordActivityFromListener(
30310
30348
  {
@@ -30408,9 +30446,9 @@ ${recentText}` : void 0
30408
30446
  * lost-update races on the nextId counter.
30409
30447
  */
30410
30448
  withConfigLock(fn) {
30411
- let resolve16;
30449
+ let resolve17;
30412
30450
  const next = new Promise((r) => {
30413
- resolve16 = r;
30451
+ resolve17 = r;
30414
30452
  });
30415
30453
  const prev = this.configLock;
30416
30454
  this.configLock = next;
@@ -30418,7 +30456,7 @@ ${recentText}` : void 0
30418
30456
  try {
30419
30457
  return await fn();
30420
30458
  } finally {
30421
- resolve16();
30459
+ resolve17();
30422
30460
  }
30423
30461
  });
30424
30462
  }
@@ -30428,9 +30466,9 @@ ${recentText}` : void 0
30428
30466
  */
30429
30467
  withTaskLock(id, fn) {
30430
30468
  const prev = this.taskLocks.get(id) ?? Promise.resolve();
30431
- let resolve16;
30469
+ let resolve17;
30432
30470
  const next = new Promise((r) => {
30433
- resolve16 = r;
30471
+ resolve17 = r;
30434
30472
  });
30435
30473
  this.taskLocks.set(id, next);
30436
30474
  return prev.then(async () => {
@@ -30440,7 +30478,7 @@ ${recentText}` : void 0
30440
30478
  if (this.taskLocks.get(id) === next) {
30441
30479
  this.taskLocks.delete(id);
30442
30480
  }
30443
- resolve16();
30481
+ resolve17();
30444
30482
  }
30445
30483
  });
30446
30484
  }
@@ -31550,6 +31588,11 @@ ${newTask.description}
31550
31588
  } else if (updates.taskDoneRetryCount !== void 0) {
31551
31589
  task.taskDoneRetryCount = updates.taskDoneRetryCount;
31552
31590
  }
31591
+ if (updates.verificationFailureCount === null) {
31592
+ task.verificationFailureCount = void 0;
31593
+ } else if (updates.verificationFailureCount !== void 0) {
31594
+ task.verificationFailureCount = updates.verificationFailureCount;
31595
+ }
31553
31596
  if (updates.nextRecoveryAt === null) {
31554
31597
  task.nextRecoveryAt = void 0;
31555
31598
  } else if (updates.nextRecoveryAt !== void 0) {
@@ -32541,7 +32584,7 @@ ${task.description}
32541
32584
  this.emit("task:deleted", cached);
32542
32585
  }
32543
32586
  }
32544
- await new Promise((resolve16) => setImmediate(resolve16));
32587
+ await new Promise((resolve17) => setImmediate(resolve17));
32545
32588
  const selectClause = this.getTaskSelectClause(true);
32546
32589
  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();
32547
32590
  this.lastPollTime = (/* @__PURE__ */ new Date()).toISOString();
@@ -32561,7 +32604,7 @@ ${task.description}
32561
32604
  this.emit("task:updated", task);
32562
32605
  }
32563
32606
  if (i > 0 && i % 50 === 0) {
32564
- await new Promise((resolve16) => setImmediate(resolve16));
32607
+ await new Promise((resolve17) => setImmediate(resolve17));
32565
32608
  }
32566
32609
  }
32567
32610
  const elapsed = Date.now() - startTime;
@@ -34382,7 +34425,7 @@ function runGh(args, cwd) {
34382
34425
  }
34383
34426
  }
34384
34427
  function runGhAsync(args, cwd) {
34385
- return new Promise((resolve16, reject) => {
34428
+ return new Promise((resolve17, reject) => {
34386
34429
  execFile2(
34387
34430
  "gh",
34388
34431
  args,
@@ -34398,7 +34441,7 @@ function runGhAsync(args, cwd) {
34398
34441
  ghError.stderr = stderr ?? "";
34399
34442
  reject(ghError);
34400
34443
  } else {
34401
- resolve16(stdout ?? "");
34444
+ resolve17(stdout ?? "");
34402
34445
  }
34403
34446
  }
34404
34447
  );
@@ -34516,14 +34559,16 @@ var init_routine_store = __esm({
34516
34559
  init_routine();
34517
34560
  CRON_TIMEZONE2 = "UTC";
34518
34561
  RoutineStore = class _RoutineStore extends EventEmitter13 {
34519
- constructor(rootDir) {
34562
+ constructor(rootDir, options) {
34520
34563
  super();
34521
34564
  this.rootDir = rootDir;
34565
+ this.inMemoryDb = options?.inMemoryDb === true;
34522
34566
  }
34523
34567
  /** SQLite database instance (lazy init). */
34524
34568
  _db = null;
34525
34569
  /** Per-routine promise chain for serializing writes. */
34526
34570
  routineLocks = /* @__PURE__ */ new Map();
34571
+ inMemoryDb;
34527
34572
  // ── Database Access ────────────────────────────────────────────────
34528
34573
  /**
34529
34574
  * Get the SQLite database, initializing it on first access.
@@ -34531,7 +34576,7 @@ var init_routine_store = __esm({
34531
34576
  get db() {
34532
34577
  if (!this._db) {
34533
34578
  const fusionDir = `${this.rootDir}/.fusion`;
34534
- this._db = new Database(fusionDir);
34579
+ this._db = new Database(fusionDir, { inMemory: this.inMemoryDb });
34535
34580
  this._db.init();
34536
34581
  }
34537
34582
  return this._db;
@@ -34652,9 +34697,9 @@ var init_routine_store = __esm({
34652
34697
  */
34653
34698
  withRoutineLock(id, fn) {
34654
34699
  const prev = this.routineLocks.get(id) ?? Promise.resolve();
34655
- let resolve16;
34700
+ let resolve17;
34656
34701
  const next = new Promise((r) => {
34657
- resolve16 = r;
34702
+ resolve17 = r;
34658
34703
  });
34659
34704
  this.routineLocks.set(id, next);
34660
34705
  return prev.then(async () => {
@@ -34664,7 +34709,7 @@ var init_routine_store = __esm({
34664
34709
  if (this.routineLocks.get(id) === next) {
34665
34710
  this.routineLocks.delete(id);
34666
34711
  }
34667
- resolve16();
34712
+ resolve17();
34668
34713
  }
34669
34714
  });
34670
34715
  }
@@ -35186,13 +35231,13 @@ var init_plugin_loader = __esm({
35186
35231
  * Execute a promise with a timeout.
35187
35232
  */
35188
35233
  withTimeout(promise, ms, timeoutMessage) {
35189
- return new Promise((resolve16, reject) => {
35234
+ return new Promise((resolve17, reject) => {
35190
35235
  const timer = setTimeout(() => {
35191
35236
  reject(new Error(timeoutMessage));
35192
35237
  }, ms);
35193
35238
  promise.then((result) => {
35194
35239
  clearTimeout(timer);
35195
- resolve16(result);
35240
+ resolve17(result);
35196
35241
  }).catch((err) => {
35197
35242
  clearTimeout(timer);
35198
35243
  reject(err);
@@ -38399,7 +38444,7 @@ var require_get_stream = __commonJS({
38399
38444
  };
38400
38445
  const { maxBuffer } = options;
38401
38446
  let stream;
38402
- await new Promise((resolve16, reject) => {
38447
+ await new Promise((resolve17, reject) => {
38403
38448
  const rejectPromise = (error) => {
38404
38449
  if (error && stream.getBufferedLength() <= BufferConstants.MAX_LENGTH) {
38405
38450
  error.bufferedData = stream.getBufferedValue();
@@ -38411,7 +38456,7 @@ var require_get_stream = __commonJS({
38411
38456
  rejectPromise(error);
38412
38457
  return;
38413
38458
  }
38414
- resolve16();
38459
+ resolve17();
38415
38460
  });
38416
38461
  stream.on("data", () => {
38417
38462
  if (stream.getBufferedLength() > maxBuffer) {
@@ -39705,7 +39750,7 @@ var require_extract_zip = __commonJS({
39705
39750
  debug("opening", this.zipPath, "with opts", this.opts);
39706
39751
  this.zipfile = await openZip(this.zipPath, { lazyEntries: true });
39707
39752
  this.canceled = false;
39708
- return new Promise((resolve16, reject) => {
39753
+ return new Promise((resolve17, reject) => {
39709
39754
  this.zipfile.on("error", (err) => {
39710
39755
  this.canceled = true;
39711
39756
  reject(err);
@@ -39714,7 +39759,7 @@ var require_extract_zip = __commonJS({
39714
39759
  this.zipfile.on("close", () => {
39715
39760
  if (!this.canceled) {
39716
39761
  debug("zip extraction complete");
39717
- resolve16();
39762
+ resolve17();
39718
39763
  }
39719
39764
  });
39720
39765
  this.zipfile.on("entry", async (entry) => {
@@ -49720,12 +49765,12 @@ var init_concurrency = __esm({
49720
49765
  this._active++;
49721
49766
  return Promise.resolve();
49722
49767
  }
49723
- return new Promise((resolve16) => {
49768
+ return new Promise((resolve17) => {
49724
49769
  this._waiters.push({
49725
49770
  priority,
49726
49771
  resolve: () => {
49727
49772
  this._active++;
49728
- resolve16();
49773
+ resolve17();
49729
49774
  }
49730
49775
  });
49731
49776
  });
@@ -52145,20 +52190,20 @@ async function withRateLimitRetry(fn, options = {}) {
52145
52190
  throw lastError ?? new Error("withRateLimitRetry: unexpected state");
52146
52191
  }
52147
52192
  function sleep(ms, signal) {
52148
- return new Promise((resolve16, reject) => {
52193
+ return new Promise((resolve17, reject) => {
52149
52194
  if (signal?.aborted) {
52150
52195
  reject(signal.reason ?? new Error("Aborted"));
52151
52196
  return;
52152
52197
  }
52153
- const timer = setTimeout(resolve16, ms);
52198
+ const timer = setTimeout(resolve17, ms);
52154
52199
  if (signal) {
52155
52200
  const onAbort = () => {
52156
52201
  clearTimeout(timer);
52157
52202
  reject(signal.reason ?? new Error("Aborted"));
52158
52203
  };
52159
52204
  signal.addEventListener("abort", onAbort, { once: true });
52160
- const origResolve = resolve16;
52161
- resolve16 = () => {
52205
+ const origResolve = resolve17;
52206
+ resolve17 = () => {
52162
52207
  signal.removeEventListener("abort", onAbort);
52163
52208
  origResolve();
52164
52209
  };
@@ -54053,7 +54098,14 @@ import { existsSync as existsSync21 } from "node:fs";
54053
54098
  import { join as join26 } from "node:path";
54054
54099
  import { Type as Type3 } from "typebox";
54055
54100
  async function execWithProcessGroup(command, options) {
54056
- return new Promise((resolve16, reject) => {
54101
+ return new Promise((resolve17, reject) => {
54102
+ if (options.signal?.aborted) {
54103
+ reject(Object.assign(
54104
+ new Error(`Command aborted before start: ${command}`),
54105
+ { code: "ABORT_ERR", aborted: true, stdout: "", stderr: "" }
54106
+ ));
54107
+ return;
54108
+ }
54057
54109
  const child = spawn2(command, {
54058
54110
  cwd: options.cwd,
54059
54111
  shell: true,
@@ -54065,24 +54117,33 @@ async function execWithProcessGroup(command, options) {
54065
54117
  let stdoutOverflow = false;
54066
54118
  let stderrOverflow = false;
54067
54119
  let timedOut = false;
54120
+ let aborted = false;
54068
54121
  let settled = false;
54122
+ const killTree = (sig) => {
54123
+ if (child.pid === void 0) return;
54124
+ try {
54125
+ process.kill(-child.pid, sig);
54126
+ } catch {
54127
+ }
54128
+ };
54069
54129
  const timer = setTimeout(() => {
54070
54130
  timedOut = true;
54071
- if (child.pid !== void 0) {
54072
- try {
54073
- process.kill(-child.pid, "SIGTERM");
54074
- } catch {
54075
- }
54076
- setTimeout(() => {
54077
- if (settled) return;
54078
- try {
54079
- process.kill(-child.pid, "SIGKILL");
54080
- } catch {
54081
- }
54082
- }, 5e3).unref();
54083
- }
54131
+ killTree("SIGTERM");
54132
+ setTimeout(() => {
54133
+ if (settled) return;
54134
+ killTree("SIGKILL");
54135
+ }, 5e3).unref();
54084
54136
  }, options.timeout);
54085
54137
  timer.unref();
54138
+ const onAbort = () => {
54139
+ aborted = true;
54140
+ killTree("SIGTERM");
54141
+ setTimeout(() => {
54142
+ if (settled) return;
54143
+ killTree("SIGKILL");
54144
+ }, 5e3).unref();
54145
+ };
54146
+ options.signal?.addEventListener("abort", onAbort, { once: true });
54086
54147
  child.stdout?.on("data", (chunk) => {
54087
54148
  if (stdoutOverflow) return;
54088
54149
  if (stdout.length + chunk.length > options.maxBuffer) {
@@ -54105,6 +54166,14 @@ async function execWithProcessGroup(command, options) {
54105
54166
  if (settled) return;
54106
54167
  settled = true;
54107
54168
  clearTimeout(timer);
54169
+ options.signal?.removeEventListener("abort", onAbort);
54170
+ if (aborted) {
54171
+ reject(Object.assign(
54172
+ new Error(`Command aborted: ${command}`),
54173
+ { code: "ABORT_ERR", aborted: true, stdout, stderr, killed: true }
54174
+ ));
54175
+ return;
54176
+ }
54108
54177
  if (timedOut) {
54109
54178
  reject(Object.assign(
54110
54179
  new Error(`Command timed out after ${options.timeout}ms: ${command}`),
@@ -54117,7 +54186,7 @@ async function execWithProcessGroup(command, options) {
54117
54186
  return;
54118
54187
  }
54119
54188
  if (code === 0) {
54120
- resolve16({ stdout, stderr, bufferOverflow: stdoutOverflow || stderrOverflow });
54189
+ resolve17({ stdout, stderr, bufferOverflow: stdoutOverflow || stderrOverflow });
54121
54190
  return;
54122
54191
  }
54123
54192
  reject(Object.assign(
@@ -54458,7 +54527,8 @@ async function runVerificationCommand(store, rootDir, taskId, command, type, sig
54458
54527
  const { stdout, stderr, bufferOverflow } = await execWithProcessGroup(command, {
54459
54528
  cwd: rootDir,
54460
54529
  timeout: VERIFICATION_COMMAND_TIMEOUT_MS,
54461
- maxBuffer: VERIFICATION_COMMAND_MAX_BUFFER
54530
+ maxBuffer: VERIFICATION_COMMAND_MAX_BUFFER,
54531
+ signal
54462
54532
  });
54463
54533
  throwIfAborted(signal, taskId);
54464
54534
  result.stdout = stdout?.toString?.() || "";
@@ -57575,7 +57645,7 @@ function resolveExecutorModelPair(taskModelProvider, taskModelId, settings) {
57575
57645
  return { provider: void 0, modelId: void 0 };
57576
57646
  }
57577
57647
  function sleep2(ms) {
57578
- return new Promise((resolve16) => setTimeout(resolve16, ms));
57648
+ return new Promise((resolve17) => setTimeout(resolve17, ms));
57579
57649
  }
57580
57650
  var execAsync4, stepExecLog, MAX_STEP_RETRIES, RETRY_DELAYS_MS, NOOP_TASK_STORE, StepSessionExecutor;
57581
57651
  var init_step_session_executor = __esm({
@@ -61378,7 +61448,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
61378
61448
  );
61379
61449
  }
61380
61450
  const delay2 = this.WORKTREE_RETRY_DELAYS[attempt] || 1e3;
61381
- await new Promise((resolve16) => setTimeout(resolve16, delay2));
61451
+ await new Promise((resolve17) => setTimeout(resolve17, delay2));
61382
61452
  }
61383
61453
  }
61384
61454
  throw new Error("Unexpected exit from worktree creation retry loop");
@@ -68266,8 +68336,8 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
68266
68336
  // ../engine/src/self-healing.ts
68267
68337
  import { exec as exec8 } from "node:child_process";
68268
68338
  import { promisify as promisify9 } from "node:util";
68269
- import { existsSync as existsSync27, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
68270
- import { join as join33 } from "node:path";
68339
+ import { existsSync as existsSync27, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
68340
+ import { isAbsolute as isAbsolute11, join as join33, relative as relative7, resolve as resolve14 } from "node:path";
68271
68341
  function shellQuote(value) {
68272
68342
  return `'${value.replace(/'/g, "'\\''")}'`;
68273
68343
  }
@@ -69423,15 +69493,26 @@ var init_self_healing = __esm({
69423
69493
  log7.error(`Worktree prune failed: ${errorMessage}`);
69424
69494
  }
69425
69495
  }
69426
- /** Remove orphaned worktrees not assigned to any active task. */
69496
+ /**
69497
+ * Remove orphaned worktrees not assigned to any active task.
69498
+ *
69499
+ * When `recycleWorktrees` is OFF: removes registered idle worktrees too —
69500
+ * they would otherwise pile up since the pool isn't keeping them.
69501
+ *
69502
+ * When `recycleWorktrees` is ON: leaves registered idle worktrees alone
69503
+ * (the pool wants them for reuse) but still reaps unregistered stale dirs
69504
+ * left behind by killed runs (e.g., `clear-hawk-broken`, `*-bak`). Those
69505
+ * dirs can never be recycled — they aren't git worktrees — so they only
69506
+ * waste disk.
69507
+ */
69427
69508
  async cleanupOrphans() {
69428
69509
  try {
69429
- const orphaned = await scanIdleWorktrees(this.options.rootDir, this.store);
69430
- if (orphaned.length === 0) return 0;
69431
69510
  const settings = await this.store.getSettings();
69432
69511
  if (settings.recycleWorktrees) {
69433
- return 0;
69512
+ return await this.reapUnregisteredOrphans();
69434
69513
  }
69514
+ const orphaned = await scanIdleWorktrees(this.options.rootDir, this.store);
69515
+ if (orphaned.length === 0) return 0;
69435
69516
  let cleaned = 0;
69436
69517
  for (const worktreePath of orphaned) {
69437
69518
  try {
@@ -69455,6 +69536,45 @@ var init_self_healing = __esm({
69455
69536
  return 0;
69456
69537
  }
69457
69538
  }
69539
+ /**
69540
+ * Sweep unregistered stale directories under `<rootDir>/.worktrees/` —
69541
+ * directories that exist on disk but are NOT registered git worktrees.
69542
+ * Safe to run alongside `recycleWorktrees: true` because the pool only
69543
+ * tracks registered idle worktrees, never these orphans.
69544
+ */
69545
+ async reapUnregisteredOrphans() {
69546
+ const worktreesDir = join33(this.options.rootDir, ".worktrees");
69547
+ if (!existsSync27(worktreesDir)) return 0;
69548
+ let dirs;
69549
+ try {
69550
+ dirs = readdirSync5(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join33(worktreesDir, e.name));
69551
+ } catch (err) {
69552
+ log7.warn(`Failed to read .worktrees/ for unregistered orphan reap: ${err instanceof Error ? err.message : String(err)}`);
69553
+ return 0;
69554
+ }
69555
+ if (dirs.length === 0) return 0;
69556
+ const registered = await getRegisteredWorktreePaths(this.options.rootDir);
69557
+ const unregistered = dirs.filter((d) => !registered.has(resolve14(d)));
69558
+ let cleaned = 0;
69559
+ for (const path of unregistered) {
69560
+ const rel = relative7(worktreesDir, path);
69561
+ if (rel === "" || rel.startsWith("..") || isAbsolute11(rel)) {
69562
+ log7.warn(`Refusing to remove path outside .worktrees: ${path}`);
69563
+ continue;
69564
+ }
69565
+ try {
69566
+ rmSync3(path, { recursive: true, force: true });
69567
+ log7.log(`Cleaned unregistered worktree dir: ${path}`);
69568
+ cleaned++;
69569
+ } catch (err) {
69570
+ log7.warn(`Failed to remove unregistered worktree dir ${path}: ${err instanceof Error ? err.message : String(err)}`);
69571
+ }
69572
+ }
69573
+ if (cleaned > 0) {
69574
+ log7.log(`Cleaned ${cleaned} unregistered worktree dir(s) (recycle mode preserves registered idle worktrees)`);
69575
+ }
69576
+ return cleaned;
69577
+ }
69458
69578
  /**
69459
69579
  * Remove orphaned `fusion/*` branches that are not associated with any
69460
69580
  * active (non-archived, non-merger-managed) task.
@@ -70071,13 +70191,13 @@ var init_plugin_runner = __esm({
70071
70191
  * Returns the result on success, throws on timeout.
70072
70192
  */
70073
70193
  withTimeout(promise, ms, timeoutMessage) {
70074
- return new Promise((resolve16, reject) => {
70194
+ return new Promise((resolve17, reject) => {
70075
70195
  const timer = setTimeout(() => {
70076
70196
  reject(new Error(timeoutMessage));
70077
70197
  }, ms);
70078
70198
  promise.then((result) => {
70079
70199
  clearTimeout(timer);
70080
- resolve16(result);
70200
+ resolve17(result);
70081
70201
  }).catch((err) => {
70082
70202
  clearTimeout(timer);
70083
70203
  reject(err);
@@ -70735,7 +70855,7 @@ var init_in_process_runtime = __esm({
70735
70855
  runtimeLog.log(
70736
70856
  `Waiting for ${metrics.inFlightTasks} in-flight tasks to complete...`
70737
70857
  );
70738
- await new Promise((resolve16) => setTimeout(resolve16, 1e3));
70858
+ await new Promise((resolve17) => setTimeout(resolve17, 1e3));
70739
70859
  }
70740
70860
  const finalMetrics = this.getMetrics();
70741
70861
  if (finalMetrics.inFlightTasks > 0) {
@@ -71124,13 +71244,13 @@ var init_ipc_host = __esm({
71124
71244
  }
71125
71245
  const id = generateCorrelationId();
71126
71246
  const message = { type, id, payload };
71127
- return new Promise((resolve16, reject) => {
71247
+ return new Promise((resolve17, reject) => {
71128
71248
  const timeout = setTimeout(() => {
71129
71249
  this.pendingCommands.delete(id);
71130
71250
  reject(new Error(`Command ${type} timed out after ${timeoutMs ?? this.commandTimeoutMs}ms`));
71131
71251
  }, timeoutMs ?? this.commandTimeoutMs);
71132
71252
  this.pendingCommands.set(id, {
71133
- resolve: resolve16,
71253
+ resolve: resolve17,
71134
71254
  reject,
71135
71255
  timeout,
71136
71256
  type
@@ -71939,8 +72059,8 @@ var init_remote_node_client = __esm({
71939
72059
  return error instanceof TypeError;
71940
72060
  }
71941
72061
  async sleep(ms) {
71942
- await new Promise((resolve16) => {
71943
- setTimeout(resolve16, ms);
72062
+ await new Promise((resolve17) => {
72063
+ setTimeout(resolve17, ms);
71944
72064
  });
71945
72065
  }
71946
72066
  };
@@ -72204,14 +72324,14 @@ var init_remote_node_runtime = __esm({
72204
72324
  return error instanceof Error ? error : new Error(String(error));
72205
72325
  }
72206
72326
  async sleep(ms, signal) {
72207
- await new Promise((resolve16) => {
72327
+ await new Promise((resolve17) => {
72208
72328
  const timeout = setTimeout(() => {
72209
72329
  cleanup();
72210
- resolve16();
72330
+ resolve17();
72211
72331
  }, ms);
72212
72332
  const onAbort = () => {
72213
72333
  cleanup();
72214
- resolve16();
72334
+ resolve17();
72215
72335
  };
72216
72336
  const cleanup = () => {
72217
72337
  clearTimeout(timeout);
@@ -72972,10 +73092,10 @@ var init_tunnel_process_manager = __esm({
72972
73092
  lastError: null
72973
73093
  });
72974
73094
  this.emitLog("info", "manager", `Stopping ${currentHandle.provider} tunnel (pid=${currentHandle.child.pid ?? "n/a"})`);
72975
- this.activeStopPromise = new Promise((resolve16) => {
73095
+ this.activeStopPromise = new Promise((resolve17) => {
72976
73096
  const onClose = () => {
72977
73097
  currentHandle.child.removeListener("close", onClose);
72978
- resolve16();
73098
+ resolve17();
72979
73099
  };
72980
73100
  currentHandle.child.once("close", onClose);
72981
73101
  killManagedProcess(currentHandle.child, "SIGTERM");
@@ -73154,6 +73274,12 @@ var init_project_engine = __esm({
73154
73274
  manualMergeResolvers = /* @__PURE__ */ new Map();
73155
73275
  shuttingDown = false;
73156
73276
  static MAX_AUTO_MERGE_RETRIES = 3;
73277
+ /** Cap on outer in-review→in-progress bounces caused by deterministic
73278
+ * verification failures during auto-merge. After this many failed merges
73279
+ * for the same task, we stop bouncing it back, mark it failed, and create
73280
+ * a follow-up triage task so a fresh agent (or human) can investigate
73281
+ * the underlying flake/regression instead of looping forever. */
73282
+ static MAX_VERIFICATION_FAILURE_BOUNCES = 3;
73157
73283
  /** 30-minute cooldown before a retry-exhausted task gets another sweep attempt */
73158
73284
  static AUTO_MERGE_COOLDOWN_MS = 30 * 60 * 1e3;
73159
73285
  // Event handler references for cleanup
@@ -73438,12 +73564,12 @@ var init_project_engine = __esm({
73438
73564
  */
73439
73565
  async onMerge(taskId) {
73440
73566
  if (this.mergeActive.has(taskId)) {
73441
- return new Promise((resolve16, reject) => {
73442
- this.manualMergeResolvers.set(taskId, { resolve: resolve16, reject });
73567
+ return new Promise((resolve17, reject) => {
73568
+ this.manualMergeResolvers.set(taskId, { resolve: resolve17, reject });
73443
73569
  });
73444
73570
  }
73445
- return new Promise((resolve16, reject) => {
73446
- this.manualMergeResolvers.set(taskId, { resolve: resolve16, reject });
73571
+ return new Promise((resolve17, reject) => {
73572
+ this.manualMergeResolvers.set(taskId, { resolve: resolve17, reject });
73447
73573
  this.internalEnqueueMerge(taskId);
73448
73574
  });
73449
73575
  }
@@ -73830,20 +73956,61 @@ var init_project_engine = __esm({
73830
73956
  const isVerificationError = err instanceof Error && err.name === "VerificationError" || errorMsg.includes("Deterministic test verification failed") || errorMsg.includes("Deterministic build verification failed");
73831
73957
  if (taskOnErr && isVerificationError) {
73832
73958
  const failedKind = errorMsg.includes("build verification") ? "build" : "test";
73959
+ const previousBounces = taskOnErr.verificationFailureCount ?? 0;
73960
+ const nextBounces = previousBounces + 1;
73961
+ const cap = _ProjectEngine.MAX_VERIFICATION_FAILURE_BOUNCES;
73962
+ if (nextBounces >= cap) {
73963
+ try {
73964
+ await store.updateTask(taskId, {
73965
+ status: "failed",
73966
+ verificationFailureCount: nextBounces,
73967
+ error: `Deterministic ${failedKind} verification failed ${nextBounces}\xD7 \u2014 auto-merge giving up to avoid infinite retry loop. See follow-up task for investigation.`
73968
+ });
73969
+ const followUpDescription = `Investigate repeated ${failedKind} verification failure on ${taskId} (${taskOnErr.title || "untitled"}). Auto-merge attempted to fix and re-verify ${nextBounces} times without success \u2014 likely a flaky test or unrelated regression rather than a fix this task can produce on its own. Look at the most recent [verification] log entries on ${taskId} for the failing command and output, then either fix the underlying issue or quarantine the flake.`;
73970
+ const followUp = await store.createTask({
73971
+ description: followUpDescription,
73972
+ column: "triage",
73973
+ priority: "high"
73974
+ });
73975
+ await store.addTaskComment(
73976
+ taskId,
73977
+ `Auto-merge giving up after ${nextBounces} verification-failure bounces. Created follow-up ${followUp.id} to investigate.`,
73978
+ "agent"
73979
+ );
73980
+ await store.logEntry(
73981
+ taskId,
73982
+ `Auto-merge gave up after ${nextBounces} verification-failure bounces \u2014 created follow-up ${followUp.id}`,
73983
+ "VerificationError"
73984
+ );
73985
+ runtimeLog.warn(
73986
+ `Auto-merge: ${taskId} hit verification-failure cap (${nextBounces}/${cap}) \u2014 failed task and created follow-up ${followUp.id}`
73987
+ );
73988
+ } catch (followUpErr) {
73989
+ runtimeLog.error(
73990
+ `Auto-merge: failed to fail-and-followup ${taskId} after verification cap: ${followUpErr instanceof Error ? followUpErr.message : String(followUpErr)}`
73991
+ );
73992
+ }
73993
+ continue;
73994
+ }
73833
73995
  try {
73834
73996
  await store.addTaskComment(
73835
73997
  taskId,
73836
- `Deterministic ${failedKind} verification failed during merge. See the prior [verification] log entry for the truncated command output. Please fix the failing ${failedKind} and push the update so the merge can retry.`,
73998
+ `Deterministic ${failedKind} verification failed during merge (attempt ${nextBounces}/${cap}). See the prior [verification] log entry for the truncated command output. Please fix the failing ${failedKind} and push the update so the merge can retry.`,
73837
73999
  "agent"
73838
74000
  );
73839
- await store.updateTask(taskId, { status: null, mergeRetries: 0, error: null });
74001
+ await store.updateTask(taskId, {
74002
+ status: null,
74003
+ mergeRetries: 0,
74004
+ error: null,
74005
+ verificationFailureCount: nextBounces
74006
+ });
73840
74007
  await store.moveTask(taskId, "in-progress");
73841
74008
  await store.logEntry(
73842
74009
  taskId,
73843
- `Deterministic ${failedKind} verification failed \u2014 moved back to in-progress for remediation`
74010
+ `Deterministic ${failedKind} verification failed (${nextBounces}/${cap}) \u2014 moved back to in-progress for remediation`
73844
74011
  );
73845
74012
  runtimeLog.log(
73846
- `Auto-merge: ${taskId} deterministic ${failedKind} verification failed \u2014 moved to in-progress`
74013
+ `Auto-merge: ${taskId} deterministic ${failedKind} verification failed (${nextBounces}/${cap}) \u2014 moved to in-progress`
73847
74014
  );
73848
74015
  } catch {
73849
74016
  runtimeLog.error(
@@ -74057,6 +74224,11 @@ var init_project_engine = __esm({
74057
74224
  wireSettingsListeners(store) {
74058
74225
  const onGlobalPause = ({ settings, previous }) => {
74059
74226
  if (settings.globalPause && !previous.globalPause) {
74227
+ if (this.mergeAbortController) {
74228
+ runtimeLog.log("Global pause \u2014 aborting in-flight merge verification");
74229
+ this.mergeAbortController.abort();
74230
+ this.mergeAbortController = null;
74231
+ }
74060
74232
  if (this.activeMergeSession) {
74061
74233
  runtimeLog.log("Global pause \u2014 terminating active merge session");
74062
74234
  this.activeMergeSession.dispose();
@@ -75099,7 +75271,6 @@ __export(src_exports2, {
75099
75271
  taskLogParams: () => taskLogParams,
75100
75272
  withRateLimitRetry: () => withRateLimitRetry
75101
75273
  });
75102
- var fusionCoreExports;
75103
75274
  var init_src2 = __esm({
75104
75275
  "../engine/src/index.ts"() {
75105
75276
  "use strict";
@@ -75114,7 +75285,6 @@ var init_src2 = __esm({
75114
75285
  init_merger();
75115
75286
  init_reviewer();
75116
75287
  init_pi();
75117
- init_src();
75118
75288
  init_pi();
75119
75289
  init_skill_resolver();
75120
75290
  init_agent_reflection();
@@ -75145,12 +75315,12 @@ var init_src2 = __esm({
75145
75315
  init_remote_node_client();
75146
75316
  init_remote_node_runtime();
75147
75317
  init_step_session_executor();
75148
- fusionCoreExports = src_exports;
75149
- if ("setCreateFnAgent" in fusionCoreExports && typeof fusionCoreExports["setCreateFnAgent"] === "function") {
75150
- fusionCoreExports["setCreateFnAgent"](
75151
- createFnAgent2
75152
- );
75153
- }
75318
+ void Promise.resolve().then(() => (init_src(), src_exports)).then((core) => {
75319
+ if ("setCreateFnAgent" in core && typeof core.setCreateFnAgent === "function") {
75320
+ core.setCreateFnAgent(createFnAgent2);
75321
+ }
75322
+ }).catch(() => {
75323
+ });
75154
75324
  }
75155
75325
  });
75156
75326
 
@@ -75302,17 +75472,6 @@ var init_ai_session_diagnostics = __esm({
75302
75472
  // ../dashboard/src/planning.ts
75303
75473
  import { randomUUID as randomUUID10 } from "node:crypto";
75304
75474
  import { EventEmitter as EventEmitter23 } from "node:events";
75305
- function buildDefaultPlanningNtfyHelpers() {
75306
- const isNtfyEventEnabled2 = "isNtfyEventEnabled" in src_exports2 ? isNtfyEventEnabled : (events, event) => Array.isArray(events) ? events.includes(event) : false;
75307
- const buildNtfyClickUrl2 = "buildNtfyClickUrl" in src_exports2 ? buildNtfyClickUrl : () => void 0;
75308
- const sendNtfyNotification2 = "sendNtfyNotification" in src_exports2 ? sendNtfyNotification : async () => {
75309
- };
75310
- return {
75311
- isNtfyEventEnabled: isNtfyEventEnabled2,
75312
- buildNtfyClickUrl: buildNtfyClickUrl2,
75313
- sendNtfyNotification: sendNtfyNotification2
75314
- };
75315
- }
75316
75475
  function ensureEngineReady() {
75317
75476
  return Promise.resolve();
75318
75477
  }
@@ -76060,7 +76219,7 @@ function getSession(sessionId) {
76060
76219
  return void 0;
76061
76220
  }
76062
76221
  }
76063
- var createFnAgent4, engineExports, engineIsNtfyEventEnabled, engineBuildNtfyClickUrl, engineSendNtfyNotification, planningNtfyHelpers, diagnostics, PLANNING_SYSTEM_PROMPT, SESSION_TTL_MS, CLEANUP_INTERVAL_MS2, MAX_SESSIONS_PER_IP_PER_HOUR, RATE_LIMIT_WINDOW_MS2, sessions, rateLimits2, _aiSessionStore, cleanupInterval2, PlanningStreamManager, planningStreamManager, MAX_PARSE_RETRIES, RateLimitError2, SessionNotFoundError, InvalidSessionStateError;
76222
+ var createFnAgent4, planningNtfyHelpers, diagnostics, PLANNING_SYSTEM_PROMPT, SESSION_TTL_MS, CLEANUP_INTERVAL_MS2, MAX_SESSIONS_PER_IP_PER_HOUR, RATE_LIMIT_WINDOW_MS2, sessions, rateLimits2, _aiSessionStore, cleanupInterval2, PlanningStreamManager, planningStreamManager, MAX_PARSE_RETRIES, RateLimitError2, SessionNotFoundError, InvalidSessionStateError;
76064
76223
  var init_planning = __esm({
76065
76224
  "../dashboard/src/planning.ts"() {
76066
76225
  "use strict";
@@ -76069,12 +76228,6 @@ var init_planning = __esm({
76069
76228
  init_ai_session_diagnostics();
76070
76229
  init_src2();
76071
76230
  createFnAgent4 = createFnAgent2;
76072
- engineExports = src_exports2;
76073
- engineIsNtfyEventEnabled = "isNtfyEventEnabled" in engineExports && typeof engineExports["isNtfyEventEnabled"] === "function" ? engineExports["isNtfyEventEnabled"] : (events, event) => Array.isArray(events) && events.includes(event);
76074
- engineBuildNtfyClickUrl = "buildNtfyClickUrl" in engineExports && typeof engineExports["buildNtfyClickUrl"] === "function" ? engineExports["buildNtfyClickUrl"] : (_options) => void 0;
76075
- engineSendNtfyNotification = "sendNtfyNotification" in engineExports && typeof engineExports["sendNtfyNotification"] === "function" ? engineExports["sendNtfyNotification"] : async (_input) => {
76076
- };
76077
- planningNtfyHelpers = buildDefaultPlanningNtfyHelpers();
76078
76231
  diagnostics = createSessionDiagnostics("planning");
76079
76232
  PLANNING_SYSTEM_PROMPT = `You are a planning assistant for the fn task board system.
76080
76233
 
@@ -76728,7 +76881,7 @@ var init_register_messaging_scripts = __esm({
76728
76881
 
76729
76882
  // ../dashboard/src/github.ts
76730
76883
  function delay(ms) {
76731
- return new Promise((resolve16) => setTimeout(resolve16, ms));
76884
+ return new Promise((resolve17) => setTimeout(resolve17, ms));
76732
76885
  }
76733
76886
  function normalizeCheckState(state) {
76734
76887
  switch ((state ?? "").toLowerCase()) {
@@ -77444,6 +77597,43 @@ var init_github = __esm({
77444
77597
  }
77445
77598
  return response.json();
77446
77599
  }
77600
+ async commentOnIssue(owner, repo, issueNumber, body) {
77601
+ if (this.hasGhAuth()) {
77602
+ try {
77603
+ runGh([
77604
+ "issue",
77605
+ "comment",
77606
+ String(issueNumber),
77607
+ "--repo",
77608
+ `${owner}/${repo}`,
77609
+ "--body",
77610
+ body
77611
+ ]);
77612
+ return;
77613
+ } catch (err) {
77614
+ if (!this.token) {
77615
+ throw new Error(getGhErrorMessage(err));
77616
+ }
77617
+ }
77618
+ }
77619
+ if (!this.token) {
77620
+ throw new Error("GitHub CLI (gh) is not available or not authenticated, and no GITHUB_TOKEN provided.");
77621
+ }
77622
+ const url = `${this.baseUrl}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}/comments`;
77623
+ const result = await this.fetchThrottled(
77624
+ url,
77625
+ {
77626
+ method: "POST",
77627
+ headers: {
77628
+ "Content-Type": "application/json"
77629
+ },
77630
+ body: JSON.stringify({ body })
77631
+ }
77632
+ );
77633
+ if (!result.success) {
77634
+ throw new Error(result.error ?? "Failed to comment on GitHub issue");
77635
+ }
77636
+ }
77447
77637
  /**
77448
77638
  * Fetch current issue status using gh CLI if available, otherwise REST API.
77449
77639
  * Returns null if the issue is not found or is a pull request.
@@ -78205,6 +78395,14 @@ var init_github = __esm({
78205
78395
  }
78206
78396
  });
78207
78397
 
78398
+ // ../dashboard/src/github-issue-comment.ts
78399
+ var init_github_issue_comment = __esm({
78400
+ "../dashboard/src/github-issue-comment.ts"() {
78401
+ "use strict";
78402
+ init_github();
78403
+ }
78404
+ });
78405
+
78208
78406
  // ../dashboard/src/github-poll.ts
78209
78407
  import { EventEmitter as EventEmitter27 } from "node:events";
78210
78408
  function toAlias(type, number) {
@@ -78514,6 +78712,7 @@ var init_register_git_github = __esm({
78514
78712
  init_src();
78515
78713
  init_api_error();
78516
78714
  init_github();
78715
+ init_github_issue_comment();
78517
78716
  init_github_poll();
78518
78717
  init_github_webhooks();
78519
78718
  init_resolve_diff_base();
@@ -82963,15 +83162,13 @@ var init_terminal_websocket_diagnostics = __esm({
82963
83162
 
82964
83163
  // ../dashboard/src/chat.ts
82965
83164
  import { EventEmitter as EventEmitter29 } from "node:events";
82966
- var engineExports2, defaultBuildAgentChatPromptFn, defaultDiagnostics, _diagnostics, diagnostics7, RATE_LIMIT_WINDOW_MS6, MAX_REFERENCED_FILE_SIZE, ChatStreamManager, chatStreamManager;
83165
+ var defaultDiagnostics, _diagnostics, diagnostics7, RATE_LIMIT_WINDOW_MS6, MAX_REFERENCED_FILE_SIZE, ChatStreamManager, chatStreamManager;
82967
83166
  var init_chat = __esm({
82968
83167
  "../dashboard/src/chat.ts"() {
82969
83168
  "use strict";
82970
83169
  init_src();
82971
83170
  init_sse_buffer();
82972
83171
  init_src2();
82973
- engineExports2 = src_exports2;
82974
- defaultBuildAgentChatPromptFn = "buildAgentChatPrompt" in engineExports2 && typeof engineExports2["buildAgentChatPrompt"] === "function" ? engineExports2["buildAgentChatPrompt"] : void 0;
82975
83172
  defaultDiagnostics = {
82976
83173
  log(message, ...args) {
82977
83174
  console.log(`[chat] ${message}`, ...args);
@@ -83208,6 +83405,7 @@ var init_src3 = __esm({
83208
83405
  init_github();
83209
83406
  init_rate_limit();
83210
83407
  init_github_poll();
83408
+ init_github_issue_comment();
83211
83409
  init_api_error();
83212
83410
  init_badge_pubsub();
83213
83411
  init_plugins();
@@ -83215,7 +83413,7 @@ var init_src3 = __esm({
83215
83413
  });
83216
83414
 
83217
83415
  // src/project-context.ts
83218
- import { resolve as resolve14, dirname as dirname10 } from "node:path";
83416
+ import { resolve as resolve15, dirname as dirname10 } from "node:path";
83219
83417
  import { existsSync as existsSync29 } from "node:fs";
83220
83418
  async function resolveProject(projectNameFlag, cwd = process.cwd(), globalDir) {
83221
83419
  const central = new CentralCore(globalDir);
@@ -83292,9 +83490,9 @@ async function clearDefaultProject(globalDir) {
83292
83490
  await globalStore.updateSettings(rest);
83293
83491
  }
83294
83492
  async function detectProjectFromCwd(cwd, central) {
83295
- let currentDir = resolve14(cwd);
83493
+ let currentDir = resolve15(cwd);
83296
83494
  while (true) {
83297
- const kbPath = resolve14(currentDir, ".fusion", "fusion.db");
83495
+ const kbPath = resolve15(currentDir, ".fusion", "fusion.db");
83298
83496
  if (existsSync29(kbPath)) {
83299
83497
  const project = await central.getProjectByPath(currentDir);
83300
83498
  if (project) {
@@ -83468,9 +83666,9 @@ async function runTaskCreate(descriptionArg, attachFiles, depends, projectName)
83468
83666
  console.log(` Path: .fusion/tasks/${task.id}/`);
83469
83667
  if (attachFiles && attachFiles.length > 0) {
83470
83668
  const { readFile: readFile19 } = await import("node:fs/promises");
83471
- const { basename: basename9, extname: extname3, resolve: resolve16 } = await import("node:path");
83669
+ const { basename: basename9, extname: extname3, resolve: resolve17 } = await import("node:path");
83472
83670
  for (const filePath of attachFiles) {
83473
- const resolvedPath = resolve16(filePath);
83671
+ const resolvedPath = resolve17(filePath);
83474
83672
  const filename = basename9(resolvedPath);
83475
83673
  const ext = extname3(filename).toLowerCase();
83476
83674
  const mimeType = MIME_TYPES[ext];
@@ -83717,8 +83915,8 @@ async function runTaskMerge(id, projectName) {
83717
83915
  async function runTaskAttach(id, filePath, projectName) {
83718
83916
  const { readFile: readFile19 } = await import("node:fs/promises");
83719
83917
  const { basename: basename9, extname: extname3 } = await import("node:path");
83720
- const { resolve: resolve16 } = await import("node:path");
83721
- const resolvedPath = resolve16(filePath);
83918
+ const { resolve: resolve17 } = await import("node:path");
83919
+ const resolvedPath = resolve17(filePath);
83722
83920
  const filename = basename9(resolvedPath);
83723
83921
  const ext = extname3(filename).toLowerCase();
83724
83922
  const mimeType = MIME_TYPES[ext];
@@ -84235,12 +84433,12 @@ async function promptText(question) {
84235
84433
  console.log(" (Enter your response. Type DONE on its own line when finished):\n");
84236
84434
  const rl = createInterface({ input: process.stdin, output: process.stdout });
84237
84435
  const lines = [];
84238
- return new Promise((resolve16) => {
84436
+ return new Promise((resolve17) => {
84239
84437
  const askLine = () => {
84240
84438
  rl.question(" ").then((line) => {
84241
84439
  if (line.trim() === "DONE") {
84242
84440
  rl.close();
84243
- resolve16(lines.join("\n"));
84441
+ resolve17(lines.join("\n"));
84244
84442
  } else {
84245
84443
  lines.push(line);
84246
84444
  askLine();
@@ -84642,9 +84840,9 @@ async function runSkillsInstall(args, options) {
84642
84840
  cwd: process.cwd(),
84643
84841
  stdio: "inherit"
84644
84842
  });
84645
- const exitCode = await new Promise((resolve16, reject) => {
84843
+ const exitCode = await new Promise((resolve17, reject) => {
84646
84844
  child.on("exit", (code) => {
84647
- resolve16(code ?? 1);
84845
+ resolve17(code ?? 1);
84648
84846
  });
84649
84847
  child.on("error", (err) => {
84650
84848
  reject(err);
@@ -84671,7 +84869,7 @@ init_src();
84671
84869
  init_gh_cli();
84672
84870
  import { Type as Type7 } from "typebox";
84673
84871
  import { StringEnum } from "@mariozechner/pi-ai";
84674
- import { resolve as resolve15, basename as basename8, extname as extname2, join as join37 } from "node:path";
84872
+ import { resolve as resolve16, basename as basename8, extname as extname2, join as join37 } from "node:path";
84675
84873
  import { readFile as readFile18 } from "node:fs/promises";
84676
84874
  import { existsSync as existsSync31 } from "node:fs";
84677
84875
  import { spawn as spawn6 } from "node:child_process";
@@ -84691,14 +84889,14 @@ var MIME_TYPES2 = {
84691
84889
  ".xml": "application/xml"
84692
84890
  };
84693
84891
  function resolveProjectRoot(cwd) {
84694
- let current = resolve15(cwd);
84892
+ let current = resolve16(cwd);
84695
84893
  while (true) {
84696
84894
  if (existsSync31(join37(current, ".fusion"))) {
84697
84895
  return current;
84698
84896
  }
84699
- const parent = resolve15(current, "..");
84897
+ const parent = resolve16(current, "..");
84700
84898
  if (parent === current) {
84701
- return resolve15(cwd);
84899
+ return resolve16(cwd);
84702
84900
  }
84703
84901
  current = parent;
84704
84902
  }
@@ -84991,7 +85189,7 @@ Column: triage
84991
85189
  path: Type7.String({ description: "Path to the file to attach" })
84992
85190
  }),
84993
85191
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
84994
- const filePath = resolve15(ctx.cwd, params.path.replace(/^@/, ""));
85192
+ const filePath = resolve16(ctx.cwd, params.path.replace(/^@/, ""));
84995
85193
  const filename = basename8(filePath);
84996
85194
  const ext = extname2(filename).toLowerCase();
84997
85195
  const mimeType = MIME_TYPES2[ext];
@@ -86085,12 +86283,12 @@ Status: ${updated.status}`
86085
86283
  child.stderr?.on("data", (data) => {
86086
86284
  stderr += data.toString();
86087
86285
  });
86088
- const exitCode = await new Promise((resolve16) => {
86286
+ const exitCode = await new Promise((resolve17) => {
86089
86287
  child.on("exit", (code) => {
86090
- resolve16(code ?? 1);
86288
+ resolve17(code ?? 1);
86091
86289
  });
86092
86290
  child.on("error", () => {
86093
- resolve16(1);
86291
+ resolve17(1);
86094
86292
  });
86095
86293
  });
86096
86294
  try {