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:
commit
41ceb95b66
4 changed files with 158 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue