FN-5652: add goal retrieval tool for agent execution

Expose goal details to agents during task execution workflows.

- add fn_goal_show tool to the CLI extension with goal lookup, formatted output, and not-found error details
- add focused tests for goal tool registration and execution behavior
- mark fn_goal_show as execution-safe in engine gating classifications and test coverage
- update Fusion skill/reference docs to include the new goal retrieval capability
- add a changeset for @runfusion/fusion patch release

Files changed:
 .changeset/fn-5652-goal-show.md                    |   7 +
 packages/cli/skill/fusion/SKILL.md                 |   2 +-
 .../cli/skill/fusion/references/extension-tools.md |  16 +++
 .../skill/fusion/references/fusion-capabilities.md |   1 +
 .../cli/src/__tests__/extension-goal-tools.test.ts | 148 +++++++++++++++++++++
 packages/cli/src/extension.ts                      |  43 ++++++
 .../src/__tests__/gating-classifications.test.ts   |   7 +
 packages/engine/src/gating-classifications.ts      |   2 +
 8 files changed, 225 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-5652

Fusion-Task-Lineage: 98c2c417-0887-4794-81b1-6b47ab0f708f
This commit is contained in:
gsxdsm
2026-05-29 07:13:23 -07:00
parent 335c49748c
commit b335f3d7c4
8 changed files with 225 additions and 1 deletions

View File

@@ -30,7 +30,7 @@ Mission → Milestone → Slice → Feature → Task
- **Task tools** — `fn_task_create`, `fn_task_update`, `fn_task_list`, `fn_task_show`, `fn_task_attach`, `fn_task_pause`, `fn_task_unpause`, `fn_task_retry`, `fn_task_duplicate`, `fn_task_refine`, `fn_task_archive`, `fn_task_unarchive`, `fn_task_delete`, `fn_task_plan`
- **GitHub tools** — `fn_task_import_github`, `fn_task_import_github_issue`, `fn_task_browse_github_issues`
- **Mission tools** — `fn_mission_create`, `fn_mission_list`, `fn_mission_show`, `fn_mission_delete`, `fn_milestone_add`, `fn_slice_add`, `fn_feature_add`, `fn_slice_activate`, `fn_feature_link_task`, `fn_feature_update`, `fn_milestone_update`
- **Goal tools** — `fn_goal_list`, `fn_goal_create`, `fn_goal_archive`
- **Goal tools** — `fn_goal_list`, `fn_goal_create`, `fn_goal_archive`, `fn_goal_show`
- **Agent tools** — `fn_agent_stop`, `fn_agent_start`, `fn_agent_create`, `fn_agent_delete`, `fn_list_agents`, `fn_delegate_task`, `fn_agent_show`, `fn_agent_org_chart`
- **Skills tools** — `fn_skills_search`, `fn_skills_install`
- **Insight tools** — `fn_insight_list`, `fn_insight_show`, `fn_insight_run_list`, `fn_insight_run_show`

View File

@@ -301,6 +301,14 @@ Archive a goal by ID.
|-----------|------|----------|-------------|
| `id` | string | ✓ | Goal ID (G-…) to archive |
### fn_goal_show
Show full details for a single goal by ID.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | ✓ | Goal ID (G-…) |
## Agent Tools
### fn_agent_stop
@@ -523,6 +531,14 @@ Group kept experiment runs into reviewable branches and finalize the session. Us
| `summary` | string | — | Optional finalize summary |
<!-- END: extension-tools -->
## Goal retrieval contract notes
### Goal-show response contract
- **Success:** returns `details.goal` as the complete goal JSON object (`id`, `title`, optional `description`, `status`, `createdAt`, `updatedAt`) and `content[0].text` with those same human-readable fields.
- **Not found:** returns `isError: true` with `details.code: "GOAL_NOT_FOUND"` and `details.goalId` set to the requested id.
## Dashboard Command
### /fn

View File

