@runfusion/fusion 0.17.2 → 0.18.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 (57) hide show
  1. package/dist/bin.js +1035 -660
  2. package/dist/client/assets/ChatView-3Sqm6teN.js +1 -0
  3. package/dist/client/assets/{DevServerView-GFFVXHVP.js → DevServerView-r6V3FqkY.js} +1 -1
  4. package/dist/client/assets/DirectoryPicker-CTZE95Fk.js +1 -0
  5. package/dist/client/assets/DocumentsView-DSEf1Lmg.js +1 -0
  6. package/dist/client/assets/{InsightsView-Bxu0TJkt.js → InsightsView-F5PZsX5u.js} +2 -2
  7. package/dist/client/assets/MemoryView-DicXjec9.js +2 -0
  8. package/dist/client/assets/NodesView-DddCS7zB.js +14 -0
  9. package/dist/client/assets/NodesView-sJgPLTzz.css +1 -0
  10. package/dist/client/assets/{PiExtensionsManager-4e3MlD62.js → PiExtensionsManager-Ch7si-v8.js} +3 -3
  11. package/dist/client/assets/PluginManager-LcTh_fHP.js +1 -0
  12. package/dist/client/assets/ResearchView-D0TY1VcX.js +1 -0
  13. package/dist/client/assets/{RoadmapsView-jHTOK0RQ.js → RoadmapsView-DfEF3mql.js} +2 -2
  14. package/dist/client/assets/SettingsModal-BNSrO1M9.css +1 -0
  15. package/dist/client/assets/SettingsModal-SOADcCNJ.js +31 -0
  16. package/dist/client/assets/{SettingsModal-4Z8ZJMzD.js → SettingsModal-YcScdFiG.js} +1 -1
  17. package/dist/client/assets/SettingsModal-oOnIed5O.css +1 -0
  18. package/dist/client/assets/SetupWizardModal-EDYuf9Yc.js +1 -0
  19. package/dist/client/assets/SkillsView-Dkq2CQla.js +1 -0
  20. package/dist/client/assets/index-4hC8zoTD.css +1 -0
  21. package/dist/client/assets/index-DNzA4aZ7.js +1229 -0
  22. package/dist/client/assets/{users-D3u6f2Rz.js → users-Cp5TSxVm.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 +505 -70
  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.
@@ -10542,50 +10549,40 @@ ${feature.acceptanceCriteria}`);
10542
10549
  }
10543
10550
  }
10544
10551
  // ── ID Generators ───────────────────────────────────────────────────
10545
- generateMissionId() {
10546
- const timestamp = Date.now();
10552
+ idSequence = 0;
10553
+ generateId(prefix) {
10554
+ const timestamp = Date.now().toString(36).toUpperCase();
10555
+ this.idSequence += 1;
10556
+ const sequence = this.idSequence.toString(36).toUpperCase().padStart(4, "0");
10547
10557
  const random = Math.random().toString(36).substring(2, 6).toUpperCase();
10548
- return `M-${timestamp.toString(36).toUpperCase()}-${random}`;
10558
+ return `${prefix}-${timestamp}-${sequence}-${random}`;
10559
+ }
10560
+ generateMissionId() {
10561
+ return this.generateId("M");
10549
10562
  }
10550
10563
  generateMilestoneId() {
10551
- const timestamp = Date.now();
10552
- const random = Math.random().toString(36).substring(2, 6).toUpperCase();
10553
- return `MS-${timestamp.toString(36).toUpperCase()}-${random}`;
10564
+ return this.generateId("MS");
10554
10565
  }
10555
10566
  generateSliceId() {
10556
- const timestamp = Date.now();
10557
- const random = Math.random().toString(36).substring(2, 6).toUpperCase();
10558
- return `SL-${timestamp.toString(36).toUpperCase()}-${random}`;
10567
+ return this.generateId("SL");
10559
10568
  }
10560
10569
  generateFeatureId() {
10561
- const timestamp = Date.now();
10562
- const random = Math.random().toString(36).substring(2, 6).toUpperCase();
10563
- return `F-${timestamp.toString(36).toUpperCase()}-${random}`;
10570
+ return this.generateId("F");
10564
10571
  }
10565
10572
  generateMissionEventId() {
10566
- const timestamp = Date.now();
10567
- const random = Math.random().toString(36).substring(2, 6).toUpperCase();
10568
- return `ME-${timestamp.toString(36).toUpperCase()}-${random}`;
10573
+ return this.generateId("ME");
10569
10574
  }
10570
10575
  generateAssertionId() {
10571
- const timestamp = Date.now();
10572
- const random = Math.random().toString(36).substring(2, 6).toUpperCase();
10573
- return `CA-${timestamp.toString(36).toUpperCase()}-${random}`;
10576
+ return this.generateId("CA");
10574
10577
  }
10575
10578
  generateValidatorRunId() {
10576
- const timestamp = Date.now();
10577
- const random = Math.random().toString(36).substring(2, 6).toUpperCase();
10578
- return `VR-${timestamp.toString(36).toUpperCase()}-${random}`;
10579
+ return this.generateId("VR");
10579
10580
  }
10580
10581
  generateFailureId() {
10581
- const timestamp = Date.now();
10582
- const random = Math.random().toString(36).substring(2, 6).toUpperCase();
10583
- return `VF-${timestamp.toString(36).toUpperCase()}-${random}`;
10582
+ return this.generateId("VF");
10584
10583
  }
10585
10584
  generateLineageId() {
10586
- const timestamp = Date.now();
10587
- const random = Math.random().toString(36).substring(2, 6).toUpperCase();
10588
- return `FL-${timestamp.toString(36).toUpperCase()}-${random}`;
10585
+ return this.generateId("FL");
10589
10586
  }
10590
10587
  };
10591
10588
  }
@@ -32645,6 +32642,7 @@ var init_store = __esm({
32645
32642
  missionId: row.missionId || void 0,
32646
32643
  sliceId: row.sliceId || void 0,
32647
32644
  assignedAgentId: row.assignedAgentId || void 0,
32645
+ pausedByAgentId: row.pausedByAgentId || void 0,
32648
32646
  assigneeUserId: row.assigneeUserId || void 0,
32649
32647
  nodeId: row.nodeId || void 0,
32650
32648
  effectiveNodeId: row.effectiveNodeId || void 0,
@@ -32909,6 +32907,7 @@ ${recentText}` : void 0
32909
32907
  "missionId",
32910
32908
  "sliceId",
32911
32909
  "assignedAgentId",
32910
+ "pausedByAgentId",
32912
32911
  "assigneeUserId",
32913
32912
  "nodeId",
32914
32913
  "effectiveNodeId",
@@ -33021,6 +33020,7 @@ ${outcome}`;
33021
33020
  "missionId",
33022
33021
  "sliceId",
33023
33022
  "assignedAgentId",
33023
+ "pausedByAgentId",
33024
33024
  "assigneeUserId",
33025
33025
  "nodeId",
33026
33026
  "effectiveNodeId",
@@ -33071,9 +33071,9 @@ ${outcome}`;
33071
33071
  dependencies, steps, log, attachments, steeringComments,
33072
33072
  comments, workflowStepResults, prInfo, issueInfo,
33073
33073
  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
33074
+ mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt
33075
33075
  ) VALUES (
33076
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
33076
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
33077
33077
  )
33078
33078
  ON CONFLICT(id) DO UPDATE SET
33079
33079
  title = excluded.title,
@@ -33142,6 +33142,7 @@ ${outcome}`;
33142
33142
  missionId = excluded.missionId,
33143
33143
  sliceId = excluded.sliceId,
33144
33144
  assignedAgentId = excluded.assignedAgentId,
33145
+ pausedByAgentId = excluded.pausedByAgentId,
33145
33146
  assigneeUserId = excluded.assigneeUserId,
33146
33147
  nodeId = excluded.nodeId,
33147
33148
  effectiveNodeId = excluded.effectiveNodeId,
@@ -33223,6 +33224,7 @@ ${outcome}`;
33223
33224
  task.missionId ?? null,
33224
33225
  task.sliceId ?? null,
33225
33226
  task.assignedAgentId ?? null,
33227
+ task.pausedByAgentId ?? null,
33226
33228
  task.assigneeUserId ?? null,
33227
33229
  task.nodeId ?? null,
33228
33230
  task.effectiveNodeId ?? null,
@@ -34340,6 +34342,23 @@ ${newTask.description}
34340
34342
  const matches = [...activeMatches, ...archiveMatches];
34341
34343
  return limit >= 0 ? matches.slice(0, limit) : matches;
34342
34344
  }
34345
+ async getTasksByAssignedAgent(agentId, options) {
34346
+ const whereClauses = ["assignedAgentId = ?"];
34347
+ const params = [agentId];
34348
+ if (options?.pausedOnly) {
34349
+ whereClauses.push("paused = 1");
34350
+ }
34351
+ if (options?.excludeArchived) {
34352
+ whereClauses.push(`"column" != 'archived'`);
34353
+ }
34354
+ const selectClause = this.getTaskSelectClause(false);
34355
+ const rows = this.db.prepare(`
34356
+ SELECT ${selectClause} FROM tasks
34357
+ WHERE ${whereClauses.join(" AND ")}
34358
+ ORDER BY createdAt ASC
34359
+ `).all(...params);
34360
+ return rows.map((row) => this.rowToTask(row));
34361
+ }
34343
34362
  async selectNextTaskForAgent(agentId) {
34344
34363
  const tasks = await this.listTasks({ slim: true });
34345
34364
  if (tasks.length === 0) {
@@ -34577,6 +34596,11 @@ ${newTask.description}
34577
34596
  } else if (updates.assignedAgentId !== void 0) {
34578
34597
  task.assignedAgentId = updates.assignedAgentId;
34579
34598
  }
34599
+ if (updates.pausedByAgentId === null) {
34600
+ task.pausedByAgentId = void 0;
34601
+ } else if (updates.pausedByAgentId !== void 0) {
34602
+ task.pausedByAgentId = updates.pausedByAgentId;
34603
+ }
34580
34604
  if (updates.assigneeUserId === null) {
34581
34605
  task.assigneeUserId = void 0;
34582
34606
  } else if (updates.assigneeUserId !== void 0) {
@@ -34815,14 +34839,21 @@ ${newTask.description}
34815
34839
  * Pause or unpause a task. Paused tasks are excluded from all automated
34816
34840
  * agent and scheduler interaction. Logs the action and emits `task:updated`.
34817
34841
  */
34818
- async pauseTask(id, paused, runContext) {
34842
+ async pauseTask(id, paused, runContext, agentOptions) {
34819
34843
  return this.withTaskLock(id, async () => {
34820
34844
  const dir = this.taskDir(id);
34821
34845
  const task = await this.readTaskJson(dir);
34822
34846
  if (!task.log) {
34823
34847
  task.log = [];
34824
34848
  }
34849
+ const previousPausedByAgentId = task.pausedByAgentId;
34825
34850
  task.paused = paused || void 0;
34851
+ if (paused && agentOptions?.pausedByAgentId) {
34852
+ task.pausedByAgentId = agentOptions.pausedByAgentId;
34853
+ }
34854
+ if (!paused) {
34855
+ task.pausedByAgentId = void 0;
34856
+ }
34826
34857
  if (task.column === "in-progress" || task.column === "in-review") {
34827
34858
  task.status = paused ? "paused" : void 0;
34828
34859
  }
@@ -34830,7 +34861,7 @@ ${newTask.description}
34830
34861
  task.updatedAt = now;
34831
34862
  const logEntry = {
34832
34863
  timestamp: now,
34833
- action: paused ? "Task paused" : "Task unpaused"
34864
+ action: paused ? agentOptions?.pausedByAgentId ? `Task paused (agent ${agentOptions.pausedByAgentId} paused)` : "Task paused" : previousPausedByAgentId ? `Task unpaused (agent ${previousPausedByAgentId} resumed)` : "Task unpaused"
34834
34865
  };
34835
34866
  if (runContext) {
34836
34867
  logEntry.runContext = runContext;
@@ -39981,10 +40012,17 @@ var init_docker_client = __esm({
39981
40012
  }
39982
40013
  return this.createDockerInstance(hostConfig);
39983
40014
  }
39984
- async getContainerInfo(containerId) {
40015
+ async getContainerInfo(containerId, hostConfig) {
39985
40016
  try {
39986
- const docker = await this.getInstance();
40017
+ const docker = await this.getDockerInstance(hostConfig);
39987
40018
  const inspect = await docker.getContainer(containerId).inspect();
40019
+ const ports = Object.entries(inspect.NetworkSettings?.Ports ?? {}).reduce((acc, [key, value]) => {
40020
+ const binding = Array.isArray(value) && value.length > 0 ? value[0] : void 0;
40021
+ if (binding?.HostPort) {
40022
+ acc[key] = binding.HostPort;
40023
+ }
40024
+ return acc;
40025
+ }, {});
39988
40026
  return {
39989
40027
  id: inspect.Id,
39990
40028
  name: (inspect.Name ?? "").replace(/^\//, ""),
@@ -39996,8 +40034,12 @@ var init_docker_client = __esm({
39996
40034
  paused: Boolean(inspect.State?.Paused),
39997
40035
  restarting: Boolean(inspect.State?.Restarting),
39998
40036
  dead: Boolean(inspect.State?.Dead),
39999
- error: inspect.State?.Error || void 0
40000
- }
40037
+ error: inspect.State?.Error || void 0,
40038
+ exitCode: typeof inspect.State?.ExitCode === "number" ? inspect.State.ExitCode : void 0,
40039
+ startedAt: inspect.State?.StartedAt || void 0,
40040
+ finishedAt: inspect.State?.FinishedAt || void 0
40041
+ },
40042
+ ports
40001
40043
  };
40002
40044
  } catch (error) {
40003
40045
  const message = toErrorMessage(error);
@@ -40007,6 +40049,18 @@ var init_docker_client = __esm({
40007
40049
  throw error;
40008
40050
  }
40009
40051
  }
40052
+ async getContainerLogs(containerId, hostConfig, options) {
40053
+ const docker = await this.getDockerInstance(hostConfig);
40054
+ const stream = await docker.getContainer(containerId).logs({
40055
+ stdout: true,
40056
+ stderr: true,
40057
+ tail: options?.tail ?? 100
40058
+ });
40059
+ if (Buffer.isBuffer(stream)) {
40060
+ return stream.toString("utf8");
40061
+ }
40062
+ return String(stream ?? "");
40063
+ }
40010
40064
  async getInstance() {
40011
40065
  if (!this.dockerInstance) {
40012
40066
  this.dockerInstance = await this.createDockerInstance(this.defaultHostConfig);
@@ -54525,6 +54579,9 @@ function normalizePattern(pattern) {
54525
54579
  function isExclusionPattern(pattern) {
54526
54580
  return pattern.startsWith("-");
54527
54581
  }
54582
+ function bareSkillName(name) {
54583
+ return name.replace(/\/SKILL\.md$/i, "");
54584
+ }
54528
54585
  function resolveSessionSkills(context) {
54529
54586
  const { requestedSkillNames } = context;
54530
54587
  const projectRootDir = resolveProjectRoot(context.projectRootDir);
@@ -54618,25 +54675,40 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
54618
54675
  const hasRequestedNames = Boolean(requestedSkillNames && requestedSkillNames.length > 0);
54619
54676
  const hasExcluded = excludedSkillPaths.size > 0;
54620
54677
  let filteredSkills;
54678
+ const skillNameMatches = (skill, pattern) => bareSkillName(skill.name).toLowerCase() === bareSkillName(pattern).toLowerCase() || skill.filePath === pattern;
54679
+ const isExcluded = (skill) => {
54680
+ for (const ep of excludedSkillPaths) {
54681
+ if (skillNameMatches(skill, ep)) return true;
54682
+ }
54683
+ return false;
54684
+ };
54685
+ const isAllowed = (skill) => {
54686
+ for (const ap of allowedSkillPaths) {
54687
+ if (skillNameMatches(skill, ap)) return true;
54688
+ }
54689
+ return false;
54690
+ };
54621
54691
  if (hasRequestedNames) {
54622
- const requestedNamesLower = new Set(requestedSkillNames.map((n) => n.toLowerCase()));
54692
+ const requestedBareNamesLower = new Set(requestedSkillNames.map((n) => bareSkillName(n).toLowerCase()));
54623
54693
  filteredSkills = base.skills.filter(
54624
- (skill) => requestedNamesLower.has(skill.name.toLowerCase()) && !excludedSkillPaths.has(skill.filePath)
54694
+ (skill) => requestedBareNamesLower.has(bareSkillName(skill.name).toLowerCase()) && !isExcluded(skill)
54625
54695
  );
54626
54696
  } else if (hasPatterns) {
54627
54697
  filteredSkills = base.skills.filter(
54628
- (skill) => allowedSkillPaths.has(skill.filePath) && !excludedSkillPaths.has(skill.filePath)
54698
+ (skill) => isAllowed(skill) && !isExcluded(skill)
54629
54699
  );
54630
54700
  } else if (hasExcluded) {
54631
- filteredSkills = base.skills.filter((skill) => !excludedSkillPaths.has(skill.filePath));
54701
+ filteredSkills = base.skills.filter((skill) => !isExcluded(skill));
54632
54702
  } else {
54633
54703
  filteredSkills = base.skills;
54634
54704
  }
54635
54705
  const newDiagnostics = [];
54636
54706
  const purpose = sessionPurpose ? ` [${sessionPurpose}]` : "";
54637
- const discoveredPaths = new Set(base.skills.map((s) => s.filePath));
54707
+ const discoveredBareNames = new Set(base.skills.map((s) => bareSkillName(s.name).toLowerCase()));
54708
+ const discoveredFilePaths = new Set(base.skills.map((s) => s.filePath));
54709
+ const hasDiscoveredMatch = (pattern) => discoveredBareNames.has(bareSkillName(pattern).toLowerCase()) || discoveredFilePaths.has(pattern);
54638
54710
  for (const excludedPath of excludedSkillPaths) {
54639
- if (discoveredPaths.has(excludedPath)) {
54711
+ if (hasDiscoveredMatch(excludedPath)) {
54640
54712
  newDiagnostics.push({
54641
54713
  type: "warning",
54642
54714
  message: `Skill at '${excludedPath}' exists but is disabled by project execution settings${purpose}`,
@@ -54645,7 +54717,7 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
54645
54717
  }
54646
54718
  }
54647
54719
  for (const allowedPath of allowedSkillPaths) {
54648
- if (!discoveredPaths.has(allowedPath)) {
54720
+ if (!hasDiscoveredMatch(allowedPath)) {
54649
54721
  newDiagnostics.push({
54650
54722
  type: "warning",
54651
54723
  message: `Configured skill pattern '${allowedPath}' not found in discovered skills${purpose}`,
@@ -54654,9 +54726,9 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
54654
54726
  }
54655
54727
  }
54656
54728
  if (requestedSkillNames) {
54657
- const discoveredNamesLower = new Set(base.skills.map((s) => s.name.toLowerCase()));
54729
+ const discoveredBareNamesLower = new Set(base.skills.map((s) => bareSkillName(s.name).toLowerCase()));
54658
54730
  for (const requestedName of requestedSkillNames) {
54659
- if (!discoveredNamesLower.has(requestedName.toLowerCase()) && !isBuiltInFallbackRequest(requestedName)) {
54731
+ if (!discoveredBareNamesLower.has(bareSkillName(requestedName).toLowerCase()) && !isBuiltInFallbackRequest(requestedName)) {
54660
54732
  const purpose2 = sessionPurpose ? ` [${sessionPurpose}]` : "";
54661
54733
  newDiagnostics.push({
54662
54734
  type: "warning",
@@ -55010,6 +55082,11 @@ async function promptSessionAndCheck(session, prompt, options) {
55010
55082
  piLog.warn(`pi state error \u2014 failed to inspect transcript: ${inspectErr instanceof Error ? inspectErr.message : String(inspectErr)}`);
55011
55083
  }
55012
55084
  }
55085
+ if (/Provider finish_reason:\s*repeat\b/i.test(stateError)) {
55086
+ piLog.warn(`pi state error \u2014 treating provider finish_reason=repeat as soft stop: ${stateError}`);
55087
+ clearSessionStateError(session);
55088
+ return;
55089
+ }
55013
55090
  throw new Error(stateError);
55014
55091
  }
55015
55092
  }
@@ -58191,9 +58268,18 @@ function normalizeAgentSkills(metadataSkills) {
58191
58268
  name = namedEntry.trim();
58192
58269
  }
58193
58270
  }
58194
- if (name && name.length > 0 && !seen.has(name)) {
58195
- seen.add(name);
58196
- result.push(name);
58271
+ if (name && name.length > 0) {
58272
+ if (name.includes("::")) {
58273
+ const idPath = name.split("::").pop();
58274
+ const parts = idPath.replace(/\\/g, "/").split("/").filter(Boolean);
58275
+ if (parts.length >= 2) {
58276
+ name = parts.slice(-2).join("/");
58277
+ }
58278
+ }
58279
+ if (!seen.has(name)) {
58280
+ seen.add(name);
58281
+ result.push(name);
58282
+ }
58197
58283
  }
58198
58284
  }
58199
58285
  return result;
@@ -68816,20 +68902,26 @@ The tool prevents your session from being killed by the inactivity watchdog duri
68816
68902
  if (from !== "in-review" && from !== "done") {
68817
68903
  return task;
68818
68904
  }
68819
- const hasMergeEvidence = Boolean(task.mergeDetails) || (task.mergeRetries ?? 0) > 0 || (task.verificationFailureCount ?? 0) > 0 || task.status === "merging" || task.status === "merging-pr";
68905
+ 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
68906
  if (!hasMergeEvidence) {
68821
68907
  return task;
68822
68908
  }
68823
68909
  return this.cleanupMergeStateForReverification(
68824
68910
  task,
68825
- `Task returned to in-progress from ${from} column \u2014 resetting verification steps and merge state for re-verification`
68911
+ `Task returned to in-progress from ${from} column \u2014 resetting verification steps and merge state for re-verification`,
68912
+ {
68913
+ // Keep deterministic merge-verification bounce budget across remediation
68914
+ // cycles. Status may be cleared by intermediate paths, so the counter is
68915
+ // the canonical signal once a bounce has started.
68916
+ preserveVerificationFailureCount: (task.verificationFailureCount ?? 0) > 0
68917
+ }
68826
68918
  );
68827
68919
  }
68828
- async cleanupMergeStateForReverification(task, logMessage) {
68920
+ async cleanupMergeStateForReverification(task, logMessage, options) {
68829
68921
  await this.store.updateTask(task.id, {
68830
68922
  mergeDetails: null,
68831
68923
  mergeRetries: 0,
68832
- verificationFailureCount: 0,
68924
+ verificationFailureCount: options?.preserveVerificationFailureCount ? task.verificationFailureCount ?? 0 : 0,
68833
68925
  workflowStepResults: []
68834
68926
  });
68835
68927
  const refreshedTask = await this.store.getTask(task.id);
@@ -69424,7 +69516,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69424
69516
  };
69425
69517
  const audit = createRunAuditor(this.store, engineRunContext);
69426
69518
  const activeColumns = /* @__PURE__ */ new Set(["in-progress", "in-review", "done"]);
69427
- const activeMergeStatuses = /* @__PURE__ */ new Set(["merging", "merging-pr"]);
69519
+ const activeMergeStatuses = /* @__PURE__ */ new Set(["merging", "merging-pr", "merging-fix"]);
69428
69520
  const isActiveTask = activeColumns.has(task.column) || activeMergeStatuses.has(task.status ?? "");
69429
69521
  if (!isActiveTask) {
69430
69522
  const tasksDir = join36(this.store.getFusionDir(), "tasks");
@@ -69763,7 +69855,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69763
69855
  `${failedType} command \`${failedCommand}\` failed (exit ${failedResult.exitCode}):
69764
69856
  ${summary}`,
69765
69857
  `Verification (${failedType})`,
69766
- `Deterministic verification failed (${failedType})`
69858
+ `Deterministic verification failed (${failedType})`,
69859
+ true,
69860
+ true
69767
69861
  );
69768
69862
  return;
69769
69863
  }
@@ -69809,7 +69903,9 @@ ${summary}`,
69809
69903
  `${failedType} command \`${failedCommand}\` failed (exit ${failedResult.exitCode}) after ${maxFixRetries} fix attempts:
69810
69904
  ${summary}`,
69811
69905
  `Verification (${failedType})`,
69812
- `Deterministic verification failed after ${maxFixRetries} fix attempts`
69906
+ `Deterministic verification failed after ${maxFixRetries} fix attempts`,
69907
+ true,
69908
+ true
69813
69909
  );
69814
69910
  return;
69815
69911
  }
@@ -71498,7 +71594,7 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
71498
71594
  * Injects failure feedback into PROMPT.md, resets steps, clears session,
71499
71595
  * and schedules a move to todo → in-progress after the executing guard clears.
71500
71596
  */
71501
- async sendTaskBackForFix(task, worktreePath, failureFeedback, stepName, reason, preserveResumeState = true) {
71597
+ async sendTaskBackForFix(task, worktreePath, failureFeedback, stepName, reason, preserveResumeState = true, mergeVerificationFailure = false) {
71502
71598
  const taskId = task.id;
71503
71599
  this.clearCompletedTaskWatchdog(taskId);
71504
71600
  await this.store.addTaskComment(
@@ -71517,7 +71613,7 @@ Please fix the issues so the verification can pass on the next attempt.`,
71517
71613
  const updatedTask = await this.store.getTask(taskId);
71518
71614
  await this.reopenLastStepForRevision(taskId, updatedTask);
71519
71615
  await this.store.updateTask(taskId, {
71520
- status: null,
71616
+ status: mergeVerificationFailure ? "merging-fix" : null,
71521
71617
  error: null,
71522
71618
  sessionFile: null,
71523
71619
  workflowStepRetries: 0
@@ -79886,14 +79982,15 @@ var init_self_healing = __esm({
79886
79982
  execAsync7 = promisify9(exec9);
79887
79983
  APPROVED_TRIAGE_RECOVERY_GRACE_MS = 6e4;
79888
79984
  ORPHANED_EXECUTION_RECOVERY_GRACE_MS = 6e4;
79889
- ACTIVE_MERGE_STATUSES = /* @__PURE__ */ new Set(["merging", "merging-pr"]);
79985
+ ACTIVE_MERGE_STATUSES = /* @__PURE__ */ new Set(["merging", "merging-pr", "merging-fix"]);
79890
79986
  NON_TERMINAL_STEP_STATUSES2 = /* @__PURE__ */ new Set(["pending", "in-progress"]);
79891
79987
  GHOST_REVIEW_PRESERVED_STATUSES = /* @__PURE__ */ new Set([
79892
79988
  "failed",
79893
79989
  "awaiting-user-review",
79894
79990
  "awaiting-approval",
79895
79991
  "merging",
79896
- "merging-pr"
79992
+ "merging-pr",
79993
+ "merging-fix"
79897
79994
  ]);
79898
79995
  ORPHANED_WITH_WORKTREE_GRACE_MS = 3e5;
79899
79996
  MAX_TASK_DONE_RETRIES = 3;
@@ -80616,7 +80713,7 @@ var init_self_healing = __esm({
80616
80713
  *
80617
80714
  * Preserved statuses (skipped):
80618
80715
  * - `awaiting-user-review`, `awaiting-approval`: explicit human handoff
80619
- * - `merging`, `merging-pr`: handled by `recoverInterruptedMergingTasks`
80716
+ * - `merging`, `merging-pr`, `merging-fix`: handled by `recoverInterruptedMergingTasks`
80620
80717
  *
80621
80718
  * Rate-limiting comes from the `updatedAt >= taskStuckTimeoutMs` gate —
80622
80719
  * each kick refreshes `updatedAt`, so a task that re-enters review and gets
@@ -86232,7 +86329,7 @@ ${detail}`
86232
86329
  "agent"
86233
86330
  );
86234
86331
  await store.updateTask(taskId, {
86235
- status: null,
86332
+ status: "merging-fix",
86236
86333
  mergeRetries: 0,
86237
86334
  error: null,
86238
86335
  verificationFailureCount: nextBounces
@@ -86240,10 +86337,10 @@ ${detail}`
86240
86337
  await store.moveTask(taskId, "in-progress");
86241
86338
  await store.logEntry(
86242
86339
  taskId,
86243
- `Deterministic ${failedKind} verification failed (${nextBounces}/${cap}) \u2014 moved back to in-progress for remediation`
86340
+ `Deterministic ${failedKind} verification failed (${nextBounces}/${cap}) \u2014 moved back to in-progress with status=merging-fix for remediation`
86244
86341
  );
86245
86342
  runtimeLog.log(
86246
- `Auto-merge: ${taskId} deterministic ${failedKind} verification failed (${nextBounces}/${cap}) \u2014 moved to in-progress`
86343
+ `Auto-merge: ${taskId} deterministic ${failedKind} verification failed (${nextBounces}/${cap}) \u2014 moved to in-progress with status=merging-fix`
86247
86344
  );
86248
86345
  } catch {
86249
86346
  runtimeLog.error(
@@ -97230,10 +97327,18 @@ var init_claude_cli_probe = __esm({
97230
97327
  }
97231
97328
  });
97232
97329
 
97330
+ // ../../plugins/fusion-plugin-droid-runtime/dist/probe.js
97331
+ var init_probe3 = __esm({
97332
+ "../../plugins/fusion-plugin-droid-runtime/dist/probe.js"() {
97333
+ "use strict";
97334
+ }
97335
+ });
97336
+
97233
97337
  // ../dashboard/src/droid-cli-probe.ts
97234
97338
  var init_droid_cli_probe = __esm({
97235
97339
  "../dashboard/src/droid-cli-probe.ts"() {
97236
97340
  "use strict";
97341
+ init_probe3();
97237
97342
  }
97238
97343
  });
97239
97344
 
@@ -103962,9 +104067,12 @@ async function getResearchAvailability(store) {
103962
104067
  }
103963
104068
  const backend = resolved.searchProvider ?? settings.researchWebSearchProvider;
103964
104069
  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) {
104070
+ if (!backend) {
103966
104071
  return { ok: false, code: "provider-unavailable", message: "Research provider is not configured. Set research provider credentials in Settings." };
103967
104072
  }
104073
+ if (!configured) {
104074
+ return { ok: false, code: "missing-credentials", message: `Missing credentials for ${backend}. Add provider keys in Authentication and verify Research defaults.` };
104075
+ }
103968
104076
  return { ok: true };
103969
104077
  }
103970
104078
  function toResearchRunDetails(run) {
@@ -104355,12 +104463,13 @@ Path: .fusion/tasks/${params.id}/attachments/${attachment.filename}`
104355
104463
  pi.registerTool({
104356
104464
  name: "fn_task_retry",
104357
104465
  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)",
104466
+ 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.",
104467
+ promptSnippet: "Retry a failed Fusion task (clears error, moves to todo or stays in in-review)",
104360
104468
  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"
104469
+ "Use when a task has failed and needs to be retried",
104470
+ "Only tasks in 'failed' or 'stuck-killed' state can be retried",
104471
+ "Tasks in 'in-review' stay in in-review \u2014 only the error/retry state is cleared, and the auto-merge system re-attempts",
104472
+ "Tasks in other columns are moved to the todo column with error state cleared"
104364
104473
  ],
104365
104474
  parameters: Type8.Object({
104366
104475
  id: Type8.String({ description: "Task ID to retry (e.g. FN-001). Must be in 'failed' state." })
@@ -104384,6 +104493,14 @@ Path: .fusion/tasks/${params.id}/attachments/${attachment.filename}`
104384
104493
  details: { taskId: params.id, currentStatus: task.status }
104385
104494
  };
104386
104495
  }
104496
+ if (task.column === "in-review") {
104497
+ await store.updateTask(params.id, { status: null, error: null, stuckKillCount: 0, mergeRetries: 0 });
104498
+ await store.logEntry(params.id, "Retry requested via Fusion extension (in-review retry, mergeRetries reset)");
104499
+ return {
104500
+ content: [{ type: "text", text: `Retried ${params.id} \u2192 in-review (merge retry state cleared, task stays in in-review)` }],
104501
+ details: { taskId: params.id, newColumn: "in-review" }
104502
+ };
104503
+ }
104387
104504
  await store.updateTask(params.id, { status: null, error: null });
104388
104505
  await store.moveTask(params.id, "todo");
104389
104506
  await store.logEntry(params.id, "Retry requested via Fusion extension", "Task reset to todo for retry");
@@ -104875,7 +104992,7 @@ Planning session completed. Task ${taskId} is now in planning and will be auto-p
104875
104992
  pi.registerTool({
104876
104993
  name: "fn_research_cancel",
104877
104994
  label: "fn: Cancel Research Run",
104878
- description: "Cancel a research run.",
104995
+ description: "Cancel an in-flight research run. Terminal runs return INVALID_TRANSITION.",
104879
104996
  parameters: Type8.Object({ id: Type8.String({ description: "Research run ID" }) }),
104880
104997
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
104881
104998
  const store = await getStore2(ctx.cwd);
@@ -104887,6 +105004,17 @@ Planning session completed. Task ${taskId} is now in planning and will be auto-p
104887
105004
  details: { runId: params.id, status: "missing", summary: null, findings: [], citations: [], error: "not found", setup: null }
104888
105005
  };
104889
105006
  }
105007
+ if (!["queued", "running", "cancelling", "retry_waiting"].includes(run.status)) {
105008
+ return {
105009
+ content: [{ type: "text", text: `Research run ${params.id} cannot be cancelled from status ${run.status}.` }],
105010
+ isError: true,
105011
+ details: {
105012
+ ...toResearchRunDetails(run),
105013
+ error: "invalid transition",
105014
+ setup: { code: "INVALID_TRANSITION", message: "Cancel is only available for queued/running/cancelling/retry_waiting runs." }
105015
+ }
105016
+ };
105017
+ }
104890
105018
  const updated = researchStore.requestCancellation(params.id);
104891
105019
  return {
104892
105020
  content: [{ type: "text", text: `Requested cancellation for research run ${params.id} (status: ${updated.status}).` }],
@@ -104894,6 +105022,41 @@ Planning session completed. Task ${taskId} is now in planning and will be auto-p
104894
105022
  };
104895
105023
  }
104896
105024
  });
105025
+ pi.registerTool({
105026
+ name: "fn_research_retry",
105027
+ label: "fn: Retry Research Run",
105028
+ description: "Retry a failed research run when lifecycle marks it retryable.",
105029
+ parameters: Type8.Object({ id: Type8.String({ description: "Research run ID" }) }),
105030
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105031
+ const store = await getStore2(ctx.cwd);
105032
+ const researchStore = store.getResearchStore();
105033
+ const run = researchStore.getRun(params.id);
105034
+ if (!run) {
105035
+ return {
105036
+ content: [{ type: "text", text: `Research run ${params.id} not found.` }],
105037
+ isError: true,
105038
+ details: { runId: params.id, status: "missing", summary: null, findings: [], citations: [], error: "not found", setup: null }
105039
+ };
105040
+ }
105041
+ const isRetryExhausted = run.status === "retry_exhausted" || run.lifecycle?.errorCode === "RETRY_EXHAUSTED";
105042
+ if (run.status !== "failed" && run.status !== "timed_out" || run.lifecycle?.retryable === false || isRetryExhausted) {
105043
+ return {
105044
+ content: [{ type: "text", text: `Research run ${params.id} is not retryable from status ${run.status}.` }],
105045
+ isError: true,
105046
+ details: {
105047
+ ...toResearchRunDetails(run),
105048
+ error: "not retryable",
105049
+ setup: { code: isRetryExhausted ? "RETRY_EXHAUSTED" : "INVALID_TRANSITION", message: "Retry is only available for failed/timed_out retryable runs." }
105050
+ }
105051
+ };
105052
+ }
105053
+ const retryRun = researchStore.createRetryRun(params.id);
105054
+ return {
105055
+ content: [{ type: "text", text: `Created retry run ${retryRun.id} from ${params.id}.` }],
105056
+ details: toResearchRunDetails(retryRun)
105057
+ };
105058
+ }
105059
+ });
104897
105060
  pi.registerTool({
104898
105061
  name: "fn_insight_list",
104899
105062
  label: "fn: List Insights",
@@ -105590,6 +105753,278 @@ Status: ${updated.status}`
105590
105753
  };
105591
105754
  }
105592
105755
  });
105756
+ pi.registerTool({
105757
+ name: "fn_list_agents",
105758
+ label: "fn: List Agents",
105759
+ 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.",
105760
+ promptSnippet: "List all available Fusion agents",
105761
+ promptGuidelines: [
105762
+ "Use fn_list_agents to discover which agents exist before delegating work",
105763
+ "Filter by role or state to narrow results",
105764
+ "Ephemeral/runtime agents are excluded by default"
105765
+ ],
105766
+ parameters: Type8.Object({
105767
+ role: Type8.Optional(
105768
+ Type8.String({ description: "Filter by agent role/capability (e.g., 'executor', 'reviewer', 'qa')" })
105769
+ ),
105770
+ state: Type8.Optional(
105771
+ Type8.String({ description: "Filter by agent state (e.g., 'idle', 'active', 'running')" })
105772
+ ),
105773
+ includeEphemeral: Type8.Optional(
105774
+ Type8.Boolean({ description: "Include ephemeral/runtime agents (default: false)" })
105775
+ )
105776
+ }),
105777
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105778
+ const { AgentStore: AgentStore2 } = await Promise.resolve().then(() => (init_src(), src_exports));
105779
+ const agentStore = new AgentStore2({ rootDir: getFusionDir(ctx.cwd) });
105780
+ await agentStore.init();
105781
+ const filter = {};
105782
+ if (params.role) filter.role = params.role;
105783
+ if (params.state) filter.state = params.state;
105784
+ if (params.includeEphemeral !== void 0) filter.includeEphemeral = params.includeEphemeral;
105785
+ const agents = await agentStore.listAgents(filter);
105786
+ if (agents.length === 0) {
105787
+ return {
105788
+ content: [{ type: "text", text: "No agents found matching the specified filters." }],
105789
+ details: { agents: [], count: 0 }
105790
+ };
105791
+ }
105792
+ const lines = agents.map((agent) => {
105793
+ const parts = [
105794
+ `ID: ${agent.id}`,
105795
+ `Name: ${agent.name}`,
105796
+ `Role: ${agent.role}`,
105797
+ `State: ${agent.state}`
105798
+ ];
105799
+ if (agent.title) parts.push(`Title: ${agent.title}`);
105800
+ if (agent.soul) parts.push(`Soul: ${agent.soul.slice(0, 200)}`);
105801
+ if (agent.instructionsText) {
105802
+ const snippet = agent.instructionsText.slice(0, 100);
105803
+ parts.push(`Custom Instructions: ${snippet}${agent.instructionsText.length > 100 ? "\u2026" : ""}`);
105804
+ }
105805
+ if (agent.taskId) parts.push(`Current Task: ${agent.taskId}`);
105806
+ return parts.join("\n");
105807
+ });
105808
+ return {
105809
+ content: [{ type: "text", text: `Available agents (${agents.length}):
105810
+
105811
+ ${lines.join("\n\n")}` }],
105812
+ details: { agents, count: agents.length }
105813
+ };
105814
+ }
105815
+ });
105816
+ pi.registerTool({
105817
+ name: "fn_delegate_task",
105818
+ label: "fn: Delegate Task",
105819
+ 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.",
105820
+ promptSnippet: "Delegate a task to a specific Fusion agent",
105821
+ promptGuidelines: [
105822
+ "Use fn_list_agents first to find available agents and their capabilities",
105823
+ "The task is created in 'todo' and assigned to the target agent",
105824
+ "Cannot delegate to ephemeral/runtime agents",
105825
+ "Optionally specify dependencies on other tasks"
105826
+ ],
105827
+ parameters: Type8.Object({
105828
+ agent_id: Type8.String({ description: "The agent ID to delegate work to" }),
105829
+ description: Type8.String({ description: "What needs to be done" }),
105830
+ dependencies: Type8.Optional(
105831
+ Type8.Array(Type8.String(), { description: 'Task IDs this new task depends on (e.g. ["KB-001"]' })
105832
+ )
105833
+ }),
105834
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105835
+ const agentError = await validateAssignableAgentId(ctx.cwd, params.agent_id);
105836
+ if (agentError) {
105837
+ return {
105838
+ content: [{ type: "text", text: `ERROR: ${agentError}` }],
105839
+ isError: true,
105840
+ details: { error: agentError }
105841
+ };
105842
+ }
105843
+ const { AgentStore: AgentStore2 } = await Promise.resolve().then(() => (init_src(), src_exports));
105844
+ const agentStore = new AgentStore2({ rootDir: getFusionDir(ctx.cwd) });
105845
+ await agentStore.init();
105846
+ const agent = await agentStore.getAgent(params.agent_id);
105847
+ const store = await getStore2(ctx.cwd);
105848
+ const task = await store.createTask({
105849
+ description: params.description,
105850
+ dependencies: params.dependencies,
105851
+ column: "todo",
105852
+ assignedAgentId: params.agent_id,
105853
+ source: { sourceType: "api" }
105854
+ });
105855
+ const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
105856
+ return {
105857
+ content: [{
105858
+ type: "text",
105859
+ 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.`
105860
+ }],
105861
+ details: { taskId: task.id, agentId: agent.id, agentName: agent.name }
105862
+ };
105863
+ }
105864
+ });
105865
+ pi.registerTool({
105866
+ name: "fn_agent_show",
105867
+ label: "fn: Show Agent",
105868
+ 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.",
105869
+ promptSnippet: "Show details of a specific Fusion agent",
105870
+ promptGuidelines: [
105871
+ "Use to get full details about a specific agent",
105872
+ "Provide agent ID or a resolvable name",
105873
+ "Shows the agent's position in the org hierarchy"
105874
+ ],
105875
+ parameters: Type8.Object({
105876
+ id: Type8.String({ description: "Agent ID or resolvable name" })
105877
+ }),
105878
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105879
+ const { AgentStore: AgentStore2 } = await Promise.resolve().then(() => (init_src(), src_exports));
105880
+ const agentStore = new AgentStore2({ rootDir: getFusionDir(ctx.cwd) });
105881
+ await agentStore.init();
105882
+ const agent = await agentStore.resolveAgent(params.id);
105883
+ if (!agent) {
105884
+ return {
105885
+ content: [{ type: "text", text: `Agent '${params.id}' not found` }],
105886
+ isError: true,
105887
+ details: { error: "Agent not found" }
105888
+ };
105889
+ }
105890
+ const directReports = await agentStore.getAgentsByReportsTo(agent.id);
105891
+ const parts = [
105892
+ `ID: ${agent.id}`,
105893
+ `Name: ${agent.name}`,
105894
+ `Role: ${agent.role}`,
105895
+ `State: ${agent.state}`
105896
+ ];
105897
+ if (agent.title) parts.push(`Title: ${agent.title}`);
105898
+ if (agent.icon) parts.push(`Icon: ${agent.icon}`);
105899
+ if (agent.reportsTo) {
105900
+ const manager = await agentStore.getAgent(agent.reportsTo);
105901
+ if (manager) {
105902
+ parts.push(`Reports To: ${manager.name} (${manager.id})`);
105903
+ } else {
105904
+ parts.push(`Reports To: ${agent.reportsTo}`);
105905
+ }
105906
+ }
105907
+ if (directReports.length > 0) {
105908
+ parts.push(`Direct Reports: ${directReports.map((r) => `${r.name} (${r.id})`).join(", ")}`);
105909
+ }
105910
+ if (agent.taskId) parts.push(`Current Task: ${agent.taskId}`);
105911
+ if (agent.instructionsText) {
105912
+ const snippet = agent.instructionsText.slice(0, 100);
105913
+ parts.push(`Custom Instructions: ${snippet}${agent.instructionsText.length > 100 ? "\u2026" : ""}`);
105914
+ }
105915
+ if (agent.soul) {
105916
+ const snippet = agent.soul.slice(0, 200);
105917
+ parts.push(`Soul: ${snippet}${agent.soul.length > 200 ? "\u2026" : ""}`);
105918
+ }
105919
+ if (agent.metadata?.skills) {
105920
+ parts.push(`Skills: ${JSON.stringify(agent.metadata.skills)}`);
105921
+ }
105922
+ return {
105923
+ content: [{ type: "text", text: parts.join("\n") }],
105924
+ details: {
105925
+ agent,
105926
+ directReports: directReports.map((r) => ({ id: r.id, name: r.name, role: r.role }))
105927
+ }
105928
+ };
105929
+ }
105930
+ });
105931
+ pi.registerTool({
105932
+ name: "fn_agent_org_chart",
105933
+ label: "fn: Agent Org Chart",
105934
+ description: "Show the organizational tree of agents, displaying the role hierarchy. Optionally filter to a subtree rooted at a specific agent.",
105935
+ promptSnippet: "Show the Fusion agent org chart",
105936
+ promptGuidelines: [
105937
+ "Use to understand the team structure and reporting hierarchy",
105938
+ "Optionally specify a root agent to see only their subtree",
105939
+ "Ephemeral/runtime agents are excluded by default"
105940
+ ],
105941
+ parameters: Type8.Object({
105942
+ root_agent_id: Type8.Optional(
105943
+ Type8.String({ description: "If provided, show only the subtree rooted at this agent" })
105944
+ ),
105945
+ include_ephemeral: Type8.Optional(
105946
+ Type8.Boolean({ description: "Include ephemeral/runtime agents (default: false)" })
105947
+ )
105948
+ }),
105949
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105950
+ const { AgentStore: AgentStore2 } = await Promise.resolve().then(() => (init_src(), src_exports));
105951
+ const agentStore = new AgentStore2({ rootDir: getFusionDir(ctx.cwd) });
105952
+ await agentStore.init();
105953
+ const includeEphemeral = params.include_ephemeral ?? false;
105954
+ if (params.root_agent_id) {
105955
+ const rootAgent = await agentStore.resolveAgent(params.root_agent_id);
105956
+ if (!rootAgent) {
105957
+ return {
105958
+ content: [{ type: "text", text: `Agent '${params.root_agent_id}' not found` }],
105959
+ isError: true,
105960
+ details: { error: "Root agent not found" }
105961
+ };
105962
+ }
105963
+ const fullTree = await agentStore.getOrgTree({ includeEphemeral });
105964
+ const findSubtree = (nodes) => {
105965
+ for (const node of nodes) {
105966
+ if (node.agent.id === rootAgent.id) return node;
105967
+ const found = findSubtree(node.children);
105968
+ if (found) return found;
105969
+ }
105970
+ return null;
105971
+ };
105972
+ const subtree = findSubtree(fullTree);
105973
+ if (!subtree) {
105974
+ return {
105975
+ content: [{
105976
+ type: "text",
105977
+ text: `${rootAgent.icon ?? "\u{1F916}"} ${rootAgent.name} (${rootAgent.role}) \u2014 ${rootAgent.state}${rootAgent.taskId ? ` [${rootAgent.taskId}]` : ""}`
105978
+ }],
105979
+ details: { tree: [{ agent: rootAgent, children: [] }] }
105980
+ };
105981
+ }
105982
+ const lines2 = [];
105983
+ const renderNode2 = (node, indent) => {
105984
+ const a = node.agent;
105985
+ lines2.push(
105986
+ `${indent}${a.icon ?? "\u{1F916}"} ${a.name} (${a.role}) \u2014 ${a.state}${a.taskId ? ` [${a.taskId}]` : ""}`
105987
+ );
105988
+ for (const child of node.children) {
105989
+ renderNode2(child, indent + " ");
105990
+ }
105991
+ };
105992
+ renderNode2(subtree, "");
105993
+ return {
105994
+ content: [{ type: "text", text: `Agent Org Tree (subtree: ${rootAgent.name}):
105995
+ ${lines2.join("\n")}` }],
105996
+ details: { tree: [subtree] }
105997
+ };
105998
+ }
105999
+ const tree = await agentStore.getOrgTree({ includeEphemeral });
106000
+ if (tree.length === 0) {
106001
+ return {
106002
+ content: [{ type: "text", text: "No agents found." }],
106003
+ details: { tree: [], count: 0 }
106004
+ };
106005
+ }
106006
+ const lines = [];
106007
+ let count = 0;
106008
+ const renderNode = (node, indent) => {
106009
+ const a = node.agent;
106010
+ lines.push(
106011
+ `${indent}${a.icon ?? "\u{1F916}"} ${a.name} (${a.role}) \u2014 ${a.state}${a.taskId ? ` [${a.taskId}]` : ""}`
106012
+ );
106013
+ count++;
106014
+ for (const child of node.children) {
106015
+ renderNode(child, indent + " ");
106016
+ }
106017
+ };
106018
+ for (const root of tree) {
106019
+ renderNode(root, "");
106020
+ }
106021
+ return {
106022
+ content: [{ type: "text", text: `Agent Org Tree (${count} agents):
106023
+ ${lines.join("\n")}` }],
106024
+ details: { tree, count }
106025
+ };
106026
+ }
106027
+ });
105593
106028
  pi.registerTool({
105594
106029
  name: "fn_skills_search",
105595
106030
  label: "FN: Search Skills",