@runfusion/fusion 0.13.0 → 0.14.1

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 (72) hide show
  1. package/README.md +13 -0
  2. package/dist/bin.js +1332 -528
  3. package/dist/client/assets/AgentDetailView-B3KAsP2O.js +18 -0
  4. package/dist/client/assets/{AgentsView-Dvf_xUkx.js → AgentsView-DoXb_amw.js} +4 -4
  5. package/dist/client/assets/ChatView-BJ2c7wvd.js +1 -0
  6. package/dist/client/assets/{DevServerView-C2qTJch7.js → DevServerView-DbgM4tlT.js} +1 -1
  7. package/dist/client/assets/{DirectoryPicker-DRfhg9zz.js → DirectoryPicker-DfmtfMiu.js} +1 -1
  8. package/dist/client/assets/{DocumentsView-j8ic1xUw.js → DocumentsView-_-Efkx_W.js} +1 -1
  9. package/dist/client/assets/{InsightsView-CpAz3o0i.js → InsightsView-DUjcfW53.js} +1 -1
  10. package/dist/client/assets/{MemoryView-BcQsi_JK.js → MemoryView-DxMPBb0q.js} +1 -1
  11. package/dist/client/assets/{NodesView-Bo_Yhr4N.js → NodesView-BEBTI15s.js} +1 -1
  12. package/dist/client/assets/PiExtensionsManager-BpMYhHH_.js +11 -0
  13. package/dist/client/assets/PluginManager-CPv7yQd3.js +1 -0
  14. package/dist/client/assets/PluginManager-DA_T0GHn.css +1 -0
  15. package/dist/client/assets/{ResearchView-CLyyqAWE.js → ResearchView-BrFvdyXT.js} +1 -1
  16. package/dist/client/assets/{RoadmapsView-tG7IdOoc.js → RoadmapsView-BDjLrtcj.js} +1 -1
  17. package/dist/client/assets/SettingsModal-Cd-QGB0C.js +31 -0
  18. package/dist/client/assets/{SettingsModal-CXUGeZ0_.js → SettingsModal-CxDxiTRy.js} +1 -1
  19. package/dist/client/assets/SettingsModal-D_AFkDJa.css +1 -0
  20. package/dist/client/assets/{SetupWizardModal-BMJL6eNR.js → SetupWizardModal-DFUA4X3z.js} +1 -1
  21. package/dist/client/assets/{SkillMultiselect-ILMft-Kz.js → SkillMultiselect-BUWe5ujb.js} +1 -1
  22. package/dist/client/assets/{SkillsView-x4_YwBz6.js → SkillsView-RAkqGX3y.js} +1 -1
  23. package/dist/client/assets/TodoView-Ceb0wrg1.js +6 -0
  24. package/dist/client/assets/TodoView-SeO9o7km.css +1 -0
  25. package/dist/client/assets/{folder-open-DDdJt8aE.js → folder-open-DcM-Vd6r.js} +1 -1
  26. package/dist/client/assets/index-C1prPuSl.css +1 -0
  27. package/dist/client/assets/index-DH3aprf6.js +661 -0
  28. package/dist/client/assets/{list-checks-DFxQ9biT.js → list-checks-ByGHVQpZ.js} +1 -1
  29. package/dist/client/assets/{star-BKs1bgJN.js → star-DlEYI8GL.js} +1 -1
  30. package/dist/client/assets/{upload-Bb5Pidne.js → upload-DKshabz-.js} +1 -1
  31. package/dist/client/assets/{users-BImNn91Q.js → users-X6tYPPBV.js} +1 -1
  32. package/dist/client/index.html +2 -2
  33. package/dist/client/sw.js +6 -0
  34. package/dist/client/version.json +1 -1
  35. package/dist/droid-cli/index.ts +127 -0
  36. package/dist/droid-cli/package.json +37 -0
  37. package/dist/droid-cli/src/__tests__/control-handler.test.ts +164 -0
  38. package/dist/droid-cli/src/__tests__/event-bridge.test.ts +1318 -0
  39. package/dist/droid-cli/src/__tests__/mcp-config.test.ts +310 -0
  40. package/dist/droid-cli/src/__tests__/process-manager.test.ts +818 -0
  41. package/dist/droid-cli/src/__tests__/prompt-builder.test.ts +1206 -0
  42. package/dist/droid-cli/src/__tests__/provider.test.ts +1894 -0
  43. package/dist/droid-cli/src/__tests__/setup-test-isolation.test.ts +32 -0
  44. package/dist/droid-cli/src/__tests__/setup-test-isolation.ts +14 -0
  45. package/dist/droid-cli/src/__tests__/stream-parser.test.ts +188 -0
  46. package/dist/droid-cli/src/__tests__/thinking-config.test.ts +141 -0
  47. package/dist/droid-cli/src/__tests__/tool-mapping.test.ts +253 -0
  48. package/dist/droid-cli/src/control-handler.ts +82 -0
  49. package/dist/droid-cli/src/event-bridge.ts +397 -0
  50. package/dist/droid-cli/src/mcp-config.ts +144 -0
  51. package/dist/droid-cli/src/mcp-schema-server.cjs +49 -0
  52. package/dist/droid-cli/src/process-manager.ts +358 -0
  53. package/dist/droid-cli/src/prompt-builder.ts +629 -0
  54. package/dist/droid-cli/src/provider.ts +447 -0
  55. package/dist/droid-cli/src/stream-parser.ts +37 -0
  56. package/dist/droid-cli/src/thinking-config.ts +83 -0
  57. package/dist/droid-cli/src/tool-mapping.ts +147 -0
  58. package/dist/droid-cli/src/types.ts +87 -0
  59. package/dist/extension.js +555 -125
  60. package/dist/pi-claude-cli/package.json +1 -1
  61. package/package.json +2 -1
  62. package/dist/client/assets/AgentDetailView-B7j297GT.js +0 -18
  63. package/dist/client/assets/ChatView-BgUt38ty.js +0 -1
  64. package/dist/client/assets/PiExtensionsManager-DHt2zFg8.js +0 -11
  65. package/dist/client/assets/PluginManager-BQhBHWrB.js +0 -1
  66. package/dist/client/assets/PluginManager-jyNkJZSz.css +0 -1
  67. package/dist/client/assets/SettingsModal-9HS8MnmW.css +0 -1
  68. package/dist/client/assets/SettingsModal-UziTDnLh.js +0 -31
  69. package/dist/client/assets/TodoView-BBYcMbXE.js +0 -6
  70. package/dist/client/assets/TodoView-C1g65hJo.css +0 -1
  71. package/dist/client/assets/index-B15xwijw.css +0 -1
  72. 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") {
@@ -10238,7 +10371,7 @@ function validatePluginManifest(manifest) {
10238
10371
  const m = manifest;
10239
10372
  if (!m.id || typeof m.id !== "string" || m.id.trim() === "") {
10240
10373
  errors.push("id is required and must be a non-empty string");
10241
- } else if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(m.id)) {
10374
+ } else if (!SLUG_PATTERN.test(m.id)) {
10242
10375
  errors.push("id must be a valid slug (lowercase, alphanumeric, hyphens only, cannot start or end with hyphen)");
10243
10376
  }
10244
10377
  if (!m.name || typeof m.name !== "string" || m.name.trim() === "") {
@@ -10291,7 +10424,7 @@ function validatePluginManifest(manifest) {
10291
10424
  const runtime = m.runtime;
10292
10425
  if (!runtime.runtimeId || typeof runtime.runtimeId !== "string" || runtime.runtimeId.trim() === "") {
10293
10426
  errors.push("runtime.runtimeId is required and must be a non-empty string");
10294
- } else if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(runtime.runtimeId)) {
10427
+ } else if (!SLUG_PATTERN.test(runtime.runtimeId)) {
10295
10428
  errors.push("runtime.runtimeId must be a valid slug (lowercase, alphanumeric, hyphens only, cannot start or end with hyphen)");
10296
10429
  }
10297
10430
  if (!runtime.name || typeof runtime.name !== "string" || runtime.name.trim() === "") {
@@ -10306,14 +10439,93 @@ function validatePluginManifest(manifest) {
10306
10439
  }
10307
10440
  }
10308
10441
  }
