@runfusion/fusion 0.13.0 → 0.14.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 (69) hide show
  1. package/README.md +13 -0
  2. package/dist/bin.js +1250 -522
  3. package/dist/client/assets/AgentDetailView-CBFUveyO.js +18 -0
  4. package/dist/client/assets/{AgentsView-Dvf_xUkx.js → AgentsView-DPezXQ-U.js} +4 -4
  5. package/dist/client/assets/ChatView-5N4-EuhD.js +1 -0
  6. package/dist/client/assets/{DevServerView-C2qTJch7.js → DevServerView-Daft4YFc.js} +1 -1
  7. package/dist/client/assets/{DirectoryPicker-DRfhg9zz.js → DirectoryPicker-rew1y6qO.js} +1 -1
  8. package/dist/client/assets/{DocumentsView-j8ic1xUw.js → DocumentsView-i72qJzwd.js} +1 -1
  9. package/dist/client/assets/{InsightsView-CpAz3o0i.js → InsightsView-BL5eZJ0a.js} +1 -1
  10. package/dist/client/assets/{MemoryView-BcQsi_JK.js → MemoryView-pl8Cdg_p.js} +1 -1
  11. package/dist/client/assets/{NodesView-Bo_Yhr4N.js → NodesView-D6eJ15zc.js} +1 -1
  12. package/dist/client/assets/PiExtensionsManager-ExInwXWP.js +11 -0
  13. package/dist/client/assets/{PluginManager-BQhBHWrB.js → PluginManager-CYhtxHun.js} +1 -1
  14. package/dist/client/assets/{ResearchView-CLyyqAWE.js → ResearchView-B_QPUEjB.js} +1 -1
  15. package/dist/client/assets/{RoadmapsView-tG7IdOoc.js → RoadmapsView-DBNLaEsK.js} +1 -1
  16. package/dist/client/assets/SettingsModal-1ET586M3.js +31 -0
  17. package/dist/client/assets/{SettingsModal-CXUGeZ0_.js → SettingsModal-CL_gWmOj.js} +1 -1
  18. package/dist/client/assets/SettingsModal-D_AFkDJa.css +1 -0
  19. package/dist/client/assets/{SetupWizardModal-BMJL6eNR.js → SetupWizardModal-CLkY9HFL.js} +1 -1
  20. package/dist/client/assets/{SkillMultiselect-ILMft-Kz.js → SkillMultiselect-B0qi32SQ.js} +1 -1
  21. package/dist/client/assets/{SkillsView-x4_YwBz6.js → SkillsView-umVjRq6o.js} +1 -1
  22. package/dist/client/assets/TodoView-CFifSvrD.js +6 -0
  23. package/dist/client/assets/TodoView-SeO9o7km.css +1 -0
  24. package/dist/client/assets/{folder-open-DDdJt8aE.js → folder-open-nYPrL1W3.js} +1 -1
  25. package/dist/client/assets/index-Bc8nfKeH.js +661 -0
  26. package/dist/client/assets/index-C1prPuSl.css +1 -0
  27. package/dist/client/assets/{list-checks-DFxQ9biT.js → list-checks-sK8xJeH_.js} +1 -1
  28. package/dist/client/assets/{star-BKs1bgJN.js → star-BRtXbYkB.js} +1 -1
  29. package/dist/client/assets/{upload-Bb5Pidne.js → upload-BP60eBwN.js} +1 -1
  30. package/dist/client/assets/{users-BImNn91Q.js → users-qSGAX2Pf.js} +1 -1
  31. package/dist/client/index.html +2 -2
  32. package/dist/client/sw.js +6 -0
  33. package/dist/client/version.json +1 -1
  34. package/dist/droid-cli/index.ts +127 -0
  35. package/dist/droid-cli/package.json +37 -0
  36. package/dist/droid-cli/src/__tests__/control-handler.test.ts +164 -0
  37. package/dist/droid-cli/src/__tests__/event-bridge.test.ts +1318 -0
  38. package/dist/droid-cli/src/__tests__/mcp-config.test.ts +310 -0
  39. package/dist/droid-cli/src/__tests__/process-manager.test.ts +818 -0
  40. package/dist/droid-cli/src/__tests__/prompt-builder.test.ts +1206 -0
  41. package/dist/droid-cli/src/__tests__/provider.test.ts +1894 -0
  42. package/dist/droid-cli/src/__tests__/setup-test-isolation.test.ts +32 -0
  43. package/dist/droid-cli/src/__tests__/setup-test-isolation.ts +14 -0
  44. package/dist/droid-cli/src/__tests__/stream-parser.test.ts +188 -0
  45. package/dist/droid-cli/src/__tests__/thinking-config.test.ts +141 -0
  46. package/dist/droid-cli/src/__tests__/tool-mapping.test.ts +253 -0
  47. package/dist/droid-cli/src/control-handler.ts +82 -0
  48. package/dist/droid-cli/src/event-bridge.ts +397 -0
  49. package/dist/droid-cli/src/mcp-config.ts +144 -0
  50. package/dist/droid-cli/src/mcp-schema-server.cjs +49 -0
  51. package/dist/droid-cli/src/process-manager.ts +358 -0
  52. package/dist/droid-cli/src/prompt-builder.ts +629 -0
  53. package/dist/droid-cli/src/provider.ts +447 -0
  54. package/dist/droid-cli/src/stream-parser.ts +37 -0
  55. package/dist/droid-cli/src/thinking-config.ts +83 -0
  56. package/dist/droid-cli/src/tool-mapping.ts +147 -0
  57. package/dist/droid-cli/src/types.ts +87 -0
  58. package/dist/extension.js +473 -119
  59. package/dist/pi-claude-cli/package.json +1 -1
  60. package/package.json +2 -1
  61. package/dist/client/assets/AgentDetailView-B7j297GT.js +0 -18
  62. package/dist/client/assets/ChatView-BgUt38ty.js +0 -1
  63. package/dist/client/assets/PiExtensionsManager-DHt2zFg8.js +0 -11
  64. package/dist/client/assets/SettingsModal-9HS8MnmW.css +0 -1
  65. package/dist/client/assets/SettingsModal-UziTDnLh.js +0 -31
  66. package/dist/client/assets/TodoView-BBYcMbXE.js +0 -6
  67. package/dist/client/assets/TodoView-C1g65hJo.css +0 -1
  68. package/dist/client/assets/index-B15xwijw.css +0 -1
  69. package/dist/client/assets/index-DmSs2FGE.js +0 -661
package/dist/extension.js CHANGED
@@ -1007,11 +1007,40 @@ function hasAgentIdentity(agent) {
1007
1007
  if (!agent) return false;
1008
1008
  return !!(agent.soul?.trim() || agent.instructionsText?.trim() || agent.instructionsPath?.trim() || agent.memory?.trim());
1009
1009
  }
1010
- function getDefaultHeartbeatProcedurePath(agentId) {
1010
+ function slugifyAgentAssetSegment(value) {
1011
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1012
+ }
1013
+ function getSafeAgentAssetIdSegment(agentId) {
1014
+ const slug = slugifyAgentAssetSegment(agentId);
1015
+ return slug || "agent";
1016
+ }
1017
+ function getCanonicalAgentAssetDirectoryName(agentName, agentId) {
1018
+ if (!agentId || typeof agentId !== "string") {
1019
+ throw new Error("getCanonicalAgentAssetDirectoryName requires a non-empty agentId");
1020
+ }
1021
+ const safeId = getSafeAgentAssetIdSegment(agentId);
1022
+ const nameSlug = slugifyAgentAssetSegment(agentName ?? "");
1023
+ const prefix = nameSlug || safeId;
1024
+ return `${prefix}-${safeId}`;
1025
+ }
1026
+ function getLegacyAgentAssetDirectoryName(agentId) {
1027
+ if (!agentId || typeof agentId !== "string") {
1028
+ throw new Error("getLegacyAgentAssetDirectoryName requires a non-empty agentId");
1029
+ }
1030
+ return agentId;
1031
+ }
1032
+ function getCanonicalAgentInstructionsBundleDirName(agentName, agentId) {
1033
+ return `${getCanonicalAgentAssetDirectoryName(agentName, agentId)}-instructions`;
1034
+ }
1035
+ function getLegacyAgentInstructionsBundleDirName(agentId) {
1036
+ return `${getLegacyAgentAssetDirectoryName(agentId)}-instructions`;
1037
+ }
1038
+ function getDefaultHeartbeatProcedurePath(agentId, agentName) {
1011
1039
  if (!agentId || typeof agentId !== "string") {
1012
1040
  throw new Error("getDefaultHeartbeatProcedurePath requires a non-empty agentId");
1013
1041
  }
1014
- return `.fusion/agents/${agentId}/HEARTBEAT.md`;
1042
+ const directory = agentName ? getCanonicalAgentAssetDirectoryName(agentName, agentId) : getLegacyAgentAssetDirectoryName(agentId);
1043
+ return `.fusion/agents/${directory}/HEARTBEAT.md`;
1015
1044
  }
1016
1045
  function agentToConfigSnapshot(agent) {
1017
1046
  return {
@@ -2705,7 +2734,7 @@ var init_db = __esm({
2705
2734
  "use strict";
2706
2735
  init_sqlite_adapter();
2707
2736
  init_types();
2708
- SCHEMA_VERSION = 57;
2737
+ SCHEMA_VERSION = 58;
2709
2738
  SCHEMA_SQL = `
2710
2739
  -- Tasks table with JSON columns for nested data
2711
2740
  CREATE TABLE IF NOT EXISTS tasks (
@@ -4406,6 +4435,27 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
4406
4435
  }
4407
4436
  });
4408
4437
  }
4438
+ if (version < 58) {
4439
+ this.applyMigration(58, () => {
4440
+ const newCommand = "npx runfusion.ai backup --create";
4441
+ if (this.hasTable("automations") && this.hasColumn("automations", "command")) {
4442
+ this.db.prepare(
4443
+ `UPDATE automations
4444
+ SET command = ?, updatedAt = ?
4445
+ WHERE name = 'Database Backup'
4446
+ AND (command LIKE 'fn backup%' OR command LIKE 'kb backup%' OR command LIKE 'fusion backup%')`
4447
+ ).run(newCommand, (/* @__PURE__ */ new Date()).toISOString());
4448
+ }
4449
+ if (this.hasTable("routines") && this.hasColumn("routines", "command")) {
4450
+ this.db.prepare(
4451
+ `UPDATE routines
4452
+ SET command = ?, updatedAt = ?
4453
+ WHERE name = 'Database Backup'
4454
+ AND (command LIKE 'fn backup%' OR command LIKE 'kb backup%' OR command LIKE 'fusion backup%')`
4455
+ ).run(newCommand, (/* @__PURE__ */ new Date()).toISOString());
4456
+ }
4457
+ });
4458
+ }
4409
4459
  }
4410
4460
  /**
4411
4461
  * Run a single migration step inside a transaction and bump the version.
@@ -4669,7 +4719,7 @@ var init_agent_store = __esm({
4669
4719
  if (agent.heartbeatProcedurePath !== DEFAULT_HEARTBEAT_PROCEDURE_PATH) {
4670
4720
  continue;
4671
4721
  }
4672
- const newRelPath = getDefaultHeartbeatProcedurePath(agent.id);
4722
+ const newRelPath = await this.resolveCompatibleHeartbeatProcedurePath(agent);
4673
4723
  const newAbsPath = join3(this.rootDir, "..", newRelPath);
4674
4724
  if (legacyContent !== null) {
4675
4725
  try {
@@ -4830,7 +4880,7 @@ var init_agent_store = __esm({
4830
4880
  const metadata = input.metadata ?? {};
4831
4881
  const runtimeConfig = resolveCreationRuntimeConfig(input.runtimeConfig, metadata);
4832
4882
  const ephemeral = isEphemeralAgent({ metadata, name: input.name, role: input.role, reportsTo: input.reportsTo });
4833
- const resolvedHeartbeatProcedurePath = input.heartbeatProcedurePath ?? (ephemeral ? void 0 : getDefaultHeartbeatProcedurePath(agentId));
4883
+ const resolvedHeartbeatProcedurePath = input.heartbeatProcedurePath ?? (ephemeral ? void 0 : getDefaultHeartbeatProcedurePath(agentId, input.name));
4834
4884
  const agent = {
4835
4885
  id: agentId,
4836
4886
  name: input.name.trim(),
@@ -5067,14 +5117,16 @@ var init_agent_store = __esm({
5067
5117
  * Does not create the directory.
5068
5118
  */
5069
5119
  getInstructionsDir(agentId) {
5070
- return this.getBundleDir(agentId);
5120
+ const agent = this.readAgent(agentId);
5121
+ const agentName = agent?.name ?? "";
5122
+ return join3(this.agentsDir, getCanonicalAgentInstructionsBundleDirName(agentName, agentId));
5071
5123
  }
5072
5124
  /**
5073
5125
  * List markdown files in an agent's managed instructions bundle.
5074
5126
  * Returns [] when the bundle directory does not exist.
5075
5127
  */
5076
5128
  async listBundleFiles(agentId) {
5077
- const bundleDir = this.getBundleDir(agentId);
5129
+ const bundleDir = await this.resolveCompatibleBundleDir(agentId, false);
5078
5130
  try {
5079
5131
  const entries = await readdir(bundleDir, { withFileTypes: true });
5080
5132
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
@@ -5090,7 +5142,8 @@ var init_agent_store = __esm({
5090
5142
  */
5091
5143
  async readBundleFile(agentId, filePath) {
5092
5144
  this.validateBundleFilePath(filePath);
5093
- const resolvedPath = join3(this.getBundleDir(agentId), filePath);
5145
+ const bundleDir = await this.resolveCompatibleBundleDir(agentId, false);
5146
+ const resolvedPath = join3(bundleDir, filePath);
5094
5147
  return readFile(resolvedPath, "utf-8");
5095
5148
  }
5096
5149
  /**
@@ -5099,7 +5152,7 @@ var init_agent_store = __esm({
5099
5152
  async writeBundleFile(agentId, filePath, content) {
5100
5153
  return this.withLock(agentId, async () => {
5101
5154
  this.validateBundleFilePath(filePath);
5102
- const bundleDir = this.getBundleDir(agentId);
5155
+ const bundleDir = await this.resolveCompatibleBundleDir(agentId, true);
5103
5156
  await mkdir(bundleDir, { recursive: true });
5104
5157
  const existingFiles = await this.listBundleFiles(agentId);
5105
5158
  const isOverwrite = existingFiles.includes(filePath);
@@ -5118,7 +5171,8 @@ var init_agent_store = __esm({
5118
5171
  async deleteBundleFile(agentId, filePath) {
5119
5172
  return this.withLock(agentId, async () => {
5120
5173
  this.validateBundleFilePath(filePath);
5121
- await unlink(join3(this.getBundleDir(agentId), filePath));
5174
+ const bundleDir = await this.resolveCompatibleBundleDir(agentId, false);
5175
+ await unlink(join3(bundleDir, filePath));
5122
5176
  });
5123
5177
  }
5124
5178
  /**
@@ -5140,7 +5194,7 @@ var init_agent_store = __esm({
5140
5194
  };
5141
5195
  const updated = await this.updateAgent(agentId, { bundleConfig: normalizedConfig });
5142
5196
  if (normalizedConfig.mode === "managed") {
5143
- await mkdir(this.getBundleDir(agentId), { recursive: true });
5197
+ await mkdir(await this.resolveCompatibleBundleDir(agentId, true), { recursive: true });
5144
5198
  }
5145
5199
  return updated;
5146
5200
  }
@@ -5163,7 +5217,7 @@ var init_agent_store = __esm({
5163
5217
  bundleConfig: { mode: "managed", entryFile, files: [] }
5164
5218
  });
5165
5219
  }
5166
- await mkdir(this.getBundleDir(agentId), { recursive: true });
5220
+ await mkdir(await this.resolveCompatibleBundleDir(agentId, true), { recursive: true });
5167
5221
  const files = [];
5168
5222
  if (hasInstructionsText) {
5169
5223
  await this.writeBundleFile(agentId, entryFile, agent.instructionsText ?? "");
@@ -6124,8 +6178,87 @@ var init_agent_store = __esm({
6124
6178
  }
6125
6179
  return null;
6126
6180
  }
6127
- getBundleDir(agentId) {
6128
- return join3(this.agentsDir, `${agentId}-instructions`);
6181
+ getCanonicalBundleDir(agent) {
6182
+ return join3(this.agentsDir, getCanonicalAgentInstructionsBundleDirName(agent.name, agent.id));
6183
+ }
6184
+ getLegacyBundleDir(agentId) {
6185
+ return join3(this.agentsDir, getLegacyAgentInstructionsBundleDirName(agentId));
6186
+ }
6187
+ async resolveCompatibleBundleDir(agentId, createIfMissing) {
6188
+ const agent = this.readAgent(agentId);
6189
+ if (!agent) {
6190
+ throw new Error(`Agent ${agentId} not found`);
6191
+ }
6192
+ const canonicalDir = this.getCanonicalBundleDir(agent);
6193
+ if (await this.pathExists(canonicalDir)) {
6194
+ return canonicalDir;
6195
+ }
6196
+ const compatibleDir = await this.findExistingDisplayNameBundleDir(agent);
6197
+ if (compatibleDir) {
6198
+ return compatibleDir;
6199
+ }
6200
+ const legacyDir = this.getLegacyBundleDir(agent.id);
6201
+ if (await this.pathExists(legacyDir)) {
6202
+ return legacyDir;
6203
+ }
6204
+ return createIfMissing ? canonicalDir : canonicalDir;
6205
+ }
6206
+ async findExistingDisplayNameBundleDir(agent) {
6207
+ const safeId = getSafeAgentAssetIdSegment(agent.id);
6208
+ try {
6209
+ const entries = await readdir(this.agentsDir, { withFileTypes: true });
6210
+ const candidates = entries.filter((entry) => entry.isDirectory() && entry.name.endsWith("-instructions")).map((entry) => entry.name).filter((name) => {
6211
+ const base = name.slice(0, -"-instructions".length);
6212
+ return base.endsWith(`-${safeId}`);
6213
+ }).sort((a, b) => a.localeCompare(b));
6214
+ if (candidates.length === 0) {
6215
+ return null;
6216
+ }
6217
+ const canonicalName = getCanonicalAgentInstructionsBundleDirName(agent.name, agent.id);
6218
+ const selected = candidates.find((candidate) => candidate === canonicalName) ?? candidates[0];
6219
+ return join3(this.agentsDir, selected);
6220
+ } catch (err) {
6221
+ if (err.code === "ENOENT") {
6222
+ return null;
6223
+ }
6224
+ throw err;
6225
+ }
6226
+ }
6227
+ async resolveCompatibleHeartbeatProcedurePath(agent) {
6228
+ const canonicalPath = getDefaultHeartbeatProcedurePath(agent.id, agent.name);
6229
+ const canonicalAbs = join3(this.rootDir, "..", canonicalPath);
6230
+ if (await this.pathExists(canonicalAbs)) {
6231
+ return canonicalPath;
6232
+ }
6233
+ const safeId = getSafeAgentAssetIdSegment(agent.id);
6234
+ try {
6235
+ const entries = await readdir(this.agentsDir, { withFileTypes: true });
6236
+ const compatibleDir = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).find((name) => name.endsWith(`-${safeId}`));
6237
+ if (compatibleDir) {
6238
+ const candidatePath = `.fusion/agents/${compatibleDir}/HEARTBEAT.md`;
6239
+ if (await this.pathExists(join3(this.rootDir, "..", candidatePath))) {
6240
+ return candidatePath;
6241
+ }
6242
+ }
6243
+ } catch (err) {
6244
+ if (err.code !== "ENOENT") {
6245
+ throw err;
6246
+ }
6247
+ }
6248
+ const legacyPath = `.fusion/agents/${getLegacyAgentAssetDirectoryName(agent.id)}/HEARTBEAT.md`;
6249
+ const legacyAbs = join3(this.rootDir, "..", legacyPath);
6250
+ if (await this.pathExists(legacyAbs)) {
6251
+ return legacyPath;
6252
+ }
6253
+ return canonicalPath;
6254
+ }
6255
+ async pathExists(path2) {
6256
+ try {
6257
+ await access(path2, fsConstants.F_OK);
6258
+ return true;
6259
+ } catch {
6260
+ return false;
6261
+ }
6129
6262
  }
6130
6263
  validateBundleFilePath(filePath) {
6131
6264
  if (typeof filePath !== "string") {
@@ -35857,6 +35990,20 @@ function reconcileClaudeCliPaths(paths, vendoredPath) {
35857
35990
  }
35858
35991
  return filtered;
35859
35992
  }
35993
+ function isExternalDroidCliPath(p, vendoredPath) {
35994
+ if (vendoredPath && p === vendoredPath) return false;
35995
+ return /(^|[/\\])droid-cli([/\\]|$)/i.test(p);
35996
+ }
35997
+ function reconcileDroidCliPaths(paths, vendoredPath) {
35998
+ if (!vendoredPath) {
35999
+ return [...paths];
36000
+ }
36001
+ const filtered = paths.filter((p) => !isExternalDroidCliPath(p, vendoredPath));
36002
+ if (!filtered.includes(vendoredPath)) {
36003
+ return [vendoredPath, ...filtered];
36004
+ }
36005
+ return filtered;
36006
+ }
35860
36007
  function getDisplayPathWithinRoot(root, targetPath) {
35861
36008
  const usesWindowsPaths = /^[A-Za-z]:[\\/]/.test(root) || /^[A-Za-z]:[\\/]/.test(targetPath) || root.includes("\\") || targetPath.includes("\\");
35862
36009
  const pathApi = usesWindowsPaths ? win32 : { relative: relative2, isAbsolute: isAbsolute5, sep: sep4 };
@@ -36114,6 +36261,85 @@ var init_gh_cli = __esm({
36114
36261
  }
36115
36262
  });
36116
36263
 
36264
+ // ../core/src/fn-binary.ts
36265
+ import { spawn as spawn2 } from "node:child_process";
36266
+ import { platform as platform2 } from "node:os";
36267
+ function runProbe(command, args, timeoutMs) {
36268
+ return new Promise((resolve19) => {
36269
+ let stdout = "";
36270
+ let stderr = "";
36271
+ const child = spawn2(command, args, { stdio: ["ignore", "pipe", "pipe"], shell: false });
36272
+ const timer = setTimeout(() => {
36273
+ try {
36274
+ child.kill("SIGKILL");
36275
+ } catch {
36276
+ }
36277
+ }, timeoutMs);
36278
+ child.stdout?.on("data", (chunk) => {
36279
+ stdout += chunk.toString("utf8");
36280
+ });
36281
+ child.stderr?.on("data", (chunk) => {
36282
+ stderr += chunk.toString("utf8");
36283
+ });
36284
+ child.on("error", (err) => {
36285
+ clearTimeout(timer);
36286
+ resolve19({ exitCode: null, stdout, stderr: stderr || err.message });
36287
+ });
36288
+ child.on("close", (exitCode) => {
36289
+ clearTimeout(timer);
36290
+ resolve19({ exitCode, stdout, stderr });
36291
+ });
36292
+ });
36293
+ }
36294
+ async function whichBinary(name) {
36295
+ const isWindows = platform2() === "win32";
36296
+ const lookup = isWindows ? "where" : "which";
36297
+ const result = await runProbe(lookup, [name], 5e3);
36298
+ if (result.exitCode !== 0) return void 0;
36299
+ const firstLine = result.stdout.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
36300
+ return firstLine || void 0;
36301
+ }
36302
+ async function probeVersion(binary) {
36303
+ const result = await runProbe(binary, ["--version"], 1e4);
36304
+ if (result.exitCode !== 0) return void 0;
36305
+ const text = (result.stdout || result.stderr).trim();
36306
+ if (!text) return void 0;
36307
+ const match = text.match(/\d+\.\d+\.\d+(?:-[\w.]+)?/);
36308
+ return match ? match[0] : text.split(/\s+/)[0];
36309
+ }
36310
+ async function detectFnBinary() {
36311
+ for (const candidate of CANDIDATES) {
36312
+ try {
36313
+ const resolvedPath = await whichBinary(candidate);
36314
+ if (!resolvedPath) continue;
36315
+ const version = await probeVersion(candidate);
36316
+ return {
36317
+ installed: true,
36318
+ binary: candidate,
36319
+ path: resolvedPath,
36320
+ version,
36321
+ invocation: candidate
36322
+ };
36323
+ } catch {
36324
+ }
36325
+ }
36326
+ return {
36327
+ installed: false,
36328
+ invocation: FN_NPX_INVOCATION
36329
+ };
36330
+ }
36331
+ var FN_NPM_PACKAGE, FN_INSTALL_CURL, FN_INSTALL_NPM, FN_NPX_INVOCATION, CANDIDATES;
36332
+ var init_fn_binary = __esm({
36333
+ "../core/src/fn-binary.ts"() {
36334
+ "use strict";
36335
+ FN_NPM_PACKAGE = "runfusion.ai";
36336
+ FN_INSTALL_CURL = "curl -fsSL https://runfusion.ai/install.sh | sh";
36337
+ FN_INSTALL_NPM = `npm install -g ${FN_NPM_PACKAGE}`;
36338
+ FN_NPX_INVOCATION = `npx -y ${FN_NPM_PACKAGE}`;
36339
+ CANDIDATES = ["fn", "fusion"];
36340
+ }
36341
+ });
36342
+
36117
36343
  // ../core/src/settings-validation.ts
36118
36344
  function validateUnavailableNodePolicy(value) {
36119
36345
  if (value === void 0) {
@@ -50401,6 +50627,10 @@ __export(src_exports, {
50401
50627
  EXECUTION_MODES: () => EXECUTION_MODES,
50402
50628
  FEATURE_LOOP_STATES: () => FEATURE_LOOP_STATES,
50403
50629
  FEATURE_STATUSES: () => FEATURE_STATUSES,
50630
+ FN_INSTALL_CURL: () => FN_INSTALL_CURL,
50631
+ FN_INSTALL_NPM: () => FN_INSTALL_NPM,
50632
+ FN_NPM_PACKAGE: () => FN_NPM_PACKAGE,
50633
+ FN_NPX_INVOCATION: () => FN_NPX_INVOCATION,
50404
50634
  FileMemoryBackend: () => FileMemoryBackend,
50405
50635
  FirstRunDetector: () => FirstRunDetector,
50406
50636
  GLOBAL_SETTINGS_KEYS: () => GLOBAL_SETTINGS_KEYS,
@@ -50512,6 +50742,7 @@ __export(src_exports, {
50512
50742
  createInsightExtractionAutomation: () => createInsightExtractionAutomation,
50513
50743
  createMemoryDreamsAutomation: () => createMemoryDreamsAutomation,
50514
50744
  dailyMemoryPath: () => dailyMemoryPath,
50745
+ detectFnBinary: () => detectFnBinary,
50515
50746
  detectLegacyData: () => detectLegacyData,
50516
50747
  diffConfigSnapshots: () => diffConfigSnapshots,
50517
50748
  discoverPiExtensions: () => discoverPiExtensions,
@@ -50633,6 +50864,7 @@ __export(src_exports, {
50633
50864
  readProjectMemoryWithBackend: () => readProjectMemoryWithBackend,
50634
50865
  readWorkingMemory: () => readWorkingMemory,
50635
50866
  reconcileClaudeCliPaths: () => reconcileClaudeCliPaths,
50867
+ reconcileDroidCliPaths: () => reconcileDroidCliPaths,
50636
50868
  refreshQmdProjectMemoryIndex: () => refreshQmdProjectMemoryIndex,
50637
50869
  registerMemoryBackend: () => registerMemoryBackend,
50638
50870
  renderMemoryAuditMarkdown: () => renderMemoryAuditMarkdown,
@@ -50722,6 +50954,7 @@ var init_src = __esm({
50722
50954
  init_automation();
50723
50955
  init_automation_store();
50724
50956
  init_run_command();
50957
+ init_fn_binary();
50725
50958
  init_node_override_guard();
50726
50959
  init_settings_validation();
50727
50960
  init_routine();
@@ -52708,6 +52941,21 @@ function resolveVendoredClaudeCliEntry() {
52708
52941
  return null;
52709
52942
  }
52710
52943
  }
52944
+ function resolveVendoredDroidCliEntry() {
52945
+ try {
52946
+ const require_ = createRequire2(import.meta.url);
52947
+ const pkgJsonPath = require_.resolve("@fusion/droid-cli/package.json");
52948
+ const pkgJson = JSON.parse(readFileSync8(pkgJsonPath, "utf-8"));
52949
+ const extensions = pkgJson.pi?.extensions;
52950
+ if (!Array.isArray(extensions) || extensions.length === 0) return null;
52951
+ const entry = extensions[0];
52952
+ if (typeof entry !== "string" || entry.length === 0) return null;
52953
+ const path2 = resolve11(dirname8(pkgJsonPath), entry);
52954
+ return existsSync20(path2) ? path2 : null;
52955
+ } catch {
52956
+ return null;
52957
+ }
52958
+ }
52711
52959
  async function registerExtensionProviders(cwd, modelRegistry) {
52712
52960
  try {
52713
52961
  const agentDir = getPackageManagerAgentDir();
@@ -52723,8 +52971,13 @@ async function registerExtensionProviders(cwd, modelRegistry) {
52723
52971
  [...getEnabledPiExtensionPaths(cwd), ...packageExtensionPaths],
52724
52972
  vendoredClaudeCli
52725
52973
  );
52726
- const extensionsResult = await discoverAndLoadExtensions(
52974
+ const vendoredDroidCli = resolveVendoredDroidCliEntry();
52975
+ const doubleReconciledPaths = reconcileDroidCliPaths(
52727
52976
  reconciledPaths,
52977
+ vendoredDroidCli
52978
+ );
52979
+ const extensionsResult = await discoverAndLoadExtensions(
52980
+ doubleReconciledPaths,
52728
52981
  cwd,
52729
52982
  join25(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery")
52730
52983
  );
@@ -54381,14 +54634,25 @@ async function getAgentMemoryWindow(rootDir, agentMemory, path2, startLine = 1,
54381
54634
  backend: "agent-memory"
54382
54635
  };
54383
54636
  }
54384
- function createTaskCreateTool(store, provenance) {
54637
+ async function createAgentTask(store, input, options) {
54638
+ const settings = typeof store.getSettings === "function" ? await store.getSettings() : {};
54639
+ const rootDir = options?.rootDir;
54640
+ return store.createTask(input, {
54641
+ settings: { autoSummarizeTitles: settings.autoSummarizeTitles === true },
54642
+ onSummarize: rootDir ? async (description) => {
54643
+ const resolved = resolveTitleSummarizerSettingsModel(settings);
54644
+ return summarizeTitle(description, rootDir, resolved.provider, resolved.modelId);
54645
+ } : void 0
54646
+ });
54647
+ }
54648
+ function createTaskCreateTool(store, provenance, options) {
54385
54649
  return {
54386
54650
  name: "fn_task_create",
54387
54651
  label: "Create Task",
54388
54652
  description: "Create a new task for out-of-scope work discovered during execution. The task goes into triage where it will be specified by the AI. Optionally set dependencies (e.g., the new task depends on the current one, or the current task should wait for the new one).",
54389
54653
  parameters: taskCreateParams,
54390
54654
  execute: async (_id, params) => {
54391
- const task = await store.createTask({
54655
+ const task = await createAgentTask(store, {
54392
54656
  description: params.description,
54393
54657
  dependencies: params.dependencies,
54394
54658
  column: "triage",
@@ -54397,7 +54661,7 @@ function createTaskCreateTool(store, provenance) {
54397
54661
  sourceAgentId: provenance.sourceAgentId,
54398
54662
  sourceRunId: provenance.sourceRunId
54399
54663
  } : void 0
54400
- });
54664
+ }, options);
54401
54665
  const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
54402
54666
  return {
54403
54667
  content: [{
@@ -54750,7 +55014,7 @@ ${lines.join("\n\n")}` }],
54750
55014
  }
54751
55015
  };
54752
55016
  }
