feat: add headless new-milestone command for programmatic milestone creation (#781)
Enables fully headless project creation from specification documents via `gsd headless new-milestone --context spec.md`. Supports file input, stdin piping, inline text, and optional auto-mode chaining with --auto. Key changes: - headless.ts: new CLI flags (--context, --context-text, --auto, --verbose), context loading (file/stdin/inline), .gsd/ bootstrapping, auto-mode chaining - commands.ts: /gsd new-milestone command routing via headless context temp file - guided-flow.ts: showHeadlessMilestoneCreation(), bootstrapGsdProject(), buildHeadlessDiscussPrompt() for non-interactive milestone creation - prompts/discuss-headless.md: headless variant of discuss.md that skips Q&A rounds and works entirely from the provided specification - help-text.ts: documentation for new-milestone subcommand and flags Closes #765 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8dcb2f9195
commit
69d37d3196
5 changed files with 331 additions and 20 deletions
138
src/headless.ts
138
src/headless.ts
|
|
@ -11,8 +11,8 @@
|
|||
* 2 — blocked (command reported a blocker)
|
||||
*/
|
||||
|
||||
import { existsSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { ChildProcess } from 'node:child_process'
|
||||
|
||||
// RpcClient is not in @gsd/pi-coding-agent's public exports — import from dist directly.
|
||||
|
|
@ -29,6 +29,10 @@ export interface HeadlessOptions {
|
|||
model?: string
|
||||
command: string
|
||||
commandArgs: string[]
|
||||
context?: string // file path or '-' for stdin
|
||||
contextText?: string // inline text
|
||||
auto?: boolean // chain into auto-mode after milestone creation
|
||||
verbose?: boolean // show tool calls in output
|
||||
}
|
||||
|
||||
interface ExtensionUIRequest {
|
||||
|
|
@ -80,6 +84,14 @@ export function parseHeadlessArgs(argv: string[]): HeadlessOptions {
|
|||
} else if (arg === '--model' && i + 1 < args.length) {
|
||||
// --model can also be passed from the main CLI; headless-specific takes precedence
|
||||
options.model = args[++i]
|
||||
} else if (arg === '--context' && i + 1 < args.length) {
|
||||
options.context = args[++i]
|
||||
} else if (arg === '--context-text' && i + 1 < args.length) {
|
||||
options.contextText = args[++i]
|
||||
} else if (arg === '--auto') {
|
||||
options.auto = true
|
||||
} else if (arg === '--verbose') {
|
||||
options.verbose = true
|
||||
}
|
||||
} else if (!positionalStarted) {
|
||||
positionalStarted = true
|
||||
|
|
@ -144,22 +156,26 @@ function handleExtensionUIRequest(
|
|||
// Progress Formatter
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function formatProgress(event: Record<string, unknown>): string | null {
|
||||
function formatProgress(event: Record<string, unknown>, verbose: boolean): string | null {
|
||||
const type = String(event.type ?? '')
|
||||
|
||||
switch (type) {
|
||||
case 'tool_execution_start':
|
||||
return `[tool] ${event.toolName ?? 'unknown'}`
|
||||
if (verbose) return ` [tool] ${event.toolName ?? 'unknown'}`
|
||||
return null
|
||||
|
||||
case 'agent_start':
|
||||
return '[agent] Session started'
|
||||
return '[agent] Session started'
|
||||
|
||||
case 'agent_end':
|
||||
return '[agent] Session ended'
|
||||
return '[agent] Session ended'
|
||||
|
||||
case 'extension_ui_request':
|
||||
if (event.method === 'notify') {
|
||||
return `[gsd] ${event.message ?? ''}`
|
||||
return `[gsd] ${event.message ?? ''}`
|
||||
}
|
||||
if (event.method === 'setStatus') {
|
||||
return `[status] ${event.message ?? ''}`
|
||||
}
|
||||
return null
|
||||
|
||||
|
|
@ -186,6 +202,11 @@ function isBlockedNotification(event: Record<string, unknown>): boolean {
|
|||
return String(event.message ?? '').toLowerCase().includes('blocked')
|
||||
}
|
||||
|
||||
function isMilestoneReadyNotification(event: Record<string, unknown>): boolean {
|
||||
if (event.type !== 'extension_ui_request' || event.method !== 'notify') return false
|
||||
return /milestone\s+m\d+.*ready/i.test(String(event.message ?? ''))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Quick Command Detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -205,12 +226,76 @@ function isQuickCommand(command: string): boolean {
|
|||
// Main Orchestrator
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context Loading (new-milestone)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function readStdin(): Promise<string> {
|
||||
const chunks: Buffer[] = []
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(chunk as Buffer)
|
||||
}
|
||||
return Buffer.concat(chunks).toString('utf-8')
|
||||
}
|
||||
|
||||
async function loadContext(options: HeadlessOptions): Promise<string> {
|
||||
if (options.contextText) return options.contextText
|
||||
if (options.context === '-') {
|
||||
return readStdin()
|
||||
}
|
||||
if (options.context) {
|
||||
return readFileSync(resolve(options.context), 'utf-8')
|
||||
}
|
||||
throw new Error('No context provided. Use --context <file> or --context-text <text>')
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap .gsd/ directory structure for headless new-milestone.
|
||||
* Mirrors the bootstrap logic from guided-flow.ts showSmartEntry().
|
||||
*/
|
||||
function bootstrapGsdProject(basePath: string): void {
|
||||
const gsdDir = join(basePath, '.gsd')
|
||||
mkdirSync(join(gsdDir, 'milestones'), { recursive: true })
|
||||
mkdirSync(join(gsdDir, 'runtime'), { recursive: true })
|
||||
}
|
||||
|
||||
export async function runHeadless(options: HeadlessOptions): Promise<void> {
|
||||
const startTime = Date.now()
|
||||
const isNewMilestone = options.command === 'new-milestone'
|
||||
|
||||
// Validate .gsd/ directory
|
||||
// For new-milestone, load context and bootstrap .gsd/ before spawning RPC child
|
||||
if (isNewMilestone) {
|
||||
if (!options.context && !options.contextText) {
|
||||
process.stderr.write('[headless] Error: new-milestone requires --context <file> or --context-text <text>\n')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
let contextContent: string
|
||||
try {
|
||||
contextContent = await loadContext(options)
|
||||
} catch (err) {
|
||||
process.stderr.write(`[headless] Error loading context: ${err instanceof Error ? err.message : String(err)}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Bootstrap .gsd/ if needed
|
||||
const gsdDir = join(process.cwd(), '.gsd')
|
||||
if (!existsSync(gsdDir)) {
|
||||
if (!options.json) {
|
||||
process.stderr.write('[headless] Bootstrapping .gsd/ project structure...\n')
|
||||
}
|
||||
bootstrapGsdProject(process.cwd())
|
||||
}
|
||||
|
||||
// Write context to temp file for the RPC child to read
|
||||
const runtimeDir = join(gsdDir, 'runtime')
|
||||
mkdirSync(runtimeDir, { recursive: true })
|
||||
writeFileSync(join(runtimeDir, 'headless-context.md'), contextContent, 'utf-8')
|
||||
}
|
||||
|
||||
// Validate .gsd/ directory (skip for new-milestone since we just bootstrapped it)
|
||||
const gsdDir = join(process.cwd(), '.gsd')
|
||||
if (!existsSync(gsdDir)) {
|
||||
if (!isNewMilestone && !existsSync(gsdDir)) {
|
||||
process.stderr.write('[headless] Error: No .gsd/ directory found in current directory.\n')
|
||||
process.stderr.write("[headless] Run 'gsd' interactively first to initialize a project.\n")
|
||||
process.exit(1)
|
||||
|
|
@ -240,6 +325,7 @@ export async function runHeadless(options: HeadlessOptions): Promise<void> {
|
|||
let blocked = false
|
||||
let completed = false
|
||||
let exitCode = 0
|
||||
let milestoneReady = false // tracks "Milestone X ready." for auto-chaining
|
||||
const recentEvents: TrackedEvent[] = []
|
||||
|
||||
function trackEvent(event: Record<string, unknown>): void {
|
||||
|
|
@ -302,7 +388,7 @@ export async function runHeadless(options: HeadlessOptions): Promise<void> {
|
|||
process.stdout.write(JSON.stringify(eventObj) + '\n')
|
||||
} else {
|
||||
// Progress output to stderr
|
||||
const line = formatProgress(eventObj)
|
||||
const line = formatProgress(eventObj, !!options.verbose)
|
||||
if (line) process.stderr.write(line + '\n')
|
||||
}
|
||||
|
||||
|
|
@ -312,6 +398,12 @@ export async function runHeadless(options: HeadlessOptions): Promise<void> {
|
|||
if (isBlockedNotification(eventObj)) {
|
||||
blocked = true
|
||||
}
|
||||
|
||||
// Detect "Milestone X ready." for auto-mode chaining
|
||||
if (isMilestoneReadyNotification(eventObj)) {
|
||||
milestoneReady = true
|
||||
}
|
||||
|
||||
if (isTerminalNotification(eventObj)) {
|
||||
completed = true
|
||||
}
|
||||
|
|
@ -400,6 +492,32 @@ export async function runHeadless(options: HeadlessOptions): Promise<void> {
|
|||
await completionPromise
|
||||
}
|
||||
|
||||
// Auto-mode chaining: if --auto and milestone creation succeeded, send /gsd auto
|
||||
if (isNewMilestone && options.auto && milestoneReady && !blocked && exitCode === 0) {
|
||||
if (!options.json) {
|
||||
process.stderr.write('[headless] Milestone ready — chaining into auto-mode...\n')
|
||||
}
|
||||
|
||||
// Reset completion state for the auto-mode phase
|
||||
completed = false
|
||||
milestoneReady = false
|
||||
blocked = false
|
||||
const autoCompletionPromise = new Promise<void>((resolve) => {
|
||||
resolveCompletion = resolve
|
||||
})
|
||||
|
||||
try {
|
||||
await client.prompt('/gsd auto')
|
||||
} catch (err) {
|
||||
process.stderr.write(`[headless] Error: Failed to start auto-mode: ${err instanceof Error ? err.message : String(err)}\n`)
|
||||
exitCode = 1
|
||||
}
|
||||
|
||||
if (exitCode === 0 || exitCode === 2) {
|
||||
await autoCompletionPromise
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
clearTimeout(timeoutTimer)
|
||||
if (idleTimer) clearTimeout(idleTimer)
|
||||
|
|
|
|||
|
|
@ -38,15 +38,30 @@ const SUBCOMMAND_HELP: Record<string, string> = {
|
|||
'Run /gsd commands without the TUI. Default command: auto',
|
||||
'',
|
||||
'Flags:',
|
||||
' --timeout N Overall timeout in ms (default: 300000)',
|
||||
' --json JSONL event stream to stdout',
|
||||
' --model ID Override model',
|
||||
' --timeout N Overall timeout in ms (default: 300000)',
|
||||
' --json JSONL event stream to stdout',
|
||||
' --model ID Override model',
|
||||
'',
|
||||
'Commands:',
|
||||
' auto Run all queued units continuously (default)',
|
||||
' next Run one unit',
|
||||
' status Show progress dashboard',
|
||||
' new-milestone Create a milestone from a specification document',
|
||||
'',
|
||||
'new-milestone flags:',
|
||||
' --context <path> Path to spec/PRD file (use \'-\' for stdin)',
|
||||
' --context-text <txt> Inline specification text',
|
||||
' --auto Start auto-mode after milestone creation',
|
||||
' --verbose Show tool calls in progress output',
|
||||
'',
|
||||
'Examples:',
|
||||
' gsd headless Run /gsd auto',
|
||||
' gsd headless next Run one unit',
|
||||
' gsd headless --json status Machine-readable status',
|
||||
' gsd headless --timeout 60000 With 1-minute timeout',
|
||||
' gsd headless Run /gsd auto',
|
||||
' gsd headless next Run one unit',
|
||||
' gsd headless --json status Machine-readable status',
|
||||
' gsd headless --timeout 60000 With 1-minute timeout',
|
||||
' gsd headless new-milestone --context spec.md Create milestone from file',
|
||||
' cat spec.md | gsd headless new-milestone --context - From stdin',
|
||||
' gsd headless new-milestone --context spec.md --auto Create + auto-execute',
|
||||
'',
|
||||
'Exit codes: 0 = complete, 1 = error/timeout, 2 = blocked',
|
||||
].join('\n'),
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@
|
|||
|
||||
import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
||||
import { AuthStorage } from "@gsd/pi-coding-agent";
|
||||
import { existsSync, readFileSync, mkdirSync } from "node:fs";
|
||||
import { existsSync, readFileSync, mkdirSync, unlinkSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { enableDebug, isDebugEnabled } from "./debug-logger.js";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { deriveState } from "./state.js";
|
||||
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
||||
import { GSDVisualizerOverlay } from "./visualizer-overlay.js";
|
||||
import { showQueue, showDiscuss } from "./guided-flow.js";
|
||||
import { showQueue, showDiscuss, showHeadlessMilestoneCreation } from "./guided-flow.js";
|
||||
import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote, dispatchDirectPhase } from "./auto.js";
|
||||
import { resolveProjectRoot } from "./worktree.js";
|
||||
import { appendCapture, hasPendingCaptures, loadPendingCaptures } from "./captures.js";
|
||||
|
|
@ -77,7 +77,7 @@ function projectRoot(): string {
|
|||
|
||||
export function registerGSDCommand(pi: ExtensionAPI): void {
|
||||
pi.registerCommand("gsd", {
|
||||
description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge|parallel",
|
||||
description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge|new-milestone|parallel",
|
||||
getArgumentCompletions: (prefix: string) => {
|
||||
const subcommands = [
|
||||
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
||||
|
|
@ -111,6 +111,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
{ cmd: "steer", desc: "Hard-steer plan documents during execution" },
|
||||
{ cmd: "inspect", desc: "Show SQLite DB diagnostics" },
|
||||
{ cmd: "knowledge", desc: "Add persistent project knowledge (rule, pattern, or lesson)" },
|
||||
{ cmd: "new-milestone", desc: "Create a milestone from a specification document (headless)" },
|
||||
{ cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge)" },
|
||||
];
|
||||
const parts = prefix.trim().split(/\s+/);
|
||||
|
|
@ -463,6 +464,21 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "new-milestone") {
|
||||
const basePath = projectRoot();
|
||||
const headlessContextPath = join(basePath, ".gsd", "runtime", "headless-context.md");
|
||||
if (existsSync(headlessContextPath)) {
|
||||
const seedContext = readFileSync(headlessContextPath, "utf-8");
|
||||
try { unlinkSync(headlessContextPath); } catch { /* non-fatal */ }
|
||||
await showHeadlessMilestoneCreation(ctx, pi, basePath, seedContext);
|
||||
} else {
|
||||
// No headless context — fall back to interactive smart entry
|
||||
const { showSmartEntry } = await import("./guided-flow.js");
|
||||
await showSmartEntry(ctx, pi, basePath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("capture ") || trimmed === "capture") {
|
||||
await handleCapture(trimmed.replace(/^capture\s*/, "").trim(), ctx);
|
||||
return;
|
||||
|
|
@ -583,6 +599,7 @@ function showHelp(ctx: ExtensionCommandContext): void {
|
|||
" /gsd stop Stop auto-mode gracefully",
|
||||
" /gsd pause Pause auto-mode (preserves state, /gsd auto to resume)",
|
||||
" /gsd discuss Start guided milestone/slice discussion",
|
||||
" /gsd new-milestone Create milestone from headless context (used by gsd headless)",
|
||||
"",
|
||||
"VISIBILITY",
|
||||
" /gsd status Show progress dashboard (Ctrl+Alt+G)",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,81 @@ function buildDiscussPrompt(nextId: string, preamble: string, _basePath: string)
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the discuss prompt for headless milestone creation.
|
||||
* Uses the discuss-headless prompt template with seed context injected.
|
||||
*/
|
||||
function buildHeadlessDiscussPrompt(nextId: string, seedContext: string, _basePath: string): string {
|
||||
const milestoneRel = `.gsd/milestones/${nextId}`;
|
||||
const inlinedTemplates = [
|
||||
inlineTemplate("project", "Project"),
|
||||
inlineTemplate("requirements", "Requirements"),
|
||||
inlineTemplate("context", "Context"),
|
||||
inlineTemplate("roadmap", "Roadmap"),
|
||||
inlineTemplate("decisions", "Decisions"),
|
||||
].join("\n\n---\n\n");
|
||||
return loadPrompt("discuss-headless", {
|
||||
milestoneId: nextId,
|
||||
seedContext,
|
||||
contextPath: `${milestoneRel}/${nextId}-CONTEXT.md`,
|
||||
roadmapPath: `${milestoneRel}/${nextId}-ROADMAP.md`,
|
||||
inlinedTemplates,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap a .gsd/ project from scratch for headless use.
|
||||
* Ensures git repo, .gsd/ structure, gitignore, and preferences all exist.
|
||||
*/
|
||||
function bootstrapGsdProject(basePath: string): void {
|
||||
if (!nativeIsRepo(basePath)) {
|
||||
const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
|
||||
nativeInit(basePath, mainBranch);
|
||||
}
|
||||
|
||||
const root = gsdRoot(basePath);
|
||||
mkdirSync(join(root, "milestones"), { recursive: true });
|
||||
mkdirSync(join(root, "runtime"), { recursive: true });
|
||||
|
||||
const commitDocs = loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs;
|
||||
ensureGitignore(basePath, { commitDocs });
|
||||
ensurePreferences(basePath);
|
||||
untrackRuntimeFiles(basePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Headless milestone creation from a seed specification document.
|
||||
* Bootstraps the project if needed, generates the next milestone ID,
|
||||
* and dispatches the headless discuss prompt (no Q&A rounds).
|
||||
*/
|
||||
export async function showHeadlessMilestoneCreation(
|
||||
ctx: ExtensionCommandContext,
|
||||
pi: ExtensionAPI,
|
||||
basePath: string,
|
||||
seedContext: string,
|
||||
): Promise<void> {
|
||||
// Ensure .gsd/ is bootstrapped
|
||||
bootstrapGsdProject(basePath);
|
||||
|
||||
// Generate next milestone ID
|
||||
const existingIds = findMilestoneIds(basePath);
|
||||
const prefs = loadEffectiveGSDPreferences();
|
||||
const nextId = nextMilestoneId(existingIds, prefs?.preferences?.unique_milestone_ids ?? false);
|
||||
|
||||
// Create milestone directory
|
||||
const milestoneDir = join(basePath, ".gsd", "milestones", nextId, "slices");
|
||||
mkdirSync(milestoneDir, { recursive: true });
|
||||
|
||||
// Build and dispatch the headless discuss prompt
|
||||
const prompt = buildHeadlessDiscussPrompt(nextId, seedContext, basePath);
|
||||
|
||||
// Set pending auto start (auto-mode triggers on "Milestone X ready." via checkAutoStartAfterDiscuss)
|
||||
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId };
|
||||
|
||||
// Dispatch
|
||||
dispatchWorkflow(pi, prompt);
|
||||
}
|
||||
|
||||
export function findMilestoneIds(basePath: string): string[] {
|
||||
const dir = milestonesDir(basePath);
|
||||
try {
|
||||
|
|
|
|||
86
src/resources/extensions/gsd/prompts/discuss-headless.md
Normal file
86
src/resources/extensions/gsd/prompts/discuss-headless.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# Headless Milestone Creation
|
||||
|
||||
You are creating a GSD milestone from a provided specification document. This is a **headless** (non-interactive) flow — do NOT ask the user any questions. Work entirely from the provided specification.
|
||||
|
||||
## Provided Specification
|
||||
|
||||
{{seedContext}}
|
||||
|
||||
## Your Task
|
||||
|
||||
### Step 1: Reflect
|
||||
|
||||
Summarize your understanding of the specification concretely:
|
||||
- What is being built
|
||||
- Major capabilities/features
|
||||
- Scope estimate (how many milestones × slices)
|
||||
- Any ambiguities or gaps you notice
|
||||
|
||||
### Step 2: Investigate
|
||||
|
||||
Scout the codebase to understand what already exists:
|
||||
- `ls` the project root and key directories
|
||||
- Search for relevant existing code, patterns, dependencies
|
||||
- Check library docs if needed (`resolve_library` / `get_library_docs`)
|
||||
|
||||
### Step 3: Make Decisions
|
||||
|
||||
For any ambiguities or gaps in the specification:
|
||||
- Make your best-guess decision based on the spec's intent, codebase patterns, and domain conventions
|
||||
- Document each assumption clearly in the Context file
|
||||
|
||||
### Step 4: Assess Scope
|
||||
|
||||
Based on reflection + investigation:
|
||||
- Is this a single milestone or multiple milestones?
|
||||
- If multi-milestone: plan the full sequence with dependencies
|
||||
|
||||
### Step 5: Write Artifacts
|
||||
|
||||
**Milestone ID**: {{milestoneId}}
|
||||
|
||||
Use these templates exactly:
|
||||
|
||||
{{inlinedTemplates}}
|
||||
|
||||
**For single milestone**, write in this order:
|
||||
1. `mkdir -p .gsd/milestones/{{milestoneId}}/slices`
|
||||
2. Write `.gsd/PROJECT.md` (using Project template)
|
||||
3. Write `.gsd/REQUIREMENTS.md` (using Requirements template)
|
||||
4. Write `{{contextPath}}` (using Context template) — preserve the specification's exact terminology, emphasis, and specific framing. Do not paraphrase domain-specific language into generics. Document assumptions under an "Assumptions" section.
|
||||
5. Write `{{roadmapPath}}` (using Roadmap template) — decompose into demoable vertical slices with checkboxes, risk, depends, demo sentences, proof strategy, verification classes, milestone definition of done, requirement coverage, and a boundary map. If the milestone crosses multiple runtime boundaries, include an explicit final integration slice.
|
||||
6. Seed `.gsd/DECISIONS.md` (using Decisions template)
|
||||
7. Update `.gsd/STATE.md`
|
||||
8. Commit: `docs({{milestoneId}}): context, requirements, and roadmap`
|
||||
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
|
||||
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)
|
||||
5. Write PRIMARY `{{contextPath}}` — full context with all assumptions documented
|
||||
6. Write PRIMARY `{{roadmapPath}}` — detailed slices for the first milestone only
|
||||
7. For each remaining milestone, write full CONTEXT.md with `depends_on` frontmatter:
|
||||
```yaml
|
||||
---
|
||||
depends_on: [M001, M002]
|
||||
---
|
||||
|
||||
# M003: Title
|
||||
```
|
||||
Each context file should be rich enough that a future agent — with no memory of this conversation — can understand the intent, constraints, dependencies, what the milestone unlocks, and what "done" looks like.
|
||||
8. Update `.gsd/STATE.md`
|
||||
9. Commit: `docs: project plan — N milestones`
|
||||
10. Say exactly: "Milestone {{milestoneId}} ready."
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- **DO NOT ask the user any questions** — this is headless mode
|
||||
- **Preserve the specification's terminology** — don't paraphrase domain-specific language
|
||||
- **Document assumptions** — when you make a judgment call, note it in CONTEXT.md under "Assumptions"
|
||||
- **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`)
|
||||
- **End with "Milestone {{milestoneId}} ready."** — this triggers auto-start detection
|
||||
Loading…
Add table
Reference in a new issue