fix(sf): remove workflow tool aliases

This commit is contained in:
Mikael Hugo 2026-05-02 18:32:50 +02:00
parent 1be11744ee
commit a3ef4bdf3f
11 changed files with 2870 additions and 2483 deletions

View file

@ -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.

View file

@ -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` 会话(非自动模式)中。

View file

@ -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

View file

@ -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 ──────────────────────────────────────────────

View file

@ -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",
];

View file

@ -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 [

View file

@ -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/);
});
});
// ═══════════════════════════════════════════════════════════════════════════
});

View file

@ -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 {

View file

@ -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)`;