diff --git a/src/resources/extensions/gsd/guided-flow.ts b/src/resources/extensions/gsd/guided-flow.ts index d0f400448..016f5335b 100644 --- a/src/resources/extensions/gsd/guided-flow.ts +++ b/src/resources/extensions/gsd/guided-flow.ts @@ -480,7 +480,7 @@ async function buildDiscussSlicePrompt( sid: string, sTitle: string, base: string, - options?: { rediscuss?: boolean }, + options?: { rediscuss?: boolean; structuredQuestionsAvailable?: string }, ): Promise { const inlined: string[] = []; @@ -560,6 +560,7 @@ async function buildDiscussSlicePrompt( contextPath: sliceContextPath, projectRoot: base, inlinedTemplates, + structuredQuestionsAvailable: options?.structuredQuestionsAvailable ?? "false", commitInstruction: buildDocsCommitInstruction(`docs(${mid}/${sid}): slice context from discuss`), }); } @@ -801,7 +802,8 @@ export async function showDiscuss( if (confirm !== "rediscuss") continue; } - const prompt = await buildDiscussSlicePrompt(mid, chosen.id, chosen.title, basePath, { rediscuss: isRediscuss }); + const sqAvail = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false"; + const prompt = await buildDiscussSlicePrompt(mid, chosen.id, chosen.title, basePath, { rediscuss: isRediscuss, structuredQuestionsAvailable: sqAvail }); await dispatchWorkflow(pi, prompt, "gsd-discuss", ctx, "discuss-slice"); // Wait for the discuss session to finish, then loop back to the picker @@ -1514,7 +1516,8 @@ export async function showSmartEntry( }), }), "gsd-run", ctx, "plan-slice"); } else if (choice === "discuss") { - await dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }), "gsd-run", ctx, "discuss-slice"); + const sqAvail = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false"; + await dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext, structuredQuestionsAvailable: sqAvail }), "gsd-run", ctx, "discuss-slice"); } else if (choice === "research") { const researchTemplates = inlineTemplate("research", "Research"); await dispatchWorkflow(pi, loadPrompt("guided-research-slice", { diff --git a/src/resources/extensions/gsd/prompts/guided-discuss-slice.md b/src/resources/extensions/gsd/prompts/guided-discuss-slice.md index 63ec13fb7..353c5f831 100644 --- a/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +++ b/src/resources/extensions/gsd/prompts/guided-discuss-slice.md @@ -22,7 +22,9 @@ Do **not** go deep — just enough that your questions reflect what's actually t ### Question rounds -Ask **1–3 questions per round** using `ask_user_questions`. **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.** Keep each question focused on one of: +**If `{{structuredQuestionsAvailable}}` is `true`:** Ask **1–3 questions per round** using `ask_user_questions`. **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 **1–3 questions per round** in plain text. Number them and wait for the user's response before asking the next round. +Keep each question focused on one of: - **UX and user-facing behaviour** — what does the user see, click, trigger, or experience? - **Edge cases and failure states** — what happens when things go wrong or are in unusual states? - **Scope boundaries** — what is explicitly in vs out for this slice? What deferred to later? @@ -37,9 +39,7 @@ After each round of answers, decide whether you already have enough signal to wr - **Incremental persistence:** After every 2 question rounds, silently save a draft `{{sliceId}}-CONTEXT-DRAFT.md` in `{{sliceDirPath}}` using `gsd_summary_save` with `milestone_id: {{milestoneId}}`, `slice_id: {{sliceId}}`, `artifact_type: "CONTEXT-DRAFT"`. This protects against session crashes losing confirmed work. Do NOT mention this to the user. The final context file will replace it. - If not, investigate any new unknowns and continue to the next round immediately. Do **not** ask a meta "ready to wrap up?" question after every round. - Ask a single wrap-up question only when you genuinely believe the slice is well understood or the user signals they want to stop. -- When you do ask it, use `ask_user_questions` with: - - "Write the context file" *(recommended when the slice is well understood)* - - "One more pass" +- When you do ask it, offer two choices: "Write the context file" *(recommended when the slice is well understood)* or "One more pass". Use `ask_user_questions` if available, otherwise ask in plain text. --- diff --git a/src/resources/extensions/gsd/tests/discuss-slice-structured-questions.test.ts b/src/resources/extensions/gsd/tests/discuss-slice-structured-questions.test.ts new file mode 100644 index 000000000..a52114df6 --- /dev/null +++ b/src/resources/extensions/gsd/tests/discuss-slice-structured-questions.test.ts @@ -0,0 +1,46 @@ +/** + * Regression test for discuss-slice structured questions availability + * + * The guided-discuss-slice.md template must use the structuredQuestionsAvailable + * template variable to conditionally switch between ask_user_questions tool + * calls and plain-text questions, so the prompt works correctly when the + * structured questions tool is not available. + */ + +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +const template = readFileSync( + resolve(process.cwd(), 'src', 'resources', 'extensions', 'gsd', 'prompts', 'guided-discuss-slice.md'), + 'utf-8', +) + +describe('discuss-slice structuredQuestionsAvailable template variable', () => { + it('template references structuredQuestionsAvailable variable', () => { + assert.ok( + template.includes('{{structuredQuestionsAvailable}}'), + 'guided-discuss-slice.md must use {{structuredQuestionsAvailable}} template variable', + ) + }) + + it('template handles both true and false cases', () => { + const trueCase = template.includes('`{{structuredQuestionsAvailable}}` is `true`') + const falseCase = template.includes('`{{structuredQuestionsAvailable}}` is `false`') + + assert.ok(trueCase, 'template must have a branch for structuredQuestionsAvailable=true') + assert.ok(falseCase, 'template must have a branch for structuredQuestionsAvailable=false') + }) + + it('false case instructs plain text questions', () => { + const falseIdx = template.indexOf('`{{structuredQuestionsAvailable}}` is `false`') + assert.ok(falseIdx !== -1) + + const afterFalse = template.slice(falseIdx, falseIdx + 300) + assert.ok( + afterFalse.includes('plain text'), + 'when structuredQuestionsAvailable is false, questions should be in plain text', + ) + }) +})