fix(sf): remove workflow tool aliases
This commit is contained in:
parent
1be11744ee
commit
a3ef4bdf3f
11 changed files with 2870 additions and 2483 deletions
|
|
@ -344,7 +344,7 @@ Doctor rebuilds `STATE.md` from plan and roadmap files on disk and fixes detecte
|
|||
|
||||
### "SF database is not available"
|
||||
|
||||
**Symptoms:** `sf_decision_save` (or its alias `sf_save_decision`), `sf_requirement_update` (or `sf_update_requirement`), or `sf_summary_save` (or `sf_save_summary`) fail with this error.
|
||||
**Symptoms:** `sf_decision_save`, `sf_requirement_update`, or `sf_summary_save` fail with this error.
|
||||
|
||||
**Cause:** The SQLite database wasn't initialized. This happens in manual `/sf` sessions (non-auto mode) on versions before v2.29.
|
||||
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ Doctor 会从磁盘上的 plan 和 roadmap 文件重建 `STATE.md`,并修复
|
|||
|
||||
### “SF database is not available”
|
||||
|
||||
**症状:** `sf_decision_save`(及其别名 `sf_save_decision`)、`sf_requirement_update`(及其别名 `sf_update_requirement`)或 `sf_summary_save`(及其别名 `sf_save_summary`)报这个错误。
|
||||
**症状:** `sf_decision_save`、`sf_requirement_update` 或 `sf_summary_save` 报这个错误。
|
||||
|
||||
**原因:** SQLite 数据库未初始化。这个问题会出现在 v2.29 之前的手动 `/sf` 会话(非自动模式)中。
|
||||
|
||||
|
|
|
|||
|
|
@ -80,27 +80,19 @@ Add to `.cursor/mcp.json`:
|
|||
The workflow MCP surface includes:
|
||||
|
||||
- `sf_decision_save`
|
||||
- `sf_save_decision`
|
||||
- `sf_requirement_update`
|
||||
- `sf_update_requirement`
|
||||
- `sf_requirement_save`
|
||||
- `sf_save_requirement`
|
||||
- `sf_milestone_generate_id`
|
||||
- `sf_plan_milestone`
|
||||
- `sf_plan_slice`
|
||||
- `sf_plan_task`
|
||||
- `sf_task_plan`
|
||||
- `sf_replan_slice`
|
||||
- `sf_slice_replan`
|
||||
- `sf_task_complete`
|
||||
- `sf_slice_complete`
|
||||
- `sf_skip_slice`
|
||||
- `sf_validate_milestone`
|
||||
- `sf_milestone_validate`
|
||||
- `sf_complete_milestone`
|
||||
- `sf_milestone_complete`
|
||||
- `sf_reassess_roadmap`
|
||||
- `sf_roadmap_reassess`
|
||||
- `sf_save_gate_result`
|
||||
- `sf_summary_save`
|
||||
- `sf_milestone_status`
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -26,31 +26,8 @@ import {
|
|||
import { logError } from "../workflow-logger.js";
|
||||
import { ensureDbOpen } from "./dynamic-tools.js";
|
||||
|
||||
/**
|
||||
* Register an alias tool that shares the same execute function as its canonical counterpart.
|
||||
* The alias description and promptGuidelines direct the LLM to prefer the canonical name.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- toolDef shape matches ToolDefinition but typing it fully requires generics
|
||||
function registerAlias(
|
||||
pi: ExtensionAPI,
|
||||
toolDef: any,
|
||||
aliasName: string,
|
||||
canonicalName: string,
|
||||
): void {
|
||||
pi.registerTool({
|
||||
...toolDef,
|
||||
name: aliasName,
|
||||
description:
|
||||
toolDef.description +
|
||||
` (alias for ${canonicalName} — prefer the canonical name)`,
|
||||
promptGuidelines: [
|
||||
`Alias for ${canonicalName} — prefer the canonical name.`,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function registerDbTools(pi: ExtensionAPI): void {
|
||||
// ─── sf_decision_save (formerly sf_save_decision) ─────────────────────
|
||||
// ─── sf_decision_save ─────────────────────────────────────────────────
|
||||
|
||||
const decisionSaveExecute = async (
|
||||
_toolCallId: string,
|
||||
|
|
@ -175,9 +152,8 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
};
|
||||
|
||||
pi.registerTool(decisionSaveTool);
|
||||
registerAlias(pi, decisionSaveTool, "sf_save_decision", "sf_decision_save");
|
||||
|
||||
// ─── sf_requirement_update (formerly sf_update_requirement) ───────────
|
||||
// ─── sf_requirement_update ────────────────────────────────────────────
|
||||
|
||||
const requirementUpdateExecute = async (
|
||||
_toolCallId: string,
|
||||
|
|
@ -302,12 +278,6 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
};
|
||||
|
||||
pi.registerTool(requirementUpdateTool);
|
||||
registerAlias(
|
||||
pi,
|
||||
requirementUpdateTool,
|
||||
"sf_update_requirement",
|
||||
"sf_requirement_update",
|
||||
);
|
||||
|
||||
// ─── sf_requirement_save ─────────────────────────────────────────────
|
||||
|
||||
|
|
@ -434,14 +404,8 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
};
|
||||
|
||||
pi.registerTool(requirementSaveTool);
|
||||
registerAlias(
|
||||
pi,
|
||||
requirementSaveTool,
|
||||
"sf_save_requirement",
|
||||
"sf_requirement_save",
|
||||
);
|
||||
|
||||
// ─── sf_summary_save (formerly sf_save_summary) ──────────────────────
|
||||
// ─── sf_summary_save ──────────────────────────────────────────────────
|
||||
|
||||
const summarySaveExecute = async (
|
||||
_toolCallId: string,
|
||||
|
|
@ -510,7 +474,6 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
};
|
||||
|
||||
pi.registerTool(summarySaveTool);
|
||||
registerAlias(pi, summarySaveTool, "sf_save_summary", "sf_summary_save");
|
||||
|
||||
// ─── sf_milestone_generate_id ────────────────────────────────────────
|
||||
|
||||
|
|
@ -817,7 +780,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
|
||||
pi.registerTool(selfReportTool);
|
||||
|
||||
// ─── sf_plan_milestone (sf_milestone_plan alias) ─────────────────────
|
||||
// ─── sf_plan_milestone ────────────────────────────────────────────────
|
||||
|
||||
const planMilestoneExecute = async (
|
||||
_toolCallId: string,
|
||||
|
|
@ -840,7 +803,6 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
"Use sf_plan_milestone for milestone planning instead of writing ROADMAP.md directly.",
|
||||
"Keep parameters flat and provide the full milestone planning payload. Use either explicit slices or templateId-based scaffolding for common feat/fix/refactor patterns.",
|
||||
"The tool validates input, writes milestone and slice planning data transactionally, renders ROADMAP.md from DB, and clears both state and parse caches after success.",
|
||||
"Use the canonical name sf_plan_milestone; sf_milestone_plan is only an alias.",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
// ── Core identification + content (required) ──────────────────────
|
||||
|
|
@ -1070,14 +1032,8 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
};
|
||||
|
||||
pi.registerTool(planMilestoneTool);
|
||||
registerAlias(
|
||||
pi,
|
||||
planMilestoneTool,
|
||||
"sf_milestone_plan",
|
||||
"sf_plan_milestone",
|
||||
);
|
||||
|
||||
// ─── sf_plan_slice (sf_slice_plan alias) ─────────────────────────────
|
||||
// ─── sf_plan_slice ────────────────────────────────────────────────────
|
||||
|
||||
const planSliceExecute = async (
|
||||
_toolCallId: string,
|
||||
|
|
@ -1100,7 +1056,6 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
"Use sf_plan_slice for slice planning instead of writing S##-PLAN.md or task PLAN files directly.",
|
||||
"Keep parameters flat and provide the full slice planning payload, including tasks.",
|
||||
"The tool validates input, requires an existing parent slice, writes slice/task planning data, renders PLAN.md and task plan files from DB, and clears both state and parse caches after success.",
|
||||
"Use the canonical name sf_plan_slice; sf_slice_plan is only an alias.",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
// ── Core identification + content (required) ──────────────────────
|
||||
|
|
@ -1231,9 +1186,8 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
};
|
||||
|
||||
pi.registerTool(planSliceTool);
|
||||
registerAlias(pi, planSliceTool, "sf_slice_plan", "sf_plan_slice");
|
||||
|
||||
// ─── sf_plan_task (sf_task_plan alias) ───────────────────────────────
|
||||
// ─── sf_plan_task ─────────────────────────────────────────────────────
|
||||
|
||||
const planTaskExecute = async (
|
||||
_toolCallId: string,
|
||||
|
|
@ -1309,7 +1263,6 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
"Use sf_plan_task for task planning instead of writing tasks/T##-PLAN.md directly.",
|
||||
"Keep parameters flat and provide the full task planning payload.",
|
||||
"The tool validates input, requires an existing parent slice, writes task planning data, renders the task PLAN file from DB, and clears both state and parse caches after success.",
|
||||
"Use the canonical name sf_plan_task; sf_task_plan is only an alias.",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
|
||||
|
|
@ -1336,7 +1289,6 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
};
|
||||
|
||||
pi.registerTool(planTaskTool);
|
||||
registerAlias(pi, planTaskTool, "sf_task_plan", "sf_plan_task");
|
||||
|
||||
// ─── sf_task_complete ─────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -1824,14 +1776,8 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
};
|
||||
|
||||
pi.registerTool(milestoneCompleteTool);
|
||||
registerAlias(
|
||||
pi,
|
||||
milestoneCompleteTool,
|
||||
"sf_milestone_complete",
|
||||
"sf_complete_milestone",
|
||||
);
|
||||
|
||||
// ─── sf_validate_milestone (sf_milestone_validate alias) ─────────────
|
||||
// ─── sf_validate_milestone ────────────────────────────────────────────
|
||||
|
||||
const milestoneValidateExecute = async (
|
||||
_toolCallId: string,
|
||||
|
|
@ -1898,14 +1844,8 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
};
|
||||
|
||||
pi.registerTool(milestoneValidateTool);
|
||||
registerAlias(
|
||||
pi,
|
||||
milestoneValidateTool,
|
||||
"sf_milestone_validate",
|
||||
"sf_validate_milestone",
|
||||
);
|
||||
|
||||
// ─── sf_replan_slice (sf_slice_replan alias) ─────────────────────────
|
||||
// ─── sf_replan_slice ──────────────────────────────────────────────────
|
||||
|
||||
const replanSliceExecute = async (
|
||||
_toolCallId: string,
|
||||
|
|
@ -1927,7 +1867,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
promptSnippet:
|
||||
"Replan a SF slice with structural enforcement of completed tasks",
|
||||
promptGuidelines: [
|
||||
"Use sf_replan_slice (canonical) or sf_slice_replan (alias) when a blocker is discovered and the slice plan needs rewriting.",
|
||||
"Use sf_replan_slice when a blocker is discovered and the slice plan needs rewriting.",
|
||||
"The tool structurally enforces that completed tasks cannot be updated or removed — violations return specific error payloads naming the blocked task ID.",
|
||||
"Parameters: milestoneId, sliceId, blockerTaskId, blockerDescription, whatChanged, optional slice-level planning/ceremony updates, updatedTasks (array), removedTaskIds (array).",
|
||||
"updatedTasks items: taskId, title, description, estimate, files, verify, inputs, expectedOutput.",
|
||||
|
|
@ -2054,9 +1994,8 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
};
|
||||
|
||||
pi.registerTool(replanSliceTool);
|
||||
registerAlias(pi, replanSliceTool, "sf_slice_replan", "sf_replan_slice");
|
||||
|
||||
// ─── sf_reassess_roadmap (sf_roadmap_reassess alias) ─────────────────
|
||||
// ─── sf_reassess_roadmap ──────────────────────────────────────────────
|
||||
|
||||
const reassessRoadmapExecute = async (
|
||||
_toolCallId: string,
|
||||
|
|
@ -2078,7 +2017,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
promptSnippet:
|
||||
"Reassess a SF roadmap with structural enforcement of completed slices",
|
||||
promptGuidelines: [
|
||||
"Use sf_reassess_roadmap (canonical) or sf_roadmap_reassess (alias) after a slice completes to reassess the roadmap.",
|
||||
"Use sf_reassess_roadmap after a slice completes to reassess the roadmap.",
|
||||
"The tool structurally enforces that completed slices cannot be modified or removed — violations return specific error payloads naming the blocked slice ID.",
|
||||
"Parameters: milestoneId, completedSliceId, verdict, assessment, sliceChanges (object with modified, added, removed arrays).",
|
||||
"sliceChanges.modified items: sliceId, title, risk (optional), depends (optional), demo (optional).",
|
||||
|
|
@ -2138,12 +2077,6 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
};
|
||||
|
||||
pi.registerTool(reassessRoadmapTool);
|
||||
registerAlias(
|
||||
pi,
|
||||
reassessRoadmapTool,
|
||||
"sf_roadmap_reassess",
|
||||
"sf_reassess_roadmap",
|
||||
);
|
||||
|
||||
// ─── sf_save_gate_result ──────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -36,30 +36,22 @@ export const CACHE_MAX = 50;
|
|||
*
|
||||
* Included tools and why:
|
||||
* - sf_summary_save: writes CONTEXT.md artifacts (all discuss prompts)
|
||||
* - sf_save_summary: alias for above
|
||||
* - sf_decision_save: records decisions (discuss.md output phase)
|
||||
* - sf_save_decision: alias for above
|
||||
* - sf_plan_milestone: writes roadmap (discuss.md single/multi milestone)
|
||||
* - sf_milestone_plan: alias for above
|
||||
* - sf_milestone_generate_id: generates milestone IDs (discuss.md multi-milestone)
|
||||
* - sf_requirement_update: updates requirements during discuss
|
||||
* - sf_update_requirement: alias for above
|
||||
*/
|
||||
export const DISCUSS_TOOLS_ALLOWLIST: readonly string[] = [
|
||||
// Context / summary writing
|
||||
"sf_summary_save",
|
||||
"sf_save_summary",
|
||||
// Decision recording
|
||||
"sf_decision_save",
|
||||
"sf_save_decision",
|
||||
// Milestone planning (needed for discuss.md output phase)
|
||||
"sf_plan_milestone",
|
||||
"sf_milestone_plan",
|
||||
// Milestone ID generation (multi-milestone flow)
|
||||
"sf_milestone_generate_id",
|
||||
// Requirement updates
|
||||
"sf_requirement_update",
|
||||
"sf_update_requirement",
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -76,7 +68,6 @@ export const DISCUSS_TOOLS_ALLOWLIST: readonly string[] = [
|
|||
*/
|
||||
export const RESEARCH_TOOLS_ALLOWLIST: readonly string[] = [
|
||||
"sf_summary_save",
|
||||
"sf_save_summary",
|
||||
"sf_self_report",
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -35,19 +35,13 @@ const guidedFlowPath = join(__dirname, "..", "guided-flow.ts");
|
|||
/** Tools that are only needed during planning, execution, or completion phases */
|
||||
const HEAVY_TOOLS = [
|
||||
"sf_plan_slice",
|
||||
"sf_slice_plan",
|
||||
"sf_plan_task",
|
||||
"sf_task_plan",
|
||||
"sf_task_complete",
|
||||
"sf_slice_complete",
|
||||
"sf_complete_milestone",
|
||||
"sf_milestone_complete",
|
||||
"sf_validate_milestone",
|
||||
"sf_milestone_validate",
|
||||
"sf_replan_slice",
|
||||
"sf_slice_replan",
|
||||
"sf_reassess_roadmap",
|
||||
"sf_roadmap_reassess",
|
||||
"sf_save_gate_result",
|
||||
];
|
||||
|
||||
|
|
@ -90,10 +84,10 @@ describe("discuss tool scoping (#2949)", () => {
|
|||
|
||||
test("DISCUSS_TOOLS_ALLOWLIST is significantly smaller than full tool set", () => {
|
||||
// Full set is 27 DB tools + dynamic + journal = 33+
|
||||
// Discuss set should be roughly 10 SF tools (5 canonical + 5 aliases)
|
||||
// Discuss set should be the small canonical subset referenced by discuss prompts.
|
||||
assert.ok(
|
||||
DISCUSS_TOOLS_ALLOWLIST.length <= 12,
|
||||
`allowlist should have at most 12 SF tools, got ${DISCUSS_TOOLS_ALLOWLIST.length}`,
|
||||
DISCUSS_TOOLS_ALLOWLIST.length <= 6,
|
||||
`allowlist should have at most 6 SF tools, got ${DISCUSS_TOOLS_ALLOWLIST.length}`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -137,7 +131,6 @@ describe("research tool scoping", () => {
|
|||
test("RESEARCH_TOOLS_ALLOWLIST permits only summary save and self-report SF tools", () => {
|
||||
assert.deepEqual(RESEARCH_TOOLS_ALLOWLIST, [
|
||||
"sf_summary_save",
|
||||
"sf_save_summary",
|
||||
"sf_self_report",
|
||||
]);
|
||||
for (const planningTool of [
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
// tool-naming — Verifies canonical + supported alias tool registration for SF DB tools.
|
||||
// tool-naming — Verifies canonical-only tool registration for SF DB tools.
|
||||
//
|
||||
// DB tools with supported aliases must register under both names.
|
||||
// Completion tools are canonical-only; do not reintroduce legacy aliases.
|
||||
// SF workflow tools have one public name per operation. Do not reintroduce
|
||||
// compatibility aliases; they make prompts, MCP, and UI cards drift.
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, test } from "vitest";
|
||||
import { registerDbTools } from "../bootstrap/db-tools.ts";
|
||||
|
||||
// ─── Mock PI ──────────────────────────────────────────────────────────────────
|
||||
import { test } from "vitest";
|
||||
|
||||
test("tool naming.test", () => {
|
||||
|
||||
function makeMockPi() {
|
||||
const tools: any[] = [];
|
||||
|
|
@ -19,212 +17,154 @@ function makeMockPi() {
|
|||
} as any;
|
||||
}
|
||||
|
||||
// ─── Rename map ───────────────────────────────────────────────────────────────
|
||||
// ─── Canonical surface ───────────────────────────────────────────────────────
|
||||
|
||||
const RENAME_MAP: Array<{ canonical: string; alias: string }> = [
|
||||
{ canonical: "sf_decision_save", alias: "sf_save_decision" },
|
||||
{ canonical: "sf_requirement_update", alias: "sf_update_requirement" },
|
||||
{ canonical: "sf_requirement_save", alias: "sf_save_requirement" },
|
||||
{ canonical: "sf_summary_save", alias: "sf_save_summary" },
|
||||
{ canonical: "sf_plan_milestone", alias: "sf_milestone_plan" },
|
||||
{ canonical: "sf_plan_slice", alias: "sf_slice_plan" },
|
||||
{ canonical: "sf_plan_task", alias: "sf_task_plan" },
|
||||
{ canonical: "sf_replan_slice", alias: "sf_slice_replan" },
|
||||
{ canonical: "sf_reassess_roadmap", alias: "sf_roadmap_reassess" },
|
||||
{ canonical: "sf_complete_milestone", alias: "sf_milestone_complete" },
|
||||
{ canonical: "sf_validate_milestone", alias: "sf_milestone_validate" },
|
||||
];
|
||||
|
||||
const EXTRA_DB_TOOLS = [
|
||||
"sf_self_report",
|
||||
"sf_skip_slice",
|
||||
"sf_save_gate_result",
|
||||
const CANONICAL_DB_TOOLS = [
|
||||
"sf_decision_save",
|
||||
"sf_requirement_update",
|
||||
"sf_requirement_save",
|
||||
"sf_summary_save",
|
||||
"sf_milestone_generate_id",
|
||||
"sf_self_report",
|
||||
"sf_plan_milestone",
|
||||
"sf_plan_slice",
|
||||
"sf_plan_task",
|
||||
"sf_task_complete",
|
||||
"sf_slice_complete",
|
||||
"sf_skip_slice",
|
||||
"sf_complete_milestone",
|
||||
"sf_validate_milestone",
|
||||
"sf_replan_slice",
|
||||
"sf_reassess_roadmap",
|
||||
"sf_save_gate_result",
|
||||
] as const;
|
||||
|
||||
const REMOVED_TOOL_ALIASES = [
|
||||
"sf_save_decision",
|
||||
"sf_update_requirement",
|
||||
"sf_save_requirement",
|
||||
"sf_save_summary",
|
||||
"sf_generate_milestone_id",
|
||||
"sf_milestone_plan",
|
||||
"sf_slice_plan",
|
||||
"sf_task_plan",
|
||||
"sf_complete_task",
|
||||
"sf_complete_slice",
|
||||
"sf_generate_milestone_id",
|
||||
"sf_milestone_complete",
|
||||
"sf_milestone_validate",
|
||||
"sf_slice_replan",
|
||||
"sf_roadmap_reassess",
|
||||
] as const;
|
||||
|
||||
// ─── Registration count ──────────────────────────────────────────────────────
|
||||
describe("SF workflow tool naming", () => {
|
||||
test("registerDbTools_registers_only_the_canonical_db_tool_surface", () => {
|
||||
const pi = makeMockPi();
|
||||
registerDbTools(pi);
|
||||
|
||||
console.log("\n── Tool naming: registration count ──");
|
||||
|
||||
const pi = makeMockPi();
|
||||
registerDbTools(pi);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
pi.tools.length,
|
||||
RENAME_MAP.length * 2 + EXTRA_DB_TOOLS.length,
|
||||
"Should register all canonical tools, aliases, and non-aliased DB helpers",
|
||||
);
|
||||
|
||||
// ─── Both names exist for each pair ──────────────────────────────────────────
|
||||
|
||||
console.log("\n── Tool naming: canonical and alias names exist ──");
|
||||
|
||||
for (const { canonical, alias } of RENAME_MAP) {
|
||||
const canonicalTool = pi.tools.find((t: any) => t.name === canonical);
|
||||
const aliasTool = pi.tools.find((t: any) => t.name === alias);
|
||||
|
||||
assert.ok(
|
||||
canonicalTool !== undefined,
|
||||
`Canonical tool "${canonical}" should be registered`,
|
||||
);
|
||||
assert.ok(
|
||||
aliasTool !== undefined,
|
||||
`Alias tool "${alias}" should be registered`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const name of EXTRA_DB_TOOLS) {
|
||||
assert.ok(
|
||||
pi.tools.some((t: any) => t.name === name),
|
||||
`Extra DB tool "${name}" should be registered`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const name of REMOVED_TOOL_ALIASES) {
|
||||
assert.ok(
|
||||
!pi.tools.some((t: any) => t.name === name),
|
||||
`Removed tool alias "${name}" should not be registered`,
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Execute function identity ───────────────────────────────────────────────
|
||||
|
||||
console.log("\n── Tool naming: execute function identity (===) ──");
|
||||
|
||||
for (const { canonical, alias } of RENAME_MAP) {
|
||||
const canonicalTool = pi.tools.find((t: any) => t.name === canonical);
|
||||
const aliasTool = pi.tools.find((t: any) => t.name === alias);
|
||||
|
||||
if (canonicalTool && aliasTool) {
|
||||
assert.ok(
|
||||
canonicalTool.execute === aliasTool.execute,
|
||||
`"${canonical}" and "${alias}" should share the same execute function reference`,
|
||||
assert.deepStrictEqual(
|
||||
pi.tools.length,
|
||||
CANONICAL_DB_TOOLS.length,
|
||||
"Should register exactly the canonical DB tool surface",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Alias descriptions include "(alias for ...)" ───────────────────────────
|
||||
for (const name of CANONICAL_DB_TOOLS) {
|
||||
assert.ok(
|
||||
pi.tools.some((t: any) => t.name === name),
|
||||
`Canonical DB tool "${name}" should be registered`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log("\n── Tool naming: alias descriptions ──");
|
||||
for (const name of REMOVED_TOOL_ALIASES) {
|
||||
assert.ok(
|
||||
!pi.tools.some((t: any) => t.name === name),
|
||||
`Removed tool alias "${name}" should not be registered`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
for (const { canonical, alias } of RENAME_MAP) {
|
||||
const aliasTool = pi.tools.find((t: any) => t.name === alias);
|
||||
test("canonical_tools_do_not_advertise_alias_names", () => {
|
||||
const pi = makeMockPi();
|
||||
registerDbTools(pi);
|
||||
|
||||
if (aliasTool) {
|
||||
assert.ok(
|
||||
aliasTool.description.includes(`alias for ${canonical}`),
|
||||
`Alias "${alias}" description should include "alias for ${canonical}"`,
|
||||
for (const canonical of CANONICAL_DB_TOOLS) {
|
||||
const canonicalTool = pi.tools.find((t: any) => t.name === canonical);
|
||||
|
||||
if (canonicalTool?.promptGuidelines) {
|
||||
const guidelinesText = canonicalTool.promptGuidelines.join(" ");
|
||||
assert.ok(
|
||||
guidelinesText.includes(canonical),
|
||||
`Canonical tool "${canonical}" promptGuidelines should reference its own name`,
|
||||
);
|
||||
assert.ok(
|
||||
!/\balias\b/i.test(guidelinesText),
|
||||
`Canonical tool "${canonical}" promptGuidelines should not mention aliases`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("custom_tool_cards_render_canonical_sf_prefixed_names", () => {
|
||||
const pi = makeMockPi();
|
||||
registerDbTools(pi);
|
||||
|
||||
const fakeTheme = {
|
||||
bold: (text: string) => text,
|
||||
fg: (_name: string, text: string) => text,
|
||||
};
|
||||
for (const tool of pi.tools.filter(
|
||||
(t: any) => typeof t.renderCall === "function",
|
||||
)) {
|
||||
const callComponent = tool.renderCall({}, fakeTheme);
|
||||
assert.match(
|
||||
String(callComponent.text),
|
||||
/^sf_[a-z_]+/,
|
||||
`Custom renderer for "${tool.name}" should display a canonical-looking sf_* tool name`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("sf_plan_milestone_renderer_summarizes_work", () => {
|
||||
const pi = makeMockPi();
|
||||
registerDbTools(pi);
|
||||
|
||||
const planMilestoneTool = pi.tools.find(
|
||||
(t: any) => t.name === "sf_plan_milestone",
|
||||
);
|
||||
}
|
||||
}
|
||||
assert.equal(typeof planMilestoneTool?.renderCall, "function");
|
||||
assert.equal(typeof planMilestoneTool?.renderResult, "function");
|
||||
|
||||
// ─── Canonical tools have proper promptGuidelines ────────────────────────────
|
||||
|
||||
console.log(
|
||||
"\n── Tool naming: canonical promptGuidelines use canonical name ──",
|
||||
);
|
||||
|
||||
for (const { canonical } of RENAME_MAP) {
|
||||
const canonicalTool = pi.tools.find((t: any) => t.name === canonical);
|
||||
|
||||
if (canonicalTool) {
|
||||
const guidelinesText = canonicalTool.promptGuidelines.join(" ");
|
||||
assert.ok(
|
||||
guidelinesText.includes(canonical),
|
||||
`Canonical tool "${canonical}" promptGuidelines should reference its own name`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Alias promptGuidelines direct to canonical ──────────────────────────────
|
||||
|
||||
console.log(
|
||||
"\n── Tool naming: alias promptGuidelines redirect to canonical ──",
|
||||
);
|
||||
|
||||
for (const { canonical, alias } of RENAME_MAP) {
|
||||
const aliasTool = pi.tools.find((t: any) => t.name === alias);
|
||||
|
||||
if (aliasTool) {
|
||||
const guidelinesText = aliasTool.promptGuidelines.join(" ");
|
||||
assert.ok(
|
||||
guidelinesText.includes(`Alias for ${canonical}`),
|
||||
`Alias "${alias}" promptGuidelines should say "Alias for ${canonical}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// guard: Custom tool cards must not invent third spellings such as
|
||||
// "milestone_generate_id"; render the visible call name with the sf_ prefix.
|
||||
console.log("\n── Tool naming: custom renderers show sf_* tool names ──");
|
||||
|
||||
{
|
||||
const fakeTheme = {
|
||||
bold: (text: string) => text,
|
||||
fg: (_name: string, text: string) => text,
|
||||
};
|
||||
for (const tool of pi.tools.filter((t: any) => typeof t.renderCall === "function")) {
|
||||
const callComponent = tool.renderCall({}, fakeTheme);
|
||||
assert.match(
|
||||
String(callComponent.text),
|
||||
/^sf_[a-z_]+/,
|
||||
`Custom renderer for "${tool.name}" should display a canonical-looking sf_* tool name`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── High-signal tool rendering ──────────────────────────────────────────────
|
||||
|
||||
console.log("\n── Tool naming: milestone planning renderer summarizes work ──");
|
||||
|
||||
{
|
||||
const planMilestoneTool = pi.tools.find(
|
||||
(t: any) => t.name === "sf_plan_milestone",
|
||||
);
|
||||
assert.equal(typeof planMilestoneTool?.renderCall, "function");
|
||||
assert.equal(typeof planMilestoneTool?.renderResult, "function");
|
||||
|
||||
const fakeTheme = {
|
||||
bold: (text: string) => text,
|
||||
fg: (_name: string, text: string) => text,
|
||||
};
|
||||
const callComponent = planMilestoneTool.renderCall(
|
||||
{
|
||||
milestoneId: "M008",
|
||||
title: "Workflow polish",
|
||||
slices: [{ sliceId: "S01", title: "Improve tool cards" }],
|
||||
},
|
||||
fakeTheme,
|
||||
);
|
||||
assert.match(callComponent.text, /M008: Workflow polish/);
|
||||
assert.match(callComponent.text, /1 slice/);
|
||||
|
||||
const resultComponent = planMilestoneTool.renderResult(
|
||||
{
|
||||
details: {
|
||||
const fakeTheme = {
|
||||
bold: (text: string) => text,
|
||||
fg: (_name: string, text: string) => text,
|
||||
};
|
||||
const callComponent = planMilestoneTool.renderCall(
|
||||
{
|
||||
milestoneId: "M008",
|
||||
title: "Workflow polish",
|
||||
sliceCount: 1,
|
||||
firstSliceId: "S01",
|
||||
firstSliceTitle: "Improve tool cards",
|
||||
slices: [{ sliceId: "S01", title: "Improve tool cards" }],
|
||||
},
|
||||
},
|
||||
{},
|
||||
fakeTheme,
|
||||
);
|
||||
assert.match(resultComponent.text, /M008 planned: Workflow polish/);
|
||||
assert.match(resultComponent.text, /1 slice/);
|
||||
assert.match(resultComponent.text, /next S01: Improve tool cards/);
|
||||
}
|
||||
fakeTheme,
|
||||
);
|
||||
assert.match(callComponent.text, /M008: Workflow polish/);
|
||||
assert.match(callComponent.text, /1 slice/);
|
||||
|
||||
const resultComponent = planMilestoneTool.renderResult(
|
||||
{
|
||||
details: {
|
||||
milestoneId: "M008",
|
||||
title: "Workflow polish",
|
||||
sliceCount: 1,
|
||||
firstSliceId: "S01",
|
||||
firstSliceTitle: "Improve tool cards",
|
||||
},
|
||||
},
|
||||
{},
|
||||
fakeTheme,
|
||||
);
|
||||
assert.match(resultComponent.text, /M008 planned: Workflow polish/);
|
||||
assert.match(resultComponent.text, /1 slice/);
|
||||
assert.match(resultComponent.text, /next S01: Improve tool cards/);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,10 +25,9 @@ const MCP_WORKFLOW_TOOL_SURFACE = new Set([
|
|||
"sf_decision_save",
|
||||
"sf_complete_milestone",
|
||||
"sf_journal_query",
|
||||
"sf_milestone_complete",
|
||||
"sf_milestone_generate_id",
|
||||
"sf_milestone_status",
|
||||
"sf_milestone_validate",
|
||||
"sf_validate_milestone",
|
||||
"sf_plan_task",
|
||||
"sf_plan_milestone",
|
||||
"sf_plan_slice",
|
||||
|
|
@ -36,18 +35,11 @@ const MCP_WORKFLOW_TOOL_SURFACE = new Set([
|
|||
"sf_reassess_roadmap",
|
||||
"sf_requirement_save",
|
||||
"sf_requirement_update",
|
||||
"sf_roadmap_reassess",
|
||||
"sf_save_decision",
|
||||
"sf_save_gate_result",
|
||||
"sf_save_requirement",
|
||||
"sf_skip_slice",
|
||||
"sf_slice_replan",
|
||||
"sf_slice_complete",
|
||||
"sf_summary_save",
|
||||
"sf_task_plan",
|
||||
"sf_task_complete",
|
||||
"sf_update_requirement",
|
||||
"sf_validate_milestone",
|
||||
]);
|
||||
|
||||
function parseLookupOutput(output: Buffer | string): string {
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ function matchesBlockedPattern(path: string): boolean {
|
|||
export const BLOCKED_WRITE_ERROR = `Direct writes to .sf/STATE.md and .sf/sf.db are blocked. Use engine tool calls instead:
|
||||
- To complete a task: call sf_task_complete(milestone_id, slice_id, task_id, summary)
|
||||
- To complete a slice: call sf_slice_complete(milestone_id, slice_id, summary, uat_result)
|
||||
- To save a decision: call sf_save_decision(scope, decision, choice, rationale)
|
||||
- To save a decision: call sf_decision_save(scope, decision, choice, rationale)
|
||||
- To start a task: call sf_start_task(milestone_id, slice_id, task_id)
|
||||
- To record verification: call sf_record_verification(milestone_id, slice_id, task_id, evidence)
|
||||
- To report a blocker: call sf_report_blocker(milestone_id, slice_id, task_id, description)`;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue