@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,217 @@
1
+ /**
2
+ * Tests for GSD metrics aggregation logic.
3
+ * Tests the pure functions — no file I/O, no extension context.
4
+ */
5
+
6
+ import {
7
+ type UnitMetrics,
8
+ type TokenCounts,
9
+ classifyUnitPhase,
10
+ aggregateByPhase,
11
+ aggregateBySlice,
12
+ aggregateByModel,
13
+ getProjectTotals,
14
+ formatCost,
15
+ formatTokenCount,
16
+ } from "../metrics.js";
17
+
18
+ // ─── Test helpers ─────────────────────────────────────────────────────────────
19
+
20
+ function makeUnit(overrides: Partial<UnitMetrics> = {}): UnitMetrics {
21
+ return {
22
+ type: "execute-task",
23
+ id: "M001/S01/T01",
24
+ model: "claude-sonnet-4-20250514",
25
+ startedAt: 1000,
26
+ finishedAt: 2000,
27
+ tokens: { input: 1000, output: 500, cacheRead: 200, cacheWrite: 100, total: 1800 },
28
+ cost: 0.05,
29
+ toolCalls: 3,
30
+ assistantMessages: 2,
31
+ userMessages: 1,
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ let passed = 0;
37
+ let failed = 0;
38
+
39
+ function assert(condition: boolean, message: string): void {
40
+ if (condition) {
41
+ passed++;
42
+ } else {
43
+ failed++;
44
+ console.error(` FAIL: ${message}`);
45
+ }
46
+ }
47
+
48
+ function assertEq<T>(actual: T, expected: T, message: string): void {
49
+ if (actual === expected) {
50
+ passed++;
51
+ } else {
52
+ failed++;
53
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
54
+ }
55
+ }
56
+
57
+ function assertClose(actual: number, expected: number, tolerance: number, message: string): void {
58
+ if (Math.abs(actual - expected) <= tolerance) {
59
+ passed++;
60
+ } else {
61
+ failed++;
62
+ console.error(` FAIL: ${message} — expected ~${expected}, got ${actual}`);
63
+ }
64
+ }
65
+
66
+ // ─── Phase classification ─────────────────────────────────────────────────────
67
+
68
+ console.log("\n=== classifyUnitPhase ===");
69
+
70
+ assertEq(classifyUnitPhase("research-milestone"), "research", "research-milestone → research");
71
+ assertEq(classifyUnitPhase("research-slice"), "research", "research-slice → research");
72
+ assertEq(classifyUnitPhase("plan-milestone"), "planning", "plan-milestone → planning");
73
+ assertEq(classifyUnitPhase("plan-slice"), "planning", "plan-slice → planning");
74
+ assertEq(classifyUnitPhase("execute-task"), "execution", "execute-task → execution");
75
+ assertEq(classifyUnitPhase("complete-slice"), "completion", "complete-slice → completion");
76
+ assertEq(classifyUnitPhase("reassess-roadmap"), "reassessment", "reassess-roadmap → reassessment");
77
+ assertEq(classifyUnitPhase("unknown-thing"), "execution", "unknown → execution (fallback)");
78
+
79
+ // ─── getProjectTotals ─────────────────────────────────────────────────────────
80
+
81
+ console.log("\n=== getProjectTotals ===");
82
+
83
+ {
84
+ const units = [
85
+ makeUnit({ tokens: { input: 1000, output: 500, cacheRead: 200, cacheWrite: 100, total: 1800 }, cost: 0.05, toolCalls: 3, startedAt: 1000, finishedAt: 2000 }),
86
+ makeUnit({ tokens: { input: 2000, output: 1000, cacheRead: 400, cacheWrite: 200, total: 3600 }, cost: 0.10, toolCalls: 5, startedAt: 2000, finishedAt: 4000 }),
87
+ ];
88
+ const totals = getProjectTotals(units);
89
+
90
+ assertEq(totals.units, 2, "total units");
91
+ assertEq(totals.tokens.input, 3000, "total input tokens");
92
+ assertEq(totals.tokens.output, 1500, "total output tokens");
93
+ assertEq(totals.tokens.cacheRead, 600, "total cacheRead");
94
+ assertEq(totals.tokens.cacheWrite, 300, "total cacheWrite");
95
+ assertEq(totals.tokens.total, 5400, "total tokens");
96
+ assertClose(totals.cost, 0.15, 0.001, "total cost");
97
+ assertEq(totals.toolCalls, 8, "total tool calls");
98
+ assertEq(totals.duration, 3000, "total duration");
99
+ }
100
+
101
+ {
102
+ const totals = getProjectTotals([]);
103
+ assertEq(totals.units, 0, "empty: zero units");
104
+ assertEq(totals.cost, 0, "empty: zero cost");
105
+ assertEq(totals.tokens.total, 0, "empty: zero tokens");
106
+ }
107
+
108
+ // ─── aggregateByPhase ─────────────────────────────────────────────────────────
109
+
110
+ console.log("\n=== aggregateByPhase ===");
111
+
112
+ {
113
+ const units = [
114
+ makeUnit({ type: "research-milestone", cost: 0.02 }),
115
+ makeUnit({ type: "research-slice", cost: 0.03 }),
116
+ makeUnit({ type: "plan-milestone", cost: 0.01 }),
117
+ makeUnit({ type: "plan-slice", cost: 0.02 }),
118
+ makeUnit({ type: "execute-task", cost: 0.10 }),
119
+ makeUnit({ type: "execute-task", cost: 0.08 }),
120
+ makeUnit({ type: "complete-slice", cost: 0.01 }),
121
+ makeUnit({ type: "reassess-roadmap", cost: 0.005 }),
122
+ ];
123
+ const phases = aggregateByPhase(units);
124
+
125
+ assertEq(phases.length, 5, "5 phases");
126
+ assertEq(phases[0].phase, "research", "first phase is research");
127
+ assertEq(phases[0].units, 2, "2 research units");
128
+ assertClose(phases[0].cost, 0.05, 0.001, "research cost");
129
+
130
+ assertEq(phases[1].phase, "planning", "second phase is planning");
131
+ assertEq(phases[1].units, 2, "2 planning units");
132
+
133
+ assertEq(phases[2].phase, "execution", "third phase is execution");
134
+ assertEq(phases[2].units, 2, "2 execution units");
135
+ assertClose(phases[2].cost, 0.18, 0.001, "execution cost");
136
+
137
+ assertEq(phases[3].phase, "completion", "fourth phase is completion");
138
+ assertEq(phases[4].phase, "reassessment", "fifth phase is reassessment");
139
+ }
140
+
141
+ // ─── aggregateBySlice ─────────────────────────────────────────────────────────
142
+
143
+ console.log("\n=== aggregateBySlice ===");
144
+
145
+ {
146
+ const units = [
147
+ makeUnit({ id: "M001/S01/T01", cost: 0.05 }),
148
+ makeUnit({ id: "M001/S01/T02", cost: 0.04 }),
149
+ makeUnit({ id: "M001/S02/T01", cost: 0.10 }),
150
+ makeUnit({ id: "M001", type: "research-milestone", cost: 0.02 }),
151
+ ];
152
+ const slices = aggregateBySlice(units);
153
+
154
+ assertEq(slices.length, 3, "3 slice groups");
155
+
156
+ const s01 = slices.find(s => s.sliceId === "M001/S01");
157
+ assert(!!s01, "M001/S01 exists");
158
+ assertEq(s01!.units, 2, "M001/S01 has 2 units");
159
+ assertClose(s01!.cost, 0.09, 0.001, "M001/S01 cost");
160
+
161
+ const s02 = slices.find(s => s.sliceId === "M001/S02");
162
+ assert(!!s02, "M001/S02 exists");
163
+ assertEq(s02!.units, 1, "M001/S02 has 1 unit");
164
+
165
+ const mLevel = slices.find(s => s.sliceId === "M001");
166
+ assert(!!mLevel, "M001 (milestone-level) exists");
167
+ }
168
+
169
+ // ─── aggregateByModel ─────────────────────────────────────────────────────────
170
+
171
+ console.log("\n=== aggregateByModel ===");
172
+
173
+ {
174
+ const units = [
175
+ makeUnit({ model: "claude-sonnet-4-20250514", cost: 0.05 }),
176
+ makeUnit({ model: "claude-sonnet-4-20250514", cost: 0.04 }),
177
+ makeUnit({ model: "claude-opus-4-20250514", cost: 0.30 }),
178
+ ];
179
+ const models = aggregateByModel(units);
180
+
181
+ assertEq(models.length, 2, "2 models");
182
+ // Sorted by cost desc — opus should be first
183
+ assertEq(models[0].model, "claude-opus-4-20250514", "opus first (higher cost)");
184
+ assertClose(models[0].cost, 0.30, 0.001, "opus cost");
185
+ assertEq(models[1].model, "claude-sonnet-4-20250514", "sonnet second");
186
+ assertEq(models[1].units, 2, "sonnet has 2 units");
187
+ }
188
+
189
+ // ─── formatCost ───────────────────────────────────────────────────────────────
190
+
191
+ console.log("\n=== formatCost ===");
192
+
193
+ assertEq(formatCost(0), "$0.0000", "zero cost");
194
+ assertEq(formatCost(0.001), "$0.0010", "sub-cent cost");
195
+ assertEq(formatCost(0.05), "$0.050", "5 cents");
196
+ assertEq(formatCost(1.50), "$1.50", "dollar+");
197
+ assertEq(formatCost(14.20), "$14.20", "double digits");
198
+
199
+ // ─── formatTokenCount ─────────────────────────────────────────────────────────
200
+
201
+ console.log("\n=== formatTokenCount ===");
202
+
203
+ assertEq(formatTokenCount(0), "0", "zero tokens");
204
+ assertEq(formatTokenCount(500), "500", "sub-k");
205
+ assertEq(formatTokenCount(1500), "1.5k", "1.5k");
206
+ assertEq(formatTokenCount(150000), "150.0k", "150k");
207
+ assertEq(formatTokenCount(1500000), "1.50M", "1.5M");
208
+
209
+ // ─── Summary ──────────────────────────────────────────────────────────────────
210
+
211
+ console.log(`\n${"=".repeat(40)}`);
212
+ console.log(`Results: ${passed} passed, ${failed} failed`);
213
+ if (failed > 0) {
214
+ process.exit(1);
215
+ } else {
216
+ console.log("All tests passed ✓");
217
+ }
@@ -0,0 +1,390 @@
1
+ // Migration command integration test
2
+ // Tests the pipeline functions as the command handler uses them:
3
+ // path resolution, validation gating, full parse→transform→preview→write→deriveState round-trip.
4
+ // Exercises pipeline modules directly — no TUI context dependency.
5
+
6
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs';
7
+ import { join, resolve } from 'node:path';
8
+ import { tmpdir } from 'node:os';
9
+
10
+ import {
11
+ validatePlanningDirectory,
12
+ parsePlanningDirectory,
13
+ transformToGSD,
14
+ generatePreview,
15
+ writeGSDDirectory,
16
+ } from '../migrate/index.ts';
17
+ import { deriveState } from '../state.ts';
18
+
19
+ let passed = 0;
20
+ let failed = 0;
21
+
22
+ function assert(condition: boolean, message: string): void {
23
+ if (condition) {
24
+ passed++;
25
+ } else {
26
+ failed++;
27
+ console.error(` FAIL: ${message}`);
28
+ }
29
+ }
30
+
31
+ function assertEq<T>(actual: T, expected: T, message: string): void {
32
+ if (JSON.stringify(actual) === JSON.stringify(expected)) {
33
+ passed++;
34
+ } else {
35
+ failed++;
36
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
37
+ }
38
+ }
39
+
40
+ // ─── Fixture Helpers ───────────────────────────────────────────────────────
41
+
42
+ const SAMPLE_PROJECT = `# Integration Test Project
43
+
44
+ A project used for command pipeline integration testing.
45
+
46
+ ## Goals
47
+
48
+ - Test the full migration pipeline
49
+ `;
50
+
51
+ const SAMPLE_ROADMAP = `# Project Roadmap
52
+
53
+ ## Phases
54
+
55
+ - [x] 10 — Foundation
56
+ - [ ] 20 — Features
57
+ `;
58
+
59
+ const SAMPLE_REQUIREMENTS = `# Requirements
60
+
61
+ ## Active
62
+
63
+ ### R001 — Core Pipeline
64
+ - Status: active
65
+ - Description: Pipeline must work end-to-end.
66
+
67
+ ## Validated
68
+
69
+ ### R002 — Output Format
70
+ - Status: validated
71
+ - Description: Output matches GSD format.
72
+ `;
73
+
74
+ const SAMPLE_STATE = `# State
75
+
76
+ **Current Phase:** 20-features
77
+ **Status:** in-progress
78
+ `;
79
+
80
+ const SAMPLE_CONFIG = JSON.stringify({
81
+ projectName: 'pipeline-test',
82
+ version: '1.0',
83
+ });
84
+
85
+ const SAMPLE_PLAN_10_01 = `---
86
+ phase: "10-foundation"
87
+ plan: "01"
88
+ type: "implementation"
89
+ wave: 1
90
+ depends_on: []
91
+ files_modified: [src/core.ts]
92
+ autonomous: true
93
+ must_haves:
94
+ truths:
95
+ - Core module works
96
+ artifacts:
97
+ - src/core.ts
98
+ key_links: []
99
+ ---
100
+
101
+ # 10-01: Build Foundation
102
+
103
+ <objective>
104
+ Set up the project foundation and core module.
105
+ </objective>
106
+
107
+ <tasks>
108
+ <task>Create core module</task>
109
+ <task>Add configuration loader</task>
110
+ </tasks>
111
+
112
+ <context>
113
+ Foundation work needed before features.
114
+ </context>
115
+
116
+ <verification>
117
+ - Core module loads
118
+ - Config is parsed
119
+ </verification>
120
+
121
+ <success_criteria>
122
+ Core is operational.
123
+ </success_criteria>
124
+ `;
125
+
126
+ const SAMPLE_SUMMARY_10_01 = `---
127
+ phase: "10-foundation"
128
+ plan: "01"
129
+ subsystem: "core"
130
+ tags:
131
+ - foundation
132
+ requires: []
133
+ provides:
134
+ - core-module
135
+ affects:
136
+ - features
137
+ tech-stack:
138
+ - typescript
139
+ key-files:
140
+ - src/core.ts
141
+ key-decisions:
142
+ - Use TypeScript strict mode
143
+ patterns-established:
144
+ - Module pattern
145
+ duration: "1h"
146
+ completed: "2026-01-10"
147
+ ---
148
+
149
+ # 10-01: Foundation Summary
150
+
151
+ Core module built and operational.
152
+
153
+ ## What Happened
154
+
155
+ Created core module and configuration loader.
156
+
157
+ ## Files Modified
158
+
159
+ - \`src/core.ts\` — Core module
160
+ `;
161
+
162
+ const SAMPLE_PLAN_20_01 = `---
163
+ phase: "20-features"
164
+ plan: "01"
165
+ type: "implementation"
166
+ wave: 1
167
+ depends_on: [10-01]
168
+ files_modified: []
169
+ autonomous: false
170
+ ---
171
+
172
+ # 20-01: Build Feature A
173
+
174
+ <objective>
175
+ Implement the first feature.
176
+ </objective>
177
+
178
+ <tasks>
179
+ <task>Design feature API</task>
180
+ <task>Implement feature logic</task>
181
+ </tasks>
182
+
183
+ <context>
184
+ Depends on foundation work.
185
+ </context>
186
+ `;
187
+
188
+ function createCompleteFixture(): string {
189
+ const base = mkdtempSync(join(tmpdir(), 'gsd-cmd-test-'));
190
+ const planning = join(base, '.planning');
191
+ mkdirSync(planning, { recursive: true });
192
+
193
+ writeFileSync(join(planning, 'PROJECT.md'), SAMPLE_PROJECT);
194
+ writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP);
195
+ writeFileSync(join(planning, 'REQUIREMENTS.md'), SAMPLE_REQUIREMENTS);
196
+ writeFileSync(join(planning, 'STATE.md'), SAMPLE_STATE);
197
+ writeFileSync(join(planning, 'config.json'), SAMPLE_CONFIG);
198
+
199
+ // Phase 10: done — has plan + summary
200
+ const phase10 = join(planning, 'phases', '10-foundation');
201
+ mkdirSync(phase10, { recursive: true });
202
+ writeFileSync(join(phase10, '10-01-PLAN.md'), SAMPLE_PLAN_10_01);
203
+ writeFileSync(join(phase10, '10-01-SUMMARY.md'), SAMPLE_SUMMARY_10_01);
204
+
205
+ // Phase 20: in-progress — has plan, no summary
206
+ const phase20 = join(planning, 'phases', '20-features');
207
+ mkdirSync(phase20, { recursive: true });
208
+ writeFileSync(join(phase20, '20-01-PLAN.md'), SAMPLE_PLAN_20_01);
209
+
210
+ return base;
211
+ }
212
+
213
+ // ═══════════════════════════════════════════════════════════════════════════
214
+ // Tests
215
+ // ═══════════════════════════════════════════════════════════════════════════
216
+
217
+ async function main(): Promise<void> {
218
+
219
+ // ─── Test 1: Path resolution — .planning appended when missing ─────────
220
+ console.log('\n=== Path resolution: .planning appended when source path lacks it ===');
221
+ {
222
+ const base = createCompleteFixture();
223
+ try {
224
+ // Simulate the command's path resolution logic
225
+ let sourcePath = resolve(base); // no .planning suffix
226
+ if (!sourcePath.endsWith('.planning')) {
227
+ sourcePath = join(sourcePath, '.planning');
228
+ }
229
+ assert(sourcePath.endsWith('.planning'), 'path-resolution: .planning appended');
230
+ assert(existsSync(sourcePath), 'path-resolution: appended path exists');
231
+ } finally {
232
+ rmSync(base, { recursive: true, force: true });
233
+ }
234
+ }
235
+
236
+ // ─── Test 2: Path resolution — .planning used as-is ────────────────────
237
+ console.log('\n=== Path resolution: .planning used as-is when already present ===');
238
+ {
239
+ const base = createCompleteFixture();
240
+ try {
241
+ const planningPath = join(base, '.planning');
242
+ let sourcePath = resolve(planningPath);
243
+ if (!sourcePath.endsWith('.planning')) {
244
+ sourcePath = join(sourcePath, '.planning');
245
+ }
246
+ assertEq(sourcePath, resolve(planningPath), 'path-resolution: .planning not double-appended');
247
+ assert(existsSync(sourcePath), 'path-resolution: direct path exists');
248
+ } finally {
249
+ rmSync(base, { recursive: true, force: true });
250
+ }
251
+ }
252
+
253
+ // ─── Test 3: Validation gating — non-existent path ─────────────────────
254
+ console.log('\n=== Validation gating: non-existent path returns invalid ===');
255
+ {
256
+ const fakePath = join(tmpdir(), 'gsd-cmd-nonexistent-' + Date.now(), '.planning');
257
+ const result = await validatePlanningDirectory(fakePath);
258
+ assertEq(result.valid, false, 'validation: non-existent path is invalid');
259
+ assert(result.issues.length > 0, 'validation: has issues for non-existent path');
260
+ const hasFatal = result.issues.some(i => i.severity === 'fatal');
261
+ assert(hasFatal, 'validation: non-existent path has fatal issue');
262
+ }
263
+
264
+ // ─── Test 4: Validation gating — valid fixture passes ──────────────────
265
+ console.log('\n=== Validation gating: valid fixture passes validation ===');
266
+ {
267
+ const base = createCompleteFixture();
268
+ try {
269
+ const result = await validatePlanningDirectory(join(base, '.planning'));
270
+ assert(result.valid === true, 'validation: valid fixture passes');
271
+ } finally {
272
+ rmSync(base, { recursive: true, force: true });
273
+ }
274
+ }
275
+
276
+ // ─── Test 5: Full pipeline round-trip ──────────────────────────────────
277
+ console.log('\n=== Full pipeline: parse → transform → preview → write → deriveState ===');
278
+ {
279
+ const base = createCompleteFixture();
280
+ const writeTarget = mkdtempSync(join(tmpdir(), 'gsd-cmd-write-'));
281
+ try {
282
+ const planningPath = join(base, '.planning');
283
+
284
+ // (a) Validate
285
+ const validation = await validatePlanningDirectory(planningPath);
286
+ assert(validation.valid === true, 'pipeline: validation passes');
287
+
288
+ // (b) Parse
289
+ const parsed = await parsePlanningDirectory(planningPath);
290
+ assert(parsed.roadmap !== null, 'pipeline: roadmap parsed');
291
+ assert(Object.keys(parsed.phases).length >= 2, 'pipeline: phases parsed');
292
+
293
+ // (c) Transform
294
+ const project = transformToGSD(parsed);
295
+ assert(project.milestones.length >= 1, 'pipeline: has milestones');
296
+ assert(project.milestones[0].slices.length >= 1, 'pipeline: has slices');
297
+
298
+ // Count totals for preview verification
299
+ let totalTasks = 0;
300
+ let doneTasks = 0;
301
+ let totalSlices = 0;
302
+ let doneSlices = 0;
303
+ for (const m of project.milestones) {
304
+ for (const s of m.slices) {
305
+ totalSlices++;
306
+ if (s.done) doneSlices++;
307
+ for (const t of s.tasks) {
308
+ totalTasks++;
309
+ if (t.done) doneTasks++;
310
+ }
311
+ }
312
+ }
313
+
314
+ // (d) Preview — verify counts match project data
315
+ const preview = generatePreview(project);
316
+ assertEq(preview.milestoneCount, project.milestones.length, 'pipeline: preview milestoneCount');
317
+ assertEq(preview.totalSlices, totalSlices, 'pipeline: preview totalSlices');
318
+ assertEq(preview.totalTasks, totalTasks, 'pipeline: preview totalTasks');
319
+ assertEq(preview.doneSlices, doneSlices, 'pipeline: preview doneSlices');
320
+ assertEq(preview.doneTasks, doneTasks, 'pipeline: preview doneTasks');
321
+
322
+ // Completion percentages
323
+ const expectedSlicePct = totalSlices > 0 ? Math.round((doneSlices / totalSlices) * 100) : 0;
324
+ const expectedTaskPct = totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0;
325
+ assertEq(preview.sliceCompletionPct, expectedSlicePct, 'pipeline: preview sliceCompletionPct');
326
+ assertEq(preview.taskCompletionPct, expectedTaskPct, 'pipeline: preview taskCompletionPct');
327
+
328
+ // Requirements in preview
329
+ assertEq(preview.requirements.active, 1, 'pipeline: preview requirements active');
330
+ assertEq(preview.requirements.validated, 1, 'pipeline: preview requirements validated');
331
+ assertEq(preview.requirements.total, 2, 'pipeline: preview requirements total');
332
+
333
+ // (e) Write
334
+ const result = await writeGSDDirectory(project, writeTarget);
335
+ assert(result.paths.length > 0, 'pipeline: files written');
336
+
337
+ // Key files exist
338
+ const gsd = join(writeTarget, '.gsd');
339
+ assert(existsSync(join(gsd, 'PROJECT.md')), 'pipeline: PROJECT.md written');
340
+ assert(existsSync(join(gsd, 'STATE.md')), 'pipeline: STATE.md written');
341
+ assert(existsSync(join(gsd, 'REQUIREMENTS.md')), 'pipeline: REQUIREMENTS.md written');
342
+
343
+ const m001 = join(gsd, 'milestones', 'M001');
344
+ assert(existsSync(join(m001, 'M001-ROADMAP.md')), 'pipeline: M001-ROADMAP.md written');
345
+ assert(existsSync(join(m001, 'M001-CONTEXT.md')), 'pipeline: M001-CONTEXT.md written');
346
+
347
+ // At least one slice plan exists
348
+ const s01Plan = join(m001, 'slices', 'S01', 'S01-PLAN.md');
349
+ assert(existsSync(s01Plan), 'pipeline: S01-PLAN.md written');
350
+
351
+ // (f) deriveState — coherent state from written output
352
+ console.log(' --- deriveState ---');
353
+ const state = await deriveState(writeTarget);
354
+ assert(state.phase !== undefined, 'pipeline: deriveState returns phase');
355
+ assert(state.activeMilestone !== null, 'pipeline: deriveState has activeMilestone');
356
+ assertEq(state.activeMilestone!.id, 'M001', 'pipeline: deriveState activeMilestone is M001');
357
+ assert(state.progress.slices !== undefined, 'pipeline: deriveState has slices progress');
358
+ assert(state.progress.tasks !== undefined, 'pipeline: deriveState has tasks progress');
359
+
360
+ } finally {
361
+ rmSync(base, { recursive: true, force: true });
362
+ rmSync(writeTarget, { recursive: true, force: true });
363
+ }
364
+ }
365
+
366
+ // ─── Test 6: .gsd/ exists detection ────────────────────────────────────
367
+ console.log('\n=== .gsd/ exists detection ===');
368
+ {
369
+ const base = mkdtempSync(join(tmpdir(), 'gsd-cmd-exists-'));
370
+ try {
371
+ // No .gsd/ yet
372
+ assert(!existsSync(join(base, '.gsd')), 'exists-detection: .gsd absent initially');
373
+
374
+ // Create .gsd/
375
+ mkdirSync(join(base, '.gsd'), { recursive: true });
376
+ assert(existsSync(join(base, '.gsd')), 'exists-detection: .gsd detected after creation');
377
+ } finally {
378
+ rmSync(base, { recursive: true, force: true });
379
+ }
380
+ }
381
+
382
+ // ─── Results ─────────────────────────────────────────────────────────────
383
+ console.log(`\n${passed + failed} assertions: ${passed} passed, ${failed} failed`);
384
+ if (failed > 0) process.exit(1);
385
+ }
386
+
387
+ main().catch((err) => {
388
+ console.error('Unhandled error:', err);
389
+ process.exit(1);
390
+ });