@runfusion/fusion 0.2.6 → 0.3.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 (56) hide show
  1. package/dist/bin.js +8488 -6999
  2. package/dist/client/assets/{AgentDetailView-BMrHuWGs.css → AgentDetailView-C1b9PC5l.css} +1 -1
  3. package/dist/client/assets/{AgentDetailView-CBdzWtd-.js → AgentDetailView-CJIxNRq-.js} +3 -3
  4. package/dist/client/assets/{AgentsView-DPlnCa_B.js → AgentsView-BS17exn3.js} +5 -5
  5. package/dist/client/assets/ChatView-BUlq3WNJ.js +1 -0
  6. package/dist/client/assets/{DevServerView-BY5cGz23.js → DevServerView-qMPpnXRb.js} +2 -2
  7. package/dist/client/assets/DirectoryPicker-CTwgv9LY.js +1 -0
  8. package/dist/client/assets/DirectoryPicker-DzKVmxOf.css +1 -0
  9. package/dist/client/assets/{DocumentsView-lQwJmc4G.js → DocumentsView-DOz1KFGN.js} +1 -1
  10. package/dist/client/assets/{InsightsView-DUiZZ0z8.js → InsightsView-CHZTJUic.js} +2 -2
  11. package/dist/client/assets/MemoryView-V0QdeO3e.js +2 -0
  12. package/dist/client/assets/{NodesView-DgyM-ktg.js → NodesView-BtGNRj2z.js} +1 -1
  13. package/dist/client/assets/PiExtensionsManager-D9Ye2Vak.js +11 -0
  14. package/dist/client/assets/PiExtensionsManager-kgTOHPE9.css +1 -0
  15. package/dist/client/assets/PluginManager-DRiIqol2.css +1 -0
  16. package/dist/client/assets/PluginManager-LeHp0jJ_.js +1 -0
  17. package/dist/client/assets/{RoadmapsView-ANn2jmsU.js → RoadmapsView-C413ISVU.js} +2 -2
  18. package/dist/client/assets/SettingsModal--vWmKBpT.css +1 -0
  19. package/dist/client/assets/SettingsModal-BZLL2xAP.js +31 -0
  20. package/dist/client/assets/SettingsModal-CDWvhvrd.css +1 -0
  21. package/dist/client/assets/SettingsModal-olTBmYJs.js +1 -0
  22. package/dist/client/assets/SetupWizardModal-BMa6p24b.css +1 -0
  23. package/dist/client/assets/{SetupWizardModal-UxlAtKWA.js → SetupWizardModal-WdaR2eQQ.js} +1 -1
  24. package/dist/client/assets/SkillsView-BcE57w8i.js +1 -0
  25. package/dist/client/assets/{folder-open-J7yPbaCt.js → folder-open-Ec4hU1xL.js} +1 -1
  26. package/dist/client/assets/index-CCYdhck-.js +616 -0
  27. package/dist/client/assets/index-lJ5WOmO9.css +1 -0
  28. package/dist/client/assets/{upload-B_grq4hM.js → upload-BksRDuGJ.js} +1 -1
  29. package/dist/client/assets/users-EFU4n9Qr.js +6 -0
  30. package/dist/client/index.html +2 -2
  31. package/dist/extension.js +1271 -205
  32. package/dist/pi-claude-cli/index.ts +72 -28
  33. package/dist/pi-claude-cli/package.json +1 -1
  34. package/dist/pi-claude-cli/src/__tests__/event-bridge.test.ts +34 -0
  35. package/dist/pi-claude-cli/src/__tests__/mcp-config.test.ts +22 -0
  36. package/dist/pi-claude-cli/src/__tests__/prompt-builder.test.ts +72 -10
  37. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +9 -9
  38. package/dist/pi-claude-cli/src/event-bridge.ts +17 -6
  39. package/dist/pi-claude-cli/src/mcp-config.ts +36 -3
  40. package/dist/pi-claude-cli/src/prompt-builder.ts +111 -7
  41. package/dist/pi-claude-cli/src/provider.ts +18 -2
  42. package/package.json +6 -5
  43. package/skill/fusion/SKILL.md +6 -1
  44. package/skill/fusion/references/engine-tools.md +54 -0
  45. package/skill/fusion/references/extension-tools.md +83 -84
  46. package/skill/fusion/references/fusion-capabilities.md +33 -31
  47. package/dist/client/assets/ChatView-BXzYysNG.js +0 -1
  48. package/dist/client/assets/DirectoryPicker-DZ90eSBn.js +0 -1
  49. package/dist/client/assets/MemoryView-DvSmMN6G.js +0 -2
  50. package/dist/client/assets/PiExtensionsManager-K7HQ08L4.css +0 -1
  51. package/dist/client/assets/PiExtensionsManager-wxB-q06A.js +0 -11
  52. package/dist/client/assets/PluginManager-LH02ybSH.js +0 -1
  53. package/dist/client/assets/PluginManager-tCFMZMLL.css +0 -1
  54. package/dist/client/assets/SkillsView-DEjGh7wW.js +0 -1
  55. package/dist/client/assets/index-BdJsO65L.css +0 -1
  56. package/dist/client/assets/index-CHoVMPAA.js +0 -649
