diff --git a/src/resources/extensions/gsd/guided-flow.ts b/src/resources/extensions/gsd/guided-flow.ts index 7202088ca..8025addcb 100644 --- a/src/resources/extensions/gsd/guided-flow.ts +++ b/src/resources/extensions/gsd/guided-flow.ts @@ -636,9 +636,11 @@ async function showQueueAdd( const existingContext = await buildExistingMilestonesContext(basePath, milestoneIds, state); // ── Determine next milestone ID ───────────────────────────────────── + // Note: the LLM will use the gsd_generate_milestone_id tool to get IDs + // at creation time, but we still mention the next ID in the preamble + // for context about where the sequence is. const uniqueEnabled = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids; const nextId = nextMilestoneId(milestoneIds, uniqueEnabled); - const nextIdPlus1 = nextMilestoneId([...milestoneIds, nextId], uniqueEnabled); // ── Build preamble ────────────────────────────────────────────────── const activePart = state.activeMilestone @@ -659,8 +661,6 @@ async function showQueueAdd( const queueInlinedTemplates = inlineTemplate("context", "Context"); const prompt = loadPrompt("queue", { preamble, - nextId, - nextIdPlus1, existingMilestonesContext: existingContext, inlinedTemplates: queueInlinedTemplates, commitInstruction: buildDocsCommitInstruction("docs: queue "), diff --git a/src/resources/extensions/gsd/index.ts b/src/resources/extensions/gsd/index.ts index f387f5f5f..fc6416a97 100644 --- a/src/resources/extensions/gsd/index.ts +++ b/src/resources/extensions/gsd/index.ts @@ -36,7 +36,7 @@ import { loadPrompt } from "./prompt-loader.js"; import { deriveState } from "./state.js"; import { isAutoActive, isAutoPaused, handleAgentEnd, pauseAuto, getAutoDashboardData, markToolStart, markToolEnd } from "./auto.js"; import { saveActivityLog } from "./activity-log.js"; -import { checkAutoStartAfterDiscuss, getDiscussionMilestoneId } from "./guided-flow.js"; +import { checkAutoStartAfterDiscuss, getDiscussionMilestoneId, findMilestoneIds, nextMilestoneId } from "./guided-flow.js"; import { GSDDashboardOverlay } from "./dashboard-overlay.js"; import { loadEffectiveGSDPreferences, @@ -467,6 +467,46 @@ export default function (pi: ExtensionAPI) { }, }); + // ── gsd_generate_milestone_id — canonical milestone ID generation ────── + // The LLM cannot generate random suffixes for unique_milestone_ids on its + // own. This tool calls back into the TS code that owns ID generation, + // ensuring the preference is always respected and IDs are always valid. + pi.registerTool({ + name: "gsd_generate_milestone_id", + label: "Generate Milestone ID", + description: + "Generate the next milestone ID for a new GSD milestone. " + + "Scans existing milestones on disk and respects the unique_milestone_ids preference. " + + "Always use this tool when creating a new milestone — never invent milestone IDs manually.", + promptSnippet: "Generate a valid milestone ID (respects unique_milestone_ids preference)", + promptGuidelines: [ + "ALWAYS call gsd_generate_milestone_id before creating a new milestone directory or writing milestone files.", + "Never invent or hardcode milestone IDs like M001, M002 — always use this tool.", + "Call it once per milestone you need to create. For multi-milestone projects, call it once for each milestone in sequence.", + "The tool returns the correct format based on project preferences (e.g. M001 or M001-r5jzab).", + ], + parameters: Type.Object({}), + async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) { + try { + const basePath = process.cwd(); + const existingIds = findMilestoneIds(basePath); + const uniqueEnabled = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids; + const newId = nextMilestoneId(existingIds, uniqueEnabled); + return { + content: [{ type: "text" as const, text: newId }], + details: { operation: "generate_milestone_id", id: newId, existingCount: existingIds.length, uniqueEnabled }, + }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return { + content: [{ type: "text" as const, text: `Error generating milestone ID: ${msg}` }], + isError: true, + details: { operation: "generate_milestone_id", error: msg }, + }; + } + }, + }); + // ── session_start: render branded GSD header + load tool keys + remote status ── pi.on("session_start", async (_event, ctx) => { // Theme access throws in RPC mode (no TUI) — header is decorative, skip it diff --git a/src/resources/extensions/gsd/prompts/discuss-headless.md b/src/resources/extensions/gsd/prompts/discuss-headless.md index 4a5afb0a2..fb908c9da 100644 --- a/src/resources/extensions/gsd/prompts/discuss-headless.md +++ b/src/resources/extensions/gsd/prompts/discuss-headless.md @@ -55,7 +55,7 @@ Use these templates exactly: 9. Say exactly: "Milestone {{milestoneId}} ready." **For multi-milestone**, write in this order: -1. Create all milestone directories: `mkdir -p .gsd/milestones/{M###}/slices` for each +1. For each milestone, call `gsd_generate_milestone_id` to get its ID — never invent milestone IDs manually. Then `mkdir -p .gsd/milestones//slices` for each. 2. Write `.gsd/PROJECT.md` — full vision across ALL milestones (using Project template) 3. Write `.gsd/REQUIREMENTS.md` — full capability contract (using Requirements template) 4. Seed `.gsd/DECISIONS.md` (using Decisions template) @@ -82,5 +82,5 @@ Use these templates exactly: - **Investigate before writing** — always scout the codebase first - **Use depends_on frontmatter** for multi-milestone sequences (the state machine reads this field to determine execution order) - **Anti-reduction rule** — if the spec describes a big vision, plan the big vision. Do not ask "what's the minimum viable version?" or reduce scope. Phase complex/risky work into later milestones — do not cut it. -- **Naming convention** — directories use bare IDs (`M001/`, `S01/`), files use ID-SUFFIX format (`M001-CONTEXT.md`, `M001-ROADMAP.md`) +- **Naming convention** — always use `gsd_generate_milestone_id` to get milestone IDs. Directories use bare IDs (e.g. `M001/` or `M001-r5jzab/`), files use ID-SUFFIX format (e.g. `M001-CONTEXT.md` or `M001-r5jzab-CONTEXT.md`). Never invent milestone IDs manually. - **End with "Milestone {{milestoneId}} ready."** — this triggers auto-start detection diff --git a/src/resources/extensions/gsd/prompts/discuss.md b/src/resources/extensions/gsd/prompts/discuss.md index de1f5a56f..3687d587c 100644 --- a/src/resources/extensions/gsd/prompts/discuss.md +++ b/src/resources/extensions/gsd/prompts/discuss.md @@ -211,7 +211,7 @@ Once the user confirms the milestone split: #### Phase 1: Shared artifacts -1. `mkdir -p .gsd/milestones/{{milestoneId}}/slices` for each milestone +1. For each milestone, call `gsd_generate_milestone_id` to get its ID — never invent milestone IDs manually. Then `mkdir -p .gsd/milestones//slices`. 2. Write `.gsd/PROJECT.md` — use the **Project** output template below. 3. Write `.gsd/REQUIREMENTS.md` — use the **Requirements** output template below. Capture Active, Deferred, Out of Scope, and any already Validated requirements. Later milestones may have provisional ownership where slice plans do not exist yet. 4. Seed `.gsd/DECISIONS.md` — use the **Decisions** output template below. diff --git a/src/resources/extensions/gsd/prompts/queue.md b/src/resources/extensions/gsd/prompts/queue.md index 55406fba7..69bdf5316 100644 --- a/src/resources/extensions/gsd/prompts/queue.md +++ b/src/resources/extensions/gsd/prompts/queue.md @@ -79,9 +79,9 @@ Determine where the new milestones should go in the overall sequence. Consider d ## Output Phase -Once the user is satisfied, in a single pass for **each** new milestone (starting from {{nextId}}): +Once the user is satisfied, in a single pass for **each** new milestone: -1. `mkdir -p .gsd/milestones//slices` +1. Call `gsd_generate_milestone_id` to get the milestone ID — never invent milestone IDs manually. Then `mkdir -p .gsd/milestones//slices`. 2. Write `.gsd/milestones//-CONTEXT.md` — use the **Context** output template below. Capture intent, scope, risks, constraints, integration points, and relevant requirements. Mark the status as "Queued — pending auto-mode execution." **If this milestone depends on other milestones, add YAML frontmatter with `depends_on`:** ```yaml ---