@runfusion/fusion 0.17.2 → 0.18.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 (57) hide show
  1. package/dist/bin.js +1004 -633
  2. package/dist/client/assets/ChatView-BomXmqar.js +1 -0
  3. package/dist/client/assets/{DevServerView-GFFVXHVP.js → DevServerView-yFvF4xL4.js} +1 -1
  4. package/dist/client/assets/DirectoryPicker-BDNodhtF.js +1 -0
  5. package/dist/client/assets/DocumentsView-CAWtDEaL.js +1 -0
  6. package/dist/client/assets/{InsightsView-Bxu0TJkt.js → InsightsView-CDkiJeW1.js} +2 -2
  7. package/dist/client/assets/MemoryView-ZRQ9EL9H.js +2 -0
  8. package/dist/client/assets/NodesView-DosrOyeH.js +14 -0
  9. package/dist/client/assets/NodesView-sJgPLTzz.css +1 -0
  10. package/dist/client/assets/{PiExtensionsManager-4e3MlD62.js → PiExtensionsManager-CzZ1LEpz.js} +3 -3
  11. package/dist/client/assets/PluginManager-Dp3vPsMO.js +1 -0
  12. package/dist/client/assets/ResearchView-PvNkdaQE.js +1 -0
  13. package/dist/client/assets/{RoadmapsView-jHTOK0RQ.js → RoadmapsView-BUW-HJz5.js} +2 -2
  14. package/dist/client/assets/SettingsModal-BNSrO1M9.css +1 -0
  15. package/dist/client/assets/{SettingsModal-4Z8ZJMzD.js → SettingsModal-ByVl_fUi.js} +1 -1
  16. package/dist/client/assets/SettingsModal-oOnIed5O.css +1 -0
  17. package/dist/client/assets/SettingsModal-uzo470XS.js +31 -0
  18. package/dist/client/assets/SetupWizardModal-DH1hpyiP.js +1 -0
  19. package/dist/client/assets/SkillsView-B-RqQSFE.js +1 -0
  20. package/dist/client/assets/index-CtiRbTNv.js +1229 -0
  21. package/dist/client/assets/index-Dy-xC2C2.css +1 -0
  22. package/dist/client/assets/{users-D3u6f2Rz.js → users-WyHhw14V.js} +2 -2
  23. package/dist/client/index.html +2 -2
  24. package/dist/client/version.json +1 -1
  25. package/dist/droid-cli/index.ts +12 -7
  26. package/dist/droid-cli/package.json +4 -1
  27. package/dist/droid-cli/src/__tests__/provider.test.ts +42 -6
  28. package/dist/extension.js +488 -43
  29. package/dist/pi-claude-cli/package.json +1 -1
  30. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  31. package/package.json +2 -1
  32. package/skill/fusion/SKILL.md +2 -2
  33. package/skill/fusion/references/best-practices.md +33 -0
  34. package/skill/fusion/references/extension-tools.md +47 -2
  35. package/skill/fusion/references/fusion-capabilities.md +7 -2
  36. package/dist/client/assets/AgentDetailView-17J-F0Rl.js +0 -18
  37. package/dist/client/assets/AgentDetailView-yu8Xltqk.css +0 -1
  38. package/dist/client/assets/AgentsView-Bs03ptrd.css +0 -1
  39. package/dist/client/assets/AgentsView-sbBkb7Wd.js +0 -517
  40. package/dist/client/assets/ChatView-BR5cvK_B.js +0 -1
  41. package/dist/client/assets/DirectoryPicker-WPDSBdT6.js +0 -1
  42. package/dist/client/assets/DocumentsView-BHpDsIIt.js +0 -1
  43. package/dist/client/assets/MemoryView-CmnzZorw.js +0 -2
  44. package/dist/client/assets/NodesView-CO9_4hCr.js +0 -14
  45. package/dist/client/assets/NodesView-DuAXX_0j.css +0 -1
  46. package/dist/client/assets/PluginManager-DGN2rvOY.js +0 -1
  47. package/dist/client/assets/ResearchView-Dsa6Gykl.js +0 -1
  48. package/dist/client/assets/SettingsModal-D0kuJpBA.js +0 -31
  49. package/dist/client/assets/SettingsModal-D_AFkDJa.css +0 -1
  50. package/dist/client/assets/SettingsModal-Dq4a5KSX.css +0 -1
  51. package/dist/client/assets/SetupWizardModal-Bhumd4Rf.js +0 -1
  52. package/dist/client/assets/SkillsView-MHweJTz4.js +0 -1
  53. package/dist/client/assets/folder-open-BNQW9dE9.js +0 -6
  54. package/dist/client/assets/index-DEVBHvyW.css +0 -1
  55. package/dist/client/assets/index-k_85J1DS.js +0 -682
  56. package/dist/client/assets/star-7L86NZrT.js +0 -6
  57. package/dist/client/assets/upload-DsAS6tno.js +0 -6
package/dist/extension.js CHANGED
@@ -2771,7 +2771,7 @@ var init_db = __esm({
2771
2771
  "use strict";
2772
2772
  init_sqlite_adapter();
2773
2773
  init_types();
2774
- SCHEMA_VERSION = 59;
2774
+ SCHEMA_VERSION = 60;
2775
2775
  SCHEMA_SQL = `
2776
2776
  -- Tasks table with JSON columns for nested data
2777
2777
  CREATE TABLE IF NOT EXISTS tasks (
@@ -2839,6 +2839,7 @@ CREATE TABLE IF NOT EXISTS tasks (
2839
2839
  missionId TEXT,
2840
2840
  sliceId TEXT,
2841
2841
  assignedAgentId TEXT,
2842
+ pausedByAgentId TEXT,
2842
2843
  assigneeUserId TEXT,
2843
2844
  sourceType TEXT,
2844
2845
  sourceAgentId TEXT,
@@ -4636,6 +4637,12 @@ This means a caller passed a .fusion directory where a project root was expected
4636
4637
  this.db.exec(`CREATE INDEX IF NOT EXISTS idxInsightRunEventsRunIdSeq ON project_insight_run_events(runId, seq)`);
4637
4638
  });
4638
4639
  }
4640
+ if (version < 60) {
4641
+ this.applyMigration(60, () => {
4642
+ this.addColumnIfMissing("tasks", "pausedByAgentId", "TEXT");
4643
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idxTasksPausedByAgentId ON tasks(pausedByAgentId)`);
4644
+ });
4645
+ }
4639
4646
  }
