singularity-forge/packages/mcp-server/src/workflow-tools.ts
ace-pm 421fccd898 refactor: rebrand gsd_ tool names and references to sf_ namespace
Updates workflow tool names, documentation references, and internal naming
conventions across MCP server, CLI, tests, and web components to complete
the singularity-forge rebrand from gsd to sf.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:51:38 +02:00

1413 lines
53 KiB
TypeScript

/**
* Workflow MCP tools — exposes the core SF mutation/read handlers over MCP.
*/
import { isAbsolute, relative, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { z } from "zod";
type WorkflowToolExecutors = {
SUPPORTED_SUMMARY_ARTIFACT_TYPES: readonly string[];
executeMilestoneStatus: (params: { milestoneId: string }, basePath?: string) => Promise<unknown>;
executePlanMilestone: (
params: {
milestoneId: string;
title: string;
vision: string;
slices: Array<{
sliceId: string;
title: string;
risk: string;
depends: string[];
demo: string;
goal: string;
successCriteria: string;
proofLevel: string;
integrationClosure: string;
observabilityImpact: string;
}>;
status?: string;
dependsOn?: string[];
successCriteria?: string[];
keyRisks?: Array<{ risk: string; whyItMatters: string }>;
proofStrategy?: Array<{ riskOrUnknown: string; retireIn: string; whatWillBeProven: string }>;
verificationContract?: string;
verificationIntegration?: string;
verificationOperational?: string;
verificationUat?: string;
definitionOfDone?: string[];
requirementCoverage?: string;
boundaryMapMarkdown?: string;
},
basePath?: string,
) => Promise<unknown>;
executePlanSlice: (
params: {
milestoneId: string;
sliceId: string;
goal: string;
tasks: Array<{
taskId: string;
title: string;
description: string;
estimate: string;
files: string[];
verify: string;
inputs: string[];
expectedOutput: string[];
observabilityImpact?: string;
}>;
successCriteria?: string;
proofLevel?: string;
integrationClosure?: string;
observabilityImpact?: string;
},
basePath?: string,
) => Promise<unknown>;
executeReplanSlice: (
params: {
milestoneId: string;
sliceId: string;
blockerTaskId: string;
blockerDescription: string;
whatChanged: string;
updatedTasks: Array<{
taskId: string;
title: string;
description: string;
estimate: string;
files: string[];
verify: string;
inputs: string[];
expectedOutput: string[];
fullPlanMd?: string;
}>;
removedTaskIds: string[];
},
basePath?: string,
) => Promise<unknown>;
executeSliceComplete: (
params: {
sliceId: string;
milestoneId: string;
sliceTitle: string;
oneLiner: string;
narrative: string;
verification: string;
uatContent: string;
deviations?: string;
knownLimitations?: string;
followUps?: string;
keyFiles?: string[] | string;
keyDecisions?: string[] | string;
patternsEstablished?: string[] | string;
observabilitySurfaces?: string[] | string;
provides?: string[] | string;
requirementsSurfaced?: string[] | string;
drillDownPaths?: string[] | string;
affects?: string[] | string;
requirementsAdvanced?: Array<{ id: string; how: string } | string>;
requirementsValidated?: Array<{ id: string; proof: string } | string>;
requirementsInvalidated?: Array<{ id: string; what: string } | string>;
filesModified?: Array<{ path: string; description: string } | string>;
requires?: Array<{ slice: string; provides: string } | string>;
},
basePath?: string,
) => Promise<unknown>;
executeCompleteMilestone: (
params: {
milestoneId: string;
title: string;
oneLiner: string;
narrative: string;
verificationPassed: boolean;
successCriteriaResults?: string;
definitionOfDoneResults?: string;
requirementOutcomes?: string;
keyDecisions?: string[];
keyFiles?: string[];
lessonsLearned?: string[];
followUps?: string;
deviations?: string;
},
basePath?: string,
) => Promise<unknown>;
executeValidateMilestone: (
params: {
milestoneId: string;
verdict: "pass" | "needs-attention" | "needs-remediation";
remediationRound: number;
successCriteriaChecklist: string;
sliceDeliveryAudit: string;
crossSliceIntegration: string;
requirementCoverage: string;
verificationClasses?: string;
verdictRationale: string;
remediationPlan?: string;
},
basePath?: string,
) => Promise<unknown>;
executeReassessRoadmap: (
params: {
milestoneId: string;
completedSliceId: string;
verdict: string;
assessment: string;
sliceChanges: {
modified: Array<{
sliceId: string;
title: string;
risk?: string;
depends?: string[];
demo?: string;
}>;
added: Array<{
sliceId: string;
title: string;
risk?: string;
depends?: string[];
demo?: string;
}>;
removed: string[];
};
},
basePath?: string,
) => Promise<unknown>;
executeSaveGateResult: (
params: {
milestoneId: string;
sliceId: string;
gateId: string;
taskId?: string;
verdict: "pass" | "flag" | "omitted";
rationale: string;
findings?: string;
},
basePath?: string,
) => Promise<unknown>;
executeSummarySave: (
params: {
milestone_id: string;
slice_id?: string;
task_id?: string;
artifact_type: string;
content: string;
},
basePath?: string,
) => Promise<unknown>;
executeTaskComplete: (
params: {
taskId: string;
sliceId: string;
milestoneId: string;
oneLiner: string;
narrative: string;
verification: string;
deviations?: string;
knownIssues?: string;
keyFiles?: string[];
keyDecisions?: string[];
blockerDiscovered?: boolean;
verificationEvidence?: Array<
{ command: string; exitCode: number; verdict: string; durationMs: number } | string
>;
},
basePath?: string,
) => Promise<unknown>;
};
type WorkflowWriteGateModule = {
loadWriteGateSnapshot: (basePath?: string) => {
verifiedDepthMilestones: string[];
activeQueuePhase: boolean;
pendingGateId: string | null;
};
shouldBlockPendingGateInSnapshot: (
snapshot: {
verifiedDepthMilestones: string[];
activeQueuePhase: boolean;
pendingGateId: string | null;
},
toolName: string,
milestoneId: string | null,
queuePhaseActive?: boolean,
) => { block: boolean; reason?: string };
shouldBlockQueueExecutionInSnapshot: (
snapshot: {
verifiedDepthMilestones: string[];
activeQueuePhase: boolean;
pendingGateId: string | null;
},
toolName: string,
input: string,
queuePhaseActive?: boolean,
) => { block: boolean; reason?: string };
};
type WorkflowDbBootstrapModule = {
ensureDbOpen: (basePath?: string) => Promise<boolean>;
};
let workflowToolExecutorsPromise: Promise<WorkflowToolExecutors> | null = null;
let workflowExecutionQueue: Promise<void> = Promise.resolve();
let workflowWriteGatePromise: Promise<WorkflowWriteGateModule> | null = null;
function getAllowedProjectRoot(env: NodeJS.ProcessEnv = process.env): string | null {
const configuredRoot = env.SF_WORKFLOW_PROJECT_ROOT?.trim();
return configuredRoot ? resolve(configuredRoot) : null;
}
function isWithinRoot(candidatePath: string, rootPath: string): boolean {
const rel = relative(rootPath, candidatePath);
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
}
function validateProjectDir(projectDir: string, env: NodeJS.ProcessEnv = process.env): string {
if (!isAbsolute(projectDir)) {
throw new Error(`projectDir must be an absolute path. Received: ${projectDir}`);
}
const resolvedProjectDir = resolve(projectDir);
const allowedRoot = getAllowedProjectRoot(env);
if (allowedRoot && !isWithinRoot(resolvedProjectDir, allowedRoot)) {
throw new Error(
`projectDir must stay within the configured workflow project root. Received: ${resolvedProjectDir}; allowed root: ${allowedRoot}`,
);
}
return resolvedProjectDir;
}
function parseToolArgs<T>(schema: z.ZodType<T>, args: Record<string, unknown>): T {
return schema.parse(args);
}
function parseWorkflowArgs<T extends { projectDir: string }>(
schema: z.ZodType<T>,
args: Record<string, unknown>,
): T {
const parsed = parseToolArgs(schema, args);
return {
...parsed,
projectDir: validateProjectDir(parsed.projectDir),
};
}
function isWorkflowToolExecutors(value: unknown): value is WorkflowToolExecutors {
if (!value || typeof value !== "object") return false;
const record = value as Record<string, unknown>;
const functionExports = [
"executeMilestoneStatus",
"executePlanMilestone",
"executePlanSlice",
"executeReplanSlice",
"executeSliceComplete",
"executeCompleteMilestone",
"executeValidateMilestone",
"executeReassessRoadmap",
"executeSaveGateResult",
"executeSummarySave",
"executeTaskComplete",
];
return Array.isArray(record.SUPPORTED_SUMMARY_ARTIFACT_TYPES) &&
functionExports.every((key) => typeof record[key] === "function");
}
function getSupportedSummaryArtifactTypes(executors: WorkflowToolExecutors): readonly string[] {
return executors.SUPPORTED_SUMMARY_ARTIFACT_TYPES;
}
function getWriteGateModuleCandidates(): string[] {
const candidates: string[] = [];
const explicitModule = process.env.SF_WORKFLOW_WRITE_GATE_MODULE?.trim();
if (explicitModule) {
if (/^[a-z]{2,}:/i.test(explicitModule) && !explicitModule.startsWith("file:")) {
throw new Error("SF_WORKFLOW_WRITE_GATE_MODULE only supports file: URLs or filesystem paths.");
}
candidates.push(explicitModule.startsWith("file:") ? explicitModule : toFileUrl(explicitModule));
}
candidates.push(
new URL("../../../src/resources/extensions/sf/bootstrap/write-gate.js", import.meta.url).href,
new URL("../../../dist/resources/extensions/sf/bootstrap/write-gate.js", import.meta.url).href,
new URL("../../../src/resources/extensions/sf/bootstrap/write-gate.ts", import.meta.url).href,
);
return [...new Set(candidates)];
}
function toFileUrl(modulePath: string): string {
return pathToFileURL(resolve(modulePath)).href;
}
/** @internal — exported for testing only */
export function _buildImportCandidates(relativePath: string): string[] {
// Build candidate paths: try the given path first, then swap src/<->dist/
// and try .ts extension. This handles both dev (tsx from src/) and prod
// (compiled from dist/) execution contexts.
const candidates: string[] = [relativePath];
const swapped = relativePath.includes("/src/")
? relativePath.replace("/src/", "/dist/")
: relativePath.includes("/dist/")
? relativePath.replace("/dist/", "/src/")
: null;
if (swapped) candidates.push(swapped);
// Also try .ts variants for dev-mode tsx execution
if (relativePath.endsWith(".js")) {
candidates.push(relativePath.replace(/\.js$/, ".ts"));
if (swapped) candidates.push(swapped.replace(/\.js$/, ".ts"));
}
return candidates;
}
async function importLocalModule<T>(relativePath: string): Promise<T> {
const candidates = _buildImportCandidates(relativePath)
.map((p) => new URL(p, import.meta.url).href);
let lastErr: unknown;
for (const candidate of candidates) {
try {
return await import(candidate) as T;
} catch (err) {
lastErr = err;
}
}
throw lastErr;
}
function getWorkflowExecutorModuleCandidates(env: NodeJS.ProcessEnv = process.env): string[] {
const candidates: string[] = [];
const explicitModule = env.SF_WORKFLOW_EXECUTORS_MODULE?.trim();
if (explicitModule) {
if (/^[a-z]{2,}:/i.test(explicitModule) && !explicitModule.startsWith("file:")) {
throw new Error("SF_WORKFLOW_EXECUTORS_MODULE only supports file: URLs or filesystem paths.");
}
candidates.push(explicitModule.startsWith("file:") ? explicitModule : toFileUrl(explicitModule));
}
candidates.push(
new URL("../../../src/resources/extensions/sf/tools/workflow-tool-executors.js", import.meta.url).href,
new URL("../../../dist/resources/extensions/sf/tools/workflow-tool-executors.js", import.meta.url).href,
new URL("../../../src/resources/extensions/sf/tools/workflow-tool-executors.ts", import.meta.url).href,
);
return [...new Set(candidates)];
}
async function getWorkflowToolExecutors(): Promise<WorkflowToolExecutors> {
if (!workflowToolExecutorsPromise) {
workflowToolExecutorsPromise = (async () => {
const attempts: string[] = [];
for (const candidate of getWorkflowExecutorModuleCandidates()) {
try {
const loaded = await import(candidate);
if (isWorkflowToolExecutors(loaded)) {
return loaded;
}
attempts.push(`${candidate} (module shape mismatch)`);
} catch (err) {
attempts.push(`${candidate} (${err instanceof Error ? err.message : String(err)})`);
}
}
throw new Error(
"Unable to load SF workflow executor bridge for MCP mutation tools. " +
"Set SF_WORKFLOW_EXECUTORS_MODULE to an importable workflow-tool-executors module, " +
"or run the MCP server from a SF checkout that includes src/resources/extensions/sf/tools/workflow-tool-executors.(js|ts). " +
`Attempts: ${attempts.join("; ")}`,
);
})();
}
return workflowToolExecutorsPromise;
}
async function getWorkflowWriteGateModule(): Promise<WorkflowWriteGateModule> {
if (!workflowWriteGatePromise) {
workflowWriteGatePromise = (async () => {
const attempts: string[] = [];
for (const candidate of getWriteGateModuleCandidates()) {
try {
const loaded = await import(candidate);
if (
loaded &&
typeof loaded.loadWriteGateSnapshot === "function" &&
typeof loaded.shouldBlockPendingGateInSnapshot === "function" &&
typeof loaded.shouldBlockQueueExecutionInSnapshot === "function"
) {
return loaded as WorkflowWriteGateModule;
}
attempts.push(`${candidate} (module shape mismatch)`);
} catch (err) {
attempts.push(`${candidate} (${err instanceof Error ? err.message : String(err)})`);
}
}
throw new Error(
"Unable to load SF write-gate bridge for workflow MCP tools. " +
`Attempts: ${attempts.join("; ")}`,
);
})();
}
return workflowWriteGatePromise;
}
interface McpToolServer {
tool(
name: string,
description: string,
params: Record<string, unknown>,
handler: (args: Record<string, unknown>) => Promise<unknown>,
): unknown;
}
export const WORKFLOW_TOOL_NAMES = [
"sf_decision_save",
"sf_save_decision",
"sf_requirement_update",
"sf_update_requirement",
"sf_requirement_save",
"sf_save_requirement",
"sf_milestone_generate_id",
"sf_generate_milestone_id",
"sf_plan_milestone",
"sf_plan_slice",
"sf_plan_task",
"sf_task_plan",
"sf_replan_slice",
"sf_slice_replan",
"sf_slice_complete",
"sf_complete_slice",
"sf_skip_slice",
"sf_complete_milestone",
"sf_milestone_complete",
"sf_validate_milestone",
"sf_milestone_validate",
"sf_reassess_roadmap",
"sf_roadmap_reassess",
"sf_save_gate_result",
"sf_summary_save",
"sf_task_complete",
"sf_complete_task",
"sf_milestone_status",
"sf_journal_query",
] as const;
async function runSerializedWorkflowOperation<T>(fn: () => Promise<T>): Promise<T> {
// The shared DB adapter and workflow log base path are process-global, so
// workflow MCP mutations must not overlap within a single server process.
const prior = workflowExecutionQueue;
let release!: () => void;
workflowExecutionQueue = new Promise<void>((resolve) => {
release = resolve;
});
await prior;
try {
return await fn();
} finally {
release();
}
}
async function runSerializedWorkflowDbOperation<T>(
projectDir: string,
fn: () => Promise<T>,
): Promise<T> {
return runSerializedWorkflowOperation(async () => {
const { ensureDbOpen } = await importLocalModule<WorkflowDbBootstrapModule>(
"../../../src/resources/extensions/sf/bootstrap/dynamic-tools.js",
);
const dbAvailable = await ensureDbOpen(projectDir);
if (!dbAvailable) {
throw new Error("SF database is not available");
}
return fn();
});
}
async function enforceWorkflowWriteGate(
toolName: string,
projectDir: string,
milestoneId: string | null = null,
): Promise<void> {
const writeGate = await getWorkflowWriteGateModule();
const snapshot = writeGate.loadWriteGateSnapshot(projectDir);
const pendingGate = writeGate.shouldBlockPendingGateInSnapshot(
snapshot,
toolName,
milestoneId,
snapshot.activeQueuePhase,
);
if (pendingGate.block) {
throw new Error(pendingGate.reason ?? "workflow tool blocked by pending discussion gate");
}
const queueGuard = writeGate.shouldBlockQueueExecutionInSnapshot(
snapshot,
toolName,
"",
snapshot.activeQueuePhase,
);
if (queueGuard.block) {
throw new Error(queueGuard.reason ?? "workflow tool blocked during queue mode");
}
}
async function handleTaskComplete(
projectDir: string,
args: Omit<z.infer<typeof taskCompleteSchema>, "projectDir">,
): Promise<unknown> {
await enforceWorkflowWriteGate("sf_task_complete", projectDir, args.milestoneId);
const {
taskId,
sliceId,
milestoneId,
oneLiner,
narrative,
verification,
deviations,
knownIssues,
keyFiles,
keyDecisions,
blockerDiscovered,
verificationEvidence,
} = args;
const { executeTaskComplete } = await getWorkflowToolExecutors();
return runSerializedWorkflowOperation(() =>
executeTaskComplete(
{
taskId,
sliceId,
milestoneId,
oneLiner,
narrative,
verification,
deviations,
knownIssues,
keyFiles,
keyDecisions,
blockerDiscovered,
verificationEvidence,
},
projectDir,
),
);
}
async function handleSliceComplete(
projectDir: string,
args: z.infer<typeof sliceCompleteSchema>,
): Promise<unknown> {
await enforceWorkflowWriteGate("sf_slice_complete", projectDir, args.milestoneId);
const { executeSliceComplete } = await getWorkflowToolExecutors();
const { projectDir: _projectDir, ...params } = args;
return runSerializedWorkflowOperation(() => executeSliceComplete(params, projectDir));
}
async function handleReplanSlice(
projectDir: string,
args: z.infer<typeof replanSliceSchema>,
): Promise<unknown> {
await enforceWorkflowWriteGate("sf_replan_slice", projectDir, args.milestoneId);
const { executeReplanSlice } = await getWorkflowToolExecutors();
const { projectDir: _projectDir, ...params } = args;
return runSerializedWorkflowOperation(() => executeReplanSlice(params, projectDir));
}
async function handleCompleteMilestone(
projectDir: string,
args: z.infer<typeof completeMilestoneSchema>,
): Promise<unknown> {
await enforceWorkflowWriteGate("sf_complete_milestone", projectDir, args.milestoneId);
const { executeCompleteMilestone } = await getWorkflowToolExecutors();
const { projectDir: _projectDir, ...params } = args;
return runSerializedWorkflowOperation(() => executeCompleteMilestone(params, projectDir));
}
async function handleValidateMilestone(
projectDir: string,
args: z.infer<typeof validateMilestoneSchema>,
): Promise<unknown> {
await enforceWorkflowWriteGate("sf_validate_milestone", projectDir, args.milestoneId);
const { executeValidateMilestone } = await getWorkflowToolExecutors();
const { projectDir: _projectDir, ...params } = args;
return runSerializedWorkflowOperation(() => executeValidateMilestone(params, projectDir));
}
async function handleReassessRoadmap(
projectDir: string,
args: z.infer<typeof reassessRoadmapSchema>,
): Promise<unknown> {
await enforceWorkflowWriteGate("sf_reassess_roadmap", projectDir, args.milestoneId);
const { executeReassessRoadmap } = await getWorkflowToolExecutors();
const { projectDir: _projectDir, ...params } = args;
return runSerializedWorkflowOperation(() => executeReassessRoadmap(params, projectDir));
}
async function handleSaveGateResult(
projectDir: string,
args: z.infer<typeof saveGateResultSchema>,
): Promise<unknown> {
await enforceWorkflowWriteGate("sf_save_gate_result", projectDir, args.milestoneId);
const { executeSaveGateResult } = await getWorkflowToolExecutors();
const { projectDir: _projectDir, ...params } = args;
return runSerializedWorkflowOperation(() => executeSaveGateResult(params, projectDir));
}
async function ensureMilestoneDbRow(milestoneId: string): Promise<void> {
try {
const { insertMilestone } = await importLocalModule<any>("../../../src/resources/extensions/sf/sf-db.js");
insertMilestone({ id: milestoneId, status: "queued" });
} catch {
// Ignore pre-existing rows or transient DB availability issues.
}
}
const projectDirParam = z.string().describe("Absolute path to the project directory within the configured workflow root");
const planMilestoneParams = {
projectDir: projectDirParam,
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
title: z.string().describe("Milestone title"),
vision: z.string().describe("Milestone vision"),
slices: z.array(z.object({
sliceId: z.string(),
title: z.string(),
risk: z.string(),
depends: z.array(z.string()),
demo: z.string(),
goal: z.string(),
successCriteria: z.string(),
proofLevel: z.string(),
integrationClosure: z.string(),
observabilityImpact: z.string(),
})).describe("Planned slices for the milestone"),
status: z.string().optional().describe("Milestone status"),
dependsOn: z.array(z.string()).optional().describe("Milestone dependencies"),
successCriteria: z.array(z.string()).optional().describe("Top-level success criteria bullets"),
keyRisks: z.array(z.object({
risk: z.string(),
whyItMatters: z.string(),
})).optional().describe("Structured risk entries"),
proofStrategy: z.array(z.object({
riskOrUnknown: z.string(),
retireIn: z.string(),
whatWillBeProven: z.string(),
})).optional().describe("Structured proof strategy entries"),
verificationContract: z.string().optional(),
verificationIntegration: z.string().optional(),
verificationOperational: z.string().optional(),
verificationUat: z.string().optional(),
definitionOfDone: z.array(z.string()).optional(),
requirementCoverage: z.string().optional(),
boundaryMapMarkdown: z.string().optional(),
};
const planMilestoneSchema = z.object(planMilestoneParams);
const planSliceParams = {
projectDir: projectDirParam,
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
sliceId: z.string().describe("Slice ID (e.g. S01)"),
goal: z.string().describe("Slice goal"),
tasks: z.array(z.object({
taskId: z.string(),
title: z.string(),
description: z.string(),
estimate: z.string(),
files: z.array(z.string()),
verify: z.string(),
inputs: z.array(z.string()),
expectedOutput: z.array(z.string()),
observabilityImpact: z.string().optional(),
})).describe("Planned tasks for the slice"),
successCriteria: z.string().optional(),
proofLevel: z.string().optional(),
integrationClosure: z.string().optional(),
observabilityImpact: z.string().optional(),
};
const planSliceSchema = z.object(planSliceParams);
const completeMilestoneParams = {
projectDir: projectDirParam,
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
title: z.string().describe("Milestone title"),
oneLiner: z.string().describe("One-sentence summary of what the milestone achieved"),
narrative: z.string().describe("Detailed narrative of what happened during the milestone"),
verificationPassed: z.boolean().describe("Must be true after milestone verification succeeds"),
successCriteriaResults: z.string().optional(),
definitionOfDoneResults: z.string().optional(),
requirementOutcomes: z.string().optional(),
keyDecisions: z.array(z.string()).optional(),
keyFiles: z.array(z.string()).optional(),
lessonsLearned: z.array(z.string()).optional(),
followUps: z.string().optional(),
deviations: z.string().optional(),
};
const completeMilestoneSchema = z.object(completeMilestoneParams);
const validateMilestoneParams = {
projectDir: projectDirParam,
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
verdict: z.enum(["pass", "needs-attention", "needs-remediation"]).describe("Validation verdict"),
remediationRound: z.number().describe("Remediation round (0 for first validation)"),
successCriteriaChecklist: z.string().describe("Markdown checklist of success criteria with evidence"),
sliceDeliveryAudit: z.string().describe("Markdown auditing each slice's claimed vs delivered output"),
crossSliceIntegration: z.string().describe("Markdown describing cross-slice issues or closure"),
requirementCoverage: z.string().describe("Markdown describing requirement coverage and gaps"),
verificationClasses: z.string().optional(),
verdictRationale: z.string().describe("Why this verdict was chosen"),
remediationPlan: z.string().optional(),
};
const validateMilestoneSchema = z.object(validateMilestoneParams);
const roadmapSliceChangeSchema = z.object({
sliceId: z.string(),
title: z.string(),
risk: z.string().optional(),
depends: z.array(z.string()).optional(),
demo: z.string().optional(),
});
const reassessRoadmapParams = {
projectDir: projectDirParam,
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
completedSliceId: z.string().describe("Slice ID that just completed"),
verdict: z.string().describe("Assessment verdict such as roadmap-confirmed or roadmap-adjusted"),
assessment: z.string().describe("Assessment text explaining the roadmap decision"),
sliceChanges: z.object({
modified: z.array(roadmapSliceChangeSchema),
added: z.array(roadmapSliceChangeSchema),
removed: z.array(z.string()),
}).describe("Slice changes to apply"),
};
const reassessRoadmapSchema = z.object(reassessRoadmapParams);
const saveGateResultParams = {
projectDir: projectDirParam,
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
sliceId: z.string().describe("Slice ID (e.g. S01)"),
gateId: z.enum(["Q3", "Q4", "Q5", "Q6", "Q7", "Q8", "MV01", "MV02", "MV03", "MV04"]).describe("Gate ID"),
taskId: z.string().optional().describe("Task ID for task-scoped gates"),
verdict: z.enum(["pass", "flag", "omitted"]).describe("Gate verdict"),
rationale: z.string().describe("One-sentence justification"),
findings: z.string().optional().describe("Detailed markdown findings"),
};
const saveGateResultSchema = z.object(saveGateResultParams);
const replanSliceParams = {
projectDir: projectDirParam,
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
sliceId: z.string().describe("Slice ID (e.g. S01)"),
blockerTaskId: z.string().describe("Task ID that discovered the blocker"),
blockerDescription: z.string().describe("Description of the blocker"),
whatChanged: z.string().describe("Summary of what changed in the plan"),
updatedTasks: z.array(z.object({
taskId: z.string(),
title: z.string(),
description: z.string(),
estimate: z.string(),
files: z.array(z.string()),
verify: z.string(),
inputs: z.array(z.string()),
expectedOutput: z.array(z.string()),
fullPlanMd: z.string().optional(),
})).describe("Tasks to upsert into the replanned slice"),
removedTaskIds: z.array(z.string()).describe("Task IDs to remove from the slice"),
};
const replanSliceSchema = z.object(replanSliceParams);
const sliceCompleteParams = {
projectDir: projectDirParam,
sliceId: z.string().describe("Slice ID (e.g. S01)"),
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
sliceTitle: z.string().describe("Title of the slice"),
oneLiner: z.string().describe("One-line summary of what the slice accomplished"),
narrative: z.string().describe("Detailed narrative of what happened across all tasks"),
verification: z.string().describe("What was verified across all tasks"),
uatContent: z.string().describe("UAT test content (markdown body)"),
deviations: z.string().optional(),
knownLimitations: z.string().optional(),
followUps: z.string().optional(),
keyFiles: z.union([z.array(z.string()), z.string()]).optional(),
keyDecisions: z.union([z.array(z.string()), z.string()]).optional(),
patternsEstablished: z.union([z.array(z.string()), z.string()]).optional(),
observabilitySurfaces: z.union([z.array(z.string()), z.string()]).optional(),
provides: z.union([z.array(z.string()), z.string()]).optional(),
requirementsSurfaced: z.union([z.array(z.string()), z.string()]).optional(),
drillDownPaths: z.union([z.array(z.string()), z.string()]).optional(),
affects: z.union([z.array(z.string()), z.string()]).optional(),
requirementsAdvanced: z.array(z.union([
z.object({ id: z.string(), how: z.string() }),
z.string(),
])).optional(),
requirementsValidated: z.array(z.union([
z.object({ id: z.string(), proof: z.string() }),
z.string(),
])).optional(),
requirementsInvalidated: z.array(z.union([
z.object({ id: z.string(), what: z.string() }),
z.string(),
])).optional(),
filesModified: z.array(z.union([
z.object({ path: z.string(), description: z.string() }),
z.string(),
])).optional(),
requires: z.array(z.union([
z.object({ slice: z.string(), provides: z.string() }),
z.string(),
])).optional(),
};
const sliceCompleteSchema = z.object(sliceCompleteParams);
const summarySaveParams = {
projectDir: projectDirParam,
milestone_id: z.string().describe("Milestone ID (e.g. M001)"),
slice_id: z.string().optional().describe("Slice ID (e.g. S01)"),
task_id: z.string().optional().describe("Task ID (e.g. T01)"),
artifact_type: z.string().describe("Artifact type to save (SUMMARY, RESEARCH, CONTEXT, ASSESSMENT, CONTEXT-DRAFT)"),
content: z.string().describe("The full markdown content of the artifact"),
};
const summarySaveSchema = z.object(summarySaveParams);
const decisionSaveParams = {
projectDir: projectDirParam,
scope: z.string().describe("Scope of the decision (e.g. architecture, library, observability)"),
decision: z.string().describe("What is being decided"),
choice: z.string().describe("The choice made"),
rationale: z.string().describe("Why this choice was made"),
revisable: z.string().optional().describe("Whether this can be revisited"),
when_context: z.string().optional().describe("When/context for the decision"),
made_by: z.enum(["human", "agent", "collaborative"]).optional().describe("Who made the decision"),
};
const decisionSaveSchema = z.object(decisionSaveParams);
const requirementUpdateParams = {
projectDir: projectDirParam,
id: z.string().describe("Requirement ID (e.g. R001)"),
status: z.string().optional().describe("New status"),
validation: z.string().optional().describe("Validation criteria or proof"),
notes: z.string().optional().describe("Additional notes"),
description: z.string().optional().describe("Updated description"),
primary_owner: z.string().optional().describe("Primary owning slice"),
supporting_slices: z.string().optional().describe("Supporting slices"),
};
const requirementUpdateSchema = z.object(requirementUpdateParams);
const requirementSaveParams = {
projectDir: projectDirParam,
class: z.string().describe("Requirement class"),
description: z.string().describe("Short description of the requirement"),
why: z.string().describe("Why this requirement matters"),
source: z.string().describe("Origin of the requirement"),
status: z.string().optional().describe("Requirement status"),
primary_owner: z.string().optional().describe("Primary owning slice"),
supporting_slices: z.string().optional().describe("Supporting slices"),
validation: z.string().optional().describe("Validation criteria"),
notes: z.string().optional().describe("Additional notes"),
};
const requirementSaveSchema = z.object(requirementSaveParams);
const milestoneGenerateIdParams = {
projectDir: projectDirParam,
};
const milestoneGenerateIdSchema = z.object(milestoneGenerateIdParams);
const planTaskParams = {
projectDir: projectDirParam,
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
sliceId: z.string().describe("Slice ID (e.g. S01)"),
taskId: z.string().describe("Task ID (e.g. T01)"),
title: z.string().describe("Task title"),
description: z.string().describe("Task description / steps block"),
estimate: z.string().describe("Task estimate"),
files: z.array(z.string()).describe("Files likely touched"),
verify: z.string().describe("Verification command or block"),
inputs: z.array(z.string()).describe("Input files or references"),
expectedOutput: z.array(z.string()).describe("Expected output files or artifacts"),
observabilityImpact: z.string().optional().describe("Task observability impact"),
};
const planTaskSchema = z.object(planTaskParams);
const skipSliceParams = {
projectDir: projectDirParam,
sliceId: z.string().describe("Slice ID (e.g. S02)"),
milestoneId: z.string().describe("Milestone ID (e.g. M003)"),
reason: z.string().optional().describe("Reason for skipping this slice"),
};
const skipSliceSchema = z.object(skipSliceParams);
const taskCompleteParams = {
projectDir: projectDirParam,
taskId: z.string().describe("Task ID (e.g. T01)"),
sliceId: z.string().describe("Slice ID (e.g. S01)"),
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
oneLiner: z.string().describe("One-line summary of what was accomplished"),
narrative: z.string().describe("Detailed narrative of what happened during the task"),
verification: z.string().describe("What was verified and how"),
deviations: z.string().optional().describe("Deviations from the task plan"),
knownIssues: z.string().optional().describe("Known issues discovered but not fixed"),
keyFiles: z.array(z.string()).optional().describe("List of key files created or modified"),
keyDecisions: z.array(z.string()).optional().describe("List of key decisions made during this task"),
blockerDiscovered: z.boolean().optional().describe("Whether a plan-invalidating blocker was discovered"),
verificationEvidence: z.array(z.union([
z.object({
command: z.string(),
exitCode: z.number(),
verdict: z.string(),
durationMs: z.number(),
}),
z.string(),
])).optional().describe("Verification evidence entries"),
};
const taskCompleteSchema = z.object(taskCompleteParams);
const milestoneStatusParams = {
projectDir: projectDirParam,
milestoneId: z.string().describe("Milestone ID to query (e.g. M001)"),
};
const milestoneStatusSchema = z.object(milestoneStatusParams);
const journalQueryParams = {
projectDir: projectDirParam,
flowId: z.string().optional().describe("Filter by flow ID"),
unitId: z.string().optional().describe("Filter by unit ID"),
rule: z.string().optional().describe("Filter by rule name"),
eventType: z.string().optional().describe("Filter by event type"),
after: z.string().optional().describe("ISO-8601 lower bound (inclusive)"),
before: z.string().optional().describe("ISO-8601 upper bound (inclusive)"),
limit: z.number().optional().describe("Maximum entries to return"),
};
const journalQuerySchema = z.object(journalQueryParams);
export function registerWorkflowTools(server: McpToolServer): void {
server.tool(
"sf_decision_save",
"Record a project decision to the SF database and regenerate DECISIONS.md.",
decisionSaveParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(decisionSaveSchema, args);
const { projectDir, ...params } = parsed;
await enforceWorkflowWriteGate("sf_decision_save", projectDir);
const result = await runSerializedWorkflowDbOperation(projectDir, async () => {
const { saveDecisionToDb } = await importLocalModule<any>("../../../src/resources/extensions/sf/db-writer.js");
return saveDecisionToDb(params, projectDir);
});
return { content: [{ type: "text" as const, text: `Saved decision ${result.id}` }] };
},
);
server.tool(
"sf_save_decision",
"Alias for sf_decision_save. Record a project decision to the SF database and regenerate DECISIONS.md.",
decisionSaveParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(decisionSaveSchema, args);
const { projectDir, ...params } = parsed;
await enforceWorkflowWriteGate("sf_decision_save", projectDir);
const result = await runSerializedWorkflowDbOperation(projectDir, async () => {
const { saveDecisionToDb } = await importLocalModule<any>("../../../src/resources/extensions/sf/db-writer.js");
return saveDecisionToDb(params, projectDir);
});
return { content: [{ type: "text" as const, text: `Saved decision ${result.id}` }] };
},
);
server.tool(
"sf_requirement_update",
"Update an existing requirement in the SF database and regenerate REQUIREMENTS.md.",
requirementUpdateParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(requirementUpdateSchema, args);
const { projectDir, id, ...updates } = parsed;
await enforceWorkflowWriteGate("sf_requirement_update", projectDir);
await runSerializedWorkflowDbOperation(projectDir, async () => {
const { updateRequirementInDb } = await importLocalModule<any>("../../../src/resources/extensions/sf/db-writer.js");
return updateRequirementInDb(id, updates, projectDir);
});
return { content: [{ type: "text" as const, text: `Updated requirement ${id}` }] };
},
);
server.tool(
"sf_update_requirement",
"Alias for sf_requirement_update. Update an existing requirement in the SF database and regenerate REQUIREMENTS.md.",
requirementUpdateParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(requirementUpdateSchema, args);
const { projectDir, id, ...updates } = parsed;
await enforceWorkflowWriteGate("sf_requirement_update", projectDir);
await runSerializedWorkflowDbOperation(projectDir, async () => {
const { updateRequirementInDb } = await importLocalModule<any>("../../../src/resources/extensions/sf/db-writer.js");
return updateRequirementInDb(id, updates, projectDir);
});
return { content: [{ type: "text" as const, text: `Updated requirement ${id}` }] };
},
);
server.tool(
"sf_requirement_save",
"Record a new requirement to the SF database and regenerate REQUIREMENTS.md.",
requirementSaveParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(requirementSaveSchema, args);
const { projectDir, ...params } = parsed;
await enforceWorkflowWriteGate("sf_requirement_save", projectDir);
const result = await runSerializedWorkflowDbOperation(projectDir, async () => {
const { saveRequirementToDb } = await importLocalModule<any>("../../../src/resources/extensions/sf/db-writer.js");
return saveRequirementToDb(params, projectDir);
});
return { content: [{ type: "text" as const, text: `Saved requirement ${result.id}` }] };
},
);
server.tool(
"sf_save_requirement",
"Alias for sf_requirement_save. Record a new requirement to the SF database and regenerate REQUIREMENTS.md.",
requirementSaveParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(requirementSaveSchema, args);
const { projectDir, ...params } = parsed;
await enforceWorkflowWriteGate("sf_requirement_save", projectDir);
const result = await runSerializedWorkflowDbOperation(projectDir, async () => {
const { saveRequirementToDb } = await importLocalModule<any>("../../../src/resources/extensions/sf/db-writer.js");
return saveRequirementToDb(params, projectDir);
});
return { content: [{ type: "text" as const, text: `Saved requirement ${result.id}` }] };
},
);
server.tool(
"sf_milestone_generate_id",
"Generate the next milestone ID for a new SF milestone.",
milestoneGenerateIdParams,
async (args: Record<string, unknown>) => {
const { projectDir } = parseWorkflowArgs(milestoneGenerateIdSchema, args);
await enforceWorkflowWriteGate("sf_milestone_generate_id", projectDir);
const id = await runSerializedWorkflowDbOperation(projectDir, async () => {
const {
claimReservedId,
findMilestoneIds,
getReservedMilestoneIds,
nextMilestoneId,
} = await importLocalModule<any>("../../../src/resources/extensions/sf/milestone-ids.js");
const reserved = claimReservedId();
if (reserved) {
await ensureMilestoneDbRow(reserved);
return reserved;
}
const allIds = [...new Set([...findMilestoneIds(projectDir), ...getReservedMilestoneIds()])];
const nextId = nextMilestoneId(allIds);
await ensureMilestoneDbRow(nextId);
return nextId;
});
return { content: [{ type: "text" as const, text: id }] };
},
);
server.tool(
"sf_generate_milestone_id",
"Alias for sf_milestone_generate_id. Generate the next milestone ID for a new SF milestone.",
milestoneGenerateIdParams,
async (args: Record<string, unknown>) => {
const { projectDir } = parseWorkflowArgs(milestoneGenerateIdSchema, args);
await enforceWorkflowWriteGate("sf_milestone_generate_id", projectDir);
const id = await runSerializedWorkflowDbOperation(projectDir, async () => {
const {
claimReservedId,
findMilestoneIds,
getReservedMilestoneIds,
nextMilestoneId,
} = await importLocalModule<any>("../../../src/resources/extensions/sf/milestone-ids.js");
const reserved = claimReservedId();
if (reserved) {
await ensureMilestoneDbRow(reserved);
return reserved;
}
const allIds = [...new Set([...findMilestoneIds(projectDir), ...getReservedMilestoneIds()])];
const nextId = nextMilestoneId(allIds);
await ensureMilestoneDbRow(nextId);
return nextId;
});
return { content: [{ type: "text" as const, text: id }] };
},
);
server.tool(
"sf_plan_milestone",
"Write milestone planning state to the SF database and render ROADMAP.md from DB.",
planMilestoneParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(planMilestoneSchema, args);
const { projectDir, ...params } = parsed;
await enforceWorkflowWriteGate("sf_plan_milestone", projectDir, params.milestoneId);
const { executePlanMilestone } = await getWorkflowToolExecutors();
return runSerializedWorkflowOperation(() => executePlanMilestone(params, projectDir));
},
);
server.tool(
"sf_plan_slice",
"Write slice/task planning state to the SF database and render plan artifacts from DB.",
planSliceParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(planSliceSchema, args);
const { projectDir, ...params } = parsed;
await enforceWorkflowWriteGate("sf_plan_slice", projectDir, params.milestoneId);
const { executePlanSlice } = await getWorkflowToolExecutors();
return runSerializedWorkflowOperation(() => executePlanSlice(params, projectDir));
},
);
server.tool(
"sf_plan_task",
"Write task planning state to the SF database and render tasks/T##-PLAN.md from DB.",
planTaskParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(planTaskSchema, args);
const { projectDir, ...params } = parsed;
await enforceWorkflowWriteGate("sf_plan_task", projectDir, params.milestoneId);
const result = await runSerializedWorkflowDbOperation(projectDir, async () => {
const { handlePlanTask } = await importLocalModule<any>("../../../src/resources/extensions/sf/tools/plan-task.js");
return handlePlanTask(params, projectDir);
});
if ("error" in result) {
throw new Error(result.error);
}
return {
content: [{ type: "text" as const, text: `Planned task ${result.taskId} (${result.sliceId}/${result.milestoneId})` }],
};
},
);
server.tool(
"sf_task_plan",
"Alias for sf_plan_task. Write task planning state to the SF database and render tasks/T##-PLAN.md from DB.",
planTaskParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(planTaskSchema, args);
const { projectDir, ...params } = parsed;
await enforceWorkflowWriteGate("sf_plan_task", projectDir, params.milestoneId);
const result = await runSerializedWorkflowDbOperation(projectDir, async () => {
const { handlePlanTask } = await importLocalModule<any>("../../../src/resources/extensions/sf/tools/plan-task.js");
return handlePlanTask(params, projectDir);
});
if ("error" in result) {
throw new Error(result.error);
}
return {
content: [{ type: "text" as const, text: `Planned task ${result.taskId} (${result.sliceId}/${result.milestoneId})` }],
};
},
);
server.tool(
"sf_replan_slice",
"Replan a slice after a blocker is discovered, preserving completed tasks and re-rendering PLAN.md + REPLAN.md.",
replanSliceParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(replanSliceSchema, args);
return handleReplanSlice(parsed.projectDir, parsed);
},
);
server.tool(
"sf_slice_replan",
"Alias for sf_replan_slice. Replan a slice after a blocker is discovered.",
replanSliceParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(replanSliceSchema, args);
return handleReplanSlice(parsed.projectDir, parsed);
},
);
server.tool(
"sf_slice_complete",
"Record a completed slice to the SF database, render SUMMARY.md + UAT.md, and update roadmap projection.",
sliceCompleteParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(sliceCompleteSchema, args);
return handleSliceComplete(parsed.projectDir, parsed);
},
);
server.tool(
"sf_complete_slice",
"Alias for sf_slice_complete. Record a completed slice to the SF database and render summary/UAT artifacts.",
sliceCompleteParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(sliceCompleteSchema, args);
return handleSliceComplete(parsed.projectDir, parsed);
},
);
server.tool(
"sf_skip_slice",
"Mark a slice as skipped so auto-mode advances past it without executing.",
skipSliceParams,
async (args: Record<string, unknown>) => {
const { projectDir, milestoneId, sliceId, reason } = parseWorkflowArgs(skipSliceSchema, args);
await enforceWorkflowWriteGate("sf_skip_slice", projectDir, milestoneId);
await runSerializedWorkflowDbOperation(projectDir, async () => {
const { getSlice, updateSliceStatus } = await importLocalModule<any>("../../../src/resources/extensions/sf/sf-db.js");
const { invalidateStateCache } = await importLocalModule<any>("../../../src/resources/extensions/sf/state.js");
const { rebuildState } = await importLocalModule<any>("../../../src/resources/extensions/sf/doctor.js");
const slice = getSlice(milestoneId, sliceId);
if (!slice) {
throw new Error(`Slice ${sliceId} not found in milestone ${milestoneId}`);
}
if (slice.status === "complete" || slice.status === "done") {
throw new Error(`Slice ${sliceId} is already complete and cannot be skipped`);
}
if (slice.status !== "skipped") {
updateSliceStatus(milestoneId, sliceId, "skipped");
invalidateStateCache();
await rebuildState(projectDir);
}
});
return {
content: [{ type: "text" as const, text: `Skipped slice ${sliceId} (${milestoneId}). Reason: ${reason ?? "User-directed skip"}.` }],
};
},
);
server.tool(
"sf_complete_milestone",
"Record a completed milestone to the SF database and render its SUMMARY.md.",
completeMilestoneParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(completeMilestoneSchema, args);
return handleCompleteMilestone(parsed.projectDir, parsed);
},
);
server.tool(
"sf_milestone_complete",
"Alias for sf_complete_milestone. Record a completed milestone to the SF database and render its SUMMARY.md.",
completeMilestoneParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(completeMilestoneSchema, args);
return handleCompleteMilestone(parsed.projectDir, parsed);
},
);
server.tool(
"sf_validate_milestone",
"Validate a milestone, persist validation results to the SF database, and render VALIDATION.md.",
validateMilestoneParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(validateMilestoneSchema, args);
return handleValidateMilestone(parsed.projectDir, parsed);
},
);
server.tool(
"sf_milestone_validate",
"Alias for sf_validate_milestone. Validate a milestone and render VALIDATION.md.",
validateMilestoneParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(validateMilestoneSchema, args);
return handleValidateMilestone(parsed.projectDir, parsed);
},
);
server.tool(
"sf_reassess_roadmap",
"Reassess a milestone roadmap after a slice completes, writing ASSESSMENT.md and re-rendering ROADMAP.md.",
reassessRoadmapParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(reassessRoadmapSchema, args);
return handleReassessRoadmap(parsed.projectDir, parsed);
},
);
server.tool(
"sf_roadmap_reassess",
"Alias for sf_reassess_roadmap. Reassess a roadmap after slice completion.",
reassessRoadmapParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(reassessRoadmapSchema, args);
return handleReassessRoadmap(parsed.projectDir, parsed);
},
);
server.tool(
"sf_save_gate_result",
"Save a quality gate result to the SF database.",
saveGateResultParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(saveGateResultSchema, args);
return handleSaveGateResult(parsed.projectDir, parsed);
},
);
server.tool(
"sf_summary_save",
"Save a SF summary/research/context/assessment artifact to the database and disk.",
summarySaveParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(summarySaveSchema, args);
const { projectDir, milestone_id, slice_id, task_id, artifact_type, content } = parsed;
await enforceWorkflowWriteGate("sf_summary_save", projectDir, milestone_id);
const executors = await getWorkflowToolExecutors();
const supportedArtifactTypes = getSupportedSummaryArtifactTypes(executors);
if (!supportedArtifactTypes.includes(artifact_type)) {
throw new Error(
`artifact_type must be one of: ${supportedArtifactTypes.join(", ")}`,
);
}
return runSerializedWorkflowOperation(() =>
executors.executeSummarySave({ milestone_id, slice_id, task_id, artifact_type, content }, projectDir),
);
},
);
server.tool(
"sf_task_complete",
"Record a completed task to the SF database and render its SUMMARY.md.",
taskCompleteParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(taskCompleteSchema, args);
const { projectDir, ...taskArgs } = parsed;
return handleTaskComplete(projectDir, taskArgs);
},
);
server.tool(
"sf_complete_task",
"Alias for sf_task_complete. Record a completed task to the SF database and render its SUMMARY.md.",
taskCompleteParams,
async (args: Record<string, unknown>) => {
const parsed = parseWorkflowArgs(taskCompleteSchema, args);
const { projectDir, ...taskArgs } = parsed;
return handleTaskComplete(projectDir, taskArgs);
},
);
server.tool(
"sf_milestone_status",
"Read the current status of a milestone and all its slices from the SF database.",
milestoneStatusParams,
async (args: Record<string, unknown>) => {
const { projectDir, milestoneId } = parseWorkflowArgs(milestoneStatusSchema, args);
await enforceWorkflowWriteGate("sf_milestone_status", projectDir, milestoneId);
const { executeMilestoneStatus } = await getWorkflowToolExecutors();
return runSerializedWorkflowOperation(() => executeMilestoneStatus({ milestoneId }, projectDir));
},
);
server.tool(
"sf_journal_query",
"Query the structured event journal for auto-mode iterations.",
journalQueryParams,
async (args: Record<string, unknown>) => {
const { projectDir, limit, ...filters } = parseWorkflowArgs(journalQuerySchema, args);
const { queryJournal } = await importLocalModule<any>("../../../src/resources/extensions/sf/journal.js");
const entries = queryJournal(projectDir, filters).slice(0, limit ?? 100);
if (entries.length === 0) {
return { content: [{ type: "text" as const, text: "No matching journal entries found." }] };
}
return { content: [{ type: "text" as const, text: JSON.stringify(entries, null, 2) }] };
},
);
}