@@ -46,6 +46,7 @@ All skill/extension tool invocations in this catalog use the public `fn_*` names
| `fn_goal_list` | List goals by status with active-goal warning details. |
| `fn_goal_create` | Create a new project goal. |
| `fn_goal_archive` | Archive a goal by ID. |
| `fn_goal_show` | Show full details for a single goal by ID. |
| `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. |

View File

@@ -0,0 +1,148 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { mkdtemp, mkdir, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import kbExtension from "../extension.js";
interface RegisteredTool {
name: string;
description: string;
parameters?: {
type: string;
properties?: Record<string, unknown>;
required?: string[];
};
execute: (
toolCallId: string,
params: any,
signal: AbortSignal | undefined,
onUpdate: ((update: any) => void) | undefined,
ctx: any,
) => Promise<any>;
}
function createMockAPI() {
const tools = new Map<string, RegisteredTool>();
const api = {
registerTool(def: RegisteredTool) {
tools.set(def.name, def);
},
registerCommand() {},
registerShortcut() {},
registerFlag() {},
on() {},
tools,
};
return api as any;
}
function makeCtx(cwd: string) {
return { cwd } as any;
}
describe("extension goal retrieval tools", () => {
let tmpDir: string;
let api: ReturnType<typeof createMockAPI>;
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), "kb-goal-tools-"));
await mkdir(join(tmpDir, ".fusion"), { recursive: true });
api = createMockAPI();
kbExtension(api);
});
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true });
});
it("registers fn_goal_show with expected schema", () => {
const tool = api.tools.get("fn_goal_show");
expect(tool).toBeDefined();
expect(tool?.name).toBe("fn_goal_show");
expect(tool?.description).toBe("Show full details for a single goal by ID.");
expect(tool?.parameters).toMatchObject({
type: "object",
required: ["id"],
properties: {
id: {
type: "string",
description: "Goal ID (G-…)",
},
},
});
});
it("returns goal details with stable details.goal payload", async () => {
const createTool = api.tools.get("fn_goal_create");
const tool = api.tools.get("fn_goal_show");
expect(createTool).toBeDefined();
expect(tool).toBeDefined();
const created = await createTool!.execute(
"goal-create-1",
{ title: "Improve reliability", description: "Reduce flaky test retries" },
undefined,
undefined,
makeCtx(tmpDir),
);
const goalId = created.details.goalId as string;
const result = await tool!.execute("goal-show-1", { id: goalId }, undefined, undefined, makeCtx(tmpDir));
expect(result.isError).toBeUndefined();
expect(result.details.goal).toMatchObject({
id: goalId,
title: "Improve reliability",
description: "Reduce flaky test retries",
status: "active",
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
expect(result.content[0].text).toContain(goalId);
expect(result.content[0].text).toContain("Improve reliability");
expect(result.content[0].text).toContain("Status: active");
expect(result.content[0].text).toContain("Created:");
expect(result.content[0].text).toContain("Updated:");
});
it("returns GOAL_NOT_FOUND error for unknown id", async () => {
const tool = api.tools.get("fn_goal_show");
expect(tool).toBeDefined();
const result = await tool!.execute("goal-show-404", { id: "G-404" }, undefined, undefined, makeCtx(tmpDir));
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe("Goal G-404 not found");
expect(result.details).toEqual({ code: "GOAL_NOT_FOUND", goalId: "G-404" });
});
it("supports list to show retrieval with stable json shape", async () => {
const createTool = api.tools.get("fn_goal_create");
const listTool = api.tools.get("fn_goal_list");
const showTool = api.tools.get("fn_goal_show");
expect(createTool).toBeDefined();
expect(listTool).toBeDefined();
expect(showTool).toBeDefined();
await createTool!.execute("goal-create-2", { title: "Ship slice 2" }, undefined, undefined, makeCtx(tmpDir));
const listResult = await listTool!.execute("goal-list-1", { status: "active" }, undefined, undefined, makeCtx(tmpDir));
expect(Array.isArray(listResult.details.goals)).toBe(true);
expect(listResult.details.goals.length).toBeGreaterThan(0);
const listedGoal = listResult.details.goals[0] as { id: string };
const showResult = await showTool!.execute("goal-show-2", { id: listedGoal.id }, undefined, undefined, makeCtx(tmpDir));
expect(showResult.details.goal.id).toBe(listedGoal.id);
expect(showResult.details.goal).toMatchObject({
id: listedGoal.id,
title: expect.any(String),
status: "active",
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
});
});

View File

@@ -2522,6 +2522,49 @@ export default function kbExtension(pi: ExtensionAPI) {
},
});
pi.registerTool({
name: "fn_goal_show",
label: "fn: Show Goal",
description: "Show full details for a single goal by ID.",
promptSnippet: "Show goal details by ID",
promptGuidelines: [
"Use to inspect a specific goal after listing or referencing its ID",
"Cite the goal ID and status when summarizing or planning from this output",
"Read-only retrieval; does not mutate goals",
],
parameters: Type.Object({
id: Type.String({ description: "Goal ID (G-…)" }),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const store = await getStore(ctx.cwd);
const goalStore = store.getGoalStore();
const goal = goalStore.getGoal(params.id);
if (!goal) {
return {
isError: true,
content: [{ type: "text", text: `Goal ${params.id} not found` }],
details: { code: "GOAL_NOT_FOUND", goalId: params.id },
};
}
const lines: string[] = [];
lines.push(`${goal.id}: ${goal.title}`);
lines.push(`Status: ${goal.status}`);
lines.push(`Created: ${goal.createdAt}`);
lines.push(`Updated: ${goal.updatedAt}`);
if (goal.description) {
lines.push(`Description: ${goal.description}`);
}
return {
content: [{ type: "text", text: lines.join("\n") }],
details: { goal },
};
},
});
// ── fn_mission_show ──────────────────────────────────────────────
pi.registerTool({