@@ -23,7 +23,20 @@ export interface PiMessage {
23
23
  content: string | unknown[];
24
24
  toolName?: string;
25
25
  }
26
- export type PiContext = { systemPrompt?: string; messages: PiMessage[] };
26
+ /**
27
+ * Minimal pi-ai Tool shape — the subset we read from `Context.tools` to build
28
+ * the deferred-tools system-prompt addendum.
29
+ */
30
+ export interface PiToolLike {
31
+ name: string;
32
+ description?: string;
33
+ }
34
+
35
+ export type PiContext = {
36
+ systemPrompt?: string;
37
+ messages: PiMessage[];
38
+ tools?: ReadonlyArray<PiToolLike>;
39
+ };
27
40
  import {
28
41
  mapPiToolNameToClaude,
29
42
  translatePiArgsToClaude,
@@ -49,7 +62,7 @@ type AnthropicContentBlock =
49
62
  * Each message is labeled with its role:
50
63
  * - USER: for user messages
51
64
  * - ASSISTANT: for assistant messages
52
- * - TOOL RESULT (historical {toolName}): for tool result messages
65
+ * - TOOL RESULT ({toolName}): for tool result messages
53
66
  */
54
67
  /** Module-level counter for placeholder images, reset per buildPrompt call. */
55
68
  let placeholderImageCount = 0;
@@ -200,7 +213,7 @@ export function buildResumePrompt(context: PiContext): string | AnthropicContent
200
213
  const claudeToolName = msg.toolName
201
214
  ? mapPiToolNameToClaude(msg.toolName)
202
215
  : "unknown";
203
- parts.push(`TOOL RESULT (historical ${claudeToolName}):`);
216
+ parts.push(`TOOL RESULT (${claudeToolName}):`);
204
217
  }
205
218
  parts.push(toolResultContentToText(msg.content));
206
219
  } else if (msg.role === "user") {
@@ -270,7 +283,7 @@ export function buildPrompt(context: PiContext): string | AnthropicContentBlock[
270
283
  const claudeToolName = message.toolName
271
284
  ? mapPiToolNameToClaude(message.toolName)
272
285
  : "unknown";
273
- historyParts.push(`TOOL RESULT (historical ${claudeToolName}):`);
286
+ historyParts.push(`TOOL RESULT (${claudeToolName}):`);
274
287
  }
275
288
  // Extract text portion of tool result
276
289
  historyParts.push(toolResultContentToText(message.content));
@@ -334,7 +347,7 @@ export function buildPrompt(context: PiContext): string | AnthropicContentBlock[
334
347
  const claudeToolName = message.toolName
335
348
  ? mapPiToolNameToClaude(message.toolName)
336
349
  : "unknown";
337
- parts.push(`TOOL RESULT (historical ${claudeToolName}):`);
350
+ parts.push(`TOOL RESULT (${claudeToolName}):`);
338
351
  }
339
352
  parts.push(toolResultContentToText(message.content));
340
353
  }
