From d572e372a1b816acfb5cbec60f7dcfa9f2a3c5b5 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 11 Apr 2026 09:51:03 -0500 Subject: [PATCH 1/2] fix(mcp): return isError flag on workflow tool execution failures This fixes an issue where MCP workflow tools (e.g. gsd_plan_slice) would return error details in their JSON response, but without setting the 'isError: true' flag at the top level of the tool response payload. This caused MCP clients (like Claude Code) to interpret failed validations (like empty tasks arrays) as successes and get stuck in infinite validation failure loops. --- .../gsd/tools/workflow-tool-executors.ts | 85 +++++++++++++------ 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/src/resources/extensions/gsd/tools/workflow-tool-executors.ts b/src/resources/extensions/gsd/tools/workflow-tool-executors.ts index edc1bfd31..14f179bff 100644 --- a/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +++ b/src/resources/extensions/gsd/tools/workflow-tool-executors.ts @@ -38,6 +38,7 @@ export function isSupportedSummaryArtifactType( export interface ToolExecutionResult { content: Array<{ type: "text"; text: string }>; details: Record; + isError?: boolean; } export interface SummarySaveParams { @@ -57,13 +58,15 @@ export async function executeSummarySave( return { content: [{ type: "text", text: "Error: GSD database is not available. Cannot save artifact." }], details: { operation: "save_summary", error: "db_unavailable" }, - }; + isError: true, + }; } if (!isSupportedSummaryArtifactType(params.artifact_type)) { return { content: [{ type: "text", text: `Error: Invalid artifact_type "${params.artifact_type}". Must be one of: ${SUPPORTED_SUMMARY_ARTIFACT_TYPES.join(", ")}` }], details: { operation: "save_summary", error: "invalid_artifact_type" }, - }; + isError: true, + }; } const contextGuard = shouldBlockContextArtifactSaveInSnapshot( loadWriteGateSnapshot(basePath), @@ -75,7 +78,8 @@ export async function executeSummarySave( return { content: [{ type: "text", text: `Error saving artifact: ${contextGuard.reason ?? "context write blocked"}` }], details: { operation: "save_summary", error: "context_write_blocked" }, - }; + isError: true, + }; } try { let relativePath: string; @@ -108,7 +112,8 @@ export async function executeSummarySave( return { content: [{ type: "text", text: `Error saving artifact: ${msg}` }], details: { operation: "save_summary", error: msg }, - }; + isError: true, + }; } } @@ -163,7 +168,8 @@ export async function executeTaskComplete( return { content: [{ type: "text", text: "Error: GSD database is not available. Cannot complete task." }], details: { operation: "complete_task", error: "db_unavailable" }, - }; + isError: true, + }; } try { const coerced = { ...params }; @@ -176,6 +182,7 @@ export async function executeTaskComplete( return { content: [{ type: "text", text: `Error completing task: ${result.error}` }], details: { operation: "complete_task", error: result.error }, + isError: true, }; } return { @@ -194,7 +201,8 @@ export async function executeTaskComplete( return { content: [{ type: "text", text: `Error completing task: ${msg}` }], details: { operation: "complete_task", error: msg }, - }; + isError: true, + }; } } @@ -207,7 +215,8 @@ export async function executeSliceComplete( return { content: [{ type: "text", text: "Error: GSD database is not available. Cannot complete slice." }], details: { operation: "complete_slice", error: "db_unavailable" }, - }; + isError: true, + }; } try { const splitPair = (s: string): [string, string] => { @@ -257,6 +266,7 @@ export async function executeSliceComplete( return { content: [{ type: "text", text: `Error completing slice: ${result.error}` }], details: { operation: "complete_slice", error: result.error }, + isError: true, }; } return { @@ -275,7 +285,8 @@ export async function executeSliceComplete( return { content: [{ type: "text", text: `Error completing slice: ${msg}` }], details: { operation: "complete_slice", error: msg }, - }; + isError: true, + }; } } @@ -288,7 +299,8 @@ export async function executeCompleteMilestone( return { content: [{ type: "text", text: "Error: GSD database is not available. Cannot complete milestone." }], details: { operation: "complete_milestone", error: "db_unavailable" }, - }; + isError: true, + }; } try { const sanitized = sanitizeCompleteMilestoneParams(params); @@ -297,6 +309,7 @@ export async function executeCompleteMilestone( return { content: [{ type: "text", text: `Error completing milestone: ${result.error}` }], details: { operation: "complete_milestone", error: result.error }, + isError: true, }; } return { @@ -313,7 +326,8 @@ export async function executeCompleteMilestone( return { content: [{ type: "text", text: `Error completing milestone: ${msg}` }], details: { operation: "complete_milestone", error: msg }, - }; + isError: true, + }; } } @@ -326,7 +340,8 @@ export async function executeValidateMilestone( return { content: [{ type: "text", text: "Error: GSD database is not available. Cannot validate milestone." }], details: { operation: "validate_milestone", error: "db_unavailable" }, - }; + isError: true, + }; } try { const result = await handleValidateMilestone(params, basePath); @@ -334,6 +349,7 @@ export async function executeValidateMilestone( return { content: [{ type: "text", text: `Error validating milestone: ${result.error}` }], details: { operation: "validate_milestone", error: result.error }, + isError: true, }; } return { @@ -351,7 +367,8 @@ export async function executeValidateMilestone( return { content: [{ type: "text", text: `Error validating milestone: ${msg}` }], details: { operation: "validate_milestone", error: msg }, - }; + isError: true, + }; } } @@ -364,7 +381,8 @@ export async function executeReassessRoadmap( return { content: [{ type: "text", text: "Error: GSD database is not available. Cannot reassess roadmap." }], details: { operation: "reassess_roadmap", error: "db_unavailable" }, - }; + isError: true, + }; } try { const result = await handleReassessRoadmap(params, basePath); @@ -372,6 +390,7 @@ export async function executeReassessRoadmap( return { content: [{ type: "text", text: `Error reassessing roadmap: ${result.error}` }], details: { operation: "reassess_roadmap", error: result.error }, + isError: true, }; } return { @@ -390,7 +409,8 @@ export async function executeReassessRoadmap( return { content: [{ type: "text", text: `Error reassessing roadmap: ${msg}` }], details: { operation: "reassess_roadmap", error: msg }, - }; + isError: true, + }; } } @@ -403,7 +423,8 @@ export async function executeSaveGateResult( return { content: [{ type: "text", text: "Error: GSD database is not available." }], details: { operation: "save_gate_result", error: "db_unavailable" }, - }; + isError: true, + }; } const validGates = ["Q3", "Q4", "Q5", "Q6", "Q7", "Q8"]; @@ -411,7 +432,8 @@ export async function executeSaveGateResult( return { content: [{ type: "text", text: `Error: Invalid gateId "${params.gateId}". Must be one of: ${validGates.join(", ")}` }], details: { operation: "save_gate_result", error: "invalid_gate_id" }, - }; + isError: true, + }; } const validVerdicts = ["pass", "flag", "omitted"]; @@ -419,7 +441,8 @@ export async function executeSaveGateResult( return { content: [{ type: "text", text: `Error: Invalid verdict "${params.verdict}". Must be one of: ${validVerdicts.join(", ")}` }], details: { operation: "save_gate_result", error: "invalid_verdict" }, - }; + isError: true, + }; } try { @@ -443,7 +466,8 @@ export async function executeSaveGateResult( return { content: [{ type: "text", text: `Error saving gate result: ${msg}` }], details: { operation: "save_gate_result", error: msg }, - }; + isError: true, + }; } } @@ -456,7 +480,8 @@ export async function executePlanMilestone( return { content: [{ type: "text", text: "Error: GSD database is not available. Cannot plan milestone." }], details: { operation: "plan_milestone", error: "db_unavailable" }, - }; + isError: true, + }; } try { const result = await handlePlanMilestone(params, basePath); @@ -464,6 +489,7 @@ export async function executePlanMilestone( return { content: [{ type: "text", text: `Error planning milestone: ${result.error}` }], details: { operation: "plan_milestone", error: result.error }, + isError: true, }; } return { @@ -480,7 +506,8 @@ export async function executePlanMilestone( return { content: [{ type: "text", text: `Error planning milestone: ${msg}` }], details: { operation: "plan_milestone", error: msg }, - }; + isError: true, + }; } } @@ -493,7 +520,8 @@ export async function executePlanSlice( return { content: [{ type: "text", text: "Error: GSD database is not available. Cannot plan slice." }], details: { operation: "plan_slice", error: "db_unavailable" }, - }; + isError: true, + }; } try { const result = await handlePlanSlice(params, basePath); @@ -501,6 +529,7 @@ export async function executePlanSlice( return { content: [{ type: "text", text: `Error planning slice: ${result.error}` }], details: { operation: "plan_slice", error: result.error }, + isError: true, }; } return { @@ -519,7 +548,8 @@ export async function executePlanSlice( return { content: [{ type: "text", text: `Error planning slice: ${msg}` }], details: { operation: "plan_slice", error: msg }, - }; + isError: true, + }; } } @@ -532,7 +562,8 @@ export async function executeReplanSlice( return { content: [{ type: "text", text: "Error: GSD database is not available. Cannot replan slice." }], details: { operation: "replan_slice", error: "db_unavailable" }, - }; + isError: true, + }; } try { const result = await handleReplanSlice(params, basePath); @@ -540,6 +571,7 @@ export async function executeReplanSlice( return { content: [{ type: "text", text: `Error replanning slice: ${result.error}` }], details: { operation: "replan_slice", error: result.error }, + isError: true, }; } return { @@ -558,7 +590,8 @@ export async function executeReplanSlice( return { content: [{ type: "text", text: `Error replanning slice: ${msg}` }], details: { operation: "replan_slice", error: msg }, - }; + isError: true, + }; } } @@ -576,6 +609,7 @@ export async function executeMilestoneStatus( return { content: [{ type: "text", text: "Error: GSD database is not available." }], details: { operation: "milestone_status", error: "db_unavailable" }, + isError: true, }; } @@ -624,6 +658,7 @@ export async function executeMilestoneStatus( return { content: [{ type: "text", text: `Error querying milestone status: ${msg}` }], details: { operation: "milestone_status", error: msg }, - }; + isError: true, + }; } } From 818bf97c36a6364a20db4c61783f1de338225a6e Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 11 Apr 2026 10:18:20 -0500 Subject: [PATCH 2/2] test(gsd): add regression for plan_slice isError failures --- .../gsd/tests/workflow-tool-executors.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts b/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts index 06c01c419..327f51759 100644 --- a/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +++ b/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts @@ -256,6 +256,28 @@ test("executePlanSlice writes task planning state and rendered plan artifacts", } }); +test("executePlanSlice marks validation failures with isError", async () => { + const base = makeTmpBase(); + try { + openTestDb(base); + + const result = await inProjectDir(base, () => executePlanSlice({ + milestoneId: "M001", + sliceId: "S01", + goal: "Trigger validation failure for empty tasks.", + tasks: [], + }, base)); + + assert.equal(result.isError, true); + assert.equal(result.details.operation, "plan_slice"); + assert.match(String(result.details.error), /validation failed: tasks must be a non-empty array/); + assert.match(result.content[0].text, /Error planning slice:/); + } finally { + closeDatabase(); + cleanup(base); + } +}); + test("executeSliceComplete coerces string enrichment entries and writes summary/UAT artifacts", async () => { const base = makeTmpBase(); try {