4640
4647
  /**
4641
4648
  * Run a single migration step inside a transaction and bump the version.
@@ -32645,6 +32652,7 @@ var init_store = __esm({
32645
32652
  missionId: row.missionId || void 0,
32646
32653
  sliceId: row.sliceId || void 0,
32647
32654
  assignedAgentId: row.assignedAgentId || void 0,
32655
+ pausedByAgentId: row.pausedByAgentId || void 0,
32648
32656
  assigneeUserId: row.assigneeUserId || void 0,
32649
32657
  nodeId: row.nodeId || void 0,
32650
32658
  effectiveNodeId: row.effectiveNodeId || void 0,
@@ -32909,6 +32917,7 @@ ${recentText}` : void 0
32909
32917
  "missionId",
32910
32918
  "sliceId",
32911
32919
  "assignedAgentId",
32920
+ "pausedByAgentId",
32912
32921
  "assigneeUserId",
32913
32922
  "nodeId",
32914
32923
  "effectiveNodeId",
@@ -33021,6 +33030,7 @@ ${outcome}`;
33021
33030
  "missionId",
33022
33031
  "sliceId",
33023
33032
  "assignedAgentId",
33033
+ "pausedByAgentId",
33024
33034
  "assigneeUserId",
33025
33035
  "nodeId",
33026
33036
  "effectiveNodeId",
@@ -33071,9 +33081,9 @@ ${outcome}`;
33071
33081
  dependencies, steps, log, attachments, steeringComments,
33072
33082
  comments, workflowStepResults, prInfo, issueInfo,
33073
33083
  sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
33074
- mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt
33084
+ mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt
33075
33085
  ) VALUES (
33076
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
33086
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
33077
33087
  )
33078
33088
  ON CONFLICT(id) DO UPDATE SET
33079
33089
  title = excluded.title,
@@ -33142,6 +33152,7 @@ ${outcome}`;
33142
33152
  missionId = excluded.missionId,
33143
33153
  sliceId = excluded.sliceId,
33144
33154
  assignedAgentId = excluded.assignedAgentId,
33155
+ pausedByAgentId = excluded.pausedByAgentId,
33145
33156
  assigneeUserId = excluded.assigneeUserId,
33146
33157
  nodeId = excluded.nodeId,
33147
33158
  effectiveNodeId = excluded.effectiveNodeId,
@@ -33223,6 +33234,7 @@ ${outcome}`;
33223
33234
  task.missionId ?? null,
33224
33235
  task.sliceId ?? null,
33225
33236
  task.assignedAgentId ?? null,
33237
+ task.pausedByAgentId ?? null,
33226
33238
  task.assigneeUserId ?? null,
33227
33239
  task.nodeId ?? null,
33228
33240
  task.effectiveNodeId ?? null,
@@ -34340,6 +34352,23 @@ ${newTask.description}
34340
34352
  const matches = [...activeMatches, ...archiveMatches];
34341
34353
  return limit >= 0 ? matches.slice(0, limit) : matches;
34342
34354
  }
34355
+ async getTasksByAssignedAgent(agentId, options) {
34356
+ const whereClauses = ["assignedAgentId = ?"];
34357
+ const params = [agentId];
34358
+ if (options?.pausedOnly) {
34359
+ whereClauses.push("paused = 1");
34360
+ }
34361
+ if (options?.excludeArchived) {
34362
+ whereClauses.push(`"column" != 'archived'`);
34363
+ }
34364
+ const selectClause = this.getTaskSelectClause(false);
34365
+ const rows = this.db.prepare(`
34366
+ SELECT ${selectClause} FROM tasks
34367
+ WHERE ${whereClauses.join(" AND ")}
34368
+ ORDER BY createdAt ASC
34369
+ `).all(...params);
34370
+ return rows.map((row) => this.rowToTask(row));
34371
+ }
34343
34372
  async selectNextTaskForAgent(agentId) {
34344
34373
  const tasks = await this.listTasks({ slim: true });
34345
34374
  if (tasks.length === 0) {
@@ -34577,6 +34606,11 @@ ${newTask.description}
34577
34606
  } else if (updates.assignedAgentId !== void 0) {
34578
34607
  task.assignedAgentId = updates.assignedAgentId;
34579
34608
  }
34609
+ if (updates.pausedByAgentId === null) {
34610
+ task.pausedByAgentId = void 0;
34611
+ } else if (updates.pausedByAgentId !== void 0) {
34612
+ task.pausedByAgentId = updates.pausedByAgentId;
34613
+ }
34580
34614
  if (updates.assigneeUserId === null) {
34581
34615
  task.assigneeUserId = void 0;
34582
34616
  } else if (updates.assigneeUserId !== void 0) {
@@ -34815,14 +34849,21 @@ ${newTask.description}
34815
34849
  * Pause or unpause a task. Paused tasks are excluded from all automated
34816
34850
  * agent and scheduler interaction. Logs the action and emits `task:updated`.
34817
34851
  */
