gsd-opencode 1.33.2 → 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 (130) 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-add-backlog.md +1 -1
  19. package/commands/gsd/gsd-add-phase.md +1 -1
  20. package/commands/gsd/gsd-add-todo.md +1 -1
  21. package/commands/gsd/gsd-ai-integration-phase.md +36 -0
  22. package/commands/gsd/gsd-audit-fix.md +33 -0
  23. package/commands/gsd/gsd-autonomous.md +1 -0
  24. package/commands/gsd/gsd-check-todos.md +1 -1
  25. package/commands/gsd/gsd-code-review-fix.md +52 -0
  26. package/commands/gsd/gsd-code-review.md +55 -0
  27. package/commands/gsd/gsd-complete-milestone.md +1 -1
  28. package/commands/gsd/gsd-debug.md +1 -1
  29. package/commands/gsd/gsd-eval-review.md +32 -0
  30. package/commands/gsd/gsd-explore.md +27 -0
  31. package/commands/gsd/gsd-from-gsd2.md +45 -0
  32. package/commands/gsd/gsd-health.md +1 -1
  33. package/commands/gsd/gsd-import.md +36 -0
  34. package/commands/gsd/gsd-insert-phase.md +1 -1
  35. package/commands/gsd/gsd-intel.md +183 -0
  36. package/commands/gsd/gsd-manager.md +1 -1
  37. package/commands/gsd/gsd-next.md +2 -0
  38. package/commands/gsd/gsd-reapply-patches.md +58 -3
  39. package/commands/gsd/gsd-remove-phase.md +1 -1
  40. package/commands/gsd/gsd-review.md +4 -2
  41. package/commands/gsd/gsd-scan.md +26 -0
  42. package/commands/gsd/gsd-set-profile.md +1 -1
  43. package/commands/gsd/gsd-thread.md +1 -1
  44. package/commands/gsd/gsd-undo.md +34 -0
  45. package/commands/gsd/gsd-workstreams.md +6 -6
  46. package/get-shit-done/bin/gsd-tools.cjs +143 -5
  47. package/get-shit-done/bin/lib/commands.cjs +10 -2
  48. package/get-shit-done/bin/lib/config.cjs +71 -37
  49. package/get-shit-done/bin/lib/core.cjs +70 -8
  50. package/get-shit-done/bin/lib/gsd2-import.cjs +511 -0
  51. package/get-shit-done/bin/lib/init.cjs +20 -6
  52. package/get-shit-done/bin/lib/intel.cjs +660 -0
  53. package/get-shit-done/bin/lib/learnings.cjs +378 -0
  54. package/get-shit-done/bin/lib/milestone.cjs +25 -15
  55. package/get-shit-done/bin/lib/model-profiles.cjs +17 -17
  56. package/get-shit-done/bin/lib/phase.cjs +148 -112
  57. package/get-shit-done/bin/lib/roadmap.cjs +12 -5
  58. package/get-shit-done/bin/lib/security.cjs +119 -0
  59. package/get-shit-done/bin/lib/state.cjs +283 -221
  60. package/get-shit-done/bin/lib/template.cjs +8 -4
  61. package/get-shit-done/bin/lib/verify.cjs +42 -5
  62. package/get-shit-done/references/ai-evals.md +156 -0
  63. package/get-shit-done/references/ai-frameworks.md +186 -0
  64. package/get-shit-done/references/common-bug-patterns.md +114 -0
  65. package/get-shit-done/references/few-shot-examples/plan-checker.md +73 -0
  66. package/get-shit-done/references/few-shot-examples/verifier.md +109 -0
  67. package/get-shit-done/references/gates.md +70 -0
  68. package/get-shit-done/references/ios-scaffold.md +123 -0
  69. package/get-shit-done/references/model-profile-resolution.md +6 -7
  70. package/get-shit-done/references/model-profiles.md +20 -14
  71. package/get-shit-done/references/planning-config.md +237 -0
  72. package/get-shit-done/references/thinking-models-debug.md +44 -0
  73. package/get-shit-done/references/thinking-models-execution.md +50 -0
  74. package/get-shit-done/references/thinking-models-planning.md +62 -0
  75. package/get-shit-done/references/thinking-models-research.md +50 -0
  76. package/get-shit-done/references/thinking-models-verification.md +55 -0
  77. package/get-shit-done/references/thinking-partner.md +96 -0
  78. package/get-shit-done/references/universal-anti-patterns.md +6 -1
  79. package/get-shit-done/references/verification-overrides.md +227 -0
  80. package/get-shit-done/templates/AI-SPEC.md +246 -0
  81. package/get-shit-done/workflows/add-tests.md +3 -0
  82. package/get-shit-done/workflows/add-todo.md +2 -0
  83. package/get-shit-done/workflows/ai-integration-phase.md +284 -0
  84. package/get-shit-done/workflows/audit-fix.md +154 -0
  85. package/get-shit-done/workflows/autonomous.md +33 -2
  86. package/get-shit-done/workflows/check-todos.md +2 -0
  87. package/get-shit-done/workflows/cleanup.md +2 -0
  88. package/get-shit-done/workflows/code-review-fix.md +497 -0
  89. package/get-shit-done/workflows/code-review.md +515 -0
  90. package/get-shit-done/workflows/complete-milestone.md +40 -15
  91. package/get-shit-done/workflows/diagnose-issues.md +1 -1
  92. package/get-shit-done/workflows/discovery-phase.md +3 -1
  93. package/get-shit-done/workflows/discuss-phase-assumptions.md +1 -1
  94. package/get-shit-done/workflows/discuss-phase.md +21 -7
  95. package/get-shit-done/workflows/do.md +2 -0
  96. package/get-shit-done/workflows/docs-update.md +2 -0
  97. package/get-shit-done/workflows/eval-review.md +155 -0
  98. package/get-shit-done/workflows/execute-phase.md +307 -57
  99. package/get-shit-done/workflows/execute-plan.md +64 -93
  100. package/get-shit-done/workflows/explore.md +136 -0
  101. package/get-shit-done/workflows/help.md +1 -1
  102. package/get-shit-done/workflows/import.md +273 -0
  103. package/get-shit-done/workflows/inbox.md +387 -0
  104. package/get-shit-done/workflows/manager.md +4 -10
  105. package/get-shit-done/workflows/new-milestone.md +3 -1
  106. package/get-shit-done/workflows/new-project.md +2 -0
  107. package/get-shit-done/workflows/new-workspace.md +2 -0
  108. package/get-shit-done/workflows/next.md +56 -0
  109. package/get-shit-done/workflows/note.md +2 -0
  110. package/get-shit-done/workflows/plan-phase.md +97 -17
  111. package/get-shit-done/workflows/plant-seed.md +3 -0
  112. package/get-shit-done/workflows/pr-branch.md +41 -13
  113. package/get-shit-done/workflows/profile-user.md +4 -2
  114. package/get-shit-done/workflows/quick.md +99 -4
  115. package/get-shit-done/workflows/remove-workspace.md +2 -0
  116. package/get-shit-done/workflows/review.md +53 -6
  117. package/get-shit-done/workflows/scan.md +98 -0
  118. package/get-shit-done/workflows/secure-phase.md +2 -0
  119. package/get-shit-done/workflows/settings.md +18 -3
  120. package/get-shit-done/workflows/ship.md +3 -0
  121. package/get-shit-done/workflows/ui-phase.md +10 -2
  122. package/get-shit-done/workflows/ui-review.md +2 -0
  123. package/get-shit-done/workflows/undo.md +314 -0
  124. package/get-shit-done/workflows/update.md +2 -0
  125. package/get-shit-done/workflows/validate-phase.md +2 -0
  126. package/get-shit-done/workflows/verify-phase.md +83 -0
  127. package/get-shit-done/workflows/verify-work.md +12 -1
  128. package/package.json +1 -1
  129. package/skills/gsd-code-review/SKILL.md +48 -0
  130. package/skills/gsd-code-review-fix/SKILL.md +44 -0
