From 32f800778b8ae69a0defa010c5df54136cfd6f5d Mon Sep 17 00:00:00 2001 From: Markus Skistad Date: Sat, 16 May 2026 07:47:38 +0200 Subject: [PATCH] fix(workflow): bypass guided discuss for custom runs --- src/resources/extensions/gsd/auto-start.ts | 11 ++- .../gsd/tests/deep-project-auto-loop.test.ts | 76 +++++++++++++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/resources/extensions/gsd/auto-start.ts b/src/resources/extensions/gsd/auto-start.ts index 512f0f4cd7..640229fd1b 100644 --- a/src/resources/extensions/gsd/auto-start.ts +++ b/src/resources/extensions/gsd/auto-start.ts @@ -1024,7 +1024,12 @@ export async function bootstrapAutoSession( s.currentMilestoneId = null; } - if (!hasSurvivorBranch && !deepProjectStagePending) { + // Custom workflow runs do not require an active milestone. They manage + // progression from GRAPH.yaml/runDir state instead of milestone planning + // state, so guided-flow discussion must not preempt them. + const isCustomWorkflowRun = !!s.activeEngineId && s.activeEngineId !== "dev"; + + if (!hasSurvivorBranch && !deepProjectStagePending && !isCustomWorkflowRun) { // No active work — start a new milestone via discuss flow if (!state.activeMilestone || state.phase === "complete") { // Guard against recursive dialog loop (#1348): @@ -1099,8 +1104,8 @@ export async function bootstrapAutoSession( } } - // Unreachable safety check - if (!state.activeMilestone && !deepProjectStagePending) { + // Unreachable safety check (also guarded by isCustomWorkflowRun above) + if (!state.activeMilestone && !deepProjectStagePending && !isCustomWorkflowRun) { const { showSmartEntry } = await import("./guided-flow.js"); await showSmartEntry(ctx, pi, base, { step: requestedStepMode }); return releaseLockAndReturn(); diff --git a/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts b/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts index c0a9e66736..5d811d8676 100644 --- a/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +++ b/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts @@ -140,6 +140,18 @@ function makeRepo(): string { return base; } +function makeLightRepo(): string { + const base = join(tmpdir(), `gsd-custom-workflow-bootstrap-${randomUUID()}`); + mkdirSync(join(base, ".gsd", "milestones"), { recursive: true }); + execFileSync("git", ["init"], { cwd: base, stdio: "ignore" }); + execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: base }); + execFileSync("git", ["config", "user.name", "Test"], { cwd: base }); + writeFileSync(join(base, "README.md"), "# test\n"); + execFileSync("git", ["add", "-A"], { cwd: base, stdio: "ignore" }); + execFileSync("git", ["commit", "-m", "init"], { cwd: base, stdio: "ignore" }); + return base; +} + function makeCtx(sessionId = "test-session") { const model = { provider: "claude-code", id: "claude-sonnet-4-6", contextWindow: 128000 }; return { @@ -311,6 +323,70 @@ test("deep project setup: bootstrap can start auto-mode without an active milest } }); +test("custom workflow bootstrap: no active milestone does not enter guided discuss", async () => { + const base = makeLightRepo(); + const previousCwd = process.cwd(); + const messages: unknown[] = []; + try { + process.chdir(base); + const s = new AutoSession(); + s.activeEngineId = "custom"; + s.activeRunDir = join(base, ".gsd", "workflow-runs", "test", "run"); + + const ready = await bootstrapAutoSession( + s, + makeCtx(`custom-${randomUUID()}`) as any, + { + ...makePi(messages), + getThinkingLevel: () => "medium", + } as any, + base, + false, + false, + { + shouldUseWorktreeIsolation: () => false, + registerSigtermHandler: () => {}, + registerAutoWorkerForSession: () => {}, + lockBase: () => base, + buildLifecycle: () => ({ + adoptSessionRoot: (sessionBase: string, originalBase?: string) => { + s.basePath = sessionBase; + if (originalBase !== undefined) { + s.originalBasePath = originalBase; + } else if (!s.originalBasePath) { + s.originalBasePath = sessionBase; + } + }, + }) as any, + }, + { + classification: "none", + lock: null, + pausedSession: null, + state: null, + recovery: null, + recoveryPrompt: null, + recoveryToolCallCount: 0, + artifactSatisfied: false, + hasResumableDiskState: false, + isBootstrapCrash: false, + }, + ); + + assert.equal(ready, true); + assert.equal(s.active, true); + assert.equal(s.currentMilestoneId, null); + assert.deepEqual(messages, [], "custom workflow bootstrap must not dispatch guided discuss"); + } finally { + process.chdir(previousCwd); + try { + const { closeDatabase } = await import("../gsd-db.ts"); + closeDatabase(); + } catch {} + rmSync(base, { recursive: true, force: true }); + } +}); + test("deep project setup: pre-dispatch can run before the first milestone exists", async () => { const base = makeBase(); try {