10442
+ if (m.skills !== void 0) {
10443
+ if (!Array.isArray(m.skills)) {
10444
+ errors.push("skills must be an array");
10445
+ } else {
10446
+ for (const [index, skill] of m.skills.entries()) {
10447
+ if (!skill || typeof skill !== "object") {
10448
+ errors.push(`skills[${index}] must be an object`);
10449
+ continue;
10450
+ }
10451
+ const skillMeta = skill;
10452
+ if (!skillMeta.skillId || typeof skillMeta.skillId !== "string" || skillMeta.skillId.trim() === "") {
10453
+ errors.push(`skills[${index}].skillId is required and must be a non-empty string`);
10454
+ } else if (!SLUG_PATTERN.test(skillMeta.skillId)) {
10455
+ errors.push(`skills[${index}].skillId must be a valid slug (lowercase, alphanumeric, hyphens only, cannot start or end with hyphen)`);
10456
+ }
10457
+ if (!skillMeta.name || typeof skillMeta.name !== "string" || skillMeta.name.trim() === "") {
10458
+ errors.push(`skills[${index}].name is required and must be a non-empty string`);
10459
+ }
10460
+ }
10461
+ }
10462
+ }
10463
+ if (m.workflowSteps !== void 0) {
10464
+ if (!Array.isArray(m.workflowSteps)) {
10465
+ errors.push("workflowSteps must be an array");
10466
+ } else {
10467
+ for (const [index, step] of m.workflowSteps.entries()) {
10468
+ if (!step || typeof step !== "object") {
10469
+ errors.push(`workflowSteps[${index}] must be an object`);
10470
+ continue;
10471
+ }
10472
+ const stepMeta = step;
10473
+ if (!stepMeta.stepId || typeof stepMeta.stepId !== "string" || stepMeta.stepId.trim() === "") {
10474
+ errors.push(`workflowSteps[${index}].stepId is required and must be a non-empty string`);
10475
+ } else if (!SLUG_PATTERN.test(stepMeta.stepId)) {
10476
+ errors.push(`workflowSteps[${index}].stepId must be a valid slug (lowercase, alphanumeric, hyphens only, cannot start or end with hyphen)`);
10477
+ }
10478
+ if (!stepMeta.name || typeof stepMeta.name !== "string" || stepMeta.name.trim() === "") {
10479
+ errors.push(`workflowSteps[${index}].name is required and must be a non-empty string`);
10480
+ }
10481
+ if (stepMeta.mode !== void 0 && (typeof stepMeta.mode !== "string" || !["prompt", "script"].includes(stepMeta.mode))) {
10482
+ errors.push(`workflowSteps[${index}].mode must be one of: prompt, script`);
10483
+ }
10484
+ }
10485
+ }
10486
+ }
10487
+ if (m.promptSurfaces !== void 0) {
10488
+ if (!Array.isArray(m.promptSurfaces)) {
10489
+ errors.push("promptSurfaces must be an array");
10490
+ } else {
10491
+ for (const [index, surface] of m.promptSurfaces.entries()) {
10492
+ if (typeof surface !== "string" || !PROMPT_CONTRIBUTION_SURFACES.includes(surface)) {
10493
+ errors.push(`promptSurfaces[${index}] must be one of: ${PROMPT_CONTRIBUTION_SURFACES.join(", ")}`);
10494
+ }
10495
+ }
10496
+ }
10497
+ }
10498
+ if (m.setup !== void 0) {
10499
+ if (typeof m.setup !== "object" || m.setup === null) {
10500
+ errors.push("setup must be an object");
10501
+ } else {
10502
+ const setup = m.setup;
10503
+ if (!setup.binaryName || typeof setup.binaryName !== "string" || setup.binaryName.trim() === "") {
10504
+ errors.push("setup.binaryName is required and must be a non-empty string");
10505
+ }
10506
+ if (!setup.description || typeof setup.description !== "string" || setup.description.trim() === "") {
10507
+ errors.push("setup.description is required and must be a non-empty string");
10508
+ }
10509
+ if (setup.channel !== void 0 && (typeof setup.channel !== "string" || !SETUP_CHANNELS.includes(setup.channel))) {
10510
+ errors.push(`setup.channel must be one of: ${SETUP_CHANNELS.join(", ")}`);
10511
+ }
10512
+ if (setup.defaultTimeoutMs !== void 0 && (typeof setup.defaultTimeoutMs !== "number" || !Number.isFinite(setup.defaultTimeoutMs) || setup.defaultTimeoutMs <= 0)) {
10513
+ errors.push("setup.defaultTimeoutMs must be a positive finite number");
10514
+ }
10515
+ }
10516
+ }
10309
10517
  return {
10310
10518
  valid: errors.length === 0,
10311
10519
  errors
10312
10520
  };
10313
10521
  }
10522
+ var SLUG_PATTERN, PROMPT_CONTRIBUTION_SURFACES, SETUP_CHANNELS;
10314
10523
  var init_plugin_types = __esm({
10315
10524
  "../core/src/plugin-types.ts"() {
10316
10525
  "use strict";
10526
+ SLUG_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
10527
+ PROMPT_CONTRIBUTION_SURFACES = ["executor-system", "executor-task", "triage", "reviewer", "heartbeat"];
10528
+ SETUP_CHANNELS = ["stable", "beta", "nightly"];
10317
10529
  }
10318
10530
  });
10319
10531
 
@@ -35857,6 +36069,20 @@ function reconcileClaudeCliPaths(paths, vendoredPath) {
35857
36069
  }
35858
36070
  return filtered;
35859
36071
  }
36072
+ function isExternalDroidCliPath(p, vendoredPath) {
36073
+ if (vendoredPath && p === vendoredPath) return false;
36074
+ return /(^|[/\\])droid-cli([/\\]|$)/i.test(p);
36075
+ }
36076
+ function reconcileDroidCliPaths(paths, vendoredPath) {
36077
+ if (!vendoredPath) {
36078
+ return [...paths];
36079
+ }
36080
+ const filtered = paths.filter((p) => !isExternalDroidCliPath(p, vendoredPath));
36081
+ if (!filtered.includes(vendoredPath)) {
36082
+ return [vendoredPath, ...filtered];
36083
+ }
36084
+ return filtered;
36085
+ }
35860
36086
  function getDisplayPathWithinRoot(root, targetPath) {
35861
36087
  const usesWindowsPaths = /^[A-Za-z]:[\\/]/.test(root) || /^[A-Za-z]:[\\/]/.test(targetPath) || root.includes("\\") || targetPath.includes("\\");
35862
36088
  const pathApi = usesWindowsPaths ? win32 : { relative: relative2, isAbsolute: isAbsolute5, sep: sep4 };
@@ -36114,6 +36340,85 @@ var init_gh_cli = __esm({
36114
36340
  }
36115
36341
  });
36116
36342
 
36343
+ // ../core/src/fn-binary.ts
36344
+ import { spawn as spawn2 } from "node:child_process";
36345
+ import { platform as platform2 } from "node:os";
36346
+ function runProbe(command, args, timeoutMs) {
36347
+ return new Promise((resolve19) => {
36348
+ let stdout = "";
36349
+ let stderr = "";
36350
+ const child = spawn2(command, args, { stdio: ["ignore", "pipe", "pipe"], shell: false });
36351
+ const timer = setTimeout(() => {
36352
+ try {
36353
+ child.kill("SIGKILL");
36354
+ } catch {
36355
+ }
36356
+ }, timeoutMs);
36357
+ child.stdout?.on("data", (chunk) => {
36358
+ stdout += chunk.toString("utf8");
36359
+ });
36360
+ child.stderr?.on("data", (chunk) => {
36361
+ stderr += chunk.toString("utf8");
36362
+ });
36363
+ child.on("error", (err) => {
36364
+ clearTimeout(timer);
36365
+ resolve19({ exitCode: null, stdout, stderr: stderr || err.message });
36366
+ });
36367
+ child.on("close", (exitCode) => {
36368
+ clearTimeout(timer);
36369
+ resolve19({ exitCode, stdout, stderr });
36370
+ });
36371
+ });
36372
+ }
36373
+ async function whichBinary(name) {
36374
+ const isWindows = platform2() === "win32";
36375
+ const lookup = isWindows ? "where" : "which";
36376
+ const result = await runProbe(lookup, [name], 5e3);
36377
+ if (result.exitCode !== 0) return void 0;
36378
+ const firstLine = result.stdout.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
36379
+ return firstLine || void 0;
36380
+ }
36381
+ async function probeVersion(binary) {
36382
+ const result = await runProbe(binary, ["--version"], 1e4);
36383
+ if (result.exitCode !== 0) return void 0;
36384
+ const text = (result.stdout || result.stderr).trim();
36385
+ if (!text) return void 0;
36386
+ const match = text.match(/\d+\.\d+\.\d+(?:-[\w.]+)?/);
36387
+ return match ? match[0] : text.split(/\s+/)[0];
36388
+ }
36389
+ async function detectFnBinary() {
36390
+ for (const candidate of CANDIDATES) {
36391
+ try {
36392
+ const resolvedPath = await whichBinary(candidate);
36393
+ if (!resolvedPath) continue;
36394
+ const version = await probeVersion(candidate);
36395
+ return {
36396
+ installed: true,
36397
+ binary: candidate,
36398
+ path: resolvedPath,
36399
+ version,
36400
+ invocation: candidate
36401
+ };
36402
+ } catch {
36403
+ }
36404
+ }
36405
+ return {
36406
+ installed: false,
36407
+ invocation: FN_NPX_INVOCATION
36408
+ };
36409
+ }
36410
+ var FN_NPM_PACKAGE, FN_INSTALL_CURL, FN_INSTALL_NPM, FN_NPX_INVOCATION, CANDIDATES;
36411
+ var init_fn_binary = __esm({
36412
+ "../core/src/fn-binary.ts"() {
36413
+ "use strict";
36414
+ FN_NPM_PACKAGE = "runfusion.ai";
36415
+ FN_INSTALL_CURL = "curl -fsSL https://runfusion.ai/install.sh | sh";
36416
+ FN_INSTALL_NPM = `npm install -g ${FN_NPM_PACKAGE}`;
36417
+ FN_NPX_INVOCATION = `npx -y ${FN_NPM_PACKAGE}`;
36418
+ CANDIDATES = ["fn", "fusion"];
36419
+ }
36420
+ });
36421
+
36117
36422
  // ../core/src/settings-validation.ts
