Files
fusion/packages/engine/src/gating-classifications.ts
gsxdsm fab8a62b55 FN-5977: expose goal retrieval tools across engine agents
Make goal lookup tools available and citable throughout engine-driven agent sessions.

- add shared engine goal list/show tools with concise list snippets, full detail lookup, and retrieval audit coverage
- expose fn_goal_list and fn_goal_show in executor, heartbeat, and triage tool surfaces and classify them as readonly coordination-exempt tools
- extend CLI and engine regression coverage, refresh tool reference docs, and add a published changeset for the CLI package

Files changed:
 .changeset/FN-5977-goal-retrieval-engine-tools.md  |   7 +
 packages/cli/skill/fusion/references/engine-tools.md    |   2 +
 packages/cli/src/__tests__/extension-goal-tools-audit.test.ts   |   8 +-
 packages/cli/src/__tests__/extension-goal-tools.test.ts | 118 +++++++++++-
 packages/cli/src/__tests__/goal-store-resolution.test.ts    |   2 +-
 packages/cli/src/extension.ts                      |  38 +++-
 packages/engine/src/__tests__/agent-tools-goal.test.ts  | 213 +++++++++++++++++++++
 packages/engine/src/__tests__/gating-classifications.test.ts   |   4 +
 packages/engine/src/__tests__/heartbeat-executor.test.ts       |  22 ++-
 packages/engine/src/__tests__/heartbeat-session-prompt.test.ts |   8 +-
 packages/engine/src/agent-heartbeat.ts             |   4 +-
 packages/engine/src/agent-tools.ts                 | 180 ++++++++++++++++-
 packages/engine/src/executor.ts                    |   8 +
 packages/engine/src/gating-classifications.ts      |   2 +
 packages/engine/src/triage.ts                      |  26 ++-
 15 files changed, 600 insertions(+), 42 deletions(-)

Fusion-Task-Id: FN-5977

Fusion-Task-Lineage: 46f7e777-da4d-4e9c-b014-329a5cb1e3da
2026-06-07 18:51:46 -07:00

227 lines
6.8 KiB
TypeScript

