gsd-pi 2.25.0 → 2.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/headless.js +24 -4
- package/dist/resources/extensions/async-jobs/index.ts +9 -1
- package/dist/resources/extensions/bg-shell/index.ts +3 -2
- package/dist/resources/extensions/gsd/auto-recovery.ts +7 -4
- package/dist/resources/extensions/gsd/auto-worktree.ts +14 -3
- package/dist/resources/extensions/gsd/auto.ts +81 -12
- package/dist/resources/extensions/gsd/doctor-proactive.ts +7 -6
- package/dist/resources/extensions/gsd/doctor.ts +24 -1
- package/dist/resources/extensions/gsd/files.ts +13 -2
- package/dist/resources/extensions/gsd/guided-flow.ts +19 -9
- package/dist/resources/extensions/gsd/index.ts +48 -7
- package/dist/resources/extensions/gsd/migrate/writer.ts +39 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
- package/dist/resources/extensions/gsd/preferences.ts +2 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +2 -2
- package/dist/resources/extensions/gsd/roadmap-slices.ts +45 -1
- package/dist/resources/extensions/gsd/state.ts +17 -6
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
- package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
- package/dist/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
- package/dist/resources/extensions/gsd/types.ts +2 -0
- package/dist/resources/extensions/search-the-web/native-search.ts +4 -0
- package/dist/resources/extensions/shared/path-display.ts +19 -0
- package/package.json +1 -6
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +25 -0
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +27 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +32 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/keybindings.js +1 -1
- package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +12 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.js +7 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +2 -2
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +8 -3
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +2 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +5 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +41 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +301 -62
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +63 -30
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts +8 -0
- package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/tests/path-display.test.js +60 -0
- package/packages/pi-coding-agent/dist/tests/path-display.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/utils/clipboard-image.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/utils/clipboard-image.js +32 -6
- package/packages/pi-coding-agent/dist/utils/clipboard-image.js.map +1 -1
- package/packages/pi-coding-agent/dist/utils/path-display.d.ts +34 -0
- package/packages/pi-coding-agent/dist/utils/path-display.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/utils/path-display.js +36 -0
- package/packages/pi-coding-agent/dist/utils/path-display.js.map +1 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +36 -0
- package/packages/pi-coding-agent/src/core/keybindings.ts +1 -1
- package/packages/pi-coding-agent/src/core/lsp/client.ts +11 -1
- package/packages/pi-coding-agent/src/core/lsp/index.ts +7 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +17 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/core/system-prompt.ts +2 -1
- package/packages/pi-coding-agent/src/index.ts +15 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +347 -62
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +40 -4
- package/packages/pi-coding-agent/src/tests/path-display.test.ts +85 -0
- package/packages/pi-coding-agent/src/utils/clipboard-image.ts +33 -6
- package/packages/pi-coding-agent/src/utils/path-display.ts +36 -0
- package/src/resources/extensions/async-jobs/index.ts +9 -1
- package/src/resources/extensions/bg-shell/index.ts +3 -2
- package/src/resources/extensions/gsd/auto-recovery.ts +7 -4
- package/src/resources/extensions/gsd/auto-worktree.ts +14 -3
- package/src/resources/extensions/gsd/auto.ts +81 -12
- package/src/resources/extensions/gsd/doctor-proactive.ts +7 -6
- package/src/resources/extensions/gsd/doctor.ts +24 -1
- package/src/resources/extensions/gsd/files.ts +13 -2
- package/src/resources/extensions/gsd/guided-flow.ts +19 -9
- package/src/resources/extensions/gsd/index.ts +48 -7
- package/src/resources/extensions/gsd/migrate/writer.ts +39 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
- package/src/resources/extensions/gsd/preferences.ts +2 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +2 -2
- package/src/resources/extensions/gsd/roadmap-slices.ts +45 -1
- package/src/resources/extensions/gsd/state.ts +17 -6
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
- package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
- package/src/resources/extensions/gsd/types.ts +2 -0
- package/src/resources/extensions/search-the-web/native-search.ts +4 -0
- package/src/resources/extensions/shared/path-display.ts +19 -0
|
@@ -483,6 +483,45 @@ export async function writeGSDDirectory(
|
|
|
483
483
|
counts.research++;
|
|
484
484
|
}
|
|
485
485
|
|
|
486
|
+
// For fully-completed milestones (all slices done), write a pass-through
|
|
487
|
+
// validation file so deriveState() doesn't enter validating-milestone
|
|
488
|
+
// phase for historical milestones that predate the validation gate (#819).
|
|
489
|
+
const allSlicesDone = milestone.slices.length > 0 && milestone.slices.every(s => s.done);
|
|
490
|
+
if (allSlicesDone) {
|
|
491
|
+
const validationPath = join(mDir, `${milestone.id}-VALIDATION.md`);
|
|
492
|
+
const validationContent = [
|
|
493
|
+
`---`,
|
|
494
|
+
`verdict: pass`,
|
|
495
|
+
`migrated: true`,
|
|
496
|
+
`---`,
|
|
497
|
+
``,
|
|
498
|
+
`# ${milestone.id} Validation`,
|
|
499
|
+
``,
|
|
500
|
+
`Migrated milestone — all slices were completed in the original project.`,
|
|
501
|
+
``,
|
|
502
|
+
].join('\n');
|
|
503
|
+
await saveFile(validationPath, validationContent);
|
|
504
|
+
paths.push(validationPath);
|
|
505
|
+
counts.other++;
|
|
506
|
+
|
|
507
|
+
// Also write a milestone summary if one doesn't exist
|
|
508
|
+
const summaryPath = join(mDir, `${milestone.id}-SUMMARY.md`);
|
|
509
|
+
const summaryContent = [
|
|
510
|
+
`---`,
|
|
511
|
+
`status: done`,
|
|
512
|
+
`migrated: true`,
|
|
513
|
+
`---`,
|
|
514
|
+
``,
|
|
515
|
+
`# ${milestone.id}: ${milestone.title}`,
|
|
516
|
+
``,
|
|
517
|
+
`Migrated from .planning — ${milestone.slices.length} slices completed.`,
|
|
518
|
+
``,
|
|
519
|
+
].join('\n');
|
|
520
|
+
await saveFile(summaryPath, summaryContent);
|
|
521
|
+
paths.push(summaryPath);
|
|
522
|
+
counts.other++;
|
|
523
|
+
}
|
|
524
|
+
|
|
486
525
|
// Slices
|
|
487
526
|
for (const slice of milestone.slices) {
|
|
488
527
|
const sDir = join(mDir, 'slices', slice.id);
|
|
@@ -124,6 +124,12 @@ export async function startParallel(
|
|
|
124
124
|
const toStart = milestoneIds.slice(0, config.max_workers);
|
|
125
125
|
|
|
126
126
|
for (const mid of toStart) {
|
|
127
|
+
// Check budget ceiling before each spawn
|
|
128
|
+
if (isBudgetExceeded()) {
|
|
129
|
+
errors.push({ mid, error: `Budget ceiling ($${config.budget_ceiling}) reached — skipping` });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
127
133
|
try {
|
|
128
134
|
// Create the worktree (without chdir — coordinator stays in project root)
|
|
129
135
|
let wtPath: string;
|
|
@@ -233,7 +239,7 @@ export function spawnWorker(
|
|
|
233
239
|
|
|
234
240
|
let child: ChildProcess;
|
|
235
241
|
try {
|
|
236
|
-
child = spawn(process.execPath, [binPath, "--print", "/gsd auto"], {
|
|
242
|
+
child = spawn(process.execPath, [binPath, "--mode", "json", "--print", "/gsd auto"], {
|
|
237
243
|
cwd: worker.worktreePath,
|
|
238
244
|
env: {
|
|
239
245
|
...process.env,
|
|
@@ -267,6 +273,28 @@ export function spawnWorker(
|
|
|
267
273
|
return false;
|
|
268
274
|
}
|
|
269
275
|
|
|
276
|
+
// ── NDJSON stdout monitoring ────────────────────────────────────────
|
|
277
|
+
// Workers run with --mode json, emitting one JSON event per line.
|
|
278
|
+
// We parse message_end events to extract cost/token usage, keeping
|
|
279
|
+
// the coordinator's cost tracking in sync with actual API spend.
|
|
280
|
+
if (child.stdout) {
|
|
281
|
+
let stdoutBuffer = "";
|
|
282
|
+
child.stdout.on("data", (data: Buffer) => {
|
|
283
|
+
stdoutBuffer += data.toString();
|
|
284
|
+
const lines = stdoutBuffer.split("\n");
|
|
285
|
+
stdoutBuffer = lines.pop() || "";
|
|
286
|
+
for (const line of lines) {
|
|
287
|
+
processWorkerLine(basePath, milestoneId, line);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
// Flush remaining buffer on close
|
|
291
|
+
child.stdout.on("close", () => {
|
|
292
|
+
if (stdoutBuffer.trim()) {
|
|
293
|
+
processWorkerLine(basePath, milestoneId, stdoutBuffer);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
270
298
|
// Update session status with real PID
|
|
271
299
|
writeSessionStatus(basePath, {
|
|
272
300
|
milestoneId,
|
|
@@ -343,6 +371,90 @@ function resolveGsdBin(): string | null {
|
|
|
343
371
|
return null;
|
|
344
372
|
}
|
|
345
373
|
|
|
374
|
+
// ─── NDJSON Processing ──────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Process a single NDJSON line from a worker's stdout.
|
|
378
|
+
* Extracts cost and token usage from message_end events and updates
|
|
379
|
+
* the worker's tracking state + session status file.
|
|
380
|
+
*/
|
|
381
|
+
function processWorkerLine(basePath: string, milestoneId: string, line: string): void {
|
|
382
|
+
if (!line.trim() || !state) return;
|
|
383
|
+
|
|
384
|
+
let event: Record<string, unknown>;
|
|
385
|
+
try {
|
|
386
|
+
event = JSON.parse(line);
|
|
387
|
+
} catch {
|
|
388
|
+
return; // Not valid JSON — skip (stderr leakage, debug output, etc.)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const type = String(event.type ?? "");
|
|
392
|
+
|
|
393
|
+
// message_end carries usage data with cost
|
|
394
|
+
if (type === "message_end" && event.message) {
|
|
395
|
+
const msg = event.message as Record<string, unknown>;
|
|
396
|
+
const usage = msg.usage as Record<string, unknown> | undefined;
|
|
397
|
+
|
|
398
|
+
if (usage) {
|
|
399
|
+
const cost = (usage.cost as Record<string, unknown>)?.total;
|
|
400
|
+
if (typeof cost === "number") {
|
|
401
|
+
const worker = state.workers.get(milestoneId);
|
|
402
|
+
if (worker) {
|
|
403
|
+
worker.cost += cost;
|
|
404
|
+
// Update aggregate
|
|
405
|
+
state.totalCost = 0;
|
|
406
|
+
for (const w of state.workers.values()) {
|
|
407
|
+
state.totalCost += w.cost;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Track completed units (each message_end from assistant = progress)
|
|
414
|
+
if (msg.role === "assistant") {
|
|
415
|
+
const worker = state.workers.get(milestoneId);
|
|
416
|
+
if (worker) {
|
|
417
|
+
worker.completedUnits++;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Update session status file so dashboard sees live cost
|
|
422
|
+
const worker = state.workers.get(milestoneId);
|
|
423
|
+
if (worker) {
|
|
424
|
+
writeSessionStatus(basePath, {
|
|
425
|
+
milestoneId,
|
|
426
|
+
pid: worker.pid,
|
|
427
|
+
state: worker.state,
|
|
428
|
+
currentUnit: null,
|
|
429
|
+
completedUnits: worker.completedUnits,
|
|
430
|
+
cost: worker.cost,
|
|
431
|
+
lastHeartbeat: Date.now(),
|
|
432
|
+
startedAt: worker.startedAt,
|
|
433
|
+
worktreePath: worker.worktreePath,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// tool_execution_start can track current unit
|
|
439
|
+
if (type === "extension_ui_request" && event.method === "notify") {
|
|
440
|
+
// GSD auto-mode sends notifications about current unit
|
|
441
|
+
const worker = state.workers.get(milestoneId);
|
|
442
|
+
if (worker) {
|
|
443
|
+
writeSessionStatus(basePath, {
|
|
444
|
+
milestoneId,
|
|
445
|
+
pid: worker.pid,
|
|
446
|
+
state: worker.state,
|
|
447
|
+
currentUnit: null,
|
|
448
|
+
completedUnits: worker.completedUnits,
|
|
449
|
+
cost: worker.cost,
|
|
450
|
+
lastHeartbeat: Date.now(),
|
|
451
|
+
startedAt: worker.startedAt,
|
|
452
|
+
worktreePath: worker.worktreePath,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
346
458
|
// ─── Stop ──────────────────────────────────────────────────────────────────
|
|
347
459
|
|
|
348
460
|
/**
|
|
@@ -366,10 +478,16 @@ export async function stopParallel(
|
|
|
366
478
|
// Send stop signal via file-based IPC (worker checks on next dispatch)
|
|
367
479
|
sendSignal(basePath, mid, "stop");
|
|
368
480
|
|
|
369
|
-
//
|
|
370
|
-
|
|
481
|
+
// Send SIGTERM to the process for immediate response.
|
|
482
|
+
// Use process handle when available, fall back to PID-based kill
|
|
483
|
+
// (handles are null after coordinator restart / deserialization).
|
|
484
|
+
if (worker.pid > 0) {
|
|
371
485
|
try {
|
|
372
|
-
worker.process
|
|
486
|
+
if (worker.process) {
|
|
487
|
+
worker.process.kill("SIGTERM");
|
|
488
|
+
} else {
|
|
489
|
+
process.kill(worker.pid, "SIGTERM");
|
|
490
|
+
}
|
|
373
491
|
} catch { /* process may already be dead */ }
|
|
374
492
|
}
|
|
375
493
|
|
|
@@ -916,8 +916,9 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
916
916
|
if (p.skip_reassess !== undefined) validatedPhases.skip_reassess = !!p.skip_reassess;
|
|
917
917
|
if (p.skip_slice_research !== undefined) validatedPhases.skip_slice_research = !!p.skip_slice_research;
|
|
918
918
|
if (p.skip_milestone_validation !== undefined) validatedPhases.skip_milestone_validation = !!p.skip_milestone_validation;
|
|
919
|
+
if ((p as any).require_slice_discussion !== undefined) (validatedPhases as any).require_slice_discussion = !!(p as any).require_slice_discussion;
|
|
919
920
|
// Warn on unknown phase keys
|
|
920
|
-
const knownPhaseKeys = new Set(["skip_research", "skip_reassess", "skip_slice_research", "skip_milestone_validation"]);
|
|
921
|
+
const knownPhaseKeys = new Set(["skip_research", "skip_reassess", "skip_slice_research", "skip_milestone_validation", "require_slice_discussion"]);
|
|
921
922
|
for (const key of Object.keys(p)) {
|
|
922
923
|
if (!knownPhaseKeys.has(key)) {
|
|
923
924
|
warnings.push(`unknown phases key "${key}" — ignored`);
|
|
@@ -55,7 +55,7 @@ Use these templates exactly:
|
|
|
55
55
|
9. Say exactly: "Milestone {{milestoneId}} ready."
|
|
56
56
|
|
|
57
57
|
**For multi-milestone**, write in this order:
|
|
58
|
-
1.
|
|
58
|
+
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.
|
|
59
59
|
2. Write `.gsd/PROJECT.md` — full vision across ALL milestones (using Project template)
|
|
60
60
|
3. Write `.gsd/REQUIREMENTS.md` — full capability contract (using Requirements template)
|
|
61
61
|
4. Seed `.gsd/DECISIONS.md` (using Decisions template)
|
|
@@ -82,5 +82,5 @@ Use these templates exactly:
|
|
|
82
82
|
- **Investigate before writing** — always scout the codebase first
|
|
83
83
|
- **Use depends_on frontmatter** for multi-milestone sequences (the state machine reads this field to determine execution order)
|
|
84
84
|
- **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.
|
|
85
|
-
- **Naming convention** —
|
|
85
|
+
- **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.
|
|
86
86
|
- **End with "Milestone {{milestoneId}} ready."** — this triggers auto-start detection
|
|
@@ -211,7 +211,7 @@ Once the user confirms the milestone split:
|
|
|
211
211
|
|
|
212
212
|
#### Phase 1: Shared artifacts
|
|
213
213
|
|
|
214
|
-
1. `mkdir -p .gsd/milestones
|
|
214
|
+
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`.
|
|
215
215
|
2. Write `.gsd/PROJECT.md` — use the **Project** output template below.
|
|
216
216
|
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.
|
|
217
217
|
4. Seed `.gsd/DECISIONS.md` — use the **Decisions** output template below.
|
|
@@ -79,9 +79,9 @@ Determine where the new milestones should go in the overall sequence. Consider d
|
|
|
79
79
|
|
|
80
80
|
## Output Phase
|
|
81
81
|
|
|
82
|
-
Once the user is satisfied, in a single pass for **each** new milestone
|
|
82
|
+
Once the user is satisfied, in a single pass for **each** new milestone:
|
|
83
83
|
|
|
84
|
-
1. `mkdir -p .gsd/milestones/<ID>/slices
|
|
84
|
+
1. Call `gsd_generate_milestone_id` to get the milestone ID — never invent milestone IDs manually. Then `mkdir -p .gsd/milestones/<ID>/slices`.
|
|
85
85
|
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`:**
|
|
86
86
|
```yaml
|
|
87
87
|
---
|
|
@@ -53,7 +53,12 @@ function extractSlicesSection(content: string): string {
|
|
|
53
53
|
export function parseRoadmapSlices(content: string): RoadmapSliceEntry[] {
|
|
54
54
|
const slicesSection = extractSlicesSection(content);
|
|
55
55
|
const slices: RoadmapSliceEntry[] = [];
|
|
56
|
-
if (!slicesSection)
|
|
56
|
+
if (!slicesSection) {
|
|
57
|
+
// Fallback: detect prose-style slice headers (## Slice S01: Title)
|
|
58
|
+
// when the LLM writes freeform prose instead of the ## Slices checklist.
|
|
59
|
+
// This prevents a permanent "No slice eligible" block (#807).
|
|
60
|
+
return parseProseSliceHeaders(content);
|
|
61
|
+
}
|
|
57
62
|
|
|
58
63
|
const checkboxItems = slicesSection.split("\n");
|
|
59
64
|
let currentSlice: RoadmapSliceEntry | null = null;
|
|
@@ -88,3 +93,42 @@ export function parseRoadmapSlices(content: string): RoadmapSliceEntry[] {
|
|
|
88
93
|
if (currentSlice) slices.push(currentSlice);
|
|
89
94
|
return slices;
|
|
90
95
|
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Fallback parser for prose-style roadmaps where the LLM wrote
|
|
99
|
+
* `## Slice S01: Title` headers instead of the machine-readable
|
|
100
|
+
* `## Slices` checklist. Extracts slice IDs and titles so auto-mode
|
|
101
|
+
* can at least identify slices and plan them.
|
|
102
|
+
*
|
|
103
|
+
* Also handles `## S01: Title` and `## S01 — Title` variants.
|
|
104
|
+
*/
|
|
105
|
+
function parseProseSliceHeaders(content: string): RoadmapSliceEntry[] {
|
|
106
|
+
const slices: RoadmapSliceEntry[] = [];
|
|
107
|
+
const headerPattern = /^##\s+(?:Slice\s+)?(S\d+)[:\s—–-]+\s*(.+)/gm;
|
|
108
|
+
let match: RegExpExecArray | null;
|
|
109
|
+
|
|
110
|
+
while ((match = headerPattern.exec(content)) !== null) {
|
|
111
|
+
const id = match[1]!;
|
|
112
|
+
const title = match[2]!.trim();
|
|
113
|
+
|
|
114
|
+
// Try to extract depends from prose: "Depends on: S01" or "**Depends on:** S01, S02"
|
|
115
|
+
const afterHeader = content.slice(match.index + match[0].length);
|
|
116
|
+
const nextHeader = afterHeader.search(/^##\s/m);
|
|
117
|
+
const section = nextHeader !== -1 ? afterHeader.slice(0, nextHeader) : afterHeader.slice(0, 500);
|
|
118
|
+
|
|
119
|
+
const depsMatch = section.match(/\*{0,2}Depends\s+on:?\*{0,2}\s*(.+)/i);
|
|
120
|
+
let depends: string[] = [];
|
|
121
|
+
if (depsMatch) {
|
|
122
|
+
const rawDeps = depsMatch[1]!.replace(/none/i, "").trim();
|
|
123
|
+
if (rawDeps) {
|
|
124
|
+
depends = expandDependencies(
|
|
125
|
+
rawDeps.split(/[,;]/).map(s => s.trim().replace(/[^A-Za-z0-9]/g, "")).filter(Boolean)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
slices.push({ id, title, risk: "medium" as RiskLevel, depends, done: false, demo: "" });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return slices;
|
|
134
|
+
}
|
|
@@ -62,7 +62,11 @@ export function isValidationTerminal(validationContent: string): boolean {
|
|
|
62
62
|
if (!match) return false;
|
|
63
63
|
const verdict = match[1].match(/verdict:\s*(\S+)/);
|
|
64
64
|
if (!verdict) return false;
|
|
65
|
-
|
|
65
|
+
// 'pass' and 'needs-attention' are always terminal.
|
|
66
|
+
// 'needs-remediation' is treated as terminal to prevent infinite loops
|
|
67
|
+
// when no remediation slices exist in the roadmap (#832). The validation
|
|
68
|
+
// report is preserved on disk for manual review.
|
|
69
|
+
return verdict[1] === 'pass' || verdict[1] === 'needs-attention' || verdict[1] === 'needs-remediation';
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
// ─── State Derivation ──────────────────────────────────────────────────────
|
|
@@ -290,19 +294,26 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
|
290
294
|
|
|
291
295
|
if (complete) {
|
|
292
296
|
// All slices done — check validation and summary state
|
|
297
|
+
const summaryFile = resolveMilestoneFile(basePath, mid, "SUMMARY");
|
|
293
298
|
const validationFile = resolveMilestoneFile(basePath, mid, "VALIDATION");
|
|
294
299
|
const validationContent = validationFile ? await cachedLoadFile(validationFile) : null;
|
|
295
300
|
const validationTerminal = validationContent ? isValidationTerminal(validationContent) : false;
|
|
296
|
-
const summaryFile = resolveMilestoneFile(basePath, mid, "SUMMARY");
|
|
297
301
|
|
|
298
|
-
if (
|
|
299
|
-
//
|
|
302
|
+
if (summaryFile) {
|
|
303
|
+
// Summary exists → milestone is complete regardless of validation state.
|
|
304
|
+
// The summary is the terminal artifact (#864).
|
|
305
|
+
registry.push({ id: mid, title, status: 'complete' });
|
|
306
|
+
} else if (!validationTerminal && !activeMilestoneFound) {
|
|
307
|
+
// No summary and no terminal validation → validating-milestone
|
|
300
308
|
activeMilestone = { id: mid, title };
|
|
301
309
|
activeRoadmap = roadmap;
|
|
302
310
|
activeMilestoneFound = true;
|
|
303
311
|
registry.push({ id: mid, title, status: 'active' });
|
|
304
|
-
} else if (!
|
|
305
|
-
//
|
|
312
|
+
} else if (!validationTerminal && activeMilestoneFound) {
|
|
313
|
+
// No summary and no terminal validation, but another milestone is already active
|
|
314
|
+
registry.push({ id: mid, title, status: 'pending' });
|
|
315
|
+
} else if (!activeMilestoneFound) {
|
|
316
|
+
// Terminal validation but no summary → completing-milestone
|
|
306
317
|
activeMilestone = { id: mid, title };
|
|
307
318
|
activeRoadmap = roadmap;
|
|
308
319
|
activeMilestoneFound = true;
|
|
@@ -700,6 +700,76 @@ slice: S01
|
|
|
700
700
|
}
|
|
701
701
|
}
|
|
702
702
|
|
|
703
|
+
// ─── Test: completed M001 (summary, no validation) skipped for active M003 (#864) ────
|
|
704
|
+
console.log('\n=== completed milestone with summary but no validation is not active (#864) ===');
|
|
705
|
+
{
|
|
706
|
+
const base = createFixtureBase();
|
|
707
|
+
try {
|
|
708
|
+
// M001: all slices done, has summary, no validation
|
|
709
|
+
writeRoadmap(base, 'M001', `# M001: First Milestone\n\n**Vision:** Done.\n\n## Slices\n\n- [x] **S01: Done slice** \`risk:low\` \`depends:[]\`\n > Completed.\n`);
|
|
710
|
+
writeMilestoneSummary(base, 'M001', '---\nid: M001\n---\n\n# M001: First Milestone\n\n**Completed.**');
|
|
711
|
+
// M003: incomplete, should be active
|
|
712
|
+
writeRoadmap(base, 'M003', `# M003: Active Milestone\n\n**Vision:** Do stuff.\n\n## Slices\n\n- [ ] **S01: Work slice** \`risk:low\` \`depends:[]\`\n > Needs work.\n`);
|
|
713
|
+
|
|
714
|
+
const state = await deriveState(base);
|
|
715
|
+
assertEq(state.activeMilestone?.id, 'M003', 'active milestone is M003, not completed M001');
|
|
716
|
+
const m001Entry = state.registry.find(e => e.id === 'M001');
|
|
717
|
+
assertEq(m001Entry?.status, 'complete', 'M001 is marked complete despite no validation');
|
|
718
|
+
} finally {
|
|
719
|
+
cleanup(base);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ─── Test: completed M001 with summary AND validation is complete (#864) ────
|
|
724
|
+
console.log('\n=== completed milestone with summary and validation is complete ===');
|
|
725
|
+
{
|
|
726
|
+
const base = createFixtureBase();
|
|
727
|
+
try {
|
|
728
|
+
writeRoadmap(base, 'M001', `# M001: First Milestone\n\n**Vision:** Done.\n\n## Slices\n\n- [x] **S01: Done slice** \`risk:low\` \`depends:[]\`\n > Completed.\n`);
|
|
729
|
+
writeMilestoneSummary(base, 'M001', '---\nid: M001\n---\n\n# M001: First Milestone\n\n**Completed.**');
|
|
730
|
+
writeMilestoneValidation(base, 'M001', 'pass');
|
|
731
|
+
writeRoadmap(base, 'M003', `# M003: Active Milestone\n\n**Vision:** Do stuff.\n\n## Slices\n\n- [ ] **S01: Work slice** \`risk:low\` \`depends:[]\`\n > Needs work.\n`);
|
|
732
|
+
|
|
733
|
+
const state = await deriveState(base);
|
|
734
|
+
assertEq(state.activeMilestone?.id, 'M003', 'active milestone is M003');
|
|
735
|
+
const m001Entry = state.registry.find(e => e.id === 'M001');
|
|
736
|
+
assertEq(m001Entry?.status, 'complete', 'M001 with both summary and validation is complete');
|
|
737
|
+
} finally {
|
|
738
|
+
cleanup(base);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ─── Test: all slices done, no summary, no validation → needs validation (#864) ────
|
|
743
|
+
console.log('\n=== all slices done, no summary, no validation → validating-milestone ===');
|
|
744
|
+
{
|
|
745
|
+
const base = createFixtureBase();
|
|
746
|
+
try {
|
|
747
|
+
writeRoadmap(base, 'M001', `# M001: First Milestone\n\n**Vision:** Validate me.\n\n## Slices\n\n- [x] **S01: Done slice** \`risk:low\` \`depends:[]\`\n > Completed.\n`);
|
|
748
|
+
// No summary, no validation — this should be active for validation
|
|
749
|
+
|
|
750
|
+
const state = await deriveState(base);
|
|
751
|
+
assertEq(state.activeMilestone?.id, 'M001', 'M001 is active for validation');
|
|
752
|
+
} finally {
|
|
753
|
+
cleanup(base);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ─── Test: all slices done, validation pass, no summary → needs completion (#864) ────
|
|
758
|
+
console.log('\n=== all slices done, validation pass, no summary → completing-milestone ===');
|
|
759
|
+
{
|
|
760
|
+
const base = createFixtureBase();
|
|
761
|
+
try {
|
|
762
|
+
writeRoadmap(base, 'M001', `# M001: First Milestone\n\n**Vision:** Complete me.\n\n## Slices\n\n- [x] **S01: Done slice** \`risk:low\` \`depends:[]\`\n > Completed.\n`);
|
|
763
|
+
writeMilestoneValidation(base, 'M001', 'pass');
|
|
764
|
+
// No summary — validated but not yet completed
|
|
765
|
+
|
|
766
|
+
const state = await deriveState(base);
|
|
767
|
+
assertEq(state.activeMilestone?.id, 'M001', 'M001 is active for completion');
|
|
768
|
+
} finally {
|
|
769
|
+
cleanup(base);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
703
773
|
report();
|
|
704
774
|
}
|
|
705
775
|
|
|
@@ -188,7 +188,7 @@ async function main(): Promise<void> {
|
|
|
188
188
|
cleanups.push(dir);
|
|
189
189
|
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
190
190
|
|
|
191
|
-
const result = preDispatchHealthGate(dir);
|
|
191
|
+
const result = await preDispatchHealthGate(dir);
|
|
192
192
|
assertTrue(result.proceed, "gate passes on clean state");
|
|
193
193
|
assertEq(result.issues.length, 0, "no issues on clean state");
|
|
194
194
|
}
|
|
@@ -206,7 +206,7 @@ async function main(): Promise<void> {
|
|
|
206
206
|
unitStartedAt: "2026-03-10T00:01:00Z", completedUnits: 3,
|
|
207
207
|
}));
|
|
208
208
|
|
|
209
|
-
const result = preDispatchHealthGate(dir);
|
|
209
|
+
const result = await preDispatchHealthGate(dir);
|
|
210
210
|
assertTrue(result.proceed, "gate passes after auto-clearing stale lock");
|
|
211
211
|
assertTrue(result.fixesApplied.some(f => f.includes("cleared stale auto.lock")), "reports lock cleared");
|
|
212
212
|
assertTrue(!existsSync(join(dir, ".gsd", "auto.lock")), "lock file removed");
|
|
@@ -222,7 +222,7 @@ async function main(): Promise<void> {
|
|
|
222
222
|
const headHash = run("git rev-parse HEAD", dir);
|
|
223
223
|
writeFileSync(join(dir, ".git", "MERGE_HEAD"), headHash + "\n");
|
|
224
224
|
|
|
225
|
-
const result = preDispatchHealthGate(dir);
|
|
225
|
+
const result = await preDispatchHealthGate(dir);
|
|
226
226
|
assertTrue(result.proceed, "gate passes after auto-healing merge state");
|
|
227
227
|
assertTrue(result.fixesApplied.some(f => f.includes("cleaned merge state")), "reports merge state cleaned");
|
|
228
228
|
assertTrue(!existsSync(join(dir, ".git", "MERGE_HEAD")), "MERGE_HEAD removed");
|
|
@@ -231,6 +231,26 @@ async function main(): Promise<void> {
|
|
|
231
231
|
console.log(" (skipped on Windows)");
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
console.log("\n=== health gate: STATE.md missing — auto-healed ===");
|
|
235
|
+
{
|
|
236
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-proactive-")));
|
|
237
|
+
cleanups.push(dir);
|
|
238
|
+
// Minimal .gsd structure: milestones dir exists but no STATE.md
|
|
239
|
+
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
|
|
240
|
+
|
|
241
|
+
const stateFile = join(dir, ".gsd", "STATE.md");
|
|
242
|
+
assertTrue(!existsSync(stateFile), "STATE.md does not exist before gate");
|
|
243
|
+
|
|
244
|
+
const result = await preDispatchHealthGate(dir);
|
|
245
|
+
assertTrue(result.proceed, "gate passes after rebuilding STATE.md");
|
|
246
|
+
assertTrue(
|
|
247
|
+
result.fixesApplied.some(f => f.includes("rebuilt missing STATE.md")),
|
|
248
|
+
"reports STATE.md rebuilt",
|
|
249
|
+
);
|
|
250
|
+
assertTrue(existsSync(stateFile), "STATE.md created by auto-heal");
|
|
251
|
+
assertTrue(result.issues.length === 0, "no blocking issues after heal");
|
|
252
|
+
}
|
|
253
|
+
|
|
234
254
|
} finally {
|
|
235
255
|
resetProactiveHealing();
|
|
236
256
|
for (const dir of cleanups) {
|
|
@@ -11,6 +11,7 @@ import { writeGSDDirectory } from '../migrate/writer.ts';
|
|
|
11
11
|
import { generatePreview } from '../migrate/preview.ts';
|
|
12
12
|
import { parseRoadmap, parsePlan, parseSummary } from '../files.ts';
|
|
13
13
|
import { deriveState } from '../state.ts';
|
|
14
|
+
import { invalidateAllCaches } from '../cache.ts';
|
|
14
15
|
import type {
|
|
15
16
|
GSDProject,
|
|
16
17
|
GSDMilestone,
|
|
@@ -207,6 +208,7 @@ async function main(): Promise<void> {
|
|
|
207
208
|
|
|
208
209
|
// (e) deriveState
|
|
209
210
|
console.log(' --- deriveState ---');
|
|
211
|
+
invalidateAllCaches();
|
|
210
212
|
const state = await deriveState(base);
|
|
211
213
|
assertEq(state.phase, 'executing', 'incomplete: deriveState phase is executing');
|
|
212
214
|
assertTrue(state.activeMilestone !== null, 'incomplete: deriveState has activeMilestone');
|
|
@@ -262,14 +264,18 @@ async function main(): Promise<void> {
|
|
|
262
264
|
assertTrue(!existsSync(join(m, 'M001-RESEARCH.md')), 'complete: M001-RESEARCH.md NOT written (null)');
|
|
263
265
|
// No REQUIREMENTS.md since empty requirements
|
|
264
266
|
assertTrue(!existsSync(join(base, '.gsd', 'REQUIREMENTS.md')), 'complete: REQUIREMENTS.md NOT written (empty)');
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
267
|
+
// Completed milestone should have VALIDATION and SUMMARY from migration (#819)
|
|
268
|
+
assertTrue(existsSync(join(m, 'M001-VALIDATION.md')), 'complete: M001-VALIDATION.md written for completed milestone');
|
|
269
|
+
assertTrue(existsSync(join(m, 'M001-SUMMARY.md')), 'complete: M001-SUMMARY.md written for completed milestone');
|
|
270
|
+
|
|
271
|
+
// deriveState: all slices done, all tasks done — migration now writes
|
|
272
|
+
// VALIDATION.md and SUMMARY.md for completed milestones (#819),
|
|
273
|
+
// so the milestone should be fully complete.
|
|
274
|
+
invalidateAllCaches();
|
|
268
275
|
const state = await deriveState(base);
|
|
269
|
-
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
assertTrue(state.activeMilestone !== null, 'complete: deriveState has activeMilestone');
|
|
276
|
+
assertEq(state.phase, 'complete', 'complete: deriveState phase is complete (validation + summary written by migration)');
|
|
277
|
+
// When all milestones are complete, activeMilestone points to the last entry (for display)
|
|
278
|
+
assertTrue(state.activeMilestone !== null, 'complete: deriveState has activeMilestone (last entry)');
|
|
273
279
|
assertEq(state.activeMilestone!.id, 'M001', 'complete: deriveState activeMilestone is M001');
|
|
274
280
|
|
|
275
281
|
// generatePreview for complete project
|