@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,539 @@
1
+ // GSD Directory Writer — Format Functions & Directory Orchestrator
2
+ // Format functions: pure string-returning functions that serialize GSD types into the exact markdown
3
+ // format that GSD-2's parsers expect (parseRoadmap, parsePlan, parseSummary, parseRequirementCounts).
4
+ // writeGSDDirectory: orchestrator that writes a complete .gsd directory tree from a GSDProject.
5
+
6
+ import { join } from 'node:path';
7
+ import { saveFile } from '../files.ts';
8
+
9
+ import type {
10
+ GSDMilestone,
11
+ GSDSlice,
12
+ GSDTask,
13
+ GSDRequirement,
14
+ GSDProject,
15
+ } from './types.ts';
16
+
17
+ // ─── Types ─────────────────────────────────────────────────────────────────
18
+
19
+ /** Result of writeGSDDirectory — lists all files that were written. */
20
+ export interface WrittenFiles {
21
+ /** Absolute paths of all files written */
22
+ paths: string[];
23
+ /** Count by category */
24
+ counts: {
25
+ roadmaps: number;
26
+ plans: number;
27
+ taskPlans: number;
28
+ taskSummaries: number;
29
+ sliceSummaries: number;
30
+ research: number;
31
+ requirements: number;
32
+ contexts: number;
33
+ other: number;
34
+ };
35
+ }
36
+
37
+ /** Pre-write statistics computed from a GSDProject without I/O. */
38
+ export interface MigrationPreview {
39
+ milestoneCount: number;
40
+ totalSlices: number;
41
+ totalTasks: number;
42
+ doneSlices: number;
43
+ doneTasks: number;
44
+ sliceCompletionPct: number;
45
+ taskCompletionPct: number;
46
+ requirements: {
47
+ active: number;
48
+ validated: number;
49
+ deferred: number;
50
+ outOfScope: number;
51
+ total: number;
52
+ };
53
+ }
54
+
55
+ // ─── Local Helpers ─────────────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Serialize a flat key-value map into YAML frontmatter block.
59
+ * Matches parseFrontmatterMap() expectations:
60
+ * - Scalars: `key: value`
61
+ * - Arrays of strings: `key:\n - item`
62
+ * - Empty arrays: `key: []`
63
+ * - Arrays of objects: `key:\n - field1: val\n field2: val`
64
+ * - Boolean: `key: true/false`
65
+ */
66
+ function serializeFrontmatter(data: Record<string, unknown>): string {
67
+ const lines: string[] = ['---'];
68
+
69
+ for (const [key, value] of Object.entries(data)) {
70
+ if (value === undefined || value === null) continue;
71
+
72
+ if (typeof value === 'boolean') {
73
+ lines.push(`${key}: ${value}`);
74
+ } else if (typeof value === 'string' || typeof value === 'number') {
75
+ lines.push(`${key}: ${value}`);
76
+ } else if (Array.isArray(value)) {
77
+ if (value.length === 0) {
78
+ lines.push(`${key}: []`);
79
+ } else if (typeof value[0] === 'object' && value[0] !== null) {
80
+ // Array of objects
81
+ lines.push(`${key}:`);
82
+ for (const obj of value) {
83
+ const entries = Object.entries(obj as Record<string, string>);
84
+ if (entries.length > 0) {
85
+ lines.push(` - ${entries[0][0]}: ${entries[0][1]}`);
86
+ for (let i = 1; i < entries.length; i++) {
87
+ lines.push(` ${entries[i][0]}: ${entries[i][1]}`);
88
+ }
89
+ }
90
+ }
91
+ } else {
92
+ // Array of scalars
93
+ lines.push(`${key}:`);
94
+ for (const item of value) {
95
+ lines.push(` - ${item}`);
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ lines.push('---');
102
+ return lines.join('\n');
103
+ }
104
+
105
+ // ─── Format Functions ──────────────────────────────────────────────────────
106
+
107
+ /**
108
+ * Format a milestone's ROADMAP.md content.
109
+ * Output must parse correctly through parseRoadmap().
110
+ */
111
+ export function formatRoadmap(milestone: GSDMilestone): string {
112
+ const lines: string[] = [];
113
+
114
+ lines.push(`# ${milestone.id}: ${milestone.title}`);
115
+ lines.push('');
116
+ lines.push(`**Vision:** ${milestone.vision || '(migrated project)'}`);
117
+ lines.push('');
118
+
119
+ lines.push('## Success Criteria');
120
+ lines.push('');
121
+ if (milestone.successCriteria.length > 0) {
122
+ for (const criterion of milestone.successCriteria) {
123
+ lines.push(`- ${criterion}`);
124
+ }
125
+ }
126
+ lines.push('');
127
+
128
+ lines.push('## Slices');
129
+ lines.push('');
130
+ for (const slice of milestone.slices) {
131
+ const check = slice.done ? 'x' : ' ';
132
+ const depsStr = slice.depends.length > 0 ? slice.depends.join(', ') : '';
133
+ lines.push(`- [${check}] **${slice.id}: ${slice.title}** \`risk:${slice.risk}\` \`depends:[${depsStr}]\``);
134
+ if (slice.demo) {
135
+ lines.push(` > After this: ${slice.demo}`);
136
+ }
137
+ }
138
+
139
+ // Skip Boundary Map section entirely per D004
140
+
141
+ return lines.join('\n') + '\n';
142
+ }
143
+
144
+ /**
145
+ * Format a slice's PLAN.md (S01-PLAN.md).
146
+ * Output must parse correctly through parsePlan().
147
+ */
148
+ export function formatPlan(slice: GSDSlice): string {
149
+ const lines: string[] = [];
150
+
151
+ lines.push(`# ${slice.id}: ${slice.title}`);
152
+ lines.push('');
153
+ lines.push(`**Goal:** ${slice.goal || slice.title}`);
154
+ lines.push(`**Demo:** ${slice.demo || slice.title}`);
155
+ lines.push('');
156
+
157
+ lines.push('## Must-Haves');
158
+ lines.push('');
159
+ // No must-haves in migrated data — empty section
160
+ lines.push('');
161
+
162
+ lines.push('## Tasks');
163
+ lines.push('');
164
+ for (const task of slice.tasks) {
165
+ const check = task.done ? 'x' : ' ';
166
+ const estPart = task.estimate ? ` \`est:${task.estimate}\`` : '';
167
+ lines.push(`- [${check}] **${task.id}: ${task.title}**${estPart}`);
168
+ if (task.description) {
169
+ lines.push(` - ${task.description}`);
170
+ }
171
+ }
172
+ lines.push('');
173
+
174
+ lines.push('## Files Likely Touched');
175
+ lines.push('');
176
+ for (const task of slice.tasks) {
177
+ for (const file of task.files) {
178
+ lines.push(`- \`${file}\``);
179
+ }
180
+ }
181
+
182
+ return lines.join('\n') + '\n';
183
+ }
184
+
185
+ /**
186
+ * Format a slice summary (S01-SUMMARY.md).
187
+ * Output must parse correctly through parseSummary().
188
+ */
189
+ export function formatSliceSummary(slice: GSDSlice, milestoneId: string): string {
190
+ if (!slice.summary) return '';
191
+
192
+ const s = slice.summary;
193
+ const fm = serializeFrontmatter({
194
+ id: slice.id,
195
+ parent: milestoneId,
196
+ milestone: milestoneId,
197
+ provides: s.provides,
198
+ requires: [],
199
+ affects: [],
200
+ key_files: s.keyFiles,
201
+ key_decisions: s.keyDecisions,
202
+ patterns_established: s.patternsEstablished,
203
+ observability_surfaces: [],
204
+ drill_down_paths: [],
205
+ duration: s.duration || '',
206
+ verification_result: 'passed',
207
+ completed_at: s.completedAt || '',
208
+ blocker_discovered: false,
209
+ });
210
+
211
+ const body = [
212
+ '',
213
+ `# ${slice.id}: ${slice.title}`,
214
+ '',
215
+ `**${s.whatHappened ? s.whatHappened.split('\n')[0] : 'Migrated from legacy format'}**`,
216
+ '',
217
+ '## What Happened',
218
+ '',
219
+ s.whatHappened || 'Migrated from legacy planning format.',
220
+ ];
221
+
222
+ return fm + body.join('\n') + '\n';
223
+ }
224
+
225
+ /**
226
+ * Format a task summary (T01-SUMMARY.md).
227
+ * Output must parse correctly through parseSummary().
228
+ */
229
+ export function formatTaskSummary(task: GSDTask, sliceId: string, milestoneId: string): string {
230
+ if (!task.summary) return '';
231
+
232
+ const s = task.summary;
233
+ const fm = serializeFrontmatter({
234
+ id: task.id,
235
+ parent: sliceId,
236
+ milestone: milestoneId,
237
+ provides: s.provides,
238
+ requires: [],
239
+ affects: [],
240
+ key_files: s.keyFiles,
241
+ key_decisions: [],
242
+ patterns_established: [],
243
+ observability_surfaces: [],
244
+ drill_down_paths: [],
245
+ duration: s.duration || '',
246
+ verification_result: 'passed',
247
+ completed_at: s.completedAt || '',
248
+ blocker_discovered: false,
249
+ });
250
+
251
+ const body = [
252
+ '',
253
+ `# ${task.id}: ${task.title}`,
254
+ '',
255
+ `**${s.whatHappened ? s.whatHappened.split('\n')[0] : 'Migrated from legacy format'}**`,
256
+ '',
257
+ '## What Happened',
258
+ '',
259
+ s.whatHappened || 'Migrated from legacy planning format.',
260
+ ];
261
+
262
+ return fm + body.join('\n') + '\n';
263
+ }
264
+
265
+ /**
266
+ * Format a task plan (T01-PLAN.md).
267
+ * deriveState() only checks for file existence, not content.
268
+ * Keep it minimal but valid markdown.
269
+ */
270
+ export function formatTaskPlan(task: GSDTask, sliceId: string, milestoneId: string): string {
271
+ const lines: string[] = [];
272
+ lines.push(`# ${task.id}: ${task.title}`);
273
+ lines.push('');
274
+ lines.push(`**Slice:** ${sliceId} — **Milestone:** ${milestoneId}`);
275
+ lines.push('');
276
+ lines.push('## Description');
277
+ lines.push('');
278
+ lines.push(task.description || 'Migrated from legacy planning format.');
279
+ lines.push('');
280
+
281
+ if (task.mustHaves.length > 0) {
282
+ lines.push('## Must-Haves');
283
+ lines.push('');
284
+ for (const mh of task.mustHaves) {
285
+ lines.push(`- [ ] ${mh}`);
286
+ }
287
+ lines.push('');
288
+ }
289
+
290
+ if (task.files.length > 0) {
291
+ lines.push('## Files');
292
+ lines.push('');
293
+ for (const f of task.files) {
294
+ lines.push(`- \`${f}\``);
295
+ }
296
+ lines.push('');
297
+ }
298
+
299
+ return lines.join('\n');
300
+ }
301
+
302
+ /**
303
+ * Format REQUIREMENTS.md grouped by status.
304
+ * Output must parse correctly through parseRequirementCounts().
305
+ * parseRequirementCounts expects: ## Active/## Validated/## Deferred/## Out of Scope sections
306
+ * with ### R001 — Title headings under each section.
307
+ */
308
+ export function formatRequirements(requirements: GSDRequirement[]): string {
309
+ const lines: string[] = [];
310
+ lines.push('# Requirements');
311
+ lines.push('');
312
+
313
+ const groups: Record<string, GSDRequirement[]> = {
314
+ active: [],
315
+ validated: [],
316
+ deferred: [],
317
+ 'out-of-scope': [],
318
+ };
319
+
320
+ for (const req of requirements) {
321
+ const status = req.status.toLowerCase();
322
+ if (status in groups) {
323
+ groups[status].push(req);
324
+ } else {
325
+ groups.active.push(req);
326
+ }
327
+ }
328
+
329
+ const sectionMap: [string, string][] = [
330
+ ['active', 'Active'],
331
+ ['validated', 'Validated'],
332
+ ['deferred', 'Deferred'],
333
+ ['out-of-scope', 'Out of Scope'],
334
+ ];
335
+
336
+ for (const [key, heading] of sectionMap) {
337
+ lines.push(`## ${heading}`);
338
+ lines.push('');
339
+ for (const req of groups[key]) {
340
+ lines.push(`### ${req.id} — ${req.title}`);
341
+ lines.push('');
342
+ lines.push(`- Status: ${req.status}`);
343
+ lines.push(`- Class: ${req.class}`);
344
+ lines.push(`- Source: ${req.source}`);
345
+ lines.push(`- Primary Slice: ${req.primarySlice}`);
346
+ lines.push('');
347
+ if (req.description) {
348
+ lines.push(req.description);
349
+ lines.push('');
350
+ }
351
+ }
352
+ }
353
+
354
+ return lines.join('\n');
355
+ }
356
+
357
+ // ─── Passthrough Format Helpers ────────────────────────────────────────────
358
+
359
+ /**
360
+ * Format PROJECT.md content.
361
+ * If content is empty, produce a minimal valid stub.
362
+ */
363
+ export function formatProject(content: string): string {
364
+ if (!content || !content.trim()) {
365
+ return '# Project\n\n(Migrated project — no description available.)\n';
366
+ }
367
+ return content.endsWith('\n') ? content : content + '\n';
368
+ }
369
+
370
+ /**
371
+ * Format DECISIONS.md content.
372
+ * If content is empty, produce the standard header.
373
+ */
374
+ export function formatDecisions(content: string): string {
375
+ if (!content || !content.trim()) {
376
+ return '# Decisions\n\n<!-- Append-only register of architectural and pattern decisions -->\n\n| ID | Decision | Rationale | Date |\n|----|----------|-----------|------|\n';
377
+ }
378
+ return content.endsWith('\n') ? content : content + '\n';
379
+ }
380
+
381
+ /**
382
+ * Format a milestone CONTEXT.md.
383
+ * Minimal context with no depends — migrated milestones have no upstream dependencies.
384
+ */
385
+ export function formatContext(milestoneId: string): string {
386
+ return `# ${milestoneId} Context\n\nMigrated milestone — no upstream dependencies.\n`;
387
+ }
388
+
389
+ /**
390
+ * Format STATE.md.
391
+ * deriveState() does not read STATE.md — it recomputes from scratch.
392
+ * Write a minimal stub that will be overwritten on first /gsd status.
393
+ */
394
+ export function formatState(milestones: GSDMilestone[]): string {
395
+ const lines: string[] = [];
396
+ lines.push('# GSD State');
397
+ lines.push('');
398
+ lines.push('<!-- Auto-generated. Updated by deriveState(). -->');
399
+ lines.push('');
400
+ for (const m of milestones) {
401
+ const doneSlices = m.slices.filter(s => s.done).length;
402
+ const totalSlices = m.slices.length;
403
+ lines.push(`## ${m.id}: ${m.title}`);
404
+ lines.push('');
405
+ lines.push(`- Slices: ${doneSlices}/${totalSlices}`);
406
+ lines.push('');
407
+ }
408
+ return lines.join('\n');
409
+ }
410
+
411
+ // ─── Directory Writer Orchestrator ─────────────────────────────────────────
412
+
413
+ /**
414
+ * Write a complete .gsd directory tree from a GSDProject.
415
+ * Iterates milestones → slices → tasks, calls format functions,
416
+ * and writes each file via saveFile(). Returns a manifest of written paths.
417
+ *
418
+ * Skips research/summary files when null (does not write empty stubs).
419
+ */
420
+ export async function writeGSDDirectory(
421
+ project: GSDProject,
422
+ targetPath: string,
423
+ ): Promise<WrittenFiles> {
424
+ const gsdDir = join(targetPath, '.gsd');
425
+ const milestonesBase = join(gsdDir, 'milestones');
426
+ const paths: string[] = [];
427
+ const counts: WrittenFiles['counts'] = {
428
+ roadmaps: 0,
429
+ plans: 0,
430
+ taskPlans: 0,
431
+ taskSummaries: 0,
432
+ sliceSummaries: 0,
433
+ research: 0,
434
+ requirements: 0,
435
+ contexts: 0,
436
+ other: 0,
437
+ };
438
+
439
+ // Root-level files
440
+ const projectPath = join(gsdDir, 'PROJECT.md');
441
+ await saveFile(projectPath, formatProject(project.projectContent));
442
+ paths.push(projectPath);
443
+ counts.other++;
444
+
445
+ const decisionsPath = join(gsdDir, 'DECISIONS.md');
446
+ await saveFile(decisionsPath, formatDecisions(project.decisionsContent));
447
+ paths.push(decisionsPath);
448
+ counts.other++;
449
+
450
+ const statePath = join(gsdDir, 'STATE.md');
451
+ await saveFile(statePath, formatState(project.milestones));
452
+ paths.push(statePath);
453
+ counts.other++;
454
+
455
+ if (project.requirements.length > 0) {
456
+ const reqPath = join(gsdDir, 'REQUIREMENTS.md');
457
+ await saveFile(reqPath, formatRequirements(project.requirements));
458
+ paths.push(reqPath);
459
+ counts.requirements++;
460
+ }
461
+
462
+ // Milestones
463
+ for (const milestone of project.milestones) {
464
+ const mDir = join(milestonesBase, milestone.id);
465
+
466
+ // Roadmap (always written, even for empty milestones)
467
+ const roadmapPath = join(mDir, `${milestone.id}-ROADMAP.md`);
468
+ await saveFile(roadmapPath, formatRoadmap(milestone));
469
+ paths.push(roadmapPath);
470
+ counts.roadmaps++;
471
+
472
+ // Context
473
+ const contextPath = join(mDir, `${milestone.id}-CONTEXT.md`);
474
+ await saveFile(contextPath, formatContext(milestone.id));
475
+ paths.push(contextPath);
476
+ counts.contexts++;
477
+
478
+ // Research (skip if null)
479
+ if (milestone.research !== null) {
480
+ const researchPath = join(mDir, `${milestone.id}-RESEARCH.md`);
481
+ await saveFile(researchPath, milestone.research);
482
+ paths.push(researchPath);
483
+ counts.research++;
484
+ }
485
+
486
+ // Slices
487
+ for (const slice of milestone.slices) {
488
+ const sDir = join(mDir, 'slices', slice.id);
489
+ const tasksDir = join(sDir, 'tasks');
490
+
491
+ // Slice plan
492
+ const planPath = join(sDir, `${slice.id}-PLAN.md`);
493
+ await saveFile(planPath, formatPlan(slice));
494
+ paths.push(planPath);
495
+ counts.plans++;
496
+
497
+ // Slice research (skip if null)
498
+ if (slice.research !== null) {
499
+ const sliceResearchPath = join(sDir, `${slice.id}-RESEARCH.md`);
500
+ await saveFile(sliceResearchPath, slice.research);
501
+ paths.push(sliceResearchPath);
502
+ counts.research++;
503
+ }
504
+
505
+ // Slice summary (skip if null)
506
+ if (slice.summary !== null) {
507
+ const summaryContent = formatSliceSummary(slice, milestone.id);
508
+ if (summaryContent) {
509
+ const summaryPath = join(sDir, `${slice.id}-SUMMARY.md`);
510
+ await saveFile(summaryPath, summaryContent);
511
+ paths.push(summaryPath);
512
+ counts.sliceSummaries++;
513
+ }
514
+ }
515
+
516
+ // Tasks
517
+ for (const task of slice.tasks) {
518
+ // Task plan (always written)
519
+ const taskPlanPath = join(tasksDir, `${task.id}-PLAN.md`);
520
+ await saveFile(taskPlanPath, formatTaskPlan(task, slice.id, milestone.id));
521
+ paths.push(taskPlanPath);
522
+ counts.taskPlans++;
523
+
524
+ // Task summary (skip if null)
525
+ if (task.summary !== null) {
526
+ const taskSummaryContent = formatTaskSummary(task, slice.id, milestone.id);
527
+ if (taskSummaryContent) {
528
+ const taskSummaryPath = join(tasksDir, `${task.id}-SUMMARY.md`);
529
+ await saveFile(taskSummaryPath, taskSummaryContent);
530
+ paths.push(taskSummaryPath);
531
+ counts.taskSummaries++;
532
+ }
533
+ }
534
+ }
535
+ }
536
+ }
537
+
538
+ return { paths, counts };
539
+ }