fix: add gsd_generate_milestone_id tool for multi-milestone unique ID generation (#818)

When unique_milestone_ids is enabled, the LLM cannot generate random
suffixes itself. Previously only the first milestone got a correct ID
(pre-generated in TS), while subsequent milestones in multi-milestone
projects got bare M002/M003 without suffixes.

Added a gsd_generate_milestone_id tool that the LLM calls to get each
milestone ID. The tool scans disk for existing milestones and respects
the unique_milestone_ids preference, making it impossible to produce
wrong-format IDs.

Updated discuss, discuss-headless, and queue prompts to instruct the
LLM to use the tool instead of inventing milestone IDs.
This commit is contained in:
Adam Dry 2026-03-17 13:47:11 +00:00 committed by GitHub
parent 7c449b8b73
commit 68edb39f9e
5 changed files with 49 additions and 9 deletions

View file

@ -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 <milestone list>"),

View file

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

View file

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

View file

@ -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/<ID>/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.

View file

@ -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/<ID>/slices`
1. Call `gsd_generate_milestone_id` to get the milestone ID — never invent milestone IDs manually. Then `mkdir -p .gsd/milestones/<ID>/slices`.
2. Write `.gsd/milestones/<ID>/<ID>-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
---