fix(discuss): add multi-round questioning to new-project discuss phase
The discuss.md prompt (used for /gsd new project creation) only asked "What's the vision?" once and relied on LLM judgment for follow-ups. This led to the agent asking a single question then jumping straight to planning without gathering enough context. Add explicit Question Rounds section with: - 1-3 questions per round structure - Conditional ask_user_questions vs plain text support - Incremental persistence (CONTEXT-DRAFT save every 2 rounds) - Depth-matching rule (1-2 rounds for simple, 4+ for large visions) - Round cadence that drives toward the depth enforcement checklist Thread structuredQuestionsAvailable through buildDiscussPrompt() and prepareAndBuildDiscussPrompt() so the template variable resolves correctly at runtime. Closes #3976
This commit is contained in:
parent
40bcf84ee8
commit
6f27e514ca
3 changed files with 41 additions and 13 deletions
|
|
@ -426,8 +426,9 @@ function resolveAvailableModel<T extends { id: string; provider: string }>(
|
|||
* Build the discuss-and-plan prompt for a new milestone.
|
||||
* Used by all three "new milestone" paths (first ever, no active, all complete).
|
||||
*/
|
||||
function buildDiscussPrompt(nextId: string, preamble: string, _basePath: string, preparationContext?: string): string {
|
||||
function buildDiscussPrompt(nextId: string, preamble: string, _basePath: string, pi: ExtensionAPI, ctx: ExtensionCommandContext, preparationContext?: string): string {
|
||||
const milestoneRel = `.gsd/milestones/${nextId}`;
|
||||
const structuredQuestionsAvailable = getStructuredQuestionsAvailability(pi, ctx);
|
||||
const inlinedTemplates = [
|
||||
inlineTemplate("project", "Project"),
|
||||
inlineTemplate("requirements", "Requirements"),
|
||||
|
|
@ -439,6 +440,7 @@ function buildDiscussPrompt(nextId: string, preamble: string, _basePath: string,
|
|||
milestoneId: nextId,
|
||||
preamble,
|
||||
preparationContext: preparationContext ?? "",
|
||||
structuredQuestionsAvailable,
|
||||
contextPath: `${milestoneRel}/${nextId}-CONTEXT.md`,
|
||||
roadmapPath: `${milestoneRel}/${nextId}-ROADMAP.md`,
|
||||
inlinedTemplates,
|
||||
|
|
@ -486,6 +488,7 @@ function buildHeadlessDiscussPrompt(nextId: string, seedContext: string, _basePa
|
|||
*/
|
||||
async function prepareAndBuildDiscussPrompt(
|
||||
ctx: ExtensionCommandContext,
|
||||
pi: ExtensionAPI,
|
||||
nextId: string,
|
||||
preamble: string,
|
||||
basePath: string,
|
||||
|
|
@ -520,7 +523,7 @@ async function prepareAndBuildDiscussPrompt(
|
|||
}
|
||||
}
|
||||
|
||||
return buildDiscussPrompt(nextId, preamble, basePath, preparationContext);
|
||||
return buildDiscussPrompt(nextId, preamble, basePath, pi, ctx, preparationContext);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -780,7 +783,7 @@ export async function showDiscuss(
|
|||
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
||||
const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
|
||||
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: false, createdAt: Date.now() });
|
||||
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
|
||||
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -1185,7 +1188,7 @@ async function handleMilestoneActions(
|
|||
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
||||
const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
|
||||
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
|
||||
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId,
|
||||
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
||||
`New milestone ${nextId}.`,
|
||||
basePath
|
||||
), "gsd-run", ctx, "discuss-milestone");
|
||||
|
|
@ -1375,7 +1378,7 @@ export async function showSmartEntry(
|
|||
if (isFirst) {
|
||||
// First ever — skip wizard, just ask directly
|
||||
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
|
||||
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId,
|
||||
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
||||
`New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`,
|
||||
basePath
|
||||
), "gsd-run", ctx, "discuss-milestone");
|
||||
|
|
@ -1396,7 +1399,7 @@ export async function showSmartEntry(
|
|||
|
||||
if (choice === "new_milestone") {
|
||||
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
|
||||
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId,
|
||||
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
||||
`New milestone ${nextId}.`,
|
||||
basePath
|
||||
), "gsd-run", ctx, "discuss-milestone");
|
||||
|
|
@ -1435,7 +1438,7 @@ export async function showSmartEntry(
|
|||
const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
|
||||
|
||||
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
|
||||
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId,
|
||||
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
||||
`New milestone ${nextId}.`,
|
||||
basePath
|
||||
), "gsd-run", ctx, "discuss-milestone");
|
||||
|
|
@ -1502,7 +1505,7 @@ export async function showSmartEntry(
|
|||
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
||||
const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
|
||||
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
|
||||
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId,
|
||||
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
||||
`New milestone ${nextId}.`,
|
||||
basePath
|
||||
), "gsd-run", ctx, "discuss-milestone");
|
||||
|
|
@ -1599,7 +1602,7 @@ export async function showSmartEntry(
|
|||
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
||||
const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
|
||||
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
|
||||
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId,
|
||||
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
||||
`New milestone ${nextId}.`,
|
||||
basePath
|
||||
), "gsd-run", ctx, "discuss-milestone");
|
||||
|
|
|
|||
|
|
@ -49,6 +49,26 @@ This happens ONCE, before the first round. The goal: your first questions should
|
|||
|
||||
For subsequent rounds, continue investigating between rounds — check docs, search, or scout as needed to make each round's questions smarter. But the first-round investigation is mandatory and explicit. Distribute searches across turns rather than clustering them in one turn.
|
||||
|
||||
## Question Rounds
|
||||
|
||||
Ask **1–3 questions per round**. Keep each round tightly focused on one or two of the depth checklist dimensions — do not try to cover all six in one round.
|
||||
|
||||
**If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions` for each round. 1–3 questions per call, each as a separate question object. Keep option labels short (3–5 words). Always include a freeform "Other / let me explain" option. When the user picks that option or writes a long freeform answer, switch to plain text follow-up for that thread before resuming structured questions. **IMPORTANT: Call `ask_user_questions` exactly once per turn. Never make multiple calls with the same or overlapping questions — wait for the user's response before asking the next round.**
|
||||
|
||||
**If `{{structuredQuestionsAvailable}}` is `false`:** ask questions in plain text. Keep each round to 1–3 focused questions. Wait for answers before asking the next round.
|
||||
|
||||
After each answer set, investigate further if any answer opens a new unknown, then ask the next round.
|
||||
|
||||
### Round cadence
|
||||
|
||||
After each round of answers, decide whether you already have enough depth to write strong output.
|
||||
|
||||
- **Incremental persistence:** After every 2 question rounds, silently save a `{{milestoneId}}-CONTEXT-DRAFT.md` using `gsd_summary_save` with `artifact_type: "CONTEXT-DRAFT"` and `milestone_id: "{{milestoneId}}"`. This protects confirmed work against session crashes. Do NOT mention this save to the user.
|
||||
- If not ready, continue to the next round immediately. Do **not** ask a meta "ready to wrap up?" question after every round.
|
||||
- **Depth-matching rule:** Simple, well-defined work needs fewer rounds — maybe 1–2. Large, ambiguous visions need more — maybe 4+. Do not pad rounds to hit a number. Stop when the Depth Enforcement checklist below is fully satisfied.
|
||||
- Do not count the reflection step as a question round. Rounds start after reflection is confirmed.
|
||||
- When you genuinely believe the depth checklist is satisfied, move to the Depth Verification step below. Do not ask a separate "ready to wrap up?" gate — the depth verification IS the gate.
|
||||
|
||||
## Questioning Philosophy
|
||||
|
||||
You are a thinking partner, not an interviewer.
|
||||
|
|
@ -94,10 +114,6 @@ Do NOT offer to proceed until ALL of the following are satisfied. Track these in
|
|||
|
||||
Before offering to proceed, demonstrate absorption: reference specific things the user emphasized, specific terminology they used, specific nuance they sharpened — and show how those shaped your understanding. Synthesize, don't recite. "Your emphasis on X led me to prioritize Y over Z" is good. "You said X, you said Y, you said Z" is not. The user should feel heard in the specifics, not just acknowledged in the abstract.
|
||||
|
||||
**Questioning depth should match scope.** Simple, well-defined work needs fewer rounds — maybe 1-2. Large, ambiguous visions need more — maybe 4+. Don't pad rounds to hit a number. Stop when the depth checklist is satisfied and you genuinely understand the work.
|
||||
|
||||
Do not count the reflection step as a question round. Rounds start after reflection is confirmed.
|
||||
|
||||
## Depth Verification
|
||||
|
||||
Before moving to the wrap-up gate, present a structured depth summary as a checkpoint.
|
||||
|
|
|
|||
|
|
@ -27,10 +27,19 @@ describe("discuss incremental persistence (#2152)", () => {
|
|||
assert.match(content, /Incremental persistence/, "should have incremental persistence section");
|
||||
});
|
||||
|
||||
test("new-project discuss prompt includes CONTEXT-DRAFT save instruction", () => {
|
||||
const content = readFileSync(join(promptsDir, "discuss.md"), "utf-8");
|
||||
assert.match(content, /CONTEXT-DRAFT/, "should mention CONTEXT-DRAFT");
|
||||
assert.match(content, /Incremental persistence/, "should have incremental persistence section");
|
||||
assert.match(content, /gsd_summary_save/, "should use gsd_summary_save tool");
|
||||
});
|
||||
|
||||
test("drafts are saved silently without user notification", () => {
|
||||
const milestone = readFileSync(join(promptsDir, "guided-discuss-milestone.md"), "utf-8");
|
||||
const slice = readFileSync(join(promptsDir, "guided-discuss-slice.md"), "utf-8");
|
||||
const discuss = readFileSync(join(promptsDir, "discuss.md"), "utf-8");
|
||||
assert.match(milestone, /Do NOT mention this save to the user/);
|
||||
assert.match(slice, /Do NOT mention this to the user/);
|
||||
assert.match(discuss, /Do NOT mention this save to the user/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue