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",
|
"verify:workspace": "pnpm lint && pnpm test && pnpm build",
|
||||||
"build:exe": "pnpm build && pnpm --filter @runfusion/fusion build:exe",
|
"build:exe": "pnpm build && pnpm --filter @runfusion/fusion build:exe",
|
||||||
"build:exe:all": "pnpm build && pnpm --filter @runfusion/fusion build:exe:all",
|
"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:serial": "pnpm -r --workspace-concurrency=1 test",
|
||||||
"test:fast": "pnpm -r --workspace-concurrency=4 test",
|
"test:fast": "pnpm -r --workspace-concurrency=4 test",
|
||||||
"test:build": "pnpm --filter @fusion/dashboard test:build",
|
"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.
|
**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:**
|
**Tool categories:**
|
||||||
<!-- BEGIN: tool-categories (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->
|
<!-- 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/extension-tools.md | All pi extension tools with parameters |
|
||||||
| references/best-practices.md | Tips for effective Fusion usage |
|
| references/best-practices.md | Tips for effective Fusion usage |
|
||||||
| references/fusion-capabilities.md | Complete feature catalog |
|
| 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 |
|
| references/skill-patterns.md | Patterns used in this skill's design |
|
||||||
|
|
||||||
</reference_index>
|
</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.
|
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
|
## Task Tools
|
||||||
|
|
||||||
### fn_task_create
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `description` | string | ✓ | What needs to be done — be descriptive |
|
| `description` | string | ✓ | What needs to be done — be descriptive |
|
||||||
| `depends` | string[] | — | Task IDs this depends on (e.g., ["FN-001"]) |
|
| `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') |
|
||||||
Returns: task ID, column, dependencies, path
|
|
||||||
|
|
||||||
### fn_task_update
|
### 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 |
|
| 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 |
|
| `title` | string | — | New task title |
|
||||||
| `description` | string | — | New task description |
|
| `description` | string | — | New task description |
|
||||||
| `depends` | string[] | — | New dependency list — replaces existing |
|
| `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') |
|
||||||
Returns: task ID, list of updated fields
|
|
||||||
|
|
||||||
### fn_task_list
|
### fn_task_list
|
||||||
|
|
||||||
List all tasks grouped by column.
|
List all tasks on the Fusion board, grouped by column.
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `column` | string | — | Filter to specific column |
|
| `column` | string(enum) | — | Filter to a specific column |
|
||||||
| `limit` | number | — | Max tasks per column (default: 10) |
|
| `limit` | number | — | Max tasks to show per column (default: 10) |
|
||||||
|
|
||||||
Returns: formatted task list grouped by column
|
|
||||||
|
|
||||||
### fn_task_show
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `id` | string | ✓ | Task ID (e.g., FN-001) |
|
| `id` | string | ✓ | Task ID (e.g. FN-001) |
|
||||||
|
|
||||||
Returns: task details with steps, prompt preview (500 chars), last 5 log entries
|
|
||||||
|
|
||||||
### fn_task_attach
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `id` | string | ✓ | Task ID |
|
| `id` | string | ✓ | Task ID (e.g. FN-001) |
|
||||||
| `path` | string | ✓ | Path to file to attach |
|
| `path` | string | ✓ | Path to the file to attach |
|
||||||
|
|
||||||
Supported formats: png, jpg, jpeg, gif, webp, txt, log, json, yaml, yml, toml, csv, xml
|
|
||||||
|
|
||||||
### fn_task_pause
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `id` | string | ✓ | Task ID |
|
| `id` | string | ✓ | Task ID (e.g. FN-001) |
|
||||||
|
|
||||||
### fn_task_unpause
|
### fn_task_unpause
|
||||||
|
|
||||||
Resume automation for a paused task.
|
Unpause a task — resumes automated agent and scheduler interaction.
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `id` | string | ✓ | Task ID |
|
| `id` | string | ✓ | Task ID (e.g. FN-001) |
|
||||||
|
|
||||||
### fn_task_retry
|
### 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 |
|
| 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
|
### 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 |
|
| 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
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `id` | string | ✓ | Task ID (must be done or in-review) |
|
| `id` | string | ✓ | Task ID to refine (e.g. FN-001). Must be in 'done' or 'in-review' column. |
|
||||||
| `feedback` | string | ✓ | What needs to be refined (1-2000 chars) |
|
| `feedback` | string | ✓ | Description of what needs to be refined or improved |
|
||||||
|
|
||||||
### fn_task_archive
|
### 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 |
|
| 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
|
### 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 |
|
| 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
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `id` | string | ✓ | Task ID |
|
| `id` | string | ✓ | Task ID to delete (e.g. FN-001) |
|
||||||
|
|
||||||
### fn_task_plan
|
### 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 |
|
| 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
|
## GitHub Tools
|
||||||
|
|
||||||
### fn_task_import_github
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `ownerRepo` | string | ✓ | Repository (e.g., "owner/repo") |
|
| `ownerRepo` | string | ✓ | Repository in owner/repo format (e.g., 'dustinbyrne/fusion') |
|
||||||
| `limit` | number | — | Max issues (default: 30, max: 100) |
|
| `limit` | number | — | Max issues to import (default: 30, max: 100) |
|
||||||
| `labels` | string[] | — | Label names to filter by |
|
| `labels` | array | — | Label names to filter by |
|
||||||
|
|
||||||
### fn_task_import_github_issue
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `owner` | string | ✓ | Repository owner |
|
| `owner` | string | ✓ | Repository owner (e.g., 'dustinbyrne') |
|
||||||
| `repo` | string | ✓ | Repository name |
|
| `repo` | string | ✓ | Repository name (e.g., 'fusion') |
|
||||||
| `issueNumber` | number | ✓ | GitHub issue number |
|
| `issueNumber` | number | ✓ | GitHub issue number to import |
|
||||||
|
|
||||||
### fn_task_browse_github_issues
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `owner` | string | ✓ | Repository owner |
|
| `owner` | string | ✓ | Repository owner (e.g., 'dustinbyrne') |
|
||||||
| `repo` | string | ✓ | Repository name |
|
| `repo` | string | ✓ | Repository name (e.g., 'fusion') |
|
||||||
| `limit` | number | — | Max issues (default: 30, max: 100) |
|
| `limit` | number | — | Max issues to show (default: 30, max: 100) |
|
||||||
| `labels` | string[] | — | Label names to filter by |
|
| `labels` | array | — | Label names to filter by |
|
||||||
|
|
||||||
## Mission Tools
|
## Mission Tools
|
||||||
|
|
||||||
### fn_mission_create
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `title` | string | ✓ | Mission title |
|
| `title` | string | ✓ | Mission title — brief but descriptive |
|
||||||
| `description` | string | — | Detailed objectives and context |
|
| `description` | string | — | Detailed mission objectives and context |
|
||||||
| `autoAdvance` | boolean | — | Auto-activate next slice on completion |
|
| `autoAdvance` | boolean | — | Automatically activate the next pending slice when the current slice completes |
|
||||||
|
|
||||||
### fn_mission_list
|
### fn_mission_list
|
||||||
|
|
||||||
List all missions with current status. No parameters.
|
List all missions with their current status.
|
||||||
|
|
||||||
|
No parameters.
|
||||||
|
|
||||||
### fn_mission_show
|
### fn_mission_show
|
||||||
|
|
||||||
Show mission details with full hierarchy.
|
Show mission details with full hierarchy: milestones → slices → features.
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
@@ -194,63 +190,65 @@ Show mission details with full hierarchy.
|
|||||||
|
|
||||||
### fn_mission_delete
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `id` | string | ✓ | Mission ID |
|
| `id` | string | ✓ | Mission ID to delete (e.g., M-001) |
|
||||||
|
|
||||||
### fn_milestone_add
|
### fn_milestone_add
|
||||||
|
|
||||||
Add a milestone to a mission.
|
Add a milestone to a mission. Milestones represent phases of work.
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `missionId` | string | ✓ | Parent mission ID |
|
| `missionId` | string | ✓ | Parent mission ID (e.g., M-001) |
|
||||||
| `title` | string | ✓ | Milestone title |
|
| `title` | string | ✓ | Milestone title |
|
||||||
| `description` | string | — | Milestone description |
|
| `description` | string | — | Milestone description |
|
||||||
|
|
||||||
### fn_slice_add
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `milestoneId` | string | ✓ | Parent milestone ID |
|
| `milestoneId` | string | ✓ | Parent milestone ID (e.g., MS-001) |
|
||||||
| `title` | string | ✓ | Slice title |
|
| `title` | string | ✓ | Slice title |
|
||||||
| `description` | string | — | Slice description |
|
| `description` | string | — | Slice description |
|
||||||
|
|
||||||
### fn_feature_add
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `sliceId` | string | ✓ | Parent slice ID |
|
| `sliceId` | string | ✓ | Parent slice ID (e.g., SL-001) |
|
||||||
| `title` | string | ✓ | Feature title |
|
| `title` | string | ✓ | Feature title |
|
||||||
| `description` | string | — | Feature description |
|
| `description` | string | — | Feature description |
|
||||||
| `acceptanceCriteria` | string | — | Acceptance criteria |
|
| `acceptanceCriteria` | string | — | Acceptance criteria for completing the feature |
|
||||||
|
|
||||||
### fn_slice_activate
|
### 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 |
|
| 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
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `featureId` | string | ✓ | Feature ID (e.g., F-001) |
|
| `featureId` | string | ✓ | Feature ID to link (e.g., F-001) |
|
||||||
| `taskId` | string | ✓ | Task ID (e.g., FN-001) |
|
| `taskId` | string | ✓ | Task ID to link to (e.g., FN-001) |
|
||||||
|
|
||||||
|
## Agent Tools
|
||||||
|
|
||||||
### fn_agent_stop
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
@@ -258,7 +256,7 @@ Stop (pause) a running agent. Transitions the agent from running/active to pause
|
|||||||
|
|
||||||
### fn_agent_start
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
@@ -268,22 +266,23 @@ Start (resume) a stopped agent. Transitions the agent from paused to active stat
|
|||||||
|
|
||||||
### fn_skills_search
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `query` | string | ✓ | Search query — framework, technology, or capability (e.g., "react", "firebase", "testing", "docker") |
|
| `query` | string | ✓ | Search query — framework name, technology, or capability (e.g., 'react', 'firebase', 'testing', 'docker') |
|
||||||
| `limit` | number | — | Max results (default: 10, max: 50) |
|
| `limit` | number | — | Max results to return (default: 10, max: 50) |
|
||||||
|
|
||||||
### fn_skills_install
|
### 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 |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `source` | string | ✓ | GitHub source in owner/repo format (e.g., "firebase/agent-skills") |
|
| `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. |
|
| `skill` | string | — | Specific skill name to install (e.g., 'firebase-basics'). Omit to install all skills from the source. |
|
||||||
|
|
||||||
|
<!-- END: extension-tools -->
|
||||||
## Dashboard Command
|
## Dashboard Command
|
||||||
|
|
||||||
### /fn
|
### /fn
|
||||||
|
|||||||
@@ -7,40 +7,42 @@ Triage → Todo → In Progress → In Review → Done → Archived
|
|||||||
|
|
||||||
## Pi Extension Tools (Available to Agents)
|
## 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 |
|
| Tool | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `fn_task_create` | Create a new task in triage |
|
| `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 task title, description, or dependencies |
|
| `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 grouped by column |
|
| `fn_task_list` | List all tasks on the Fusion board, grouped by column. |
|
||||||
| `fn_task_show` | Show full task details, steps, and log preview |
|
| `fn_task_show` | Show full details for a task including steps, progress, and log entries. |
|
||||||
| `fn_task_attach` | Attach a file to a task |
|
| `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 automation for a task |
|
| `fn_task_pause` | Pause a task — stops all automated agent and scheduler interaction for this task. |
|
||||||
| `fn_task_unpause` | Resume automation for a task |
|
| `fn_task_unpause` | Unpause a task — resumes automated agent and scheduler interaction. |
|
||||||
| `fn_task_retry` | Retry a failed task (clears error, moves to todo) |
|
| `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 a task (copy to triage) |
|
| `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` | Create refinement task for follow-up work |
|
| `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 |
|
| `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` | Restore an archived task |
|
| `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 |
|
| `fn_task_delete` | Permanently delete a task from the Fusion board. Tasks are deleted immediately and cannot be recovered. |
|
||||||
| `fn_task_import_github` | Batch import GitHub issues as tasks |
|
| `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 single GitHub 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` | Browse GitHub issues before importing |
|
| `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 task via AI-guided planning mode |
|
| `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 |
|
| `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 |
|
| `fn_mission_list` | List all missions with their current status. |
|
||||||
| `fn_mission_show` | Show mission hierarchy |
|
| `fn_mission_show` | Show mission details with full hierarchy: milestones → slices → features. |
|
||||||
| `fn_mission_delete` | Delete a mission |
|
| `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 |
|
| `fn_milestone_add` | Add a milestone to a mission. Milestones represent phases of work. |
|
||||||
| `fn_slice_add` | Add a slice to a milestone |
|
| `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 |
|
| `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 |
|
| `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 task |
|
| `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 (pause) a running agent |
|
| `fn_agent_stop` | Stop a running agent — pauses its execution. Transitions the agent from running/active to paused state. |
|
||||||
| `fn_agent_start` | Start (resume) a stopped agent |
|
| `fn_agent_start` | Start a stopped agent — resumes its execution. Transitions the agent from paused to active state. |
|
||||||
| `fn_skills_search` | Search skills.sh for agent skills |
|
| `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 a skill from skills.sh |
|
| `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)
|
## CLI Commands (fn)
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,243 @@ import { spawnSync } from "node:child_process";
|
|||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const cliRoot = resolve(__dirname, "../..");
|
const cliRoot = resolve(__dirname, "../..");
|
||||||
|
const repoRoot = resolve(cliRoot, "../..");
|
||||||
const skillDir = resolve(cliRoot, "skill/fusion");
|
const skillDir = resolve(cliRoot, "skill/fusion");
|
||||||
const extensionPath = resolve(cliRoot, "src/extension.ts");
|
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: "..." })
|
* Extract all tool names registered via pi.registerTool({ name: "..." })
|
||||||
* from the extension source code.
|
* from the extension source code.
|
||||||
*/
|
*/
|
||||||
function getExtensionToolNames(): string[] {
|
function getExtensionToolNames(): string[] {
|
||||||
const src = readFileSync(extensionPath, "utf-8");
|
return getRegisterToolBlocks()
|
||||||
const matches = [...src.matchAll(/name:\s*"(fn_[a-z_]+)"/g)];
|
.map((entry) => entry.name)
|
||||||
return matches.map((m) => m[1]).sort();
|
.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();
|
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.
|
* Extract tool names listed in SKILL.md under the tool categories.
|
||||||
*/
|
*/
|
||||||
function getSkillMdToolNames(): string[] {
|
function getSkillMdToolNames(): string[] {
|
||||||
const doc = readFileSync(resolve(skillDir, "SKILL.md"), "utf-8");
|
const doc = readFileSync(resolve(skillDir, "SKILL.md"), "utf-8");
|
||||||
const matches = [...doc.matchAll(/`(fn_[a-z_]+)`/g)];
|
const matches = [...doc.matchAll(/`(fn_[a-z_]+)`/g)];
|
||||||
// Deduplicate
|
|
||||||
return [...new Set(matches.map((m) => m[1]))].sort();
|
return [...new Set(matches.map((m) => m[1]))].sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +294,22 @@ function getCapabilitiesToolNames(): string[] {
|
|||||||
return matches.map((m) => m[1]).sort();
|
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[] {
|
function collectMarkdownFiles(dir: string): string[] {
|
||||||
const files: string[] = [];
|
const files: string[] = [];
|
||||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||||
@@ -105,6 +362,17 @@ describe("Skill-Extension Sync", () => {
|
|||||||
expect(documentedTools).toEqual(extensionTools);
|
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", () => {
|
it("SKILL.md tool listing includes all registered tools", () => {
|
||||||
const extensionTools = getExtensionToolNames();
|
const extensionTools = getExtensionToolNames();
|
||||||
const skillTools = getSkillMdToolNames();
|
const skillTools = getSkillMdToolNames();
|
||||||
@@ -123,6 +391,26 @@ describe("Skill-Extension Sync", () => {
|
|||||||
(t) => !capTools.includes(t),
|
(t) => !capTools.includes(t),
|
||||||
);
|
);
|
||||||
expect(missingFromCaps).toEqual([]);
|
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", () => {
|
it("covers the full Fusion skill markdown surface", () => {
|
||||||
@@ -134,6 +422,7 @@ describe("Skill-Extension Sync", () => {
|
|||||||
"SKILL.md",
|
"SKILL.md",
|
||||||
"references/best-practices.md",
|
"references/best-practices.md",
|
||||||
"references/cli-commands.md",
|
"references/cli-commands.md",
|
||||||
|
"references/engine-tools.md",
|
||||||
"references/extension-tools.md",
|
"references/extension-tools.md",
|
||||||
"references/fusion-capabilities.md",
|
"references/fusion-capabilities.md",
|
||||||
"references/skill-patterns.md",
|
"references/skill-patterns.md",
|
||||||
@@ -151,8 +440,6 @@ describe("Skill-Extension Sync", () => {
|
|||||||
toolName.replace(/^fn_/, ""),
|
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([
|
const allowedUnprefixedInternalTools = new Set([
|
||||||
"task_create",
|
"task_create",
|
||||||
"task_update",
|
"task_update",
|
||||||
@@ -195,8 +482,7 @@ describe("Skill-Extension Sync", () => {
|
|||||||
expect(dashboardCli).toContain("/fn");
|
expect(dashboardCli).toContain("/fn");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("SKILL.md tool-categories block matches the sync script output (no drift)", () => {
|
it("SKILL.md and reference generated blocks match sync script output (no drift)", () => {
|
||||||
const repoRoot = resolve(cliRoot, "../..");
|
|
||||||
const script = resolve(repoRoot, "scripts/sync-fusion-skill-tools.mjs");
|
const script = resolve(repoRoot, "scripts/sync-fusion-skill-tools.mjs");
|
||||||
const result = spawnSync("node", [script, "--check"], {
|
const result = spawnSync("node", [script, "--check"], {
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Regenerates the `tool-categories` block in the Fusion skill SKILL.md from
|
* Syncs Fusion skill docs from the canonical pi extension registrations.
|
||||||
* the canonical list of `fn_*` tools registered in `packages/cli/src/extension.ts`.
|
|
||||||
*
|
*
|
||||||
* Source of truth: extension.ts (each tool is registered with `name: "fn_..."`).
|
* Full flow:
|
||||||
* Output: the `<!-- BEGIN: tool-categories ... -->` / `<!-- END: tool-categories -->`
|
* packages/cli/src/extension.ts
|
||||||
* block inside `packages/cli/skill/fusion/SKILL.md`.
|
* → scripts/sync-fusion-skill-tools.mjs
|
||||||
*
|
* → packages/cli/skill/fusion/SKILL.md (tool categories)
|
||||||
* The `.claude/skills/fusion` path is a symlink to `packages/cli/skill/fusion`,
|
* → packages/cli/skill/fusion/references/extension-tools.md (tool sections + params)
|
||||||
* so it picks up the change automatically.
|
* → packages/cli/skill/fusion/references/fusion-capabilities.md (tool table)
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* node scripts/sync-fusion-skill-tools.mjs # rewrite SKILL.md in place
|
* node scripts/sync-fusion-skill-tools.mjs
|
||||||
* node scripts/sync-fusion-skill-tools.mjs --check # exit 1 if drift detected
|
* node scripts/sync-fusion-skill-tools.mjs --check
|
||||||
*
|
|
||||||
* 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`.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync, writeFileSync } from "node:fs";
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
@@ -26,15 +22,46 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|||||||
const repoRoot = resolve(__dirname, "..");
|
const repoRoot = resolve(__dirname, "..");
|
||||||
const extensionPath = resolve(repoRoot, "packages/cli/src/extension.ts");
|
const extensionPath = resolve(repoRoot, "packages/cli/src/extension.ts");
|
||||||
const skillPath = resolve(repoRoot, "packages/cli/skill/fusion/SKILL.md");
|
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) -->";
|
"<!-- 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) {
|
function categorize(name) {
|
||||||
if (name.includes("github")) return "GitHub";
|
if (name.includes("github")) return "GitHub";
|
||||||
if (name.startsWith("fn_task_")) return "Task";
|
if (name.startsWith("fn_task_")) return "Task";
|
||||||
@@ -51,90 +78,430 @@ function categorize(name) {
|
|||||||
return "Other";
|
return "Other";
|
||||||
}
|
}
|
||||||
|
|
||||||
const CATEGORY_ORDER = ["Task", "GitHub", "Mission", "Agent", "Skills", "Other"];
|
function parseStringLiterals(expression) {
|
||||||
const CATEGORY_LABELS = {
|
const literals = [];
|
||||||
Task: "Task tools",
|
const regex = /"([^"\\]*(?:\\.[^"\\]*)*)"/g;
|
||||||
GitHub: "GitHub tools",
|
for (const match of expression.matchAll(regex)) {
|
||||||
Mission: "Mission tools",
|
literals.push(match[1].replace(/\\n/g, " ").replace(/\\"/g, '"'));
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
if (names.length === 0) {
|
return literals;
|
||||||
throw new Error(
|
}
|
||||||
`No fn_* tool registrations found in ${extensionPath} — refusing to wipe the skill.`,
|
|
||||||
|
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 tool of tools) {
|
||||||
|
grouped.get(categorize(tool.name)).push(tool.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [SKILL_BEGIN];
|
||||||
|
for (const category of CATEGORY_ORDER) {
|
||||||
|
const names = grouped.get(category);
|
||||||
|
if (!names.length) continue;
|
||||||
|
lines.push(
|
||||||
|
`- **${CATEGORY_LABELS[category]}** — ${names.map((n) => `\`${n}\``).join(", ")}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return names;
|
lines.push(SKILL_END);
|
||||||
}
|
|
||||||
|
|
||||||
function buildBlock(toolNames) {
|
|
||||||
const grouped = new Map(CATEGORY_ORDER.map((c) => [c, []]));
|
|
||||||
for (const name of toolNames) {
|
|
||||||
grouped.get(categorize(name)).push(name);
|
|
||||||
}
|
|
||||||
const lines = [BEGIN_MARKER];
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
lines.push(END_MARKER);
|
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceBlock(skillContent, newBlock) {
|
function buildExtensionToolsBlock(tools) {
|
||||||
const beginIdx = skillContent.indexOf(BEGIN_MARKER);
|
const grouped = new Map(CATEGORY_ORDER.map((c) => [c, []]));
|
||||||
const endIdx = skillContent.indexOf(END_MARKER);
|
for (const tool of tools) {
|
||||||
if (beginIdx === -1 || endIdx === -1 || endIdx < beginIdx) {
|
grouped.get(categorize(tool.name)).push(tool);
|
||||||
throw new Error(
|
}
|
||||||
`Could not find BEGIN/END tool-categories markers in ${skillPath}. ` +
|
|
||||||
`Add them around the tool-categories bullet list.`,
|
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 || ""} |`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const endLineEnd = endIdx + END_MARKER.length;
|
}
|
||||||
return (
|
lines.push("");
|
||||||
skillContent.slice(0, beginIdx) + newBlock + skillContent.slice(endLineEnd)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 markers in ${filePath}: ${beginMarker} ... ${endMarker}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const endLineEnd = endIdx + endMarker.length;
|
||||||
|
return content.slice(0, beginIdx) + newBlock + content.slice(endLineEnd);
|
||||||
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
const checkOnly = process.argv.includes("--check");
|
const checkOnly = process.argv.includes("--check");
|
||||||
|
|
||||||
const source = readFileSync(extensionPath, "utf-8");
|
const extensionSource = readFileSync(extensionPath, "utf-8");
|
||||||
const skillContent = readFileSync(skillPath, "utf-8");
|
const tools = extractTools(extensionSource);
|
||||||
|
|
||||||
const toolNames = extractToolNames(source);
|
const files = [
|
||||||
const newBlock = buildBlock(toolNames);
|
{
|
||||||
const updated = replaceBlock(skillContent, newBlock);
|
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) {
|
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) {
|
if (!checkOnly) {
|
||||||
console.log(
|
writeFileSync(file.path, updated);
|
||||||
`[sync-fusion-skill-tools] SKILL.md tool-categories block is already up to date (${toolNames.length} tools).`,
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkOnly) {
|
if (changedCount === 0) {
|
||||||
console.error(
|
console.log(
|
||||||
`[sync-fusion-skill-tools] SKILL.md tool-categories block is out of date.\n` +
|
`[sync-fusion-skill-tools] All generated docs are up to date (${tools.length} tools).`,
|
||||||
`Run \`node scripts/sync-fusion-skill-tools.mjs\` to regenerate it.`,
|
|
||||||
);
|
);
|
||||||
process.exit(1);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFileSync(skillPath, updated);
|
|
||||||
console.log(
|
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