@@ -27,6 +27,16 @@ const WORKSTREAM_SESSION_ENV_KEYS = [
27
27
  let cachedControllingTtyToken = null;
28
28
  let didProbeControllingTtyToken = false;
29
29
 
30
+ // Track all .planning/.lock files held by this process so they can be removed
31
+ // on exit. process.on('exit') fires even on process.exit(1), unlike try/finally
32
+ // which is skipped when error() calls process.exit(1) inside a locked region (#1916).
33
+ const _heldPlanningLocks = new Set();
34
+ process.on('exit', () => {
35
+ for (const lockPath of _heldPlanningLocks) {
36
+ try { fs.unlinkSync(lockPath); } catch { /* already gone */ }
37
+ }
38
+ });
39
+
30
40
  // ─── Path helpers ────────────────────────────────────────────────────────────
31
41
 
32
42
  /** Normalize a relative path to always use forward slashes (cross-platform). */
@@ -229,6 +239,7 @@ const CONFIG_DEFAULTS = {
229
239
  plan_checker: true,
230
240
  verifier: true,
231
241
  nyquist_validation: true,
242
+ ai_integration_phase: true,
232
243
  parallelization: true,
233
244
  brave_search: false,
234
245
  firecrawl: false,
@@ -300,7 +311,7 @@ function loadConfig(cwd) {
300
311
  // Extract top-level key names from dot-notation paths (e.g., 'workflow.research' → 'workflow')
301
312
  ...[...VALID_CONFIG_KEYS].map(k => k.split('.')[0]),
302
313
  // Section containers that hold nested sub-keys
303
- 'git', 'workflow', 'planning', 'hooks',
314
+ 'git', 'workflow', 'planning', 'hooks', 'features',
304
315
  // Internal keys loadConfig reads but config-set doesn't expose
305
316
  'model_overrides', 'agent_skills', 'context_window', 'resolve_model_ids',
306
317
  // Deprecated keys (still accepted for migration, not in config-set)
@@ -400,7 +411,11 @@ function loadConfig(cwd) {
400
411
 
401
412
  // ─── Git utilities ────────────────────────────────────────────────────────────
402
413
 
414
+ const _gitIgnoredCache = new Map();
415
+
403
416
  function isGitIgnored(cwd, targetPath) {
417
+ const key = cwd + '::' + targetPath;
418
+ if (_gitIgnoredCache.has(key)) return _gitIgnoredCache.get(key);
404
419
  try {
405
420
  // --no-index checks .gitignore rules regardless of whether the file is tracked.
406
421
  // Without it, git check-ignore returns "not ignored" for tracked files even when
@@ -412,8 +427,10 @@ function isGitIgnored(cwd, targetPath) {
412
427
  cwd,
413
428
  stdio: 'pipe',
414
429
  });
430
+ _gitIgnoredCache.set(key, true);
415
431
  return true;
416
432
  } catch {
433
+ _gitIgnoredCache.set(key, false);
417
434
  return false;
418
435
  }
419
436
  }
@@ -598,10 +615,15 @@ function withPlanningLock(cwd, fn) {
598
615
  acquired: new Date().toISOString(),
599
616
  }), { flag: 'wx' });
600
617
 
618
+ // Register for exit-time cleanup so process.exit(1) inside a locked region
619
+ // cannot leave a stale lock file (#1916).
620
+ _heldPlanningLocks.add(lockPath);
621
+
601
622
  // Lock acquired — run the function
602
623
  try {
603
624
  return fn();
604
625
  } finally {
626
+ _heldPlanningLocks.delete(lockPath);
605
627
  try { fs.unlinkSync(lockPath); } catch { /* already released */ }
606
628
  }
607
629
  } catch (err) {
@@ -670,19 +692,23 @@ function planningRoot(cwd) {
670
692
  }
671
693
 
672
694
  /**
673
- * Get common .planning file paths, workstream-aware.
674
- * Scoped paths (state, roadmap, phases, requirements) resolve to the active workstream.
675
- * Shared paths (project, config) always resolve to the root .planning/.
695
+ * Get common .planning file paths, project-and-workstream-aware.
696
+ *
697
+ * All paths route through planningDir(cwd, ws), which honors the GSD_PROJECT
698
+ * env var and active workstream. This matches loadConfig() above (line 256),
699
+ * which has always read config.json via planningDir(cwd). Previously project
700
+ * and config were resolved against the unrouted .planning/ root, which broke
701
+ * `gsd-tools config-get` in multi-project layouts (the CRUD writers and the
702
+ * reader pointed at different files).
676
703
  */
677
704
  function planningPaths(cwd, ws) {
678
705
  const base = planningDir(cwd, ws);
679
- const root = path.join(cwd, '.planning');
680
706
  return {
681
707
  planning: base,
682
708
  state: path.join(base, 'STATE.md'),
683
709
  roadmap: path.join(base, 'ROADMAP.md'),
684
- project: path.join(root, 'PROJECT.md'),
685
- config: path.join(root, 'config.json'),
710
+ project: path.join(base, 'PROJECT.md'),
711
+ config: path.join(base, 'config.json'),
686
712
  phases: path.join(base, 'phases'),
687
713
  requirements: path.join(base, 'REQUIREMENTS.md'),
688
714
  };
@@ -879,7 +905,10 @@ function normalizePhaseName(phase) {
879
905
  const match = stripped.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
880
906
  if (match) {
881
907
  const padded = match[1].padStart(2, '0');
882
- const letter = match[2] ? match[2].toUpperCase() : '';
908
+ // Preserve original case of letter suffix (#1962).
909
+ // Uppercasing causes directory/roadmap mismatches on case-sensitive filesystems
910
+ // (e.g., "16c" in ROADMAP.md → directory "16C-name" → progress can't match).
911
+ const letter = match[2] || '';
883
912
  const decimal = match[3] || '';
884
913
  return padded + letter + decimal;
885
914
  }
@@ -1485,6 +1514,38 @@ function readSubdirectories(dirPath, sort = false) {
1485
1514
  }
1486
1515
  }
1487
1516
 
1517
+ // ─── Atomic file writes ───────────────────────────────────────────────────────
1518
+
1519
+ /**
1520
+ * write a file atomically using write-to-temp-then-rename.
1521
+ *
1522
+ * On POSIX systems, `fs.renameSync` is atomic when the source and destination
1523
+ * are on the same filesystem. This prevents a process killed mid-write from
1524
+ * leaving a truncated file that is unparseable on next read.
1525
+ *
1526
+ * The temp file is placed alongside the target so it is guaranteed to be on
1527
+ * the same filesystem (required for rename atomicity). The PID is embedded in
1528
+ * the temp file name so concurrent writers use distinct paths.
1529
+ *
1530
+ * If `renameSync` fails (e.g. cross-device move), the function falls back to a
1531
+ * direct `writeFileSync` so callers always get a best-effort write.
1532
+ *
1533
+ * @param {string} filePath Absolute path to write.
1534
+ * @param {string|Buffer} content File content.
1535
+ * @param {string} [encoding='utf-8'] Encoding passed to writeFileSync.
1536
+ */
1537
+ function atomicWriteFileSync(filePath, content, encoding = 'utf-8') {
1538
+ const tmpPath = filePath + '.tmp.' + process.pid;
1539
+ try {
1540
+ fs.writeFileSync(tmpPath, content, encoding);
1541
+ fs.renameSync(tmpPath, filePath);
1542
+ } catch (renameErr) {
1543
+ // Clean up the temp file if rename failed, then fall back to direct write.
1544
+ try { fs.unlinkSync(tmpPath); } catch { /* already gone or never created */ }
1545
+ fs.writeFileSync(filePath, content, encoding);
1546
+ }
1547
+ }
1548
+
1488
1549
  module.exports = {
1489
1550
  output,
1490
1551
  error,
@@ -1530,4 +1591,5 @@ module.exports = {
1530
1591
  readSubdirectories,
1531
1592
  getAgentsDir,
1532
1593
  checkAgentsInstalled,
1594
+ atomicWriteFileSync,
1533
1595
  };
@@ -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
+ };