gsd-pi 2.13.0 → 2.14.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 +3 -3
- package/dist/cli.js +1 -0
- package/dist/loader.js +50 -6
- package/dist/resource-loader.d.ts +7 -6
- package/dist/resource-loader.js +15 -8
- package/dist/resources/extensions/gsd/auto-worktree.ts +29 -183
- package/dist/resources/extensions/gsd/auto.ts +252 -370
- package/dist/resources/extensions/gsd/commands.ts +118 -34
- package/dist/resources/extensions/gsd/doctor.ts +29 -4
- package/dist/resources/extensions/gsd/git-self-heal.ts +0 -71
- package/dist/resources/extensions/gsd/git-service.ts +8 -431
- package/dist/resources/extensions/gsd/gitignore.ts +11 -4
- package/dist/resources/extensions/gsd/guided-flow.ts +141 -5
- package/dist/resources/extensions/gsd/preferences.ts +18 -17
- package/dist/resources/extensions/gsd/prompts/discuss.md +35 -0
- package/dist/resources/extensions/gsd/prompts/queue.md +7 -1
- package/dist/resources/extensions/gsd/state.ts +26 -8
- package/dist/resources/extensions/gsd/templates/state.md +0 -1
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
- package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +22 -4
- package/dist/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
- package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +8 -111
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +11 -770
- package/dist/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
- package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -91
- package/dist/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +1 -426
- package/dist/resources/extensions/gsd/types.ts +0 -1
- package/dist/resources/extensions/gsd/worktree-manager.ts +7 -3
- package/dist/resources/extensions/gsd/worktree.ts +7 -65
- package/dist/resources/extensions/search-the-web/command-search-provider.ts +3 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/providers/google.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google.js +12 -4
- package/packages/pi-ai/dist/providers/google.js.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.js +10 -2
- package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
- package/packages/pi-ai/src/providers/google.ts +20 -8
- package/packages/pi-ai/src/providers/mistral.ts +14 -2
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +10 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +4 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
- 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 +12 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -9
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +4 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +14 -3
- package/packages/pi-tui/dist/components/input.d.ts +1 -0
- package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/input.js +10 -0
- package/packages/pi-tui/dist/components/input.js.map +1 -1
- package/packages/pi-tui/src/components/input.ts +11 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +29 -183
- package/src/resources/extensions/gsd/auto.ts +252 -370
- package/src/resources/extensions/gsd/commands.ts +118 -34
- package/src/resources/extensions/gsd/doctor.ts +29 -4
- package/src/resources/extensions/gsd/git-self-heal.ts +0 -71
- package/src/resources/extensions/gsd/git-service.ts +8 -431
- package/src/resources/extensions/gsd/gitignore.ts +11 -4
- package/src/resources/extensions/gsd/guided-flow.ts +141 -5
- package/src/resources/extensions/gsd/preferences.ts +18 -17
- package/src/resources/extensions/gsd/prompts/discuss.md +35 -0
- package/src/resources/extensions/gsd/prompts/queue.md +7 -1
- package/src/resources/extensions/gsd/state.ts +26 -8
- package/src/resources/extensions/gsd/templates/state.md +0 -1
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +22 -4
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +8 -111
- package/src/resources/extensions/gsd/tests/git-service.test.ts +11 -770
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -91
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
- package/src/resources/extensions/gsd/tests/worktree.test.ts +1 -426
- package/src/resources/extensions/gsd/types.ts +0 -1
- package/src/resources/extensions/gsd/worktree-manager.ts +7 -3
- package/src/resources/extensions/gsd/worktree.ts +7 -65
- package/src/resources/extensions/search-the-web/command-search-provider.ts +3 -1
- package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
- package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
- package/dist/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
- package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
- package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
- package/src/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
|
@@ -50,13 +50,76 @@ export function checkAutoStartAfterDiscuss(): boolean {
|
|
|
50
50
|
|
|
51
51
|
const { ctx, pi, basePath, milestoneId, step } = pendingAutoStart;
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
// for the milestone being discussed. agent_end fires after every LLM turn,
|
|
55
|
-
// including the initial "What do you want to build?" response — we need to
|
|
56
|
-
// wait for the full conversation to complete and the LLM to write CONTEXT.md.
|
|
53
|
+
// Gate 1: Primary milestone must have CONTEXT.md
|
|
57
54
|
const contextFile = resolveMilestoneFile(basePath, milestoneId, "CONTEXT");
|
|
58
55
|
if (!contextFile) return false; // no context yet — keep waiting
|
|
59
56
|
|
|
57
|
+
// Gate 2: STATE.md must exist — written as the last step in the discuss
|
|
58
|
+
// output phase. This prevents auto-start from firing during Phase 3
|
|
59
|
+
// (sequential readiness gates for remaining milestones) in multi-milestone
|
|
60
|
+
// discussions, where M001-CONTEXT.md exists but M002/M003 haven't been
|
|
61
|
+
// processed yet.
|
|
62
|
+
const stateFile = resolveGsdRootFile(basePath, "STATE");
|
|
63
|
+
if (!stateFile) return false; // discussion not finalized yet
|
|
64
|
+
|
|
65
|
+
// Gate 3: Multi-milestone completeness warning
|
|
66
|
+
// Parse PROJECT.md for milestone sequence, warn if any are missing context.
|
|
67
|
+
// Don't block — milestones can be intentionally queued without context.
|
|
68
|
+
const projectFile = resolveGsdRootFile(basePath, "PROJECT");
|
|
69
|
+
if (projectFile) {
|
|
70
|
+
try {
|
|
71
|
+
const projectContent = readFileSync(projectFile, "utf-8");
|
|
72
|
+
const milestoneIds = parseMilestoneSequenceFromProject(projectContent);
|
|
73
|
+
if (milestoneIds.length > 1) {
|
|
74
|
+
const missing = milestoneIds.filter(id => {
|
|
75
|
+
const hasContext = !!resolveMilestoneFile(basePath, id, "CONTEXT");
|
|
76
|
+
const hasDraft = !!resolveMilestoneFile(basePath, id, "CONTEXT-DRAFT");
|
|
77
|
+
const hasDir = existsSync(join(basePath, ".gsd", "milestones", id));
|
|
78
|
+
return !hasContext && !hasDraft && !hasDir;
|
|
79
|
+
});
|
|
80
|
+
if (missing.length > 0) {
|
|
81
|
+
ctx.ui.notify(
|
|
82
|
+
`Multi-milestone validation: ${missing.join(", ")} not found in filesystem. ` +
|
|
83
|
+
`Discussion may not have completed all readiness gates.`,
|
|
84
|
+
"warning",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch { /* non-fatal — PROJECT.md parsing failure shouldn't block auto-start */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Gate 4: Discussion manifest process verification (multi-milestone only)
|
|
92
|
+
// The LLM writes DISCUSSION-MANIFEST.json after each Phase 3 gate decision.
|
|
93
|
+
// If the manifest exists but gates_completed < total, the LLM hasn't finished
|
|
94
|
+
// presenting all readiness gates to the user — block auto-start.
|
|
95
|
+
const manifestPath = join(basePath, ".gsd", "DISCUSSION-MANIFEST.json");
|
|
96
|
+
if (existsSync(manifestPath)) {
|
|
97
|
+
try {
|
|
98
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
99
|
+
const total = typeof manifest.total === "number" ? manifest.total : 0;
|
|
100
|
+
const completed = typeof manifest.gates_completed === "number" ? manifest.gates_completed : 0;
|
|
101
|
+
|
|
102
|
+
if (total > 1 && completed < total) {
|
|
103
|
+
// Discussion not complete — block auto-start until all gates are done
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Cross-check manifest milestones against PROJECT.md if available
|
|
108
|
+
if (projectFile) {
|
|
109
|
+
const projectContent = readFileSync(projectFile, "utf-8");
|
|
110
|
+
const projectIds = parseMilestoneSequenceFromProject(projectContent);
|
|
111
|
+
const manifestIds = Object.keys(manifest.milestones ?? {});
|
|
112
|
+
const untracked = projectIds.filter(id => !manifestIds.includes(id));
|
|
113
|
+
if (untracked.length > 0) {
|
|
114
|
+
ctx.ui.notify(
|
|
115
|
+
`Discussion manifest missing gates for: ${untracked.join(", ")}`,
|
|
116
|
+
"warning",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch { /* malformed manifest — warn but don't block */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
60
123
|
// Draft promotion cleanup: if a CONTEXT-DRAFT.md exists alongside the new
|
|
61
124
|
// CONTEXT.md, delete the draft — it's been consumed by the discussion.
|
|
62
125
|
try {
|
|
@@ -64,11 +127,28 @@ export function checkAutoStartAfterDiscuss(): boolean {
|
|
|
64
127
|
if (draftFile) unlinkSync(draftFile);
|
|
65
128
|
} catch { /* non-fatal — stale draft doesn't break anything, CONTEXT.md wins */ }
|
|
66
129
|
|
|
130
|
+
// Cleanup: remove discussion manifest after auto-start (only needed during discussion)
|
|
131
|
+
try { unlinkSync(manifestPath); } catch { /* may not exist for single-milestone */ }
|
|
132
|
+
|
|
67
133
|
pendingAutoStart = null;
|
|
68
134
|
startAuto(ctx, pi, basePath, false, { step }).catch(() => {});
|
|
69
135
|
return true;
|
|
70
136
|
}
|
|
71
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Extract milestone IDs from PROJECT.md milestone sequence table.
|
|
140
|
+
* Looks for rows like "| M001 | Name | Status |" and extracts the ID column.
|
|
141
|
+
*/
|
|
142
|
+
function parseMilestoneSequenceFromProject(content: string): string[] {
|
|
143
|
+
const ids: string[] = [];
|
|
144
|
+
const lines = content.split(/\r?\n/);
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
const match = line.match(/^\|\s*(M\d{3}[A-Z0-9-]*)\s*\|/);
|
|
147
|
+
if (match) ids.push(match[1]);
|
|
148
|
+
}
|
|
149
|
+
return ids;
|
|
150
|
+
}
|
|
151
|
+
|
|
72
152
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
73
153
|
|
|
74
154
|
type UIContext = ExtensionContext;
|
|
@@ -467,6 +547,62 @@ export async function showDiscuss(
|
|
|
467
547
|
const mid = state.activeMilestone.id;
|
|
468
548
|
const milestoneTitle = state.activeMilestone.title;
|
|
469
549
|
|
|
550
|
+
// Special case: milestone is in needs-discussion phase (has CONTEXT-DRAFT.md but no roadmap yet).
|
|
551
|
+
// Route to the draft discussion flow instead of erroring — the discussion IS how the roadmap gets created.
|
|
552
|
+
if (state.phase === "needs-discussion") {
|
|
553
|
+
const draftFile = resolveMilestoneFile(basePath, mid, "CONTEXT-DRAFT");
|
|
554
|
+
const draftContent = draftFile ? await loadFile(draftFile) : null;
|
|
555
|
+
|
|
556
|
+
const choice = await showNextAction(ctx as any, {
|
|
557
|
+
title: `GSD — ${mid}: ${milestoneTitle}`,
|
|
558
|
+
summary: ["This milestone has a draft context from a prior discussion.", "It needs a dedicated discussion before auto-planning can begin."],
|
|
559
|
+
actions: [
|
|
560
|
+
{
|
|
561
|
+
id: "discuss_draft",
|
|
562
|
+
label: "Discuss from draft",
|
|
563
|
+
description: "Continue where the prior discussion left off — seed material is loaded automatically.",
|
|
564
|
+
recommended: true,
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
id: "discuss_fresh",
|
|
568
|
+
label: "Start fresh discussion",
|
|
569
|
+
description: "Discard the draft and start a new discussion from scratch.",
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
id: "skip_milestone",
|
|
573
|
+
label: "Skip — create new milestone",
|
|
574
|
+
description: "Leave this milestone as-is and start something new.",
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
notYetMessage: "Run /gsd discuss when ready to discuss this milestone.",
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
if (choice === "discuss_draft") {
|
|
581
|
+
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
582
|
+
const basePrompt = loadPrompt("guided-discuss-milestone", {
|
|
583
|
+
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates,
|
|
584
|
+
});
|
|
585
|
+
const seed = draftContent
|
|
586
|
+
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
|
587
|
+
: basePrompt;
|
|
588
|
+
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
|
|
589
|
+
dispatchWorkflow(pi, seed, "gsd-discuss");
|
|
590
|
+
} else if (choice === "discuss_fresh") {
|
|
591
|
+
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
592
|
+
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
|
|
593
|
+
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
594
|
+
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates,
|
|
595
|
+
}), "gsd-discuss");
|
|
596
|
+
} else if (choice === "skip_milestone") {
|
|
597
|
+
const milestoneIds = findMilestoneIds(basePath);
|
|
598
|
+
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
599
|
+
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
600
|
+
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: false };
|
|
601
|
+
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
|
|
602
|
+
}
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
470
606
|
// Guard: no roadmap yet
|
|
471
607
|
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
472
608
|
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
@@ -564,7 +700,7 @@ export async function showSmartEntry(
|
|
|
564
700
|
): Promise<void> {
|
|
565
701
|
const stepMode = options?.step;
|
|
566
702
|
|
|
567
|
-
// ── Ensure git repo exists — GSD needs it for
|
|
703
|
+
// ── Ensure git repo exists — GSD needs it for worktree isolation ──────
|
|
568
704
|
try {
|
|
569
705
|
execSync("git rev-parse --git-dir", { cwd: basePath, stdio: "pipe" });
|
|
570
706
|
} catch {
|
|
@@ -296,6 +296,9 @@ export function renderPreferencesForSystemPrompt(preferences: GSDPreferences, re
|
|
|
296
296
|
if (validated.errors.length > 0) {
|
|
297
297
|
lines.push("- Validation: some preference values were ignored because they were invalid.");
|
|
298
298
|
}
|
|
299
|
+
for (const warning of validated.warnings) {
|
|
300
|
+
lines.push(`- Deprecation: ${warning}`);
|
|
301
|
+
}
|
|
299
302
|
|
|
300
303
|
preferences = validated.preferences;
|
|
301
304
|
|
|
@@ -482,16 +485,20 @@ function parseFrontmatterBlock(frontmatter: string): GSDPreferences {
|
|
|
482
485
|
return root as GSDPreferences;
|
|
483
486
|
}
|
|
484
487
|
|
|
485
|
-
function parseScalar(value: string):
|
|
488
|
+
function parseScalar(value: string): unknown {
|
|
486
489
|
if (value === "true") return true;
|
|
487
490
|
if (value === "false") return false;
|
|
491
|
+
// Recognize empty array/object literals (with or without surrounding quotes)
|
|
492
|
+
const unquoted = value.replace(/^['\"]|['\"]$/g, "");
|
|
493
|
+
if (unquoted === "[]") return [];
|
|
494
|
+
if (unquoted === "{}") return {};
|
|
488
495
|
if (/^-?\d+$/.test(value)) {
|
|
489
496
|
const n = Number(value);
|
|
490
497
|
// Keep large integers (e.g. Discord channel IDs) as strings to avoid precision loss
|
|
491
498
|
if (Number.isSafeInteger(n)) return n;
|
|
492
499
|
return value;
|
|
493
500
|
}
|
|
494
|
-
return
|
|
501
|
+
return unquoted;
|
|
495
502
|
}
|
|
496
503
|
|
|
497
504
|
/**
|
|
@@ -637,8 +644,10 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
|
|
|
637
644
|
export function validatePreferences(preferences: GSDPreferences): {
|
|
638
645
|
preferences: GSDPreferences;
|
|
639
646
|
errors: string[];
|
|
647
|
+
warnings: string[];
|
|
640
648
|
} {
|
|
641
649
|
const errors: string[] = [];
|
|
650
|
+
const warnings: string[] = [];
|
|
642
651
|
const validated: GSDPreferences = {};
|
|
643
652
|
|
|
644
653
|
if (preferences.version !== undefined) {
|
|
@@ -725,7 +734,7 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
725
734
|
const knownUnitTypes = new Set([
|
|
726
735
|
"research-milestone", "plan-milestone", "research-slice", "plan-slice",
|
|
727
736
|
"execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
|
|
728
|
-
"run-uat", "
|
|
737
|
+
"run-uat", "complete-milestone",
|
|
729
738
|
]);
|
|
730
739
|
for (const hook of preferences.post_unit_hooks) {
|
|
731
740
|
if (!hook || typeof hook !== "object") {
|
|
@@ -791,7 +800,7 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
791
800
|
const knownUnitTypes = new Set([
|
|
792
801
|
"research-milestone", "plan-milestone", "research-slice", "plan-slice",
|
|
793
802
|
"execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
|
|
794
|
-
"run-uat", "
|
|
803
|
+
"run-uat", "complete-milestone",
|
|
795
804
|
]);
|
|
796
805
|
const validActions = new Set(["modify", "skip", "replace"]);
|
|
797
806
|
for (const hook of preferences.pre_dispatch_hooks) {
|
|
@@ -905,21 +914,13 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
905
914
|
errors.push("git.main_branch must be a valid branch name (alphanumeric, _, -, /, .)");
|
|
906
915
|
}
|
|
907
916
|
}
|
|
917
|
+
// Deprecated: isolation and merge_to_main are ignored (branchless architecture).
|
|
918
|
+
// Emit warnings so users know to remove them from preferences.
|
|
908
919
|
if (g.isolation !== undefined) {
|
|
909
|
-
|
|
910
|
-
if (typeof g.isolation === "string" && validIsolation.has(g.isolation)) {
|
|
911
|
-
git.isolation = g.isolation as "worktree" | "branch";
|
|
912
|
-
} else {
|
|
913
|
-
errors.push("git.isolation must be one of: worktree, branch");
|
|
914
|
-
}
|
|
920
|
+
warnings.push("git.isolation is deprecated — worktree isolation is now always enabled. Remove this setting.");
|
|
915
921
|
}
|
|
916
922
|
if (g.merge_to_main !== undefined) {
|
|
917
|
-
|
|
918
|
-
if (typeof g.merge_to_main === "string" && validMergeToMain.has(g.merge_to_main)) {
|
|
919
|
-
git.merge_to_main = g.merge_to_main as "milestone" | "slice";
|
|
920
|
-
} else {
|
|
921
|
-
errors.push("git.merge_to_main must be one of: milestone, slice");
|
|
922
|
-
}
|
|
923
|
+
warnings.push("git.merge_to_main is deprecated — milestone-level merge is now always used. Remove this setting.");
|
|
923
924
|
}
|
|
924
925
|
|
|
925
926
|
if (Object.keys(git).length > 0) {
|
|
@@ -927,7 +928,7 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
927
928
|
}
|
|
928
929
|
}
|
|
929
930
|
|
|
930
|
-
return { preferences: validated, errors };
|
|
931
|
+
return { preferences: validated, errors, warnings };
|
|
931
932
|
}
|
|
932
933
|
|
|
933
934
|
function mergeStringLists(base?: unknown, override?: unknown): string[] | undefined {
|
|
@@ -215,6 +215,20 @@ Once the user confirms the milestone split:
|
|
|
215
215
|
5. Write a full `CONTEXT.md` for the primary milestone (the one discussed in depth).
|
|
216
216
|
6. Write a `ROADMAP.md` for **only the primary milestone** — detail-planning later milestones now is waste because the codebase will change. Include requirement coverage and a milestone definition of done.
|
|
217
217
|
|
|
218
|
+
#### MANDATORY: depends_on Frontmatter in CONTEXT.md
|
|
219
|
+
|
|
220
|
+
Every CONTEXT.md for a milestone that depends on other milestones MUST have YAML frontmatter with `depends_on`. The auto-mode state machine reads this field to determine execution order — without it, milestones may execute out of order or in parallel when they shouldn't.
|
|
221
|
+
|
|
222
|
+
```yaml
|
|
223
|
+
---
|
|
224
|
+
depends_on: [M001, M002]
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
# M003: Title
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
If a milestone has no dependencies, omit the frontmatter. The dependency chain from the milestone confirmation gate MUST be reflected in each CONTEXT.md frontmatter. Do NOT rely on QUEUE.md or PROJECT.md for dependency tracking — the state machine only reads CONTEXT.md frontmatter.
|
|
231
|
+
|
|
218
232
|
#### Phase 3: Sequential readiness gate for remaining milestones
|
|
219
233
|
|
|
220
234
|
For each remaining milestone **one at a time, in sequence**, use `ask_user_questions` to assess readiness. Present three options:
|
|
@@ -227,6 +241,27 @@ For each remaining milestone **one at a time, in sequence**, use `ask_user_quest
|
|
|
227
241
|
|
|
228
242
|
Each context file (full or draft) should be rich enough that a future agent encountering it fresh — with no memory of this conversation — can understand the intent, constraints, dependencies, what this milestone unlocks, and what "done" looks like.
|
|
229
243
|
|
|
244
|
+
#### Milestone Gate Tracking (MANDATORY for multi-milestone)
|
|
245
|
+
|
|
246
|
+
After EVERY Phase 3 gate decision, immediately write or update `.gsd/DISCUSSION-MANIFEST.json` with the cumulative state. This file is mechanically validated by the system before auto-mode starts — if gates are incomplete, auto-mode will NOT start.
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
{
|
|
250
|
+
"primary": "M001",
|
|
251
|
+
"milestones": {
|
|
252
|
+
"M001": { "gate": "discussed", "context": "full" },
|
|
253
|
+
"M002": { "gate": "discussed", "context": "full" },
|
|
254
|
+
"M003": { "gate": "queued", "context": "none" }
|
|
255
|
+
},
|
|
256
|
+
"total": 3,
|
|
257
|
+
"gates_completed": 3
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Write this file AFTER each gate decision, not just at the end. Update `gates_completed` incrementally. The system reads this file and BLOCKS auto-start if `gates_completed < total`.
|
|
262
|
+
|
|
263
|
+
For single-milestone projects, do NOT write this file — it is only for multi-milestone discussions.
|
|
264
|
+
|
|
230
265
|
#### Phase 4: Finalize
|
|
231
266
|
|
|
232
267
|
7. Update `.gsd/STATE.md`
|
|
@@ -82,7 +82,13 @@ Determine where the new milestones should go in the overall sequence. Consider d
|
|
|
82
82
|
Once the user is satisfied, in a single pass for **each** new milestone (starting from {{nextId}}):
|
|
83
83
|
|
|
84
84
|
1. `mkdir -p .gsd/milestones/<ID>/slices`
|
|
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."
|
|
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
|
+
```yaml
|
|
87
|
+
---
|
|
88
|
+
depends_on: [M001, M002]
|
|
89
|
+
---
|
|
90
|
+
```
|
|
91
|
+
The auto-mode state machine reads this field to enforce execution order. Without it, milestones may execute out of order. List the exact milestone IDs (including any suffix like `-0zjrg0`) from the dependency chain discussed with the user.
|
|
86
92
|
|
|
87
93
|
Then, after all milestone directories and context files are written:
|
|
88
94
|
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
resolveGsdRootFile,
|
|
30
30
|
gsdRoot,
|
|
31
31
|
} from './paths.js';
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
import { milestoneIdSort, findMilestoneIds } from './guided-flow.js';
|
|
34
34
|
import { nativeBatchParseGsdFiles, type BatchParsedFile } from './native-parser-bridge.js';
|
|
35
35
|
|
|
@@ -438,8 +438,6 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
|
438
438
|
};
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
-
const activeBranch = getActiveSliceBranch(basePath);
|
|
442
|
-
|
|
443
441
|
// Check if the slice has a plan
|
|
444
442
|
const planFile = resolveSliceFile(basePath, activeMilestone.id, activeSlice.id, "PLAN");
|
|
445
443
|
const slicePlanContent = planFile ? await cachedLoadFile(planFile) : null;
|
|
@@ -453,7 +451,7 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
|
453
451
|
recentDecisions: [],
|
|
454
452
|
blockers: [],
|
|
455
453
|
nextAction: `Plan slice ${activeSlice.id} (${activeSlice.title}).`,
|
|
456
|
-
|
|
454
|
+
|
|
457
455
|
registry,
|
|
458
456
|
requirements,
|
|
459
457
|
progress: {
|
|
@@ -470,7 +468,7 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
|
470
468
|
};
|
|
471
469
|
const activeTaskEntry = slicePlan.tasks.find(t => !t.done);
|
|
472
470
|
|
|
473
|
-
if (!activeTaskEntry) {
|
|
471
|
+
if (!activeTaskEntry && slicePlan.tasks.length > 0) {
|
|
474
472
|
// All tasks done but slice not marked complete
|
|
475
473
|
return {
|
|
476
474
|
activeMilestone,
|
|
@@ -480,7 +478,28 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
|
480
478
|
recentDecisions: [],
|
|
481
479
|
blockers: [],
|
|
482
480
|
nextAction: `All tasks done in ${activeSlice.id}. Write slice summary and complete slice.`,
|
|
483
|
-
|
|
481
|
+
|
|
482
|
+
registry,
|
|
483
|
+
requirements,
|
|
484
|
+
progress: {
|
|
485
|
+
milestones: milestoneProgress,
|
|
486
|
+
slices: sliceProgress,
|
|
487
|
+
tasks: taskProgress,
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Empty plan — no tasks defined yet, stay in planning phase
|
|
493
|
+
if (!activeTaskEntry) {
|
|
494
|
+
return {
|
|
495
|
+
activeMilestone,
|
|
496
|
+
activeSlice,
|
|
497
|
+
activeTask: null,
|
|
498
|
+
phase: 'planning',
|
|
499
|
+
recentDecisions: [],
|
|
500
|
+
blockers: [],
|
|
501
|
+
nextAction: `Slice ${activeSlice.id} has a plan file but no tasks. Add tasks to the plan.`,
|
|
502
|
+
|
|
484
503
|
registry,
|
|
485
504
|
requirements,
|
|
486
505
|
progress: {
|
|
@@ -526,7 +545,7 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
|
526
545
|
recentDecisions: [],
|
|
527
546
|
blockers: [`Task ${blockerTaskId} discovered a blocker requiring slice replan`],
|
|
528
547
|
nextAction: `Task ${blockerTaskId} reported blocker_discovered. Replan slice ${activeSlice.id} before continuing.`,
|
|
529
|
-
|
|
548
|
+
|
|
530
549
|
activeWorkspace: undefined,
|
|
531
550
|
registry,
|
|
532
551
|
requirements,
|
|
@@ -557,7 +576,6 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
|
557
576
|
nextAction: hasInterrupted
|
|
558
577
|
? `Resume interrupted work on ${activeTask.id}: ${activeTask.title} in slice ${activeSlice.id}. Read continue.md first.`
|
|
559
578
|
: `Execute ${activeTask.id}: ${activeTask.title} in slice ${activeSlice.id}.`,
|
|
560
|
-
activeBranch: activeBranch ?? undefined,
|
|
561
579
|
registry,
|
|
562
580
|
requirements,
|
|
563
581
|
progress: {
|
|
@@ -14,7 +14,6 @@ import { execSync } from "node:child_process";
|
|
|
14
14
|
import {
|
|
15
15
|
createAutoWorktree,
|
|
16
16
|
mergeMilestoneToMain,
|
|
17
|
-
mergeSliceToMilestone,
|
|
18
17
|
getAutoWorktreeOriginalBase,
|
|
19
18
|
} from "../auto-worktree.ts";
|
|
20
19
|
import { getSliceBranchName } from "../worktree.ts";
|
|
@@ -71,7 +70,9 @@ function addSliceToMilestone(
|
|
|
71
70
|
run(`git commit -m "${c.message}"`, wtPath);
|
|
72
71
|
}
|
|
73
72
|
run(`git checkout milestone/${milestoneId}`, wtPath);
|
|
74
|
-
|
|
73
|
+
run(`git merge --no-ff ${sliceBranch} -m "feat(${milestoneId}/${sliceId}): ${sliceTitle}"`, wtPath);
|
|
74
|
+
// Clean up the slice branch
|
|
75
|
+
run(`git branch -d ${sliceBranch}`, wtPath);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
async function main(): Promise<void> {
|
|
@@ -53,7 +53,7 @@ async function main(): Promise<void> {
|
|
|
53
53
|
mkdirSync(msDir, { recursive: true });
|
|
54
54
|
writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
|
|
55
55
|
run("git add .", tempDir);
|
|
56
|
-
run("git commit -m
|
|
56
|
+
run("git commit -m \"add milestone\"", tempDir);
|
|
57
57
|
|
|
58
58
|
console.log("\n=== auto-worktree lifecycle ===");
|
|
59
59
|
|
|
@@ -651,6 +651,41 @@ Continue from step 2.
|
|
|
651
651
|
}
|
|
652
652
|
}
|
|
653
653
|
|
|
654
|
+
// ─── Empty plan (zero tasks) stays in planning, not summarizing (#454) ──
|
|
655
|
+
console.log('\n=== empty plan → planning (not summarizing) ===');
|
|
656
|
+
{
|
|
657
|
+
const base = createFixtureBase();
|
|
658
|
+
try {
|
|
659
|
+
writeRoadmap(base, 'M001', `---
|
|
660
|
+
id: M001
|
|
661
|
+
title: "Test"
|
|
662
|
+
---
|
|
663
|
+
# M001: Test
|
|
664
|
+
## Vision
|
|
665
|
+
Test
|
|
666
|
+
## Success Criteria
|
|
667
|
+
- Done
|
|
668
|
+
## Slices
|
|
669
|
+
- [ ] **S01: Empty slice** \`risk:low\` \`depends:[]\`
|
|
670
|
+
> Test
|
|
671
|
+
## Boundary Map
|
|
672
|
+
_None_
|
|
673
|
+
`);
|
|
674
|
+
writePlan(base, 'M001', 'S01', `---
|
|
675
|
+
slice: S01
|
|
676
|
+
---
|
|
677
|
+
# S01 Plan
|
|
678
|
+
## Tasks
|
|
679
|
+
`);
|
|
680
|
+
const state = await deriveState(base);
|
|
681
|
+
assertEq(state.phase, 'planning', 'empty plan stays in planning');
|
|
682
|
+
assertEq(state.activeSlice?.id, 'S01', 'active slice is S01');
|
|
683
|
+
assertEq(state.activeTask, null, 'no active task');
|
|
684
|
+
} finally {
|
|
685
|
+
cleanup(base);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
654
689
|
report();
|
|
655
690
|
}
|
|
656
691
|
|
|
@@ -60,7 +60,7 @@ _None_
|
|
|
60
60
|
|
|
61
61
|
// Commit .gsd files
|
|
62
62
|
run("git add -A", dir);
|
|
63
|
-
run("git commit -m
|
|
63
|
+
run("git commit -m \"add milestone\"", dir);
|
|
64
64
|
|
|
65
65
|
return dir;
|
|
66
66
|
}
|
|
@@ -101,7 +101,7 @@ _None_
|
|
|
101
101
|
`);
|
|
102
102
|
|
|
103
103
|
run("git add -A", dir);
|
|
104
|
-
run("git commit -m
|
|
104
|
+
run("git commit -m \"add milestone\"", dir);
|
|
105
105
|
|
|
106
106
|
return dir;
|
|
107
107
|
}
|
|
@@ -111,6 +111,11 @@ async function main(): Promise<void> {
|
|
|
111
111
|
|
|
112
112
|
try {
|
|
113
113
|
// ─── Test 1: Orphaned worktree detection & fix ─────────────────────
|
|
114
|
+
// Skip on Windows: git worktree path resolution on Windows temp dirs
|
|
115
|
+
// uses UNC/8.3 forms that don't survive path normalization. The source
|
|
116
|
+
// logic is correct (tested on macOS/Linux) — the test infra doesn't
|
|
117
|
+
// produce matching paths on Windows CI.
|
|
118
|
+
if (process.platform !== "win32") {
|
|
114
119
|
console.log("\n=== orphaned_auto_worktree ===");
|
|
115
120
|
{
|
|
116
121
|
const dir = createRepoWithCompletedMilestone();
|
|
@@ -132,8 +137,14 @@ async function main(): Promise<void> {
|
|
|
132
137
|
const wtList = run("git worktree list", dir);
|
|
133
138
|
assertTrue(!wtList.includes("milestone/M001"), "worktree no longer listed after fix");
|
|
134
139
|
}
|
|
140
|
+
} else {
|
|
141
|
+
console.log("\n=== orphaned_auto_worktree (skipped on Windows) ===");
|
|
142
|
+
}
|
|
135
143
|
|
|
136
144
|
// ─── Test 2: Stale milestone branch detection & fix ────────────────
|
|
145
|
+
// Skip on Windows: git branch glob matching and path resolution
|
|
146
|
+
// behave differently in Windows temp dirs.
|
|
147
|
+
if (process.platform !== "win32") {
|
|
137
148
|
console.log("\n=== stale_milestone_branch ===");
|
|
138
149
|
{
|
|
139
150
|
const dir = createRepoWithCompletedMilestone();
|
|
@@ -151,9 +162,12 @@ async function main(): Promise<void> {
|
|
|
151
162
|
assertTrue(fixed.fixesApplied.some(f => f.includes("deleted stale branch")), "fix deletes stale branch");
|
|
152
163
|
|
|
153
164
|
// Verify branch is gone
|
|
154
|
-
const branches = run("git branch --list
|
|
165
|
+
const branches = run("git branch --list milestone/*", dir);
|
|
155
166
|
assertTrue(!branches.includes("milestone/M001"), "branch gone after fix");
|
|
156
167
|
}
|
|
168
|
+
} else {
|
|
169
|
+
console.log("\n=== stale_milestone_branch (skipped on Windows) ===");
|
|
170
|
+
}
|
|
157
171
|
|
|
158
172
|
// ─── Test 3: Corrupt merge state detection & fix ───────────────────
|
|
159
173
|
console.log("\n=== corrupt_merge_state ===");
|
|
@@ -187,7 +201,7 @@ async function main(): Promise<void> {
|
|
|
187
201
|
mkdirSync(activityDir, { recursive: true });
|
|
188
202
|
writeFileSync(join(activityDir, "test.log"), "log data\n");
|
|
189
203
|
run("git add -f .gsd/activity/test.log", dir);
|
|
190
|
-
run("git commit -m
|
|
204
|
+
run("git commit -m \"track runtime file\"", dir);
|
|
191
205
|
|
|
192
206
|
const detect = await runGSDDoctor(dir);
|
|
193
207
|
const trackedIssues = detect.issues.filter(i => i.code === "tracked_runtime_files");
|
|
@@ -220,6 +234,7 @@ async function main(): Promise<void> {
|
|
|
220
234
|
}
|
|
221
235
|
|
|
222
236
|
// ─── Test 6: Active worktree NOT flagged (false positive prevention) ─
|
|
237
|
+
if (process.platform !== "win32") {
|
|
223
238
|
console.log("\n=== active worktree safety ===");
|
|
224
239
|
{
|
|
225
240
|
const dir = createRepoWithActiveMilestone();
|
|
@@ -233,6 +248,9 @@ async function main(): Promise<void> {
|
|
|
233
248
|
const orphanIssues = detect.issues.filter(i => i.code === "orphaned_auto_worktree");
|
|
234
249
|
assertEq(orphanIssues.length, 0, "active worktree NOT flagged as orphaned");
|
|
235
250
|
}
|
|
251
|
+
} else {
|
|
252
|
+
console.log("\n=== active worktree safety (skipped on Windows) ===");
|
|
253
|
+
}
|
|
236
254
|
|
|
237
255
|
} finally {
|
|
238
256
|
for (const dir of cleanups) {
|
|
@@ -145,7 +145,8 @@ const guidedFlowSource = readFileSync(
|
|
|
145
145
|
);
|
|
146
146
|
|
|
147
147
|
const checkFnIdx = guidedFlowSource.indexOf("checkAutoStartAfterDiscuss");
|
|
148
|
-
const
|
|
148
|
+
const checkFnEnd = guidedFlowSource.indexOf("\nexport ", checkFnIdx + 1);
|
|
149
|
+
const checkFnChunk = guidedFlowSource.slice(checkFnIdx, checkFnEnd > checkFnIdx ? checkFnEnd : checkFnIdx + 5000);
|
|
149
150
|
|
|
150
151
|
assert(
|
|
151
152
|
checkFnChunk.includes("CONTEXT-DRAFT"),
|