@runfusion/fusion 0.8.0 → 0.8.2

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 (33) hide show
  1. package/dist/bin.js +962 -566
  2. package/dist/client/assets/{AgentDetailView-C3Xcrxnp.js → AgentDetailView-B6wfIQ9j.js} +1 -1
  3. package/dist/client/assets/{AgentsView-EjE4y4rM.js → AgentsView-Fcym0XCw.js} +3 -3
  4. package/dist/client/assets/{ChatView-DQLvKCYj.js → ChatView-DEckS3f3.js} +1 -1
  5. package/dist/client/assets/{DevServerView-CX7paFRQ.js → DevServerView-CmMS4D6V.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-_cBPx6Nx.js → DirectoryPicker-CQtE-YyA.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-Wz33aYqp.js → DocumentsView-C4HDkN_0.js} +1 -1
  8. package/dist/client/assets/{InsightsView-C7YPnS92.js → InsightsView-oNQ7h5e8.js} +1 -1
  9. package/dist/client/assets/{MemoryView-DKQtFzFQ.js → MemoryView-DPYGW09y.js} +1 -1
  10. package/dist/client/assets/{NodesView-CI4rUQC4.js → NodesView-DsM-c8RF.js} +1 -1
  11. package/dist/client/assets/{PiExtensionsManager-BFmdKgHZ.js → PiExtensionsManager-DHgMjjRE.js} +1 -1
  12. package/dist/client/assets/{PluginManager-BGQU1IIw.js → PluginManager-N-7Sw_tE.js} +1 -1
  13. package/dist/client/assets/{RoadmapsView-Cts3hoIS.js → RoadmapsView-BgX79uYM.js} +1 -1
  14. package/dist/client/assets/{SettingsModal-DXvBGZHf.js → SettingsModal-BIKEMPwb.js} +1 -1
  15. package/dist/client/assets/{SettingsModal-DvRd0ZOE.js → SettingsModal-C4EwtN6K.js} +4 -4
  16. package/dist/client/assets/SetupWizardModal-D2m0i9Io.js +1 -0
  17. package/dist/client/assets/{SkillsView-BXvrHzEZ.js → SkillsView-Eb1Mngnt.js} +1 -1
  18. package/dist/client/assets/{TodoView-NZHkv9YQ.js → TodoView-BN8FQYyp.js} +1 -1
  19. package/dist/client/assets/{folder-open-Kh0ScTc5.js → folder-open-BqZBHfoZ.js} +1 -1
  20. package/dist/client/assets/{index-CWz44REw.css → index-D2fXOwWF.css} +1 -1
  21. package/dist/client/assets/{index-D1gavMG-.js → index-DlHPhpDu.js} +3 -3
  22. package/dist/client/assets/{list-checks-CvoT0bwU.js → list-checks-D2EURsP0.js} +1 -1
  23. package/dist/client/assets/{star-BdfwSLBU.js → star-CNQlAD8p.js} +1 -1
  24. package/dist/client/assets/{upload-Bx8Yk_7Q.js → upload-uoxlYkig.js} +1 -1
  25. package/dist/client/assets/{users-DgVaFEsz.js → users-BLvidusm.js} +1 -1
  26. package/dist/client/index.html +2 -2
  27. package/dist/client/version.json +1 -1
  28. package/dist/extension.js +500 -196
  29. package/dist/pi-claude-cli/package.json +1 -1
  30. package/dist/pi-claude-cli/src/process-manager.ts +1 -1
  31. package/package.json +1 -1
  32. package/dist/client/assets/SetupWizardModal-Y2ewEE8Y.js +0 -1
  33. package/dist/pi-claude-cli/src/types/cross-spawn.d.ts +0 -7
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 = 50;
2435
+ SCHEMA_VERSION = 51;
2436
2436
  SCHEMA_SQL = `
2437
2437
  -- Tasks table with JSON columns for nested data
2438
2438
  CREATE TABLE IF NOT EXISTS tasks (
@@ -3965,6 +3965,13 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
3965
3965
  this.addColumnIfMissing("tasks", "effectiveNodeSource", "TEXT");
3966
3966
  });
3967
3967
  }
3968
+ if (version < 51) {
3969
+ this.applyMigration(51, () => {
3970
+ if (this.hasTable("chat_messages")) {
3971
+ this.addColumnIfMissing("chat_messages", "attachments", "TEXT");
3972
+ }
3973
+ });
3974
+ }
3968
3975
  }
3969
3976
  /**
3970
3977
  * Run a single migration step inside a transaction and bump the version.
@@ -6477,9 +6484,9 @@ var init_global_settings = __esm({
6477
6484
  * Serialize operations via promise chain to prevent lost-update races.
6478
6485
  */
6479
6486
  withLock(fn) {
6480
- let resolve17;
6487
+ let resolve19;
6481
6488
  const next = new Promise((r) => {
6482
- resolve17 = r;
6489
+ resolve19 = r;
6483
6490
  });
6484
6491
  const prev = this.lock;
6485
6492
  this.lock = next;
@@ -6487,7 +6494,7 @@ var init_global_settings = __esm({
6487
6494
  try {
6488
6495
  return await fn();
6489
6496
  } finally {
6490
- resolve17();
6497
+ resolve19();
6491
6498
  }
6492
6499
  });
6493
6500
  }
@@ -11797,8 +11804,8 @@ import { join as join8, dirname as dirname2 } from "node:path";
11797
11804
  import { fileURLToPath } from "node:url";
11798
11805
  function getAppVersion() {
11799
11806
  if (cachedVersion !== null) return cachedVersion;
11800
- const __dirname3 = dirname2(fileURLToPath(import.meta.url));
11801
- let currentDir = __dirname3;
11807
+ const __dirname2 = dirname2(fileURLToPath(import.meta.url));
11808
+ let currentDir = __dirname2;
11802
11809
  for (let i = 0; i < 10; i++) {
11803
11810
  try {
11804
11811
  const pkgPath = join8(currentDir, "package.json");
@@ -27783,9 +27790,9 @@ var init_automation_store = __esm({
27783
27790
  */
27784
27791
  withScheduleLock(id, fn) {
27785
27792
  const prev = this.scheduleLocks.get(id) ?? Promise.resolve();
27786
- let resolve17;
27793
+ let resolve19;
27787
27794
  const next = new Promise((r) => {
27788
- resolve17 = r;
27795
+ resolve19 = r;
27789
27796
  });
27790
27797
  this.scheduleLocks.set(id, next);
27791
27798
  return prev.then(async () => {
@@ -27795,7 +27802,7 @@ var init_automation_store = __esm({
27795
27802
  if (this.scheduleLocks.get(id) === next) {
27796
27803
  this.scheduleLocks.delete(id);
27797
27804
  }
27798
- resolve17();
27805
+ resolve19();
27799
27806
  }
27800
27807
  });
27801
27808
  }
@@ -29583,7 +29590,7 @@ var init_project_memory = __esm({
29583
29590
  // ../core/src/run-command.ts
29584
29591
  import { spawn } from "node:child_process";
29585
29592
  function runCommandAsync(command, options = {}) {
29586
- return new Promise((resolve17) => {
29593
+ return new Promise((resolve19) => {
29587
29594
  const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
29588
29595
  let stdout = "";
29589
29596
  let stderr = "";
@@ -29642,7 +29649,7 @@ function runCommandAsync(command, options = {}) {
29642
29649
  clearTimeout(forceKillTimer);
29643
29650
  forceKillTimer = null;
29644
29651
  }
29645
- resolve17({
29652
+ resolve19({
29646
29653
  stdout,
29647
29654
  stderr,
29648
29655
  exitCode: null,
@@ -29660,7 +29667,7 @@ function runCommandAsync(command, options = {}) {
29660
29667
  }
29661
29668
  signalProcessGroup("SIGTERM");
29662
29669
  scheduleForceKill(NORMAL_CLEANUP_FORCE_KILL_DELAY_MS);
29663
- resolve17({
29670
+ resolve19({
29664
29671
  stdout,
29665
29672
  stderr,
29666
29673
  exitCode: code,
@@ -30810,9 +30817,9 @@ ${outcome}`;
30810
30817
  * lost-update races on the nextId counter.
30811
30818
  */
