@tuan_son.dinh/gsd 2.6.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 (227) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +453 -0
  3. package/dist/app-paths.d.ts +4 -0
  4. package/dist/app-paths.js +6 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +269 -0
  7. package/dist/loader.d.ts +2 -0
  8. package/dist/loader.js +70 -0
  9. package/dist/logo.d.ts +16 -0
  10. package/dist/logo.js +25 -0
  11. package/dist/onboarding.d.ts +43 -0
  12. package/dist/onboarding.js +418 -0
  13. package/dist/pi-migration.d.ts +14 -0
  14. package/dist/pi-migration.js +57 -0
  15. package/dist/resource-loader.d.ts +22 -0
  16. package/dist/resource-loader.js +60 -0
  17. package/dist/tool-bootstrap.d.ts +4 -0
  18. package/dist/tool-bootstrap.js +74 -0
  19. package/dist/wizard.d.ts +7 -0
  20. package/dist/wizard.js +25 -0
  21. package/package.json +60 -0
  22. package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +108 -0
  23. package/patches/@mariozechner+pi-tui+0.57.1.patch +47 -0
  24. package/pkg/dist/modes/interactive/theme/dark.json +85 -0
  25. package/pkg/dist/modes/interactive/theme/light.json +84 -0
  26. package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
  27. package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
  28. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  29. package/pkg/dist/modes/interactive/theme/theme.js +949 -0
  30. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
  31. package/pkg/package.json +8 -0
  32. package/scripts/postinstall.js +127 -0
  33. package/src/resources/GSD-WORKFLOW.md +661 -0
  34. package/src/resources/agents/researcher.md +29 -0
  35. package/src/resources/agents/scout.md +56 -0
  36. package/src/resources/agents/worker.md +31 -0
  37. package/src/resources/extensions/ask-user-questions.ts +249 -0
  38. package/src/resources/extensions/bg-shell/index.ts +2808 -0
  39. package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
  40. package/src/resources/extensions/browser-tools/core.js +1057 -0
  41. package/src/resources/extensions/browser-tools/index.ts +4989 -0
  42. package/src/resources/extensions/browser-tools/package.json +20 -0
  43. package/src/resources/extensions/context7/index.ts +428 -0
  44. package/src/resources/extensions/context7/package.json +11 -0
  45. package/src/resources/extensions/get-secrets-from-user.ts +352 -0
  46. package/src/resources/extensions/google-search/index.ts +323 -0
  47. package/src/resources/extensions/google-search/package.json +9 -0
  48. package/src/resources/extensions/gsd/activity-log.ts +69 -0
  49. package/src/resources/extensions/gsd/auto.ts +2744 -0
  50. package/src/resources/extensions/gsd/commands.ts +313 -0
  51. package/src/resources/extensions/gsd/crash-recovery.ts +85 -0
  52. package/src/resources/extensions/gsd/dashboard-overlay.ts +521 -0
  53. package/src/resources/extensions/gsd/docs/preferences-reference.md +176 -0
  54. package/src/resources/extensions/gsd/doctor.ts +690 -0
  55. package/src/resources/extensions/gsd/files.ts +732 -0
  56. package/src/resources/extensions/gsd/git-service.ts +597 -0
  57. package/src/resources/extensions/gsd/gitignore.ts +168 -0
  58. package/src/resources/extensions/gsd/guided-flow.ts +817 -0
  59. package/src/resources/extensions/gsd/index.ts +558 -0
  60. package/src/resources/extensions/gsd/metrics.ts +374 -0
  61. package/src/resources/extensions/gsd/migrate/command.ts +218 -0
  62. package/src/resources/extensions/gsd/migrate/index.ts +42 -0
  63. package/src/resources/extensions/gsd/migrate/parser.ts +323 -0
  64. package/src/resources/extensions/gsd/migrate/parsers.ts +624 -0
  65. package/src/resources/extensions/gsd/migrate/preview.ts +48 -0
  66. package/src/resources/extensions/gsd/migrate/transformer.ts +346 -0
  67. package/src/resources/extensions/gsd/migrate/types.ts +370 -0
  68. package/src/resources/extensions/gsd/migrate/validator.ts +55 -0
  69. package/src/resources/extensions/gsd/migrate/writer.ts +539 -0
  70. package/src/resources/extensions/gsd/observability-validator.ts +408 -0
  71. package/src/resources/extensions/gsd/package.json +11 -0
  72. package/src/resources/extensions/gsd/paths.ts +308 -0
  73. package/src/resources/extensions/gsd/preferences.ts +757 -0
  74. package/src/resources/extensions/gsd/prompt-loader.ts +50 -0
  75. package/src/resources/extensions/gsd/prompts/complete-milestone.md +25 -0
  76. package/src/resources/extensions/gsd/prompts/complete-slice.md +29 -0
  77. package/src/resources/extensions/gsd/prompts/discuss.md +189 -0
  78. package/src/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
  79. package/src/resources/extensions/gsd/prompts/execute-task.md +61 -0
  80. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -0
  81. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -0
  82. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +59 -0
  83. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -0
  84. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +23 -0
  85. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -0
  86. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +11 -0
  87. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -0
  88. package/src/resources/extensions/gsd/prompts/plan-milestone.md +65 -0
  89. package/src/resources/extensions/gsd/prompts/plan-slice.md +51 -0
  90. package/src/resources/extensions/gsd/prompts/queue.md +85 -0
  91. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +48 -0
  92. package/src/resources/extensions/gsd/prompts/replan-slice.md +39 -0
  93. package/src/resources/extensions/gsd/prompts/research-milestone.md +37 -0
  94. package/src/resources/extensions/gsd/prompts/research-slice.md +28 -0
  95. package/src/resources/extensions/gsd/prompts/review-migration.md +66 -0
  96. package/src/resources/extensions/gsd/prompts/run-uat.md +109 -0
  97. package/src/resources/extensions/gsd/prompts/system.md +187 -0
  98. package/src/resources/extensions/gsd/prompts/worktree-merge.md +123 -0
  99. package/src/resources/extensions/gsd/session-forensics.ts +487 -0
  100. package/src/resources/extensions/gsd/skill-discovery.ts +137 -0
  101. package/src/resources/extensions/gsd/state.ts +460 -0
  102. package/src/resources/extensions/gsd/templates/context.md +76 -0
  103. package/src/resources/extensions/gsd/templates/decisions.md +8 -0
  104. package/src/resources/extensions/gsd/templates/milestone-summary.md +73 -0
  105. package/src/resources/extensions/gsd/templates/plan.md +131 -0
  106. package/src/resources/extensions/gsd/templates/preferences.md +24 -0
  107. package/src/resources/extensions/gsd/templates/project.md +31 -0
  108. package/src/resources/extensions/gsd/templates/reassessment.md +28 -0
  109. package/src/resources/extensions/gsd/templates/requirements.md +81 -0
  110. package/src/resources/extensions/gsd/templates/research.md +46 -0
  111. package/src/resources/extensions/gsd/templates/roadmap.md +118 -0
  112. package/src/resources/extensions/gsd/templates/slice-context.md +58 -0
  113. package/src/resources/extensions/gsd/templates/slice-summary.md +99 -0
  114. package/src/resources/extensions/gsd/templates/state.md +19 -0
  115. package/src/resources/extensions/gsd/templates/task-plan.md +52 -0
  116. package/src/resources/extensions/gsd/templates/task-summary.md +57 -0
  117. package/src/resources/extensions/gsd/templates/uat.md +54 -0
  118. package/src/resources/extensions/gsd/tests/activity-log-prune.test.ts +327 -0
  119. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +56 -0
  120. package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +53 -0
  121. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +225 -0
  122. package/src/resources/extensions/gsd/tests/cost-projection.test.ts +160 -0
  123. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +341 -0
  124. package/src/resources/extensions/gsd/tests/derive-state.test.ts +689 -0
  125. package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +38 -0
  126. package/src/resources/extensions/gsd/tests/doctor.test.ts +505 -0
  127. package/src/resources/extensions/gsd/tests/git-service.test.ts +1313 -0
  128. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +308 -0
  129. package/src/resources/extensions/gsd/tests/metrics-io.test.ts +201 -0
  130. package/src/resources/extensions/gsd/tests/metrics.test.ts +217 -0
  131. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +390 -0
  132. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +786 -0
  133. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +657 -0
  134. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +443 -0
  135. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +318 -0
  136. package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +420 -0
  137. package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +309 -0
  138. package/src/resources/extensions/gsd/tests/parsers.test.ts +1351 -0
  139. package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +163 -0
  140. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +386 -0
  141. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +171 -0
  142. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
  143. package/src/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
  144. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +521 -0
  145. package/src/resources/extensions/gsd/tests/requirements.test.ts +125 -0
  146. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +34 -0
  147. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +11 -0
  148. package/src/resources/extensions/gsd/tests/run-uat.test.ts +348 -0
  149. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +247 -0
  150. package/src/resources/extensions/gsd/tests/workflow-config.test.mjs +53 -0
  151. package/src/resources/extensions/gsd/tests/workspace-index.test.ts +94 -0
  152. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +253 -0
  153. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +160 -0
  154. package/src/resources/extensions/gsd/tests/worktree.test.ts +264 -0
  155. package/src/resources/extensions/gsd/types.ts +159 -0
  156. package/src/resources/extensions/gsd/unit-runtime.ts +184 -0
  157. package/src/resources/extensions/gsd/workspace-index.ts +203 -0
  158. package/src/resources/extensions/gsd/worktree-command.ts +845 -0
  159. package/src/resources/extensions/gsd/worktree-manager.ts +392 -0
  160. package/src/resources/extensions/gsd/worktree.ts +183 -0
  161. package/src/resources/extensions/mac-tools/index.ts +852 -0
  162. package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
  163. package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
  164. package/src/resources/extensions/mcporter/index.ts +429 -0
  165. package/src/resources/extensions/remote-questions/config.ts +81 -0
  166. package/src/resources/extensions/remote-questions/discord-adapter.ts +128 -0
  167. package/src/resources/extensions/remote-questions/format.ts +163 -0
  168. package/src/resources/extensions/remote-questions/manager.ts +192 -0
  169. package/src/resources/extensions/remote-questions/remote-command.ts +307 -0
  170. package/src/resources/extensions/remote-questions/slack-adapter.ts +92 -0
  171. package/src/resources/extensions/remote-questions/status.ts +31 -0
  172. package/src/resources/extensions/remote-questions/store.ts +77 -0
  173. package/src/resources/extensions/remote-questions/types.ts +75 -0
  174. package/src/resources/extensions/search-the-web/cache.ts +78 -0
  175. package/src/resources/extensions/search-the-web/command-search-provider.ts +95 -0
  176. package/src/resources/extensions/search-the-web/format.ts +258 -0
  177. package/src/resources/extensions/search-the-web/http.ts +238 -0
  178. package/src/resources/extensions/search-the-web/index.ts +65 -0
  179. package/src/resources/extensions/search-the-web/native-search.ts +157 -0
  180. package/src/resources/extensions/search-the-web/provider.ts +118 -0
  181. package/src/resources/extensions/search-the-web/tavily.ts +116 -0
  182. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
  183. package/src/resources/extensions/search-the-web/tool-llm-context.ts +561 -0
  184. package/src/resources/extensions/search-the-web/tool-search.ts +576 -0
  185. package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
  186. package/src/resources/extensions/shared/confirm-ui.ts +126 -0
  187. package/src/resources/extensions/shared/interview-ui.ts +613 -0
  188. package/src/resources/extensions/shared/next-action-ui.ts +197 -0
  189. package/src/resources/extensions/shared/progress-widget.ts +282 -0
  190. package/src/resources/extensions/shared/terminal.ts +23 -0
  191. package/src/resources/extensions/shared/thinking-widget.ts +107 -0
  192. package/src/resources/extensions/shared/ui.ts +400 -0
  193. package/src/resources/extensions/shared/wizard-ui.ts +551 -0
  194. package/src/resources/extensions/slash-commands/audit.ts +88 -0
  195. package/src/resources/extensions/slash-commands/clear.ts +10 -0
  196. package/src/resources/extensions/slash-commands/create-extension.ts +297 -0
  197. package/src/resources/extensions/slash-commands/create-slash-command.ts +234 -0
  198. package/src/resources/extensions/slash-commands/index.ts +12 -0
  199. package/src/resources/extensions/subagent/agents.ts +126 -0
  200. package/src/resources/extensions/subagent/index.ts +1020 -0
  201. package/src/resources/extensions/voice/index.ts +195 -0
  202. package/src/resources/extensions/voice/speech-recognizer.swift +154 -0
  203. package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
  204. package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
  205. package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
  206. package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
  207. package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
  208. package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
  209. package/src/resources/skills/frontend-design/SKILL.md +45 -0
  210. package/src/resources/skills/swiftui/SKILL.md +208 -0
  211. package/src/resources/skills/swiftui/references/animations.md +921 -0
  212. package/src/resources/skills/swiftui/references/architecture.md +1561 -0
  213. package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
  214. package/src/resources/skills/swiftui/references/navigation.md +1492 -0
  215. package/src/resources/skills/swiftui/references/networking-async.md +214 -0
  216. package/src/resources/skills/swiftui/references/performance.md +1706 -0
  217. package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
  218. package/src/resources/skills/swiftui/references/state-management.md +1443 -0
  219. package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
  220. package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
  221. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
  222. package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
  223. package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
  224. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
  225. package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
  226. package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
  227. package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
@@ -0,0 +1,624 @@
1
+ // Old .planning format per-file parsers
2
+ // Pure functions that take file content (string) and return typed data.
3
+ // Zero Pi dependencies — uses only exported helpers from files.ts.
4
+
5
+ import { splitFrontmatter, parseFrontmatterMap, extractBoldField } from '../files.ts';
6
+
7
+ import type {
8
+ PlanningRoadmap,
9
+ PlanningRoadmapMilestone,
10
+ PlanningRoadmapEntry,
11
+ PlanningPlan,
12
+ PlanningPlanFrontmatter,
13
+ PlanningPlanMustHaves,
14
+ PlanningSummary,
15
+ PlanningSummaryFrontmatter,
16
+ PlanningSummaryRequires,
17
+ PlanningRequirement,
18
+ PlanningState,
19
+ PlanningConfig,
20
+ } from './types.ts';
21
+
22
+ // Re-export PlanningProjectMeta — not in types.ts yet, use string for project field
23
+ // Actually PlanningProjectMeta isn't in types.ts — project is stored as string | null.
24
+ // We'll keep parseOldProject returning a simple shape.
25
+
26
+ // ─── XML-in-Markdown Extraction ────────────────────────────────────────────
27
+
28
+ /**
29
+ * Extract content between XML-like tags in markdown.
30
+ * NOT a real XML parser — handles `<tag>content</tag>` with markdown inside.
31
+ */
32
+ function extractXmlTag(content: string, tagName: string): string {
33
+ const regex = new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`, 'i');
34
+ const match = regex.exec(content);
35
+ return match ? match[1].trim() : '';
36
+ }
37
+
38
+ /**
39
+ * Extract all nested `<task>` entries from within a `<tasks>` block.
40
+ */
41
+ function extractTasks(content: string): string[] {
42
+ const tasksBlock = extractXmlTag(content, 'tasks');
43
+ if (!tasksBlock) return [];
44
+
45
+ const tasks: string[] = [];
46
+ const regex = /<task>([\s\S]*?)<\/task>/gi;
47
+ let match: RegExpExecArray | null;
48
+ while ((match = regex.exec(tasksBlock)) !== null) {
49
+ const trimmed = match[1].trim();
50
+ if (trimmed) tasks.push(trimmed);
51
+ }
52
+ return tasks;
53
+ }
54
+
55
+ // ─── Roadmap Parser ────────────────────────────────────────────────────────
56
+
57
+ /** Parse a checkbox phase entry line: `- [x] 29 — Auth System` */
58
+ function parsePhaseEntry(line: string): PlanningRoadmapEntry | null {
59
+ // Strip bold markers (**) for uniform matching — old roadmaps often bold phase entries
60
+ const stripped = line.replace(/\*\*/g, '');
61
+
62
+ // Format 1: - [x] Phase 25: Title (N/N plans) -- completed ...
63
+ // Also handles: - [x] Phase 25: Title - Description (completed ...)
64
+ const fmtPhaseColon = stripped.match(/^-\s+\[([ xX])\]\s+(?:Phase\s+)?(\d+(?:\.\d+)?)\s*:\s*(.+)$/);
65
+ if (fmtPhaseColon) {
66
+ let title = fmtPhaseColon[3].trim();
67
+ // Strip trailing parentheticals, plan counts, and completion notes
68
+ title = title.replace(/\s*\(\d+\/\d+\s+plans?\)/, '')
69
+ .replace(/\s*--\s+.*$/, '')
70
+ .replace(/\s*-\s+.*$/, '') // strip "- description" suffix
71
+ .replace(/\s*\(completed.*\)$/i, '')
72
+ .replace(/\s*\(shipped.*\)$/i, '')
73
+ .trim();
74
+ return {
75
+ number: parseFloat(fmtPhaseColon[2]),
76
+ title,
77
+ done: fmtPhaseColon[1].toLowerCase() === 'x',
78
+ raw: line,
79
+ };
80
+ }
81
+
82
+ // Format 2: - [x] 25 — Title (em-dash/en-dash only — NOT plain hyphen to avoid plan file refs)
83
+ const fmtDash = stripped.match(/^-\s+\[([ xX])\]\s+(?:Phase\s+)?(\d+(?:\.\d+)?)\s*[—–]\s*(.+)$/);
84
+ if (fmtDash) {
85
+ let title = fmtDash[3].trim();
86
+ title = title.replace(/\s*\(\d+\/\d+\s+plans?\)/, '')
87
+ .replace(/\s*--\s+.*$/, '')
88
+ .trim();
89
+ return {
90
+ number: parseFloat(fmtDash[2]),
91
+ title,
92
+ done: fmtDash[1].toLowerCase() === 'x',
93
+ raw: line,
94
+ };
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * Parse old-format ROADMAP.md.
102
+ * Handles two formats:
103
+ * 1. Flat phase lists — checkbox lines under a single Phases heading
104
+ * 2. Milestone-sectioned — `## v2.0 — Title` headings with optional `<details>` blocks
105
+ * 3. Details-sectioned — `<details><summary>v1.0 Title (Phases N-M)</summary>` blocks with phase checkboxes inside
106
+ */
107
+ export function parseOldRoadmap(content: string): PlanningRoadmap {
108
+ const result: PlanningRoadmap = {
109
+ raw: content,
110
+ milestones: [],
111
+ phases: [],
112
+ };
113
+
114
+ const lines = content.split('\n');
115
+
116
+ // ─── Strategy 1: Detect <details><summary>vN.N Title</summary> blocks ───
117
+ // This handles the format where milestones are <details> blocks containing phase checkboxes
118
+ const detailsMilestones = parseDetailsBlockMilestones(lines);
119
+ if (detailsMilestones.length > 0) {
120
+ result.milestones = detailsMilestones;
121
+
122
+ // Also check for non-collapsed milestone sections (### v3.0 Title)
123
+ // that follow the <details> blocks
124
+ for (let i = 0; i < lines.length; i++) {
125
+ const heading = lines[i].match(/^###\s+(v[\d.]+)\s+(.+?)(?:\s*\(.*\))?\s*$/);
126
+ if (heading) {
127
+ // Already captured as a details block?
128
+ const id = heading[1];
129
+ if (result.milestones.some(m => m.id === id)) continue;
130
+
131
+ // Collect phase entries until next ## or ### heading
132
+ const phases: PlanningRoadmapEntry[] = [];
133
+ for (let j = i + 1; j < lines.length; j++) {
134
+ if (/^##?\s/.test(lines[j]) || /^###\s/.test(lines[j])) break;
135
+ const entry = parsePhaseEntry(lines[j].trim());
136
+ if (entry) phases.push(entry);
137
+ }
138
+ result.milestones.push({
139
+ id,
140
+ title: heading[2].trim(),
141
+ collapsed: false,
142
+ phases,
143
+ });
144
+ }
145
+ }
146
+ return result;
147
+ }
148
+
149
+ // ─── Strategy 2: Detect ## heading-sectioned milestones ───
150
+ const milestoneHeadingRegex = /^##\s+(.+)$/;
151
+ const milestoneHeadings: { index: number; id: string; title: string }[] = [];
152
+
153
+ for (let i = 0; i < lines.length; i++) {
154
+ const match = lines[i].match(milestoneHeadingRegex);
155
+ if (match) {
156
+ const heading = match[1].trim();
157
+ // Skip generic headings like "## Phases", "## Milestones", "## Phase Details", "## Progress"
158
+ if (/^(phases?|milestones?|phase\s+details?|progress)$/i.test(heading)) continue;
159
+ // Extract milestone ID (e.g. "v2.0" from "v2.0 — Foundation")
160
+ const idMatch = heading.match(/^(v[\d.]+|[\w.-]+)\s*[—–-]\s*(.+)$/);
161
+ if (idMatch) {
162
+ milestoneHeadings.push({ index: i, id: idMatch[1], title: idMatch[2].trim() });
163
+ }
164
+ }
165
+ }
166
+
167
+ if (milestoneHeadings.length > 0) {
168
+ // Milestone-sectioned format
169
+ for (let m = 0; m < milestoneHeadings.length; m++) {
170
+ const startIdx = milestoneHeadings[m].index + 1;
171
+ const endIdx = m + 1 < milestoneHeadings.length ? milestoneHeadings[m + 1].index : lines.length;
172
+ const sectionLines = lines.slice(startIdx, endIdx);
173
+
174
+ const milestone: PlanningRoadmapMilestone = {
175
+ id: milestoneHeadings[m].id,
176
+ title: milestoneHeadings[m].title,
177
+ collapsed: false,
178
+ phases: [],
179
+ };
180
+
181
+ // Check for <details> block
182
+ const sectionText = sectionLines.join('\n');
183
+ if (sectionText.includes('<details>')) {
184
+ milestone.collapsed = true;
185
+ }
186
+
187
+ // Extract phase entries from the section (including inside <details>)
188
+ for (const line of sectionLines) {
189
+ const entry = parsePhaseEntry(line.trim());
190
+ if (entry) {
191
+ milestone.phases.push(entry);
192
+ }
193
+ }
194
+
195
+ result.milestones.push(milestone);
196
+ }
197
+ } else {
198
+ // ─── Strategy 3: Flat format — just extract all phase checkbox lines ───
199
+ for (const line of lines) {
200
+ const entry = parsePhaseEntry(line.trim());
201
+ if (entry) {
202
+ result.phases.push(entry);
203
+ }
204
+ }
205
+ }
206
+
207
+ return result;
208
+ }
209
+
210
+ /**
211
+ * Parse <details><summary>vN.N Title (Phases N-M)</summary>...</details> blocks.
212
+ * Each block becomes a milestone with the phase entries inside it.
213
+ */
214
+ function parseDetailsBlockMilestones(lines: string[]): PlanningRoadmapMilestone[] {
215
+ const milestones: PlanningRoadmapMilestone[] = [];
216
+ let inDetails = false;
217
+ let currentMilestone: PlanningRoadmapMilestone | null = null;
218
+
219
+ for (const line of lines) {
220
+ const trimmed = line.trim();
221
+
222
+ if (trimmed === '<details>') {
223
+ inDetails = true;
224
+ continue;
225
+ }
226
+
227
+ if (inDetails && !currentMilestone) {
228
+ // Look for <summary>vN.N Title (Phases N-M) -- STATUS</summary>
229
+ const summaryMatch = trimmed.match(/<summary>\s*(v[\d.]+)\s+(.+?)\s*(?:\(.*\))?\s*(?:--\s*.*)?\s*<\/summary>/);
230
+ if (summaryMatch) {
231
+ currentMilestone = {
232
+ id: summaryMatch[1],
233
+ title: summaryMatch[2].trim(),
234
+ collapsed: true,
235
+ phases: [],
236
+ };
237
+ }
238
+ continue;
239
+ }
240
+
241
+ if (trimmed === '</details>') {
242
+ if (currentMilestone) {
243
+ milestones.push(currentMilestone);
244
+ currentMilestone = null;
245
+ }
246
+ inDetails = false;
247
+ continue;
248
+ }
249
+
250
+ if (currentMilestone) {
251
+ const entry = parsePhaseEntry(trimmed);
252
+ if (entry) {
253
+ currentMilestone.phases.push(entry);
254
+ }
255
+ }
256
+ }
257
+
258
+ return milestones;
259
+ }
260
+
261
+ // ─── Plan Parser (XML-in-Markdown) ─────────────────────────────────────────
262
+
263
+ /** Strip surrounding quotes from YAML string values */
264
+ function unquote(val: unknown): string {
265
+ const s = String(val ?? '');
266
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
267
+ return s.slice(1, -1);
268
+ }
269
+ return s;
270
+ }
271
+
272
+ /**
273
+ * Parse the must_haves nested structure from frontmatter lines directly.
274
+ * parseFrontmatterMap doesn't handle 3-level nesting well, so we re-parse.
275
+ */
276
+ function parseMustHavesFromLines(fmLines: string[]): PlanningPlanMustHaves | null {
277
+ const start = fmLines.findIndex(l => /^must_haves\s*:/.test(l));
278
+ if (start === -1) return null;
279
+
280
+ const truths: string[] = [];
281
+ const artifacts: string[] = [];
282
+ const keyLinks: string[] = [];
283
+ let currentList: string[] | null = null;
284
+
285
+ for (let i = start + 1; i < fmLines.length; i++) {
286
+ const line = fmLines[i];
287
+ // New top-level key — stop
288
+ if (/^\w/.test(line)) break;
289
+ // Sub-key at 2-space indent
290
+ const subKey = line.match(/^ (\w[\w_]*):/);
291
+ if (subKey) {
292
+ const key = subKey[1];
293
+ if (key === 'truths') currentList = truths;
294
+ else if (key === 'artifacts') currentList = artifacts;
295
+ else if (key === 'key_links') currentList = keyLinks;
296
+ else currentList = null;
297
+ // Check for inline empty array
298
+ if (/:\s*\[\]/.test(line)) currentList = null;
299
+ continue;
300
+ }
301
+ // Array item at 4-space indent
302
+ const item = line.match(/^ - (.+)$/);
303
+ if (item && currentList) {
304
+ currentList.push(item[1].trim());
305
+ }
306
+ }
307
+
308
+ if (truths.length === 0 && artifacts.length === 0 && keyLinks.length === 0) return null;
309
+ return { truths, artifacts, key_links: keyLinks };
310
+ }
311
+
312
+ function parsePlanFrontmatter(fm: Record<string, unknown>, fmLines: string[] | null): PlanningPlanFrontmatter {
313
+ const mustHaves = fmLines ? parseMustHavesFromLines(fmLines) : null;
314
+
315
+ return {
316
+ phase: unquote(fm.phase),
317
+ plan: unquote(fm.plan),
318
+ type: unquote(fm.type),
319
+ wave: fm.wave !== undefined ? Number(fm.wave) : null,
320
+ depends_on: Array.isArray(fm.depends_on) ? fm.depends_on.map(s => unquote(s)) : [],
321
+ files_modified: Array.isArray(fm.files_modified) ? fm.files_modified.map(s => unquote(s)) : [],
322
+ autonomous: fm.autonomous === 'true' || fm.autonomous === true,
323
+ must_haves: mustHaves,
324
+ };
325
+ }
326
+
327
+ /**
328
+ * Parse old-format plan file with YAML frontmatter and XML-in-markdown sections.
329
+ * Falls back to plain markdown for quick-task plans that lack XML tags.
330
+ */
331
+ export function parseOldPlan(content: string, fileName: string = '', planNumber: string = ''): PlanningPlan {
332
+ const [fmLines, body] = splitFrontmatter(content);
333
+ const fm = fmLines ? parseFrontmatterMap(fmLines) : {};
334
+ const frontmatter = parsePlanFrontmatter(fm, fmLines);
335
+
336
+ // Extract XML-in-markdown sections
337
+ const objective = extractXmlTag(content, 'objective');
338
+ const tasks = extractTasks(content);
339
+ const context = extractXmlTag(content, 'context');
340
+ const verification = extractXmlTag(content, 'verification');
341
+ const successCriteria = extractXmlTag(content, 'success_criteria');
342
+
343
+ return {
344
+ fileName,
345
+ planNumber: planNumber || String(fm.plan ?? ''),
346
+ frontmatter,
347
+ objective,
348
+ tasks,
349
+ context,
350
+ verification,
351
+ successCriteria,
352
+ raw: content,
353
+ };
354
+ }
355
+
356
+ // ─── Summary Parser (YAML Frontmatter) ─────────────────────────────────────
357
+
358
+ function parseRequiresArray(raw: unknown): PlanningSummaryRequires[] {
359
+ if (!Array.isArray(raw)) return [];
360
+ return raw.map(item => {
361
+ if (typeof item === 'object' && item !== null) {
362
+ const obj = item as Record<string, string>;
363
+ return { phase: obj.phase ?? '', provides: obj.provides ?? '' };
364
+ }
365
+ return { phase: '', provides: String(item) };
366
+ });
367
+ }
368
+
369
+ function toStringArray(val: unknown): string[] {
370
+ if (Array.isArray(val)) return val.map(String);
371
+ return [];
372
+ }
373
+
374
+ /**
375
+ * Parse YAML-like frontmatter lines into a flat key-value map.
376
+ * Like parseFrontmatterMap but supports hyphenated keys (e.g. `tech-stack:`).
377
+ */
378
+ function parseFrontmatterMapHyphen(lines: string[]): Record<string, unknown> {
379
+ const result: Record<string, unknown> = {};
380
+ let currentKey: string | null = null;
381
+ let currentArray: unknown[] | null = null;
382
+ let currentObj: Record<string, string> | null = null;
383
+
384
+ for (const line of lines) {
385
+ // Nested object property (4-space indent with key: value)
386
+ const nestedMatch = line.match(/^ ([\w][\w_-]*)\s*:\s*(.*)$/);
387
+ if (nestedMatch && currentArray && currentObj) {
388
+ currentObj[nestedMatch[1]] = nestedMatch[2].trim();
389
+ continue;
390
+ }
391
+
392
+ // Array item (2-space indent)
393
+ const arrayMatch = line.match(/^ - (.*)$/);
394
+ if (arrayMatch && currentKey) {
395
+ if (currentObj && Object.keys(currentObj).length > 0) {
396
+ currentArray!.push(currentObj);
397
+ }
398
+ currentObj = null;
399
+
400
+ const val = arrayMatch[1].trim();
401
+ if (!currentArray) currentArray = [];
402
+
403
+ const nestedStart = val.match(/^([\w][\w_-]*)\s*:\s*(.*)$/);
404
+ if (nestedStart) {
405
+ currentObj = { [nestedStart[1]]: nestedStart[2].trim() };
406
+ } else {
407
+ currentArray.push(val);
408
+ }
409
+ continue;
410
+ }
411
+
412
+ // Flush previous key
413
+ if (currentKey) {
414
+ if (currentObj && Object.keys(currentObj).length > 0 && currentArray) {
415
+ currentArray.push(currentObj);
416
+ currentObj = null;
417
+ }
418
+ if (currentArray) {
419
+ result[currentKey] = currentArray;
420
+ }
421
+ currentArray = null;
422
+ }
423
+
424
+ // Top-level key: value (supports hyphens in key names)
425
+ const kvMatch = line.match(/^([\w][\w_-]*)\s*:\s*(.*)$/);
426
+ if (kvMatch) {
427
+ currentKey = kvMatch[1];
428
+ const val = kvMatch[2].trim();
429
+
430
+ if (val === '' || val === '[]') {
431
+ currentArray = [];
432
+ } else if (val.startsWith('[') && val.endsWith(']')) {
433
+ const inner = val.slice(1, -1).trim();
434
+ result[currentKey] = inner ? inner.split(',').map(s => s.trim()) : [];
435
+ currentKey = null;
436
+ } else {
437
+ result[currentKey] = val;
438
+ currentKey = null;
439
+ }
440
+ }
441
+ }
442
+
443
+ // Flush final key
444
+ if (currentKey) {
445
+ if (currentObj && Object.keys(currentObj).length > 0 && currentArray) {
446
+ currentArray.push(currentObj);
447
+ currentObj = null;
448
+ }
449
+ if (currentArray) {
450
+ result[currentKey] = currentArray;
451
+ }
452
+ }
453
+
454
+ return result;
455
+ }
456
+
457
+ function parseSummaryFrontmatter(fm: Record<string, unknown>): PlanningSummaryFrontmatter {
458
+ return {
459
+ phase: unquote(fm.phase),
460
+ plan: unquote(fm.plan),
461
+ subsystem: unquote(fm.subsystem),
462
+ tags: toStringArray(fm.tags),
463
+ requires: parseRequiresArray(fm.requires),
464
+ provides: toStringArray(fm.provides),
465
+ affects: toStringArray(fm.affects),
466
+ 'tech-stack': toStringArray(fm['tech-stack']),
467
+ 'key-files': toStringArray(fm['key-files']),
468
+ 'key-decisions': toStringArray(fm['key-decisions']),
469
+ 'patterns-established': toStringArray(fm['patterns-established']),
470
+ duration: unquote(fm.duration),
471
+ completed: unquote(fm.completed),
472
+ };
473
+ }
474
+
475
+ /**
476
+ * Parse old-format summary file with YAML frontmatter.
477
+ */
478
+ export function parseOldSummary(content: string, fileName: string = '', planNumber: string = ''): PlanningSummary {
479
+ const [fmLines, body] = splitFrontmatter(content);
480
+ const fm = fmLines ? parseFrontmatterMapHyphen(fmLines) : {};
481
+
482
+ return {
483
+ fileName,
484
+ planNumber: planNumber || String(fm.plan ?? ''),
485
+ frontmatter: parseSummaryFrontmatter(fm),
486
+ body,
487
+ raw: content,
488
+ };
489
+ }
490
+
491
+ // ─── Requirements Parser ───────────────────────────────────────────────────
492
+
493
+ /**
494
+ * Parse old-format REQUIREMENTS.md.
495
+ * Extracts requirement entries from markdown with status sections and requirement headings.
496
+ */
497
+ export function parseOldRequirements(content: string): PlanningRequirement[] {
498
+ const requirements: PlanningRequirement[] = [];
499
+ const lines = content.split('\n');
500
+
501
+ let currentStatus = '';
502
+ let currentReq: Partial<PlanningRequirement> | null = null;
503
+ let currentRaw: string[] = [];
504
+
505
+ function flushReq() {
506
+ if (currentReq?.id && currentReq?.title) {
507
+ requirements.push({
508
+ id: currentReq.id,
509
+ title: currentReq.title,
510
+ status: currentReq.status || currentStatus || 'unknown',
511
+ description: currentReq.description || '',
512
+ raw: currentRaw.join('\n').trim(),
513
+ });
514
+ }
515
+ currentReq = null;
516
+ currentRaw = [];
517
+ }
518
+
519
+ for (const line of lines) {
520
+ // Status section heading (## Active, ## Validated, ## Deferred)
521
+ const statusMatch = line.match(/^##\s+(\w[\w\s&]*\w)\s*$/);
522
+ if (statusMatch) {
523
+ flushReq();
524
+ currentStatus = statusMatch[1].toLowerCase();
525
+ continue;
526
+ }
527
+
528
+ // Section heading (### Category Name) — use as context for bullet requirements
529
+ const sectionMatch = line.match(/^###\s+(.+)$/);
530
+ if (sectionMatch) {
531
+ // Check if this is a requirement heading (### R001 — Title)
532
+ const reqHeading = sectionMatch[1].match(/^(R\d+)\s*[—–-]\s*(.+)$/);
533
+ if (reqHeading) {
534
+ flushReq();
535
+ currentReq = { id: reqHeading[1], title: reqHeading[2].trim(), status: currentStatus, description: '' };
536
+ currentRaw.push(line);
537
+ continue;
538
+ }
539
+ // Otherwise just note the section — don't flush, could be a category for bullet reqs
540
+ flushReq();
541
+ continue;
542
+ }
543
+
544
+ // Bullet-format requirement: - [x] **ID**: Description
545
+ const bulletReqMatch = line.match(/^-\s+\[([ xX])\]\s+\*\*([^*]+)\*\*\s*:\s*(.+)$/);
546
+ if (bulletReqMatch) {
547
+ flushReq();
548
+ const done = bulletReqMatch[1].toLowerCase() === 'x';
549
+ const id = bulletReqMatch[2].trim();
550
+ const desc = bulletReqMatch[3].trim();
551
+ requirements.push({
552
+ id,
553
+ title: desc,
554
+ status: done ? 'complete' : (currentStatus || 'active'),
555
+ description: desc,
556
+ raw: line,
557
+ });
558
+ continue;
559
+ }
560
+
561
+ // Description or metadata within a requirement
562
+ if (currentReq) {
563
+ currentRaw.push(line);
564
+ const descMatch = line.match(/^-\s+Description:\s*(.+)$/);
565
+ if (descMatch) {
566
+ currentReq.description = descMatch[1].trim();
567
+ continue;
568
+ }
569
+ const statMatch = line.match(/^-\s+Status:\s*(.+)$/);
570
+ if (statMatch) {
571
+ currentReq.status = statMatch[1].trim();
572
+ }
573
+ }
574
+ }
575
+
576
+ flushReq();
577
+ return requirements;
578
+ }
579
+
580
+ // ─── Project Parser ────────────────────────────────────────────────────────
581
+
582
+ // PlanningProjectMeta isn't in types.ts — project field on PlanningProject is `string | null`.
583
+ // This parser returns the raw content as a string. The top-level parser stores it directly.
584
+
585
+ /**
586
+ * Parse old-format PROJECT.md.
587
+ * Returns the raw content as a string (stored as project field on PlanningProject).
588
+ */
589
+ export function parseOldProject(content: string): string {
590
+ return content;
591
+ }
592
+
593
+ // ─── State Parser ──────────────────────────────────────────────────────────
594
+
595
+ /**
596
+ * Parse old-format STATE.md.
597
+ * Extracts current phase and status from bold-field patterns.
598
+ */
599
+ export function parseOldState(content: string): PlanningState {
600
+ const currentPhase = extractBoldField(content, 'Current Phase');
601
+ const status = extractBoldField(content, 'Status');
602
+
603
+ return {
604
+ raw: content,
605
+ currentPhase,
606
+ status,
607
+ };
608
+ }
609
+
610
+ // ─── Config Parser ─────────────────────────────────────────────────────────
611
+
612
+ /**
613
+ * Parse old-format config.json.
614
+ * Returns null on invalid JSON (graceful error handling).
615
+ */
616
+ export function parseOldConfig(content: string): PlanningConfig | null {
617
+ try {
618
+ const parsed = JSON.parse(content);
619
+ if (typeof parsed !== 'object' || parsed === null) return null;
620
+ return parsed as PlanningConfig;
621
+ } catch {
622
+ return null;
623
+ }
624
+ }
@@ -0,0 +1,48 @@
1
+ // GSD Migration Preview — Pre-write statistics
2
+ // Pure function, no I/O. Computes counts from a GSDProject.
3
+
4
+ import type { GSDProject } from './types.ts';
5
+ import type { MigrationPreview } from './writer.ts';
6
+
7
+ /**
8
+ * Compute pre-write statistics from a GSDProject without performing I/O.
9
+ * Used to show the user what a migration will produce before writing anything.
10
+ */
11
+ export function generatePreview(project: GSDProject): MigrationPreview {
12
+ let totalSlices = 0;
13
+ let totalTasks = 0;
14
+ let doneSlices = 0;
15
+ let doneTasks = 0;
16
+
17
+ for (const milestone of project.milestones) {
18
+ for (const slice of milestone.slices) {
19
+ totalSlices++;
20
+ if (slice.done) doneSlices++;
21
+ for (const task of slice.tasks) {
22
+ totalTasks++;
23
+ if (task.done) doneTasks++;
24
+ }
25
+ }
26
+ }
27
+
28
+ const reqCounts = { active: 0, validated: 0, deferred: 0, outOfScope: 0, total: 0 };
29
+ for (const req of project.requirements) {
30
+ const status = req.status.toLowerCase();
31
+ if (status === 'active') reqCounts.active++;
32
+ else if (status === 'validated') reqCounts.validated++;
33
+ else if (status === 'deferred') reqCounts.deferred++;
34
+ else if (status === 'out-of-scope') reqCounts.outOfScope++;
35
+ reqCounts.total++;
36
+ }
37
+
38
+ return {
39
+ milestoneCount: project.milestones.length,
40
+ totalSlices,
41
+ totalTasks,
42
+ doneSlices,
43
+ doneTasks,
44
+ sliceCompletionPct: totalSlices > 0 ? Math.round((doneSlices / totalSlices) * 100) : 0,
45
+ taskCompletionPct: totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0,
46
+ requirements: reqCounts,
47
+ };
48
+ }