@@ -372,7 +385,7 @@ export function buildSystemPrompt(
372
385
  const parts: string[] = [];
373
386
 
374
387
  if (context.systemPrompt) {
375
- parts.push(context.systemPrompt);
388
+ parts.push(rewriteCustomToolReferences(context.systemPrompt, context.tools));
376
389
  }
377
390
 
378
391
  // Look for AGENTS.md
@@ -396,9 +409,100 @@ export function buildSystemPrompt(
396
409
  );
397
410
  }
398
411
 
412
+ const customToolsAddendum = buildCustomToolsAddendum(context.tools);
413
+ if (customToolsAddendum) {
414
+ parts.push(customToolsAddendum);
415
+ }
416
+
399
417
  return parts.join("\n\n");
400
418
  }
401
419
 
420
+ /** Pi built-in tool names — these go through pi's wrapped built-ins, not MCP. */
421
+ const BUILT_IN_PI_TOOLS = new Set([
422
+ "read",
423
+ "write",
424
+ "edit",
425
+ "bash",
426
+ "grep",
427
+ "find",
428
+ "ls",
429
+ ]);
430
+
431
+ /**
432
+ * Rewrite bare references to custom pi tool names (e.g. `fn_review_spec`,
433
+ * `fn_review_spec()`) in the system prompt so they appear as their
434
+ * MCP-prefixed names (`mcp__custom-tools__fn_review_spec`). Engine prompts are
435
+ * written for direct API tool calls; under pi-claude-cli the same tools are
436
+ * reachable only through the MCP shim. Without this rewrite, models like
437
+ * Sonnet 4.6 inconsistently translate the names — sometimes calling MCP
438
+ * variants, sometimes silently skipping the call (observed in triage where
439
+ * `fn_review_spec` was never invoked even though the prompt said "MUST call").
440
+ *
441
+ * Only rewrites whole-word matches anchored to a non-identifier boundary, so
442
+ * substrings inside other identifiers stay intact. Skips already-prefixed
443
+ * occurrences (`mcp__custom-tools__fn_review_spec`) and pi built-ins.
444
+ */
445
+ function rewriteCustomToolReferences(
446
+ prompt: string,
447
+ tools: ReadonlyArray<PiToolLike> | undefined,
448
+ ): string {
449
+ if (!prompt || !tools || tools.length === 0) {
450
+ return prompt;
451
+ }
452
+
453
+ let result = prompt;
454
+ for (const tool of tools) {
455
+ if (BUILT_IN_PI_TOOLS.has(tool.name)) continue;
456
+ // \b doesn't treat `_` as a word boundary the way we want here, so anchor
457
+ // the match between either start-of-string/non-identifier-char and either
458
+ // end-of-string/non-identifier-char. Also negative-lookbehind for
459
+ // `mcp__custom-tools__` so we don't double-prefix.
460
+ const escaped = tool.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
461
+ const pattern = new RegExp(
462
+ `(?<![A-Za-z0-9_])(?<!mcp__custom-tools__)${escaped}(?![A-Za-z0-9_])`,
463
+ "g",
464
+ );
465
+ result = result.replace(pattern, `mcp__custom-tools__${tool.name}`);
466
+ }
467
+ return result;
468
+ }
469
+
470
+ /**
471
+ * Build a system-prompt addendum that maps each custom pi tool to its
472
+ * MCP-exposed name (`mcp__custom-tools__<name>`) and explains Claude Code's
473
+ * deferred-tool protocol. Without this, the model sees instructions like
474
+ * "call fn_review_spec()" but only finds `mcp__custom-tools__fn_review_spec`
475
+ * via ToolSearch — and may report "tool not found" or skip the call.
476
+ *
477
+ * Returns an empty string when there are no custom tools so the addendum
478
+ * doesn't pollute prompts on plain chat sessions with only built-ins.
479
+ */
480
+ function buildCustomToolsAddendum(
481
+ tools: ReadonlyArray<PiToolLike> | undefined,
482
+ ): string {
483
+ if (!tools || tools.length === 0) return "";
484
+ const customNames = tools
485
+ .map((t) => t.name)
486
+ .filter((name) => !BUILT_IN_PI_TOOLS.has(name));
487
+ if (customNames.length === 0) return "";
488
+
489
+ const lines = customNames
490
+ .sort()
491
+ .map((name) => `- \`${name}\` is exposed as \`mcp__custom-tools__${name}\``);
492
+
493
+ return [
494
+ "## Custom tool naming (Claude Code deferred-tools protocol)",
495
+ "",
496
+ "The following pi extension tools are available, but Claude Code exposes",
497
+ "them under MCP-prefixed names. When a system prompt or task instruction",
498
+ "asks you to call one of these by its short name, use the MCP-prefixed",
499
+ "name instead. Their schemas are deferred — call `ToolSearch` with",
500
+ '`select:mcp__custom-tools__<name>` first, then call the tool directly.',
501
+ "",
502
+ ...lines,
503
+ ].join("\n");
504
+ }
505
+
402
506
  /**
403
507
  * Converts user message content to text.
404
508
  * Handles string content and array of content blocks.
@@ -462,7 +566,7 @@ function contentToText(content: string | unknown[]): string {
462
566
  : typeof rawArgs === "string"
463
567
  ? JSON.stringify(rawArgs)
464
568
  : "{}";
465
- return `Historical tool call (non-executable): ${claudeName} args=${argsStr}`;
569
+ return `[Prior tool call — already executed; result follows in TOOL RESULT (${claudeName}):] args=${argsStr}`;
466
570
  }
467
571
  // Unknown block types are represented as a placeholder
468
572
  return `[${String(block.type)}]`;
@@ -44,8 +44,23 @@ import { createEventBridge } from "./event-bridge.js";
44
44
  import { handleControlRequest } from "./control-handler.js";
45
45
  import { mapThinkingEffort } from "./thinking-config.js";
46
46
  import { isPiKnownClaudeTool } from "./tool-mapping.js";
47
- /** Inactivity timeout: kill subprocess if no stdout for 180 seconds (3 minutes). */
48
- const INACTIVITY_TIMEOUT_MS = 180_000;
47
+ /**
48
+ * Inactivity safety net for the Claude CLI subprocess.
49
+ *
50
+ * Set very high (30 minutes) because the caller is the authoritative source of
51
+ * truth for "this session is stuck": Fusion's engine runs a `StuckTaskDetector`
52
+ * with a configurable heartbeat (default 1 hour) and aborts the session via
53
+ * `AbortSignal` when it decides the agent has gone quiet. pi-claude-cli already
54
+ * forwards that signal to the subprocess (`forceKillProcess` on `signal.abort`).
55
+ *
56
+ * A short timeout here was racing the engine: Sonnet 4.6 with extended thinking
57
+ * on the triage prompt (~40k chars) routinely goes >3 minutes between thinking
58
+ * deltas, and we were killing those subprocesses before they could write
59
+ * PROMPT.md and call `fn_review_spec`. The half-hour ceiling is just a
60
+ * last-resort guard for catastrophically hung processes when no abort signal
61
+ * arrives (e.g. someone embeds pi-claude-cli without a stuck detector).
62
+ */
63
+ const INACTIVITY_TIMEOUT_MS = 30 * 60_000;
49
64
 
50
65
  /** Extended stream options: pi's SimpleStreamOptions plus optional cwd and mcpConfigPath */
51
66
  type StreamViaCLiOptions = SimpleStreamOptions & {
@@ -120,6 +135,7 @@ export function streamViaCli(
120
135
  newSessionId: !resumeSessionId ? options?.sessionId : undefined,
121
136
  });
122
137
  const getStderr = captureStderr(proc);
138
+ const spawnTime = Date.now();
123
139
 
124
140
  // Register in global process registry for teardown cleanup
125
141
  registerProcess(proc);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runfusion/fusion",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "description": "Fusion CLI: HTTP API server, daemon, dashboard launcher, and task tooling for the Fusion AI coding agent.",
6
6
  "homepage": "https://github.com/Runfusion/Fusion#readme",
@@ -75,10 +75,10 @@
75
75
  "typescript": "^5.7.0",
76
76
  "vitest": "^3.1.0",
77
77
  "yaml": "^2.8.3",
78
- "@fusion/core": "0.2.6",
79
- "@fusion/dashboard": "0.2.6",
80
- "@fusion/engine": "0.2.6",
81
- "@fusion/pi-claude-cli": "0.2.6"
78
+ "@fusion/core": "0.3.0",
79
+ "@fusion/engine": "0.3.0",
80
+ "@fusion/dashboard": "0.3.0",
81
+ "@fusion/pi-claude-cli": "0.3.0"
82
82
  },