30812
30819
  withConfigLock(fn) {
30813
- let resolve17;
30820
+ let resolve19;
30814
30821
  const next = new Promise((r) => {
30815
- resolve17 = r;
30822
+ resolve19 = r;
30816
30823
  });
30817
30824
  const prev = this.configLock;
30818
30825
  this.configLock = next;
@@ -30820,7 +30827,7 @@ ${outcome}`;
30820
30827
  try {
30821
30828
  return await fn();
30822
30829
  } finally {
30823
- resolve17();
30830
+ resolve19();
30824
30831
  }
30825
30832
  });
30826
30833
  }
@@ -30830,9 +30837,9 @@ ${outcome}`;
30830
30837
  */
30831
30838
  withTaskLock(id, fn) {
30832
30839
  const prev = this.taskLocks.get(id) ?? Promise.resolve();
30833
- let resolve17;
30840
+ let resolve19;
30834
30841
  const next = new Promise((r) => {
30835
- resolve17 = r;
30842
+ resolve19 = r;
30836
30843
  });
30837
30844
  this.taskLocks.set(id, next);
30838
30845
  return prev.then(async () => {
@@ -30842,7 +30849,7 @@ ${outcome}`;
30842
30849
  if (this.taskLocks.get(id) === next) {
30843
30850
  this.taskLocks.delete(id);
30844
30851
  }
30845
- resolve17();
30852
+ resolve19();
30846
30853
  }
30847
30854
  });
30848
30855
  }
@@ -32997,7 +33004,7 @@ ${task.description}
32997
33004
  }
32998
33005
  }
32999
33006
  }
33000
- await new Promise((resolve17) => setImmediate(resolve17));
33007
+ await new Promise((resolve19) => setImmediate(resolve19));
33001
33008
  const selectClause = this.getTaskSelectClause(true);
33002
33009
  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();
33003
33010
  this.lastPollTime = (/* @__PURE__ */ new Date()).toISOString();
@@ -33017,7 +33024,7 @@ ${task.description}
33017
33024
  this.emit("task:updated", task);
33018
33025
  }
33019
33026
  if (i > 0 && i % 50 === 0) {
33020
- await new Promise((resolve17) => setImmediate(resolve17));
33027
+ await new Promise((resolve19) => setImmediate(resolve19));
33021
33028
  }
33022
33029
  }
33023
33030
  const elapsed = Date.now() - startTime;
@@ -34843,7 +34850,7 @@ function runGh(args, cwd) {
34843
34850
  }
34844
34851
  function runGhAsync(args, cwdOrOptions) {
34845
34852
  const { cwd, signal: externalSignal, timeoutMs = DEFAULT_GH_TIMEOUT_MS } = normalizeRunGhOptions(cwdOrOptions);
34846
- return new Promise((resolve17, reject) => {
34853
+ return new Promise((resolve19, reject) => {
34847
34854
  if (externalSignal?.aborted) {
34848
34855
  reject(makeGhError(`gh command aborted: ${describeAbortReason(externalSignal.reason)}`, "ABORT_ERR"));
34849
34856
  return;
@@ -34894,7 +34901,7 @@ function runGhAsync(args, cwdOrOptions) {
34894
34901
  ghError.stderr = stderr ?? "";
34895
34902
  reject(ghError);
34896
34903
  } else {
34897
- resolve17(stdout ?? "");
34904
+ resolve19(stdout ?? "");
34898
34905
  }
34899
34906
  }
34900
34907
  );
@@ -35182,9 +35189,9 @@ var init_routine_store = __esm({
35182
35189
  */
35183
35190
  withRoutineLock(id, fn) {
35184
35191
  const prev = this.routineLocks.get(id) ?? Promise.resolve();
35185
- let resolve17;
35192
+ let resolve19;
35186
35193
  const next = new Promise((r) => {
35187
- resolve17 = r;
35194
+ resolve19 = r;
35188
35195
  });
35189
35196
  this.routineLocks.set(id, next);
35190
35197
  return prev.then(async () => {
@@ -35194,7 +35201,7 @@ var init_routine_store = __esm({
35194
35201
  if (this.routineLocks.get(id) === next) {
35195
35202
  this.routineLocks.delete(id);
35196
35203
  }
35197
- resolve17();
35204
+ resolve19();
35198
35205
  }
35199
35206
  });
35200
35207
  }
@@ -35793,13 +35800,13 @@ var init_plugin_loader = __esm({
35793
35800
  * Execute a promise with a timeout.
35794
35801
  */
35795
35802
  withTimeout(promise, ms, timeoutMessage) {
35796
- return new Promise((resolve17, reject) => {
35803
+ return new Promise((resolve19, reject) => {
35797
35804
  const timer = setTimeout(() => {
35798
35805
  reject(new Error(timeoutMessage));
35799
35806
  }, ms);
35800
35807
  promise.then((result) => {
35801
35808
  clearTimeout(timer);
35802
- resolve17(result);
35809
+ resolve19(result);
35803
35810
  }).catch((err) => {
35804
35811
  clearTimeout(timer);
35805
35812
  reject(err);
@@ -39006,7 +39013,7 @@ var require_get_stream = __commonJS({
39006
39013
  };
39007
39014
  const { maxBuffer } = options;
39008
39015
  let stream;
39009
- await new Promise((resolve17, reject) => {
39016
+ await new Promise((resolve19, reject) => {
39010
39017
  const rejectPromise = (error) => {
39011
39018
  if (error && stream.getBufferedLength() <= BufferConstants.MAX_LENGTH) {
39012
39019
  error.bufferedData = stream.getBufferedValue();
@@ -39018,7 +39025,7 @@ var require_get_stream = __commonJS({
39018
39025
  rejectPromise(error);
39019
39026
  return;
39020
39027
  }
39021
- resolve17();
39028
+ resolve19();
39022
39029
  });
39023
39030
  stream.on("data", () => {
39024
39031
  if (stream.getBufferedLength() > maxBuffer) {
@@ -40312,7 +40319,7 @@ var require_extract_zip = __commonJS({
40312
40319
  debug("opening", this.zipPath, "with opts", this.opts);
40313
40320
  this.zipfile = await openZip(this.zipPath, { lazyEntries: true });
40314
40321
  this.canceled = false;
40315
- return new Promise((resolve17, reject) => {
40322
+ return new Promise((resolve19, reject) => {
40316
40323
  this.zipfile.on("error", (err) => {
40317
40324
  this.canceled = true;
40318
40325
  reject(err);
@@ -40321,7 +40328,7 @@ var require_extract_zip = __commonJS({
40321
40328
  this.zipfile.on("close", () => {
40322
40329
  if (!this.canceled) {
40323
40330
  debug("zip extraction complete");
40324
- resolve17();
40331
+ resolve19();
40325
40332
  }
40326
40333
  });
40327
40334
  this.zipfile.on("entry", async (entry) => {
@@ -48501,6 +48508,7 @@ var init_chat_store = __esm({
48501
48508
  content: row.content,
48502
48509
  thinkingOutput: row.thinkingOutput ?? null,
48503
48510
  metadata: fromJson(row.metadata) ?? null,
48511
+ attachments: fromJson(row.attachments) ?? void 0,
48504
48512
  createdAt: row.createdAt
48505
48513
  };
48506
48514
  }
@@ -48719,11 +48727,12 @@ var init_chat_store = __esm({
48719
48727
  content: input.content,
48720
48728
  thinkingOutput: input.thinkingOutput ?? null,
48721
48729
  metadata: input.metadata ?? null,
48730
+ attachments: input.attachments,
48722
48731
  createdAt: now
48723
48732
  };
48724
48733
  this.db.prepare(`
48725
- INSERT INTO chat_messages (id, sessionId, role, content, thinkingOutput, metadata, createdAt)
48726
- VALUES (?, ?, ?, ?, ?, ?, ?)
48734
+ INSERT INTO chat_messages (id, sessionId, role, content, thinkingOutput, metadata, attachments, createdAt)
48735
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
48727
48736
  `).run(
48728
48737
  message.id,
48729
48738
  message.sessionId,
@@ -48731,6 +48740,7 @@ var init_chat_store = __esm({
48731
48740
  message.content,
48732
48741
  message.thinkingOutput,
48733
48742
  toJsonNullable(message.metadata),
48743
+ toJsonNullable(message.attachments),
48734
48744
  message.createdAt
48735
48745
  );
48736
48746
  this.db.prepare("UPDATE chat_sessions SET updatedAt = ? WHERE id = ?").run(now, sessionId);
@@ -48738,6 +48748,28 @@ var init_chat_store = __esm({
48738
48748
  this.emit("chat:message:added", message);
48739
48749
  return message;
48740
48750
  }
48751
+ /**
48752
+ * Append a file attachment metadata record to an existing message.
48753
+ */
48754
+ addMessageAttachment(sessionId, messageId, attachment) {
48755
+ const message = this.getMessage(messageId);
48756
+ if (!message || message.sessionId !== sessionId) {
48757
+ throw new Error(`Message ${messageId} not found in session ${sessionId}`);
48758
+ }
48759
+ const updatedAttachments = [...message.attachments ?? [], attachment];
48760
+ this.db.prepare(`
48761
+ UPDATE chat_messages
48762
+ SET attachments = ?
48763
+ WHERE id = ?
48764
+ `).run(toJsonNullable(updatedAttachments), messageId);
48765
+ const updated = this.getMessage(messageId);
48766
+ if (!updated) {
48767
+ throw new Error(`Failed to update message ${messageId}`);
48768
+ }
48769
+ this.db.bumpLastModified();
48770
+ this.emit("chat:message:updated", updated);
48771
+ return updated;
48772
+ }
48741
48773
  /**
48742
48774
  * Get messages for a chat session with optional filtering.
48743
48775
  *
@@ -50345,12 +50377,12 @@ var init_concurrency = __esm({
50345
50377
  this._active++;
50346
50378
  return Promise.resolve();
50347
50379
  }
50348
- return new Promise((resolve17) => {
50380
+ return new Promise((resolve19) => {
50349
50381
  this._waiters.push({
50350
50382
  priority,
50351
50383
  resolve: () => {
50352
50384
  this._active++;
50353
- resolve17();
50385
+ resolve19();
50354
50386
  }
50355
50387
  });
50356
50388
  });
@@ -52771,20 +52803,20 @@ async function withRateLimitRetry(fn, options = {}) {
52771
52803
  throw lastError ?? new Error("withRateLimitRetry: unexpected state");
52772
52804
  }
52773
52805
  function sleep(ms, signal) {
52774
- return new Promise((resolve17, reject) => {
52806
+ return new Promise((resolve19, reject) => {
52775
52807
  if (signal?.aborted) {
52776
52808
  reject(signal.reason ?? new Error("Aborted"));
52777
52809
  return;
52778
52810
  }
52779
- const timer = setTimeout(resolve17, ms);
52811
+ const timer = setTimeout(resolve19, ms);
52780
52812
  if (signal) {
52781
52813
  const onAbort = () => {
52782
52814
  clearTimeout(timer);
52783
52815
  reject(signal.reason ?? new Error("Aborted"));
52784
52816
  };
52785
52817
  signal.addEventListener("abort", onAbort, { once: true });
52786
- const origResolve = resolve17;
52787
- resolve17 = () => {
52818
+ const origResolve = resolve19;
52819
+ resolve19 = () => {
52788
52820
  signal.removeEventListener("abort", onAbort);
52789
52821
  origResolve();
52790
52822
  };
@@ -52864,9 +52896,9 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
52864
52896
  return { attachmentContents, imageContents };
52865
52897
  }
52866
52898
  const { readFile: readFile19 } = await import("node:fs/promises");
52867
- const { join: join39 } = await import("node:path");
52899
+ const { join: join38 } = await import("node:path");
52868
52900
  for (const att of attachments) {
52869
- const filePath = join39(
52901
+ const filePath = join38(
52870
52902
  rootDir,
52871
52903
  ".fusion",
52872
52904
  "tasks",
@@ -54380,9 +54412,9 @@ Remove or replace these ids and call fn_task_create again.`
54380
54412
  }
54381
54413
  try {
54382
54414
  const { readFile: readFile19 } = await import("node:fs/promises");
54383
- const { join: join39 } = await import("node:path");
54415
+ const { join: join38 } = await import("node:path");
54384
54416
  const promptContent = await readFile19(
54385
- join39(rootDir, promptPath),
54417
+ join38(rootDir, promptPath),
54386
54418
  "utf-8"
54387
54419
  ).catch((err) => {
54388
54420
  const msg = err instanceof Error ? err.message : String(err);
@@ -54751,7 +54783,7 @@ import { existsSync as existsSync21 } from "node:fs";
54751
54783
  import { join as join26 } from "node:path";
54752
54784
  import { Type as Type3 } from "typebox";
54753
54785
  async function execWithProcessGroup(command, options) {
54754
- return new Promise((resolve17, reject) => {
54786
+ return new Promise((resolve19, reject) => {
54755
54787
  if (options.signal?.aborted) {
54756
54788
  reject(Object.assign(
54757
54789
  new Error(`Command aborted before start: ${command}`),
@@ -54844,7 +54876,7 @@ async function execWithProcessGroup(command, options) {
54844
54876
  return;
54845
54877
  }
54846
54878
  if (code === 0) {
54847
- resolve17({ stdout, stderr, bufferOverflow: stdoutOverflow || stderrOverflow });
54879
+ resolve19({ stdout, stderr, bufferOverflow: stdoutOverflow || stderrOverflow });
54848
54880
  return;
54849
54881
  }
54850
54882
  reject(Object.assign(
@@ -55407,21 +55439,31 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
55407
55439
  return false;
55408
55440
  }
55409
55441
  }
55410
- async function amendMergeCommitWithFixes(rootDir, taskId, authorArg) {
55442
+ function resetMergeWithWarn(rootDir, taskId, label) {
55443
+ try {
55444
+ execSync("git reset --merge", { cwd: rootDir, stdio: "pipe" });
55445
+ } catch (err) {
55446
+ const msg = err instanceof Error ? err.message : String(err);
55447
+ mergerLog.warn(`${taskId}: git reset --merge cleanup failed during ${label}: ${msg}`);
55448
+ }
55449
+ }
55450
+ function buildDeterministicMergeMessage(params) {
55451
+ const { taskId, branch, commitLog, includeTaskId } = params;
55452
+ const prefix = includeTaskId ? `feat(${taskId})` : "feat";
55453
+ const subject = `${prefix}: merge ${branch}`;
55454
+ const body = commitLog && commitLog.trim().length > 0 ? commitLog.trim() : `- merge ${branch}`;
55455
+ const escape = (s) => s.replace(/(["\\$`])/g, "\\$1");
55456
+ return {
55457
+ subjectArg: `-m "${escape(subject)}"`,
55458
+ bodyArg: `-m "${escape(body)}"`
55459
+ };
55460
+ }
55461
+ async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, includeTaskId, preAttemptHeadSha, authorArg) {
55411
55462
  try {
55412
- const { stdout: stagedFiles } = await execAsync2("git diff --cached --name-only", {
55413
- cwd: rootDir,
55414
- encoding: "utf-8"
55415
- });
55416
55463
  const { stdout: unstagedFiles } = await execAsync2("git diff --name-only", {
55417
55464
  cwd: rootDir,
55418
55465
  encoding: "utf-8"
55419
55466
  });
55420
- const hasChanges = stagedFiles.trim().length > 0 || unstagedFiles.trim().length > 0;
55421
- if (!hasChanges) {
55422
- mergerLog.log(`${taskId}: no changes to amend after verification fix`);
55423
- return false;
55424
- }
55425
55467
  if (unstagedFiles.trim().length > 0) {
55426
55468
  await execAsync2("git add -A", { cwd: rootDir });
55427
55469
  }
@@ -55429,12 +55471,10 @@ async function amendMergeCommitWithFixes(rootDir, taskId, authorArg) {
55429
55471
  cwd: rootDir,
55430
55472
  encoding: "utf-8"
55431
55473
  });
55432
- const gitlinkPaths = [];
55433
55474
  for (const line of staged.split("\n")) {
55434
55475
  const match = line.match(/^:\d{6} 160000 [^\t]+\t(.+)$/);
55435
- if (match) gitlinkPaths.push(match[1]);
55436
- }
55437
- for (const path2 of gitlinkPaths) {
55476
+ if (!match) continue;
55477
+ const path2 = match[1];
55438
55478
  mergerLog.warn(`${taskId}: refusing to stage gitlink "${path2}" (project uses no submodules \u2014 likely a nested worktree). Unstaging.`);
55439
55479
  try {
55440
55480
  await execAsync2(`git reset HEAD -- "${path2}"`, { cwd: rootDir });
@@ -55447,15 +55487,43 @@ async function amendMergeCommitWithFixes(rootDir, taskId, authorArg) {
55447
55487
  cwd: rootDir,
55448
55488
  encoding: "utf-8"
55449
55489
  });
55450
- if (finalStaged.trim().length > 0) {
55451
- await execAsync2(`git commit --amend --no-edit${authorArg}`, { cwd: rootDir });
55452
- mergerLog.log(`${taskId}: amended merge commit with verification fixes`);
55490
+ const hasStaged = finalStaged.trim().length > 0;
55491
+ const { stdout: currentHeadOut } = await execAsync2("git rev-parse HEAD", {
55492
+ cwd: rootDir,
55493
+ encoding: "utf-8"
55494
+ });
55495
+ const currentHead = currentHeadOut.trim();
55496
+ const headMoved = currentHead !== preAttemptHeadSha;
55497
+ if (!hasStaged && !headMoved) {
55498
+ mergerLog.warn(
55499
+ `${taskId}: refusing to record merge \u2014 no commit was created and no changes are staged. This usually means the AI agent never ran git commit and the in-merge fix had nothing to add.`
55500
+ );
55501
+ return false;
55502
+ }
55503
+ const { subjectArg, bodyArg } = buildDeterministicMergeMessage({
55504
+ taskId,
55505
+ branch,
55506
+ commitLog,
55507
+ includeTaskId
55508
+ });
55509
+ const trailerArg = buildTaskIdTrailerArg(taskId);
55510
+ if (!headMoved) {
55511
+ await execAsync2(
55512
+ `git commit ${subjectArg} ${bodyArg}${trailerArg}${authorArg}`,
55513
+ { cwd: rootDir }
55514
+ );
55515
+ mergerLog.log(`${taskId}: created fresh merge commit after verification fix (no prior commit to amend)`);
55453
55516
  return true;
55454
55517
  }
55455
- return false;
55518
+ await execAsync2(
55519
+ `git commit --amend ${subjectArg} ${bodyArg}${trailerArg}${authorArg}`,
55520
+ { cwd: rootDir }
55521
+ );
55522
+ mergerLog.log(`${taskId}: amended merge commit with verification fixes (deterministic message)`);
55523
+ return true;
55456
55524
  } catch (err) {
55457
55525
  const errorMessage = err instanceof Error ? err.message : String(err);
55458
- mergerLog.warn(`${taskId}: failed to amend merge commit: ${errorMessage}`);
55526
+ mergerLog.warn(`${taskId}: failed to finalize merge commit: ${errorMessage}`);
55459
55527
  return false;
55460
55528
  }
55461
55529
  }
@@ -55706,6 +55774,11 @@ function getCommitAuthorArg(settings) {
55706
55774
  const email = settings.commitAuthorEmail || "noreply@runfusion.ai";
55707
55775
  return ` --author="${name} <${email}>"`;
55708
55776
  }
55777
+ function buildSourceIssueRef(sourceIssue) {
55778
+ if (!sourceIssue || sourceIssue.provider !== "github") return "";
55779
+ if (!sourceIssue.repository || !sourceIssue.issueNumber) return "";
55780
+ return `${sourceIssue.repository}#${sourceIssue.issueNumber}`;
55781
+ }
55709
55782
  function buildMergeSystemPrompt(includeTaskId, agentPrompts, authorArg) {
55710
55783
  const commitFormat = includeTaskId ? `\`\`\`
55711
55784
  git commit -m "<type>(<scope>): <summary>" -m "<body>"${authorArg || ""}
@@ -55716,6 +55789,7 @@ Message format:
55716
55789
  - **Scope:** the task ID (e.g., KB-001)
55717
55790
  - **Summary:** one line describing what the squash brings in (imperative mood)
55718
55791
  - **Body:** 2-5 bullet points summarizing the key changes, each starting with "- "
55792
+ - **GitHub reference:** when the prompt includes a source issue reference, add \`Ref: owner/repo#N\` to the commit body
55719
55793
  ${authorArg ? `- **Author:** Always include the --author flag as shown in the example above.` : ""}
55720
55794
 
55721
55795
  Example:
@@ -55733,6 +55807,7 @@ Message format:
55733
55807
  - **Type:** feat, fix, refactor, docs, test, chore
55734
55808
  - **Summary:** one line describing what the squash brings in (imperative mood)
55735
55809
  - **Body:** 2-5 bullet points summarizing the key changes, each starting with "- "
55810
+ - **GitHub reference:** when the prompt includes a source issue reference, add \`Ref: owner/repo#N\` to the commit body
55736
55811
  ${authorArg ? `- **Author:** Always include the --author flag as shown in the example above.` : ""}
55737
55812
  Do NOT include a scope in the commit message type.
55738
55813
 
@@ -56116,6 +56191,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
56116
56191
  throw new Error(`Cannot merge ${taskId}: ${mergeBlocker}`);
56117
56192
  }
56118
56193
  const branch = task.branch || `fusion/${taskId.toLowerCase()}`;
56194
+ const sourceIssueRef = buildSourceIssueRef(task.sourceIssue);
56119
56195
  const worktreePath = task.worktree;
56120
56196
  const result = {
56121
56197
  task,
@@ -56419,6 +56495,17 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
56419
56495
  void 0,
56420
56496
  "merger"
56421
56497
  );
56498
+ let preAttemptHeadSha = "";
56499
+ try {
56500
+ const { stdout } = await execAsync2("git rev-parse HEAD", {
56501
+ cwd: rootDir,
56502
+ encoding: "utf-8"
56503
+ });
56504
+ preAttemptHeadSha = stdout.trim();
56505
+ } catch (err) {
56506
+ const msg = err instanceof Error ? err.message : String(err);
56507
+ mergerLog.warn(`${taskId}: failed to capture pre-attempt HEAD (${msg}) \u2014 verification-fix finalizer will fall back to amend`);
56508
+ }
56422
56509
  try {
56423
56510
  const success = await executeMergeAttempt({
56424
56511
  store,
@@ -56428,6 +56515,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
56428
56515
  commitLog,
56429
56516
  diffStat,
56430
56517
  includeTaskId,
56518
+ sourceIssueRef,
56431
56519
  smartConflictResolution,
56432
56520
  mergeConflictStrategy,
56433
56521
  attemptNum,
@@ -56536,12 +56624,27 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
56536
56624
  }
56537
56625
  if (fixSuccess) {
56538
56626
  const authorArg = getCommitAuthorArg(settings);
56539
- await amendMergeCommitWithFixes(rootDir, taskId, authorArg);
56627
+ const finalized = await commitOrAmendMergeWithFixes(
56628
+ rootDir,
56629
+ taskId,
56630
+ branch,
56631
+ commitLog,
56632
+ includeTaskId,
56633
+ preAttemptHeadSha,
56634
+ authorArg
56635
+ );
56636
+ if (!finalized) {
56637
+ resetMergeWithWarn(rootDir, taskId, "verification-fix finalize");
56638
+ throw new Error(
56639
+ `${taskId}: verification fix succeeded but no merge commit could be created \u2014 refusing to mark merge complete.`
56640
+ );
56641
+ }
56540
56642
  return true;
56541
56643
  }
56542
56644
  }
56543
56645
  }
56544
56646
  mergerLog.error(`${taskId}: deterministic verification failed \u2014 aborting merge (in-merge fix exhausted or disabled)`);
56647
+ resetMergeWithWarn(rootDir, taskId, "deterministic-verification rollback");
56545
56648
  throw error;
56546
56649
  }
56547
56650
  if (error.message?.includes("Build verification failed")) {
@@ -56612,7 +56715,21 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
56612
56715
  }
56613
56716
  if (fixSuccess) {
56614
56717
  const authorArg = getCommitAuthorArg(settings);
56615
- await amendMergeCommitWithFixes(rootDir, taskId, authorArg);
56718
+ const finalized = await commitOrAmendMergeWithFixes(
56719
+ rootDir,
56720
+ taskId,
56721
+ branch,
56722
+ commitLog,
56723
+ includeTaskId,
56724
+ preAttemptHeadSha,
56725
+ authorArg
56726
+ );
56727
+ if (!finalized) {
56728
+ resetMergeWithWarn(rootDir, taskId, "build-verification fix finalize");
56729
+ throw new Error(
56730
+ `${taskId}: build verification fix succeeded but no merge commit could be created \u2014 refusing to mark merge complete.`
56731
+ );
56732
+ }
56616
56733
  return true;
56617
56734
  }
56618
56735
  }
@@ -56626,10 +56743,11 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
56626
56743
  await audit.git({ type: "reset:hard", target: branch, metadata: { purpose: "build-retry" } });
56627
56744
  } catch (err) {
56628
56745
  const msg = err instanceof Error ? err.message : String(err);
56629
- mergerLog.warn(`${taskId}: git reset --merge cleanup failed (build-retry): ${msg}`);
56746
+ mergerLog.warn(`${taskId}: git reset --merge cleanup failed during build-verification rollback (build-retry): ${msg}`);
56630
56747
  }
56631
56748
  return false;
56632
56749
  }
56750
+ resetMergeWithWarn(rootDir, taskId, "build-verification rollback (no retries left)");
56633
56751
  throw error;
56634
56752
  }
56635
56753
  if (error.name === "MergeNonConflictError") {
@@ -56977,6 +57095,7 @@ async function executeMergeAttempt(params, aiTracker) {
56977
57095
  commitLog,
56978
57096
  diffStat,
56979
57097
  includeTaskId,
57098
+ sourceIssueRef,
56980
57099
  smartConflictResolution,
56981
57100
  attemptNum,
56982
57101
  options,
@@ -57167,19 +57286,12 @@ async function executeMergeAttempt(params, aiTracker) {
57167
57286
  simplifiedContext: attemptNum === 2,
57168
57287
  options,
57169
57288
  testCommand,
57170
- buildCommand: buildCommand2
57289
+ buildCommand: buildCommand2,
57290
+ sourceIssueRef
57171
57291
  });
57172
57292
  if (!agentResult.success) {
57173
57293
  const errorMessage = agentResult.error || "Build verification failed";
57174
57294
  await store.logEntry(taskId, "Build verification failed during merge", errorMessage);
57175
- try {
57176
- execSync("git reset --merge", { cwd: rootDir, stdio: "pipe" });
57177
- } catch (resetErr) {
57178
- const msg = resetErr instanceof Error ? resetErr.message : String(resetErr);
57179
- mergerLog.warn(
57180
- `${taskId}: git reset --merge cleanup failed during build-verification rollback (build-verification reset, build-retry): ${msg}`
57181
- );
57182
- }
57183
57295
  throw new Error(`Build verification failed for ${taskId}: ${errorMessage}`);
57184
57296
  }
57185
57297
  if (testCommand || buildCommand2) {
@@ -57195,6 +57307,24 @@ async function executeMergeAttempt(params, aiTracker) {
57195
57307
  options.signal
57196
57308
  );
57197
57309
  }
57310
+ try {
57311
+ const authorArg = getCommitAuthorArg(params.settings);
57312
+ const { subjectArg, bodyArg } = buildDeterministicMergeMessage({
57313
+ taskId,
57314
+ branch,
57315
+ commitLog,
57316
+ includeTaskId
57317
+ });
57318
+ const trailerArg = buildTaskIdTrailerArg(taskId);
57319
+ await execAsync2(
57320
+ `git commit --amend ${subjectArg} ${bodyArg}${trailerArg}${authorArg}`,
57321
+ { cwd: rootDir }
57322
+ );
57323
+ mergerLog.log(`${taskId}: rewrote AI-authored merge commit message with deterministic body`);
57324
+ } catch (err) {
57325
+ const msg = err instanceof Error ? err.message : String(err);
57326
+ mergerLog.warn(`${taskId}: failed to canonicalize merge commit message (${msg}) \u2014 keeping AI-written message`);
57327
+ }
57198
57328
  return true;
57199
57329
  } catch (error) {
57200
57330
  if (error instanceof Error && error.name === "MergeAbortedError") {
@@ -57217,7 +57347,7 @@ async function executeMergeAttempt(params, aiTracker) {
57217
57347
  }
57218
57348
  }
57219
57349
  async function attemptWithSideStrategy(params, side = "theirs", aiTracker) {
57220
- const { rootDir, branch, commitLog, includeTaskId, taskId, store, settings, testCommand, buildCommand: buildCommand2, testSource, buildSource } = params;
57350
+ const { rootDir, branch, commitLog, includeTaskId, sourceIssueRef, taskId, store, settings, testCommand, buildCommand: buildCommand2, testSource, buildSource } = params;
57221
57351
  mergerLog.log(`${taskId}: attempting merge with -X ${side} strategy`);
57222
57352
  try {
57223
57353
  throwIfAborted(params.options.signal, taskId);
@@ -57258,8 +57388,9 @@ async function attemptWithSideStrategy(params, side = "theirs", aiTracker) {
57258
57388
  const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
57259
57389
  const authorArg = getCommitAuthorArg(settings);
57260
57390
  const trailerArg = buildTaskIdTrailerArg(taskId);
57391
+ const issueRefBodyArg = sourceIssueRef ? ` -m "Ref: ${sourceIssueRef}"` : "";
57261
57392
  await execAsync2(
57262
- `git commit -m "${fallbackPrefix}: merge ${branch} (auto-resolved)" -m "${escapedLog}"${trailerArg}${authorArg}`,
57393
+ `git commit -m "${fallbackPrefix}: merge ${branch} (auto-resolved)" -m "${escapedLog}"${issueRefBodyArg}${trailerArg}${authorArg}`,
57263
57394
  { cwd: rootDir }
57264
57395
  );
57265
57396
  mergerLog.log(`${taskId}: committed with -X ${side} auto-resolution`);
@@ -57296,6 +57427,7 @@ async function runAiAgentForCommit(params) {
57296
57427
  includeTaskId,
57297
57428
  hasConflicts,
57298
57429
  simplifiedContext,
57430
+ sourceIssueRef,
57299
57431
  options,
57300
57432
  testCommand,
57301
57433
  buildCommand: buildCommand2
@@ -57394,7 +57526,8 @@ async function runAiAgentForCommit(params) {
57394
57526
  simplifiedContext,
57395
57527
  testCommand,
57396
57528
  buildCommand: buildCommand2,
57397
- authorArg
57529
+ authorArg,
57530
+ sourceIssueRef
57398
57531
  });
57399
57532
  mergerLog.log(`${taskId}: starting fresh merge agent session`);
57400
57533
  try {
@@ -57426,7 +57559,8 @@ async function runAiAgentForCommit(params) {
57426
57559
  // Also skip detailed context
57427
57560
  testCommand,
57428
57561
  buildCommand: buildCommand2,
57429
- authorArg
57562
+ authorArg,
57563
+ sourceIssueRef
57430
57564
  });
57431
57565
  try {
57432
57566
  await withRateLimitRetry(async () => {
@@ -57468,8 +57602,9 @@ async function runAiAgentForCommit(params) {
57468
57602
  const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
57469
57603
  const authorArg2 = getCommitAuthorArg(settings);
57470
57604
  const trailerArg = buildTaskIdTrailerArg(taskId);
57605
+ const issueRefBodyArg = sourceIssueRef ? ` -m "Ref: ${sourceIssueRef}"` : "";
57471
57606
  await execAsync2(
57472
- `git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"${trailerArg}${authorArg2}`,
57607
+ `git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"${issueRefBodyArg}${trailerArg}${authorArg2}`,
57473
57608
  { cwd: rootDir }
57474
57609
  );
57475
57610
  } else {
@@ -57492,7 +57627,7 @@ async function runAiAgentForCommit(params) {
57492
57627
  }
57493
57628
  }
57494
57629
  function buildMergePrompt(params) {
57495
- const { taskId, branch, commitLog, diffStat, hasConflicts, simplifiedContext, testCommand, buildCommand: buildCommand2, authorArg } = params;
57630
+ const { taskId, branch, commitLog, diffStat, hasConflicts, simplifiedContext, sourceIssueRef, testCommand, buildCommand: buildCommand2, authorArg } = params;
57496
57631
  const truncatedCommitLog = truncateWithEllipsis(commitLog, MERGE_COMMIT_LOG_MAX_CHARS);
57497
57632
  const truncatedDiffStat = truncateWithEllipsis(diffStat, MERGE_DIFF_STAT_MAX_CHARS);
57498
57633
  const parts = [
@@ -57528,6 +57663,13 @@ function buildMergePrompt(params) {
57528
57663
  `Write and run the \`git commit\` command with a good message summarizing the work.${authorArg ? ` Be sure to include \`${authorArg.trim()}\` in the commit command.` : ""}`
57529
57664
  );
57530
57665
  }
57666
+ if (sourceIssueRef) {
57667
+ parts.push(
57668
+ "",
57669
+ "Include this in the commit message body:",
57670
+ `- Ref: ${sourceIssueRef}`
57671
+ );
57672
+ }
57531
57673
  if (testCommand) {
57532
57674
  parts.push(
57533
57675
  "",
@@ -58703,7 +58845,7 @@ function resolveExecutorModelPair(taskModelProvider, taskModelId, settings) {
58703
58845
  return { provider: void 0, modelId: void 0 };
58704
58846
  }
58705
58847
  function sleep2(ms) {
58706
- return new Promise((resolve17) => setTimeout(resolve17, ms));
58848
+ return new Promise((resolve19) => setTimeout(resolve19, ms));
58707
58849
  }
58708
58850
  var execAsync4, stepExecLog, MAX_STEP_RETRIES, RETRY_DELAYS_MS, NOOP_TASK_STORE, StepSessionExecutor;
58709
58851
  var init_step_session_executor = __esm({
@@ -59451,6 +59593,7 @@ function buildExecutionPrompt(task, rootDir, settings, worktreePath) {
59451
59593
  const reviewMatch = prompt.match(/##\s*Review Level[:\s]*(\d)/);
59452
59594
  const reviewLevel = reviewMatch ? parseInt(reviewMatch[1], 10) : 0;
59453
59595
  const authorArg = settings?.commitAuthorEnabled !== false ? ` --author="${settings?.commitAuthorName || "Fusion"} <${settings?.commitAuthorEmail || "noreply@runfusion.ai"}>"` : "";
59596
+ const sourceIssueRef = task.sourceIssue?.provider === "github" && task.sourceIssue.repository && task.sourceIssue.issueNumber ? `${task.sourceIssue.repository}#${task.sourceIssue.issueNumber}` : "";
59454
59597
  const hasProgress = task.steps.length > 0 && task.steps.some((s) => s.status !== "pending");
59455
59598
  let progressSection = "";
59456
59599
  if (hasProgress) {
@@ -59553,7 +59696,7 @@ ${hasProgress ? `Resume from Step ${task.currentStep}. Do NOT redo completed ste
59553
59696
  Use \`fn_task_update\` to report progress on every step transition.
59554
59697
  Use \`fn_task_log\` for important actions and decisions.
59555
59698
  Use \`fn_task_create\` for truly separate follow-up work, not for fixes required to get tests, build, or typecheck back to green.
59556
- Commit at step boundaries: \`git commit -m "feat(${task.id}): complete Step N \u2014 description"${authorArg}\`
59699
+ Commit at step boundaries: \`git commit -m "feat(${task.id}): complete Step N \u2014 description"${sourceIssueRef ? ` -m "Ref: ${sourceIssueRef}"` : ""}${authorArg}\`
59557
59700
  When all steps are complete: call \`fn_task_done()\`
59558
59701
 
59559
59702
  If a build command is configured, run that exact command in this worktree before calling \`fn_task_done()\`.
@@ -59586,7 +59729,7 @@ function detectReviewHandoffIntent(commentText) {
59586
59729
  ];
59587
59730
  return handoffPhrases.some((phrase) => text.includes(phrase));
59588
59731
  }
59589
- var execAsync5, STEP_STATUSES, MAX_WORKFLOW_STEP_RETRIES, MAX_TASK_DONE_SESSION_RETRIES, MAX_TASK_DONE_REQUEUE_RETRIES, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2, NonRetryableWorktreeError, taskUpdateParams, taskAddDepParams, spawnAgentParams, reviewStepParams, EXECUTOR_SYSTEM_PROMPT, TaskExecutor;
59732
+ var execAsync5, STEP_STATUSES, MAX_WORKFLOW_STEP_RETRIES, MAX_TASK_DONE_SESSION_RETRIES, MAX_TASK_DONE_REQUEUE_RETRIES, COMPLETED_TASK_WATCHDOG_MS, WORKFLOW_RERUN_WATCHDOG_MS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2, NonRetryableWorktreeError, taskUpdateParams, taskAddDepParams, spawnAgentParams, reviewStepParams, EXECUTOR_SYSTEM_PROMPT, TaskExecutor;
59590
59733
  var init_executor = __esm({
59591
59734
  "../engine/src/executor.ts"() {
59592
59735
  "use strict";
@@ -59622,6 +59765,8 @@ var init_executor = __esm({
59622
59765
  MAX_WORKFLOW_STEP_RETRIES = 3;
59623
59766
  MAX_TASK_DONE_SESSION_RETRIES = 3;
59624
59767
  MAX_TASK_DONE_REQUEUE_RETRIES = 3;
59768
+ COMPLETED_TASK_WATCHDOG_MS = 6e4;
59769
+ WORKFLOW_RERUN_WATCHDOG_MS = 15e3;
59625
59770
  WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2 = 4e3;
59626
59771
  NonRetryableWorktreeError = class extends Error {
59627
59772
  };
@@ -59739,6 +59884,7 @@ If the task's PROMPT.md includes a "Documentation Requirements" section listing
59739
59884
  ## Git discipline
59740
59885
  - Commit after completing each step (not after every file change)
59741
59886
  - Use conventional commit messages prefixed with the task ID
59887
+ - When the task has a GitHub issue reference, include \`Ref: owner/repo#N\` in the commit body
59742
59888
  - Do NOT commit broken or half-implemented code
59743
59889
 
59744
59890
  ## Worktree Boundaries
@@ -59831,6 +59977,7 @@ Lint, tests, and typecheck are also hard quality gates:
59831
59977
  store.on("task:moved", ({ task, from, to }) => {
59832
59978
  executorLog.log(`[event:task:moved] ${task.id}: ${from} \u2192 ${to}`);
59833
59979
  if (to === "in-progress") {
59980
+ this.clearWorkflowRerunWatchdog(task.id);
59834
59981
  executorLog.log(`[event:task:moved] Initiating execute() for ${task.id}`);
59835
59982
  void (async () => {
59836
59983
  const taskForExecution = await this.resetMergeStateIfNeeded(task, from);
@@ -59839,6 +59986,7 @@ Lint, tests, and typecheck are also hard quality gates:
59839
59986
  (err) => executorLog.error(`Failed to start ${task.id}:`, err)
59840
59987
  );
59841
59988
  } else if (from === "in-progress") {
59989
+ this.clearCompletedTaskWatchdog(task.id);
59842
59990
  if (this.activeSessions.has(task.id)) {
59843
59991
  executorLog.log(`${task.id} moved from in-progress to ${to} \u2014 terminating agent session`);
59844
59992
  this.pausedAborted.add(task.id);
@@ -60030,6 +60178,10 @@ Lint, tests, and typecheck are also hard quality gates:
60030
60178
  spawnedAgents = /* @__PURE__ */ new Map();
60031
60179
  /** Per-task baseline of session stats used for delta persistence across repeated updates. */
60032
60180
  tokenUsageBaselines = /* @__PURE__ */ new Map();
60181
+ /** One-shot watchdogs for completed tasks that should have transitioned to in-review. */
60182
+ completedTaskWatchdogs = /* @__PURE__ */ new Map();
60183
+ /** One-shot watchdogs for workflow reruns that should have bounced back to in-progress. */
60184
+ workflowRerunWatchdogs = /* @__PURE__ */ new Map();
60033
60185
  async finalizeAlreadyReviewedTask(taskId) {
60034
60186
  const latestTask = await this.store.getTask(taskId);
60035
60187
  if (!latestTask || latestTask.column !== "in-review") {
@@ -60185,6 +60337,120 @@ Lint, tests, and typecheck are also hard quality gates:
60185
60337
  await this.store.updateTask(task.id, updates);
60186
60338
  }
60187
60339
  }
60340
+ clearCompletedTaskWatchdog(taskId) {
60341
+ const handle = this.completedTaskWatchdogs.get(taskId);
60342
+ if (!handle) return;
60343
+ clearTimeout(handle);
60344
+ this.completedTaskWatchdogs.delete(taskId);
60345
+ }
60346
+ clearWorkflowRerunWatchdog(taskId) {
60347
+ const handle = this.workflowRerunWatchdogs.get(taskId);
60348
+ if (!handle) return;
60349
+ clearTimeout(handle);
60350
+ this.workflowRerunWatchdogs.delete(taskId);
60351
+ }
60352
+ scheduleCompletedTaskWatchdog(taskId, trigger) {
60353
+ this.clearCompletedTaskWatchdog(taskId);
60354
+ const handle = setTimeout(async () => {
60355
+ this.completedTaskWatchdogs.delete(taskId);
60356
+ let currentTask = null;
60357
+ try {
60358
+ currentTask = await this.store.getTask(taskId);
60359
+ } catch (err) {
60360
+ const errorMessage = err instanceof Error ? err.message : String(err);
60361
+ executorLog.warn(`${taskId}: completed-task watchdog could not read latest task state: ${errorMessage}`);
60362
+ return;
60363
+ }
60364
+ if (!currentTask || currentTask.column !== "in-progress" || currentTask.paused) {
60365
+ return;
60366
+ }
60367
+ if (this.activeSessions.has(taskId) || this.activeStepExecutors.has(taskId) || this.recoveringCompleted.has(taskId)) {
60368
+ return;
60369
+ }
60370
+ if (!this.isTaskWorkComplete(currentTask)) {
60371
+ return;
60372
+ }
60373
+ executorLog.warn(
60374
+ `${taskId}: completed-task watchdog fired after ${COMPLETED_TASK_WATCHDOG_MS / 1e3}s (${trigger}) \u2014 attempting direct recovery to in-review`
60375
+ );
60376
+ await this.store.logEntry(
60377
+ taskId,
60378
+ `Watchdog: task remained in-progress ${COMPLETED_TASK_WATCHDOG_MS / 1e3}s after ${trigger} \u2014 attempting direct recovery to in-review`
60379
+ ).catch(() => void 0);
60380
+ this.recoveringCompleted.add(taskId);
60381
+ try {
60382
+ const recovered = await this.recoverCompletedTask(currentTask);
60383
+ if (!recovered) {
60384
+ await this.store.logEntry(
60385
+ taskId,
60386
+ "Watchdog recovery attempt could not finalize completed task \u2014 leaving for follow-up recovery"
60387
+ ).catch(() => void 0);
60388
+ }
60389
+ } finally {
60390
+ this.recoveringCompleted.delete(taskId);
60391
+ }
60392
+ }, COMPLETED_TASK_WATCHDOG_MS);
60393
+ this.completedTaskWatchdogs.set(taskId, handle);
60394
+ }
60395
+ async performWorkflowRerunBounce(taskId, worktreePath) {
60396
+ const latestTask = await this.store.getTask(taskId);
60397
+ if (!latestTask) {
60398
+ throw new Error("task missing during workflow rerun bounce");
60399
+ }
60400
+ if (latestTask.column === "in-progress") {
60401
+ await this.store.moveTask(taskId, "todo");
60402
+ await this.store.updateTask(taskId, { worktree: worktreePath });
60403
+ await this.store.moveTask(taskId, "in-progress");
60404
+ return;
60405
+ }
60406
+ if (latestTask.column === "todo") {
60407
+ await this.store.updateTask(taskId, { worktree: worktreePath });
60408
+ await this.store.moveTask(taskId, "in-progress");
60409
+ return;
60410
+ }
60411
+ throw new Error(`task is in '${latestTask.column}', cannot bounce to in-progress`);
60412
+ }
60413
+ scheduleWorkflowRerun(taskId, worktreePath, successMessage) {
60414
+ this.clearWorkflowRerunWatchdog(taskId);
60415
+ setTimeout(async () => {
60416
+ try {
60417
+ await this.performWorkflowRerunBounce(taskId, worktreePath);
60418
+ executorLog.log(successMessage);
60419
+ } catch (err) {
60420
+ const errorMessage = err instanceof Error ? err.message : String(err);
60421
+ executorLog.error(`${taskId}: failed to schedule rerun bounce: ${errorMessage}`);
60422
+ }
60423
+ }, 0);
60424
+ const watchdog = setTimeout(async () => {
60425
+ this.workflowRerunWatchdogs.delete(taskId);
60426
+ let currentTask = null;
60427
+ try {
60428
+ currentTask = await this.store.getTask(taskId);
60429
+ } catch (err) {
60430
+ const errorMessage = err instanceof Error ? err.message : String(err);
60431
+ executorLog.warn(`${taskId}: workflow rerun watchdog could not read latest task state: ${errorMessage}`);
60432
+ return;
60433
+ }
60434
+ if (!currentTask || currentTask.paused || currentTask.column === "in-progress") {
60435
+ return;
60436
+ }
60437
+ executorLog.warn(
60438
+ `${taskId}: workflow rerun watchdog fired after ${WORKFLOW_RERUN_WATCHDOG_MS / 1e3}s \u2014 task is still ${currentTask.column}; retrying handoff once`
60439
+ );
60440
+ await this.store.logEntry(
60441
+ taskId,
60442
+ `Watchdog: workflow rerun handoff stalled for ${WORKFLOW_RERUN_WATCHDOG_MS / 1e3}s (still ${currentTask.column}) \u2014 retrying once`
60443
+ ).catch(() => void 0);
60444
+ try {
60445
+ await this.performWorkflowRerunBounce(taskId, worktreePath);
60446
+ executorLog.warn(`${taskId}: workflow rerun watchdog retry succeeded`);
60447
+ } catch (err) {
60448
+ const errorMessage = err instanceof Error ? err.message : String(err);
60449
+ executorLog.error(`${taskId}: workflow rerun watchdog retry failed: ${errorMessage}`);
60450
+ }
60451
+ }, WORKFLOW_RERUN_WATCHDOG_MS);
60452
+ this.workflowRerunWatchdogs.set(taskId, watchdog);
60453
+ }
60188
60454
  async shouldFinalizeCompletedTask(taskId, taskDone) {
60189
60455
  const task = await this.store.getTask(taskId);
60190
60456
  const completionBlocker = await this.getTaskCompletionBlocker(task);
@@ -60318,6 +60584,7 @@ Lint, tests, and typecheck are also hard quality gates:
60318
60584
  }
60319
60585
  await this.persistTokenUsage(task.id);
60320
60586
  await this.store.moveTask(task.id, "in-review");
60587
+ this.clearCompletedTaskWatchdog(task.id);
60321
60588
  await this.store.logEntry(task.id, "Auto-recovered: task work was complete but stuck in in-progress \u2014 moved to in-review");
60322
60589
  executorLog.log(`\u2713 ${task.id} auto-recovered completed task \u2192 in-review`);
60323
60590
  this.options.onComplete?.(task);
@@ -60814,6 +61081,7 @@ Lint, tests, and typecheck are also hard quality gates:
60814
61081
  executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
60815
61082
  await audit.filesystem({ type: "file:capture-modified", target: task.id, metadata: { files: modifiedFiles } });
60816
61083
  }
61084
+ this.scheduleCompletedTaskWatchdog(task.id, "step-session completion");
60817
61085
  if (executionMode !== "fast") {
60818
61086
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
60819
61087
  if (!workflowResult.allPassed) {
@@ -60834,6 +61102,7 @@ Lint, tests, and typecheck are also hard quality gates:
60834
61102
  }
60835
61103
  await this.store.updateTask(task.id, { workflowStepRetries: void 0, taskDoneRetryCount: null });
60836
61104
  await this.store.moveTask(task.id, "in-review");
61105
+ this.clearCompletedTaskWatchdog(task.id);
60837
61106
  await audit.database({ type: "task:move", target: task.id, metadata: { to: "in-review" } });
60838
61107
  executorLog.log(`\u2713 ${task.id} completed (step-session) \u2192 in-review`);
60839
61108
  this.options.onComplete?.(task);
@@ -61170,6 +61439,7 @@ Lint, tests, and typecheck are also hard quality gates:
61170
61439
  await this.store.logEntry(task.id, "Execution paused after completion \u2014 finalizing to in-review");
61171
61440
  await this.persistTokenUsage(task.id);
61172
61441
  await this.store.moveTask(task.id, "in-review");
61442
+ this.clearCompletedTaskWatchdog(task.id);
61173
61443
  this.options.onComplete?.(task);
61174
61444
  } else {
61175
61445
  executorLog.log(`${task.id} paused (graceful session exit) \u2014 moving to todo`);
@@ -61190,6 +61460,7 @@ Lint, tests, and typecheck are also hard quality gates:
61190
61460
  taskDone = true;
61191
61461
  executorLog.log(`${task.id} all steps done \u2014 treating as implicit fn_task_done`);
61192
61462
  await this.store.logEntry(task.id, "All steps complete \u2014 implicit fn_task_done (agent did not call tool explicitly)", void 0, this.currentRunContext);
61463
+ this.scheduleCompletedTaskWatchdog(task.id, "implicit fn_task_done");
61193
61464
  }
61194
61465
  }
61195
61466
  if (taskDone) {
@@ -61199,6 +61470,7 @@ Lint, tests, and typecheck are also hard quality gates:
61199
61470
  await this.store.updateTask(task.id, { modifiedFiles });
61200
61471
  executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
61201
61472
  }
61473
+ this.scheduleCompletedTaskWatchdog(task.id, "task completion");
61202
61474
  if (executionMode !== "fast") {
61203
61475
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
61204
61476
  if (!workflowResult.allPassed) {
@@ -61220,6 +61492,7 @@ Lint, tests, and typecheck are also hard quality gates:
61220
61492
  await this.store.updateTask(task.id, { workflowStepRetries: void 0, taskDoneRetryCount: null });
61221
61493
  await this.persistTokenUsage(task.id);
61222
61494
  await this.store.moveTask(task.id, "in-review");
61495
+ this.clearCompletedTaskWatchdog(task.id);
61223
61496
  executorLog.log(`\u2713 ${task.id} completed \u2192 in-review`);
61224
61497
  this.options.onComplete?.(task);
61225
61498
  } else {
@@ -61293,6 +61566,7 @@ Lint, tests, and typecheck are also hard quality gates:
61293
61566
  taskDone = true;
61294
61567
  executorLog.log(`${task.id} all steps done \u2014 treating as implicit fn_task_done`);
61295
61568
  await this.store.logEntry(task.id, "All steps complete \u2014 implicit fn_task_done (agent did not call tool explicitly)", void 0, this.currentRunContext);
61569
+ this.scheduleCompletedTaskWatchdog(task.id, "implicit fn_task_done");
61296
61570
  }
61297
61571
  }
61298
61572
  }
@@ -61303,6 +61577,7 @@ Lint, tests, and typecheck are also hard quality gates:
61303
61577
  await this.store.updateTask(task.id, { modifiedFiles });
61304
61578
  executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
61305
61579
  }
61580
+ this.scheduleCompletedTaskWatchdog(task.id, "task completion retry");
61306
61581
  if (executionMode !== "fast") {
61307
61582
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
61308
61583
  if (!workflowResult.allPassed) {
@@ -61320,6 +61595,7 @@ Lint, tests, and typecheck are also hard quality gates:
61320
61595
  await this.store.updateTask(task.id, { workflowStepRetries: void 0, taskDoneRetryCount: null });
61321
61596
  await this.persistTokenUsage(task.id);
61322
61597
  await this.store.moveTask(task.id, "in-review");
61598
+ this.clearCompletedTaskWatchdog(task.id);
61323
61599
  executorLog.log(`\u2713 ${task.id} completed on retry \u2192 in-review`);
61324
61600
  this.options.onComplete?.(task);
61325
61601
  } else {
@@ -61747,6 +62023,7 @@ Lint, tests, and typecheck are also hard quality gates:
61747
62023
  await store.updateTask(taskId, { summary: params.summary });
61748
62024
  }
61749
62025
  await store.logEntry(taskId, "Task marked done by agent");
62026
+ this.scheduleCompletedTaskWatchdog(taskId, "fn_task_done");
61750
62027
  const successMessage = params.summary ? "Task marked complete with summary. All steps done. Moving to in-review." : "Task marked complete. All steps done. Moving to in-review.";
61751
62028
  return {
61752
62029
  content: [{ type: "text", text: successMessage }],
@@ -61973,6 +62250,7 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
61973
62250
  */
61974
62251
  async handleWorkflowRevisionRequest(task, worktreePath, feedback, stepName) {
61975
62252
  executorLog.log(`${task.id}: workflow revision requested by step "${stepName}"`);
62253
+ this.clearCompletedTaskWatchdog(task.id);
61976
62254
  const updatedTask = await this.store.getTask(task.id);
61977
62255
  const reopen = await this.reopenLastStepForRevision(task.id, updatedTask);
61978
62256
  const reopenSummary = reopen ? `re-opening Step ${reopen.index + 1} ("${reopen.name}") for in-place fix` : "no step to re-open (none were completed)";
@@ -61987,21 +62265,11 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
61987
62265
  sessionFile: null
61988
62266
  });
61989
62267
  executorLog.log(`${task.id}: scheduling fresh execution after revision request`);
61990
- setTimeout(async () => {
61991
- try {
61992
- await this.store.moveTask(task.id, "todo");
61993
- await this.store.updateTask(task.id, { worktree: worktreePath });
61994
- await this.store.moveTask(task.id, "in-progress");
61995
- executorLog.log(`${task.id}: revision rerun scheduled \u2014 moved to todo then in-progress`);
61996
- } catch (err) {
61997
- const errorMessage = err instanceof Error ? err.message : String(err);
61998
- executorLog.error(`${task.id}: failed to schedule revision rerun: ${errorMessage}`);
61999
- await this.store.logEntry(
62000
- task.id,
62001
- "Workflow revision requested \u2014 executor ready for fresh execution"
62002
- );
62003
- }
62004
- }, 0);
62268
+ this.scheduleWorkflowRerun(
62269
+ task.id,
62270
+ worktreePath,
62271
+ `${task.id}: revision rerun scheduled \u2014 moved to todo then in-progress`
62272
+ );
62005
62273
  }
62006
62274
  /**
62007
62275
  * Re-open the last non-pending step so a revision/failure handler gives the
@@ -62088,6 +62356,7 @@ ${feedback}
62088
62356
  * @returns true if a retry was scheduled, false if retries are exhausted
62089
62357
  */
62090
62358
  async handleWorkflowStepFailure(task, worktreePath, failureFeedback, stepName) {
62359
+ this.clearCompletedTaskWatchdog(task.id);
62091
62360
  const currentRetries = task.workflowStepRetries ?? 0;
62092
62361
  if (currentRetries >= MAX_WORKFLOW_STEP_RETRIES) {
62093
62362
  executorLog.warn(`${task.id}: workflow step "${stepName}" failed \u2014 retries exhausted (${MAX_WORKFLOW_STEP_RETRIES}/${MAX_WORKFLOW_STEP_RETRIES})`);
@@ -62106,21 +62375,11 @@ ${feedback}
62106
62375
  sessionFile: null
62107
62376
  });
62108
62377
  executorLog.log(`${task.id}: scheduling fresh execution after workflow step failure (retry ${retryCount}/${MAX_WORKFLOW_STEP_RETRIES})`);
62109
- setTimeout(async () => {
62110
- try {
62111
- await this.store.moveTask(task.id, "todo");
62112
- await this.store.updateTask(task.id, { worktree: worktreePath });
62113
- await this.store.moveTask(task.id, "in-progress");
62114
- executorLog.log(`${task.id}: workflow step retry scheduled \u2014 moved to todo then in-progress`);
62115
- } catch (err) {
62116
- const errorMessage = err instanceof Error ? err.message : String(err);
62117
- executorLog.error(`${task.id}: failed to schedule workflow step retry: ${errorMessage}`);
62118
- await this.store.logEntry(
62119
- task.id,
62120
- "Workflow step failed \u2014 executor ready for fresh execution"
62121
- );
62122
- }
62123
- }, 0);
62378
+ this.scheduleWorkflowRerun(
62379
+ task.id,
62380
+ worktreePath,
62381
+ `${task.id}: workflow step retry scheduled \u2014 moved to todo then in-progress`
62382
+ );
62124
62383
  return true;
62125
62384
  }
62126
62385
  /**
@@ -62130,6 +62389,7 @@ ${feedback}
62130
62389
  */
62131
62390
  async sendTaskBackForFix(task, worktreePath, failureFeedback, stepName, reason) {
62132
62391
  const taskId = task.id;
62392
+ this.clearCompletedTaskWatchdog(taskId);
62133
62393
  await this.store.addTaskComment(
62134
62394
  taskId,
62135
62395
  `${reason}. The failing workflow step was "${stepName}". Feedback:
@@ -62151,17 +62411,11 @@ Please fix the issues so the verification can pass on the next attempt.`,
62151
62411
  sessionFile: null,
62152
62412
  workflowStepRetries: 0
62153
62413
  });
62154
- setTimeout(async () => {
62155
- try {
62156
- await this.store.moveTask(taskId, "todo");
62157
- await this.store.updateTask(taskId, { worktree: worktreePath });
62158
- await this.store.moveTask(taskId, "in-progress");
62159
- executorLog.log(`${taskId}: sent back to in-progress for remediation`);
62160
- } catch (err) {
62161
- const errorMessage = err instanceof Error ? err.message : String(err);
62162
- executorLog.error(`${taskId}: failed to move back to in-progress: ${errorMessage}`);
62163
- }
62164
- }, 0);
62414
+ this.scheduleWorkflowRerun(
62415
+ taskId,
62416
+ worktreePath,
62417
+ `${taskId}: sent back to in-progress for remediation`
62418
+ );
62165
62419
  }
62166
62420
  /**
62167
62421
  * Inject or update the "Workflow Step Failure" section in PROMPT.md.
@@ -62712,7 +62966,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
62712
62966
  );
62713
62967
  }
62714
62968
  const delay2 = this.WORKTREE_RETRY_DELAYS[attempt] || 1e3;
62715
- await new Promise((resolve17) => setTimeout(resolve17, delay2));
62969
+ await new Promise((resolve19) => setTimeout(resolve19, delay2));
62716
62970
  }
62717
62971
  }
62718
62972
  throw new Error("Unexpected exit from worktree creation retry loop");
@@ -72036,13 +72290,13 @@ var init_plugin_runner = __esm({
72036
72290
  * Returns the result on success, throws on timeout.
72037
72291
  */
72038
72292
  withTimeout(promise, ms, timeoutMessage) {
72039
- return new Promise((resolve17, reject) => {
72293
+ return new Promise((resolve19, reject) => {
72040
72294
  const timer = setTimeout(() => {
72041
72295
  reject(new Error(timeoutMessage));
72042
72296
  }, ms);
72043
72297
  promise.then((result) => {
72044
72298
  clearTimeout(timer);
72045
- resolve17(result);
72299
+ resolve19(result);
72046
72300
  }).catch((err) => {
72047
72301
  clearTimeout(timer);
72048
72302
  reject(err);
@@ -72711,7 +72965,7 @@ var init_in_process_runtime = __esm({
72711
72965
  runtimeLog.log(
72712
72966
  `Waiting for ${metrics.inFlightTasks} in-flight tasks to complete...`
72713
72967
  );
72714
- await new Promise((resolve17) => setTimeout(resolve17, 1e3));
72968
+ await new Promise((resolve19) => setTimeout(resolve19, 1e3));
72715
72969
  }
72716
72970
  const finalMetrics = this.getMetrics();
72717
72971
  if (finalMetrics.inFlightTasks > 0) {
@@ -73108,13 +73362,13 @@ var init_ipc_host = __esm({
73108
73362
  }
73109
73363
  const id = generateCorrelationId();
73110
73364
  const message = { type, id, payload };
73111
- return new Promise((resolve17, reject) => {
73365
+ return new Promise((resolve19, reject) => {
73112
73366
  const timeout = setTimeout(() => {
73113
73367
  this.pendingCommands.delete(id);
73114
73368
  reject(new Error(`Command ${type} timed out after ${timeoutMs ?? this.commandTimeoutMs}ms`));
73115
73369
  }, timeoutMs ?? this.commandTimeoutMs);
73116
73370
  this.pendingCommands.set(id, {
73117
- resolve: resolve17,
73371
+ resolve: resolve19,
73118
73372
  reject,
73119
73373
  timeout,
73120
73374
  type
@@ -73923,8 +74177,8 @@ var init_remote_node_client = __esm({
73923
74177
  return error instanceof TypeError;
73924
74178
  }
73925
74179
  async sleep(ms) {
73926
- await new Promise((resolve17) => {
73927
- setTimeout(resolve17, ms);
74180
+ await new Promise((resolve19) => {
74181
+ setTimeout(resolve19, ms);
73928
74182
  });
73929
74183
  }
73930
74184
  };
@@ -74188,14 +74442,14 @@ var init_remote_node_runtime = __esm({
74188
74442
  return error instanceof Error ? error : new Error(String(error));
74189
74443
  }
74190
74444
  async sleep(ms, signal) {
74191
- await new Promise((resolve17) => {
74445
+ await new Promise((resolve19) => {
74192
74446
  const timeout = setTimeout(() => {
74193
74447
  cleanup();
74194
- resolve17();
74448
+ resolve19();
74195
74449
  }, ms);
74196
74450
  const onAbort = () => {
74197
74451
  cleanup();
74198
- resolve17();
74452
+ resolve19();
74199
74453
  };
74200
74454
  const cleanup = () => {
74201
74455
  clearTimeout(timeout);
@@ -75082,10 +75336,10 @@ var init_tunnel_process_manager = __esm({
75082
75336
  lastError: null
75083
75337
  });
75084
75338
  this.emitLog("info", "manager", `Stopping ${currentHandle.provider} tunnel (pid=${currentHandle.child.pid ?? "n/a"})`);
75085
- this.activeStopPromise = new Promise((resolve17) => {
75339
+ this.activeStopPromise = new Promise((resolve19) => {
75086
75340
  const onClose = () => {
75087
75341
  currentHandle.child.removeListener("close", onClose);
75088
- resolve17();
75342
+ resolve19();
75089
75343
  };
75090
75344
  currentHandle.child.once("close", onClose);
75091
75345
  killManagedProcess(currentHandle.child, "SIGTERM");
@@ -75641,12 +75895,12 @@ ${detail}`
75641
75895
  */
75642
75896
  async onMerge(taskId) {
75643
75897
  if (this.mergeActive.has(taskId)) {
75644
- return new Promise((resolve17, reject) => {
75645
- this.manualMergeResolvers.set(taskId, { resolve: resolve17, reject });
75898
+ return new Promise((resolve19, reject) => {
75899
+ this.manualMergeResolvers.set(taskId, { resolve: resolve19, reject });
75646
75900
  });
75647
75901
  }
75648
- return new Promise((resolve17, reject) => {
75649
- this.manualMergeResolvers.set(taskId, { resolve: resolve17, reject });
75902
+ return new Promise((resolve19, reject) => {
75903
+ this.manualMergeResolvers.set(taskId, { resolve: resolve19, reject });
75650
75904
  this.internalEnqueueMerge(taskId);
75651
75905
  });
75652
75906
  }
@@ -79170,7 +79424,7 @@ function buildHermesArgs(prompt, settings, resumeSessionId) {
79170
79424
  async function invokeHermesCli(prompt, settings, resumeSessionId, signal) {
79171
79425
  const args = buildHermesArgs(prompt, settings, resumeSessionId);
79172
79426
  const binary = resolveBinaryForSpawn(settings.binaryPath);
79173
- return new Promise((resolve17, reject) => {
79427
+ return new Promise((resolve19, reject) => {
79174
79428
  let settled = false;
79175
79429
  const spawnEnv = { ...process.env, PYTHONUNBUFFERED: "1" };
79176
79430
  if (settings.profile) {
@@ -79238,7 +79492,7 @@ ${combined}`));
79238
79492
  return;
79239
79493
  }
79240
79494
  try {
79241
- resolve17(parseHermesOutput(stdout, stderr));
79495
+ resolve19(parseHermesOutput(stdout, stderr));
79242
79496
  } catch (parseErr) {
79243
79497
  reject(parseErr);
79244
79498
  }
@@ -79464,7 +79718,7 @@ async function promptCli(session, message, config, callbacks, signal) {
79464
79718
  const args = buildOpenClawArgs(config, session.sessionId, message);
79465
79719
  const cb = { ...session.callbacks, ...callbacks };
79466
79720
  cb.onToolStart?.("openclaw.agent", { sessionId: session.sessionId });
79467
- return new Promise((resolve17, reject) => {
79721
+ return new Promise((resolve19, reject) => {
79468
79722
  let settled = false;
79469
79723
  const child = spawn5(config.binaryPath, args, {
79470
79724
  stdio: ["ignore", "pipe", "pipe"]
@@ -79557,7 +79811,7 @@ async function promptCli(session, message, config, callbacks, signal) {
79557
79811
  ...metaError ? { error: metaError } : {},
79558
79812
  ...errorText.length > 0 ? { toolErrors: errorText } : {}
79559
79813
  });
79560
- resolve17();
79814
+ resolve19();
79561
79815
  });
79562
79816
  });
79563
79817
  }
@@ -79816,12 +80070,14 @@ var init_rate_limit = __esm({
79816
80070
  });
79817
80071
 
79818
80072
  // ../dashboard/src/routes/register-chat-routes.ts
80073
+ var CHAT_MAX_ATTACHMENT_SIZE;
79819
80074
  var init_register_chat_routes = __esm({
79820
80075
  "../dashboard/src/routes/register-chat-routes.ts"() {
79821
80076
  "use strict";
79822
80077
  init_api_error();
79823
80078
  init_rate_limit();
79824
80079
  init_sse_buffer();
80080
+ CHAT_MAX_ATTACHMENT_SIZE = 5 * 1024 * 1024;
79825
80081
  }
79826
80082
  });
79827
80083
 
@@ -79866,7 +80122,7 @@ var init_register_messaging_scripts = __esm({
79866
80122
 
79867
80123
  // ../dashboard/src/github.ts
79868
80124
  function delay(ms) {
79869
- return new Promise((resolve17) => setTimeout(resolve17, ms));
80125
+ return new Promise((resolve19) => setTimeout(resolve19, ms));
79870
80126
  }
79871
80127
  function normalizeCheckState(state) {
79872
80128
  switch ((state ?? "").toLowerCase()) {
@@ -82577,7 +82833,7 @@ async function spawnPaperclipCliJson(args, opts) {
82577
82833
  }
82578
82834
  const timeoutMs = opts.cliTimeoutMs ?? 15e3;
82579
82835
  const label = ["paperclipai", ...args].join(" ");
82580
- return new Promise((resolve17, reject) => {
82836
+ return new Promise((resolve19, reject) => {
82581
82837
  let child;
82582
82838
  try {
82583
82839
  child = spawn10(bin, fullArgs, { stdio: ["ignore", "pipe", "pipe"] });
@@ -82623,7 +82879,7 @@ async function spawnPaperclipCliJson(args, opts) {
82623
82879
  return;
82624
82880
  }
82625
82881
  try {
82626
- resolve17(JSON.parse(cleaned));
82882
+ resolve19(JSON.parse(cleaned));
82627
82883
  } catch {
82628
82884
  reject(new Error(`${label} returned non-JSON output: ${cleaned.slice(0, 200)}`));
82629
82885
  }
@@ -82693,7 +82949,7 @@ var init_paperclip_client = __esm({
82693
82949
  // ../../plugins/fusion-plugin-paperclip-runtime/dist/runtime-adapter.js
82694
82950
  import { randomUUID as randomUUID13 } from "node:crypto";
82695
82951
  function sleep3(ms) {
82696
- return new Promise((resolve17) => setTimeout(resolve17, ms));
82952
+ return new Promise((resolve19) => setTimeout(resolve19, ms));
82697
82953
  }
82698
82954
  function asString2(value) {
82699
82955
  return typeof value === "string" ? value : void 0;
@@ -83102,22 +83358,29 @@ var init_update_check = __esm({
83102
83358
  });
83103
83359
 
83104
83360
  // ../dashboard/src/routes/register-update-check-routes.ts
83105
- import { readFileSync as readFileSync7 } from "node:fs";
83106
- import { dirname as dirname9, join as join35 } from "node:path";
83361
+ import { existsSync as existsSync28, readFileSync as readFileSync7 } from "node:fs";
83362
+ import { dirname as dirname9, resolve as resolve15 } from "node:path";
83107
83363
  import { fileURLToPath as fileURLToPath3 } from "node:url";
83108
- var __dirname, CLI_PACKAGE_VERSION;
83364
+ var CLI_PACKAGE_VERSION;
83109
83365
  var init_register_update_check_routes = __esm({
83110
83366
  "../dashboard/src/routes/register-update-check-routes.ts"() {
83111
83367
  "use strict";
83112
83368
  init_src();
83113
83369
  init_update_check();
83114
- __dirname = dirname9(fileURLToPath3(import.meta.url));
83115
83370
  CLI_PACKAGE_VERSION = (() => {
83116
83371
  try {
83117
- const packageJsonPath = join35(__dirname, "..", "..", "..", "cli", "package.json");
83118
- const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
83119
- if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
83120
- return packageJson.version;
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;
83121
83384
  }
83122
83385
  } catch {
83123
83386
  }
@@ -87149,8 +87412,8 @@ var init_auth_middleware = __esm({
87149
87412
 
87150
87413
  // ../dashboard/src/server.ts
87151
87414
  import express from "express";
87152
- import { join as join36, dirname as dirname10 } from "node:path";
87153
- import { existsSync as existsSync28, readFileSync as readFileSync8 } from "node:fs";
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";
87154
87417
  import { fileURLToPath as fileURLToPath4 } from "node:url";
87155
87418
  function clearAiSessionCleanupInterval() {
87156
87419
  if (!aiSessionCleanupIntervalHandle) {
@@ -87159,7 +87422,7 @@ function clearAiSessionCleanupInterval() {
87159
87422
  clearInterval(aiSessionCleanupIntervalHandle);
87160
87423
  aiSessionCleanupIntervalHandle = void 0;
87161
87424
  }
87162
- var __dirname2, 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;
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;
87163
87426
  var init_server = __esm({
87164
87427
  "../dashboard/src/server.ts"() {
87165
87428
  "use strict";
@@ -87185,10 +87448,10 @@ var init_server = __esm({
87185
87448
  init_dev_server_routes();
87186
87449
  init_auth_middleware();
87187
87450
  init_remote_auth();
87188
- __dirname2 = dirname10(fileURLToPath4(import.meta.url));
87451
+ __dirname = dirname10(fileURLToPath4(import.meta.url));
87189
87452
  PACKAGE_VERSION = (() => {
87190
87453
  try {
87191
- const packageJsonPath = join36(__dirname2, "..", "package.json");
87454
+ const packageJsonPath = join35(__dirname, "..", "package.json");
87192
87455
  const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf-8"));
87193
87456
  if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
87194
87457
  return packageJson.version;
@@ -87199,10 +87462,18 @@ var init_server = __esm({
87199
87462
  })();
87200
87463
  CLI_PACKAGE_VERSION2 = (() => {
87201
87464
  try {
87202
- const packageJsonPath = join36(__dirname2, "..", "..", "cli", "package.json");
87203
- const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf-8"));
87204
- if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
87205
- return packageJson.version;
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;
87206
87477
  }
87207
87478
  } catch {
87208
87479
  }
@@ -87278,8 +87549,8 @@ var init_src4 = __esm({
87278
87549
  });
87279
87550
 
87280
87551
  // src/project-context.ts
87281
- import { resolve as resolve15, dirname as dirname11 } from "node:path";
87282
- import { existsSync as existsSync29 } from "node:fs";
87552
+ import { resolve as resolve17, dirname as dirname11 } from "node:path";
87553
+ import { existsSync as existsSync30 } from "node:fs";
87283
87554
  async function resolveProject(projectNameFlag, cwd = process.cwd(), globalDir) {
87284
87555
  const central = new CentralCore(globalDir);
87285
87556
  await central.init();
@@ -87355,10 +87626,10 @@ async function clearDefaultProject(globalDir) {
87355
87626
  await globalStore.updateSettings(rest);
87356
87627
  }
87357
87628
  async function detectProjectFromCwd(cwd, central) {
87358
- let currentDir = resolve15(cwd);
87629
+ let currentDir = resolve17(cwd);
87359
87630
  while (true) {
87360
- const kbPath = resolve15(currentDir, ".fusion", "fusion.db");
87361
- if (existsSync29(kbPath)) {
87631
+ const kbPath = resolve17(currentDir, ".fusion", "fusion.db");
87632
+ if (existsSync30(kbPath)) {
87362
87633
  const project = await central.getProjectByPath(currentDir);
87363
87634
  if (project) {
87364
87635
  return project;
@@ -87458,8 +87729,8 @@ __export(task_exports, {
87458
87729
  runTaskUpdate: () => runTaskUpdate
87459
87730
  });
87460
87731
  import { createInterface as createInterface2 } from "node:readline/promises";
87461
- import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync30, readFileSync as readFileSync9 } from "node:fs";
87462
- import { join as join37 } from "node:path";
87732
+ import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync31, readFileSync as readFileSync9 } from "node:fs";
87733
+ import { join as join36 } from "node:path";
87463
87734
  function asLocalProjectContext(store) {
87464
87735
  const cwd = process.cwd();
87465
87736
  return {
@@ -87579,9 +87850,9 @@ async function runTaskCreate(descriptionArg, attachFiles, depends, projectName,
87579
87850
  console.log(` Path: .fusion/tasks/${task.id}/`);
87580
87851
  if (attachFiles && attachFiles.length > 0) {
87581
87852
  const { readFile: readFile19 } = await import("node:fs/promises");
87582
- const { basename: basename9, extname: extname3, resolve: resolve17 } = await import("node:path");
87853
+ const { basename: basename9, extname: extname3, resolve: resolve19 } = await import("node:path");
87583
87854
  for (const filePath of attachFiles) {
87584
- const resolvedPath = resolve17(filePath);
87855
+ const resolvedPath = resolve19(filePath);
87585
87856
  const filename = basename9(resolvedPath);
87586
87857
  const ext = extname3(filename).toLowerCase();
87587
87858
  const mimeType = MIME_TYPES[ext];
@@ -87715,8 +87986,8 @@ async function runTaskLogs(id, options = {}, projectName) {
87715
87986
  printEntries(filteredEntries);
87716
87987
  if (options.follow) {
87717
87988
  const projectPath = projectContext?.projectPath ?? process.cwd();
87718
- const logPath = join37(projectPath, ".fusion", "tasks", id, "agent.log");
87719
- if (!existsSync30(logPath)) {
87989
+ const logPath = join36(projectPath, ".fusion", "tasks", id, "agent.log");
87990
+ if (!existsSync31(logPath)) {
87720
87991
  console.log(`
87721
87992
  Waiting for log file to be created...`);
87722
87993
  }
@@ -87875,8 +88146,8 @@ async function runTaskMerge(id, projectName) {
87875
88146
  async function runTaskAttach(id, filePath, projectName) {
87876
88147
  const { readFile: readFile19 } = await import("node:fs/promises");
87877
88148
  const { basename: basename9, extname: extname3 } = await import("node:path");
87878
- const { resolve: resolve17 } = await import("node:path");
87879
- const resolvedPath = resolve17(filePath);
88149
+ const { resolve: resolve19 } = await import("node:path");
88150
+ const resolvedPath = resolve19(filePath);
87880
88151
  const filename = basename9(resolvedPath);
87881
88152
  const ext = extname3(filename).toLowerCase();
87882
88153
  const mimeType = MIME_TYPES[ext];
@@ -88407,12 +88678,12 @@ async function promptText(question) {
88407
88678
  console.log(" (Enter your response. Type DONE on its own line when finished):\n");
88408
88679
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
88409
88680
  const lines = [];
88410
- return new Promise((resolve17) => {
88681
+ return new Promise((resolve19) => {
88411
88682
  const askLine = () => {
88412
88683
  rl.question(" ").then((line) => {
88413
88684
  if (line.trim() === "DONE") {
88414
88685
  rl.close();
88415
- resolve17(lines.join("\n"));
88686
+ resolve19(lines.join("\n"));
88416
88687
  } else {
88417
88688
  lines.push(line);
88418
88689
  askLine();
@@ -88816,9 +89087,9 @@ async function runSkillsInstall(args, options) {
88816
89087
  stdio: "inherit",
88817
89088
  shell: true
88818
89089
  });
88819
- const exitCode = await new Promise((resolve17, reject) => {
89090
+ const exitCode = await new Promise((resolve19, reject) => {
88820
89091
  child.on("exit", (code) => {
88821
- resolve17(code ?? 1);
89092
+ resolve19(code ?? 1);
88822
89093
  });
88823
89094
  child.on("error", (err) => {
88824
89095
  reject(err);
@@ -88845,9 +89116,9 @@ init_src();
88845
89116
  init_gh_cli();
88846
89117
  import { Type as Type7 } from "typebox";
88847
89118
  import { StringEnum } from "@mariozechner/pi-ai";
88848
- import { resolve as resolve16, basename as basename8, extname as extname2, join as join38 } from "node:path";
89119
+ import { resolve as resolve18, basename as basename8, extname as extname2, join as join37 } from "node:path";
88849
89120
  import { readFile as readFile18 } from "node:fs/promises";
88850
- import { existsSync as existsSync31 } from "node:fs";
89121
+ import { existsSync as existsSync32 } from "node:fs";
88851
89122
  import { spawn as spawn9 } from "node:child_process";
88852
89123
  var MIME_TYPES2 = {
88853
89124
  ".png": "image/png",
@@ -88865,14 +89136,14 @@ var MIME_TYPES2 = {
88865
89136
  ".xml": "application/xml"
88866
89137
  };
88867
89138
  function resolveProjectRoot(cwd) {
88868
- let current = resolve16(cwd);
89139
+ let current = resolve18(cwd);
88869
89140
  while (true) {
88870
- if (existsSync31(join38(current, ".fusion"))) {
89141
+ if (existsSync32(join37(current, ".fusion"))) {
88871
89142
  return current;
88872
89143
  }
88873
- const parent = resolve16(current, "..");
89144
+ const parent = resolve18(current, "..");
88874
89145
  if (parent === current) {
88875
- return resolve16(cwd);
89146
+ return resolve18(cwd);
88876
89147
  }
88877
89148
  current = parent;
88878
89149
  }
@@ -88888,7 +89159,20 @@ async function getStore2(cwd) {
88888
89159
  return store;
88889
89160
  }
88890
89161
  function getFusionDir(cwd) {
88891
- return join38(resolveProjectRoot(cwd), ".fusion");
89162
+ return join37(resolveProjectRoot(cwd), ".fusion");
89163
+ }
89164
+ async function validateAssignableAgentId(cwd, agentId) {
89165
+ const { AgentStore: AgentStore2, isEphemeralAgent: isEphemeralAgent2 } = await Promise.resolve().then(() => (init_src(), src_exports));
89166
+ const agentStore = new AgentStore2({ rootDir: getFusionDir(cwd) });
89167
+ await agentStore.init();
89168
+ const agent = await agentStore.getAgent(agentId);
89169
+ if (!agent) {
89170
+ return `Agent ${agentId} not found`;
89171
+ }
89172
+ if (isEphemeralAgent2(agent)) {
89173
+ return `Cannot assign task to ephemeral/runtime agent ${agentId}`;
89174
+ }
89175
+ return null;
88892
89176
  }
88893
89177
  function formatTaskLine(t) {
88894
89178
  const label = t.title || t.description.slice(0, 60) + (t.description.length > 60 ? "\u2026" : "");
@@ -88951,6 +89235,16 @@ function kbExtension(pi) {
88951
89235
  }),
88952
89236
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
88953
89237
  const store = await getStore2(ctx.cwd);
89238
+ if (params.agentId !== void 0) {
89239
+ const error = await validateAssignableAgentId(ctx.cwd, params.agentId);
89240
+ if (error) {
89241
+ return {
89242
+ content: [{ type: "text", text: error }],
89243
+ isError: true,
89244
+ details: { error }
89245
+ };
89246
+ }
89247
+ }
88954
89248
  const task = await store.createTask({
88955
89249
  description: params.description.trim(),
88956
89250
  dependencies: params.depends,
@@ -89036,6 +89330,16 @@ Column: triage
89036
89330
  updatedFields.push("dependencies");
89037
89331
  }
89038
89332
  if (params.agentId !== void 0) {
89333
+ if (params.agentId !== null) {
89334
+ const error = await validateAssignableAgentId(ctx.cwd, params.agentId);
89335
+ if (error) {
89336
+ return {
89337
+ content: [{ type: "text", text: error }],
89338
+ isError: true,
89339
+ details: { error }
89340
+ };
89341
+ }
89342
+ }
89039
89343
  updates.assignedAgentId = params.agentId;
89040
89344
  updatedFields.push("agentId");
89041
89345
  }
@@ -89183,7 +89487,7 @@ Column: triage
89183
89487
  path: Type7.String({ description: "Path to the file to attach" })
89184
89488
  }),
89185
89489
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
89186
- const filePath = resolve16(ctx.cwd, params.path.replace(/^@/, ""));
89490
+ const filePath = resolve18(ctx.cwd, params.path.replace(/^@/, ""));
89187
89491
  const filename = basename8(filePath);
89188
89492
  const ext = extname2(filename).toLowerCase();
89189
89493
  const mimeType = MIME_TYPES2[ext];
@@ -90292,12 +90596,12 @@ Status: ${updated.status}`
90292
90596
  child.stderr?.on("data", (data) => {
90293
90597
  stderr += data.toString();
90294
90598
  });
90295
- const exitCode = await new Promise((resolve17) => {
90599
+ const exitCode = await new Promise((resolve19) => {
90296
90600
  child.on("exit", (code) => {
90297
- resolve17(code ?? 1);
90601
+ resolve19(code ?? 1);
90298
90602
  });
90299
90603
  child.on("error", () => {
90300
- resolve17(1);
90604
+ resolve19(1);
90301
90605
  });
90302
90606
  });
90303
90607
  try {