34818
- async pauseTask(id, paused, runContext) {
34852
+ async pauseTask(id, paused, runContext, agentOptions) {
34819
34853
  return this.withTaskLock(id, async () => {
34820
34854
  const dir = this.taskDir(id);
34821
34855
  const task = await this.readTaskJson(dir);
34822
34856
  if (!task.log) {
34823
34857
  task.log = [];
34824
34858
  }
34859
+ const previousPausedByAgentId = task.pausedByAgentId;
34825
34860
  task.paused = paused || void 0;
34861
+ if (paused && agentOptions?.pausedByAgentId) {
34862
+ task.pausedByAgentId = agentOptions.pausedByAgentId;
34863
+ }
34864
+ if (!paused) {
34865
+ task.pausedByAgentId = void 0;
34866
+ }
34826
34867
  if (task.column === "in-progress" || task.column === "in-review") {
34827
34868
  task.status = paused ? "paused" : void 0;
34828
34869
  }
@@ -34830,7 +34871,7 @@ ${newTask.description}
34830
34871
  task.updatedAt = now;
34831
34872
  const logEntry = {
34832
34873
  timestamp: now,
34833
- action: paused ? "Task paused" : "Task unpaused"
34874
+ action: paused ? agentOptions?.pausedByAgentId ? `Task paused (agent ${agentOptions.pausedByAgentId} paused)` : "Task paused" : previousPausedByAgentId ? `Task unpaused (agent ${previousPausedByAgentId} resumed)` : "Task unpaused"
34834
34875
  };
34835
34876
  if (runContext) {
34836
34877
  logEntry.runContext = runContext;
@@ -39981,10 +40022,17 @@ var init_docker_client = __esm({
39981
40022
  }
39982
40023
  return this.createDockerInstance(hostConfig);
39983
40024
  }
39984
- async getContainerInfo(containerId) {
40025
+ async getContainerInfo(containerId, hostConfig) {
39985
40026
  try {
39986
- const docker = await this.getInstance();
40027
+ const docker = await this.getDockerInstance(hostConfig);
39987
40028
  const inspect = await docker.getContainer(containerId).inspect();
40029
+ const ports = Object.entries(inspect.NetworkSettings?.Ports ?? {}).reduce((acc, [key, value]) => {
40030
+ const binding = Array.isArray(value) && value.length > 0 ? value[0] : void 0;
40031
+ if (binding?.HostPort) {
40032
+ acc[key] = binding.HostPort;
40033
+ }
40034
+ return acc;
40035
+ }, {});
39988
40036
  return {
39989
40037
  id: inspect.Id,
39990
40038
  name: (inspect.Name ?? "").replace(/^\//, ""),
@@ -39996,8 +40044,12 @@ var init_docker_client = __esm({
39996
40044
  paused: Boolean(inspect.State?.Paused),
39997
40045
  restarting: Boolean(inspect.State?.Restarting),
39998
40046
  dead: Boolean(inspect.State?.Dead),
39999
- error: inspect.State?.Error || void 0
40000
- }
40047
+ error: inspect.State?.Error || void 0,
40048
+ exitCode: typeof inspect.State?.ExitCode === "number" ? inspect.State.ExitCode : void 0,
40049
+ startedAt: inspect.State?.StartedAt || void 0,
40050
+ finishedAt: inspect.State?.FinishedAt || void 0
40051
+ },
40052
+ ports
40001
40053
  };
40002
40054
  } catch (error) {
40003
40055
  const message = toErrorMessage(error);
@@ -40007,6 +40059,18 @@ var init_docker_client = __esm({
40007
40059
  throw error;
40008
40060
  }
40009
40061
  }
40062
+ async getContainerLogs(containerId, hostConfig, options) {
40063
+ const docker = await this.getDockerInstance(hostConfig);
40064
+ const stream = await docker.getContainer(containerId).logs({
40065
+ stdout: true,
40066
+ stderr: true,
40067
+ tail: options?.tail ?? 100
40068
+ });
40069
+ if (Buffer.isBuffer(stream)) {
40070
+ return stream.toString("utf8");
40071
+ }
40072
+ return String(stream ?? "");
40073
+ }
40010
40074
  async getInstance() {
40011
40075
  if (!this.dockerInstance) {
40012
40076
  this.dockerInstance = await this.createDockerInstance(this.defaultHostConfig);
@@ -54525,6 +54589,9 @@ function normalizePattern(pattern) {
54525
54589
  function isExclusionPattern(pattern) {
54526
54590
  return pattern.startsWith("-");
54527
54591
  }
54592
+ function bareSkillName(name) {
54593
+ return name.replace(/\/SKILL\.md$/i, "");
54594
+ }
54528
54595
  function resolveSessionSkills(context) {
54529
54596
  const { requestedSkillNames } = context;
54530
54597
  const projectRootDir = resolveProjectRoot(context.projectRootDir);
@@ -54618,25 +54685,40 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
54618
54685
  const hasRequestedNames = Boolean(requestedSkillNames && requestedSkillNames.length > 0);
54619
54686
  const hasExcluded = excludedSkillPaths.size > 0;
54620
54687
  let filteredSkills;
54688
+ const skillNameMatches = (skill, pattern) => bareSkillName(skill.name).toLowerCase() === bareSkillName(pattern).toLowerCase() || skill.filePath === pattern;
54689
+ const isExcluded = (skill) => {
54690
+ for (const ep of excludedSkillPaths) {
54691
+ if (skillNameMatches(skill, ep)) return true;
54692
+ }
54693
+ return false;
54694
+ };
54695
+ const isAllowed = (skill) => {
54696
+ for (const ap of allowedSkillPaths) {
54697
+ if (skillNameMatches(skill, ap)) return true;
54698
+ }
54699
+ return false;
54700
+ };
54621
54701
  if (hasRequestedNames) {
54622
- const requestedNamesLower = new Set(requestedSkillNames.map((n) => n.toLowerCase()));
54702
+ const requestedBareNamesLower = new Set(requestedSkillNames.map((n) => bareSkillName(n).toLowerCase()));
54623
54703
  filteredSkills = base.skills.filter(
54624
- (skill) => requestedNamesLower.has(skill.name.toLowerCase()) && !excludedSkillPaths.has(skill.filePath)
54704
+ (skill) => requestedBareNamesLower.has(bareSkillName(skill.name).toLowerCase()) && !isExcluded(skill)
54625
54705
  );
54626
54706
  } else if (hasPatterns) {
54627
54707
  filteredSkills = base.skills.filter(
54628
- (skill) => allowedSkillPaths.has(skill.filePath) && !excludedSkillPaths.has(skill.filePath)
54708
+ (skill) => isAllowed(skill) && !isExcluded(skill)
54629
54709
  );
54630
54710
  } else if (hasExcluded) {
54631
- filteredSkills = base.skills.filter((skill) => !excludedSkillPaths.has(skill.filePath));
54711
+ filteredSkills = base.skills.filter((skill) => !isExcluded(skill));
54632
54712
  } else {
54633
54713
  filteredSkills = base.skills;
54634
54714
  }
54635
54715
  const newDiagnostics = [];
54636
54716
  const purpose = sessionPurpose ? ` [${sessionPurpose}]` : "";
54637
- const discoveredPaths = new Set(base.skills.map((s) => s.filePath));
54717
+ const discoveredBareNames = new Set(base.skills.map((s) => bareSkillName(s.name).toLowerCase()));
54718
+ const discoveredFilePaths = new Set(base.skills.map((s) => s.filePath));
54719
+ const hasDiscoveredMatch = (pattern) => discoveredBareNames.has(bareSkillName(pattern).toLowerCase()) || discoveredFilePaths.has(pattern);
54638
54720
  for (const excludedPath of excludedSkillPaths) {
54639
- if (discoveredPaths.has(excludedPath)) {
54721
+ if (hasDiscoveredMatch(excludedPath)) {
54640
54722
  newDiagnostics.push({
54641
54723
  type: "warning",
54642
54724
  message: `Skill at '${excludedPath}' exists but is disabled by project execution settings${purpose}`,
@@ -54645,7 +54727,7 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
54645
54727
  }
54646
54728
  }
54647
54729
  for (const allowedPath of allowedSkillPaths) {
54648
- if (!discoveredPaths.has(allowedPath)) {
54730
+ if (!hasDiscoveredMatch(allowedPath)) {
54649
54731
  newDiagnostics.push({
54650
54732
  type: "warning",
54651
54733
  message: `Configured skill pattern '${allowedPath}' not found in discovered skills${purpose}`,
@@ -54654,9 +54736,9 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
54654
54736
  }
54655
54737
  }
54656
54738
  if (requestedSkillNames) {
54657
- const discoveredNamesLower = new Set(base.skills.map((s) => s.name.toLowerCase()));
54739
+ const discoveredBareNamesLower = new Set(base.skills.map((s) => bareSkillName(s.name).toLowerCase()));
54658
54740
  for (const requestedName of requestedSkillNames) {
54659
- if (!discoveredNamesLower.has(requestedName.toLowerCase()) && !isBuiltInFallbackRequest(requestedName)) {
54741
+ if (!discoveredBareNamesLower.has(bareSkillName(requestedName).toLowerCase()) && !isBuiltInFallbackRequest(requestedName)) {
54660
54742
  const purpose2 = sessionPurpose ? ` [${sessionPurpose}]` : "";
54661
54743
  newDiagnostics.push({
54662
54744
  type: "warning",
@@ -55010,6 +55092,11 @@ async function promptSessionAndCheck(session, prompt, options) {
55010
55092
  piLog.warn(`pi state error \u2014 failed to inspect transcript: ${inspectErr instanceof Error ? inspectErr.message : String(inspectErr)}`);
55011
55093
  }
55012
55094
  }
55095
+ if (/Provider finish_reason:\s*repeat\b/i.test(stateError)) {
55096
+ piLog.warn(`pi state error \u2014 treating provider finish_reason=repeat as soft stop: ${stateError}`);
55097
+ clearSessionStateError(session);
55098
+ return;
55099
+ }
55013
55100
  throw new Error(stateError);
55014
55101
  }
55015
55102
  }
@@ -58191,9 +58278,18 @@ function normalizeAgentSkills(metadataSkills) {
58191
58278
  name = namedEntry.trim();
58192
58279
  }
58193
58280
  }
58194
- if (name && name.length > 0 && !seen.has(name)) {
58195
- seen.add(name);
58196
- result.push(name);
58281
+ if (name && name.length > 0) {
58282
+ if (name.includes("::")) {
58283
+ const idPath = name.split("::").pop();
58284
+ const parts = idPath.replace(/\\/g, "/").split("/").filter(Boolean);
58285
+ if (parts.length >= 2) {
58286
+ name = parts.slice(-2).join("/");
58287
+ }
58288
+ }
58289
+ if (!seen.has(name)) {
58290
+ seen.add(name);
58291
+ result.push(name);
58292
+ }
58197
58293
  }
58198
58294
  }
58199
58295
  return result;
@@ -68816,20 +68912,26 @@ The tool prevents your session from being killed by the inactivity watchdog duri
68816
68912
  if (from !== "in-review" && from !== "done") {
68817
68913
  return task;
68818
68914
  }
68819
- const hasMergeEvidence = Boolean(task.mergeDetails) || (task.mergeRetries ?? 0) > 0 || (task.verificationFailureCount ?? 0) > 0 || task.status === "merging" || task.status === "merging-pr";
68915
+ const hasMergeEvidence = Boolean(task.mergeDetails) || (task.mergeRetries ?? 0) > 0 || (task.verificationFailureCount ?? 0) > 0 || task.status === "merging" || task.status === "merging-pr" || task.status === "merging-fix";
68820
68916
  if (!hasMergeEvidence) {
68821
68917
  return task;
68822
68918
  }
68823
68919
  return this.cleanupMergeStateForReverification(
68824
68920
  task,
68825
- `Task returned to in-progress from ${from} column \u2014 resetting verification steps and merge state for re-verification`
68921
+ `Task returned to in-progress from ${from} column \u2014 resetting verification steps and merge state for re-verification`,
68922
+ {
68923
+ // Keep deterministic merge-verification bounce budget across remediation
68924
+ // cycles. Status may be cleared by intermediate paths, so the counter is
68925
+ // the canonical signal once a bounce has started.
68926
+ preserveVerificationFailureCount: (task.verificationFailureCount ?? 0) > 0
68927
+ }
68826
68928
  );
68827
68929
  }
68828
- async cleanupMergeStateForReverification(task, logMessage) {
68930
+ async cleanupMergeStateForReverification(task, logMessage, options) {
68829
68931
  await this.store.updateTask(task.id, {
68830
68932
  mergeDetails: null,
68831
68933
  mergeRetries: 0,
68832
- verificationFailureCount: 0,
68934
+ verificationFailureCount: options?.preserveVerificationFailureCount ? task.verificationFailureCount ?? 0 : 0,
68833
68935
  workflowStepResults: []
68834
68936
  });
68835
68937
  const refreshedTask = await this.store.getTask(task.id);
@@ -69424,7 +69526,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69424
69526
  };
69425
69527
  const audit = createRunAuditor(this.store, engineRunContext);
69426
69528
  const activeColumns = /* @__PURE__ */ new Set(["in-progress", "in-review", "done"]);
69427
- const activeMergeStatuses = /* @__PURE__ */ new Set(["merging", "merging-pr"]);
69529
+ const activeMergeStatuses = /* @__PURE__ */ new Set(["merging", "merging-pr", "merging-fix"]);
69428
69530
  const isActiveTask = activeColumns.has(task.column) || activeMergeStatuses.has(task.status ?? "");
69429
69531
  if (!isActiveTask) {
69430
69532
  const tasksDir = join36(this.store.getFusionDir(), "tasks");
@@ -69763,7 +69865,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69763
69865
  `${failedType} command \`${failedCommand}\` failed (exit ${failedResult.exitCode}):
69764
69866
  ${summary}`,
69765
69867
  `Verification (${failedType})`,
69766
- `Deterministic verification failed (${failedType})`
69868
+ `Deterministic verification failed (${failedType})`,
69869
+ true,
69870
+ true
69767
69871
  );
69768
69872
  return;
69769
69873
  }
@@ -69809,7 +69913,9 @@ ${summary}`,
69809
69913
  `${failedType} command \`${failedCommand}\` failed (exit ${failedResult.exitCode}) after ${maxFixRetries} fix attempts:
69810
69914
  ${summary}`,
69811
69915
  `Verification (${failedType})`,
69812
- `Deterministic verification failed after ${maxFixRetries} fix attempts`
69916
+ `Deterministic verification failed after ${maxFixRetries} fix attempts`,
69917
+ true,
69918
+ true
69813
69919
  );
69814
69920
  return;
69815
69921
  }
@@ -71498,7 +71604,7 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
71498
71604
  * Injects failure feedback into PROMPT.md, resets steps, clears session,
71499
71605
  * and schedules a move to todo → in-progress after the executing guard clears.
71500
71606
  */
71501
- async sendTaskBackForFix(task, worktreePath, failureFeedback, stepName, reason, preserveResumeState = true) {
71607
+ async sendTaskBackForFix(task, worktreePath, failureFeedback, stepName, reason, preserveResumeState = true, mergeVerificationFailure = false) {
71502
71608
  const taskId = task.id;
71503
71609
  this.clearCompletedTaskWatchdog(taskId);
71504
71610
  await this.store.addTaskComment(
@@ -71517,7 +71623,7 @@ Please fix the issues so the verification can pass on the next attempt.`,
71517
71623
  const updatedTask = await this.store.getTask(taskId);
71518
71624
  await this.reopenLastStepForRevision(taskId, updatedTask);
71519
71625
  await this.store.updateTask(taskId, {
71520
- status: null,
71626
+ status: mergeVerificationFailure ? "merging-fix" : null,
71521
71627
  error: null,
71522
71628
  sessionFile: null,
71523
71629
  workflowStepRetries: 0
@@ -79886,14 +79992,15 @@ var init_self_healing = __esm({
79886
79992
  execAsync7 = promisify9(exec9);
79887
79993
  APPROVED_TRIAGE_RECOVERY_GRACE_MS = 6e4;
79888
79994
  ORPHANED_EXECUTION_RECOVERY_GRACE_MS = 6e4;
79889
- ACTIVE_MERGE_STATUSES = /* @__PURE__ */ new Set(["merging", "merging-pr"]);
79995
+ ACTIVE_MERGE_STATUSES = /* @__PURE__ */ new Set(["merging", "merging-pr", "merging-fix"]);
79890
79996
  NON_TERMINAL_STEP_STATUSES2 = /* @__PURE__ */ new Set(["pending", "in-progress"]);
79891
79997
  GHOST_REVIEW_PRESERVED_STATUSES = /* @__PURE__ */ new Set([
79892
79998
  "failed",
79893
79999
  "awaiting-user-review",
79894
80000
  "awaiting-approval",
79895
80001
  "merging",
79896
- "merging-pr"
80002
+ "merging-pr",
80003
+ "merging-fix"
79897
80004
  ]);
79898
80005
  ORPHANED_WITH_WORKTREE_GRACE_MS = 3e5;
79899
80006
  MAX_TASK_DONE_RETRIES = 3;
@@ -80616,7 +80723,7 @@ var init_self_healing = __esm({
80616
80723
  *
80617
80724
  * Preserved statuses (skipped):
80618
80725
  * - `awaiting-user-review`, `awaiting-approval`: explicit human handoff
80619
- * - `merging`, `merging-pr`: handled by `recoverInterruptedMergingTasks`
80726
+ * - `merging`, `merging-pr`, `merging-fix`: handled by `recoverInterruptedMergingTasks`
80620
80727
  *
80621
80728
  * Rate-limiting comes from the `updatedAt >= taskStuckTimeoutMs` gate —
80622
80729
  * each kick refreshes `updatedAt`, so a task that re-enters review and gets
@@ -86232,7 +86339,7 @@ ${detail}`
86232
86339
  "agent"
86233
86340
  );
86234
86341
  await store.updateTask(taskId, {
86235
- status: null,
86342
+ status: "merging-fix",
86236
86343
  mergeRetries: 0,
86237
86344
  error: null,
86238
86345
  verificationFailureCount: nextBounces
@@ -86240,10 +86347,10 @@ ${detail}`
86240
86347
  await store.moveTask(taskId, "in-progress");
86241
86348
  await store.logEntry(
86242
86349
  taskId,
86243
- `Deterministic ${failedKind} verification failed (${nextBounces}/${cap}) \u2014 moved back to in-progress for remediation`
86350
+ `Deterministic ${failedKind} verification failed (${nextBounces}/${cap}) \u2014 moved back to in-progress with status=merging-fix for remediation`
86244
86351
  );
86245
86352
  runtimeLog.log(
86246
- `Auto-merge: ${taskId} deterministic ${failedKind} verification failed (${nextBounces}/${cap}) \u2014 moved to in-progress`
86353
+ `Auto-merge: ${taskId} deterministic ${failedKind} verification failed (${nextBounces}/${cap}) \u2014 moved to in-progress with status=merging-fix`
86247
86354
  );
86248
86355
  } catch {
86249
86356
  runtimeLog.error(
@@ -97230,10 +97337,18 @@ var init_claude_cli_probe = __esm({
97230
97337
  }
97231
97338
  });
97232
97339
 
97340
+ // ../../plugins/fusion-plugin-droid-runtime/dist/probe.js
97341
+ var init_probe3 = __esm({
97342
+ "../../plugins/fusion-plugin-droid-runtime/dist/probe.js"() {
97343
+ "use strict";
97344
+ }
97345
+ });
97346
+
97233
97347
  // ../dashboard/src/droid-cli-probe.ts
97234
97348
  var init_droid_cli_probe = __esm({
97235
97349
  "../dashboard/src/droid-cli-probe.ts"() {
97236
97350
  "use strict";
97351
+ init_probe3();
97237
97352
  }
97238
97353
  });
97239
97354
 
@@ -103962,9 +104077,12 @@ async function getResearchAvailability(store) {
103962
104077
  }
103963
104078
  const backend = resolved.searchProvider ?? settings.researchWebSearchProvider;
103964
104079
  const configured = backend === "searxng" ? Boolean(settings.researchSearxngUrl) : backend === "brave" ? Boolean(settings.researchBraveApiKey) : backend === "google" ? Boolean(settings.researchGoogleSearchApiKey && settings.researchGoogleSearchCx) : backend === "tavily" ? Boolean(settings.researchTavilyApiKey) : false;
103965
- if (!configured && !resolved.searchProvider) {
104080
+ if (!backend) {
103966
104081
  return { ok: false, code: "provider-unavailable", message: "Research provider is not configured. Set research provider credentials in Settings." };
103967
104082
  }
104083
+ if (!configured) {
104084
+ return { ok: false, code: "missing-credentials", message: `Missing credentials for ${backend}. Add provider keys in Authentication and verify Research defaults.` };
104085
+ }
103968
104086
  return { ok: true };
103969
104087
  }
103970
104088
  function toResearchRunDetails(run) {
@@ -104355,12 +104473,13 @@ Path: .fusion/tasks/${params.id}/attachments/${attachment.filename}`
104355
104473
  pi.registerTool({
104356
104474
  name: "fn_task_retry",
104357
104475
  label: "fn: Retry Task",
104358
- description: "Retry a failed task \u2014 clears the error state and moves it back to the todo column for re-execution.",
104359
- promptSnippet: "Retry a failed Fusion task (clears error, moves to todo)",
104476
+ description: "Retry a failed task \u2014 clears the error state. Tasks in other columns move to todo; tasks in in-review stay in-place for auto-merge retry.",
104477
+ promptSnippet: "Retry a failed Fusion task (clears error, moves to todo or stays in in-review)",
104360
104478
  promptGuidelines: [
104361
- "Use when a task has failed and needs to be retried from the beginning",
104362
- "Only tasks in 'failed' state can be retried",
104363
- "The task will be moved to the todo column with error state cleared"
104479
+ "Use when a task has failed and needs to be retried",
104480
+ "Only tasks in 'failed' or 'stuck-killed' state can be retried",
104481
+ "Tasks in 'in-review' stay in in-review \u2014 only the error/retry state is cleared, and the auto-merge system re-attempts",
104482
+ "Tasks in other columns are moved to the todo column with error state cleared"
104364
104483
  ],
104365
104484
  parameters: Type8.Object({
104366
104485
  id: Type8.String({ description: "Task ID to retry (e.g. FN-001). Must be in 'failed' state." })
@@ -104384,6 +104503,14 @@ Path: .fusion/tasks/${params.id}/attachments/${attachment.filename}`
104384
104503
  details: { taskId: params.id, currentStatus: task.status }
104385
104504
  };
104386
104505
  }
104506
+ if (task.column === "in-review") {
104507
+ await store.updateTask(params.id, { status: null, error: null, stuckKillCount: 0, mergeRetries: 0 });
104508
+ await store.logEntry(params.id, "Retry requested via Fusion extension (in-review retry, mergeRetries reset)");
104509
+ return {
104510
+ content: [{ type: "text", text: `Retried ${params.id} \u2192 in-review (merge retry state cleared, task stays in in-review)` }],
104511
+ details: { taskId: params.id, newColumn: "in-review" }
104512
+ };
104513
+ }
104387
104514
  await store.updateTask(params.id, { status: null, error: null });
104388
104515
  await store.moveTask(params.id, "todo");
104389
104516
  await store.logEntry(params.id, "Retry requested via Fusion extension", "Task reset to todo for retry");
@@ -104875,7 +105002,7 @@ Planning session completed. Task ${taskId} is now in planning and will be auto-p
104875
105002
  pi.registerTool({
104876
105003
  name: "fn_research_cancel",
104877
105004
  label: "fn: Cancel Research Run",
104878
- description: "Cancel a research run.",
105005
+ description: "Cancel an in-flight research run. Terminal runs return INVALID_TRANSITION.",
104879
105006
  parameters: Type8.Object({ id: Type8.String({ description: "Research run ID" }) }),
104880
105007
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
104881
105008
  const store = await getStore2(ctx.cwd);
@@ -104887,6 +105014,17 @@ Planning session completed. Task ${taskId} is now in planning and will be auto-p
104887
105014
  details: { runId: params.id, status: "missing", summary: null, findings: [], citations: [], error: "not found", setup: null }
104888
105015
  };
104889
105016
  }
105017
+ if (!["queued", "running", "cancelling", "retry_waiting"].includes(run.status)) {
105018
+ return {
105019
+ content: [{ type: "text", text: `Research run ${params.id} cannot be cancelled from status ${run.status}.` }],
105020
+ isError: true,
105021
+ details: {
105022
+ ...toResearchRunDetails(run),
105023
+ error: "invalid transition",
105024
+ setup: { code: "INVALID_TRANSITION", message: "Cancel is only available for queued/running/cancelling/retry_waiting runs." }
105025
+ }
105026
+ };
105027
+ }
104890
105028
  const updated = researchStore.requestCancellation(params.id);
104891
105029
  return {
104892
105030
  content: [{ type: "text", text: `Requested cancellation for research run ${params.id} (status: ${updated.status}).` }],
@@ -104894,6 +105032,41 @@ Planning session completed. Task ${taskId} is now in planning and will be auto-p
104894
105032
  };
104895
105033
  }
104896
105034
  });
105035
+ pi.registerTool({
105036
+ name: "fn_research_retry",
105037
+ label: "fn: Retry Research Run",
105038
+ description: "Retry a failed research run when lifecycle marks it retryable.",
105039
+ parameters: Type8.Object({ id: Type8.String({ description: "Research run ID" }) }),
105040
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105041
+ const store = await getStore2(ctx.cwd);
105042
+ const researchStore = store.getResearchStore();
105043
+ const run = researchStore.getRun(params.id);
105044
+ if (!run) {
105045
+ return {
105046
+ content: [{ type: "text", text: `Research run ${params.id} not found.` }],
105047
+ isError: true,
105048
+ details: { runId: params.id, status: "missing", summary: null, findings: [], citations: [], error: "not found", setup: null }
105049
+ };
105050
+ }
105051
+ const isRetryExhausted = run.status === "retry_exhausted" || run.lifecycle?.errorCode === "RETRY_EXHAUSTED";
105052
+ if (run.status !== "failed" && run.status !== "timed_out" || run.lifecycle?.retryable === false || isRetryExhausted) {
105053
+ return {
105054
+ content: [{ type: "text", text: `Research run ${params.id} is not retryable from status ${run.status}.` }],
105055
+ isError: true,
105056
+ details: {
105057
+ ...toResearchRunDetails(run),
105058
+ error: "not retryable",
105059
+ setup: { code: isRetryExhausted ? "RETRY_EXHAUSTED" : "INVALID_TRANSITION", message: "Retry is only available for failed/timed_out retryable runs." }
105060
+ }
105061
+ };
105062
+ }
105063
+ const retryRun = researchStore.createRetryRun(params.id);
105064
+ return {
105065
+ content: [{ type: "text", text: `Created retry run ${retryRun.id} from ${params.id}.` }],
105066
+ details: toResearchRunDetails(retryRun)
105067
+ };
105068
+ }
105069
+ });
104897
105070
  pi.registerTool({
104898
105071
  name: "fn_insight_list",
104899
105072
  label: "fn: List Insights",
@@ -105590,6 +105763,278 @@ Status: ${updated.status}`
105590
105763
  };
105591
105764
  }
105592
105765
  });
105766
+ pi.registerTool({
105767
+ name: "fn_list_agents",
105768
+ label: "fn: List Agents",
105769
+ description: "List all available agents in the system. Shows each agent's name, role, state, personality (soul), and current assignment. Use this to discover which agents exist and what they specialize in before delegating work.",
105770
+ promptSnippet: "List all available Fusion agents",
105771
+ promptGuidelines: [
105772
+ "Use fn_list_agents to discover which agents exist before delegating work",
105773
+ "Filter by role or state to narrow results",
105774
+ "Ephemeral/runtime agents are excluded by default"
105775
+ ],
105776
+ parameters: Type8.Object({
105777
+ role: Type8.Optional(
105778
+ Type8.String({ description: "Filter by agent role/capability (e.g., 'executor', 'reviewer', 'qa')" })
105779
+ ),
105780
+ state: Type8.Optional(
105781
+ Type8.String({ description: "Filter by agent state (e.g., 'idle', 'active', 'running')" })
105782
+ ),
105783
+ includeEphemeral: Type8.Optional(
105784
+ Type8.Boolean({ description: "Include ephemeral/runtime agents (default: false)" })
105785
+ )
105786
+ }),
105787
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105788
+ const { AgentStore: AgentStore2 } = await Promise.resolve().then(() => (init_src(), src_exports));
105789
+ const agentStore = new AgentStore2({ rootDir: getFusionDir(ctx.cwd) });
105790
+ await agentStore.init();
105791
+ const filter = {};
105792
+ if (params.role) filter.role = params.role;
105793
+ if (params.state) filter.state = params.state;
105794
+ if (params.includeEphemeral !== void 0) filter.includeEphemeral = params.includeEphemeral;
105795
+ const agents = await agentStore.listAgents(filter);
105796
+ if (agents.length === 0) {
105797
+ return {
105798
+ content: [{ type: "text", text: "No agents found matching the specified filters." }],
105799
+ details: { agents: [], count: 0 }
105800
+ };
105801
+ }
105802
+ const lines = agents.map((agent) => {
105803
+ const parts = [
105804
+ `ID: ${agent.id}`,
105805
+ `Name: ${agent.name}`,
105806
+ `Role: ${agent.role}`,
105807
+ `State: ${agent.state}`
105808
+ ];
105809
+ if (agent.title) parts.push(`Title: ${agent.title}`);
105810
+ if (agent.soul) parts.push(`Soul: ${agent.soul.slice(0, 200)}`);
105811
+ if (agent.instructionsText) {
105812
+ const snippet = agent.instructionsText.slice(0, 100);
105813
+ parts.push(`Custom Instructions: ${snippet}${agent.instructionsText.length > 100 ? "\u2026" : ""}`);
105814
+ }
105815
+ if (agent.taskId) parts.push(`Current Task: ${agent.taskId}`);
105816
+ return parts.join("\n");
105817
+ });
105818
+ return {
105819
+ content: [{ type: "text", text: `Available agents (${agents.length}):
105820
+
105821
+ ${lines.join("\n\n")}` }],
105822
+ details: { agents, count: agents.length }
105823
+ };
105824
+ }
105825
+ });
105826
+ pi.registerTool({
105827
+ name: "fn_delegate_task",
105828
+ label: "fn: Delegate Task",
105829
+ description: "Create a new task and assign it to a specific agent for execution. The task goes to 'todo' and will be picked up by the target agent on their next heartbeat cycle. Use fn_list_agents first to find available agents and their capabilities.",
105830
+ promptSnippet: "Delegate a task to a specific Fusion agent",
105831
+ promptGuidelines: [
105832
+ "Use fn_list_agents first to find available agents and their capabilities",
105833
+ "The task is created in 'todo' and assigned to the target agent",
105834
+ "Cannot delegate to ephemeral/runtime agents",
105835
+ "Optionally specify dependencies on other tasks"
105836
+ ],
105837
+ parameters: Type8.Object({
105838
+ agent_id: Type8.String({ description: "The agent ID to delegate work to" }),
105839
+ description: Type8.String({ description: "What needs to be done" }),
105840
+ dependencies: Type8.Optional(
105841
+ Type8.Array(Type8.String(), { description: 'Task IDs this new task depends on (e.g. ["KB-001"]' })
105842
+ )
105843
+ }),
105844
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105845
+ const agentError = await validateAssignableAgentId(ctx.cwd, params.agent_id);
105846
+ if (agentError) {
105847
+ return {
105848
+ content: [{ type: "text", text: `ERROR: ${agentError}` }],
105849
+ isError: true,
105850
+ details: { error: agentError }
105851
+ };
105852
+ }
105853
+ const { AgentStore: AgentStore2 } = await Promise.resolve().then(() => (init_src(), src_exports));
105854
+ const agentStore = new AgentStore2({ rootDir: getFusionDir(ctx.cwd) });
105855
+ await agentStore.init();
105856
+ const agent = await agentStore.getAgent(params.agent_id);
105857
+ const store = await getStore2(ctx.cwd);
105858
+ const task = await store.createTask({
105859
+ description: params.description,
105860
+ dependencies: params.dependencies,
105861
+ column: "todo",
105862
+ assignedAgentId: params.agent_id,
105863
+ source: { sourceType: "api" }
105864
+ });
105865
+ const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
105866
+ return {
105867
+ content: [{
105868
+ type: "text",
105869
+ text: `Delegated to ${agent.name} (${agent.id}): Created ${task.id}${deps}. The task will be picked up by ${agent.name} on their next heartbeat cycle.`
105870
+ }],
105871
+ details: { taskId: task.id, agentId: agent.id, agentName: agent.name }
105872
+ };
105873
+ }
105874
+ });
105875
+ pi.registerTool({
105876
+ name: "fn_agent_show",
105877
+ label: "fn: Show Agent",
105878
+ description: "Show detailed information about a single agent, including their role, state, position in the org hierarchy (reports-to, direct reports), skills, and current assignment.",
105879
+ promptSnippet: "Show details of a specific Fusion agent",
105880
+ promptGuidelines: [
105881
+ "Use to get full details about a specific agent",
105882
+ "Provide agent ID or a resolvable name",
105883
+ "Shows the agent's position in the org hierarchy"
105884
+ ],
105885
+ parameters: Type8.Object({
105886
+ id: Type8.String({ description: "Agent ID or resolvable name" })
105887
+ }),
105888
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105889
+ const { AgentStore: AgentStore2 } = await Promise.resolve().then(() => (init_src(), src_exports));
105890
+ const agentStore = new AgentStore2({ rootDir: getFusionDir(ctx.cwd) });
105891
+ await agentStore.init();
105892
+ const agent = await agentStore.resolveAgent(params.id);
105893
+ if (!agent) {
105894
+ return {
105895
+ content: [{ type: "text", text: `Agent '${params.id}' not found` }],
105896
+ isError: true,
105897
+ details: { error: "Agent not found" }
105898
+ };
105899
+ }
105900
+ const directReports = await agentStore.getAgentsByReportsTo(agent.id);
105901
+ const parts = [
105902
+ `ID: ${agent.id}`,
105903
+ `Name: ${agent.name}`,
105904
+ `Role: ${agent.role}`,
105905
+ `State: ${agent.state}`
105906
+ ];
105907
+ if (agent.title) parts.push(`Title: ${agent.title}`);
105908
+ if (agent.icon) parts.push(`Icon: ${agent.icon}`);
105909
+ if (agent.reportsTo) {
105910
+ const manager = await agentStore.getAgent(agent.reportsTo);
105911
+ if (manager) {
105912
+ parts.push(`Reports To: ${manager.name} (${manager.id})`);
105913
+ } else {
105914
+ parts.push(`Reports To: ${agent.reportsTo}`);
105915
+ }
105916
+ }
105917
+ if (directReports.length > 0) {
105918
+ parts.push(`Direct Reports: ${directReports.map((r) => `${r.name} (${r.id})`).join(", ")}`);
105919
+ }
105920
+ if (agent.taskId) parts.push(`Current Task: ${agent.taskId}`);
105921
+ if (agent.instructionsText) {
105922
+ const snippet = agent.instructionsText.slice(0, 100);
105923
+ parts.push(`Custom Instructions: ${snippet}${agent.instructionsText.length > 100 ? "\u2026" : ""}`);
105924
+ }
105925
+ if (agent.soul) {
105926
+ const snippet = agent.soul.slice(0, 200);
105927
+ parts.push(`Soul: ${snippet}${agent.soul.length > 200 ? "\u2026" : ""}`);
105928
+ }
105929
+ if (agent.metadata?.skills) {
105930
+ parts.push(`Skills: ${JSON.stringify(agent.metadata.skills)}`);
105931
+ }
105932
+ return {
105933
+ content: [{ type: "text", text: parts.join("\n") }],
105934
+ details: {
105935
+ agent,
105936
+ directReports: directReports.map((r) => ({ id: r.id, name: r.name, role: r.role }))
105937
+ }
105938
+ };
105939
+ }
105940
+ });
105941
+ pi.registerTool({
105942
+ name: "fn_agent_org_chart",
105943
+ label: "fn: Agent Org Chart",
105944
+ description: "Show the organizational tree of agents, displaying the role hierarchy. Optionally filter to a subtree rooted at a specific agent.",
105945
+ promptSnippet: "Show the Fusion agent org chart",
105946
+ promptGuidelines: [
105947
+ "Use to understand the team structure and reporting hierarchy",
105948
+ "Optionally specify a root agent to see only their subtree",
105949
+ "Ephemeral/runtime agents are excluded by default"
105950
+ ],
105951
+ parameters: Type8.Object({
105952
+ root_agent_id: Type8.Optional(
105953
+ Type8.String({ description: "If provided, show only the subtree rooted at this agent" })
105954
+ ),
105955
+ include_ephemeral: Type8.Optional(
105956
+ Type8.Boolean({ description: "Include ephemeral/runtime agents (default: false)" })
105957
+ )
105958
+ }),
105959
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105960
+ const { AgentStore: AgentStore2 } = await Promise.resolve().then(() => (init_src(), src_exports));
105961
+ const agentStore = new AgentStore2({ rootDir: getFusionDir(ctx.cwd) });
105962
+ await agentStore.init();
105963
+ const includeEphemeral = params.include_ephemeral ?? false;
105964
+ if (params.root_agent_id) {
105965
+ const rootAgent = await agentStore.resolveAgent(params.root_agent_id);
105966
+ if (!rootAgent) {
105967
+ return {
105968
+ content: [{ type: "text", text: `Agent '${params.root_agent_id}' not found` }],
105969
+ isError: true,
105970
+ details: { error: "Root agent not found" }
105971
+ };
105972
+ }
105973
+ const fullTree = await agentStore.getOrgTree({ includeEphemeral });
105974
+ const findSubtree = (nodes) => {
105975
+ for (const node of nodes) {
105976
+ if (node.agent.id === rootAgent.id) return node;
105977
+ const found = findSubtree(node.children);
105978
+ if (found) return found;
105979
+ }
105980
+ return null;
105981
+ };
105982
+ const subtree = findSubtree(fullTree);
105983
+ if (!subtree) {
105984
+ return {
105985
+ content: [{
105986
+ type: "text",
105987
+ text: `${rootAgent.icon ?? "\u{1F916}"} ${rootAgent.name} (${rootAgent.role}) \u2014 ${rootAgent.state}${rootAgent.taskId ? ` [${rootAgent.taskId}]` : ""}`
105988
+ }],
105989
+ details: { tree: [{ agent: rootAgent, children: [] }] }
105990
+ };
105991
+ }
105992
+ const lines2 = [];
105993
+ const renderNode2 = (node, indent) => {
105994
+ const a = node.agent;
105995
+ lines2.push(
105996
+ `${indent}${a.icon ?? "\u{1F916}"} ${a.name} (${a.role}) \u2014 ${a.state}${a.taskId ? ` [${a.taskId}]` : ""}`
105997
+ );
105998
+ for (const child of node.children) {
105999
+ renderNode2(child, indent + " ");
106000
+ }
106001
+ };
106002
+ renderNode2(subtree, "");
106003
+ return {
106004
+ content: [{ type: "text", text: `Agent Org Tree (subtree: ${rootAgent.name}):
106005
+ ${lines2.join("\n")}` }],
106006
+ details: { tree: [subtree] }
106007
+ };
106008
+ }
106009
+ const tree = await agentStore.getOrgTree({ includeEphemeral });
106010
+ if (tree.length === 0) {
106011
+ return {
106012
+ content: [{ type: "text", text: "No agents found." }],
106013
+ details: { tree: [], count: 0 }
106014
+ };
106015
+ }
106016
+ const lines = [];
106017
+ let count = 0;
106018
+ const renderNode = (node, indent) => {
106019
+ const a = node.agent;
106020
+ lines.push(
106021
+ `${indent}${a.icon ?? "\u{1F916}"} ${a.name} (${a.role}) \u2014 ${a.state}${a.taskId ? ` [${a.taskId}]` : ""}`
106022
+ );
106023
+ count++;
106024
+ for (const child of node.children) {
106025
+ renderNode(child, indent + " ");
106026
+ }
106027
+ };
106028
+ for (const root of tree) {
106029
+ renderNode(root, "");
106030
+ }
106031
+ return {
106032
+ content: [{ type: "text", text: `Agent Org Tree (${count} agents):
106033
+ ${lines.join("\n")}` }],
106034
+ details: { tree, count }
106035
+ };
106036
+ }
106037
+ });
105593
106038
  pi.registerTool({
105594
106039
  name: "fn_skills_search",
105595
106040
  label: "FN: Search Skills",