83
83
  "repository": {
84
84
  "type": "git",
@@ -86,6 +86,7 @@
86
86
  },
87
87
  "scripts": {
88
88
  "dev": "tsx src/bin.ts",
89
+ "prebuild": "node ../../scripts/sync-fusion-skill-tools.mjs",
89
90
  "build": "tsup",
90
91
  "build:exe": "bun run build.ts",
91
92
  "build:exe:all": "bun run build.ts --all",
@@ -21,14 +21,18 @@ Mission → Milestone → Slice → Feature → Task
21
21
 
22
22
  **Available tools:** Fusion registers tools via the pi extension (prefixed `fn_*`). No CLI commands or Bash needed — use the registered tools directly.
23
23
 
24
- **Naming boundary:** The published skill surface always uses `fn_*` tool names (for example `fn_task_create`, `fn_mission_create`). Internal engine runtime tools like `task_create`, `task_update`, `task_log`, and `task_done` are intentionally unprefixed and not part of this skill.
24
+ **Naming boundary:** The published pi-extension skill surface uses `fn_*` tool names (for example `fn_task_create`, `fn_mission_create`). Engine runtime sessions also inject additional `fn_*` tools (for example `fn_review_spec`, `fn_review_step`, `fn_spawn_agent`) that are not user-invokable extension tools.
25
+
26
+ **Engine runtime tools:** Triage/executor/merger/heartbeat sessions include auto-injected engine tools that do not come from the pi extension registration list. See `references/engine-tools.md` for the canonical runtime-only catalog and usage boundaries.
25
27
 
26
28
  **Tool categories:**
29
+ <!-- BEGIN: tool-categories (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->
27
30
  - **Task tools** — `fn_task_create`, `fn_task_update`, `fn_task_list`, `fn_task_show`, `fn_task_attach`, `fn_task_pause`, `fn_task_unpause`, `fn_task_retry`, `fn_task_duplicate`, `fn_task_refine`, `fn_task_archive`, `fn_task_unarchive`, `fn_task_delete`, `fn_task_plan`
28
31
  - **GitHub tools** — `fn_task_import_github`, `fn_task_import_github_issue`, `fn_task_browse_github_issues`
29
32
  - **Mission tools** — `fn_mission_create`, `fn_mission_list`, `fn_mission_show`, `fn_mission_delete`, `fn_milestone_add`, `fn_slice_add`, `fn_feature_add`, `fn_slice_activate`, `fn_feature_link_task`
30
33
  - **Agent tools** — `fn_agent_stop`, `fn_agent_start`
31
34
  - **Skills tools** — `fn_skills_search`, `fn_skills_install`
35
+ <!-- END: tool-categories -->
32
36
  - **Dashboard** — Use `/fn` command to start/stop the dashboard
33
37
 
34
38
  </essential_principles>
@@ -100,6 +104,7 @@ For these operations, guide the user to the dashboard (`/fn`) or CLI commands do
100
104
  | references/extension-tools.md | All pi extension tools with parameters |
101
105
  | references/best-practices.md | Tips for effective Fusion usage |
102
106
  | references/fusion-capabilities.md | Complete feature catalog |
107
+ | references/engine-tools.md | Engine session-scoped runtime tools (not extension-invokable) |
103
108
  | references/skill-patterns.md | Patterns used in this skill's design |
104
109
 
105
110
  </reference_index>
@@ -0,0 +1,54 @@
1
+ # Engine Session-Scoped Tools
2
+
3
+ These tools are **not** part of the pi extension's user-invokable `extension.ts` surface. They are injected by the engine at runtime for specific agent session types.
4
+
5
+ - Source files: `packages/engine/src/agent-tools.ts`, `triage.ts`, `executor.ts`, `merger.ts`, `agent-heartbeat.ts`
6
+ - Availability: only when the engine creates a session for the matching agent role
7
+ - Important: do not tell users to call these directly from the generic pi extension tool list
8
+
9
+ ## Shared runtime tools (`agent-tools.ts`)
10
+
11
+ | Tool | Agent Types | Purpose | Parameters |
12
+ |---|---|---|---|
13
+ | `fn_task_create` | triage, executor, heartbeat | Create a follow-up task from within an agent run | `description` (string), `dependencies?` (string[]) |
14
+ | `fn_task_log` | executor, heartbeat | Write significant task log entries | `message` (string), `outcome?` (string) |
15
+ | `fn_task_document_write` | triage, executor, heartbeat | Save/update a named task document revision | `key` (string), `content` (string), `author?` (string) |
16
+ | `fn_task_document_read` | triage, executor, heartbeat | Read one task document or list all | `key?` (string) |
17
+ | `fn_memory_search` | triage, executor, heartbeat | Search project/agent memory snippets | `query` (string), `limit?` (number) |
18
+ | `fn_memory_get` | triage, executor, heartbeat | Read a bounded memory file window | `path` (string), `startLine?` (number), `lineCount?` (number) |
19
+ | `fn_memory_append` | executor, heartbeat (when writable backend enabled) | Append long-term/daily memory notes | `scope?` (`project` \| `agent`), `layer` (`long-term` \| `daily`), `content` (string) |
20
+ | `fn_reflect_on_performance` | executor | Generate reflection insights from prior runs | `focus_area?` (string) |
21
+ | `fn_list_agents` | triage, executor, heartbeat | List agents (optionally filtered) | `role?` (string), `state?` (string), `includeEphemeral?` (boolean) |
22
+ | `fn_delegate_task` | triage, executor, heartbeat | Create and assign a new task to a specific agent | `agent_id` (string), `description` (string), `dependencies?` (string[]) |
23
+ | `fn_send_message` | executor, heartbeat | Send inbox messages to agents/users | `to_id` (string), `content` (string), `type?` (`agent-to-agent` \| `agent-to-user`), `reply_to_message_id?` (string) |
24
+ | `fn_read_messages` | executor, heartbeat | Read inbox messages | `unread_only?` (boolean), `limit?` (number) |
25
+
26
+ ## Triage-only runtime tools (`triage.ts`)
27
+
28
+ | Tool | Purpose | Parameters |
29
+ |---|---|---|
30
+ | `fn_task_list` | List active tasks during specification (duplicate check, discovery) | none |
31
+ | `fn_task_get` | Fetch full task detail including PROMPT.md | `id` (string) |
32
+ | `fn_review_spec` | Spawn spec reviewer and return `APPROVE`/`REVISE`/`RETHINK`/`UNAVAILABLE` | none |
33
+
34
+ ## Executor-only runtime tools (`executor.ts`)
35
+
36
+ | Tool | Purpose | Parameters |
37
+ |---|---|---|
38
+ | `fn_task_update` | Update a spec step status (`pending`/`in-progress`/`done`/`skipped`) | `step` (number), `status` (enum) |
39
+ | `fn_task_add_dep` | Add a dependency to current task (confirmation-gated) | `task_id` (string), `confirm?` (boolean) |
40
+ | `fn_task_done` | Mark task complete and optionally store summary | `summary?` (string) |
41
+ | `fn_review_step` | Spawn step plan/code reviewer | `step` (number), `type` (`plan` \| `code`), `step_name` (string), `baseline?` (string) |
42
+ | `fn_spawn_agent` | Spawn child agent in separate worktree | `name` (string), `role` (enum), `task` (string) |
43
+
44
+ ## Merger-only runtime tools (`merger.ts`)
45
+
46
+ | Tool | Purpose | Parameters |
47
+ |---|---|---|
48
+ | `fn_report_build_failure` | Explicitly signal merge-time build verification failure | `message` (string) |
49
+
50
+ ## Heartbeat-only runtime tools (`agent-heartbeat.ts`)
51
+
52
+ | Tool | Purpose | Parameters |
53
+ |---|---|---|
54
+ | `fn_heartbeat_done` | Signal end of heartbeat run with optional summary | `summary?` (string) |