diff --git a/packages/mcp-server/src/workflow-tools.test.ts b/packages/mcp-server/src/workflow-tools.test.ts index 4f50f490a..35a883b3b 100644 --- a/packages/mcp-server/src/workflow-tools.test.ts +++ b/packages/mcp-server/src/workflow-tools.test.ts @@ -27,6 +27,26 @@ function cleanup(base: string): void { } } +function writeWriteGateSnapshot( + base: string, + snapshot: { verifiedDepthMilestones?: string[]; activeQueuePhase?: boolean; pendingGateId?: string | null }, +): void { + mkdirSync(join(base, ".gsd", "runtime"), { recursive: true }); + writeFileSync( + join(base, ".gsd", "runtime", "write-gate-state.json"), + JSON.stringify( + { + verifiedDepthMilestones: snapshot.verifiedDepthMilestones ?? [], + activeQueuePhase: snapshot.activeQueuePhase ?? false, + pendingGateId: snapshot.pendingGateId ?? null, + }, + null, + 2, + ), + "utf-8", + ); +} + function makeMockServer() { const tools: Array<{ name: string; @@ -176,6 +196,72 @@ describe("workflow MCP tools", () => { } }); + it("blocks workflow mutation tools while a discussion gate is pending", async () => { + const base = makeTmpBase(); + try { + mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01"), { recursive: true }); + writeFileSync( + join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md"), + "# S01\n\n- [ ] **T01: Demo** `est:5m`\n", + ); + writeWriteGateSnapshot(base, { pendingGateId: "depth_verification_M001_confirm" }); + + const server = makeMockServer(); + registerWorkflowTools(server as any); + const taskTool = server.tools.find((t) => t.name === "gsd_task_complete"); + assert.ok(taskTool, "task tool should be registered"); + + await assert.rejects( + () => + taskTool!.handler({ + projectDir: base, + taskId: "T01", + sliceId: "S01", + milestoneId: "M001", + oneLiner: "Completed task", + narrative: "Did the work", + verification: "npm test", + }), + /Discussion gate .* has not been confirmed/, + ); + } finally { + cleanup(base); + } + }); + + it("blocks workflow mutation tools during queue mode", async () => { + const base = makeTmpBase(); + try { + mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01"), { recursive: true }); + writeFileSync( + join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md"), + "# S01\n\n- [ ] **T01: Demo** `est:5m`\n", + ); + writeWriteGateSnapshot(base, { activeQueuePhase: true }); + + const server = makeMockServer(); + registerWorkflowTools(server as any); + const taskTool = server.tools.find((t) => t.name === "gsd_task_complete"); + assert.ok(taskTool, "task tool should be registered"); + + await assert.rejects( + () => + taskTool!.handler({ + projectDir: base, + taskId: "T01", + sliceId: "S01", + milestoneId: "M001", + oneLiner: "Completed task", + narrative: "Did the work", + verification: "npm test", + }), + /planning tool .* not executes work|Cannot gsd_task_complete|Unknown tools are not permitted during queue mode/, + ); + } finally { + cleanup(base); + } + }); + it("gsd_task_complete and gsd_milestone_status work end-to-end", async () => { const base = makeTmpBase(); try { diff --git a/packages/mcp-server/src/workflow-tools.ts b/packages/mcp-server/src/workflow-tools.ts index 72e84f31b..d06cba00b 100644 --- a/packages/mcp-server/src/workflow-tools.ts +++ b/packages/mcp-server/src/workflow-tools.ts @@ -216,8 +216,37 @@ type WorkflowToolExecutors = { ) => Promise; }; +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 }; +}; + let workflowToolExecutorsPromise: Promise | null = null; let workflowExecutionQueue: Promise = Promise.resolve(); +let workflowWriteGatePromise: Promise | null = null; function getAllowedProjectRoot(env: NodeJS.ProcessEnv = process.env): string | null { const configuredRoot = env.GSD_WORKFLOW_PROJECT_ROOT?.trim(); @@ -285,6 +314,13 @@ function getSupportedSummaryArtifactTypes(executors: WorkflowToolExecutors): rea return executors.SUPPORTED_SUMMARY_ARTIFACT_TYPES; } +function getWriteGateModuleCandidates(): string[] { + return [ + new URL("../../../src/resources/extensions/gsd/bootstrap/write-gate.js", import.meta.url).href, + new URL("../../../src/resources/extensions/gsd/bootstrap/write-gate.ts", import.meta.url).href, + ]; +} + function toFileUrl(modulePath: string): string { return pathToFileURL(resolve(modulePath)).href; } @@ -334,6 +370,36 @@ async function getWorkflowToolExecutors(): Promise { return workflowToolExecutorsPromise; } +async function getWorkflowWriteGateModule(): Promise { + 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 GSD write-gate bridge for workflow MCP tools. " + + `Attempts: ${attempts.join("; ")}`, + ); + })(); + } + return workflowWriteGatePromise; +} + interface McpToolServer { tool( name: string, @@ -360,10 +426,39 @@ async function runSerializedWorkflowOperation(fn: () => Promise): Promise< } } +async function enforceWorkflowWriteGate( + toolName: string, + projectDir: string, + milestoneId: string | null = null, +): Promise { + 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, "projectDir">, ): Promise { + await enforceWorkflowWriteGate("gsd_task_complete", projectDir, args.milestoneId); const { taskId, sliceId, @@ -404,6 +499,7 @@ async function handleSliceComplete( projectDir: string, args: z.infer, ): Promise { + await enforceWorkflowWriteGate("gsd_slice_complete", projectDir, args.milestoneId); const { executeSliceComplete } = await getWorkflowToolExecutors(); const { projectDir: _projectDir, ...params } = args; return runSerializedWorkflowOperation(() => executeSliceComplete(params, projectDir)); @@ -413,6 +509,7 @@ async function handleReplanSlice( projectDir: string, args: z.infer, ): Promise { + await enforceWorkflowWriteGate("gsd_replan_slice", projectDir, args.milestoneId); const { executeReplanSlice } = await getWorkflowToolExecutors(); const { projectDir: _projectDir, ...params } = args; return runSerializedWorkflowOperation(() => executeReplanSlice(params, projectDir)); @@ -422,6 +519,7 @@ async function handleCompleteMilestone( projectDir: string, args: z.infer, ): Promise { + await enforceWorkflowWriteGate("gsd_complete_milestone", projectDir, args.milestoneId); const { executeCompleteMilestone } = await getWorkflowToolExecutors(); const { projectDir: _projectDir, ...params } = args; return runSerializedWorkflowOperation(() => executeCompleteMilestone(params, projectDir)); @@ -431,6 +529,7 @@ async function handleValidateMilestone( projectDir: string, args: z.infer, ): Promise { + await enforceWorkflowWriteGate("gsd_validate_milestone", projectDir, args.milestoneId); const { executeValidateMilestone } = await getWorkflowToolExecutors(); const { projectDir: _projectDir, ...params } = args; return runSerializedWorkflowOperation(() => executeValidateMilestone(params, projectDir)); @@ -440,6 +539,7 @@ async function handleReassessRoadmap( projectDir: string, args: z.infer, ): Promise { + await enforceWorkflowWriteGate("gsd_reassess_roadmap", projectDir, args.milestoneId); const { executeReassessRoadmap } = await getWorkflowToolExecutors(); const { projectDir: _projectDir, ...params } = args; return runSerializedWorkflowOperation(() => executeReassessRoadmap(params, projectDir)); @@ -449,6 +549,7 @@ async function handleSaveGateResult( projectDir: string, args: z.infer, ): Promise { + await enforceWorkflowWriteGate("gsd_save_gate_result", projectDir, args.milestoneId); const { executeSaveGateResult } = await getWorkflowToolExecutors(); const { projectDir: _projectDir, ...params } = args; return runSerializedWorkflowOperation(() => executeSaveGateResult(params, projectDir)); @@ -699,6 +800,7 @@ export function registerWorkflowTools(server: McpToolServer): void { async (args: Record) => { const parsed = parseWorkflowArgs(planMilestoneSchema, args); const { projectDir, ...params } = parsed; + await enforceWorkflowWriteGate("gsd_plan_milestone", projectDir, params.milestoneId); const { executePlanMilestone } = await getWorkflowToolExecutors(); return runSerializedWorkflowOperation(() => executePlanMilestone(params, projectDir)); }, @@ -711,6 +813,7 @@ export function registerWorkflowTools(server: McpToolServer): void { async (args: Record) => { const parsed = parseWorkflowArgs(planSliceSchema, args); const { projectDir, ...params } = parsed; + await enforceWorkflowWriteGate("gsd_plan_slice", projectDir, params.milestoneId); const { executePlanSlice } = await getWorkflowToolExecutors(); return runSerializedWorkflowOperation(() => executePlanSlice(params, projectDir)); }, @@ -833,6 +936,7 @@ export function registerWorkflowTools(server: McpToolServer): void { async (args: Record) => { const parsed = parseWorkflowArgs(summarySaveSchema, args); const { projectDir, milestone_id, slice_id, task_id, artifact_type, content } = parsed; + await enforceWorkflowWriteGate("gsd_summary_save", projectDir, milestone_id); const executors = await getWorkflowToolExecutors(); const supportedArtifactTypes = getSupportedSummaryArtifactTypes(executors); if (!supportedArtifactTypes.includes(artifact_type)) { @@ -874,6 +978,7 @@ export function registerWorkflowTools(server: McpToolServer): void { milestoneStatusParams, async (args: Record) => { const { projectDir, milestoneId } = parseWorkflowArgs(milestoneStatusSchema, args); + await enforceWorkflowWriteGate("gsd_milestone_status", projectDir, milestoneId); const { executeMilestoneStatus } = await getWorkflowToolExecutors(); return runSerializedWorkflowOperation(() => executeMilestoneStatus({ milestoneId }, projectDir)); }, diff --git a/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts b/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts index a709005f5..0c84440a8 100644 --- a/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +++ b/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts @@ -153,6 +153,7 @@ describe("stream-adapter — session persistence (#2859)", () => { args: ["packages/mcp-server/dist/cli.js"], env: { GSD_CLI_PATH: "/tmp/gsd", + GSD_PERSIST_WRITE_GATE_STATE: "1", GSD_WORKFLOW_PROJECT_ROOT: "/tmp/project", }, cwd: "/tmp/project", @@ -230,6 +231,7 @@ describe("stream-adapter — session persistence (#2859)", () => { args: [realpathSync(resolve(repoDir, "packages", "mcp-server", "dist", "cli.js"))], env: { GSD_CLI_PATH: "/tmp/gsd", + GSD_PERSIST_WRITE_GATE_STATE: "1", GSD_WORKFLOW_PROJECT_ROOT: resolvedRepoDir, }, cwd: resolvedRepoDir, diff --git a/src/resources/extensions/gsd/bootstrap/write-gate.ts b/src/resources/extensions/gsd/bootstrap/write-gate.ts index 959c8a78f..0215faae8 100644 --- a/src/resources/extensions/gsd/bootstrap/write-gate.ts +++ b/src/resources/extensions/gsd/bootstrap/write-gate.ts @@ -1,3 +1,6 @@ +import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + const MILESTONE_CONTEXT_RE = /M\d+(?:-[a-z0-9]{6})?-CONTEXT\.md$/; const CONTEXT_MILESTONE_RE = /(?:^|[/\\])(M\d+(?:-[a-z0-9]{6})?)-CONTEXT\.md$/i; const DEPTH_VERIFICATION_MILESTONE_RE = /depth_verification[_-](M\d+(?:-[a-z0-9]{6})?)/i; @@ -65,6 +68,69 @@ const GATE_SAFE_TOOLS = new Set([ "search_and_read", ]); +export interface WriteGateSnapshot { + verifiedDepthMilestones: string[]; + activeQueuePhase: boolean; + pendingGateId: string | null; +} + +function shouldPersistWriteGateSnapshot(env: NodeJS.ProcessEnv = process.env): boolean { + return env.GSD_PERSIST_WRITE_GATE_STATE === "1"; +} + +function writeGateSnapshotPath(basePath: string = process.cwd()): string { + return join(basePath, ".gsd", "runtime", "write-gate-state.json"); +} + +function currentWriteGateSnapshot(): WriteGateSnapshot { + return { + verifiedDepthMilestones: [...verifiedDepthMilestones].sort(), + activeQueuePhase, + pendingGateId, + }; +} + +function persistWriteGateSnapshot(basePath: string = process.cwd()): void { + if (!shouldPersistWriteGateSnapshot()) return; + const path = writeGateSnapshotPath(basePath); + mkdirSync(join(basePath, ".gsd", "runtime"), { recursive: true }); + const tempPath = `${path}.tmp`; + writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(), null, 2), "utf-8"); + renameSync(tempPath, path); +} + +function clearPersistedWriteGateSnapshot(basePath: string = process.cwd()): void { + if (!shouldPersistWriteGateSnapshot()) return; + const path = writeGateSnapshotPath(basePath); + try { + unlinkSync(path); + } catch { + // swallow + } +} + +function normalizeWriteGateSnapshot(value: unknown): WriteGateSnapshot { + const record = value && typeof value === "object" ? value as Record : {}; + const verified = Array.isArray(record.verifiedDepthMilestones) + ? record.verifiedDepthMilestones.filter((item): item is string => typeof item === "string") + : []; + return { + verifiedDepthMilestones: [...new Set(verified)].sort(), + activeQueuePhase: record.activeQueuePhase === true, + pendingGateId: typeof record.pendingGateId === "string" ? record.pendingGateId : null, + }; +} + +export function loadWriteGateSnapshot(basePath: string = process.cwd()): WriteGateSnapshot { + const path = writeGateSnapshotPath(basePath); + if (!existsSync(path)) return currentWriteGateSnapshot(); + try { + return normalizeWriteGateSnapshot(JSON.parse(readFileSync(path, "utf-8"))); + } catch { + return currentWriteGateSnapshot(); + } +} + export function isDepthVerified(): boolean { return verifiedDepthMilestones.size > 0; } @@ -77,28 +143,40 @@ export function isMilestoneDepthVerified(milestoneId: string | null | undefined) return verifiedDepthMilestones.has(milestoneId); } +export function isMilestoneDepthVerifiedInSnapshot( + snapshot: WriteGateSnapshot, + milestoneId: string | null | undefined, +): boolean { + if (!milestoneId) return false; + return snapshot.verifiedDepthMilestones.includes(milestoneId); +} + export function isQueuePhaseActive(): boolean { return activeQueuePhase; } export function setQueuePhaseActive(active: boolean): void { activeQueuePhase = active; + persistWriteGateSnapshot(); } export function resetWriteGateState(): void { verifiedDepthMilestones.clear(); pendingGateId = null; + persistWriteGateSnapshot(); } export function clearDiscussionFlowState(): void { verifiedDepthMilestones.clear(); activeQueuePhase = false; pendingGateId = null; + clearPersistedWriteGateSnapshot(); } -export function markDepthVerified(milestoneId?: string | null): void { +export function markDepthVerified(milestoneId?: string | null, basePath: string = process.cwd()): void { if (!milestoneId) return; verifiedDepthMilestones.add(milestoneId); + persistWriteGateSnapshot(basePath); } /** @@ -130,6 +208,7 @@ function extractContextMilestoneId(inputPath: string): string | null { */ export function setPendingGate(gateId: string): void { pendingGateId = gateId; + persistWriteGateSnapshot(); } /** @@ -137,6 +216,7 @@ export function setPendingGate(gateId: string): void { */ export function clearPendingGate(): void { pendingGateId = null; + persistWriteGateSnapshot(); } /** @@ -154,11 +234,20 @@ export function getPendingGate(): string | null { * Read-only tools and ask_user_questions itself are always allowed. */ export function shouldBlockPendingGate( + toolName: string, + milestoneId: string | null, + queuePhaseActive?: boolean, +): { block: boolean; reason?: string } { + return shouldBlockPendingGateInSnapshot(currentWriteGateSnapshot(), toolName, milestoneId, queuePhaseActive); +} + +export function shouldBlockPendingGateInSnapshot( + snapshot: WriteGateSnapshot, toolName: string, _milestoneId: string | null, _queuePhaseActive?: boolean, ): { block: boolean; reason?: string } { - if (!pendingGateId) return { block: false }; + if (!snapshot.pendingGateId) return { block: false }; if (GATE_SAFE_TOOLS.has(toolName)) return { block: false }; @@ -168,7 +257,7 @@ export function shouldBlockPendingGate( return { block: true, reason: [ - `HARD BLOCK: Discussion gate "${pendingGateId}" has not been confirmed by the user.`, + `HARD BLOCK: Discussion gate "${snapshot.pendingGateId}" has not been confirmed by the user.`, `You MUST re-call ask_user_questions with the gate question before making any other tool calls.`, `If the previous ask_user_questions call failed, errored, was cancelled, or the user's response`, `did not match a provided option, you MUST re-ask — never rationalize past the block.`, @@ -182,11 +271,20 @@ export function shouldBlockPendingGate( * Read-only bash commands are allowed; mutating commands are blocked. */ export function shouldBlockPendingGateBash( + command: string, + milestoneId: string | null, + queuePhaseActive?: boolean, +): { block: boolean; reason?: string } { + return shouldBlockPendingGateBashInSnapshot(currentWriteGateSnapshot(), command, milestoneId, queuePhaseActive); +} + +export function shouldBlockPendingGateBashInSnapshot( + snapshot: WriteGateSnapshot, command: string, _milestoneId: string | null, _queuePhaseActive?: boolean, ): { block: boolean; reason?: string } { - if (!pendingGateId) return { block: false }; + if (!snapshot.pendingGateId) return { block: false }; // Allow read-only bash commands if (BASH_READ_ONLY_RE.test(command)) return { block: false }; @@ -194,7 +292,7 @@ export function shouldBlockPendingGateBash( return { block: true, reason: [ - `HARD BLOCK: Discussion gate "${pendingGateId}" has not been confirmed by the user.`, + `HARD BLOCK: Discussion gate "${snapshot.pendingGateId}" has not been confirmed by the user.`, `You MUST re-call ask_user_questions with the gate question before running mutating commands.`, `If the previous ask_user_questions call failed, errored, was cancelled, or the user's response`, `did not match a provided option, you MUST re-ask — never rationalize past the block.`, @@ -275,6 +373,15 @@ export function shouldBlockContextArtifactSave( artifactType: string, milestoneId: string | null, sliceId?: string | null, +): { block: boolean; reason?: string } { + return shouldBlockContextArtifactSaveInSnapshot(currentWriteGateSnapshot(), artifactType, milestoneId, sliceId); +} + +export function shouldBlockContextArtifactSaveInSnapshot( + snapshot: WriteGateSnapshot, + artifactType: string, + milestoneId: string | null, + sliceId?: string | null, ): { block: boolean; reason?: string } { if (artifactType !== "CONTEXT") return { block: false }; if (sliceId) return { block: false }; @@ -287,7 +394,7 @@ export function shouldBlockContextArtifactSave( ].join(" "), }; } - if (isMilestoneDepthVerified(milestoneId)) return { block: false }; + if (isMilestoneDepthVerifiedInSnapshot(snapshot, milestoneId)) return { block: false }; return { block: true, @@ -317,6 +424,15 @@ export function shouldBlockQueueExecution( toolName: string, input: string, queuePhaseActive: boolean, +): { block: boolean; reason?: string } { + return shouldBlockQueueExecutionInSnapshot(currentWriteGateSnapshot(), toolName, input, queuePhaseActive); +} + +export function shouldBlockQueueExecutionInSnapshot( + snapshot: WriteGateSnapshot, + toolName: string, + input: string, + queuePhaseActive: boolean = snapshot.activeQueuePhase, ): { block: boolean; reason?: string } { if (!queuePhaseActive) return { block: false }; diff --git a/src/resources/extensions/gsd/tests/workflow-mcp.test.ts b/src/resources/extensions/gsd/tests/workflow-mcp.test.ts index 24cf135fd..97cb3b3c1 100644 --- a/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +++ b/src/resources/extensions/gsd/tests/workflow-mcp.test.ts @@ -46,6 +46,7 @@ test("detectWorkflowMcpLaunchConfig prefers explicit env override", () => { env: { FOO: "bar", GSD_CLI_PATH: "/tmp/gsd", + GSD_PERSIST_WRITE_GATE_STATE: "1", GSD_WORKFLOW_PROJECT_ROOT: "/tmp/project", }, }); @@ -62,6 +63,7 @@ test("buildWorkflowMcpServers mirrors explicit launch config", () => { command: "node", args: ["dist/cli.js"], env: { + GSD_PERSIST_WRITE_GATE_STATE: "1", GSD_WORKFLOW_PROJECT_ROOT: "/tmp/project", }, }, diff --git a/src/resources/extensions/gsd/tools/workflow-tool-executors.ts b/src/resources/extensions/gsd/tools/workflow-tool-executors.ts index 036c0b326..edc1bfd31 100644 --- a/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +++ b/src/resources/extensions/gsd/tools/workflow-tool-executors.ts @@ -1,6 +1,6 @@ import { ensureDbOpen } from "../bootstrap/dynamic-tools.js"; import { sanitizeCompleteMilestoneParams } from "../bootstrap/sanitize-complete-milestone.js"; -import { shouldBlockContextArtifactSave } from "../bootstrap/write-gate.js"; +import { loadWriteGateSnapshot, shouldBlockContextArtifactSaveInSnapshot } from "../bootstrap/write-gate.js"; import { getMilestone, getSliceStatusSummary, @@ -65,7 +65,8 @@ export async function executeSummarySave( details: { operation: "save_summary", error: "invalid_artifact_type" }, }; } - const contextGuard = shouldBlockContextArtifactSave( + const contextGuard = shouldBlockContextArtifactSaveInSnapshot( + loadWriteGateSnapshot(basePath), params.artifact_type, params.milestone_id ?? null, params.slice_id ?? null, diff --git a/src/resources/extensions/gsd/workflow-mcp.ts b/src/resources/extensions/gsd/workflow-mcp.ts index 44f972e46..a4b5047cc 100644 --- a/src/resources/extensions/gsd/workflow-mcp.ts +++ b/src/resources/extensions/gsd/workflow-mcp.ts @@ -85,6 +85,7 @@ export function detectWorkflowMcpLaunchConfig( const launchEnv = { ...(explicitEnv ?? {}), ...(env.GSD_CLI_PATH ? { GSD_CLI_PATH: env.GSD_CLI_PATH } : {}), + GSD_PERSIST_WRITE_GATE_STATE: "1", GSD_WORKFLOW_PROJECT_ROOT: resolve(workflowProjectRoot), }; return { @@ -105,6 +106,7 @@ export function detectWorkflowMcpLaunchConfig( cwd: projectRoot, env: { ...(env.GSD_CLI_PATH ? { GSD_CLI_PATH: env.GSD_CLI_PATH } : {}), + GSD_PERSIST_WRITE_GATE_STATE: "1", GSD_WORKFLOW_PROJECT_ROOT: resolve(projectRoot), }, }; @@ -117,6 +119,7 @@ export function detectWorkflowMcpLaunchConfig( command: binPath, env: { ...(env.GSD_CLI_PATH ? { GSD_CLI_PATH: env.GSD_CLI_PATH } : {}), + GSD_PERSIST_WRITE_GATE_STATE: "1", GSD_WORKFLOW_PROJECT_ROOT: resolve(projectRoot), }, };