diff --git a/.changeset/fn-2392-skill-tool-prefix.md b/.changeset/fn-2392-skill-tool-prefix.md
new file mode 100644
index 000000000..23657877c
--- /dev/null
+++ b/.changeset/fn-2392-skill-tool-prefix.md
@@ -0,0 +1,5 @@
+---
+"@runfusion/fusion": patch
+---
+
+Normalize Fusion skill-facing tool naming to the public `fn_*` namespace and clarify the boundary between extension tools and internal engine runtime tools across skill docs.
diff --git a/packages/cli/skill/fusion/SKILL.md b/packages/cli/skill/fusion/SKILL.md
index 9be29f2cf..58a00a527 100644
--- a/packages/cli/skill/fusion/SKILL.md
+++ b/packages/cli/skill/fusion/SKILL.md
@@ -21,6 +21,8 @@ Mission → Milestone → Slice → Feature → Task
**Available tools:** Fusion registers tools via the pi extension (prefixed `fn_*`). No CLI commands or Bash needed — use the registered tools directly.
+**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.
+
**Tool categories:**
- **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`
- **GitHub tools** — `fn_task_import_github`, `fn_task_import_github_issue`, `fn_task_browse_github_issues`
diff --git a/packages/cli/skill/fusion/references/extension-tools.md b/packages/cli/skill/fusion/references/extension-tools.md
index 5d2ac8696..2f9288ebf 100644
--- a/packages/cli/skill/fusion/references/extension-tools.md
+++ b/packages/cli/skill/fusion/references/extension-tools.md
@@ -2,6 +2,8 @@
All tools are registered via the pi extension. They are available in any pi agent session when the Fusion extension is installed.
+> Naming contract: all externally exposed Fusion extension tools are `fn_*` (for example `fn_task_create`). Internal engine/executor runtime tools (`task_create`, `task_update`, `task_log`, `task_done`, etc.) are separate and intentionally out of scope for this skill surface.
+
## Task Tools
### fn_task_create
diff --git a/packages/cli/skill/fusion/references/fusion-capabilities.md b/packages/cli/skill/fusion/references/fusion-capabilities.md
index c16305175..349b32b64 100644
--- a/packages/cli/skill/fusion/references/fusion-capabilities.md
+++ b/packages/cli/skill/fusion/references/fusion-capabilities.md
@@ -7,6 +7,8 @@ Triage → Todo → In Progress → In Review → Done → Archived
## Pi Extension Tools (Available to Agents)
+All skill/extension tool invocations in this catalog use the public `fn_*` namespace. Engine runtime tools (for example `task_create`, `task_update`, `task_log`, `task_done`) are internal and intentionally not listed here.
+
| Tool | Purpose |
|------|---------|
| `fn_task_create` | Create a new task in triage |
diff --git a/packages/cli/skill/fusion/workflows/specifications.md b/packages/cli/skill/fusion/workflows/specifications.md
index 9ae5129e5..95e1cb786 100644
--- a/packages/cli/skill/fusion/workflows/specifications.md
+++ b/packages/cli/skill/fusion/workflows/specifications.md
@@ -5,6 +5,8 @@
Guide the agent through creating well-specified tasks and organizing work using the mission hierarchy for complex multi-phase projects.
+
+All tool examples in this workflow intentionally use the public `fn_*` extension namespace.
diff --git a/packages/cli/skill/fusion/workflows/task-management.md b/packages/cli/skill/fusion/workflows/task-management.md
index a4bbc9f05..b48326f01 100644
--- a/packages/cli/skill/fusion/workflows/task-management.md
+++ b/packages/cli/skill/fusion/workflows/task-management.md
@@ -5,6 +5,8 @@
Guide the agent through creating, viewing, and managing tasks on the Fusion board using pi extension tools.
+
+Use only the public `fn_*` extension tools in this workflow. Do not substitute internal engine runtime tools like `task_create`, `task_update`, `task_log`, or `task_done`.
diff --git a/packages/cli/src/__tests__/skill-sync.test.ts b/packages/cli/src/__tests__/skill-sync.test.ts
index ee2a67ad4..6ae53b593 100644
--- a/packages/cli/src/__tests__/skill-sync.test.ts
+++ b/packages/cli/src/__tests__/skill-sync.test.ts
@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest";
-import { readFileSync, existsSync } from "node:fs";
-import { resolve, dirname } from "node:path";
+import { readFileSync, existsSync, readdirSync } from "node:fs";
+import { resolve, dirname, relative, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -52,6 +52,25 @@ function getCapabilitiesToolNames(): string[] {
return matches.map((m) => m[1]).sort();
}
+function collectMarkdownFiles(dir: string): string[] {
+ const files: string[] = [];
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
+ const fullPath = join(dir, entry.name);
+ if (entry.isDirectory()) {
+ files.push(...collectMarkdownFiles(fullPath));
+ continue;
+ }
+ if (entry.isFile() && entry.name.endsWith(".md")) {
+ files.push(fullPath);
+ }
+ }
+ return files.sort();
+}
+
+function escapeRegex(value: string): string {
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
describe("Skill-Extension Sync", () => {
it("skill directory structure exists", () => {
expect(existsSync(resolve(skillDir, "SKILL.md"))).toBe(true);
@@ -105,6 +124,65 @@ describe("Skill-Extension Sync", () => {
expect(missingFromCaps).toEqual([]);
});
+ it("covers the full Fusion skill markdown surface", () => {
+ const markdownFiles = collectMarkdownFiles(skillDir).map((filePath) =>
+ relative(skillDir, filePath),
+ );
+
+ expect(markdownFiles).toEqual([
+ "SKILL.md",
+ "references/best-practices.md",
+ "references/cli-commands.md",
+ "references/extension-tools.md",
+ "references/fusion-capabilities.md",
+ "references/skill-patterns.md",
+ "references/task-structure.md",
+ "workflows/dashboard-cli.md",
+ "workflows/specifications.md",
+ "workflows/task-lifecycle.md",
+ "workflows/task-management.md",
+ ]);
+ });
+
+ it("enforces fn_* naming across extension + all skill markdown for public tools", () => {
+ const extensionTools = getExtensionToolNames();
+ const publicSuffixes = extensionTools.map((toolName) =>
+ toolName.replace(/^fn_/, ""),
+ );
+
+ // These names are intentionally unprefixed engine/runtime tools and are allowed
+ // to appear in docs that explain capability boundaries.
+ const allowedUnprefixedInternalTools = new Set([
+ "task_create",
+ "task_update",
+ "task_log",
+ "task_done",
+ "review_step",
+ "spawn_agent",
+ ]);
+
+ const forbiddenSuffixes = publicSuffixes.filter(
+ (suffix) => !allowedUnprefixedInternalTools.has(suffix),
+ );
+
+ const filesToScan = [extensionPath, ...collectMarkdownFiles(skillDir)];
+ const violations: string[] = [];
+
+ for (const filePath of filesToScan) {
+ const content = readFileSync(filePath, "utf-8");
+ const relativePath = relative(cliRoot, filePath);
+
+ for (const suffix of forbiddenSuffixes) {
+ const regex = new RegExp(`(? {
const skillMd = readFileSync(resolve(skillDir, "SKILL.md"), "utf-8");
expect(skillMd).toContain("/fn");
diff --git a/packages/core/src/agent-store.test.ts b/packages/core/src/agent-store.test.ts
index dc5fa769d..34f0ed2d1 100644
--- a/packages/core/src/agent-store.test.ts
+++ b/packages/core/src/agent-store.test.ts
@@ -1741,7 +1741,7 @@ describe("AgentStore", () => {
const activeRun = await store.getActiveHeartbeatRun(agent.id);
expect(activeRun).not.toBeNull();
expect(activeRun!.id).toBe(run.id);
- });
+ }, 15_000);
it("throws for non-existent agent", async () => {
await expect(
diff --git a/packages/core/src/plugin-loader.test.ts b/packages/core/src/plugin-loader.test.ts
index d1ec563c4..909cc731d 100644
--- a/packages/core/src/plugin-loader.test.ts
+++ b/packages/core/src/plugin-loader.test.ts
@@ -931,7 +931,7 @@ describe("PluginLoader", () => {
"Failed to load plugin bad-load-all-log:",
expect.any(Error),
);
- });
+ }, 15_000);
it("logs invokeHook failures", async () => {
await pluginStore.init();
@@ -1412,7 +1412,7 @@ describe("PluginLoader", () => {
expect(runtimes).toHaveLength(2);
expect(runtimes.find((r) => r.pluginId === "plugin-a")?.runtime.metadata.runtimeId).toBe("runtime-a");
expect(runtimes.find((r) => r.pluginId === "plugin-b")?.runtime.metadata.runtimeId).toBe("runtime-b");
- });
+ }, 15_000);
it("skips plugins without runtime registration when other plugins have runtimes", async () => {
await pluginStore.init();
diff --git a/packages/core/src/plugin-loader.ts b/packages/core/src/plugin-loader.ts
index 364e679fc..7eb758da0 100644
--- a/packages/core/src/plugin-loader.ts
+++ b/packages/core/src/plugin-loader.ts
@@ -289,7 +289,6 @@ export class PluginLoader extends EventEmitter<{
} else {
mod = await import(moduleUrl);
}
-
this.loadedModules.set(path, mod);
return mod;
}
diff --git a/packages/core/src/project-memory.ts b/packages/core/src/project-memory.ts
index c36f4b3b3..7a1b9c34c 100644
--- a/packages/core/src/project-memory.ts
+++ b/packages/core/src/project-memory.ts
@@ -387,8 +387,8 @@ This project has OpenClaw-style memory files:
- \`.fusion/memory/YYYY-MM-DD.md\` — append-only daily notes for running context
**Before writing the specification:**
-1. Use \`memory_search\` first for task-relevant context
-2. Use \`memory_get\` only for specific memory files/line ranges returned by search
+1. Use \`fn_memory_search\` first for task-relevant context
+2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
3. Incorporate relevant learnings into your specification — reference actual patterns, constraints, and conventions documented there
Do not read all memory directly by default. If memory is irrelevant, skip it.
@@ -402,8 +402,8 @@ Do not read all memory directly by default. If memory is irrelevant, skip it.
This project has a memory system that stores durable project learnings.
**Before writing the specification:**
-1. Use \`memory_search\` first for task-relevant context
-2. Use \`memory_get\` only for specific memory files/line ranges returned by search
+1. Use \`fn_memory_search\` first for task-relevant context
+2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
3. Incorporate useful learnings into your specification
**If the memory contains useful context for this task, reference it in the specification.**
@@ -465,12 +465,12 @@ This project has OpenClaw-style memory files:
- \`.fusion/memory/YYYY-MM-DD.md\` — append-only daily notes for running observations and open loops
**At the start of execution:**
-1. Use \`memory_search\` first for task-relevant context
-2. Use \`memory_get\` only for specific memory files/line ranges returned by search
+1. Use \`fn_memory_search\` first for task-relevant context
+2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
3. Apply relevant learnings to your implementation — follow documented patterns and avoid known pitfalls
4. Do not load all memory directly by default. Skip memory reads when memory is irrelevant or context is tight.
-**At the end of execution (before calling \`task_done()\`):**
+**At the end of execution (before calling \`fn_task_done()\`):**
1. Review what you learned during this task that would genuinely benefit future runs
2. Write durable decisions, conventions, and pitfalls to \`.fusion/memory/MEMORY.md\`
3. Write running observations, unresolved context, and open loops to today's \`.fusion/memory/YYYY-MM-DD.md\`
@@ -501,11 +501,11 @@ This project has OpenClaw-style memory files:
This project has a memory system that stores durable project learnings accumulated from past task runs.
**At the start of execution:**
-1. Use \`memory_search\` first for task-relevant context
-2. Use \`memory_get\` only for specific memory files/line ranges returned by search
+1. Use \`fn_memory_search\` first for task-relevant context
+2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
3. Apply useful learnings to your implementation
-**At the end of execution (before calling \`task_done()\`):**
+**At the end of execution (before calling \`fn_task_done()\`):**
1. Review what you learned during this task that would genuinely benefit future runs
2. **If nothing durable was learned, skip the memory update entirely** — do not append trivial or task-specific notes
3. Only write when you have genuinely durable, reusable insights such as:
@@ -538,8 +538,8 @@ export function buildReviewerMemoryInstructions(
This project has a memory system that stores durable project learnings.
**During review:**
-1. Use \`memory_search\` for task-relevant project conventions, pitfalls, and prior decisions when they could affect your verdict
-2. Use \`memory_get\` only for specific memory files/line ranges returned by search
+1. Use \`fn_memory_search\` for task-relevant project conventions, pitfalls, and prior decisions when they could affect your verdict
+2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
3. Treat documented durable conventions and pitfalls as review evidence when deciding APPROVE, REVISE, or RETHINK
4. Do not update memory during review; reviewer memory access is read-only
5. Skip memory reads when they are not relevant to the reviewed plan or code
diff --git a/packages/dashboard/app/components/AgentsView.tsx b/packages/dashboard/app/components/AgentsView.tsx
index 923216a0a..7badcf7af 100644
--- a/packages/dashboard/app/components/AgentsView.tsx
+++ b/packages/dashboard/app/components/AgentsView.tsx
@@ -350,6 +350,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
.filter((n): n is OrgTreeNode => n !== null);
}, [orgTree, showSystemAgents]);
+
useEffect(() => {
if (agentView !== "org") return;
diff --git a/packages/dashboard/app/components/ClaudeCliProviderCard.tsx b/packages/dashboard/app/components/ClaudeCliProviderCard.tsx
index 590ea4f1e..7952981ad 100644
--- a/packages/dashboard/app/components/ClaudeCliProviderCard.tsx
+++ b/packages/dashboard/app/components/ClaudeCliProviderCard.tsx
@@ -130,9 +130,81 @@ export function ClaudeCliProviderCard({
);
+ const actions = (
+ <>
+
+ {currentlyEnabled ? (
+
+ ) : (
+
+ )}
+ >
+ );
+
+ // Compact layout mirrors `.auth-provider-card` so it slots cleanly into
+ // the Settings > Authentication list and picks up the shared mobile rules.
+ if (compact) {
+ return (
+