From 5a221410dc8fc22bb10bf3c3d9c128188dbef2eb Mon Sep 17 00:00:00 2001 From: Nils Reeh Date: Sun, 5 Apr 2026 07:43:39 +0200 Subject: [PATCH 1/2] feat(gsd): add fast path for queued milestone discussion - Add {{fastPathInstruction}} template variable to guided-discuss-milestone.md - dispatchDiscussForMilestone accepts opts.fastPath and computes fast path instruction - showDiscussQueuedMilestone shows full/fast mode picker; auto fast-paths when draft exists - Add fastPathInstruction: "" to all other loadPrompt call sites for the same template - Add queued-discuss-fast-path.test.ts with 6 source-reading tests --- src/resources/extensions/gsd/guided-flow.ts | 49 +++++++- .../gsd/prompts/guided-discuss-milestone.md | 2 + .../tests/queued-discuss-fast-path.test.ts | 107 ++++++++++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 src/resources/extensions/gsd/tests/queued-discuss-fast-path.test.ts diff --git a/src/resources/extensions/gsd/guided-flow.ts b/src/resources/extensions/gsd/guided-flow.ts index c6fdb2ea9..bd1b6feaf 100644 --- a/src/resources/extensions/gsd/guided-flow.ts +++ b/src/resources/extensions/gsd/guided-flow.ts @@ -569,6 +569,7 @@ export async function showDiscuss( const basePrompt = loadPrompt("guided-discuss-milestone", { milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable, commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`), + fastPathInstruction: "", }); const seed = draftContent ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}` @@ -582,6 +583,7 @@ export async function showDiscuss( await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", { milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable, commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`), + fastPathInstruction: "", }), "gsd-discuss", ctx, "discuss-milestone"); } else if (choice === "skip_milestone") { const milestoneIds = findMilestoneIds(basePath); @@ -767,7 +769,36 @@ async function showDiscussQueuedMilestone( const chosen = pendingMilestones.find(m => m.id === choice); if (!chosen) return; - await dispatchDiscussForMilestone(ctx, pi, basePath, chosen.id, chosen.title); + const hasDraft = !!resolveMilestoneFile(basePath, chosen.id, "CONTEXT-DRAFT"); + let fastPath = hasDraft; + + if (!hasDraft) { + const mode = await showNextAction(ctx, { + title: `Discuss ${chosen.id}`, + summary: [ + "Choose how to start the discussion.", + "Fast path skips generic scouting — use it when you already know the scope.", + ], + actions: [ + { + id: "full", + label: "Full discussion", + description: "Scout the codebase, ask open-ended questions, explore deeply", + recommended: true, + }, + { + id: "fast", + label: "I have the scope — fast path", + description: "Treat your first message as authoritative seed context; skip scouting", + }, + ], + notYetMessage: "Run /gsd discuss when ready.", + }); + if (mode === "not_yet") return; + fastPath = mode === "fast"; + } + + await dispatchDiscussForMilestone(ctx, pi, basePath, chosen.id, chosen.title, { fastPath }); } /** @@ -781,9 +812,21 @@ async function dispatchDiscussForMilestone( basePath: string, mid: string, milestoneTitle: string, + opts: { fastPath?: boolean } = {}, ): Promise { const draftFile = resolveMilestoneFile(basePath, mid, "CONTEXT-DRAFT"); const draftContent = draftFile ? await loadFile(draftFile) : null; + const hasSeed = !!(draftContent || opts.fastPath); + const fastPathInstruction = hasSeed + ? [ + "> **Fast path active — scope provided.**", + "> Do NOT perform a generic codebase scouting pass.", + "> Do at most 2 targeted reads to check for obvious conflicts with existing work.", + "> Treat the seed context or the operator's first message as authoritative.", + "> Move directly to the depth summary and write step.", + "> Ask only questions where the answer would materially change scope.", + ].join("\n") + : ""; const discussMilestoneTemplates = inlineTemplate("context", "Context"); const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false"; const basePrompt = loadPrompt("guided-discuss-milestone", { @@ -792,6 +835,7 @@ async function dispatchDiscussForMilestone( inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable, commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`), + fastPathInstruction, }); const prompt = draftContent ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}` @@ -1190,6 +1234,7 @@ export async function showSmartEntry( const basePrompt = loadPrompt("guided-discuss-milestone", { milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable, commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`), + fastPathInstruction: "", }); const seed = draftContent ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}` @@ -1203,6 +1248,7 @@ export async function showSmartEntry( await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", { milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable, commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`), + fastPathInstruction: "", }), "gsd-discuss", ctx, "discuss-milestone"); } else if (choice === "skip_milestone") { const milestoneIds = findMilestoneIds(basePath); @@ -1287,6 +1333,7 @@ export async function showSmartEntry( await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", { milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable, commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`), + fastPathInstruction: "", }), "gsd-run", ctx, "discuss-milestone"); } else if (choice === "skip_milestone") { const milestoneIds = findMilestoneIds(basePath); diff --git a/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md b/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md index b8746d1d1..d79fc6a01 100644 --- a/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +++ b/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md @@ -8,6 +8,8 @@ Discuss milestone {{milestoneId}} ("{{milestoneTitle}}"). Identify gray areas, a ## Interview Protocol +{{fastPathInstruction}} + ### Before your first question round Do a lightweight targeted investigation so your questions are grounded in reality: diff --git a/src/resources/extensions/gsd/tests/queued-discuss-fast-path.test.ts b/src/resources/extensions/gsd/tests/queued-discuss-fast-path.test.ts new file mode 100644 index 000000000..75b249485 --- /dev/null +++ b/src/resources/extensions/gsd/tests/queued-discuss-fast-path.test.ts @@ -0,0 +1,107 @@ +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function guidedFlowSrc(): string { + return readFileSync(join(__dirname, "..", "guided-flow.ts"), "utf-8"); +} + +function promptSrc(): string { + return readFileSync(join(__dirname, "..", "prompts", "guided-discuss-milestone.md"), "utf-8"); +} + +describe("queued-discuss-fast-path", () => { + test("1. guided-discuss-milestone.md contains {{fastPathInstruction}}", () => { + const prompt = promptSrc(); + assert.ok( + prompt.includes("{{fastPathInstruction}}"), + "guided-discuss-milestone.md must contain {{fastPathInstruction}} template variable", + ); + }); + + test("2. dispatchDiscussForMilestone computes fastPathInstruction and passes it to loadPrompt", () => { + const source = guidedFlowSrc(); + const fnStart = source.indexOf("async function dispatchDiscussForMilestone("); + assert.ok(fnStart > 0, "dispatchDiscussForMilestone must exist"); + const fnEnd = source.indexOf("\nasync function ", fnStart + 1); + const fnBody = fnEnd > 0 ? source.slice(fnStart, fnEnd) : source.slice(fnStart, fnStart + 2000); + assert.ok( + fnBody.includes("fastPathInstruction"), + "dispatchDiscussForMilestone must compute fastPathInstruction", + ); + assert.ok( + fnBody.includes("loadPrompt("), + "dispatchDiscussForMilestone must call loadPrompt", + ); + const loadPromptIdx = fnBody.indexOf("loadPrompt("); + const fastPathIdx = fnBody.indexOf("fastPathInstruction", loadPromptIdx); + assert.ok( + fastPathIdx > loadPromptIdx, + "fastPathInstruction must be passed to loadPrompt in dispatchDiscussForMilestone", + ); + }); + + test("3. fast path instruction mentions scouting and conflict checking", () => { + const source = guidedFlowSrc(); + assert.ok( + source.includes("scouting pass"), + "fast path instruction must mention scouting pass", + ); + assert.ok( + source.includes("conflicts with existing work"), + "fast path instruction must mention conflict checking", + ); + }); + + test("4. showDiscussQueuedMilestone shows a mode picker when no draft", () => { + const source = guidedFlowSrc(); + const fnStart = source.indexOf("async function showDiscussQueuedMilestone("); + assert.ok(fnStart > 0, "showDiscussQueuedMilestone must exist"); + const fnEnd = source.indexOf("\nasync function ", fnStart + 1); + const fnBody = fnEnd > 0 ? source.slice(fnStart, fnEnd) : source.slice(fnStart, fnStart + 3000); + assert.ok( + fnBody.includes("hasDraft"), + "showDiscussQueuedMilestone must check hasDraft", + ); + assert.ok( + fnBody.includes('"full"') || fnBody.includes("\"full\""), + "showDiscussQueuedMilestone must offer a 'full' discussion mode", + ); + assert.ok( + fnBody.includes('"fast"') || fnBody.includes("\"fast\""), + "showDiscussQueuedMilestone must offer a 'fast' path mode", + ); + }); + + test("5. showDiscussQueuedMilestone fast-paths automatically when draft exists", () => { + const source = guidedFlowSrc(); + const fnStart = source.indexOf("async function showDiscussQueuedMilestone("); + assert.ok(fnStart > 0, "showDiscussQueuedMilestone must exist"); + const fnEnd = source.indexOf("\nasync function ", fnStart + 1); + const fnBody = fnEnd > 0 ? source.slice(fnStart, fnEnd) : source.slice(fnStart, fnStart + 3000); + assert.ok( + fnBody.includes("let fastPath = hasDraft"), + "showDiscussQueuedMilestone must set fastPath = hasDraft so draft presence auto-enables fast path", + ); + assert.ok( + fnBody.includes("if (!hasDraft)"), + "showDiscussQueuedMilestone must skip the mode picker when hasDraft is true", + ); + }); + + test("6. dispatchDiscussForMilestone accepts opts with fastPath parameter", () => { + const source = guidedFlowSrc(); + const fnStart = source.indexOf("async function dispatchDiscussForMilestone("); + assert.ok(fnStart > 0, "dispatchDiscussForMilestone must exist"); + const signatureEnd = source.indexOf("): Promise", fnStart); + const signature = source.slice(fnStart, signatureEnd + 16); + assert.ok( + signature.includes("opts") && signature.includes("fastPath"), + "dispatchDiscussForMilestone must accept opts: { fastPath?: boolean } parameter", + ); + }); +}); From e20313e4216f1f5acaf2bb5f9b48e9922836988b Mon Sep 17 00:00:00 2001 From: Nils Reeh Date: Sun, 5 Apr 2026 08:07:42 +0200 Subject: [PATCH 2/2] fix(gsd): add fastPathInstruction to buildDiscussMilestonePrompt loadPrompt call The guided-discuss-milestone.md template now declares {{fastPathInstruction}} but buildDiscussMilestonePrompt in auto-prompts.ts was not passing the variable, causing loadPrompt to throw a GSDError at runtime. Co-Authored-By: Claude Sonnet 4.6 --- src/resources/extensions/gsd/auto-prompts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/resources/extensions/gsd/auto-prompts.ts b/src/resources/extensions/gsd/auto-prompts.ts index 1ea0e3366..4ac8176a6 100644 --- a/src/resources/extensions/gsd/auto-prompts.ts +++ b/src/resources/extensions/gsd/auto-prompts.ts @@ -849,6 +849,7 @@ export async function buildDiscussMilestonePrompt(mid: string, midTitle: string, inlinedTemplates: discussTemplates, structuredQuestionsAvailable: "true", commitInstruction: "Do not commit planning artifacts — .gsd/ is managed externally.", + fastPathInstruction: "", }); // If a CONTEXT-DRAFT.md exists, append it as seed material