feat(FN-2569): sync fusion skill docs with runtime tools
- Add generated engine-tools reference and refresh extension/capabilities docs from source definitions - Enhance sync-fusion-skill-tools script to verify and maintain skill documentation consistency - Expand skill-sync tests and enforce sync:fusion-skill:check in the workspace test pipeline - Add FN-2569 changeset documenting the published @runfusion/fusion patch update
This commit is contained in:
5
.changeset/fn-2569-sync-skill-tools.md
Normal file
5
.changeset/fn-2569-sync-skill-tools.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Synchronize Fusion skill documentation from `extension.ts` across `SKILL.md`, `references/extension-tools.md`, and `references/fusion-capabilities.md`, and document engine session-scoped runtime tools in a new `references/engine-tools.md` reference.
|
||||
@@ -24,7 +24,7 @@
|
||||
"verify:workspace": "pnpm lint && pnpm test && pnpm build",
|
||||
"build:exe": "pnpm build && pnpm --filter @runfusion/fusion build:exe",
|
||||
"build:exe:all": "pnpm build && pnpm --filter @runfusion/fusion build:exe:all",
|
||||
"test": "pnpm -r --workspace-concurrency=4 test",
|
||||
"test": "pnpm sync:fusion-skill:check && pnpm -r --workspace-concurrency=4 test",
|
||||
"test:serial": "pnpm -r --workspace-concurrency=1 test",
|
||||
"test:fast": "pnpm -r --workspace-concurrency=4 test",
|
||||
"test:build": "pnpm --filter @fusion/dashboard test:build",
|
||||
|
||||
@@ -21,7 +21,9 @@ 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.
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**Tool categories:**
|
||||
<!-- BEGIN: tool-categories (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->
|
||||
@@ -102,6 +104,7 @@ For these operations, guide the user to the dashboard (`/fn`) or CLI commands do
|
||||
| references/extension-tools.md | All pi extension tools with parameters |
|
||||
| references/best-practices.md | Tips for effective Fusion usage |
|
||||
| references/fusion-capabilities.md | Complete feature catalog |
|
||||
| references/engine-tools.md | Engine session-scoped runtime tools (not extension-invokable) |
|
||||
| references/skill-patterns.md | Patterns used in this skill's design |
|
||||
|
||||
</reference_index>
|
||||
|
||||
54
packages/cli/skill/fusion/references/engine-tools.md
Normal file
54
packages/cli/skill/fusion/references/engine-tools.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Engine Session-Scoped Tools
|
||||
|
||||
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.
|
||||
|
||||
- Source files: `packages/engine/src/agent-tools.ts`, `triage.ts`, `executor.ts`, `merger.ts`, `agent-heartbeat.ts`
|
||||
- Availability: only when the engine creates a session for the matching agent role
|
||||
- Important: do not tell users to call these directly from the generic pi extension tool list
|
||||
|
||||
## Shared runtime tools (`agent-tools.ts`)
|
||||
|
||||
| Tool | Agent Types | Purpose | Parameters |
|
||||
|---|---|---|---|
|
||||
| `fn_task_create` | triage, executor, heartbeat | Create a follow-up task from within an agent run | `description` (string), `dependencies?` (string[]) |
|
||||
| `fn_task_log` | executor, heartbeat | Write significant task log entries | `message` (string), `outcome?` (string) |
|
||||
| `fn_task_document_write` | triage, executor, heartbeat | Save/update a named task document revision | `key` (string), `content` (string), `author?` (string) |
|
||||
| `fn_task_document_read` | triage, executor, heartbeat | Read one task document or list all | `key?` (string) |
|
||||
| `fn_memory_search` | triage, executor, heartbeat | Search project/agent memory snippets | `query` (string), `limit?` (number) |
|
||||
| `fn_memory_get` | triage, executor, heartbeat | Read a bounded memory file window | `path` (string), `startLine?` (number), `lineCount?` (number) |
|
||||
| `fn_memory_append` | executor, heartbeat (when writable backend enabled) | Append long-term/daily memory notes | `scope?` (`project` \| `agent`), `layer` (`long-term` \| `daily`), `content` (string) |
|
||||
| `fn_reflect_on_performance` | executor | Generate reflection insights from prior runs | `focus_area?` (string) |
|
||||
| `fn_list_agents` | triage, executor, heartbeat | List agents (optionally filtered) | `role?` (string), `state?` (string), `includeEphemeral?` (boolean) |
|
||||
| `fn_delegate_task` | triage, executor, heartbeat | Create and assign a new task to a specific agent | `agent_id` (string), `description` (string), `dependencies?` (string[]) |
|
||||
| `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) |
|
||||
| `fn_read_messages` | executor, heartbeat | Read inbox messages | `unread_only?` (boolean), `limit?` (number) |
|
||||
|
||||
## Triage-only runtime tools (`triage.ts`)
|
||||
|
||||
| Tool | Purpose | Parameters |
|
||||
|---|---|---|
|
||||
| `fn_task_list` | List active tasks during specification (duplicate check, discovery) | none |
|
||||
| `fn_task_get` | Fetch full task detail including PROMPT.md | `id` (string) |
|
||||
| `fn_review_spec` | Spawn spec reviewer and return `APPROVE`/`REVISE`/`RETHINK`/`UNAVAILABLE` | none |
|
||||
|
||||
## Executor-only runtime tools (`executor.ts`)
|
||||
|
||||
| Tool | Purpose | Parameters |
|
||||
|---|---|---|
|
||||
| `fn_task_update` | Update a spec step status (`pending`/`in-progress`/`done`/`skipped`) | `step` (number), `status` (enum) |
|
||||
| `fn_task_add_dep` | Add a dependency to current task (confirmation-gated) | `task_id` (string), `confirm?` (boolean) |
|
||||
| `fn_task_done` | Mark task complete and optionally store summary | `summary?` (string) |
|
||||
| `fn_review_step` | Spawn step plan/code reviewer | `step` (number), `type` (`plan` \| `code`), `step_name` (string), `baseline?` (string) |
|
||||
| `fn_spawn_agent` | Spawn child agent in separate worktree | `name` (string), `role` (enum), `task` (string) |
|
||||
|
||||
## Merger-only runtime tools (`merger.ts`)
|
||||
|
||||
| Tool | Purpose | Parameters |
|
||||
|---|---|---|
|
||||
| `fn_report_build_failure` | Explicitly signal merge-time build verification failure | `message` (string) |
|
||||
|
||||
## Heartbeat-only runtime tools (`agent-heartbeat.ts`)
|
||||
|
||||
| Tool | Purpose | Parameters |
|
||||
|---|---|---|
|
||||
| `fn_heartbeat_done` | Signal end of heartbeat run with optional summary | `summary?` (string) |
|
||||
@@ -2,191 +2,187 @@
|
||||
|
||||
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.
|
||||
> Naming contract: all externally exposed Fusion extension tools are `fn_*` (for example `fn_task_create`). Engine runtime sessions also inject additional `fn_*` tools (for example `fn_review_step`, `fn_spawn_agent`, `fn_task_document_write`) that are separate from this extension surface and documented in `engine-tools.md`.
|
||||
|
||||
<!-- BEGIN: extension-tools (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->
|
||||
|
||||
## Task Tools
|
||||
|
||||
### fn_task_create
|
||||
|
||||
Create a new task on the Fusion board. Enters triage for AI specification.
|
||||
Create a new task on the Fusion task board. The task enters the triage column where the AI triage agent will specify it into a full prompt with steps, file scope, and acceptance criteria.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `description` | string | ✓ | What needs to be done — be descriptive |
|
||||
| `depends` | string[] | — | Task IDs this depends on (e.g., ["FN-001"]) |
|
||||
|
||||
Returns: task ID, column, dependencies, path
|
||||
| `depends` | array | — | Task IDs this depends on (e.g. ['FN-001', 'FN-002']) |
|
||||
| `agentId` | string | — | Agent ID to assign this task to (e.g. 'agent-abc123') |
|
||||
|
||||
### fn_task_update
|
||||
|
||||
Update fields on an existing task (title, description, dependencies).
|
||||
Update fields on an existing task. Supports modifying the title, description, dependencies, and assigned agent after task creation.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Task ID (e.g., FN-001) |
|
||||
| `id` | string | ✓ | Task ID (e.g. FN-001) |
|
||||
| `title` | string | — | New task title |
|
||||
| `description` | string | — | New task description |
|
||||
| `depends` | string[] | — | New dependency list — replaces existing |
|
||||
|
||||
Returns: task ID, list of updated fields
|
||||
| `depends` | array | — | New dependency list — replaces existing dependencies (e.g. ['FN-001', 'FN-002']) |
|
||||
| `agentId` | union | — | Agent ID to assign this task to, or null to clear (e.g. 'agent-abc123') |
|
||||
|
||||
### fn_task_list
|
||||
|
||||
List all tasks grouped by column.
|
||||
List all tasks on the Fusion board, grouped by column.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `column` | string | — | Filter to specific column |
|
||||
| `limit` | number | — | Max tasks per column (default: 10) |
|
||||
|
||||
Returns: formatted task list grouped by column
|
||||
| `column` | string(enum) | — | Filter to a specific column |
|
||||
| `limit` | number | — | Max tasks to show per column (default: 10) |
|
||||
|
||||
### fn_task_show
|
||||
|
||||
Show full task details including steps, progress, prompt preview, and log.
|
||||
Show full details for a task including steps, progress, and log entries.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Task ID (e.g., FN-001) |
|
||||
|
||||
Returns: task details with steps, prompt preview (500 chars), last 5 log entries
|
||||
| `id` | string | ✓ | Task ID (e.g. FN-001) |
|
||||
|
||||
### fn_task_attach
|
||||
|
||||
Attach a file to a task. Copies file to task's attachments directory.
|
||||
Attach a file to a task. Supports images (png, jpg, gif, webp) and text files (txt, log, json, yaml, yml, toml, csv, xml).
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Task ID |
|
||||
| `path` | string | ✓ | Path to file to attach |
|
||||
|
||||
Supported formats: png, jpg, jpeg, gif, webp, txt, log, json, yaml, yml, toml, csv, xml
|
||||
| `id` | string | ✓ | Task ID (e.g. FN-001) |
|
||||
| `path` | string | ✓ | Path to the file to attach |
|
||||
|
||||
### fn_task_pause
|
||||
|
||||
Pause automation for a task. Scheduler and executor will skip this task.
|
||||
Pause a task — stops all automated agent and scheduler interaction for this task.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Task ID |
|
||||
| `id` | string | ✓ | Task ID (e.g. FN-001) |
|
||||
|
||||
### fn_task_unpause
|
||||
|
||||
Resume automation for a paused task.
|
||||
Unpause a task — resumes automated agent and scheduler interaction.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Task ID |
|
||||
| `id` | string | ✓ | Task ID (e.g. FN-001) |
|
||||
|
||||
### fn_task_retry
|
||||
|
||||
Retry a failed task. Clears error state, moves to todo for re-execution.
|
||||
Retry a failed task — clears the error state and moves it back to the todo column for re-execution.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Task ID (must be in failed state) |
|
||||
| `id` | string | ✓ | Task ID to retry (e.g. FN-001). Must be in 'failed' state. |
|
||||
|
||||
### fn_task_duplicate
|
||||
|
||||
Duplicate a task. Creates a fresh copy in triage with same title and description.
|
||||
Duplicate an existing task, creating a fresh copy in triage. Copies the title and description but resets all execution state. The AI triage agent will re-specify the new task.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Source task ID to duplicate |
|
||||
| `id` | string | ✓ | Source task ID to duplicate (e.g. FN-001) |
|
||||
|
||||
### fn_task_refine
|
||||
|
||||
Create a follow-up task for a completed task. New task depends on the original.
|
||||
Request a refinement of a completed or in-review task. Creates a new follow-up task in triage that references the original task as a dependency. Use this when a done or in-review task needs additional work, improvements, or follow-up changes.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Task ID (must be done or in-review) |
|
||||
| `feedback` | string | ✓ | What needs to be refined (1-2000 chars) |
|
||||
| `id` | string | ✓ | Task ID to refine (e.g. FN-001). Must be in 'done' or 'in-review' column. |
|
||||
| `feedback` | string | ✓ | Description of what needs to be refined or improved |
|
||||
|
||||
### fn_task_archive
|
||||
|
||||
Archive a done task. Moves from done → archived.
|
||||
Archive a done task (move from done → archived). Archived tasks are preserved for historical reference but moved out of the main board view.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Task ID (must be in done column) |
|
||||
| `id` | string | ✓ | Task ID to archive (e.g. FN-001). Must be in 'done' column. |
|
||||
|
||||
### fn_task_unarchive
|
||||
|
||||
Restore an archived task. Moves from archived → done.
|
||||
Unarchive an archived task (move from archived → done). Restores the task to the done column.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Task ID (must be in archived column) |
|
||||
| `id` | string | ✓ | Task ID to unarchive (e.g. FN-001). Must be in 'archived' column. |
|
||||
|
||||
### fn_task_delete
|
||||
|
||||
Permanently delete a task. Cannot be undone.
|
||||
Permanently delete a task from the Fusion board. Tasks are deleted immediately and cannot be recovered.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Task ID |
|
||||
| `id` | string | ✓ | Task ID to delete (e.g. FN-001) |
|
||||
|
||||
### fn_task_plan
|
||||
|
||||
Create a task via AI-guided planning mode. Non-interactive when called from extension.
|
||||
Create a task via AI-guided planning mode — interactive conversation to refine your idea into a well-specified task.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `description` | string | — | Initial plan description |
|
||||
| `description` | string | — | Initial plan description (optional) — the AI will ask clarifying questions if not provided |
|
||||
|
||||
## GitHub Tools
|
||||
|
||||
### fn_task_import_github
|
||||
|
||||
Batch import GitHub issues as Fusion tasks.
|
||||
Import GitHub issues as Fusion tasks. Fetches open issues from a repository and creates tasks in the triage column. Each task includes the issue title and body with a link to the source issue.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `ownerRepo` | string | ✓ | Repository (e.g., "owner/repo") |
|
||||
| `limit` | number | — | Max issues (default: 30, max: 100) |
|
||||
| `labels` | string[] | — | Label names to filter by |
|
||||
| `ownerRepo` | string | ✓ | Repository in owner/repo format (e.g., 'dustinbyrne/fusion') |
|
||||
| `limit` | number | — | Max issues to import (default: 30, max: 100) |
|
||||
| `labels` | array | — | Label names to filter by |
|
||||
|
||||
### fn_task_import_github_issue
|
||||
|
||||
Import a single GitHub issue by number.
|
||||
Import a specific GitHub issue as a Fusion task. Fetches the issue by number and creates a single task in the triage column with the issue title and body.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `owner` | string | ✓ | Repository owner |
|
||||
| `repo` | string | ✓ | Repository name |
|
||||
| `issueNumber` | number | ✓ | GitHub issue number |
|
||||
| `owner` | string | ✓ | Repository owner (e.g., 'dustinbyrne') |
|
||||
| `repo` | string | ✓ | Repository name (e.g., 'fusion') |
|
||||
| `issueNumber` | number | ✓ | GitHub issue number to import |
|
||||
|
||||
### fn_task_browse_github_issues
|
||||
|
||||
Browse open issues from a repository before importing.
|
||||
List open GitHub issues from a repository to browse before importing. Returns issue numbers, titles, and URLs for selection. Use with fn_task_import_github_issue to import specific issues by number.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `owner` | string | ✓ | Repository owner |
|
||||
| `repo` | string | ✓ | Repository name |
|
||||
| `limit` | number | — | Max issues (default: 30, max: 100) |
|
||||
| `labels` | string[] | — | Label names to filter by |
|
||||
| `owner` | string | ✓ | Repository owner (e.g., 'dustinbyrne') |
|
||||
| `repo` | string | ✓ | Repository name (e.g., 'fusion') |
|
||||
| `limit` | number | — | Max issues to show (default: 30, max: 100) |
|
||||
| `labels` | array | — | Label names to filter by |
|
||||
|
||||
## Mission Tools
|
||||
|
||||
### fn_mission_create
|
||||
|
||||
Create a new mission — a high-level objective spanning multiple milestones.
|
||||
Create a new mission — a high-level objective that can span multiple milestones. Missions contain milestones that break down work into phases.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `title` | string | ✓ | Mission title |
|
||||
| `description` | string | — | Detailed objectives and context |
|
||||
| `autoAdvance` | boolean | — | Auto-activate next slice on completion |
|
||||
| `title` | string | ✓ | Mission title — brief but descriptive |
|
||||
| `description` | string | — | Detailed mission objectives and context |
|
||||
| `autoAdvance` | boolean | — | Automatically activate the next pending slice when the current slice completes |
|
||||
|
||||
### fn_mission_list
|
||||
|
||||
List all missions with current status. No parameters.
|
||||
List all missions with their current status.
|
||||
|
||||
No parameters.
|
||||
|
||||
### fn_mission_show
|
||||
|
||||
Show mission details with full hierarchy.
|
||||
Show mission details with full hierarchy: milestones → slices → features.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
@@ -194,63 +190,65 @@ Show mission details with full hierarchy.
|
||||
|
||||
### fn_mission_delete
|
||||
|
||||
Delete a mission and all children. Tasks are NOT deleted.
|
||||
Delete a mission and all its milestones, slices, and features. Cannot be undone.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Mission ID |
|
||||
| `id` | string | ✓ | Mission ID to delete (e.g., M-001) |
|
||||
|
||||
### fn_milestone_add
|
||||
|
||||
Add a milestone to a mission.
|
||||
Add a milestone to a mission. Milestones represent phases of work.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `missionId` | string | ✓ | Parent mission ID |
|
||||
| `missionId` | string | ✓ | Parent mission ID (e.g., M-001) |
|
||||
| `title` | string | ✓ | Milestone title |
|
||||
| `description` | string | — | Milestone description |
|
||||
|
||||
### fn_slice_add
|
||||
|
||||
Add a slice to a milestone.
|
||||
Add a slice to a milestone. Slices are work units that can be activated for implementation.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `milestoneId` | string | ✓ | Parent milestone ID |
|
||||
| `milestoneId` | string | ✓ | Parent milestone ID (e.g., MS-001) |
|
||||
| `title` | string | ✓ | Slice title |
|
||||
| `description` | string | — | Slice description |
|
||||
|
||||
### fn_feature_add
|
||||
|
||||
Add a feature to a slice.
|
||||
Add a feature to a slice. Features are deliverables that can be linked to tasks.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `sliceId` | string | ✓ | Parent slice ID |
|
||||
| `sliceId` | string | ✓ | Parent slice ID (e.g., SL-001) |
|
||||
| `title` | string | ✓ | Feature title |
|
||||
| `description` | string | — | Feature description |
|
||||
| `acceptanceCriteria` | string | — | Acceptance criteria |
|
||||
| `acceptanceCriteria` | string | — | Acceptance criteria for completing the feature |
|
||||
|
||||
### fn_slice_activate
|
||||
|
||||
Activate a pending slice for implementation.
|
||||
Activate a pending slice for implementation. Sets status to 'active' and enables task linking for its features.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Slice ID (must be pending) |
|
||||
| `id` | string | ✓ | Slice ID to activate (e.g., SL-001) |
|
||||
|
||||
### fn_feature_link_task
|
||||
|
||||
Link a feature to a Fusion task. Updates feature status to triaged.
|
||||
Link a feature to a fn task for implementation. Updates the feature status to 'triaged' and associates it with the task.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `featureId` | string | ✓ | Feature ID (e.g., F-001) |
|
||||
| `taskId` | string | ✓ | Task ID (e.g., FN-001) |
|
||||
| `featureId` | string | ✓ | Feature ID to link (e.g., F-001) |
|
||||
| `taskId` | string | ✓ | Task ID to link to (e.g., FN-001) |
|
||||
|
||||
## Agent Tools
|
||||
|
||||
### fn_agent_stop
|
||||
|
||||
Stop (pause) a running agent. Transitions the agent from running/active to paused state.
|
||||
Stop a running agent — pauses its execution. Transitions the agent from running/active to paused state.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
@@ -258,7 +256,7 @@ Stop (pause) a running agent. Transitions the agent from running/active to pause
|
||||
|
||||
### fn_agent_start
|
||||
|
||||
Start (resume) a stopped agent. Transitions the agent from paused to active state.
|
||||
Start a stopped agent — resumes its execution. Transitions the agent from paused to active state.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
@@ -268,22 +266,23 @@ Start (resume) a stopped agent. Transitions the agent from paused to active stat
|
||||
|
||||
### fn_skills_search
|
||||
|
||||
Search the skills.sh directory for agent skills. Returns matching skills with names, sources, install counts, and install commands.
|
||||
Search the skills.sh directory for agent skills. Returns matching skills with names, sources (owner/repo), install counts, and install commands. Use fn_skills_install to install a selected skill.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `query` | string | ✓ | Search query — framework, technology, or capability (e.g., "react", "firebase", "testing", "docker") |
|
||||
| `limit` | number | — | Max results (default: 10, max: 50) |
|
||||
| `query` | string | ✓ | Search query — framework name, technology, or capability (e.g., 'react', 'firebase', 'testing', 'docker') |
|
||||
| `limit` | number | — | Max results to return (default: 10, max: 50) |
|
||||
|
||||
### fn_skills_install
|
||||
|
||||
Install an agent skill from skills.sh into the current project. Downloads skill files into the project's skill directories.
|
||||
Install an agent skill from skills.sh into the current project. Downloads skill files into the project's skill directories (.fusion/skills/, legacy .pi/skills/, .agents/skills/). The skill becomes available to AI agents in subsequent sessions.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `source` | string | ✓ | GitHub source in owner/repo format (e.g., "firebase/agent-skills") |
|
||||
| `skill` | string | — | Specific skill name to install. Omit to install all skills from the source. |
|
||||
| `source` | string | ✓ | GitHub source in owner/repo format (e.g., 'firebase/agent-skills') |
|
||||
| `skill` | string | — | Specific skill name to install (e.g., 'firebase-basics'). Omit to install all skills from the source. |
|
||||
|
||||
<!-- END: extension-tools -->
|
||||
## Dashboard Command
|
||||
|
||||
### /fn
|
||||
|
||||
@@ -7,40 +7,42 @@ 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.
|
||||
All skill/extension tool invocations in this catalog use the public `fn_*` namespace. Engine runtime sessions also have additional runtime-only `fn_*` tools that are intentionally not listed here (see `references/engine-tools.md`).
|
||||
|
||||
<!-- BEGIN: fusion-capabilities-tool-table (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `fn_task_create` | Create a new task in triage |
|
||||
| `fn_task_update` | Update task title, description, or dependencies |
|
||||
| `fn_task_list` | List all tasks grouped by column |
|
||||
| `fn_task_show` | Show full task details, steps, and log preview |
|
||||
| `fn_task_attach` | Attach a file to a task |
|
||||
| `fn_task_pause` | Pause automation for a task |
|
||||
| `fn_task_unpause` | Resume automation for a task |
|
||||
| `fn_task_retry` | Retry a failed task (clears error, moves to todo) |
|
||||
| `fn_task_duplicate` | Duplicate a task (copy to triage) |
|
||||
| `fn_task_refine` | Create refinement task for follow-up work |
|
||||
| `fn_task_archive` | Archive a done task |
|
||||
| `fn_task_unarchive` | Restore an archived task |
|
||||
| `fn_task_delete` | Permanently delete a task |
|
||||
| `fn_task_import_github` | Batch import GitHub issues as tasks |
|
||||
| `fn_task_import_github_issue` | Import a single GitHub issue |
|
||||
| `fn_task_browse_github_issues` | Browse GitHub issues before importing |
|
||||
| `fn_task_plan` | Create task via AI-guided planning mode |
|
||||
| `fn_mission_create` | Create a new mission |
|
||||
| `fn_mission_list` | List all missions |
|
||||
| `fn_mission_show` | Show mission hierarchy |
|
||||
| `fn_mission_delete` | Delete a mission |
|
||||
| `fn_milestone_add` | Add a milestone to a mission |
|
||||
| `fn_slice_add` | Add a slice to a milestone |
|
||||
| `fn_feature_add` | Add a feature to a slice |
|
||||
| `fn_slice_activate` | Activate a pending slice |
|
||||
| `fn_feature_link_task` | Link a feature to a task |
|
||||
| `fn_agent_stop` | Stop (pause) a running agent |
|
||||
| `fn_agent_start` | Start (resume) a stopped agent |
|
||||
| `fn_skills_search` | Search skills.sh for agent skills |
|
||||
| `fn_skills_install` | Install a skill from skills.sh |
|
||||
| `fn_task_create` | Create a new task on the Fusion task board. The task enters the triage column where the AI triage agent will specify it into a full prompt with steps, file scope, and acceptance criteria. |
|
||||
| `fn_task_update` | Update fields on an existing task. Supports modifying the title, description, dependencies, and assigned agent after task creation. |
|
||||
| `fn_task_list` | List all tasks on the Fusion board, grouped by column. |
|
||||
| `fn_task_show` | Show full details for a task including steps, progress, and log entries. |
|
||||
| `fn_task_attach` | Attach a file to a task. Supports images (png, jpg, gif, webp) and text files (txt, log, json, yaml, yml, toml, csv, xml). |
|
||||
| `fn_task_pause` | Pause a task — stops all automated agent and scheduler interaction for this task. |
|
||||
| `fn_task_unpause` | Unpause a task — resumes automated agent and scheduler interaction. |
|
||||
| `fn_task_retry` | Retry a failed task — clears the error state and moves it back to the todo column for re-execution. |
|
||||
| `fn_task_duplicate` | Duplicate an existing task, creating a fresh copy in triage. Copies the title and description but resets all execution state. The AI triage agent will re-specify the new task. |
|
||||
| `fn_task_refine` | Request a refinement of a completed or in-review task. Creates a new follow-up task in triage that references the original task as a dependency. Use this when a done or in-review task needs additional work, improvements, or follow-up changes. |
|
||||
| `fn_task_archive` | Archive a done task (move from done → archived). Archived tasks are preserved for historical reference but moved out of the main board view. |
|
||||
| `fn_task_unarchive` | Unarchive an archived task (move from archived → done). Restores the task to the done column. |
|
||||
| `fn_task_delete` | Permanently delete a task from the Fusion board. Tasks are deleted immediately and cannot be recovered. |
|
||||
| `fn_task_import_github` | Import GitHub issues as Fusion tasks. Fetches open issues from a repository and creates tasks in the triage column. Each task includes the issue title and body with a link to the source issue. |
|
||||
| `fn_task_import_github_issue` | Import a specific GitHub issue as a Fusion task. Fetches the issue by number and creates a single task in the triage column with the issue title and body. |
|
||||
| `fn_task_browse_github_issues` | List open GitHub issues from a repository to browse before importing. Returns issue numbers, titles, and URLs for selection. Use with fn_task_import_github_issue to import specific issues by number. |
|
||||
| `fn_task_plan` | Create a task via AI-guided planning mode — interactive conversation to refine your idea into a well-specified task. |
|
||||
| `fn_mission_create` | Create a new mission — a high-level objective that can span multiple milestones. Missions contain milestones that break down work into phases. |
|
||||
| `fn_mission_list` | List all missions with their current status. |
|
||||
| `fn_mission_show` | Show mission details with full hierarchy: milestones → slices → features. |
|
||||
| `fn_mission_delete` | Delete a mission and all its milestones, slices, and features. Cannot be undone. |
|
||||
| `fn_milestone_add` | Add a milestone to a mission. Milestones represent phases of work. |
|
||||
| `fn_slice_add` | Add a slice to a milestone. Slices are work units that can be activated for implementation. |
|
||||
| `fn_feature_add` | Add a feature to a slice. Features are deliverables that can be linked to tasks. |
|
||||
| `fn_slice_activate` | Activate a pending slice for implementation. Sets status to 'active' and enables task linking for its features. |
|
||||
| `fn_feature_link_task` | Link a feature to a fn task for implementation. Updates the feature status to 'triaged' and associates it with the task. |
|
||||
| `fn_agent_stop` | Stop a running agent — pauses its execution. Transitions the agent from running/active to paused state. |
|
||||
| `fn_agent_start` | Start a stopped agent — resumes its execution. Transitions the agent from paused to active state. |
|
||||
| `fn_skills_search` | Search the skills.sh directory for agent skills. Returns matching skills with names, sources (owner/repo), install counts, and install commands. Use fn_skills_install to install a selected skill. |
|
||||
| `fn_skills_install` | Install an agent skill from skills.sh into the current project. Downloads skill files into the project's skill directories (.fusion/skills/, legacy .pi/skills/, .agents/skills/). The skill becomes available to AI agents in subsequent sessions. |
|
||||
<!-- END: fusion-capabilities-tool-table -->
|
||||
|
||||
## CLI Commands (fn)
|
||||
|
||||
|
||||
@@ -6,17 +6,243 @@ import { spawnSync } from "node:child_process";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const cliRoot = resolve(__dirname, "../..");
|
||||
const repoRoot = resolve(cliRoot, "../..");
|
||||
const skillDir = resolve(cliRoot, "skill/fusion");
|
||||
const extensionPath = resolve(cliRoot, "src/extension.ts");
|
||||
|
||||
const EXT_TOOLS_BEGIN =
|
||||
"<!-- BEGIN: extension-tools (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->";
|
||||
const EXT_TOOLS_END = "<!-- END: extension-tools -->";
|
||||
const CAP_TABLE_BEGIN =
|
||||
"<!-- BEGIN: fusion-capabilities-tool-table (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->";
|
||||
const CAP_TABLE_END = "<!-- END: fusion-capabilities-tool-table -->";
|
||||
|
||||
const engineToolSourceFiles = [
|
||||
resolve(repoRoot, "packages/engine/src/agent-tools.ts"),
|
||||
resolve(repoRoot, "packages/engine/src/triage.ts"),
|
||||
resolve(repoRoot, "packages/engine/src/executor.ts"),
|
||||
resolve(repoRoot, "packages/engine/src/merger.ts"),
|
||||
resolve(repoRoot, "packages/engine/src/agent-heartbeat.ts"),
|
||||
];
|
||||
|
||||
function findMatchingBrace(source: string, openIndex: number): number {
|
||||
let depth = 0;
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
let inTemplate = false;
|
||||
let inLineComment = false;
|
||||
let inBlockComment = false;
|
||||
let escaped = false;
|
||||
|
||||
for (let i = openIndex; i < source.length; i++) {
|
||||
const char = source[i];
|
||||
const next = source[i + 1];
|
||||
|
||||
if (inLineComment) {
|
||||
if (char === "\n") inLineComment = false;
|
||||
continue;
|
||||
}
|
||||
if (inBlockComment) {
|
||||
if (char === "*" && next === "/") {
|
||||
inBlockComment = false;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingle || inDouble || inTemplate) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (char === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (inSingle && char === "'") inSingle = false;
|
||||
else if (inDouble && char === '"') inDouble = false;
|
||||
else if (inTemplate && char === "`") inTemplate = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "/" && next === "/") {
|
||||
inLineComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (char === "/" && next === "*") {
|
||||
inBlockComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "'") {
|
||||
inSingle = true;
|
||||
continue;
|
||||
}
|
||||
if (char === '"') {
|
||||
inDouble = true;
|
||||
continue;
|
||||
}
|
||||
if (char === "`") {
|
||||
inTemplate = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "{") depth++;
|
||||
if (char === "}") {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Unbalanced braces");
|
||||
}
|
||||
|
||||
function splitTopLevelProperties(objectBody: string): string[] {
|
||||
const props: string[] = [];
|
||||
let start = 0;
|
||||
let depthParen = 0;
|
||||
let depthBrace = 0;
|
||||
let depthBracket = 0;
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
let inTemplate = false;
|
||||
let inLineComment = false;
|
||||
let inBlockComment = false;
|
||||
let escaped = false;
|
||||
|
||||
for (let i = 0; i < objectBody.length; i++) {
|
||||
const ch = objectBody[i];
|
||||
const next = objectBody[i + 1];
|
||||
|
||||
if (inLineComment) {
|
||||
if (ch === "\n") inLineComment = false;
|
||||
continue;
|
||||
}
|
||||
if (inBlockComment) {
|
||||
if (ch === "*" && next === "/") {
|
||||
inBlockComment = false;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingle || inDouble || inTemplate) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (inSingle && ch === "'") inSingle = false;
|
||||
else if (inDouble && ch === '"') inDouble = false;
|
||||
else if (inTemplate && ch === "`") inTemplate = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "/" && next === "/") {
|
||||
inLineComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (ch === "/" && next === "*") {
|
||||
inBlockComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "'") {
|
||||
inSingle = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
inDouble = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === "`") {
|
||||
inTemplate = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "(") depthParen++;
|
||||
else if (ch === ")") depthParen--;
|
||||
else if (ch === "{") depthBrace++;
|
||||
else if (ch === "}") depthBrace--;
|
||||
else if (ch === "[") depthBracket++;
|
||||
else if (ch === "]") depthBracket--;
|
||||
|
||||
if (ch === "," && depthParen === 0 && depthBrace === 0 && depthBracket === 0) {
|
||||
const prop = objectBody.slice(start, i).trim();
|
||||
if (prop) props.push(prop);
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const tail = objectBody.slice(start).trim();
|
||||
if (tail) props.push(tail);
|
||||
return props;
|
||||
}
|
||||
|
||||
function getRegisterToolBlocks(): Array<{ name: string; block: string }> {
|
||||
const src = readFileSync(extensionPath, "utf-8");
|
||||
const blocks: Array<{ name: string; block: string }> = [];
|
||||
const token = "pi.registerTool(";
|
||||
let from = 0;
|
||||
|
||||
while (true) {
|
||||
const start = src.indexOf(token, from);
|
||||
if (start === -1) break;
|
||||
const braceStart = src.indexOf("{", start);
|
||||
const braceEnd = findMatchingBrace(src, braceStart);
|
||||
const block = src.slice(braceStart, braceEnd + 1);
|
||||
const nameMatch = block.match(/name:\s*"(fn_[a-z_]+)"/);
|
||||
if (nameMatch) {
|
||||
blocks.push({ name: nameMatch[1], block });
|
||||
}
|
||||
from = braceEnd + 1;
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all tool names registered via pi.registerTool({ name: "..." })
|
||||
* from the extension source code.
|
||||
*/
|
||||
function getExtensionToolNames(): string[] {
|
||||
const src = readFileSync(extensionPath, "utf-8");
|
||||
const matches = [...src.matchAll(/name:\s*"(fn_[a-z_]+)"/g)];
|
||||
return matches.map((m) => m[1]).sort();
|
||||
return getRegisterToolBlocks()
|
||||
.map((entry) => entry.name)
|
||||
.sort();
|
||||
}
|
||||
|
||||
function getExtensionToolParamNames(): Map<string, string[]> {
|
||||
const result = new Map<string, string[]>();
|
||||
for (const { name, block } of getRegisterToolBlocks()) {
|
||||
const paramsStart = block.indexOf("parameters:");
|
||||
if (paramsStart === -1) {
|
||||
result.set(name, []);
|
||||
continue;
|
||||
}
|
||||
const objectStart = block.indexOf("Type.Object(", paramsStart);
|
||||
if (objectStart === -1) {
|
||||
result.set(name, []);
|
||||
continue;
|
||||
}
|
||||
const braceStart = block.indexOf("{", objectStart);
|
||||
const braceEnd = findMatchingBrace(block, braceStart);
|
||||
const body = block.slice(braceStart + 1, braceEnd);
|
||||
|
||||
const params = splitTopLevelProperties(body)
|
||||
.map((prop) => prop.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:/)?.[1])
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.sort();
|
||||
|
||||
result.set(name, params);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,13 +257,28 @@ function getDocumentedToolNames(): string[] {
|
||||
return matches.map((m) => m[1]).sort();
|
||||
}
|
||||
|
||||
function getDocumentedToolParamNames(): Map<string, string[]> {
|
||||
const doc = readFileSync(resolve(skillDir, "references/extension-tools.md"), "utf-8");
|
||||
const map = new Map<string, string[]>();
|
||||
|
||||
const toolRegex = /^### (fn_[a-z_]+)\n([\s\S]*?)(?=^### fn_|^## [A-Za-z]|^<!-- END: extension-tools -->)/gm;
|
||||
for (const match of doc.matchAll(toolRegex)) {
|
||||
const [, toolName, section] = match;
|
||||
const params = [...section.matchAll(/\| `([A-Za-z_][A-Za-z0-9_]*)` \|/g)]
|
||||
.map((m) => m[1])
|
||||
.sort();
|
||||
map.set(toolName, params);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tool names listed in SKILL.md under the tool categories.
|
||||
*/
|
||||
function getSkillMdToolNames(): string[] {
|
||||
const doc = readFileSync(resolve(skillDir, "SKILL.md"), "utf-8");
|
||||
const matches = [...doc.matchAll(/`(fn_[a-z_]+)`/g)];
|
||||
// Deduplicate
|
||||
return [...new Set(matches.map((m) => m[1]))].sort();
|
||||
}
|
||||
|
||||
@@ -53,6 +294,22 @@ function getCapabilitiesToolNames(): string[] {
|
||||
return matches.map((m) => m[1]).sort();
|
||||
}
|
||||
|
||||
function getEngineSessionToolNames(): string[] {
|
||||
const names = new Set<string>();
|
||||
for (const path of engineToolSourceFiles) {
|
||||
const src = readFileSync(path, "utf-8");
|
||||
for (const match of src.matchAll(/name:\s*"(fn_[a-z_]+)"/g)) {
|
||||
names.add(match[1]);
|
||||
}
|
||||
}
|
||||
return [...names].sort();
|
||||
}
|
||||
|
||||
function getDocumentedEngineToolNames(): string[] {
|
||||
const doc = readFileSync(resolve(skillDir, "references/engine-tools.md"), "utf-8");
|
||||
return [...new Set([...doc.matchAll(/`(fn_[a-z_]+)`/g)].map((m) => m[1]))].sort();
|
||||
}
|
||||
|
||||
function collectMarkdownFiles(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
@@ -105,6 +362,17 @@ describe("Skill-Extension Sync", () => {
|
||||
expect(documentedTools).toEqual(extensionTools);
|
||||
});
|
||||
|
||||
it("extension-tools.md tool parameter tables match extension.ts registrations", () => {
|
||||
const extensionTools = getExtensionToolParamNames();
|
||||
const documentedTools = getDocumentedToolParamNames();
|
||||
|
||||
for (const [toolName, extensionParams] of extensionTools.entries()) {
|
||||
expect(documentedTools.has(toolName), `missing documented section for ${toolName}`).toBe(true);
|
||||
const documentedParams = documentedTools.get(toolName) ?? [];
|
||||
expect(documentedParams).toEqual(extensionParams);
|
||||
}
|
||||
});
|
||||
|
||||
it("SKILL.md tool listing includes all registered tools", () => {
|
||||
const extensionTools = getExtensionToolNames();
|
||||
const skillTools = getSkillMdToolNames();
|
||||
@@ -123,6 +391,26 @@ describe("Skill-Extension Sync", () => {
|
||||
(t) => !capTools.includes(t),
|
||||
);
|
||||
expect(missingFromCaps).toEqual([]);
|
||||
expect(capTools).toEqual(extensionTools);
|
||||
});
|
||||
|
||||
it("fusion-capabilities.md tool table is auto-generated with markers", () => {
|
||||
const doc = readFileSync(resolve(skillDir, "references/fusion-capabilities.md"), "utf-8");
|
||||
expect(doc).toContain(CAP_TABLE_BEGIN);
|
||||
expect(doc).toContain(CAP_TABLE_END);
|
||||
});
|
||||
|
||||
it("extension-tools.md has auto-generated markers", () => {
|
||||
const doc = readFileSync(resolve(skillDir, "references/extension-tools.md"), "utf-8");
|
||||
expect(doc).toContain(EXT_TOOLS_BEGIN);
|
||||
expect(doc).toContain(EXT_TOOLS_END);
|
||||
});
|
||||
|
||||
it("engine-tools.md documents all engine session-scoped tools", () => {
|
||||
const engineTools = getEngineSessionToolNames();
|
||||
const documented = getDocumentedEngineToolNames();
|
||||
const missing = engineTools.filter((name) => !documented.includes(name));
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
it("covers the full Fusion skill markdown surface", () => {
|
||||
@@ -134,6 +422,7 @@ describe("Skill-Extension Sync", () => {
|
||||
"SKILL.md",
|
||||
"references/best-practices.md",
|
||||
"references/cli-commands.md",
|
||||
"references/engine-tools.md",
|
||||
"references/extension-tools.md",
|
||||
"references/fusion-capabilities.md",
|
||||
"references/skill-patterns.md",
|
||||
@@ -151,8 +440,6 @@ describe("Skill-Extension Sync", () => {
|
||||
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",
|
||||
@@ -195,8 +482,7 @@ describe("Skill-Extension Sync", () => {
|
||||
expect(dashboardCli).toContain("/fn");
|
||||
});
|
||||
|
||||
it("SKILL.md tool-categories block matches the sync script output (no drift)", () => {
|
||||
const repoRoot = resolve(cliRoot, "../..");
|
||||
it("SKILL.md and reference generated blocks match sync script output (no drift)", () => {
|
||||
const script = resolve(repoRoot, "scripts/sync-fusion-skill-tools.mjs");
|
||||
const result = spawnSync("node", [script, "--check"], {
|
||||
encoding: "utf-8",
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Regenerates the `tool-categories` block in the Fusion skill SKILL.md from
|
||||
* the canonical list of `fn_*` tools registered in `packages/cli/src/extension.ts`.
|
||||
* Syncs Fusion skill docs from the canonical pi extension registrations.
|
||||
*
|
||||
* Source of truth: extension.ts (each tool is registered with `name: "fn_..."`).
|
||||
* Output: the `<!-- BEGIN: tool-categories ... -->` / `<!-- END: tool-categories -->`
|
||||
* block inside `packages/cli/skill/fusion/SKILL.md`.
|
||||
*
|
||||
* The `.claude/skills/fusion` path is a symlink to `packages/cli/skill/fusion`,
|
||||
* so it picks up the change automatically.
|
||||
* Full flow:
|
||||
* packages/cli/src/extension.ts
|
||||
* → scripts/sync-fusion-skill-tools.mjs
|
||||
* → packages/cli/skill/fusion/SKILL.md (tool categories)
|
||||
* → packages/cli/skill/fusion/references/extension-tools.md (tool sections + params)
|
||||
* → packages/cli/skill/fusion/references/fusion-capabilities.md (tool table)
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/sync-fusion-skill-tools.mjs # rewrite SKILL.md in place
|
||||
* node scripts/sync-fusion-skill-tools.mjs --check # exit 1 if drift detected
|
||||
*
|
||||
* Wired into `packages/cli` build via the `prebuild` script so a fresh build
|
||||
* always emits an up-to-date skill, and CI catches drift via `--check`.
|
||||
* node scripts/sync-fusion-skill-tools.mjs
|
||||
* node scripts/sync-fusion-skill-tools.mjs --check
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
@@ -26,15 +22,46 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(__dirname, "..");
|
||||
const extensionPath = resolve(repoRoot, "packages/cli/src/extension.ts");
|
||||
const skillPath = resolve(repoRoot, "packages/cli/skill/fusion/SKILL.md");
|
||||
const extensionToolsPath = resolve(
|
||||
repoRoot,
|
||||
"packages/cli/skill/fusion/references/extension-tools.md",
|
||||
);
|
||||
const capabilitiesPath = resolve(
|
||||
repoRoot,
|
||||
"packages/cli/skill/fusion/references/fusion-capabilities.md",
|
||||
);
|
||||
|
||||
const BEGIN_MARKER =
|
||||
const SKILL_BEGIN =
|
||||
"<!-- BEGIN: tool-categories (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->";
|
||||
const END_MARKER = "<!-- END: tool-categories -->";
|
||||
const SKILL_END = "<!-- END: tool-categories -->";
|
||||
|
||||
const EXT_TOOLS_BEGIN =
|
||||
"<!-- BEGIN: extension-tools (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->";
|
||||
const EXT_TOOLS_END = "<!-- END: extension-tools -->";
|
||||
|
||||
const CAP_TABLE_BEGIN =
|
||||
"<!-- BEGIN: fusion-capabilities-tool-table (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->";
|
||||
const CAP_TABLE_END = "<!-- END: fusion-capabilities-tool-table -->";
|
||||
|
||||
const CATEGORY_ORDER = ["Task", "GitHub", "Mission", "Agent", "Skills", "Other"];
|
||||
const CATEGORY_LABELS = {
|
||||
Task: "Task tools",
|
||||
GitHub: "GitHub tools",
|
||||
Mission: "Mission tools",
|
||||
Agent: "Agent tools",
|
||||
Skills: "Skills tools",
|
||||
Other: "Other tools",
|
||||
};
|
||||
|
||||
const CATEGORY_HEADERS = {
|
||||
Task: "## Task Tools",
|
||||
GitHub: "## GitHub Tools",
|
||||
Mission: "## Mission Tools",
|
||||
Agent: "## Agent Tools",
|
||||
Skills: "## Skills Tools",
|
||||
Other: "## Other Tools",
|
||||
};
|
||||
|
||||
/**
|
||||
* Categorize a tool name into one of the published skill categories.
|
||||
* The order of categories here defines the order of bullets in SKILL.md.
|
||||
*/
|
||||
function categorize(name) {
|
||||
if (name.includes("github")) return "GitHub";
|
||||
if (name.startsWith("fn_task_")) return "Task";
|
||||
@@ -51,90 +78,430 @@ function categorize(name) {
|
||||
return "Other";
|
||||
}
|
||||
|
||||
const CATEGORY_ORDER = ["Task", "GitHub", "Mission", "Agent", "Skills", "Other"];
|
||||
const CATEGORY_LABELS = {
|
||||
Task: "Task tools",
|
||||
GitHub: "GitHub tools",
|
||||
Mission: "Mission tools",
|
||||
Agent: "Agent tools",
|
||||
Skills: "Skills tools",
|
||||
Other: "Other tools",
|
||||
};
|
||||
|
||||
function extractToolNames(source) {
|
||||
const names = [];
|
||||
for (const match of source.matchAll(/name:\s*"(fn_[a-z_]+)"/g)) {
|
||||
names.push(match[1]);
|
||||
function parseStringLiterals(expression) {
|
||||
const literals = [];
|
||||
const regex = /"([^"\\]*(?:\\.[^"\\]*)*)"/g;
|
||||
for (const match of expression.matchAll(regex)) {
|
||||
literals.push(match[1].replace(/\\n/g, " ").replace(/\\"/g, '"'));
|
||||
}
|
||||
if (names.length === 0) {
|
||||
throw new Error(
|
||||
`No fn_* tool registrations found in ${extensionPath} — refusing to wipe the skill.`,
|
||||
);
|
||||
}
|
||||
return names;
|
||||
return literals;
|
||||
}
|
||||
|
||||
function buildBlock(toolNames) {
|
||||
function normalizeWhitespace(text) {
|
||||
return text.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function findMatchingBrace(source, openIndex) {
|
||||
let depth = 0;
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
let inTemplate = false;
|
||||
let inLineComment = false;
|
||||
let inBlockComment = false;
|
||||
let escaped = false;
|
||||
|
||||
for (let i = openIndex; i < source.length; i++) {
|
||||
const char = source[i];
|
||||
const next = source[i + 1];
|
||||
|
||||
if (inLineComment) {
|
||||
if (char === "\n") inLineComment = false;
|
||||
continue;
|
||||
}
|
||||
if (inBlockComment) {
|
||||
if (char === "*" && next === "/") {
|
||||
inBlockComment = false;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingle || inDouble || inTemplate) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (char === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (inSingle && char === "'") inSingle = false;
|
||||
else if (inDouble && char === '"') inDouble = false;
|
||||
else if (inTemplate && char === "`") inTemplate = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "/" && next === "/") {
|
||||
inLineComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (char === "/" && next === "*") {
|
||||
inBlockComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "'") {
|
||||
inSingle = true;
|
||||
continue;
|
||||
}
|
||||
if (char === '"') {
|
||||
inDouble = true;
|
||||
continue;
|
||||
}
|
||||
if (char === "`") {
|
||||
inTemplate = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "{") depth++;
|
||||
if (char === "}") {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Unbalanced braces while parsing extension.ts");
|
||||
}
|
||||
|
||||
function splitTopLevelProperties(objectBody) {
|
||||
const props = [];
|
||||
let start = 0;
|
||||
let depthParen = 0;
|
||||
let depthBrace = 0;
|
||||
let depthBracket = 0;
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
let inTemplate = false;
|
||||
let inLineComment = false;
|
||||
let inBlockComment = false;
|
||||
let escaped = false;
|
||||
|
||||
for (let i = 0; i < objectBody.length; i++) {
|
||||
const ch = objectBody[i];
|
||||
const next = objectBody[i + 1];
|
||||
|
||||
if (inLineComment) {
|
||||
if (ch === "\n") inLineComment = false;
|
||||
continue;
|
||||
}
|
||||
if (inBlockComment) {
|
||||
if (ch === "*" && next === "/") {
|
||||
inBlockComment = false;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingle || inDouble || inTemplate) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (inSingle && ch === "'") inSingle = false;
|
||||
else if (inDouble && ch === '"') inDouble = false;
|
||||
else if (inTemplate && ch === "`") inTemplate = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "/" && next === "/") {
|
||||
inLineComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (ch === "/" && next === "*") {
|
||||
inBlockComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "'") {
|
||||
inSingle = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
inDouble = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === "`") {
|
||||
inTemplate = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "(") depthParen++;
|
||||
else if (ch === ")") depthParen--;
|
||||
else if (ch === "{") depthBrace++;
|
||||
else if (ch === "}") depthBrace--;
|
||||
else if (ch === "[") depthBracket++;
|
||||
else if (ch === "]") depthBracket--;
|
||||
|
||||
if (
|
||||
ch === "," &&
|
||||
depthParen === 0 &&
|
||||
depthBrace === 0 &&
|
||||
depthBracket === 0
|
||||
) {
|
||||
const prop = objectBody.slice(start, i).trim();
|
||||
if (prop) props.push(prop);
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const tail = objectBody.slice(start).trim();
|
||||
if (tail) props.push(tail);
|
||||
return props;
|
||||
}
|
||||
|
||||
function extractDescription(block) {
|
||||
const descMatch = block.match(/description:\s*([\s\S]*?),\s*promptSnippet:/);
|
||||
if (!descMatch) {
|
||||
const inline = block.match(/description:\s*([\s\S]*?),\s*parameters:/);
|
||||
if (!inline) return "";
|
||||
return normalizeWhitespace(parseStringLiterals(inline[1]).join(" "));
|
||||
}
|
||||
return normalizeWhitespace(parseStringLiterals(descMatch[1]).join(" "));
|
||||
}
|
||||
|
||||
function mapType(raw) {
|
||||
const value = raw.trim();
|
||||
if (/^Type\.String\(/.test(value)) return "string";
|
||||
if (/^Type\.Number\(/.test(value)) return "number";
|
||||
if (/^Type\.Boolean\(/.test(value)) return "boolean";
|
||||
if (/^Type\.Array\(/.test(value)) return "array";
|
||||
if (/^Type\.Union\(/.test(value)) return "union";
|
||||
if (/^StringEnum\(/.test(value)) return "string(enum)";
|
||||
if (/^Type\.Null\(/.test(value)) return "null";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function parseParameters(block) {
|
||||
const paramsStart = block.indexOf("parameters:");
|
||||
if (paramsStart === -1) return [];
|
||||
|
||||
const objectStart = block.indexOf("Type.Object(", paramsStart);
|
||||
if (objectStart === -1) return [];
|
||||
|
||||
const braceStart = block.indexOf("{", objectStart);
|
||||
if (braceStart === -1) return [];
|
||||
const braceEnd = findMatchingBrace(block, braceStart);
|
||||
const body = block.slice(braceStart + 1, braceEnd);
|
||||
|
||||
const params = [];
|
||||
for (const prop of splitTopLevelProperties(body)) {
|
||||
const match = prop.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([\s\S]+)$/);
|
||||
if (!match) continue;
|
||||
const [, name, rawValue] = match;
|
||||
const optional = /^Type\.Optional\(/.test(rawValue.trim());
|
||||
const inner = optional
|
||||
? rawValue.trim().replace(/^Type\.Optional\(/, "").replace(/\)\s*$/, "")
|
||||
: rawValue.trim();
|
||||
|
||||
const descMatch = rawValue.match(/description:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/);
|
||||
const description = descMatch
|
||||
? normalizeWhitespace(descMatch[1].replace(/\\n/g, " "))
|
||||
: "";
|
||||
|
||||
params.push({
|
||||
name,
|
||||
type: mapType(inner),
|
||||
required: !optional,
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function extractTools(source) {
|
||||
const tools = [];
|
||||
const registerToken = "pi.registerTool(";
|
||||
let fromIndex = 0;
|
||||
|
||||
while (true) {
|
||||
const start = source.indexOf(registerToken, fromIndex);
|
||||
if (start === -1) break;
|
||||
const braceStart = source.indexOf("{", start);
|
||||
const braceEnd = findMatchingBrace(source, braceStart);
|
||||
const block = source.slice(braceStart, braceEnd + 1);
|
||||
|
||||
const nameMatch = block.match(/name:\s*"(fn_[a-z_]+)"/);
|
||||
if (nameMatch) {
|
||||
const name = nameMatch[1];
|
||||
const labelMatch = block.match(/label:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/);
|
||||
const label = labelMatch ? labelMatch[1] : "";
|
||||
const description = extractDescription(block);
|
||||
const parameters = parseParameters(block);
|
||||
tools.push({ name, label, description, parameters });
|
||||
}
|
||||
|
||||
fromIndex = braceEnd + 1;
|
||||
}
|
||||
|
||||
if (tools.length === 0) {
|
||||
throw new Error(`No fn_* tool registrations found in ${extensionPath}.`);
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
function buildSkillCategoriesBlock(tools) {
|
||||
const grouped = new Map(CATEGORY_ORDER.map((c) => [c, []]));
|
||||
for (const name of toolNames) {
|
||||
grouped.get(categorize(name)).push(name);
|
||||
for (const tool of tools) {
|
||||
grouped.get(categorize(tool.name)).push(tool.name);
|
||||
}
|
||||
const lines = [BEGIN_MARKER];
|
||||
|
||||
const lines = [SKILL_BEGIN];
|
||||
for (const category of CATEGORY_ORDER) {
|
||||
const tools = grouped.get(category);
|
||||
if (tools.length === 0) continue;
|
||||
const formatted = tools.map((t) => `\`${t}\``).join(", ");
|
||||
lines.push(`- **${CATEGORY_LABELS[category]}** — ${formatted}`);
|
||||
const names = grouped.get(category);
|
||||
if (!names.length) continue;
|
||||
lines.push(
|
||||
`- **${CATEGORY_LABELS[category]}** — ${names.map((n) => `\`${n}\``).join(", ")}`,
|
||||
);
|
||||
}
|
||||
lines.push(END_MARKER);
|
||||
lines.push(SKILL_END);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function replaceBlock(skillContent, newBlock) {
|
||||
const beginIdx = skillContent.indexOf(BEGIN_MARKER);
|
||||
const endIdx = skillContent.indexOf(END_MARKER);
|
||||
function buildExtensionToolsBlock(tools) {
|
||||
const grouped = new Map(CATEGORY_ORDER.map((c) => [c, []]));
|
||||
for (const tool of tools) {
|
||||
grouped.get(categorize(tool.name)).push(tool);
|
||||
}
|
||||
|
||||
const lines = [EXT_TOOLS_BEGIN, ""];
|
||||
|
||||
for (const category of CATEGORY_ORDER) {
|
||||
const categoryTools = grouped.get(category);
|
||||
if (!categoryTools.length) continue;
|
||||
|
||||
lines.push(CATEGORY_HEADERS[category]);
|
||||
lines.push("");
|
||||
|
||||
for (const tool of categoryTools) {
|
||||
lines.push(`### ${tool.name}`);
|
||||
lines.push("");
|
||||
lines.push(tool.description || tool.label || "No description provided.");
|
||||
lines.push("");
|
||||
|
||||
if (tool.parameters.length === 0) {
|
||||
lines.push("No parameters.");
|
||||
} else {
|
||||
lines.push("| Parameter | Type | Required | Description |");
|
||||
lines.push("|-----------|------|----------|-------------|");
|
||||
for (const param of tool.parameters) {
|
||||
lines.push(
|
||||
`| \`${param.name}\` | ${param.type} | ${param.required ? "✓" : "—"} | ${param.description || ""} |`,
|
||||
);
|
||||
}
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(EXT_TOOLS_END);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function buildCapabilitiesTableBlock(tools) {
|
||||
const lines = [
|
||||
CAP_TABLE_BEGIN,
|
||||
"| Tool | Purpose |",
|
||||
"|------|---------|",
|
||||
];
|
||||
|
||||
for (const tool of tools) {
|
||||
lines.push(`| \`${tool.name}\` | ${tool.description || tool.label || ""} |`);
|
||||
}
|
||||
|
||||
lines.push(CAP_TABLE_END);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function replaceBlock(content, beginMarker, endMarker, newBlock, filePath) {
|
||||
const beginIdx = content.indexOf(beginMarker);
|
||||
const endIdx = content.indexOf(endMarker);
|
||||
|
||||
if (beginIdx === -1 || endIdx === -1 || endIdx < beginIdx) {
|
||||
throw new Error(
|
||||
`Could not find BEGIN/END tool-categories markers in ${skillPath}. ` +
|
||||
`Add them around the tool-categories bullet list.`,
|
||||
`Could not find markers in ${filePath}: ${beginMarker} ... ${endMarker}`,
|
||||
);
|
||||
}
|
||||
const endLineEnd = endIdx + END_MARKER.length;
|
||||
return (
|
||||
skillContent.slice(0, beginIdx) + newBlock + skillContent.slice(endLineEnd)
|
||||
);
|
||||
|
||||
const endLineEnd = endIdx + endMarker.length;
|
||||
return content.slice(0, beginIdx) + newBlock + content.slice(endLineEnd);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const checkOnly = process.argv.includes("--check");
|
||||
|
||||
const source = readFileSync(extensionPath, "utf-8");
|
||||
const skillContent = readFileSync(skillPath, "utf-8");
|
||||
const extensionSource = readFileSync(extensionPath, "utf-8");
|
||||
const tools = extractTools(extensionSource);
|
||||
|
||||
const toolNames = extractToolNames(source);
|
||||
const newBlock = buildBlock(toolNames);
|
||||
const updated = replaceBlock(skillContent, newBlock);
|
||||
const files = [
|
||||
{
|
||||
path: skillPath,
|
||||
begin: SKILL_BEGIN,
|
||||
end: SKILL_END,
|
||||
block: buildSkillCategoriesBlock(tools),
|
||||
label: "SKILL.md tool-categories",
|
||||
},
|
||||
{
|
||||
path: extensionToolsPath,
|
||||
begin: EXT_TOOLS_BEGIN,
|
||||
end: EXT_TOOLS_END,
|
||||
block: buildExtensionToolsBlock(tools),
|
||||
label: "extension-tools.md",
|
||||
},
|
||||
{
|
||||
path: capabilitiesPath,
|
||||
begin: CAP_TABLE_BEGIN,
|
||||
end: CAP_TABLE_END,
|
||||
block: buildCapabilitiesTableBlock(tools),
|
||||
label: "fusion-capabilities.md tool table",
|
||||
},
|
||||
];
|
||||
|
||||
if (updated === skillContent) {
|
||||
if (!checkOnly) {
|
||||
console.log(
|
||||
`[sync-fusion-skill-tools] SKILL.md tool-categories block is already up to date (${toolNames.length} tools).`,
|
||||
let changedCount = 0;
|
||||
const driftLabels = [];
|
||||
|
||||
for (const file of files) {
|
||||
const current = readFileSync(file.path, "utf-8");
|
||||
const updated = replaceBlock(current, file.begin, file.end, file.block, file.path);
|
||||
|
||||
if (updated !== current) {
|
||||
changedCount++;
|
||||
driftLabels.push(file.label);
|
||||
if (!checkOnly) {
|
||||
writeFileSync(file.path, updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (checkOnly) {
|
||||
if (changedCount > 0) {
|
||||
console.error(
|
||||
`[sync-fusion-skill-tools] Drift detected in ${driftLabels.join(", ")}.\n` +
|
||||
"Run `node scripts/sync-fusion-skill-tools.mjs` to regenerate synced docs.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkOnly) {
|
||||
console.error(
|
||||
`[sync-fusion-skill-tools] SKILL.md tool-categories block is out of date.\n` +
|
||||
`Run \`node scripts/sync-fusion-skill-tools.mjs\` to regenerate it.`,
|
||||
if (changedCount === 0) {
|
||||
console.log(
|
||||
`[sync-fusion-skill-tools] All generated docs are up to date (${tools.length} tools).`,
|
||||
);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
writeFileSync(skillPath, updated);
|
||||
console.log(
|
||||
`[sync-fusion-skill-tools] Rewrote tool-categories block in ${skillPath} (${toolNames.length} tools).`,
|
||||
`[sync-fusion-skill-tools] Updated ${changedCount} file(s) from extension.ts (${tools.length} tools).`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user