gsd-opencode 1.33.3 → 1.35.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.
Files changed (118) hide show
  1. package/agents/gsd-advisor-researcher.md +23 -0
  2. package/agents/gsd-ai-researcher.md +142 -0
  3. package/agents/gsd-code-fixer.md +523 -0
  4. package/agents/gsd-code-reviewer.md +361 -0
  5. package/agents/gsd-debugger.md +14 -1
  6. package/agents/gsd-domain-researcher.md +162 -0
  7. package/agents/gsd-eval-auditor.md +170 -0
  8. package/agents/gsd-eval-planner.md +161 -0
  9. package/agents/gsd-executor.md +70 -7
  10. package/agents/gsd-framework-selector.md +167 -0
  11. package/agents/gsd-intel-updater.md +320 -0
  12. package/agents/gsd-phase-researcher.md +26 -0
  13. package/agents/gsd-plan-checker.md +12 -0
  14. package/agents/gsd-planner.md +16 -6
  15. package/agents/gsd-project-researcher.md +23 -0
  16. package/agents/gsd-ui-researcher.md +23 -0
  17. package/agents/gsd-verifier.md +55 -1
  18. package/commands/gsd/gsd-ai-integration-phase.md +36 -0
  19. package/commands/gsd/gsd-audit-fix.md +33 -0
  20. package/commands/gsd/gsd-autonomous.md +1 -0
  21. package/commands/gsd/gsd-code-review-fix.md +52 -0
  22. package/commands/gsd/gsd-code-review.md +55 -0
  23. package/commands/gsd/gsd-eval-review.md +32 -0
  24. package/commands/gsd/gsd-explore.md +27 -0
  25. package/commands/gsd/gsd-from-gsd2.md +45 -0
  26. package/commands/gsd/gsd-import.md +36 -0
  27. package/commands/gsd/gsd-intel.md +183 -0
  28. package/commands/gsd/gsd-next.md +2 -0
  29. package/commands/gsd/gsd-reapply-patches.md +58 -3
  30. package/commands/gsd/gsd-review.md +4 -2
  31. package/commands/gsd/gsd-scan.md +26 -0
  32. package/commands/gsd/gsd-undo.md +34 -0
  33. package/commands/gsd/gsd-workstreams.md +6 -6
  34. package/get-shit-done/bin/gsd-tools.cjs +143 -5
  35. package/get-shit-done/bin/lib/commands.cjs +10 -2
  36. package/get-shit-done/bin/lib/config.cjs +71 -37
  37. package/get-shit-done/bin/lib/core.cjs +70 -8
  38. package/get-shit-done/bin/lib/gsd2-import.cjs +511 -0
  39. package/get-shit-done/bin/lib/init.cjs +20 -6
  40. package/get-shit-done/bin/lib/intel.cjs +660 -0
  41. package/get-shit-done/bin/lib/learnings.cjs +378 -0
  42. package/get-shit-done/bin/lib/milestone.cjs +25 -15
  43. package/get-shit-done/bin/lib/model-profiles.cjs +17 -17
  44. package/get-shit-done/bin/lib/phase.cjs +148 -112
  45. package/get-shit-done/bin/lib/roadmap.cjs +12 -5
  46. package/get-shit-done/bin/lib/security.cjs +119 -0
  47. package/get-shit-done/bin/lib/state.cjs +283 -221
  48. package/get-shit-done/bin/lib/template.cjs +8 -4
  49. package/get-shit-done/bin/lib/verify.cjs +42 -5
  50. package/get-shit-done/references/ai-evals.md +156 -0
  51. package/get-shit-done/references/ai-frameworks.md +186 -0
  52. package/get-shit-done/references/common-bug-patterns.md +114 -0
  53. package/get-shit-done/references/few-shot-examples/plan-checker.md +73 -0
  54. package/get-shit-done/references/few-shot-examples/verifier.md +109 -0
  55. package/get-shit-done/references/gates.md +70 -0
  56. package/get-shit-done/references/ios-scaffold.md +123 -0
  57. package/get-shit-done/references/model-profile-resolution.md +6 -7
  58. package/get-shit-done/references/model-profiles.md +20 -14
  59. package/get-shit-done/references/planning-config.md +237 -0
  60. package/get-shit-done/references/thinking-models-debug.md +44 -0
  61. package/get-shit-done/references/thinking-models-execution.md +50 -0
  62. package/get-shit-done/references/thinking-models-planning.md +62 -0
  63. package/get-shit-done/references/thinking-models-research.md +50 -0
  64. package/get-shit-done/references/thinking-models-verification.md +55 -0
  65. package/get-shit-done/references/thinking-partner.md +96 -0
  66. package/get-shit-done/references/universal-anti-patterns.md +6 -1
  67. package/get-shit-done/references/verification-overrides.md +227 -0
  68. package/get-shit-done/templates/AI-SPEC.md +246 -0
  69. package/get-shit-done/workflows/add-tests.md +3 -0
  70. package/get-shit-done/workflows/add-todo.md +2 -0
  71. package/get-shit-done/workflows/ai-integration-phase.md +284 -0
  72. package/get-shit-done/workflows/audit-fix.md +154 -0
  73. package/get-shit-done/workflows/autonomous.md +33 -2
  74. package/get-shit-done/workflows/check-todos.md +2 -0
  75. package/get-shit-done/workflows/cleanup.md +2 -0
  76. package/get-shit-done/workflows/code-review-fix.md +497 -0
  77. package/get-shit-done/workflows/code-review.md +515 -0
  78. package/get-shit-done/workflows/complete-milestone.md +40 -15
  79. package/get-shit-done/workflows/diagnose-issues.md +1 -1
  80. package/get-shit-done/workflows/discovery-phase.md +3 -1
  81. package/get-shit-done/workflows/discuss-phase-assumptions.md +1 -1
  82. package/get-shit-done/workflows/discuss-phase.md +21 -7
  83. package/get-shit-done/workflows/do.md +2 -0
  84. package/get-shit-done/workflows/docs-update.md +2 -0
  85. package/get-shit-done/workflows/eval-review.md +155 -0
  86. package/get-shit-done/workflows/execute-phase.md +307 -57
  87. package/get-shit-done/workflows/execute-plan.md +64 -93
  88. package/get-shit-done/workflows/explore.md +136 -0
  89. package/get-shit-done/workflows/help.md +1 -1
  90. package/get-shit-done/workflows/import.md +273 -0
  91. package/get-shit-done/workflows/inbox.md +387 -0
  92. package/get-shit-done/workflows/manager.md +4 -10
  93. package/get-shit-done/workflows/new-milestone.md +3 -1
  94. package/get-shit-done/workflows/new-project.md +2 -0
  95. package/get-shit-done/workflows/new-workspace.md +2 -0
  96. package/get-shit-done/workflows/next.md +56 -0
  97. package/get-shit-done/workflows/note.md +2 -0
  98. package/get-shit-done/workflows/plan-phase.md +97 -17
  99. package/get-shit-done/workflows/plant-seed.md +3 -0
  100. package/get-shit-done/workflows/pr-branch.md +41 -13
  101. package/get-shit-done/workflows/profile-user.md +4 -2
  102. package/get-shit-done/workflows/quick.md +99 -4
  103. package/get-shit-done/workflows/remove-workspace.md +2 -0
  104. package/get-shit-done/workflows/review.md +53 -6
  105. package/get-shit-done/workflows/scan.md +98 -0
  106. package/get-shit-done/workflows/secure-phase.md +2 -0
  107. package/get-shit-done/workflows/settings.md +18 -3
  108. package/get-shit-done/workflows/ship.md +3 -0
  109. package/get-shit-done/workflows/ui-phase.md +10 -2
  110. package/get-shit-done/workflows/ui-review.md +2 -0
  111. package/get-shit-done/workflows/undo.md +314 -0
  112. package/get-shit-done/workflows/update.md +2 -0
  113. package/get-shit-done/workflows/validate-phase.md +2 -0
  114. package/get-shit-done/workflows/verify-phase.md +83 -0
  115. package/get-shit-done/workflows/verify-work.md +12 -1
  116. package/package.json +1 -1
  117. package/skills/gsd-code-review/SKILL.md +48 -0
  118. package/skills/gsd-code-review-fix/SKILL.md +44 -0