36118
36423
  function validateUnavailableNodePolicy(value) {
36119
36424
  if (value === void 0) {
@@ -50401,6 +50706,10 @@ __export(src_exports, {
50401
50706
  EXECUTION_MODES: () => EXECUTION_MODES,
50402
50707
  FEATURE_LOOP_STATES: () => FEATURE_LOOP_STATES,
50403
50708
  FEATURE_STATUSES: () => FEATURE_STATUSES,
50709
+ FN_INSTALL_CURL: () => FN_INSTALL_CURL,
50710
+ FN_INSTALL_NPM: () => FN_INSTALL_NPM,
50711
+ FN_NPM_PACKAGE: () => FN_NPM_PACKAGE,
50712
+ FN_NPX_INVOCATION: () => FN_NPX_INVOCATION,
50404
50713
  FileMemoryBackend: () => FileMemoryBackend,
50405
50714
  FirstRunDetector: () => FirstRunDetector,
50406
50715
  GLOBAL_SETTINGS_KEYS: () => GLOBAL_SETTINGS_KEYS,
@@ -50512,6 +50821,7 @@ __export(src_exports, {
50512
50821
  createInsightExtractionAutomation: () => createInsightExtractionAutomation,
50513
50822
  createMemoryDreamsAutomation: () => createMemoryDreamsAutomation,
50514
50823
  dailyMemoryPath: () => dailyMemoryPath,
50824
+ detectFnBinary: () => detectFnBinary,
50515
50825
  detectLegacyData: () => detectLegacyData,
50516
50826
  diffConfigSnapshots: () => diffConfigSnapshots,
50517
50827
  discoverPiExtensions: () => discoverPiExtensions,
@@ -50633,6 +50943,7 @@ __export(src_exports, {
50633
50943
  readProjectMemoryWithBackend: () => readProjectMemoryWithBackend,
50634
50944
  readWorkingMemory: () => readWorkingMemory,
50635
50945
  reconcileClaudeCliPaths: () => reconcileClaudeCliPaths,
50946
+ reconcileDroidCliPaths: () => reconcileDroidCliPaths,
50636
50947
  refreshQmdProjectMemoryIndex: () => refreshQmdProjectMemoryIndex,
50637
50948
  registerMemoryBackend: () => registerMemoryBackend,
50638
50949
  renderMemoryAuditMarkdown: () => renderMemoryAuditMarkdown,
@@ -50722,6 +51033,7 @@ var init_src = __esm({
50722
51033
  init_automation();
50723
51034
  init_automation_store();
50724
51035
  init_run_command();
51036
+ init_fn_binary();
50725
51037
  init_node_override_guard();
50726
51038
  init_settings_validation();
50727
51039
  init_routine();
@@ -52708,6 +53020,21 @@ function resolveVendoredClaudeCliEntry() {
52708
53020
  return null;
52709
53021
  }
52710
53022
  }
53023
+ function resolveVendoredDroidCliEntry() {
53024
+ try {
53025
+ const require_ = createRequire2(import.meta.url);
53026
+ const pkgJsonPath = require_.resolve("@fusion/droid-cli/package.json");
53027
+ const pkgJson = JSON.parse(readFileSync8(pkgJsonPath, "utf-8"));
53028
+ const extensions = pkgJson.pi?.extensions;
53029
+ if (!Array.isArray(extensions) || extensions.length === 0) return null;
53030
+ const entry = extensions[0];
53031
+ if (typeof entry !== "string" || entry.length === 0) return null;
53032
+ const path2 = resolve11(dirname8(pkgJsonPath), entry);
53033
+ return existsSync20(path2) ? path2 : null;
53034
+ } catch {
53035
+ return null;
53036
+ }
53037
+ }
52711
53038
  async function registerExtensionProviders(cwd, modelRegistry) {
52712
53039
  try {
52713
53040
  const agentDir = getPackageManagerAgentDir();
@@ -52723,8 +53050,13 @@ async function registerExtensionProviders(cwd, modelRegistry) {
52723
53050
  [...getEnabledPiExtensionPaths(cwd), ...packageExtensionPaths],
52724
53051
  vendoredClaudeCli
52725
53052
  );
52726
- const extensionsResult = await discoverAndLoadExtensions(
53053
+ const vendoredDroidCli = resolveVendoredDroidCliEntry();
53054
+ const doubleReconciledPaths = reconcileDroidCliPaths(
52727
53055
  reconciledPaths,
53056
+ vendoredDroidCli
53057
+ );
53058
+ const extensionsResult = await discoverAndLoadExtensions(
53059
+ doubleReconciledPaths,
52728
53060
  cwd,
52729
53061
  join25(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery")
52730
53062
  );
@@ -52968,11 +53300,8 @@ async function createFnAgent2(options) {
52968
53300
  const createSessionWithModel = async (modelOverride) => {
52969
53301
  const customToolList = [
52970
53302
  ...wrappedTools,
52971
- ...isReadonly ? [] : options.customTools ?? []
53303
+ ...options.customTools ?? []
52972
53304
  ];
52973
- if (isReadonly && (options.customTools?.length ?? 0) > 0) {
52974
- piLog.log(`readonly session \u2014 customTools (${options.customTools.length}) skipped`);
52975
- }
52976
53305
  if (options.beforeSpawnSession) {
52977
53306
  await options.beforeSpawnSession();
52978
53307
  }
@@ -54381,14 +54710,25 @@ async function getAgentMemoryWindow(rootDir, agentMemory, path2, startLine = 1,
54381
54710
  backend: "agent-memory"
54382
54711
  };
54383
54712
  }
54384
- function createTaskCreateTool(store, provenance) {
54713
+ async function createAgentTask(store, input, options) {
54714
+ const settings = typeof store.getSettings === "function" ? await store.getSettings() : {};
54715
+ const rootDir = options?.rootDir;
54716
+ return store.createTask(input, {
54717
+ settings: { autoSummarizeTitles: settings.autoSummarizeTitles === true },
54718
+ onSummarize: rootDir ? async (description) => {
54719
+ const resolved = resolveTitleSummarizerSettingsModel(settings);
54720
+ return summarizeTitle(description, rootDir, resolved.provider, resolved.modelId);
54721
+ } : void 0
54722
+ });
54723
+ }
54724
+ function createTaskCreateTool(store, provenance, options) {
54385
54725
  return {
54386
54726
  name: "fn_task_create",
54387
54727
  label: "Create Task",
54388
54728
  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
54729
  parameters: taskCreateParams,
54390
54730
  execute: async (_id, params) => {
54391
- const task = await store.createTask({
54731
+ const task = await createAgentTask(store, {
54392
54732
  description: params.description,
54393
54733
  dependencies: params.dependencies,
54394
54734
  column: "triage",
@@ -54397,7 +54737,7 @@ function createTaskCreateTool(store, provenance) {
54397
54737
  sourceAgentId: provenance.sourceAgentId,
54398
54738
  sourceRunId: provenance.sourceRunId
54399
54739
  } : void 0
54400
- });
54740
+ }, options);
54401
54741
  const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
54402
54742
  return {
54403
54743
  content: [{
@@ -54750,7 +55090,7 @@ ${lines.join("\n\n")}` }],
54750
55090
  }
54751
55091
  };
54752
55092
  }
54753
- function createDelegateTaskTool(agentStore, taskStore) {
55093
+ function createDelegateTaskTool(agentStore, taskStore, options) {
54754
55094
  return {
54755
55095
  name: "fn_delegate_task",
54756
55096
  label: "Delegate Task",
@@ -54770,13 +55110,13 @@ function createDelegateTaskTool(agentStore, taskStore) {
54770
55110
  details: {}
54771
55111
  };
54772
55112
  }
54773
- const task = await taskStore.createTask({
55113
+ const task = await createAgentTask(taskStore, {
54774
55114
  description: params.description,
54775
55115
  dependencies: params.dependencies,
54776
55116
  column: "todo",
54777
55117
  assignedAgentId: params.agent_id,
54778
55118
  source: { sourceType: "api" }
54779
- });
55119
+ }, options);
54780
55120
  const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
54781
55121
  return {
54782
55122
  content: [{
@@ -57603,7 +57943,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
57603
57943
  // Agent delegation tools — discover and delegate work to other agents.
57604
57944
  ...this.options.agentStore ? [
57605
57945
  createListAgentsTool(this.options.agentStore),
57606
- createDelegateTaskTool(this.options.agentStore, this.store)
57946
+ createDelegateTaskTool(this.options.agentStore, this.store, { rootDir: this.rootDir })
57607
57947
  ] : [],
57608
57948
  this.createReviewSpecTool(
57609
57949
  task.id,
@@ -58142,7 +58482,7 @@ Remove or replace these ids and call fn_task_create again.`
58142
58482
  planLog.warn(`${options.parentTaskId}: failed to load parent task for fn_task_create inheritance: ${msg}`);
58143
58483
  parentTask = void 0;
58144
58484
  }
58145
- const newTask = await store.createTask({
58485
+ const newTask = await createAgentTask(store, {
58146
58486
  title: params.title,
58147
58487
  description: params.description,
58148
58488
  dependencies: validDeps,
@@ -58156,7 +58496,7 @@ Remove or replace these ids and call fn_task_create again.`
58156
58496
  sourceType: "agent_heartbeat",
58157
58497
  sourceParentTaskId: options.parentTaskId
58158
58498
  }
58159
- });
58499
+ }, { rootDir: this.rootDir });
58160
58500
  options.createdSubtasksRef.current.push(newTask.id);
58161
58501
  return {
58162
58502
  content: [
@@ -58584,7 +58924,7 @@ var init_run_audit = __esm({
58584
58924
  });
58585
58925
 
58586
58926
  // ../engine/src/merger.ts
58587
- import { execSync, exec as exec2, spawn as spawn2 } from "node:child_process";
58927
+ import { execSync, exec as exec2, spawn as spawn3 } from "node:child_process";
58588
58928
  import { promisify as promisify3 } from "node:util";
58589
58929
  import { existsSync as existsSync22 } from "node:fs";
58590
58930
  import { join as join29 } from "node:path";
@@ -58599,7 +58939,7 @@ async function execWithProcessGroup(command, options) {
58599
58939
  return;
58600
58940
  }
58601
58941
  const useProcessGroup = process.platform !== "win32";
58602
- const child = spawn2(command, {
58942
+ const child = spawn3(command, {
58603
58943
  cwd: options.cwd,
58604
58944
  shell: true,
58605
58945
  detached: useProcessGroup,
@@ -63335,10 +63675,10 @@ var init_step_session_executor = __esm({
63335
63675
  ] : [];
63336
63676
  const memoryTools = createMemoryTools(this.options.rootDir, settings);
63337
63677
  const taskLogTool = this.options.store ? [createTaskLogTool(this.options.store, taskDetail.id)] : [];
63338
- const taskCreateTool = this.options.store ? [createTaskCreateTool(this.options.store)] : [];
63678
+ const taskCreateTool = this.options.store ? [createTaskCreateTool(this.options.store, void 0, { rootDir: this.options.rootDir })] : [];
63339
63679
  const delegationTools = this.options.agentStore ? [
63340
63680
  createListAgentsTool(this.options.agentStore),
63341
- createDelegateTaskTool(this.options.agentStore, this.options.store)
63681
+ createDelegateTaskTool(this.options.agentStore, this.options.store, { rootDir: this.options.rootDir })
63342
63682
  ] : [];
63343
63683
  const messagingTools = this.options.messageStore && taskDetail.assignedAgentId ? [
63344
63684
  createSendMessageTool(this.options.messageStore, taskDetail.assignedAgentId),
@@ -63749,7 +64089,7 @@ var init_task_completion = __esm({
63749
64089
  });
63750
64090
 
63751
64091
  // ../engine/src/run-verification-tool.ts
63752
- import { spawn as spawn3 } from "node:child_process";
64092
+ import { spawn as spawn4 } from "node:child_process";
63753
64093
  import { existsSync as existsSync26 } from "node:fs";
63754
64094
  import { isAbsolute as isAbsolute10, join as join34 } from "node:path";
63755
64095
  import { Type as Type4 } from "@mariozechner/pi-ai";
@@ -63782,7 +64122,7 @@ async function runVerificationCommand2(opts) {
63782
64122
  const stdoutBuf = { head: "", tail: "", totalBytes: 0 };
63783
64123
  const stderrBuf = { head: "", tail: "", totalBytes: 0 };
63784
64124
  return new Promise((resolve19) => {
63785
- const child = spawn3(command, {
64125
+ const child = spawn4(command, {
63786
64126
  cwd,
63787
64127
  stdio: ["ignore", "pipe", "pipe"],
63788
64128
  env: { ...process.env },
@@ -66070,7 +66410,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66070
66410
  // Agent delegation tools — discover and delegate work to other agents.
66071
66411
  ...this.options.agentStore ? [
66072
66412
  createListAgentsTool(this.options.agentStore),
66073
- createDelegateTaskTool(this.options.agentStore, this.store)
66413
+ createDelegateTaskTool(this.options.agentStore, this.store, { rootDir: this.rootDir })
66074
66414
  ] : [],
66075
66415
  // Messaging tools — allows executor agents to send and receive messages.
66076
66416
  ...this.options.messageStore && assignedAgentId ? [
@@ -66764,7 +67104,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66764
67104
  return createTaskLogTool(this.store, taskId);
66765
67105
  }
66766
67106
  createTaskCreateTool() {
66767
- return createTaskCreateTool(this.store, { sourceType: "api" });
67107
+ return createTaskCreateTool(this.store, { sourceType: "api" }, { rootDir: this.rootDir });
66768
67108
  }
66769
67109
  createTaskDocumentWriteTool(taskId) {
66770
67110
  return createTaskDocumentWriteTool(this.store, taskId);
@@ -71699,7 +72039,7 @@ function isTickableState(state) {
71699
72039
  function isHeartbeatManaged(agent) {
71700
72040
  return !isEphemeralAgent(agent);
71701
72041
  }
71702
- var HEARTBEAT_SYSTEM_PROMPT, HEARTBEAT_NO_TASK_SYSTEM_PROMPT, HEARTBEAT_PROCEDURE, heartbeatDoneParams, HeartbeatMonitor, OVERDUE_FIRE_JITTER_MS, HeartbeatTriggerScheduler;
72042
+ var HEARTBEAT_SYSTEM_PROMPT, HEARTBEAT_NO_TASK_SYSTEM_PROMPT, HEARTBEAT_PROCEDURE, HEARTBEAT_NO_TASK_PROCEDURE, heartbeatDoneParams, HeartbeatMonitor, OVERDUE_FIRE_JITTER_MS, HeartbeatTriggerScheduler;
71703
72043
  var init_agent_heartbeat = __esm({
71704
72044
  "../engine/src/agent-heartbeat.ts"() {
71705
72045
  "use strict";
@@ -71889,6 +72229,32 @@ When sending messages:
71889
72229
  Critical: a heartbeat without observable progress (a log, a document write, a
71890
72230
  status change, a comment, a delegation, or an explicit "no-op with reason") is
71891
72231
  a bug. Do not loop on the same plan across heartbeats without recording why.`;
72232
+ HEARTBEAT_NO_TASK_PROCEDURE = `## Heartbeat Procedure (run every tick, in order)
72233
+
72234
+ 1. **Identity & context** \u2014 review the **Identity Snapshot** at the top of
72235
+ this prompt. Confirm your role, soul, instructions, and memory match what
72236
+ you expect, and surface any anomalies in your first text output before
72237
+ doing anything else. (If fn_identity is available in your runtime you may
72238
+ also call it for full structured detail; the snapshot above is the
72239
+ authoritative source.)
72240
+ 2. **Inbox** \u2014 when fn_read_messages is available, call it. Process any pending
72241
+ messages first; reply with reply_to_message_id when answering.
72242
+ 3. **Wake delta** \u2014 read the Wake Delta block above. The wake reason is the
72243
+ highest-priority change for this heartbeat. If you were woken by a comment
72244
+ or a message, acknowledge it before doing anything else.
72245
+ 4. **Ambient review** \u2014 since you have no assigned task, review board/project
72246
+ signals and recent memory context before acting.
72247
+ 5. **Pick the next concrete action** \u2014 exactly ONE useful action this heartbeat:
72248
+ create a focused task, delegate work, send/reply to a message, or append
72249
+ durable memory.
72250
+ 6. **Persist progress** \u2014 use available ambient tools only:
72251
+ fn_task_create, fn_delegate_task, fn_send_message, fn_memory_append.
72252
+ 7. **Exit** \u2014 call fn_heartbeat_done with a one-line summary of what changed
72253
+ this tick. If you took no action, say so and explain why.
72254
+
72255
+ Critical: a heartbeat without observable progress (a created task, delegation,
72256
+ message reply, memory append, or explicit "no-op with reason") is a bug. Do
72257
+ not loop on the same plan across heartbeats without recording why.`;
71892
72258
  heartbeatDoneParams = Type6.Object({
71893
72259
  summary: Type6.Optional(Type6.String({ description: "Summary of what was accomplished this heartbeat" }))
71894
72260
  });
@@ -71928,13 +72294,6 @@ a bug. Do not loop on the same plan across heartbeats without recording why.`;
71928
72294
  this.rootDir = options.rootDir;
71929
72295
  this.messageStore = options.messageStore;
71930
72296
  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
72297
  }
71939
72298
  /**
71940
72299
  * Start the heartbeat monitoring loop.
@@ -72633,9 +72992,9 @@ a bug. Do not loop on the same plan across heartbeats without recording why.`;
72633
72992
  sourceType: "agent_heartbeat",
72634
72993
  sourceAgentId: agentId,
72635
72994
  sourceRunId: runContext?.runId
72636
- }));
72995
+ }, { rootDir: this.rootDir }));
72637
72996
  heartbeatTools.push(createListAgentsTool(this.store));
72638
- heartbeatTools.push(createDelegateTaskTool(this.store, taskStore));
72997
+ heartbeatTools.push(createDelegateTaskTool(this.store, taskStore, { rootDir: this.rootDir }));
72639
72998
  if (this.messageStore) {
72640
72999
  heartbeatTools.push(createSendMessageTool(this.messageStore, agentId));
72641
73000
  heartbeatTools.push(createReadMessagesTool(this.messageStore, agentId));
@@ -72658,22 +73017,27 @@ a bug. Do not loop on the same plan across heartbeats without recording why.`;
72658
73017
  heartbeatLog.warn(`Failed to configure heartbeat memory tools for ${agentId}: ${message}`);
72659
73018
  }
72660
73019
  const skillContext = buildSessionSkillContextSync2(agent, "heartbeat", rootDir);
72661
- let systemPrompt = isNoTaskRun ? HEARTBEAT_NO_TASK_SYSTEM_PROMPT : HEARTBEAT_SYSTEM_PROMPT;
72662
- const baseHeartbeatSystemPrompt = systemPrompt;
73020
+ const baseHeartbeatSystemPrompt = isNoTaskRun ? HEARTBEAT_NO_TASK_SYSTEM_PROMPT : HEARTBEAT_SYSTEM_PROMPT;
72663
73021
  let resolvedInstructionsForIdentity = "";
72664
73022
  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
- );
73023
+ resolvedInstructionsForIdentity = await resolveAgentInstructionsWithRatings(agent, rootDir, this.store);
72672
73024
  } catch (instructionError) {
72673
- systemPrompt = baseHeartbeatSystemPrompt;
72674
73025
  const message = instructionError instanceof Error ? instructionError.message : String(instructionError);
72675
- heartbeatLog.warn(`Failed to enrich heartbeat system prompt for ${agentId}: ${message}`);
73026
+ heartbeatLog.warn(`Failed to resolve agent instructions for heartbeat ${agentId}: ${message}`);
72676
73027
  }
73028
+ let memoryInstructions = "";
73029
+ if (memorySettings?.memoryEnabled !== false) {
73030
+ try {
73031
+ memoryInstructions = buildExecutionMemoryInstructions(rootDir, memorySettings);
73032
+ } catch (memoryInstructionErr) {
73033
+ const message = memoryInstructionErr instanceof Error ? memoryInstructionErr.message : String(memoryInstructionErr);
73034
+ heartbeatLog.warn(`Failed to resolve project memory instructions for heartbeat ${agentId}: ${message}`);
73035
+ }
73036
+ }
73037
+ const systemPrompt = buildSystemPromptWithInstructions(
73038
+ baseHeartbeatSystemPrompt,
73039
+ [resolvedInstructionsForIdentity, memoryInstructions].filter((part) => part.trim()).join("\n\n")
73040
+ );
72677
73041
  heartbeatTools.push(createIdentityTool({ agent, resolvedInstructions: resolvedInstructionsForIdentity }));
72678
73042
  heartbeatTools.push(heartbeatDoneTool);
72679
73043
  if (isNoTaskRun) {
@@ -72734,7 +73098,7 @@ a bug. Do not loop on the same plan across heartbeats without recording why.`;
72734
73098
  };
72735
73099
  const wakeReason = deriveWakeReason();
72736
73100
  const customProcedure = await resolveAgentHeartbeatProcedure(agent, rootDir);
72737
- const heartbeatProcedureText = customProcedure ?? HEARTBEAT_PROCEDURE;
73101
+ const heartbeatProcedureText = customProcedure ?? (isNoTaskRun ? HEARTBEAT_NO_TASK_PROCEDURE : HEARTBEAT_PROCEDURE);
72738
73102
  if (isNoTaskRun) {
72739
73103
  if (this.messageStore) {
72740
73104
  try {
@@ -72767,6 +73131,8 @@ a bug. Do not loop on the same plan across heartbeats without recording why.`;
72767
73131
  `- pending messages: ${pendingMessages.length}`,
72768
73132
  "",
72769
73133
  "Treat this wake delta as the highest-priority change for this heartbeat.",
73134
+ "This is an autonomous heartbeat run (manual or automatic): re-anchor on",
73135
+ "identity, process wake context, then complete ONE concrete action.",
72770
73136
  "Run the Heartbeat Procedure (below) before doing anything else \u2014 even a",
72771
73137
  "timer-only wake should re-check messages, memory, and project state.",
72772
73138
  "",
@@ -72858,6 +73224,8 @@ a bug. Do not loop on the same plan across heartbeats without recording why.`;
72858
73224
  `- triggering comments: ${effectiveTriggeringCommentIds?.length ?? 0}`,
72859
73225
  "",
72860
73226
  "Treat this wake delta as the highest-priority change for this heartbeat.",
73227
+ "This is an autonomous heartbeat run (manual or automatic): re-anchor on",
73228
+ "identity, process wake context, then complete ONE concrete action.",
72861
73229
  "Before resuming prior task work, run the Heartbeat Procedure (below) and",
72862
73230
  "decide what action this delta requires. Your assigned task is one input",
72863
73231
  "to the procedure \u2014 not the only thing to consider.",
@@ -73017,7 +73385,7 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
73017
73385
  const baseCreateTool = createTaskCreateTool(taskStore, {
73018
73386
  sourceType: "agent_heartbeat",
73019
73387
  sourceAgentId: agentId
73020
- });
73388
+ }, { rootDir: this.rootDir });
73021
73389
  const trackedCreateTool = {
73022
73390
  ...baseCreateTool,
73023
73391
  execute: async (id, params, signal, onUpdate, ctx) => {
@@ -73044,7 +73412,7 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
73044
73412
  tools.push(createTaskDocumentWriteTool(taskStore, taskId));
73045
73413
  tools.push(createTaskDocumentReadTool(taskStore, taskId));
73046
73414
  tools.push(createListAgentsTool(this.store));
73047
- tools.push(createDelegateTaskTool(this.store, taskStore));
73415
+ tools.push(createDelegateTaskTool(this.store, taskStore, { rootDir: this.rootDir }));
73048
73416
  if (messageStore) {
73049
73417
  tools.push(createSendMessageTool(messageStore, agentId));
73050
73418
  tools.push(createReadMessagesTool(messageStore, agentId));
@@ -80746,7 +81114,7 @@ var init_provider_adapters = __esm({
80746
81114
 
80747
81115
  // ../engine/src/remote-access/tunnel-process-manager.ts
80748
81116
  import { EventEmitter as EventEmitter23 } from "node:events";
80749
- import { exec as exec9, execFile as execFile3, spawn as spawn4 } from "node:child_process";
81117
+ import { exec as exec9, execFile as execFile3, spawn as spawn5 } from "node:child_process";
80750
81118
  import { promisify as promisify9 } from "node:util";
80751
81119
  function nowIso() {
80752
81120
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -80837,7 +81205,7 @@ var init_tunnel_process_manager = __esm({
80837
81205
  super();
80838
81206
  this.maxLogEntries = options.maxLogEntries ?? DEFAULT_MAX_LOG_ENTRIES;
80839
81207
  this.defaultStopTimeoutMs = options.stopTimeoutMs ?? DEFAULT_STOP_TIMEOUT_MS2;
80840
- this.spawnImpl = options.spawnImpl ?? spawn4;
81208
+ this.spawnImpl = options.spawnImpl ?? spawn5;
80841
81209
  }
80842
81210
  getStatus() {
80843
81211
  return { ...this.status, lastError: this.status.lastError ? { ...this.status.lastError } : null };
@@ -81211,6 +81579,7 @@ var execFileAsync2, MERGE_HANDOFF_GRACE_MS, isRemoteActive, ProjectEngine;
81211
81579
  var init_project_engine = __esm({
81212
81580
  "../engine/src/project-engine.ts"() {
81213
81581
  "use strict";
81582
+ init_src();
81214
81583
  init_in_process_runtime();
81215
81584
  init_pr_monitor();
81216
81585
  init_pr_comment_handler();
@@ -81905,6 +82274,48 @@ ${detail}`
81905
82274
  if (task.status === "failed") return false;
81906
82275
  return (task.mergeRetries ?? 0) < _ProjectEngine.MAX_AUTO_MERGE_RETRIES || this.hasAutoHealableVerificationBufferFailure(task) || this.isRetryCooldownElapsed(task);
81907
82276
  }
82277
+ /**
82278
+ * Remove and return the highest-priority taskId from the merge queue.
82279
+ * Ordering: priority (urgent→low), then createdAt ASC, then id ASC — matching
82280
+ * the triage and scheduler comparators. Manual merges (onMerge resolvers) are
82281
+ * preferred over auto-merges so awaited callers aren't starved by a flood of
82282
+ * higher-priority auto-enqueues. IDs whose tasks can't be loaded fall back to
82283
+ * FIFO order so they still drain.
82284
+ */
82285
+ async pickNextMergeTaskId(store) {
82286
+ if (this.mergeQueue.length === 0) return void 0;
82287
+ if (this.mergeQueue.length === 1) {
82288
+ return this.mergeQueue.shift();
82289
+ }
82290
+ const queueSnapshot = [...this.mergeQueue];
82291
+ const entries = [];
82292
+ for (let i = 0; i < queueSnapshot.length; i++) {
82293
+ const taskId = queueSnapshot[i];
82294
+ const task = await store.getTask(taskId).catch(() => void 0);
82295
+ entries.push({
82296
+ taskId,
82297
+ task,
82298
+ manual: this.manualMergeResolvers.has(taskId),
82299
+ order: i
82300
+ });
82301
+ }
82302
+ if (this.shuttingDown) return void 0;
82303
+ entries.sort((a, b) => {
82304
+ if (a.manual !== b.manual) return a.manual ? -1 : 1;
82305
+ if (a.task && b.task) return compareTasksByPriorityThenAgeAndId(a.task, b.task);
82306
+ if (a.task) return -1;
82307
+ if (b.task) return 1;
82308
+ return a.order - b.order;
82309
+ });
82310
+ for (const entry of entries) {
82311
+ const liveIndex = this.mergeQueue.indexOf(entry.taskId);
82312
+ if (liveIndex !== -1) {
82313
+ this.mergeQueue.splice(liveIndex, 1);
82314
+ return entry.taskId;
82315
+ }
82316
+ }
82317
+ return void 0;
82318
+ }
81908
82319
  internalEnqueueMerge(taskId) {
81909
82320
  if (this.shuttingDown) return;
81910
82321
  if (this.mergeActive.has(taskId)) return;
@@ -81912,6 +82323,23 @@ ${detail}`
81912
82323
  this.mergeQueue.push(taskId);
81913
82324
  void this.drainMergeQueue();
81914
82325
  }
82326
+ /**
82327
+ * Filter a sweep's listTasks() result to merge-eligible tasks, sort by
82328
+ * priority (urgent → low, then createdAt ASC, then id ASC), and enqueue.
82329
+ * Sorting before enqueue matters because each enqueue may immediately
82330
+ * trigger drainMergeQueue's single-item fast path, so the first task
82331
+ * pushed wins. listTasks returns createdAt ASC — without this sort an
82332
+ * older low-priority task would start before a later urgent one.
82333
+ */
82334
+ enqueueEligibleInReviewTasks(tasks) {
82335
+ const eligible = sortTasksByPriorityThenAgeAndId(
82336
+ tasks.filter((t) => !t.paused && this.canMergeTask(t))
82337
+ );
82338
+ for (const t of eligible) {
82339
+ this.internalEnqueueMerge(t.id);
82340
+ }
82341
+ return eligible.length;
82342
+ }
81915
82343
  async drainMergeQueue() {
81916
82344
  if (this.mergeRunning) return;
81917
82345
  this.mergeRunning = true;
@@ -81919,7 +82347,9 @@ ${detail}`
81919
82347
  const store = this.runtime.getTaskStore();
81920
82348
  const cwd = this.config.workingDirectory;
81921
82349
  while (this.mergeQueue.length > 0 && !this.shuttingDown) {
81922
- const taskId = this.mergeQueue.shift();
82350
+ const taskId = await this.pickNextMergeTaskId(store);
82351
+ if (!taskId) break;
82352
+ if (this.shuttingDown) break;
81923
82353
  const manualResolver = this.manualMergeResolvers.get(taskId);
81924
82354
  try {
81925
82355
  if (!manualResolver) {
@@ -82389,12 +82819,9 @@ ${detail}`
82389
82819
  }
82390
82820
  const settings = await store.getSettings();
82391
82821
  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
- }
82822
+ const enqueued = this.enqueueEligibleInReviewTasks(tasks);
82823
+ if (enqueued > 0) {
82824
+ runtimeLog.log(`Auto-merge startup sweep: enqueueing ${enqueued} task(s)`);
82398
82825
  }
82399
82826
  } catch (err) {
82400
82827
  runtimeLog.warn(
@@ -82410,14 +82837,7 @@ ${detail}`
82410
82837
  const settings = await store.getSettings();
82411
82838
  if (!settings.globalPause && !settings.enginePaused && settings.autoMerge) {
82412
82839
  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
- }
82840
+ this.enqueueEligibleInReviewTasks(tasks);
82421
82841
  }
82422
82842
  } catch (err) {
82423
82843
  runtimeLog.warn(
@@ -82473,14 +82893,7 @@ ${detail}`
82473
82893
  if (s.autoMerge) {
82474
82894
  try {
82475
82895
  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
- }
82896
+ this.enqueueEligibleInReviewTasks(tasks);
82484
82897
  } catch (err) {
82485
82898
  runtimeLog.warn(
82486
82899
  `Global unpause: failed to scan in-review tasks for auto-merge: ${err instanceof Error ? err.message : String(err)}`
@@ -82510,14 +82923,7 @@ ${detail}`
82510
82923
  if (s.autoMerge) {
82511
82924
  try {
82512
82925
  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
- }
82926
+ this.enqueueEligibleInReviewTasks(tasks);
82521
82927
  } catch (err) {
82522
82928
  runtimeLog.warn(
82523
82929
  `Engine unpause: failed to scan in-review tasks for auto-merge: ${err instanceof Error ? err.message : String(err)}`
@@ -83110,7 +83516,7 @@ var init_peer_exchange_service = __esm({
83110
83516
  syncIntervalMs;
83111
83517
  interval = null;
83112
83518
  activeSync = null;
83113
- stopped = false;
83519
+ running = false;
83114
83520
  /** Whether settings sync is enabled. Default: false. */
83115
83521
  settingsSyncEnabled;
83116
83522
  /** Minimum interval between settings syncs with the same node in ms. Default: 5 minutes. */
@@ -83152,10 +83558,11 @@ var init_peer_exchange_service = __esm({
83152
83558
  * Begins periodic gossip with all online remote nodes.
83153
83559
  */
83154
83560
  start() {
83155
- if (this.stopped) {
83156
- peerExchangeLog.warn("Cannot start - service has been stopped");
83561
+ if (this.running) {
83562
+ peerExchangeLog.log("Peer exchange service already running");
83157
83563
  return;
83158
83564
  }
83565
+ this.running = true;
83159
83566
  this.centralCore.listNodes().then((nodes) => {
83160
83567
  const onlineRemoteCount = nodes.filter(
83161
83568
  (n) => n.type === "remote" && n.status === "online" && n.url
@@ -83165,6 +83572,7 @@ var init_peer_exchange_service = __esm({
83165
83572
  peerExchangeLog.warn(`Failed to get initial peer count: ${err}`);
83166
83573
  });
83167
83574
  this.interval = setInterval(() => {
83575
+ if (!this.running) return;
83168
83576
  void this.syncWithAllPeers();
83169
83577
  }, this.syncIntervalMs);
83170
83578
  }
@@ -83172,12 +83580,21 @@ var init_peer_exchange_service = __esm({
83172
83580
  * Stop the peer exchange service.
83173
83581
  * Clears the sync interval and prevents further syncs.
83174
83582
  */
83175
- stop() {
83583
+ async stop() {
83584
+ if (!this.running) {
83585
+ return;
83586
+ }
83587
+ this.running = false;
83176
83588
  if (this.interval) {
83177
83589
  clearInterval(this.interval);
83178
83590
  this.interval = null;
83179
83591
  }
83180
- this.stopped = true;
83592
+ if (this.activeSync) {
83593
+ try {
83594
+ await this.activeSync;
83595
+ } catch {
83596
+ }
83597
+ }
83181
83598
  peerExchangeLog.log("Stopped peer exchange service");
83182
83599
  }
83183
83600
  /**
@@ -84597,7 +85014,7 @@ For completion:
84597
85014
  }`;
84598
85015
  SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
84599
85016
  CLEANUP_INTERVAL_MS2 = 5 * 60 * 1e3;
84600
- MAX_SESSIONS_PER_IP_PER_HOUR = 5;
85017
+ MAX_SESSIONS_PER_IP_PER_HOUR = 1e3;
84601
85018
  RATE_LIMIT_WINDOW_MS2 = 60 * 60 * 1e3;
84602
85019
  GENERATION_TIMEOUT_MS = 12e4;
84603
85020
  sessions = /* @__PURE__ */ new Map();
@@ -85220,7 +85637,7 @@ var init_src3 = __esm({
85220
85637
  });
85221
85638
 
85222
85639
  // ../../plugins/fusion-plugin-hermes-runtime/dist/cli-spawn.js
85223
- import { spawn as spawn5, spawnSync } from "node:child_process";
85640
+ import { spawn as spawn6, spawnSync } from "node:child_process";
85224
85641
  import os2 from "node:os";
85225
85642
  import path, { sep as PATH_SEP } from "node:path";
85226
85643
  function resolveBinaryForSpawn(binary) {
@@ -85330,7 +85747,7 @@ async function invokeHermesCli(prompt, settings, resumeSessionId, signal) {
85330
85747
  if (settings.profile) {
85331
85748
  spawnEnv.HERMES_HOME = hermesProfileHome(settings.profile);
85332
85749
  }
85333
- const child = spawn5(binary, args, {
85750
+ const child = spawn6(binary, args, {
85334
85751
  stdio: ["ignore", "pipe", "pipe"],
85335
85752
  env: spawnEnv
85336
85753
  });
@@ -85543,7 +85960,7 @@ var init_dist = __esm({
85543
85960
  });
85544
85961
 
85545
85962
  // ../../plugins/fusion-plugin-openclaw-runtime/dist/pi-module.js
85546
- import { spawn as spawn6 } from "node:child_process";
85963
+ import { spawn as spawn7 } from "node:child_process";
85547
85964
  import { randomUUID as randomUUID12 } from "node:crypto";
85548
85965
  function asString(v) {
85549
85966
  return typeof v === "string" && v.trim() !== "" ? v.trim() : void 0;
@@ -85620,7 +86037,7 @@ async function promptCli(session, message, config, callbacks, signal) {
85620
86037
  cb.onToolStart?.("openclaw.agent", { sessionId: session.sessionId });
85621
86038
  return new Promise((resolve19, reject) => {
85622
86039
  let settled = false;
85623
- const child = spawn6(config.binaryPath, args, {
86040
+ const child = spawn7(config.binaryPath, args, {
85624
86041
  stdio: ["ignore", "pipe", "pipe"]
85625
86042
  });
85626
86043
  const hardKill = setTimeout(() => {
@@ -85771,7 +86188,7 @@ var init_runtime_adapter2 = __esm({
85771
86188
  });
85772
86189
 
85773
86190
  // ../../plugins/fusion-plugin-openclaw-runtime/dist/probe.js
85774
- import { spawn as spawn7 } from "node:child_process";
86191
+ import { spawn as spawn8 } from "node:child_process";
85775
86192
  async function probeOpenClawBinary(opts = {}) {
85776
86193
  const startedAt = Date.now();
85777
86194
  const binary = opts.binaryPath ?? "openclaw";
@@ -85782,7 +86199,7 @@ async function probeOpenClawBinary(opts = {}) {
85782
86199
  resolvePromise({ ...partial, probeDurationMs: Date.now() - startedAt });
85783
86200
  };
85784
86201
  let settled = false;
85785
- const child = spawn7(resolvedPath ?? binary, ["--version"], {
86202
+ const child = spawn8(resolvedPath ?? binary, ["--version"], {
85786
86203
  stdio: ["ignore", "pipe", "pipe"]
85787
86204
  });
85788
86205
  const timer = setTimeout(() => {
@@ -85843,7 +86260,7 @@ async function probeOpenClawBinary(opts = {}) {
85843
86260
  async function tryResolveBinaryPath(binary) {
85844
86261
  return new Promise((resolvePromise) => {
85845
86262
  const which = process.platform === "win32" ? "where" : "which";
85846
- const child = spawn7(which, [binary], { stdio: ["ignore", "pipe", "ignore"] });
86263
+ const child = spawn8(which, [binary], { stdio: ["ignore", "pipe", "ignore"] });
85847
86264
  let out = "";
85848
86265
  child.stdout?.on("data", (chunk) => {
85849
86266
  out += chunk.toString("utf-8");
@@ -92432,7 +92849,7 @@ var init_register_git_github = __esm({
92432
92849
  });
92433
92850
 
92434
92851
  // ../dashboard/src/terminal.ts
92435
- import { spawn as spawn8 } from "node:child_process";
92852
+ import { spawn as spawn9 } from "node:child_process";
92436
92853
  import { randomUUID as randomUUID13 } from "node:crypto";
92437
92854
  import { EventEmitter as EventEmitter29 } from "node:events";
92438
92855
  function extractBaseCommand(command) {
@@ -92594,7 +93011,7 @@ var init_terminal = __esm({
92594
93011
  return { sessionId: "", error: validation.error };
92595
93012
  }
92596
93013
  const sessionId = randomUUID13();
92597
- const childProcess = spawn8(command, [], {
93014
+ const childProcess = spawn9(command, [], {
92598
93015
  cwd,
92599
93016
  shell: true,
92600
93017
  stdio: ["pipe", "pipe", "pipe"],
@@ -93318,7 +93735,7 @@ function remapSpawnError(err, bin) {
93318
93735
  return err instanceof Error ? err : new Error(String(err));
93319
93736
  }
93320
93737
  async function spawnPaperclipCliJson(args, opts) {
93321
- const { spawn: spawn11 } = await import("node:child_process");
93738
+ const { spawn: spawn12 } = await import("node:child_process");
93322
93739
  const bin = opts.cliBinaryPath ?? "paperclipai";
93323
93740
  const fullArgs = [...args, "--json"];
93324
93741
  if (opts.cliConfigPath) {
@@ -93329,7 +93746,7 @@ async function spawnPaperclipCliJson(args, opts) {
93329
93746
  return new Promise((resolve19, reject) => {
93330
93747
  let child;
93331
93748
  try {
93332
- child = spawn11(bin, fullArgs, { stdio: ["ignore", "pipe", "pipe"] });
93749
+ child = spawn12(bin, fullArgs, { stdio: ["ignore", "pipe", "pipe"] });
93333
93750
  } catch (err) {
93334
93751
  reject(remapSpawnError(err, bin));
93335
93752
  return;
@@ -93841,6 +94258,25 @@ var init_register_runtime_provider_routes = __esm({
93841
94258
  }
93842
94259
  });
93843
94260
 
94261
+ // ../dashboard/src/cli-package-version.ts
94262
+ var init_cli_package_version = __esm({
94263
+ "../dashboard/src/cli-package-version.ts"() {
94264
+ "use strict";
94265
+ }
94266
+ });
94267
+
94268
+ // ../dashboard/src/routes/register-fn-binary-routes.ts
94269
+ var MAX_OUTPUT_BYTES2;
94270
+ var init_register_fn_binary_routes = __esm({
94271
+ "../dashboard/src/routes/register-fn-binary-routes.ts"() {
94272
+ "use strict";
94273
+ init_src();
94274
+ init_api_error();
94275
+ init_cli_package_version();
94276
+ MAX_OUTPUT_BYTES2 = 64 * 1024;
94277
+ }
94278
+ });
94279
+
93844
94280
  // ../dashboard/src/update-check.ts
93845
94281
  var DAY_MS;
93846
94282
  var init_update_check = __esm({
@@ -93850,13 +94286,6 @@ var init_update_check = __esm({
93850
94286
  }
93851
94287
  });
93852
94288
 
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
94289
  // ../dashboard/src/routes/register-update-check-routes.ts
93861
94290
  var init_register_update_check_routes = __esm({
93862
94291
  "../dashboard/src/routes/register-update-check-routes.ts"() {
@@ -94042,6 +94471,7 @@ var init_routes = __esm({
94042
94471
  init_register_usage_routes();
94043
94472
  init_register_auth_routes();
94044
94473
  init_register_runtime_provider_routes();
94474
+ init_register_fn_binary_routes();
94045
94475
  init_register_update_check_routes();
94046
94476
  init_register_integrated_routers();
94047
94477
  init_resolve_diff_base();
@@ -99369,7 +99799,7 @@ async function runTaskPlan(initialPlanArg, yesFlag = false, projectName) {
99369
99799
  } catch (err) {
99370
99800
  clearThinking();
99371
99801
  if (err instanceof RateLimitError2) {
99372
- console.error("\n Rate limit exceeded. Maximum 5 planning sessions per hour.\n");
99802
+ console.error("\n Rate limit exceeded. Maximum 1000 planning sessions per hour.\n");
99373
99803
  process.exit(1);
99374
99804
  }
99375
99805
  console.error(`
@@ -99526,7 +99956,7 @@ __export(skills_exports, {
99526
99956
  runSkillsSearch: () => runSkillsSearch,
99527
99957
  searchSkills: () => searchSkills
99528
99958
  });
99529
- import { spawn as spawn9 } from "node:child_process";
99959
+ import { spawn as spawn10 } from "node:child_process";
99530
99960
  async function searchSkills(query, limit = 10) {
99531
99961
  const url = `${SKILLS_API_BASE}/api/search?q=${encodeURIComponent(query)}&limit=${limit}`;
99532
99962
  try {
@@ -99604,7 +100034,7 @@ async function runSkillsInstall(args, options) {
99604
100034
  npxArgs.push("--skill", options.skill);
99605
100035
  }
99606
100036
  npxArgs.push("-y", "-a", "pi");
99607
- const child = spawn9("npx", npxArgs, {
100037
+ const child = spawn10("npx", npxArgs, {
99608
100038
  cwd: process.cwd(),
99609
100039
  stdio: "inherit",
99610
100040
  shell: true
@@ -99641,7 +100071,7 @@ import { StringEnum } from "@mariozechner/pi-ai";
99641
100071
  import { resolve as resolve18, basename as basename11, extname as extname3, join as join41 } from "node:path";
99642
100072
  import { readFile as readFile18 } from "node:fs/promises";
99643
100073
  import { existsSync as existsSync31 } from "node:fs";
99644
- import { spawn as spawn10 } from "node:child_process";
100074
+ import { spawn as spawn11 } from "node:child_process";
99645
100075
  var MIME_TYPES2 = {
99646
100076
  ".png": "image/png",
99647
100077
  ".jpg": "image/jpeg",
@@ -101450,7 +101880,7 @@ Status: ${updated.status}`
101450
101880
  npxArgs.push("--skill", params.skill);
101451
101881
  }
101452
101882
  npxArgs.push("-y", "-a", "pi");
101453
- const child = spawn10("npx", npxArgs, {
101883
+ const child = spawn11("npx", npxArgs, {
101454
101884
  cwd: resolveProjectRoot(ctx.cwd),
101455
101885
  stdio: "pipe",
101456
101886
  shell: true
@@ -101532,7 +101962,7 @@ Status: ${updated.status}`
101532
101962
  return;
101533
101963
  }
101534
101964
  const port = trimmed ? parseInt(trimmed, 10) || 4040 : 4040;
101535
- const child = spawn10("fn", ["dashboard", "--port", String(port)], {
101965
+ const child = spawn11("fn", ["dashboard", "--port", String(port)], {
101536
101966
  cwd: resolveProjectRoot(ctx.cwd),
101537
101967
  stdio: ["ignore", "pipe", "pipe"],
101538
101968
  detached: false,