54753
- function createDelegateTaskTool(agentStore, taskStore) {
55017
+ function createDelegateTaskTool(agentStore, taskStore, options) {
54754
55018
  return {
54755
55019
  name: "fn_delegate_task",
54756
55020
  label: "Delegate Task",
@@ -54770,13 +55034,13 @@ function createDelegateTaskTool(agentStore, taskStore) {
54770
55034
  details: {}
54771
55035
  };
54772
55036
  }
54773
- const task = await taskStore.createTask({
55037
+ const task = await createAgentTask(taskStore, {
54774
55038
  description: params.description,
54775
55039
  dependencies: params.dependencies,
54776
55040
  column: "todo",
54777
55041
  assignedAgentId: params.agent_id,
54778
55042
  source: { sourceType: "api" }
54779
- });
55043
+ }, options);
54780
55044
  const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
54781
55045
  return {
54782
55046
  content: [{
@@ -57603,7 +57867,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
57603
57867
  // Agent delegation tools — discover and delegate work to other agents.
57604
57868
  ...this.options.agentStore ? [
57605
57869
  createListAgentsTool(this.options.agentStore),
57606
- createDelegateTaskTool(this.options.agentStore, this.store)
57870
+ createDelegateTaskTool(this.options.agentStore, this.store, { rootDir: this.rootDir })
57607
57871
  ] : [],
57608
57872
  this.createReviewSpecTool(
57609
57873
  task.id,
@@ -58142,7 +58406,7 @@ Remove or replace these ids and call fn_task_create again.`
58142
58406
  planLog.warn(`${options.parentTaskId}: failed to load parent task for fn_task_create inheritance: ${msg}`);
58143
58407
  parentTask = void 0;
58144
58408
  }
58145
- const newTask = await store.createTask({
58409
+ const newTask = await createAgentTask(store, {
58146
58410
  title: params.title,
58147
58411
  description: params.description,
58148
58412
  dependencies: validDeps,
@@ -58156,7 +58420,7 @@ Remove or replace these ids and call fn_task_create again.`
58156
58420
  sourceType: "agent_heartbeat",
58157
58421
  sourceParentTaskId: options.parentTaskId
58158
58422
  }
58159
- });
58423
+ }, { rootDir: this.rootDir });
58160
58424
  options.createdSubtasksRef.current.push(newTask.id);
58161
58425
  return {
58162
58426
  content: [
@@ -58584,7 +58848,7 @@ var init_run_audit = __esm({
58584
58848
  });
58585
58849
 
58586
58850
  // ../engine/src/merger.ts
58587
- import { execSync, exec as exec2, spawn as spawn2 } from "node:child_process";
58851
+ import { execSync, exec as exec2, spawn as spawn3 } from "node:child_process";
58588
58852
  import { promisify as promisify3 } from "node:util";
58589
58853
  import { existsSync as existsSync22 } from "node:fs";
58590
58854
  import { join as join29 } from "node:path";
@@ -58599,7 +58863,7 @@ async function execWithProcessGroup(command, options) {
58599
58863
  return;
58600
58864
  }
58601
58865
  const useProcessGroup = process.platform !== "win32";
58602
- const child = spawn2(command, {
58866
+ const child = spawn3(command, {
58603
58867
  cwd: options.cwd,
58604
58868
  shell: true,
58605
58869
  detached: useProcessGroup,
@@ -63335,10 +63599,10 @@ var init_step_session_executor = __esm({
63335
63599
  ] : [];
63336
63600
  const memoryTools = createMemoryTools(this.options.rootDir, settings);
63337
63601
  const taskLogTool = this.options.store ? [createTaskLogTool(this.options.store, taskDetail.id)] : [];
63338
- const taskCreateTool = this.options.store ? [createTaskCreateTool(this.options.store)] : [];
63602
+ const taskCreateTool = this.options.store ? [createTaskCreateTool(this.options.store, void 0, { rootDir: this.options.rootDir })] : [];
63339
63603
  const delegationTools = this.options.agentStore ? [
63340
63604
  createListAgentsTool(this.options.agentStore),
63341
- createDelegateTaskTool(this.options.agentStore, this.options.store)
63605
+ createDelegateTaskTool(this.options.agentStore, this.options.store, { rootDir: this.options.rootDir })
63342
63606
  ] : [];
63343
63607
  const messagingTools = this.options.messageStore && taskDetail.assignedAgentId ? [
63344
63608
  createSendMessageTool(this.options.messageStore, taskDetail.assignedAgentId),
@@ -63749,7 +64013,7 @@ var init_task_completion = __esm({
63749
64013
  });
63750
64014
 
63751
64015
  // ../engine/src/run-verification-tool.ts
63752
- import { spawn as spawn3 } from "node:child_process";
64016
+ import { spawn as spawn4 } from "node:child_process";
63753
64017
  import { existsSync as existsSync26 } from "node:fs";
63754
64018
  import { isAbsolute as isAbsolute10, join as join34 } from "node:path";
63755
64019
  import { Type as Type4 } from "@mariozechner/pi-ai";
@@ -63782,7 +64046,7 @@ async function runVerificationCommand2(opts) {
63782
64046
  const stdoutBuf = { head: "", tail: "", totalBytes: 0 };
63783
64047
  const stderrBuf = { head: "", tail: "", totalBytes: 0 };
63784
64048
  return new Promise((resolve19) => {
63785
- const child = spawn3(command, {
64049
+ const child = spawn4(command, {
63786
64050
  cwd,
63787
64051
  stdio: ["ignore", "pipe", "pipe"],
63788
64052
  env: { ...process.env },
@@ -66070,7 +66334,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66070
66334
  // Agent delegation tools — discover and delegate work to other agents.
66071
66335
  ...this.options.agentStore ? [
66072
66336
  createListAgentsTool(this.options.agentStore),
66073
- createDelegateTaskTool(this.options.agentStore, this.store)
66337
+ createDelegateTaskTool(this.options.agentStore, this.store, { rootDir: this.rootDir })
66074
66338
  ] : [],
66075
66339
  // Messaging tools — allows executor agents to send and receive messages.
66076
66340
  ...this.options.messageStore && assignedAgentId ? [
@@ -66764,7 +67028,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66764
67028
  return createTaskLogTool(this.store, taskId);
66765
67029
  }
66766
67030
  createTaskCreateTool() {
66767
- return createTaskCreateTool(this.store, { sourceType: "api" });
67031
+ return createTaskCreateTool(this.store, { sourceType: "api" }, { rootDir: this.rootDir });
66768
67032
  }
66769
67033
  createTaskDocumentWriteTool(taskId) {
66770
67034
  return createTaskDocumentWriteTool(this.store, taskId);
@@ -71699,7 +71963,7 @@ function isTickableState(state) {
71699
71963
  function isHeartbeatManaged(agent) {
71700
71964
  return !isEphemeralAgent(agent);
71701
71965
  }
71702
- var HEARTBEAT_SYSTEM_PROMPT, HEARTBEAT_NO_TASK_SYSTEM_PROMPT, HEARTBEAT_PROCEDURE, heartbeatDoneParams, HeartbeatMonitor, OVERDUE_FIRE_JITTER_MS, HeartbeatTriggerScheduler;
71966
+ var HEARTBEAT_SYSTEM_PROMPT, HEARTBEAT_NO_TASK_SYSTEM_PROMPT, HEARTBEAT_PROCEDURE, HEARTBEAT_NO_TASK_PROCEDURE, heartbeatDoneParams, HeartbeatMonitor, OVERDUE_FIRE_JITTER_MS, HeartbeatTriggerScheduler;
71703
71967
  var init_agent_heartbeat = __esm({
71704
71968
  "../engine/src/agent-heartbeat.ts"() {
71705
71969
  "use strict";
@@ -71889,6 +72153,32 @@ When sending messages:
71889
72153
  Critical: a heartbeat without observable progress (a log, a document write, a
71890
72154
  status change, a comment, a delegation, or an explicit "no-op with reason") is
71891
72155
  a bug. Do not loop on the same plan across heartbeats without recording why.`;
72156
+ HEARTBEAT_NO_TASK_PROCEDURE = `## Heartbeat Procedure (run every tick, in order)
72157
+
72158
+ 1. **Identity & context** \u2014 review the **Identity Snapshot** at the top of
72159
+ this prompt. Confirm your role, soul, instructions, and memory match what
72160
+ you expect, and surface any anomalies in your first text output before
72161
+ doing anything else. (If fn_identity is available in your runtime you may
72162
+ also call it for full structured detail; the snapshot above is the
72163
+ authoritative source.)
72164
+ 2. **Inbox** \u2014 when fn_read_messages is available, call it. Process any pending
72165
+ messages first; reply with reply_to_message_id when answering.
72166
+ 3. **Wake delta** \u2014 read the Wake Delta block above. The wake reason is the
72167
+ highest-priority change for this heartbeat. If you were woken by a comment
72168
+ or a message, acknowledge it before doing anything else.
72169
+ 4. **Ambient review** \u2014 since you have no assigned task, review board/project
72170
+ signals and recent memory context before acting.
72171
+ 5. **Pick the next concrete action** \u2014 exactly ONE useful action this heartbeat:
72172
+ create a focused task, delegate work, send/reply to a message, or append
72173
+ durable memory.
72174
+ 6. **Persist progress** \u2014 use available ambient tools only:
72175
+ fn_task_create, fn_delegate_task, fn_send_message, fn_memory_append.
72176
+ 7. **Exit** \u2014 call fn_heartbeat_done with a one-line summary of what changed
72177
+ this tick. If you took no action, say so and explain why.
72178
+
72179
+ Critical: a heartbeat without observable progress (a created task, delegation,
72180
+ message reply, memory append, or explicit "no-op with reason") is a bug. Do
72181
+ not loop on the same plan across heartbeats without recording why.`;
71892
72182
  heartbeatDoneParams = Type6.Object({
71893
72183
  summary: Type6.Optional(Type6.String({ description: "Summary of what was accomplished this heartbeat" }))
71894
72184
  });
@@ -71928,13 +72218,6 @@ a bug. Do not loop on the same plan across heartbeats without recording why.`;
71928
72218
  this.rootDir = options.rootDir;
71929
72219
  this.messageStore = options.messageStore;
71930
72220
  this.pluginRunner = options.pluginRunner;
71931
- this.onRecovered = options.onRecovered;
71932
- this.onTerminated = options.onTerminated;
71933
- this.onRunStarted = options.onRunStarted;
71934
- this.onRunCompleted = options.onRunCompleted;
71935
- this.taskStore = options.taskStore;
71936
- this.rootDir = options.rootDir;
71937
- this.messageStore = options.messageStore;
71938
72221
  }
71939
72222
  /**
71940
72223
  * Start the heartbeat monitoring loop.
@@ -72633,9 +72916,9 @@ a bug. Do not loop on the same plan across heartbeats without recording why.`;
72633
72916
  sourceType: "agent_heartbeat",
72634
72917
  sourceAgentId: agentId,
72635
72918
  sourceRunId: runContext?.runId
72636
- }));
72919
+ }, { rootDir: this.rootDir }));
72637
72920
  heartbeatTools.push(createListAgentsTool(this.store));
72638
- heartbeatTools.push(createDelegateTaskTool(this.store, taskStore));
72921
+ heartbeatTools.push(createDelegateTaskTool(this.store, taskStore, { rootDir: this.rootDir }));
72639
72922
  if (this.messageStore) {
72640
72923
  heartbeatTools.push(createSendMessageTool(this.messageStore, agentId));
72641
72924
  heartbeatTools.push(createReadMessagesTool(this.messageStore, agentId));
@@ -72658,22 +72941,27 @@ a bug. Do not loop on the same plan across heartbeats without recording why.`;
72658
72941
  heartbeatLog.warn(`Failed to configure heartbeat memory tools for ${agentId}: ${message}`);
72659
72942
  }
72660
72943
  const skillContext = buildSessionSkillContextSync2(agent, "heartbeat", rootDir);
72661
- let systemPrompt = isNoTaskRun ? HEARTBEAT_NO_TASK_SYSTEM_PROMPT : HEARTBEAT_SYSTEM_PROMPT;
72662
- const baseHeartbeatSystemPrompt = systemPrompt;
72944
+ const baseHeartbeatSystemPrompt = isNoTaskRun ? HEARTBEAT_NO_TASK_SYSTEM_PROMPT : HEARTBEAT_SYSTEM_PROMPT;
72663
72945
  let resolvedInstructionsForIdentity = "";
72664
72946
  try {
72665
- const agentInstructions = await resolveAgentInstructionsWithRatings(agent, rootDir, this.store);
72666
- resolvedInstructionsForIdentity = agentInstructions;
72667
- const memoryInstructions = memorySettings?.memoryEnabled === false ? "" : buildExecutionMemoryInstructions(rootDir, memorySettings);
72668
- systemPrompt = buildSystemPromptWithInstructions(
72669
- baseHeartbeatSystemPrompt,
72670
- [agentInstructions, memoryInstructions].filter((part) => part.trim()).join("\n\n")
72671
- );
72947
+ resolvedInstructionsForIdentity = await resolveAgentInstructionsWithRatings(agent, rootDir, this.store);
72672
72948
  } catch (instructionError) {
72673
- systemPrompt = baseHeartbeatSystemPrompt;
72674
72949
  const message = instructionError instanceof Error ? instructionError.message : String(instructionError);
72675
- heartbeatLog.warn(`Failed to enrich heartbeat system prompt for ${agentId}: ${message}`);
72950
+ heartbeatLog.warn(`Failed to resolve agent instructions for heartbeat ${agentId}: ${message}`);
72951
+ }
72952
+ let memoryInstructions = "";
72953
+ if (memorySettings?.memoryEnabled !== false) {
72954
+ try {
72955
+ memoryInstructions = buildExecutionMemoryInstructions(rootDir, memorySettings);
72956
+ } catch (memoryInstructionErr) {
72957
+ const message = memoryInstructionErr instanceof Error ? memoryInstructionErr.message : String(memoryInstructionErr);
72958
+ heartbeatLog.warn(`Failed to resolve project memory instructions for heartbeat ${agentId}: ${message}`);
72959
+ }
72676
72960
  }
72961
+ const systemPrompt = buildSystemPromptWithInstructions(
72962
+ baseHeartbeatSystemPrompt,
72963
+ [resolvedInstructionsForIdentity, memoryInstructions].filter((part) => part.trim()).join("\n\n")
72964
+ );
72677
72965
  heartbeatTools.push(createIdentityTool({ agent, resolvedInstructions: resolvedInstructionsForIdentity }));
72678
72966
  heartbeatTools.push(heartbeatDoneTool);
72679
72967
  if (isNoTaskRun) {
@@ -72734,7 +73022,7 @@ a bug. Do not loop on the same plan across heartbeats without recording why.`;
72734
73022
  };
72735
73023
  const wakeReason = deriveWakeReason();
72736
73024
  const customProcedure = await resolveAgentHeartbeatProcedure(agent, rootDir);
72737
- const heartbeatProcedureText = customProcedure ?? HEARTBEAT_PROCEDURE;
73025
+ const heartbeatProcedureText = customProcedure ?? (isNoTaskRun ? HEARTBEAT_NO_TASK_PROCEDURE : HEARTBEAT_PROCEDURE);
72738
73026
  if (isNoTaskRun) {
72739
73027
  if (this.messageStore) {
72740
73028
  try {
@@ -72767,6 +73055,8 @@ a bug. Do not loop on the same plan across heartbeats without recording why.`;
72767
73055
  `- pending messages: ${pendingMessages.length}`,
72768
73056
  "",
72769
73057
  "Treat this wake delta as the highest-priority change for this heartbeat.",
73058
+ "This is an autonomous heartbeat run (manual or automatic): re-anchor on",
73059
+ "identity, process wake context, then complete ONE concrete action.",
72770
73060
  "Run the Heartbeat Procedure (below) before doing anything else \u2014 even a",
72771
73061
  "timer-only wake should re-check messages, memory, and project state.",
72772
73062
  "",
@@ -72858,6 +73148,8 @@ a bug. Do not loop on the same plan across heartbeats without recording why.`;
72858
73148
  `- triggering comments: ${effectiveTriggeringCommentIds?.length ?? 0}`,
72859
73149
  "",
72860
73150
  "Treat this wake delta as the highest-priority change for this heartbeat.",
73151
+ "This is an autonomous heartbeat run (manual or automatic): re-anchor on",
73152
+ "identity, process wake context, then complete ONE concrete action.",
72861
73153
  "Before resuming prior task work, run the Heartbeat Procedure (below) and",
72862
73154
  "decide what action this delta requires. Your assigned task is one input",
72863
73155
  "to the procedure \u2014 not the only thing to consider.",
@@ -73017,7 +73309,7 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
73017
73309
  const baseCreateTool = createTaskCreateTool(taskStore, {
73018
73310
  sourceType: "agent_heartbeat",
73019
73311
  sourceAgentId: agentId
73020
- });
73312
+ }, { rootDir: this.rootDir });
73021
73313
  const trackedCreateTool = {
73022
73314
  ...baseCreateTool,
73023
73315
  execute: async (id, params, signal, onUpdate, ctx) => {
@@ -73044,7 +73336,7 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
73044
73336
  tools.push(createTaskDocumentWriteTool(taskStore, taskId));
73045
73337
  tools.push(createTaskDocumentReadTool(taskStore, taskId));
73046
73338
  tools.push(createListAgentsTool(this.store));
73047
- tools.push(createDelegateTaskTool(this.store, taskStore));
73339
+ tools.push(createDelegateTaskTool(this.store, taskStore, { rootDir: this.rootDir }));
73048
73340
  if (messageStore) {
73049
73341
  tools.push(createSendMessageTool(messageStore, agentId));
73050
73342
  tools.push(createReadMessagesTool(messageStore, agentId));
@@ -80746,7 +81038,7 @@ var init_provider_adapters = __esm({
80746
81038
 
80747
81039
  // ../engine/src/remote-access/tunnel-process-manager.ts
80748
81040
  import { EventEmitter as EventEmitter23 } from "node:events";
80749
- import { exec as exec9, execFile as execFile3, spawn as spawn4 } from "node:child_process";
81041
+ import { exec as exec9, execFile as execFile3, spawn as spawn5 } from "node:child_process";
80750
81042
  import { promisify as promisify9 } from "node:util";
80751
81043
  function nowIso() {
80752
81044
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -80837,7 +81129,7 @@ var init_tunnel_process_manager = __esm({
80837
81129
  super();
80838
81130
  this.maxLogEntries = options.maxLogEntries ?? DEFAULT_MAX_LOG_ENTRIES;
80839
81131
  this.defaultStopTimeoutMs = options.stopTimeoutMs ?? DEFAULT_STOP_TIMEOUT_MS2;
80840
- this.spawnImpl = options.spawnImpl ?? spawn4;
81132
+ this.spawnImpl = options.spawnImpl ?? spawn5;
80841
81133
  }
80842
81134
  getStatus() {
80843
81135
  return { ...this.status, lastError: this.status.lastError ? { ...this.status.lastError } : null };
@@ -81211,6 +81503,7 @@ var execFileAsync2, MERGE_HANDOFF_GRACE_MS, isRemoteActive, ProjectEngine;
81211
81503
  var init_project_engine = __esm({
81212
81504
  "../engine/src/project-engine.ts"() {
81213
81505
  "use strict";
81506
+ init_src();
81214
81507
  init_in_process_runtime();
81215
81508
  init_pr_monitor();
81216
81509
  init_pr_comment_handler();
@@ -81905,6 +82198,48 @@ ${detail}`
81905
82198
  if (task.status === "failed") return false;
81906
82199
  return (task.mergeRetries ?? 0) < _ProjectEngine.MAX_AUTO_MERGE_RETRIES || this.hasAutoHealableVerificationBufferFailure(task) || this.isRetryCooldownElapsed(task);
81907
82200
  }
82201
+ /**
82202
+ * Remove and return the highest-priority taskId from the merge queue.
82203
+ * Ordering: priority (urgent→low), then createdAt ASC, then id ASC — matching
82204
+ * the triage and scheduler comparators. Manual merges (onMerge resolvers) are
82205
+ * preferred over auto-merges so awaited callers aren't starved by a flood of
82206
+ * higher-priority auto-enqueues. IDs whose tasks can't be loaded fall back to
82207
+ * FIFO order so they still drain.
82208
+ */
82209
+ async pickNextMergeTaskId(store) {
82210
+ if (this.mergeQueue.length === 0) return void 0;
82211
+ if (this.mergeQueue.length === 1) {
82212
+ return this.mergeQueue.shift();
82213
+ }
82214
+ const queueSnapshot = [...this.mergeQueue];
82215
+ const entries = [];
82216
+ for (let i = 0; i < queueSnapshot.length; i++) {
82217
+ const taskId = queueSnapshot[i];
82218
+ const task = await store.getTask(taskId).catch(() => void 0);
82219
+ entries.push({
82220
+ taskId,
82221
+ task,
82222
+ manual: this.manualMergeResolvers.has(taskId),
82223
+ order: i
82224
+ });
82225
+ }
82226
+ if (this.shuttingDown) return void 0;
82227
+ entries.sort((a, b) => {
82228
+ if (a.manual !== b.manual) return a.manual ? -1 : 1;
82229
+ if (a.task && b.task) return compareTasksByPriorityThenAgeAndId(a.task, b.task);
82230
+ if (a.task) return -1;
82231
+ if (b.task) return 1;
82232
+ return a.order - b.order;
82233
+ });
82234
+ for (const entry of entries) {
82235
+ const liveIndex = this.mergeQueue.indexOf(entry.taskId);
82236
+ if (liveIndex !== -1) {
82237
+ this.mergeQueue.splice(liveIndex, 1);
82238
+ return entry.taskId;
82239
+ }
82240
+ }
82241
+ return void 0;
82242
+ }
81908
82243
  internalEnqueueMerge(taskId) {
81909
82244
  if (this.shuttingDown) return;
81910
82245
  if (this.mergeActive.has(taskId)) return;
@@ -81912,6 +82247,23 @@ ${detail}`
81912
82247
  this.mergeQueue.push(taskId);
81913
82248
  void this.drainMergeQueue();
81914
82249
  }
82250
+ /**
82251
+ * Filter a sweep's listTasks() result to merge-eligible tasks, sort by
82252
+ * priority (urgent → low, then createdAt ASC, then id ASC), and enqueue.
82253
+ * Sorting before enqueue matters because each enqueue may immediately
82254
+ * trigger drainMergeQueue's single-item fast path, so the first task
82255
+ * pushed wins. listTasks returns createdAt ASC — without this sort an
82256
+ * older low-priority task would start before a later urgent one.
82257
+ */
82258
+ enqueueEligibleInReviewTasks(tasks) {
82259
+ const eligible = sortTasksByPriorityThenAgeAndId(
82260
+ tasks.filter((t) => !t.paused && this.canMergeTask(t))
82261
+ );
82262
+ for (const t of eligible) {
82263
+ this.internalEnqueueMerge(t.id);
82264
+ }
82265
+ return eligible.length;
82266
+ }
81915
82267
  async drainMergeQueue() {
81916
82268
  if (this.mergeRunning) return;
81917
82269
  this.mergeRunning = true;
@@ -81919,7 +82271,9 @@ ${detail}`
81919
82271
  const store = this.runtime.getTaskStore();
81920
82272
  const cwd = this.config.workingDirectory;
81921
82273
  while (this.mergeQueue.length > 0 && !this.shuttingDown) {
81922
- const taskId = this.mergeQueue.shift();
82274
+ const taskId = await this.pickNextMergeTaskId(store);
82275
+ if (!taskId) break;
82276
+ if (this.shuttingDown) break;
81923
82277
  const manualResolver = this.manualMergeResolvers.get(taskId);
81924
82278
  try {
81925
82279
  if (!manualResolver) {
@@ -82389,12 +82743,9 @@ ${detail}`
82389
82743
  }
82390
82744
  const settings = await store.getSettings();
82391
82745
  if (!settings.autoMerge) return;
82392
- const eligible = tasks.filter((t) => !t.paused && this.canMergeTask(t));
82393
- if (eligible.length > 0) {
82394
- runtimeLog.log(`Auto-merge startup sweep: enqueueing ${eligible.length} task(s)`);
82395
- for (const t of eligible) {
82396
- this.internalEnqueueMerge(t.id);
82397
- }
82746
+ const enqueued = this.enqueueEligibleInReviewTasks(tasks);
82747
+ if (enqueued > 0) {
82748
+ runtimeLog.log(`Auto-merge startup sweep: enqueueing ${enqueued} task(s)`);
82398
82749
  }
82399
82750
  } catch (err) {
82400
82751
  runtimeLog.warn(
@@ -82410,14 +82761,7 @@ ${detail}`
82410
82761
  const settings = await store.getSettings();
82411
82762
  if (!settings.globalPause && !settings.enginePaused && settings.autoMerge) {
82412
82763
  const tasks = await store.listTasks({ column: "in-review" });
82413
- for (const t of tasks) {
82414
- if (t.paused) {
82415
- continue;
82416
- }
82417
- if (this.canMergeTask(t)) {
82418
- this.internalEnqueueMerge(t.id);
82419
- }
82420
- }
82764
+ this.enqueueEligibleInReviewTasks(tasks);
82421
82765
  }
82422
82766
  } catch (err) {
82423
82767
  runtimeLog.warn(
@@ -82473,14 +82817,7 @@ ${detail}`
82473
82817
  if (s.autoMerge) {
82474
82818
  try {
82475
82819
  const tasks = await store.listTasks({ column: "in-review" });
82476
- for (const t of tasks) {
82477
- if (t.paused) {
82478
- continue;
82479
- }
82480
- if (this.canMergeTask(t)) {
82481
- this.internalEnqueueMerge(t.id);
82482
- }
82483
- }
82820
+ this.enqueueEligibleInReviewTasks(tasks);
82484
82821
  } catch (err) {
82485
82822
  runtimeLog.warn(
82486
82823
  `Global unpause: failed to scan in-review tasks for auto-merge: ${err instanceof Error ? err.message : String(err)}`
@@ -82510,14 +82847,7 @@ ${detail}`
82510
82847
  if (s.autoMerge) {
82511
82848
  try {
82512
82849
  const tasks = await store.listTasks({ column: "in-review" });
82513
- for (const t of tasks) {
82514
- if (t.paused) {
82515
- continue;
82516
- }
82517
- if (this.canMergeTask(t)) {
82518
- this.internalEnqueueMerge(t.id);
82519
- }
82520
- }
82850
+ this.enqueueEligibleInReviewTasks(tasks);
82521
82851
  } catch (err) {
82522
82852
  runtimeLog.warn(
82523
82853
  `Engine unpause: failed to scan in-review tasks for auto-merge: ${err instanceof Error ? err.message : String(err)}`
@@ -83110,7 +83440,7 @@ var init_peer_exchange_service = __esm({
83110
83440
  syncIntervalMs;
83111
83441
  interval = null;
83112
83442
  activeSync = null;
83113
- stopped = false;
83443
+ running = false;
83114
83444
  /** Whether settings sync is enabled. Default: false. */
83115
83445
  settingsSyncEnabled;
83116
83446
  /** Minimum interval between settings syncs with the same node in ms. Default: 5 minutes. */
@@ -83152,10 +83482,11 @@ var init_peer_exchange_service = __esm({
83152
83482
  * Begins periodic gossip with all online remote nodes.
83153
83483
  */
83154
83484
  start() {
83155
- if (this.stopped) {
83156
- peerExchangeLog.warn("Cannot start - service has been stopped");
83485
+ if (this.running) {
83486
+ peerExchangeLog.log("Peer exchange service already running");
83157
83487
  return;
83158
83488
  }
83489
+ this.running = true;
83159
83490
  this.centralCore.listNodes().then((nodes) => {
83160
83491
  const onlineRemoteCount = nodes.filter(
83161
83492
  (n) => n.type === "remote" && n.status === "online" && n.url
@@ -83165,6 +83496,7 @@ var init_peer_exchange_service = __esm({
83165
83496
  peerExchangeLog.warn(`Failed to get initial peer count: ${err}`);
83166
83497
  });
83167
83498
  this.interval = setInterval(() => {
83499
+ if (!this.running) return;
83168
83500
  void this.syncWithAllPeers();
83169
83501
  }, this.syncIntervalMs);
83170
83502
  }
@@ -83172,12 +83504,21 @@ var init_peer_exchange_service = __esm({
83172
83504
  * Stop the peer exchange service.
83173
83505
  * Clears the sync interval and prevents further syncs.
83174
83506
  */
83175
- stop() {
83507
+ async stop() {
83508
+ if (!this.running) {
83509
+ return;
83510
+ }
83511
+ this.running = false;
83176
83512
  if (this.interval) {
83177
83513
  clearInterval(this.interval);
83178
83514
  this.interval = null;
83179
83515
  }
83180
- this.stopped = true;
83516
+ if (this.activeSync) {
83517
+ try {
83518
+ await this.activeSync;
83519
+ } catch {
83520
+ }
83521
+ }
83181
83522
  peerExchangeLog.log("Stopped peer exchange service");
83182
83523
  }
83183
83524
  /**
@@ -84597,7 +84938,7 @@ For completion:
84597
84938
  }`;
84598
84939
  SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
84599
84940
  CLEANUP_INTERVAL_MS2 = 5 * 60 * 1e3;
84600
- MAX_SESSIONS_PER_IP_PER_HOUR = 5;
84941
+ MAX_SESSIONS_PER_IP_PER_HOUR = 1e3;
84601
84942
  RATE_LIMIT_WINDOW_MS2 = 60 * 60 * 1e3;
84602
84943
  GENERATION_TIMEOUT_MS = 12e4;
84603
84944
  sessions = /* @__PURE__ */ new Map();
@@ -85220,7 +85561,7 @@ var init_src3 = __esm({
85220
85561
  });
85221
85562
 
85222
85563
  // ../../plugins/fusion-plugin-hermes-runtime/dist/cli-spawn.js
85223
- import { spawn as spawn5, spawnSync } from "node:child_process";
85564
+ import { spawn as spawn6, spawnSync } from "node:child_process";
85224
85565
  import os2 from "node:os";
85225
85566
  import path, { sep as PATH_SEP } from "node:path";
85226
85567
  function resolveBinaryForSpawn(binary) {
@@ -85330,7 +85671,7 @@ async function invokeHermesCli(prompt, settings, resumeSessionId, signal) {
85330
85671
  if (settings.profile) {
85331
85672
  spawnEnv.HERMES_HOME = hermesProfileHome(settings.profile);
85332
85673
  }
85333
- const child = spawn5(binary, args, {
85674
+ const child = spawn6(binary, args, {
85334
85675
  stdio: ["ignore", "pipe", "pipe"],
85335
85676
  env: spawnEnv
85336
85677
  });
@@ -85543,7 +85884,7 @@ var init_dist = __esm({
85543
85884
  });
85544
85885
 
85545
85886
  // ../../plugins/fusion-plugin-openclaw-runtime/dist/pi-module.js
85546
- import { spawn as spawn6 } from "node:child_process";
85887
+ import { spawn as spawn7 } from "node:child_process";
85547
85888
  import { randomUUID as randomUUID12 } from "node:crypto";
85548
85889
  function asString(v) {
85549
85890
  return typeof v === "string" && v.trim() !== "" ? v.trim() : void 0;
@@ -85620,7 +85961,7 @@ async function promptCli(session, message, config, callbacks, signal) {
85620
85961
  cb.onToolStart?.("openclaw.agent", { sessionId: session.sessionId });
85621
85962
  return new Promise((resolve19, reject) => {
85622
85963
  let settled = false;
85623
- const child = spawn6(config.binaryPath, args, {
85964
+ const child = spawn7(config.binaryPath, args, {
85624
85965
  stdio: ["ignore", "pipe", "pipe"]
85625
85966
  });
85626
85967
  const hardKill = setTimeout(() => {
@@ -85771,7 +86112,7 @@ var init_runtime_adapter2 = __esm({
85771
86112
  });
85772
86113
 
85773
86114
  // ../../plugins/fusion-plugin-openclaw-runtime/dist/probe.js
85774
- import { spawn as spawn7 } from "node:child_process";
86115
+ import { spawn as spawn8 } from "node:child_process";
85775
86116
  async function probeOpenClawBinary(opts = {}) {
85776
86117
  const startedAt = Date.now();
85777
86118
  const binary = opts.binaryPath ?? "openclaw";
@@ -85782,7 +86123,7 @@ async function probeOpenClawBinary(opts = {}) {
85782
86123
  resolvePromise({ ...partial, probeDurationMs: Date.now() - startedAt });
85783
86124
  };
85784
86125
  let settled = false;
85785
- const child = spawn7(resolvedPath ?? binary, ["--version"], {
86126
+ const child = spawn8(resolvedPath ?? binary, ["--version"], {
85786
86127
  stdio: ["ignore", "pipe", "pipe"]
85787
86128
  });
85788
86129
  const timer = setTimeout(() => {
@@ -85843,7 +86184,7 @@ async function probeOpenClawBinary(opts = {}) {
85843
86184
  async function tryResolveBinaryPath(binary) {
85844
86185
  return new Promise((resolvePromise) => {
85845
86186
  const which = process.platform === "win32" ? "where" : "which";
85846
- const child = spawn7(which, [binary], { stdio: ["ignore", "pipe", "ignore"] });
86187
+ const child = spawn8(which, [binary], { stdio: ["ignore", "pipe", "ignore"] });
85847
86188
  let out = "";
85848
86189
  child.stdout?.on("data", (chunk) => {
85849
86190
  out += chunk.toString("utf-8");
@@ -92432,7 +92773,7 @@ var init_register_git_github = __esm({
92432
92773
  });
92433
92774
 
92434
92775
  // ../dashboard/src/terminal.ts
92435
- import { spawn as spawn8 } from "node:child_process";
92776
+ import { spawn as spawn9 } from "node:child_process";
92436
92777
  import { randomUUID as randomUUID13 } from "node:crypto";
92437
92778
  import { EventEmitter as EventEmitter29 } from "node:events";
92438
92779
  function extractBaseCommand(command) {
@@ -92594,7 +92935,7 @@ var init_terminal = __esm({
92594
92935
  return { sessionId: "", error: validation.error };
92595
92936
  }
92596
92937
  const sessionId = randomUUID13();
92597
- const childProcess = spawn8(command, [], {
92938
+ const childProcess = spawn9(command, [], {
92598
92939
  cwd,
92599
92940
  shell: true,
92600
92941
  stdio: ["pipe", "pipe", "pipe"],
@@ -93318,7 +93659,7 @@ function remapSpawnError(err, bin) {
93318
93659
  return err instanceof Error ? err : new Error(String(err));
93319
93660
  }
93320
93661
  async function spawnPaperclipCliJson(args, opts) {
93321
- const { spawn: spawn11 } = await import("node:child_process");
93662
+ const { spawn: spawn12 } = await import("node:child_process");
93322
93663
  const bin = opts.cliBinaryPath ?? "paperclipai";
93323
93664
  const fullArgs = [...args, "--json"];
93324
93665
  if (opts.cliConfigPath) {
@@ -93329,7 +93670,7 @@ async function spawnPaperclipCliJson(args, opts) {
93329
93670
  return new Promise((resolve19, reject) => {
93330
93671
  let child;
93331
93672
  try {
93332
- child = spawn11(bin, fullArgs, { stdio: ["ignore", "pipe", "pipe"] });
93673
+ child = spawn12(bin, fullArgs, { stdio: ["ignore", "pipe", "pipe"] });
93333
93674
  } catch (err) {
93334
93675
  reject(remapSpawnError(err, bin));
93335
93676
  return;
@@ -93841,6 +94182,25 @@ var init_register_runtime_provider_routes = __esm({
93841
94182
  }
93842
94183
  });
93843
94184
 
94185
+ // ../dashboard/src/cli-package-version.ts
94186
+ var init_cli_package_version = __esm({
94187
+ "../dashboard/src/cli-package-version.ts"() {
94188
+ "use strict";
94189
+ }
94190
+ });
94191
+
94192
+ // ../dashboard/src/routes/register-fn-binary-routes.ts
94193
+ var MAX_OUTPUT_BYTES2;
94194
+ var init_register_fn_binary_routes = __esm({
94195
+ "../dashboard/src/routes/register-fn-binary-routes.ts"() {
94196
+ "use strict";
94197
+ init_src();
94198
+ init_api_error();
94199
+ init_cli_package_version();
94200
+ MAX_OUTPUT_BYTES2 = 64 * 1024;
94201
+ }
94202
+ });
94203
+
93844
94204
  // ../dashboard/src/update-check.ts
93845
94205
  var DAY_MS;
93846
94206
  var init_update_check = __esm({
@@ -93850,13 +94210,6 @@ var init_update_check = __esm({
93850
94210
  }
93851
94211
  });
93852
94212
 
93853
- // ../dashboard/src/cli-package-version.ts
93854
- var init_cli_package_version = __esm({
93855
- "../dashboard/src/cli-package-version.ts"() {
93856
- "use strict";
93857
- }
93858
- });
93859
-
93860
94213
  // ../dashboard/src/routes/register-update-check-routes.ts
93861
94214
  var init_register_update_check_routes = __esm({
93862
94215
  "../dashboard/src/routes/register-update-check-routes.ts"() {
@@ -94042,6 +94395,7 @@ var init_routes = __esm({
94042
94395
  init_register_usage_routes();
94043
94396
  init_register_auth_routes();
94044
94397
  init_register_runtime_provider_routes();
94398
+ init_register_fn_binary_routes();
94045
94399
  init_register_update_check_routes();
94046
94400
  init_register_integrated_routers();
94047
94401
  init_resolve_diff_base();
@@ -99369,7 +99723,7 @@ async function runTaskPlan(initialPlanArg, yesFlag = false, projectName) {
99369
99723
  } catch (err) {
99370
99724
  clearThinking();
99371
99725
  if (err instanceof RateLimitError2) {
99372
- console.error("\n Rate limit exceeded. Maximum 5 planning sessions per hour.\n");
99726
+ console.error("\n Rate limit exceeded. Maximum 1000 planning sessions per hour.\n");
99373
99727
  process.exit(1);
99374
99728
  }
99375
99729
  console.error(`
@@ -99526,7 +99880,7 @@ __export(skills_exports, {
99526
99880
  runSkillsSearch: () => runSkillsSearch,
99527
99881
  searchSkills: () => searchSkills
99528
99882
  });
99529
- import { spawn as spawn9 } from "node:child_process";
99883
+ import { spawn as spawn10 } from "node:child_process";
99530
99884
  async function searchSkills(query, limit = 10) {
99531
99885
  const url = `${SKILLS_API_BASE}/api/search?q=${encodeURIComponent(query)}&limit=${limit}`;
99532
99886
  try {
@@ -99604,7 +99958,7 @@ async function runSkillsInstall(args, options) {
99604
99958
  npxArgs.push("--skill", options.skill);
99605
99959
  }
99606
99960
  npxArgs.push("-y", "-a", "pi");
99607
- const child = spawn9("npx", npxArgs, {
99961
+ const child = spawn10("npx", npxArgs, {
99608
99962
  cwd: process.cwd(),
99609
99963
  stdio: "inherit",
99610
99964
  shell: true
@@ -99641,7 +99995,7 @@ import { StringEnum } from "@mariozechner/pi-ai";
99641
99995
  import { resolve as resolve18, basename as basename11, extname as extname3, join as join41 } from "node:path";
99642
99996
  import { readFile as readFile18 } from "node:fs/promises";
99643
99997
  import { existsSync as existsSync31 } from "node:fs";
99644
- import { spawn as spawn10 } from "node:child_process";
99998
+ import { spawn as spawn11 } from "node:child_process";
99645
99999
  var MIME_TYPES2 = {
99646
100000
  ".png": "image/png",
99647
100001
  ".jpg": "image/jpeg",
@@ -101450,7 +101804,7 @@ Status: ${updated.status}`
101450
101804
  npxArgs.push("--skill", params.skill);
101451
101805
  }
101452
101806
  npxArgs.push("-y", "-a", "pi");
101453
- const child = spawn10("npx", npxArgs, {
101807
+ const child = spawn11("npx", npxArgs, {
101454
101808
  cwd: resolveProjectRoot(ctx.cwd),
101455
101809
  stdio: "pipe",
101456
101810
  shell: true
@@ -101532,7 +101886,7 @@ Status: ${updated.status}`
101532
101886
  return;
101533
101887
  }
101534
101888
  const port = trimmed ? parseInt(trimmed, 10) || 4040 : 4040;
101535
- const child = spawn10("fn", ["dashboard", "--port", String(port)], {
101889
+ const child = spawn11("fn", ["dashboard", "--port", String(port)], {
101536
101890
  cwd: resolveProjectRoot(ctx.cwd),
101537
101891
  stdio: ["ignore", "pipe", "pipe"],
101538
101892
  detached: false,