@@ -0,0 +1,511 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * gsd2-import — Reverse migration from GSD-2 (.gsd/) to GSD v1 (.planning/)
5
+ *
6
+ * Reads a GSD-2 project directory structure and produces a complete
7
+ * .planning/ artifact tree in GSD v1 format.
8
+ *
9
+ * GSD-2 hierarchy: Milestone → Slice → task
10
+ * GSD v1 hierarchy: Milestone (in ROADMAP.md) → Phase → Plan
11
+ *
12
+ * Mapping rules:
13
+ * - Slices are numbered sequentially across all milestones (01, 02, …)
14
+ * - Tasks within a slice become plans (01-01, 01-02, …)
15
+ * - Completed slices ([x] in ROADMAP) → [x] phases in ROADMAP.md
16
+ * - Tasks with a SUMMARY file → SUMMARY.md written
17
+ * - Slice RESEARCH.md → phase XX-RESEARCH.md
18
+ */
19
+
20
+ const fs = require('node:fs');
21
+ const path = require('node:path');
22
+
23
+ // ─── Utilities ──────────────────────────────────────────────────────────────
24
+
25
+ function readOptional(filePath) {
26
+ try { return fs.readFileSync(filePath, 'utf8'); } catch { return null; }
27
+ }
28
+
29
+ function zeroPad(n, width = 2) {
30
+ return String(n).padStart(width, '0');
31
+ }
32
+
33
+ function slugify(title) {
34
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
35
+ }
36
+
37
+ // ─── GSD-2 Parser ───────────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Find the .gsd/ directory starting from a project root.
41
+ * Returns the absolute path or null if not found.
42
+ */
43
+ function findGsd2Root(startPath) {
44
+ if (path.basename(startPath) === '.gsd' && fs.existsSync(startPath)) {
45
+ return startPath;
46
+ }
47
+ const candidate = path.join(startPath, '.gsd');
48
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
49
+ return candidate;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ /**
55
+ * Parse the ## Slices section from a GSD-2 milestone ROADMAP.md.
56
+ * Each slice entry looks like:
57
+ * - [x] **S01: Title** `risk:medium` `depends:[S00]`
58
+ */
59
+ function parseSlicesFromRoadmap(content) {
60
+ const slices = [];
61
+ const sectionMatch = content.match(/## Slices\n([\s\S]*?)(?:\n## |\n# |$)/);
62
+ if (!sectionMatch) return slices;
63
+
64
+ for (const line of sectionMatch[1].split('\n')) {
65
+ const m = line.match(/^- \[([x ])\]\s+\*\*(\w+):\s*([^*]+)\*\*/);
66
+ if (!m) continue;
67
+ slices.push({ done: m[1] === 'x', id: m[2].trim(), title: m[3].trim() });
68
+ }
69
+ return slices;
70
+ }
71
+
72
+ /**
73
+ * Parse the milestone title from the first heading in a GSD-2 ROADMAP.md.
74
+ * Format: # M001: Title
75
+ */
76
+ function parseMilestoneTitle(content) {
77
+ const m = content.match(/^# \w+:\s*(.+)/m);
78
+ return m ? m[1].trim() : null;
79
+ }
80
+
81
+ /**
82
+ * Parse a task title from a GSD-2 T##-PLAN.md.
83
+ * Format: # T01: Title
84
+ */
85
+ function parseTaskTitle(content, fallback) {
86
+ const m = content.match(/^# \w+:\s*(.+)/m);
87
+ return m ? m[1].trim() : fallback;
88
+ }
89
+
90
+ /**
91
+ * Parse the ## Description body from a GSD-2 task plan.
92
+ */
93
+ function parseTaskDescription(content) {
94
+ const m = content.match(/## Description\n+([\s\S]+?)(?:\n## |\n# |$)/);
95
+ return m ? m[1].trim() : '';
96
+ }
97
+
98
+ /**
99
+ * Parse ## Must-Haves items from a GSD-2 task plan.
100
+ */
101
+ function parseTaskMustHaves(content) {
102
+ const m = content.match(/## Must-Haves\n+([\s\S]+?)(?:\n## |\n# |$)/);
103
+ if (!m) return [];
104
+ return m[1].split('\n')
105
+ .map(l => l.match(/^- \[[ x]\]\s*(.+)/))
106
+ .filter(Boolean)
107
+ .map(match => match[1].trim());
108
+ }
109
+
110
+ /**
111
+ * read all task plan files from a GSD-2 tasks/ directory.
112
+ */
113
+ function readTasksDir(tasksDir) {
114
+ if (!fs.existsSync(tasksDir)) return [];
115
+
116
+ return fs.readdirSync(tasksDir)
117
+ .filter(f => f.endsWith('-PLAN.md'))
118
+ .sort()
119
+ .map(tf => {
120
+ const tid = tf.replace('-PLAN.md', '');
121
+ const plan = readOptional(path.join(tasksDir, tf));
122
+ const summary = readOptional(path.join(tasksDir, `${tid}-SUMMARY.md`));
123
+ return {
124
+ id: tid,
125
+ title: plan ? parseTaskTitle(plan, tid) : tid,
126
+ description: plan ? parseTaskDescription(plan) : '',
127
+ mustHaves: plan ? parseTaskMustHaves(plan) : [],
128
+ plan,
129
+ summary,
130
+ done: !!summary,
131
+ };
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Parse a complete GSD-2 .gsd/ directory into a structured representation.
137
+ */
138
+ function parseGsd2(gsdDir) {
139
+ const data = {
140
+ projectContent: readOptional(path.join(gsdDir, 'PROJECT.md')),
141
+ requirements: readOptional(path.join(gsdDir, 'REQUIREMENTS.md')),
142
+ milestones: [],
143
+ };
144
+
145
+ const milestonesBase = path.join(gsdDir, 'milestones');
146
+ if (!fs.existsSync(milestonesBase)) return data;
147
+
148
+ const milestoneIds = fs.readdirSync(milestonesBase)
149
+ .filter(d => fs.statSync(path.join(milestonesBase, d)).isDirectory())
150
+ .sort();
151
+
152
+ for (const mid of milestoneIds) {
153
+ const mDir = path.join(milestonesBase, mid);
154
+ const roadmapContent = readOptional(path.join(mDir, `${mid}-ROADMAP.md`));
155
+ const slicesDir = path.join(mDir, 'slices');
156
+
157
+ const sliceInfos = roadmapContent ? parseSlicesFromRoadmap(roadmapContent) : [];
158
+
159
+ const slices = sliceInfos.map(info => {
160
+ const sDir = path.join(slicesDir, info.id);
161
+ const hasSDir = fs.existsSync(sDir);
162
+ return {
163
+ id: info.id,
164
+ title: info.title,
165
+ done: info.done,
166
+ plan: hasSDir ? readOptional(path.join(sDir, `${info.id}-PLAN.md`)) : null,
167
+ summary: hasSDir ? readOptional(path.join(sDir, `${info.id}-SUMMARY.md`)) : null,
168
+ research: hasSDir ? readOptional(path.join(sDir, `${info.id}-RESEARCH.md`)) : null,
169
+ context: hasSDir ? readOptional(path.join(sDir, `${info.id}-CONTEXT.md`)) : null,
170
+ tasks: hasSDir ? readTasksDir(path.join(sDir, 'tasks')) : [],
171
+ };
172
+ });
173
+
174
+ data.milestones.push({
175
+ id: mid,
176
+ title: roadmapContent ? (parseMilestoneTitle(roadmapContent) ?? mid) : mid,
177
+ research: readOptional(path.join(mDir, `${mid}-RESEARCH.md`)),
178
+ slices,
179
+ });
180
+ }
181
+
182
+ return data;
183
+ }
184
+
185
+ // ─── Artifact Builders ──────────────────────────────────────────────────────
186
+
187
+ /**
188
+ * Build a GSD v1 PLAN.md from a GSD-2 task.
189
+ */
190
+ function buildPlanMd(task, phasePrefix, planPrefix, phaseSlug, milestoneTitle) {
191
+ const lines = [
192
+ '---',
193
+ `phase: "${phasePrefix}"`,
194
+ `plan: "${planPrefix}"`,
195
+ 'type: "implementation"',
196
+ '---',
197
+ '',
198
+ '<objective>',
199
+ task.title,
200
+ '</objective>',
201
+ '',
202
+ '<context>',
203
+ `Phase: ${phasePrefix} (${phaseSlug}) — Milestone: ${milestoneTitle}`,
204
+ ];
205
+
206
+ if (task.description) {
207
+ lines.push('', task.description);
208
+ }
209
+
210
+ lines.push('</context>');
211
+
212
+ if (task.mustHaves.length > 0) {
213
+ lines.push('', '<must_haves>');
214
+ for (const mh of task.mustHaves) {
215
+ lines.push(`- ${mh}`);
216
+ }
217
+ lines.push('</must_haves>');
218
+ }
219
+
220
+ return lines.join('\n') + '\n';
221
+ }
222
+
223
+ /**
224
+ * Build a GSD v1 SUMMARY.md from a GSD-2 task summary.
225
+ * Strips the GSD-2 frontmatter and preserves the body.
226
+ */
227
+ function buildSummaryMd(task, phasePrefix, planPrefix) {
228
+ const raw = task.summary || '';
229
+ // Strip GSD-2 frontmatter block (--- ... ---) if present
230
+ const bodyMatch = raw.match(/^---[\s\S]*?---\n+([\s\S]*)$/);
231
+ const body = bodyMatch ? bodyMatch[1].trim() : raw.trim();
232
+
233
+ return [
234
+ '---',
235
+ `phase: "${phasePrefix}"`,
236
+ `plan: "${planPrefix}"`,
237
+ '---',
238
+ '',
239
+ body || 'task completed (migrated from GSD-2).',
240
+ '',
241
+ ].join('\n');
242
+ }
243
+
244
+ /**
245
+ * Build a GSD v1 XX-CONTEXT.md from a GSD-2 slice.
246
+ */
247
+ function buildContextMd(slice, phasePrefix) {
248
+ const lines = [
249
+ `# Phase ${phasePrefix} Context`,
250
+ '',
251
+ `Migrated from GSD-2 slice ${slice.id}: ${slice.title}`,
252
+ ];
253
+
254
+ const extra = slice.context || '';
255
+ if (extra.trim()) {
256
+ lines.push('', extra.trim());
257
+ }
258
+
259
+ return lines.join('\n') + '\n';
260
+ }
261
+
262
+ /**
263
+ * Build the GSD v1 ROADMAP.md with milestone-sectioned format.
264
+ */
265
+ function buildRoadmapMd(milestones, phaseMap) {
266
+ const lines = ['# Roadmap', ''];
267
+
268
+ for (const milestone of milestones) {
269
+ lines.push(`## ${milestone.id}: ${milestone.title}`, '');
270
+ const mPhases = phaseMap.filter(p => p.milestoneId === milestone.id);
271
+ for (const { slice, phaseNum } of mPhases) {
272
+ const prefix = zeroPad(phaseNum);
273
+ const slug = slugify(slice.title);
274
+ const check = slice.done ? 'x' : ' ';
275
+ lines.push(`- [${check}] **Phase ${prefix}: ${slug}** — ${slice.title}`);
276
+ }
277
+ lines.push('');
278
+ }
279
+
280
+ return lines.join('\n');
281
+ }
282
+
283
+ /**
284
+ * Build the GSD v1 STATE.md reflecting the current position in the project.
285
+ */
286
+ function buildStateMd(phaseMap) {
287
+ const currentEntry = phaseMap.find(p => !p.slice.done);
288
+ const totalPhases = phaseMap.length;
289
+ const donePhases = phaseMap.filter(p => p.slice.done).length;
290
+ const pct = totalPhases > 0 ? Math.round((donePhases / totalPhases) * 100) : 0;
291
+
292
+ const currentPhaseNum = currentEntry ? zeroPad(currentEntry.phaseNum) : zeroPad(totalPhases);
293
+ const currentSlug = currentEntry ? slugify(currentEntry.slice.title) : 'complete';
294
+ const status = currentEntry ? 'Ready to plan' : 'All phases complete';
295
+
296
+ const filled = Math.round(pct / 10);
297
+ const bar = `[${'█'.repeat(filled)}${'░'.repeat(10 - filled)}]`;
298
+ const today = new Date().toISOString().split('T')[0];
299
+
300
+ return [
301
+ '# Project State',
302
+ '',
303
+ '## Project Reference',
304
+ '',
305
+ 'See: .planning/PROJECT.md',
306
+ '',
307
+ `**Current focus:** Phase ${currentPhaseNum} (${currentSlug})`,
308
+ '',
309
+ '## Current Position',
310
+ '',
311
+ `Phase: ${currentPhaseNum} of ${zeroPad(totalPhases)} (${currentSlug})`,
312
+ `Status: ${status}`,
313
+ `Last activity: ${today} — Migrated from GSD-2`,
314
+ '',
315
+ `Progress: ${bar} ${pct}%`,
316
+ '',
317
+ '## Accumulated Context',
318
+ '',
319
+ '### Decisions',
320
+ '',
321
+ 'Migrated from GSD-2. Review PROJECT.md for key decisions.',
322
+ '',
323
+ '### Blockers/Concerns',
324
+ '',
325
+ 'None.',
326
+ '',
327
+ '## Session Continuity',
328
+ '',
329
+ `Last session: ${today}`,
330
+ 'Stopped at: Migration from GSD-2 completed',
331
+ 'Resume file: None',
332
+ '',
333
+ ].join('\n');
334
+ }
335
+
336
+ // ─── Transformer ─────────────────────────────────────────────────────────────
337
+
338
+ /**
339
+ * Convert parsed GSD-2 data into a map of relative path → file content.
340
+ * All paths are relative to the .planning/ root.
341
+ */
342
+ function buildPlanningArtifacts(gsd2Data) {
343
+ const artifacts = new Map();
344
+
345
+ // Passthrough files
346
+ artifacts.set('PROJECT.md', gsd2Data.projectContent || '# Project\n\n(Migrated from GSD-2)\n');
347
+ if (gsd2Data.requirements) {
348
+ artifacts.set('REQUIREMENTS.md', gsd2Data.requirements);
349
+ }
350
+
351
+ // Minimal valid v1 config
352
+ artifacts.set('config.json', JSON.stringify({ version: 1 }, null, 2) + '\n');
353
+
354
+ // Build sequential phase map: flatten Milestones → Slices into numbered phases
355
+ const phaseMap = [];
356
+ let phaseNum = 1;
357
+ for (const milestone of gsd2Data.milestones) {
358
+ for (const slice of milestone.slices) {
359
+ phaseMap.push({ milestoneId: milestone.id, milestoneTitle: milestone.title, slice, phaseNum });
360
+ phaseNum++;
361
+ }
362
+ }
363
+
364
+ artifacts.set('ROADMAP.md', buildRoadmapMd(gsd2Data.milestones, phaseMap));
365
+ artifacts.set('STATE.md', buildStateMd(phaseMap));
366
+
367
+ for (const { slice, phaseNum, milestoneTitle } of phaseMap) {
368
+ const prefix = zeroPad(phaseNum);
369
+ const slug = slugify(slice.title);
370
+ const dir = `phases/${prefix}-${slug}`;
371
+
372
+ artifacts.set(`${dir}/${prefix}-CONTEXT.md`, buildContextMd(slice, prefix));
373
+
374
+ if (slice.research) {
375
+ artifacts.set(`${dir}/${prefix}-RESEARCH.md`, slice.research);
376
+ }
377
+
378
+ for (let i = 0; i < slice.tasks.length; i++) {
379
+ const task = slice.tasks[i];
380
+ const planPrefix = zeroPad(i + 1);
381
+
382
+ artifacts.set(
383
+ `${dir}/${prefix}-${planPrefix}-PLAN.md`,
384
+ buildPlanMd(task, prefix, planPrefix, slug, milestoneTitle)
385
+ );
386
+
387
+ if (task.done && task.summary) {
388
+ artifacts.set(
389
+ `${dir}/${prefix}-${planPrefix}-SUMMARY.md`,
390
+ buildSummaryMd(task, prefix, planPrefix)
391
+ );
392
+ }
393
+ }
394
+ }
395
+
396
+ return artifacts;
397
+ }
398
+
399
+ // ─── Preview ─────────────────────────────────────────────────────────────────
400
+
401
+ /**
402
+ * Format a dry-run preview string for display before writing.
403
+ */
404
+ function buildPreview(gsd2Data, artifacts) {
405
+ const lines = ['Preview — files that will be created in .planning/:'];
406
+
407
+ for (const rel of artifacts.keys()) {
408
+ lines.push(` ${rel}`);
409
+ }
410
+
411
+ const totalSlices = gsd2Data.milestones.reduce((s, m) => s + m.slices.length, 0);
412
+ const doneSlices = gsd2Data.milestones.reduce((s, m) => s + m.slices.filter(sl => sl.done).length, 0);
413
+ const allTasks = gsd2Data.milestones.flatMap(m => m.slices.flatMap(sl => sl.tasks));
414
+ const doneTasks = allTasks.filter(t => t.done).length;
415
+
416
+ lines.push('');
417
+ lines.push(`Milestones: ${gsd2Data.milestones.length}`);
418
+ lines.push(`Phases (slices): ${totalSlices} (${doneSlices} completed)`);
419
+ lines.push(`Plans (tasks): ${allTasks.length} (${doneTasks} completed)`);
420
+ lines.push('');
421
+ lines.push('Cannot migrate automatically:');
422
+ lines.push(' - GSD-2 cost/token ledger (no v1 equivalent)');
423
+ lines.push(' - GSD-2 database state (rebuilt from files on first /gsd-health)');
424
+ lines.push(' - VS Code extension state');
425
+
426
+ return lines.join('\n');
427
+ }
428
+
429
+ // ─── Writer ───────────────────────────────────────────────────────────────────
430
+
431
+ /**
432
+ * write all artifacts to the .planning/ directory.
433
+ */
434
+ function writePlanningDir(artifacts, planningRoot) {
435
+ for (const [rel, content] of artifacts) {
436
+ const absPath = path.join(planningRoot, rel);
437
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
438
+ fs.writeFileSync(absPath, content, 'utf8');
439
+ }
440
+ }
441
+
442
+ // ─── Command Handler ──────────────────────────────────────────────────────────
443
+
444
+ /**
445
+ * Entry point called from gsd-tools.cjs.
446
+ * Supports: --force, --dry-run, --path <dir>
447
+ */
448
+ function cmdFromGsd2(args, cwd, raw) {
449
+ const { output, error } = require('./core.cjs');
450
+
451
+ const force = args.includes('--force');
452
+ const dryRun = args.includes('--dry-run');
453
+
454
+ const pathIdx = args.indexOf('--path');
455
+ const projectDir = pathIdx >= 0 && args[pathIdx + 1]
456
+ ? path.resolve(cwd, args[pathIdx + 1])
457
+ : cwd;
458
+
459
+ const gsdDir = findGsd2Root(projectDir);
460
+ if (!gsdDir) {
461
+ return output({ success: false, error: `No .gsd/ directory found in ${projectDir}` }, raw);
462
+ }
463
+
464
+ const planningRoot = path.join(path.dirname(gsdDir), '.planning');
465
+ if (fs.existsSync(planningRoot) && !force) {
466
+ return output({
467
+ success: false,
468
+ error: `.planning/ already exists at ${planningRoot}. Pass --force to overwrite.`,
469
+ }, raw);
470
+ }
471
+
472
+ const gsd2Data = parseGsd2(gsdDir);
473
+ const artifacts = buildPlanningArtifacts(gsd2Data);
474
+ const preview = buildPreview(gsd2Data, artifacts);
475
+
476
+ if (dryRun) {
477
+ return output({ success: true, dryRun: true, preview }, raw);
478
+ }
479
+
480
+ writePlanningDir(artifacts, planningRoot);
481
+
482
+ return output({
483
+ success: true,
484
+ planningDir: planningRoot,
485
+ filesWritten: artifacts.size,
486
+ milestones: gsd2Data.milestones.length,
487
+ preview,
488
+ }, raw);
489
+ }
490
+
491
+ module.exports = {
492
+ findGsd2Root,
493
+ parseGsd2,
494
+ buildPlanningArtifacts,
495
+ buildPreview,
496
+ writePlanningDir,
497
+ cmdFromGsd2,
498
+ // Exported for unit tests
499
+ parseSlicesFromRoadmap,
500
+ parseMilestoneTitle,
501
+ parseTaskTitle,
502
+ parseTaskDescription,
503
+ parseTaskMustHaves,
504
+ buildPlanMd,
505
+ buildSummaryMd,
506
+ buildContextMd,
507
+ buildRoadmapMd,
508
+ buildStateMd,
509
+ slugify,
510
+ zeroPad,
511
+ };
@@ -870,6 +870,23 @@ function cmdInitManager(cwd, raw) {
870
870
  const phasesDir = paths.phases;
871
871
  const isDirInMilestone = getMilestonePhaseFilter(cwd);
872
872
 
873
+ // Pre-compute directory listing once (avoids O(N) readdirSync per phase)
874
+ const _phaseDirEntries = (() => {
875
+ try {
876
+ return fs.readdirSync(phasesDir, { withFileTypes: true })
877
+ .filter(e => e.isDirectory())
878
+ .map(e => e.name);
879
+ } catch { return []; }
880
+ })();
881
+
882
+ // Pre-extract all checkbox states in a single pass (avoids O(N) regex per phase)
883
+ const _checkboxStates = new Map();
884
+ const _cbPattern = /-\s*\[(x| )\]\s*.*Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s]/gi;
885
+ let _cbMatch;
886
+ while ((_cbMatch = _cbPattern.exec(content)) !== null) {
887
+ _checkboxStates.set(_cbMatch[2], _cbMatch[1].toLowerCase() === 'x');
888
+ }
889
+
873
890
  const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
874
891
  const phases = [];
875
892
  let match;
@@ -900,8 +917,7 @@ function cmdInitManager(cwd, raw) {
900
917
  let isActive = false;
901
918
 
902
919
  try {
903
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
904
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).filter(isDirInMilestone);
920
+ const dirs = _phaseDirEntries.filter(isDirInMilestone);
905
921
  const dirMatch = dirs.find(d => phaseTokenMatches(d, normalized));
906
922
 
907
923
  if (dirMatch) {
@@ -935,10 +951,8 @@ function cmdInitManager(cwd, raw) {
935
951
  }
936
952
  } catch { /* intentionally empty */ }
937
953
 
938
- // Check ROADMAP checkbox status
939
- const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[:\\s]`, 'i');
940
- const checkboxMatch = content.match(checkboxPattern);
941
- const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
954
+ // Check ROADMAP checkbox status (pre-extracted above the loop)
955
+ const roadmapComplete = _checkboxStates.get(phaseNum) || false;
942
956
  if (roadmapComplete && diskStatus !== 'complete') {
943
957
  diskStatus = 'complete';
944
958
  }