gsd-pi 0.2.9 → 0.3.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 +23 -0
- package/dist/cli.js +47 -5
- package/dist/wizard.js +2 -1
- package/package.json +1 -1
- package/src/resources/extensions/gsd/commands.ts +9 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +6 -1
- package/src/resources/extensions/gsd/files.ts +7 -7
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/index.ts +36 -1
- package/src/resources/extensions/gsd/migrate/command.ts +215 -0
- package/src/resources/extensions/gsd/migrate/index.ts +42 -0
- package/src/resources/extensions/gsd/migrate/parser.ts +323 -0
- package/src/resources/extensions/gsd/migrate/parsers.ts +624 -0
- package/src/resources/extensions/gsd/migrate/preview.ts +48 -0
- package/src/resources/extensions/gsd/migrate/transformer.ts +346 -0
- package/src/resources/extensions/gsd/migrate/types.ts +370 -0
- package/src/resources/extensions/gsd/migrate/validator.ts +53 -0
- package/src/resources/extensions/gsd/migrate/writer.ts +539 -0
- package/src/resources/extensions/gsd/prompts/review-migration.md +66 -0
- package/src/resources/extensions/gsd/prompts/worktree-merge.md +89 -0
- package/src/resources/extensions/gsd/tests/migrate-command.test.ts +390 -0
- package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +786 -0
- package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +657 -0
- package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +443 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +318 -0
- package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +420 -0
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +160 -0
- package/src/resources/extensions/gsd/worktree-command.ts +527 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +302 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
// GSD Directory Writer — Format Functions & Directory Orchestrator
|
|
2
|
+
// Format functions: pure string-returning functions that serialize GSD types into the exact markdown
|
|
3
|
+
// format that GSD-2's parsers expect (parseRoadmap, parsePlan, parseSummary, parseRequirementCounts).
|
|
4
|
+
// writeGSDDirectory: orchestrator that writes a complete .gsd directory tree from a GSDProject.
|
|
5
|
+
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { saveFile } from '../files.ts';
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
GSDMilestone,
|
|
11
|
+
GSDSlice,
|
|
12
|
+
GSDTask,
|
|
13
|
+
GSDRequirement,
|
|
14
|
+
GSDProject,
|
|
15
|
+
} from './types.ts';
|
|
16
|
+
|
|
17
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/** Result of writeGSDDirectory — lists all files that were written. */
|
|
20
|
+
export interface WrittenFiles {
|
|
21
|
+
/** Absolute paths of all files written */
|
|
22
|
+
paths: string[];
|
|
23
|
+
/** Count by category */
|
|
24
|
+
counts: {
|
|
25
|
+
roadmaps: number;
|
|
26
|
+
plans: number;
|
|
27
|
+
taskPlans: number;
|
|
28
|
+
taskSummaries: number;
|
|
29
|
+
sliceSummaries: number;
|
|
30
|
+
research: number;
|
|
31
|
+
requirements: number;
|
|
32
|
+
contexts: number;
|
|
33
|
+
other: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Pre-write statistics computed from a GSDProject without I/O. */
|
|
38
|
+
export interface MigrationPreview {
|
|
39
|
+
milestoneCount: number;
|
|
40
|
+
totalSlices: number;
|
|
41
|
+
totalTasks: number;
|
|
42
|
+
doneSlices: number;
|
|
43
|
+
doneTasks: number;
|
|
44
|
+
sliceCompletionPct: number;
|
|
45
|
+
taskCompletionPct: number;
|
|
46
|
+
requirements: {
|
|
47
|
+
active: number;
|
|
48
|
+
validated: number;
|
|
49
|
+
deferred: number;
|
|
50
|
+
outOfScope: number;
|
|
51
|
+
total: number;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Local Helpers ─────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Serialize a flat key-value map into YAML frontmatter block.
|
|
59
|
+
* Matches parseFrontmatterMap() expectations:
|
|
60
|
+
* - Scalars: `key: value`
|
|
61
|
+
* - Arrays of strings: `key:\n - item`
|
|
62
|
+
* - Empty arrays: `key: []`
|
|
63
|
+
* - Arrays of objects: `key:\n - field1: val\n field2: val`
|
|
64
|
+
* - Boolean: `key: true/false`
|
|
65
|
+
*/
|
|
66
|
+
function serializeFrontmatter(data: Record<string, unknown>): string {
|
|
67
|
+
const lines: string[] = ['---'];
|
|
68
|
+
|
|
69
|
+
for (const [key, value] of Object.entries(data)) {
|
|
70
|
+
if (value === undefined || value === null) continue;
|
|
71
|
+
|
|
72
|
+
if (typeof value === 'boolean') {
|
|
73
|
+
lines.push(`${key}: ${value}`);
|
|
74
|
+
} else if (typeof value === 'string' || typeof value === 'number') {
|
|
75
|
+
lines.push(`${key}: ${value}`);
|
|
76
|
+
} else if (Array.isArray(value)) {
|
|
77
|
+
if (value.length === 0) {
|
|
78
|
+
lines.push(`${key}: []`);
|
|
79
|
+
} else if (typeof value[0] === 'object' && value[0] !== null) {
|
|
80
|
+
// Array of objects
|
|
81
|
+
lines.push(`${key}:`);
|
|
82
|
+
for (const obj of value) {
|
|
83
|
+
const entries = Object.entries(obj as Record<string, string>);
|
|
84
|
+
if (entries.length > 0) {
|
|
85
|
+
lines.push(` - ${entries[0][0]}: ${entries[0][1]}`);
|
|
86
|
+
for (let i = 1; i < entries.length; i++) {
|
|
87
|
+
lines.push(` ${entries[i][0]}: ${entries[i][1]}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
// Array of scalars
|
|
93
|
+
lines.push(`${key}:`);
|
|
94
|
+
for (const item of value) {
|
|
95
|
+
lines.push(` - ${item}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
lines.push('---');
|
|
102
|
+
return lines.join('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── Format Functions ──────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Format a milestone's ROADMAP.md content.
|
|
109
|
+
* Output must parse correctly through parseRoadmap().
|
|
110
|
+
*/
|
|
111
|
+
export function formatRoadmap(milestone: GSDMilestone): string {
|
|
112
|
+
const lines: string[] = [];
|
|
113
|
+
|
|
114
|
+
lines.push(`# ${milestone.id}: ${milestone.title}`);
|
|
115
|
+
lines.push('');
|
|
116
|
+
lines.push(`**Vision:** ${milestone.vision || '(migrated project)'}`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
|
|
119
|
+
lines.push('## Success Criteria');
|
|
120
|
+
lines.push('');
|
|
121
|
+
if (milestone.successCriteria.length > 0) {
|
|
122
|
+
for (const criterion of milestone.successCriteria) {
|
|
123
|
+
lines.push(`- ${criterion}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
lines.push('');
|
|
127
|
+
|
|
128
|
+
lines.push('## Slices');
|
|
129
|
+
lines.push('');
|
|
130
|
+
for (const slice of milestone.slices) {
|
|
131
|
+
const check = slice.done ? 'x' : ' ';
|
|
132
|
+
const depsStr = slice.depends.length > 0 ? slice.depends.join(', ') : '';
|
|
133
|
+
lines.push(`- [${check}] **${slice.id}: ${slice.title}** \`risk:${slice.risk}\` \`depends:[${depsStr}]\``);
|
|
134
|
+
if (slice.demo) {
|
|
135
|
+
lines.push(` > After this: ${slice.demo}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Skip Boundary Map section entirely per D004
|
|
140
|
+
|
|
141
|
+
return lines.join('\n') + '\n';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Format a slice's PLAN.md (S01-PLAN.md).
|
|
146
|
+
* Output must parse correctly through parsePlan().
|
|
147
|
+
*/
|
|
148
|
+
export function formatPlan(slice: GSDSlice): string {
|
|
149
|
+
const lines: string[] = [];
|
|
150
|
+
|
|
151
|
+
lines.push(`# ${slice.id}: ${slice.title}`);
|
|
152
|
+
lines.push('');
|
|
153
|
+
lines.push(`**Goal:** ${slice.goal || slice.title}`);
|
|
154
|
+
lines.push(`**Demo:** ${slice.demo || slice.title}`);
|
|
155
|
+
lines.push('');
|
|
156
|
+
|
|
157
|
+
lines.push('## Must-Haves');
|
|
158
|
+
lines.push('');
|
|
159
|
+
// No must-haves in migrated data — empty section
|
|
160
|
+
lines.push('');
|
|
161
|
+
|
|
162
|
+
lines.push('## Tasks');
|
|
163
|
+
lines.push('');
|
|
164
|
+
for (const task of slice.tasks) {
|
|
165
|
+
const check = task.done ? 'x' : ' ';
|
|
166
|
+
const estPart = task.estimate ? ` \`est:${task.estimate}\`` : '';
|
|
167
|
+
lines.push(`- [${check}] **${task.id}: ${task.title}**${estPart}`);
|
|
168
|
+
if (task.description) {
|
|
169
|
+
lines.push(` - ${task.description}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
lines.push('');
|
|
173
|
+
|
|
174
|
+
lines.push('## Files Likely Touched');
|
|
175
|
+
lines.push('');
|
|
176
|
+
for (const task of slice.tasks) {
|
|
177
|
+
for (const file of task.files) {
|
|
178
|
+
lines.push(`- \`${file}\``);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return lines.join('\n') + '\n';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Format a slice summary (S01-SUMMARY.md).
|
|
187
|
+
* Output must parse correctly through parseSummary().
|
|
188
|
+
*/
|
|
189
|
+
export function formatSliceSummary(slice: GSDSlice, milestoneId: string): string {
|
|
190
|
+
if (!slice.summary) return '';
|
|
191
|
+
|
|
192
|
+
const s = slice.summary;
|
|
193
|
+
const fm = serializeFrontmatter({
|
|
194
|
+
id: slice.id,
|
|
195
|
+
parent: milestoneId,
|
|
196
|
+
milestone: milestoneId,
|
|
197
|
+
provides: s.provides,
|
|
198
|
+
requires: [],
|
|
199
|
+
affects: [],
|
|
200
|
+
key_files: s.keyFiles,
|
|
201
|
+
key_decisions: s.keyDecisions,
|
|
202
|
+
patterns_established: s.patternsEstablished,
|
|
203
|
+
observability_surfaces: [],
|
|
204
|
+
drill_down_paths: [],
|
|
205
|
+
duration: s.duration || '',
|
|
206
|
+
verification_result: 'passed',
|
|
207
|
+
completed_at: s.completedAt || '',
|
|
208
|
+
blocker_discovered: false,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const body = [
|
|
212
|
+
'',
|
|
213
|
+
`# ${slice.id}: ${slice.title}`,
|
|
214
|
+
'',
|
|
215
|
+
`**${s.whatHappened ? s.whatHappened.split('\n')[0] : 'Migrated from legacy format'}**`,
|
|
216
|
+
'',
|
|
217
|
+
'## What Happened',
|
|
218
|
+
'',
|
|
219
|
+
s.whatHappened || 'Migrated from legacy planning format.',
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
return fm + body.join('\n') + '\n';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Format a task summary (T01-SUMMARY.md).
|
|
227
|
+
* Output must parse correctly through parseSummary().
|
|
228
|
+
*/
|
|
229
|
+
export function formatTaskSummary(task: GSDTask, sliceId: string, milestoneId: string): string {
|
|
230
|
+
if (!task.summary) return '';
|
|
231
|
+
|
|
232
|
+
const s = task.summary;
|
|
233
|
+
const fm = serializeFrontmatter({
|
|
234
|
+
id: task.id,
|
|
235
|
+
parent: sliceId,
|
|
236
|
+
milestone: milestoneId,
|
|
237
|
+
provides: s.provides,
|
|
238
|
+
requires: [],
|
|
239
|
+
affects: [],
|
|
240
|
+
key_files: s.keyFiles,
|
|
241
|
+
key_decisions: [],
|
|
242
|
+
patterns_established: [],
|
|
243
|
+
observability_surfaces: [],
|
|
244
|
+
drill_down_paths: [],
|
|
245
|
+
duration: s.duration || '',
|
|
246
|
+
verification_result: 'passed',
|
|
247
|
+
completed_at: s.completedAt || '',
|
|
248
|
+
blocker_discovered: false,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const body = [
|
|
252
|
+
'',
|
|
253
|
+
`# ${task.id}: ${task.title}`,
|
|
254
|
+
'',
|
|
255
|
+
`**${s.whatHappened ? s.whatHappened.split('\n')[0] : 'Migrated from legacy format'}**`,
|
|
256
|
+
'',
|
|
257
|
+
'## What Happened',
|
|
258
|
+
'',
|
|
259
|
+
s.whatHappened || 'Migrated from legacy planning format.',
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
return fm + body.join('\n') + '\n';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Format a task plan (T01-PLAN.md).
|
|
267
|
+
* deriveState() only checks for file existence, not content.
|
|
268
|
+
* Keep it minimal but valid markdown.
|
|
269
|
+
*/
|
|
270
|
+
export function formatTaskPlan(task: GSDTask, sliceId: string, milestoneId: string): string {
|
|
271
|
+
const lines: string[] = [];
|
|
272
|
+
lines.push(`# ${task.id}: ${task.title}`);
|
|
273
|
+
lines.push('');
|
|
274
|
+
lines.push(`**Slice:** ${sliceId} — **Milestone:** ${milestoneId}`);
|
|
275
|
+
lines.push('');
|
|
276
|
+
lines.push('## Description');
|
|
277
|
+
lines.push('');
|
|
278
|
+
lines.push(task.description || 'Migrated from legacy planning format.');
|
|
279
|
+
lines.push('');
|
|
280
|
+
|
|
281
|
+
if (task.mustHaves.length > 0) {
|
|
282
|
+
lines.push('## Must-Haves');
|
|
283
|
+
lines.push('');
|
|
284
|
+
for (const mh of task.mustHaves) {
|
|
285
|
+
lines.push(`- [ ] ${mh}`);
|
|
286
|
+
}
|
|
287
|
+
lines.push('');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (task.files.length > 0) {
|
|
291
|
+
lines.push('## Files');
|
|
292
|
+
lines.push('');
|
|
293
|
+
for (const f of task.files) {
|
|
294
|
+
lines.push(`- \`${f}\``);
|
|
295
|
+
}
|
|
296
|
+
lines.push('');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return lines.join('\n');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Format REQUIREMENTS.md grouped by status.
|
|
304
|
+
* Output must parse correctly through parseRequirementCounts().
|
|
305
|
+
* parseRequirementCounts expects: ## Active/## Validated/## Deferred/## Out of Scope sections
|
|
306
|
+
* with ### R001 — Title headings under each section.
|
|
307
|
+
*/
|
|
308
|
+
export function formatRequirements(requirements: GSDRequirement[]): string {
|
|
309
|
+
const lines: string[] = [];
|
|
310
|
+
lines.push('# Requirements');
|
|
311
|
+
lines.push('');
|
|
312
|
+
|
|
313
|
+
const groups: Record<string, GSDRequirement[]> = {
|
|
314
|
+
active: [],
|
|
315
|
+
validated: [],
|
|
316
|
+
deferred: [],
|
|
317
|
+
'out-of-scope': [],
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
for (const req of requirements) {
|
|
321
|
+
const status = req.status.toLowerCase();
|
|
322
|
+
if (status in groups) {
|
|
323
|
+
groups[status].push(req);
|
|
324
|
+
} else {
|
|
325
|
+
groups.active.push(req);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const sectionMap: [string, string][] = [
|
|
330
|
+
['active', 'Active'],
|
|
331
|
+
['validated', 'Validated'],
|
|
332
|
+
['deferred', 'Deferred'],
|
|
333
|
+
['out-of-scope', 'Out of Scope'],
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
for (const [key, heading] of sectionMap) {
|
|
337
|
+
lines.push(`## ${heading}`);
|
|
338
|
+
lines.push('');
|
|
339
|
+
for (const req of groups[key]) {
|
|
340
|
+
lines.push(`### ${req.id} — ${req.title}`);
|
|
341
|
+
lines.push('');
|
|
342
|
+
lines.push(`- Status: ${req.status}`);
|
|
343
|
+
lines.push(`- Class: ${req.class}`);
|
|
344
|
+
lines.push(`- Source: ${req.source}`);
|
|
345
|
+
lines.push(`- Primary Slice: ${req.primarySlice}`);
|
|
346
|
+
lines.push('');
|
|
347
|
+
if (req.description) {
|
|
348
|
+
lines.push(req.description);
|
|
349
|
+
lines.push('');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return lines.join('\n');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ─── Passthrough Format Helpers ────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Format PROJECT.md content.
|
|
361
|
+
* If content is empty, produce a minimal valid stub.
|
|
362
|
+
*/
|
|
363
|
+
export function formatProject(content: string): string {
|
|
364
|
+
if (!content || !content.trim()) {
|
|
365
|
+
return '# Project\n\n(Migrated project — no description available.)\n';
|
|
366
|
+
}
|
|
367
|
+
return content.endsWith('\n') ? content : content + '\n';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Format DECISIONS.md content.
|
|
372
|
+
* If content is empty, produce the standard header.
|
|
373
|
+
*/
|
|
374
|
+
export function formatDecisions(content: string): string {
|
|
375
|
+
if (!content || !content.trim()) {
|
|
376
|
+
return '# Decisions\n\n<!-- Append-only register of architectural and pattern decisions -->\n\n| ID | Decision | Rationale | Date |\n|----|----------|-----------|------|\n';
|
|
377
|
+
}
|
|
378
|
+
return content.endsWith('\n') ? content : content + '\n';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Format a milestone CONTEXT.md.
|
|
383
|
+
* Minimal context with no depends — migrated milestones have no upstream dependencies.
|
|
384
|
+
*/
|
|
385
|
+
export function formatContext(milestoneId: string): string {
|
|
386
|
+
return `# ${milestoneId} Context\n\nMigrated milestone — no upstream dependencies.\n`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Format STATE.md.
|
|
391
|
+
* deriveState() does not read STATE.md — it recomputes from scratch.
|
|
392
|
+
* Write a minimal stub that will be overwritten on first /gsd status.
|
|
393
|
+
*/
|
|
394
|
+
export function formatState(milestones: GSDMilestone[]): string {
|
|
395
|
+
const lines: string[] = [];
|
|
396
|
+
lines.push('# GSD State');
|
|
397
|
+
lines.push('');
|
|
398
|
+
lines.push('<!-- Auto-generated. Updated by deriveState(). -->');
|
|
399
|
+
lines.push('');
|
|
400
|
+
for (const m of milestones) {
|
|
401
|
+
const doneSlices = m.slices.filter(s => s.done).length;
|
|
402
|
+
const totalSlices = m.slices.length;
|
|
403
|
+
lines.push(`## ${m.id}: ${m.title}`);
|
|
404
|
+
lines.push('');
|
|
405
|
+
lines.push(`- Slices: ${doneSlices}/${totalSlices}`);
|
|
406
|
+
lines.push('');
|
|
407
|
+
}
|
|
408
|
+
return lines.join('\n');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ─── Directory Writer Orchestrator ─────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Write a complete .gsd directory tree from a GSDProject.
|
|
415
|
+
* Iterates milestones → slices → tasks, calls format functions,
|
|
416
|
+
* and writes each file via saveFile(). Returns a manifest of written paths.
|
|
417
|
+
*
|
|
418
|
+
* Skips research/summary files when null (does not write empty stubs).
|
|
419
|
+
*/
|
|
420
|
+
export async function writeGSDDirectory(
|
|
421
|
+
project: GSDProject,
|
|
422
|
+
targetPath: string,
|
|
423
|
+
): Promise<WrittenFiles> {
|
|
424
|
+
const gsdDir = join(targetPath, '.gsd');
|
|
425
|
+
const milestonesBase = join(gsdDir, 'milestones');
|
|
426
|
+
const paths: string[] = [];
|
|
427
|
+
const counts: WrittenFiles['counts'] = {
|
|
428
|
+
roadmaps: 0,
|
|
429
|
+
plans: 0,
|
|
430
|
+
taskPlans: 0,
|
|
431
|
+
taskSummaries: 0,
|
|
432
|
+
sliceSummaries: 0,
|
|
433
|
+
research: 0,
|
|
434
|
+
requirements: 0,
|
|
435
|
+
contexts: 0,
|
|
436
|
+
other: 0,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Root-level files
|
|
440
|
+
const projectPath = join(gsdDir, 'PROJECT.md');
|
|
441
|
+
await saveFile(projectPath, formatProject(project.projectContent));
|
|
442
|
+
paths.push(projectPath);
|
|
443
|
+
counts.other++;
|
|
444
|
+
|
|
445
|
+
const decisionsPath = join(gsdDir, 'DECISIONS.md');
|
|
446
|
+
await saveFile(decisionsPath, formatDecisions(project.decisionsContent));
|
|
447
|
+
paths.push(decisionsPath);
|
|
448
|
+
counts.other++;
|
|
449
|
+
|
|
450
|
+
const statePath = join(gsdDir, 'STATE.md');
|
|
451
|
+
await saveFile(statePath, formatState(project.milestones));
|
|
452
|
+
paths.push(statePath);
|
|
453
|
+
counts.other++;
|
|
454
|
+
|
|
455
|
+
if (project.requirements.length > 0) {
|
|
456
|
+
const reqPath = join(gsdDir, 'REQUIREMENTS.md');
|
|
457
|
+
await saveFile(reqPath, formatRequirements(project.requirements));
|
|
458
|
+
paths.push(reqPath);
|
|
459
|
+
counts.requirements++;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Milestones
|
|
463
|
+
for (const milestone of project.milestones) {
|
|
464
|
+
const mDir = join(milestonesBase, milestone.id);
|
|
465
|
+
|
|
466
|
+
// Roadmap (always written, even for empty milestones)
|
|
467
|
+
const roadmapPath = join(mDir, `${milestone.id}-ROADMAP.md`);
|
|
468
|
+
await saveFile(roadmapPath, formatRoadmap(milestone));
|
|
469
|
+
paths.push(roadmapPath);
|
|
470
|
+
counts.roadmaps++;
|
|
471
|
+
|
|
472
|
+
// Context
|
|
473
|
+
const contextPath = join(mDir, `${milestone.id}-CONTEXT.md`);
|
|
474
|
+
await saveFile(contextPath, formatContext(milestone.id));
|
|
475
|
+
paths.push(contextPath);
|
|
476
|
+
counts.contexts++;
|
|
477
|
+
|
|
478
|
+
// Research (skip if null)
|
|
479
|
+
if (milestone.research !== null) {
|
|
480
|
+
const researchPath = join(mDir, `${milestone.id}-RESEARCH.md`);
|
|
481
|
+
await saveFile(researchPath, milestone.research);
|
|
482
|
+
paths.push(researchPath);
|
|
483
|
+
counts.research++;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Slices
|
|
487
|
+
for (const slice of milestone.slices) {
|
|
488
|
+
const sDir = join(mDir, 'slices', slice.id);
|
|
489
|
+
const tasksDir = join(sDir, 'tasks');
|
|
490
|
+
|
|
491
|
+
// Slice plan
|
|
492
|
+
const planPath = join(sDir, `${slice.id}-PLAN.md`);
|
|
493
|
+
await saveFile(planPath, formatPlan(slice));
|
|
494
|
+
paths.push(planPath);
|
|
495
|
+
counts.plans++;
|
|
496
|
+
|
|
497
|
+
// Slice research (skip if null)
|
|
498
|
+
if (slice.research !== null) {
|
|
499
|
+
const sliceResearchPath = join(sDir, `${slice.id}-RESEARCH.md`);
|
|
500
|
+
await saveFile(sliceResearchPath, slice.research);
|
|
501
|
+
paths.push(sliceResearchPath);
|
|
502
|
+
counts.research++;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Slice summary (skip if null)
|
|
506
|
+
if (slice.summary !== null) {
|
|
507
|
+
const summaryContent = formatSliceSummary(slice, milestone.id);
|
|
508
|
+
if (summaryContent) {
|
|
509
|
+
const summaryPath = join(sDir, `${slice.id}-SUMMARY.md`);
|
|
510
|
+
await saveFile(summaryPath, summaryContent);
|
|
511
|
+
paths.push(summaryPath);
|
|
512
|
+
counts.sliceSummaries++;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Tasks
|
|
517
|
+
for (const task of slice.tasks) {
|
|
518
|
+
// Task plan (always written)
|
|
519
|
+
const taskPlanPath = join(tasksDir, `${task.id}-PLAN.md`);
|
|
520
|
+
await saveFile(taskPlanPath, formatTaskPlan(task, slice.id, milestone.id));
|
|
521
|
+
paths.push(taskPlanPath);
|
|
522
|
+
counts.taskPlans++;
|
|
523
|
+
|
|
524
|
+
// Task summary (skip if null)
|
|
525
|
+
if (task.summary !== null) {
|
|
526
|
+
const taskSummaryContent = formatTaskSummary(task, slice.id, milestone.id);
|
|
527
|
+
if (taskSummaryContent) {
|
|
528
|
+
const taskSummaryPath = join(tasksDir, `${task.id}-SUMMARY.md`);
|
|
529
|
+
await saveFile(taskSummaryPath, taskSummaryContent);
|
|
530
|
+
paths.push(taskSummaryPath);
|
|
531
|
+
counts.taskSummaries++;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return { paths, counts };
|
|
539
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
## Review Migrated .gsd Directory
|
|
2
|
+
|
|
3
|
+
A `/gsd migrate` command just wrote a `.gsd/` directory from an old `.planning` source. Your job is to audit the output and verify it meets GSD-2 standards before the user starts working with it.
|
|
4
|
+
|
|
5
|
+
### Source
|
|
6
|
+
- Old `.planning` directory: `{{sourcePath}}`
|
|
7
|
+
- Written `.gsd` directory: `{{gsdPath}}`
|
|
8
|
+
|
|
9
|
+
### Migration Stats
|
|
10
|
+
{{previewStats}}
|
|
11
|
+
|
|
12
|
+
### Review Checklist
|
|
13
|
+
|
|
14
|
+
Work through each check. Report PASS/FAIL with specifics. Fix anything fixable in-place.
|
|
15
|
+
|
|
16
|
+
#### 1. Structure Validation
|
|
17
|
+
- Run `deriveState()` on the `.gsd` directory (import from `state.ts`, pass the **project root** as basePath)
|
|
18
|
+
- Confirm it returns a coherent phase (not `pre-planning` unless the project is truly empty)
|
|
19
|
+
- Confirm activeMilestone, activeSlice, activeTask are sensible for the project's completion state
|
|
20
|
+
- Confirm progress counts match the migration preview stats
|
|
21
|
+
|
|
22
|
+
#### 2. Roadmap Quality
|
|
23
|
+
- Read `M001-ROADMAP.md` (and any other milestone roadmaps)
|
|
24
|
+
- Confirm slice entries have meaningful titles (not file paths or garbled text)
|
|
25
|
+
- Confirm `[x]`/`[ ]` completion markers are correct relative to the old roadmap
|
|
26
|
+
- Confirm vision statement is present and meaningful (not empty or "Migration")
|
|
27
|
+
|
|
28
|
+
#### 3. Content Spot-Check
|
|
29
|
+
- Pick 2-3 slices with the most tasks and read their plan files
|
|
30
|
+
- Confirm task titles and descriptions carry over meaningfully from the old plans
|
|
31
|
+
- Confirm summary files exist for completed tasks and contain relevant content
|
|
32
|
+
- Check that research files (if present) contain consolidated content, not empty stubs
|
|
33
|
+
|
|
34
|
+
#### 4. Requirements (if any)
|
|
35
|
+
- Read REQUIREMENTS.md
|
|
36
|
+
- Confirm requirement IDs are present and non-duplicate
|
|
37
|
+
- Confirm statuses make sense: completed old requirements should be `validated`, in-progress should be `active`
|
|
38
|
+
|
|
39
|
+
#### 5. PROJECT.md
|
|
40
|
+
- Read the written PROJECT.md
|
|
41
|
+
- Confirm it contains the old project's description, not boilerplate
|
|
42
|
+
- Confirm it reads like a useful project summary
|
|
43
|
+
|
|
44
|
+
#### 6. Decisions
|
|
45
|
+
- If DECISIONS.md was written, confirm it contains extracted decisions from old summaries (or is empty if no decisions existed)
|
|
46
|
+
|
|
47
|
+
### Output Format
|
|
48
|
+
|
|
49
|
+
Summarize your findings as:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Migration Review: <project name>
|
|
53
|
+
================================
|
|
54
|
+
Structure: PASS/FAIL — <details>
|
|
55
|
+
Roadmap: PASS/FAIL — <details>
|
|
56
|
+
Content: PASS/FAIL — <details>
|
|
57
|
+
Requirements: PASS/FAIL/SKIP — <details>
|
|
58
|
+
Project: PASS/FAIL — <details>
|
|
59
|
+
Decisions: PASS/FAIL/SKIP — <details>
|
|
60
|
+
|
|
61
|
+
Overall: PASS / PASS WITH NOTES / FAIL
|
|
62
|
+
Issues: <list any problems found>
|
|
63
|
+
Fixes applied: <list any in-place fixes made>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If the overall result is FAIL, explain what needs manual attention. If PASS WITH NOTES, explain what's imperfect but acceptable. If PASS, confirm the `.gsd` directory is ready for GSD-2 auto-mode.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
You are merging GSD artifacts from worktree **{{worktreeName}}** (branch `{{worktreeBranch}}`) into target branch `{{mainBranch}}`.
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
The worktree was created as a parallel workspace. It may contain new milestones, updated roadmaps, new plans, research, decisions, or other GSD artifacts that need to be reconciled with the main branch.
|
|
6
|
+
|
|
7
|
+
### Commit History (worktree)
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
{{commitLog}}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### GSD Artifact Changes
|
|
14
|
+
|
|
15
|
+
**Added files:**
|
|
16
|
+
{{addedFiles}}
|
|
17
|
+
|
|
18
|
+
**Modified files:**
|
|
19
|
+
{{modifiedFiles}}
|
|
20
|
+
|
|
21
|
+
**Removed files:**
|
|
22
|
+
{{removedFiles}}
|
|
23
|
+
|
|
24
|
+
### Full Diff
|
|
25
|
+
|
|
26
|
+
```diff
|
|
27
|
+
{{fullDiff}}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Your Task
|
|
31
|
+
|
|
32
|
+
Analyze the changes and guide the merge. Follow these steps exactly:
|
|
33
|
+
|
|
34
|
+
### Step 1: Categorize Changes
|
|
35
|
+
|
|
36
|
+
Classify each changed GSD artifact:
|
|
37
|
+
- **New milestones** — entirely new M###/ directories with roadmaps
|
|
38
|
+
- **New slices/tasks** — new planning artifacts within existing milestones
|
|
39
|
+
- **Updated roadmaps** — modifications to existing M###-ROADMAP.md files
|
|
40
|
+
- **Updated plans** — modifications to existing slice or task plans
|
|
41
|
+
- **Research/context** — new or updated RESEARCH.md, CONTEXT.md files
|
|
42
|
+
- **Decisions** — changes to DECISIONS.md
|
|
43
|
+
- **Requirements** — changes to REQUIREMENTS.md
|
|
44
|
+
- **Other** — anything else
|
|
45
|
+
|
|
46
|
+
### Step 2: Conflict Assessment
|
|
47
|
+
|
|
48
|
+
For each **modified** file, check whether the main branch version has also changed since the worktree branched off. Flag any files where both branches have diverged — these need manual reconciliation.
|
|
49
|
+
|
|
50
|
+
Read the current main-branch version of each modified file and compare it against both the worktree version and the common ancestor to identify:
|
|
51
|
+
- **Clean merges** — main hasn't changed, worktree changes can apply directly
|
|
52
|
+
- **Conflicts** — both branches changed the same file; needs reconciliation
|
|
53
|
+
- **Stale changes** — worktree modified a file that main has since replaced or removed
|
|
54
|
+
|
|
55
|
+
### Step 3: Merge Strategy
|
|
56
|
+
|
|
57
|
+
Present a merge plan to the user:
|
|
58
|
+
|
|
59
|
+
1. For **clean merges**: list files that will merge without conflict
|
|
60
|
+
2. For **conflicts**: show both versions side-by-side and propose a reconciled version
|
|
61
|
+
3. For **new artifacts**: confirm they should be added to the main branch
|
|
62
|
+
4. For **removed artifacts**: confirm the removals are intentional
|
|
63
|
+
|
|
64
|
+
Ask the user to confirm the merge plan before proceeding.
|
|
65
|
+
|
|
66
|
+
### Step 4: Execute Merge
|
|
67
|
+
|
|
68
|
+
Once confirmed:
|
|
69
|
+
|
|
70
|
+
1. If there are conflicts requiring manual reconciliation, apply the reconciled versions to the main branch working tree
|
|
71
|
+
2. Run `git merge --squash {{worktreeBranch}}` to bring in all changes
|
|
72
|
+
3. Review the staged changes — if any reconciled files need adjustment, apply them now
|
|
73
|
+
4. Commit with message: `merge(worktree/{{worktreeName}}): <summary of what was merged>`
|
|
74
|
+
5. Report what was merged
|
|
75
|
+
|
|
76
|
+
### Step 5: Cleanup Prompt
|
|
77
|
+
|
|
78
|
+
After a successful merge, ask the user whether to:
|
|
79
|
+
- **Remove the worktree** — delete `.gsd/worktrees/{{worktreeName}}/` and the `{{worktreeBranch}}` branch
|
|
80
|
+
- **Keep the worktree** — leave it for continued parallel work
|
|
81
|
+
|
|
82
|
+
If the user chooses to remove it, run `/worktree remove {{worktreeName}}`.
|
|
83
|
+
|
|
84
|
+
## Important
|
|
85
|
+
|
|
86
|
+
- Never silently discard changes from either branch
|
|
87
|
+
- When in doubt about a conflict, show both versions and ask the user
|
|
88
|
+
- Preserve all GSD artifact formatting conventions (frontmatter, section structure, checkbox states)
|
|
89
|
+
- If the worktree introduced new milestone IDs that conflict with main, flag this immediately
|