diff --git a/src/resources/extensions/gsd/auto-prompts.ts b/src/resources/extensions/gsd/auto-prompts.ts index 92ad153ec..7e9aa5aac 100644 --- a/src/resources/extensions/gsd/auto-prompts.ts +++ b/src/resources/extensions/gsd/auto-prompts.ts @@ -858,6 +858,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 diff --git a/src/resources/extensions/gsd/guided-flow.ts b/src/resources/extensions/gsd/guided-flow.ts index 6371f554f..3eb7eb243 100644 --- a/src/resources/extensions/gsd/guided-flow.ts +++ b/src/resources/extensions/gsd/guided-flow.ts @@ -657,6 +657,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}` @@ -670,6 +671,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); @@ -873,7 +875,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 }); } /** @@ -887,9 +918,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", { @@ -898,6 +941,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}` @@ -1326,6 +1370,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}` @@ -1339,6 +1384,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); @@ -1435,6 +1481,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 698690b7b..0d651e30c 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", + ); + }); +});