Merge pull request #3533 from NilsR0711/feat/queued-discuss-fast-path

feat(gsd): add fast path for queued milestone discussion
This commit is contained in:
Jeremy McSpadden 2026-04-07 07:17:53 -05:00 committed by GitHub
commit 41ceb95b66
4 changed files with 158 additions and 1 deletions

View file

@ -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

View file

@ -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<void> {
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);

View file

@ -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:

View file

@ -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<void>", fnStart);
const signature = source.slice(fnStart, signatureEnd + 16);
assert.ok(
signature.includes("opts") && signature.includes("fastPath"),
"dispatchDiscussForMilestone must accept opts: { fastPath?: boolean } parameter",
);
});
});