// FN-3548 / FN-3724 / FN-3751: keep agent-action-gate and permanent-agent-gating
// classifications sourced from one module to prevent two-path drift (see MEMORY.md drift note).
export const READONLY_BUILTIN_TOOLS: ReadonlySet<string> = new Set(["read", "find", "grep", "ls"]);
export const FILE_WRITE_BUILTIN_TOOLS: ReadonlySet<string> = new Set(["write", "edit"]);
const SHARED_TASK_AGENT_TOOLS = ["fn_task_add_dep", "fn_spawn_agent", "fn_update_agent_config", "fn_agent_create", "fn_agent_delete"] as const;
const PROVISIONING_TOOLS = ["fn_agent_create", "fn_agent_delete"] as const;
const ACTION_GATE_TASK_AGENT_ONLY_TOOLS = [
"fn_task_create",
"fn_delegate_task",
"fn_task_import_github",
"fn_task_import_github_issue",
"fn_update_identity",
] as const;
const ACTION_GATE_SHARED_TASK_AGENT_TOOLS = SHARED_TASK_AGENT_TOOLS.filter(
(tool) => !(PROVISIONING_TOOLS as readonly string[]).includes(tool),
);
const PERMANENT_TASK_AGENT_ONLY_TOOLS = [
"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",
"fn_mission_create",
"fn_mission_delete",
"fn_mission_update",
"fn_mission_backfill_assertions",
"fn_milestone_add",
"fn_slice_add",
"fn_feature_add",
"fn_feature_delete",
"fn_slice_delete",
"fn_milestone_delete",
"fn_slice_activate",
"fn_feature_link_task",
"fn_feature_update",
"fn_milestone_update",
"fn_agent_stop",
"fn_agent_start",
] as const;
export const TASK_AGENT_MUTATION_TOOLS: ReadonlySet<string> = new Set([
...SHARED_TASK_AGENT_TOOLS,
...ACTION_GATE_TASK_AGENT_ONLY_TOOLS,
...PERMANENT_TASK_AGENT_ONLY_TOOLS,
]);
// FN-3953: provisioning tools are gated by dedicated agent_provisioning policy;
// keep them out of action-gate task_agent_mutation to avoid double approval rows.
export const ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS: ReadonlySet<string> = new Set([
...ACTION_GATE_SHARED_TASK_AGENT_TOOLS,
...ACTION_GATE_TASK_AGENT_ONLY_TOOLS,
]);
export const PERMANENT_AGENT_TASK_MUTATION_TOOLS: ReadonlySet<string> = new Set([
...SHARED_TASK_AGENT_TOOLS,
...PERMANENT_TASK_AGENT_ONLY_TOOLS,
]);
export const FILE_WRITE_DELETE_FN_TOOLS: ReadonlySet<string> = new Set(["fn_task_attach"]);
export const NETWORK_API_TOOLS: ReadonlySet<string> = new Set([
"fn_research_run",
"fn_research_cancel",
"fn_research_retry",
"fn_web_fetch", // FN-4603: outbound HTTP fetch should be network-classified.
"worktrunk_install", // FN-4624: binary auto-install downloads from GitHub.
]);
export const ACTION_GATE_NETWORK_API_TOOLS: ReadonlySet<string> = new Set([
"fn_research_run",
"fn_web_fetch", // FN-4603: honor network_api approval policy for web fetches.
"worktrunk_install", // FN-4624: gate binary auto-install under network_api policy.
]);
export const READONLY_FN_TOOLS: ReadonlySet<string> = new Set([
"fn_task_list",
"fn_task_show",
"fn_task_create",
"fn_task_document_write",
"fn_task_document_read",
"fn_delegate_task",
"fn_task_import_github",
"fn_task_import_github_issue",
"fn_research_list",
"fn_research_get",
"fn_insight_list",
"fn_insight_show",
"fn_insight_run_list",
"fn_insight_run_show",
"fn_goal_list",
"fn_goal_show",
"fn_mission_list",
"fn_mission_show",
"fn_list_agents",
"fn_agent_show",
"fn_agent_org_chart",
"fn_skills_search",
"fn_memory_search",
"fn_memory_get",
"fn_task_update",
"fn_task_log",
"fn_task_done",
"fn_heartbeat_done",
"fn_memory_append",
"fn_send_message",
"fn_read_messages",
"fn_post_room_message",
"fn_update_identity",
"fn_reflect_on_performance",
"fn_read_evaluations",
]);
export const COORDINATION_EXEMPT_TOOLS = [
"read",
"find",
"grep",
"ls",
"fn_task_update",
"fn_task_log",
"fn_task_done",
"fn_task_document_write",
"fn_task_document_read",
"fn_memory_search",
"fn_memory_get",
"fn_read_messages",
"fn_heartbeat_done",
"fn_task_create",
"fn_delegate_task",
"fn_goal_list",
"fn_goal_show",
"fn_list_agents",
"fn_agent_show",
"fn_agent_org_chart",
"fn_send_message",
"fn_post_room_message",
"fn_memory_append",
"fn_read_evaluations",
"fn_update_identity",
"fn_reflect_on_performance",
] as const;
export const MUTATING_GIT_SUBCOMMANDS: ReadonlySet<string> = new Set([
"add",
"commit",
"merge",
"rebase",
"cherry-pick",
"am",
"apply",
"stash",
"tag",
"push",
"reset",
"rm",
"mv",
"clean",
]);
export const READONLY_GIT_SUBCOMMANDS: ReadonlySet<string> = new Set(["status", "diff", "log", "show", "rev-parse"]);
export function classifyGitCommand(command: string): { write: boolean; operation: string } | null {
const match = command.match(/(?:^|&&|\|\||;|\||\n)\s*git\s+([^\s]+)/);
if (!match) return null;
const sub = match[1]?.trim() ?? "";
if (!sub) return { write: false, operation: "git" };
if (READONLY_GIT_SUBCOMMANDS.has(sub)) {
if (sub === "rev-parse" && /--show-current\b/.test(command)) {
return { write: false, operation: "git rev-parse --show-current" };
}
return { write: false, operation: `git ${sub}` };
}
if (sub === "branch") {
const mutatingFlags = /\s-d\b|\s-D\b|\s-m\b|\s-M\b|\s-c\b|\s-C\b/.test(command);
if (mutatingFlags) return { write: true, operation: "git branch" };
const tail = command.replace(/^[\s\S]*?\bgit\s+branch\b/, "").trim();
const hasPositionalArg = tail.length > 0 && !tail.startsWith("-");
if (hasPositionalArg) return { write: true, operation: "git branch" };
return { write: false, operation: /--show-current\b/.test(command) ? "git branch --show-current" : "git branch" };
}
if (sub === "switch") {
const write = /\s-c\b/.test(command);
return { write, operation: write ? "git switch -c" : "git switch" };
}
if (sub === "checkout") {
const write = /\s-b\b/.test(command);
return { write, operation: write ? "git checkout -b" : "git checkout" };
}
if (sub === "pull") {
const write = /--rebase\b/.test(command);
return { write, operation: write ? "git pull --rebase" : "git pull" };
}
if (sub === "restore") {
const write = /--staged\b/.test(command);
return { write, operation: write ? "git restore --staged" : "git restore" };
}
if (sub === "remote") {
const write = /\s+add\b|\s+remove\b|\s+rename\b|\s+set-url\b/.test(command);
return { write, operation: /\s-v\b/.test(command) ? "git remote -v" : "git remote" };
}
if (sub === "worktree") {
if (/\s+add\b/.test(command)) return { write: true, operation: "git worktree add" };
if (/\s+remove\b/.test(command)) return { write: true, operation: "git worktree remove" };
return { write: false, operation: "git worktree" };
}
return { write: MUTATING_GIT_SUBCOMMANDS.has(sub), operation: `git ${sub}` };
}
export function isGitWriteCommand(command: string): boolean {
return classifyGitCommand(command)?.write ?? false;
}