@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,689 @@
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+
5
+ import { deriveState, isSliceComplete, isMilestoneComplete } from '../state.ts';
6
+
7
+ let passed = 0;
8
+ let failed = 0;
9
+
10
+ function assert(condition: boolean, message: string): void {
11
+ if (condition) {
12
+ passed++;
13
+ } else {
14
+ failed++;
15
+ console.error(` FAIL: ${message}`);
16
+ }
17
+ }
18
+
19
+ function assertEq<T>(actual: T, expected: T, message: string): void {
20
+ if (JSON.stringify(actual) === JSON.stringify(expected)) {
21
+ passed++;
22
+ } else {
23
+ failed++;
24
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
25
+ }
26
+ }
27
+
28
+ // ─── Fixture Helpers ───────────────────────────────────────────────────────
29
+
30
+ function createFixtureBase(): string {
31
+ const base = mkdtempSync(join(tmpdir(), 'gsd-state-test-'));
32
+ mkdirSync(join(base, '.gsd', 'milestones'), { recursive: true });
33
+ return base;
34
+ }
35
+
36
+ function writeRoadmap(base: string, mid: string, content: string): void {
37
+ const dir = join(base, '.gsd', 'milestones', mid);
38
+ mkdirSync(dir, { recursive: true });
39
+ writeFileSync(join(dir, `${mid}-ROADMAP.md`), content);
40
+ }
41
+
42
+ function writePlan(base: string, mid: string, sid: string, content: string): void {
43
+ const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
44
+ mkdirSync(join(dir, 'tasks'), { recursive: true });
45
+ writeFileSync(join(dir, `${sid}-PLAN.md`), content);
46
+ }
47
+
48
+ function writeContinue(base: string, mid: string, sid: string, content: string): void {
49
+ const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
50
+ mkdirSync(dir, { recursive: true });
51
+ writeFileSync(join(dir, `${sid}-CONTINUE.md`), content);
52
+ }
53
+
54
+ function writeMilestoneSummary(base: string, mid: string, content: string): void {
55
+ const dir = join(base, '.gsd', 'milestones', mid);
56
+ mkdirSync(dir, { recursive: true });
57
+ writeFileSync(join(dir, `${mid}-SUMMARY.md`), content);
58
+ }
59
+
60
+ function writeRequirements(base: string, content: string): void {
61
+ writeFileSync(join(base, '.gsd', 'REQUIREMENTS.md'), content);
62
+ }
63
+
64
+ function cleanup(base: string): void {
65
+ rmSync(base, { recursive: true, force: true });
66
+ }
67
+
68
+ // ═══════════════════════════════════════════════════════════════════════════
69
+ // Test Groups
70
+ // ═══════════════════════════════════════════════════════════════════════════
71
+
72
+ async function main(): Promise<void> {
73
+
74
+ // ─── Test 1: empty milestones dir → pre-planning ───────────────────────
75
+ console.log('\n=== empty milestones dir → pre-planning ===');
76
+ {
77
+ const base = createFixtureBase();
78
+ try {
79
+ const state = await deriveState(base);
80
+
81
+ assertEq(state.phase, 'pre-planning', 'phase is pre-planning');
82
+ assertEq(state.activeMilestone, null, 'activeMilestone is null');
83
+ assertEq(state.activeSlice, null, 'activeSlice is null');
84
+ assertEq(state.activeTask, null, 'activeTask is null');
85
+ assertEq(state.registry, [], 'registry is empty');
86
+ assertEq(state.progress?.milestones?.done, 0, 'milestones done = 0');
87
+ assertEq(state.progress?.milestones?.total, 0, 'milestones total = 0');
88
+ } finally {
89
+ cleanup(base);
90
+ }
91
+ }
92
+
93
+ // ─── Test 2: milestone dir exists but no roadmap → pre-planning ────────
94
+ console.log('\n=== milestone dir exists but no roadmap → pre-planning ===');
95
+ {
96
+ const base = createFixtureBase();
97
+ try {
98
+ // Create M001 directory but no roadmap file
99
+ mkdirSync(join(base, '.gsd', 'milestones', 'M001'), { recursive: true });
100
+
101
+ const state = await deriveState(base);
102
+
103
+ assertEq(state.phase, 'pre-planning', 'phase is pre-planning');
104
+ assert(state.activeMilestone !== null, 'activeMilestone is not null');
105
+ assertEq(state.activeMilestone?.id, 'M001', 'activeMilestone id is M001');
106
+ assertEq(state.activeSlice, null, 'activeSlice is null');
107
+ assertEq(state.activeTask, null, 'activeTask is null');
108
+ assertEq(state.registry.length, 1, 'registry has 1 entry');
109
+ assertEq(state.registry[0]?.status, 'active', 'registry entry status is active');
110
+ } finally {
111
+ cleanup(base);
112
+ }
113
+ }
114
+
115
+ // ─── Test 3: roadmap with incomplete slice, no plan → planning ─────────
116
+ console.log('\n=== roadmap with incomplete slice, no plan → planning ===');
117
+ {
118
+ const base = createFixtureBase();
119
+ try {
120
+ writeRoadmap(base, 'M001', `# M001: Test Milestone
121
+
122
+ **Vision:** Test planning phase.
123
+
124
+ ## Slices
125
+
126
+ - [ ] **S01: Test Slice** \`risk:low\` \`depends:[]\`
127
+ > After this: Slice is done.
128
+ `);
129
+
130
+ const state = await deriveState(base);
131
+
132
+ assertEq(state.phase, 'planning', 'phase is planning');
133
+ assert(state.activeSlice !== null, 'activeSlice is not null');
134
+ assertEq(state.activeSlice?.id, 'S01', 'activeSlice id is S01');
135
+ assertEq(state.activeTask, null, 'activeTask is null');
136
+ assertEq(state.progress?.slices?.done, 0, 'slices done = 0');
137
+ assertEq(state.progress?.slices?.total, 1, 'slices total = 1');
138
+ } finally {
139
+ cleanup(base);
140
+ }
141
+ }
142
+
143
+ // ─── Test 4: roadmap + plan with incomplete tasks → executing ──────────
144
+ console.log('\n=== roadmap + plan with incomplete tasks → executing ===');
145
+ {
146
+ const base = createFixtureBase();
147
+ try {
148
+ writeRoadmap(base, 'M001', `# M001: Test Milestone
149
+
150
+ **Vision:** Test executing phase.
151
+
152
+ ## Slices
153
+
154
+ - [ ] **S01: Test Slice** \`risk:low\` \`depends:[]\`
155
+ > After this: Slice is done.
156
+ `);
157
+
158
+ writePlan(base, 'M001', 'S01', `# S01: Test Slice
159
+
160
+ **Goal:** Test executing.
161
+ **Demo:** Tests pass.
162
+
163
+ ## Tasks
164
+
165
+ - [ ] **T01: First** \`est:10m\`
166
+ First task description.
167
+
168
+ - [ ] **T02: Second** \`est:10m\`
169
+ Second task description.
170
+ `);
171
+
172
+ const state = await deriveState(base);
173
+
174
+ assertEq(state.phase, 'executing', 'phase is executing');
175
+ assert(state.activeTask !== null, 'activeTask is not null');
176
+ assertEq(state.activeTask?.id, 'T01', 'activeTask id is T01');
177
+ assertEq(state.progress?.tasks?.done, 0, 'tasks done = 0');
178
+ assertEq(state.progress?.tasks?.total, 2, 'tasks total = 2');
179
+ } finally {
180
+ cleanup(base);
181
+ }
182
+ }
183
+
184
+ // ─── Test 5: executing + continue file → resume message ─────────────
185
+ console.log('\n=== executing + continue file → resume message ===');
186
+ {
187
+ const base = createFixtureBase();
188
+ try {
189
+ writeRoadmap(base, 'M001', `# M001: Test Milestone
190
+
191
+ **Vision:** Test interrupted resume.
192
+
193
+ ## Slices
194
+
195
+ - [ ] **S01: Test Slice** \`risk:low\` \`depends:[]\`
196
+ > After this: Slice is done.
197
+ `);
198
+
199
+ writePlan(base, 'M001', 'S01', `# S01: Test Slice
200
+
201
+ **Goal:** Test interrupted.
202
+ **Demo:** Tests pass.
203
+
204
+ ## Tasks
205
+
206
+ - [ ] **T01: First Task** \`est:10m\`
207
+ First task description.
208
+ `);
209
+
210
+ writeContinue(base, 'M001', 'S01', `---
211
+ milestone: M001
212
+ slice: S01
213
+ task: T01
214
+ step: 2
215
+ totalSteps: 5
216
+ status: interrupted
217
+ savedAt: 2026-03-10T10:00:00Z
218
+ ---
219
+
220
+ # Continue: T01
221
+
222
+ ## Completed Work
223
+ Steps 1 done.
224
+
225
+ ## Remaining Work
226
+ Steps 2-5.
227
+
228
+ ## Next Action
229
+ Continue from step 2.
230
+ `);
231
+
232
+ const state = await deriveState(base);
233
+
234
+ assertEq(state.phase, 'executing', 'interrupted: phase is executing');
235
+ assert(state.activeTask !== null, 'interrupted: activeTask is not null');
236
+ assertEq(state.activeTask?.id, 'T01', 'interrupted: activeTask id is T01');
237
+ assert(
238
+ state.nextAction.includes('Resume') || state.nextAction.includes('resume') || state.nextAction.includes('continue.md'),
239
+ 'interrupted: nextAction mentions Resume/resume/continue.md'
240
+ );
241
+ } finally {
242
+ cleanup(base);
243
+ }
244
+ }
245
+
246
+ // ─── Test 6: all tasks done, slice not [x] → summarizing ──────────────
247
+ console.log('\n=== all tasks done, slice not [x] → summarizing ===');
248
+ {
249
+ const base = createFixtureBase();
250
+ try {
251
+ writeRoadmap(base, 'M001', `# M001: Test Milestone
252
+
253
+ **Vision:** Test summarizing phase.
254
+
255
+ ## Slices
256
+
257
+ - [ ] **S01: Test Slice** \`risk:low\` \`depends:[]\`
258
+ > After this: Slice is done.
259
+ `);
260
+
261
+ writePlan(base, 'M001', 'S01', `# S01: Test Slice
262
+
263
+ **Goal:** Test summarizing.
264
+ **Demo:** Tests pass.
265
+
266
+ ## Tasks
267
+
268
+ - [x] **T01: First Done** \`est:10m\`
269
+ Already completed.
270
+
271
+ - [x] **T02: Second Done** \`est:10m\`
272
+ Also completed.
273
+ `);
274
+
275
+ const state = await deriveState(base);
276
+
277
+ assertEq(state.phase, 'summarizing', 'summarizing: phase is summarizing');
278
+ assert(state.activeSlice !== null, 'summarizing: activeSlice is not null');
279
+ assertEq(state.activeSlice?.id, 'S01', 'summarizing: activeSlice id is S01');
280
+ assertEq(state.activeTask, null, 'summarizing: activeTask is null');
281
+ assert(
282
+ state.nextAction.toLowerCase().includes('summary') || state.nextAction.toLowerCase().includes('complete'),
283
+ 'summarizing: nextAction mentions summary or complete'
284
+ );
285
+ assertEq(state.progress?.tasks?.done, 2, 'summarizing: tasks done = 2');
286
+ assertEq(state.progress?.tasks?.total, 2, 'summarizing: tasks total = 2');
287
+ } finally {
288
+ cleanup(base);
289
+ }
290
+ }
291
+
292
+ // ─── Test 7: all milestones complete → complete ────────────────────────
293
+ console.log('\n=== all milestones complete → complete ===');
294
+ {
295
+ const base = createFixtureBase();
296
+ try {
297
+ writeRoadmap(base, 'M001', `# M001: Test Milestone
298
+
299
+ **Vision:** Test complete phase.
300
+
301
+ ## Slices
302
+
303
+ - [x] **S01: Done Slice** \`risk:low\` \`depends:[]\`
304
+ > After this: Done.
305
+ `);
306
+
307
+ writeMilestoneSummary(base, 'M001', `# M001 Summary\n\nMilestone complete.`);
308
+
309
+ const state = await deriveState(base);
310
+
311
+ assertEq(state.phase, 'complete', 'complete: phase is complete');
312
+ assertEq(state.activeSlice, null, 'complete: activeSlice is null');
313
+ assertEq(state.activeTask, null, 'complete: activeTask is null');
314
+ assert(
315
+ state.nextAction.toLowerCase().includes('complete'),
316
+ 'complete: nextAction mentions complete'
317
+ );
318
+ assertEq(state.registry.length, 1, 'complete: registry has 1 entry');
319
+ assertEq(state.registry[0]?.status, 'complete', 'complete: registry[0] status is complete');
320
+ } finally {
321
+ cleanup(base);
322
+ }
323
+ }
324
+
325
+ // ─── Test 8: blocked dependencies ──────────────────────────────────────
326
+ console.log('\n=== blocked dependencies ===');
327
+ {
328
+ // Case A: S01 active (deps satisfied), S02 blocked on S01
329
+ const base1 = createFixtureBase();
330
+ try {
331
+ writeRoadmap(base1, 'M001', `# M001: Test Milestone
332
+
333
+ **Vision:** Test blocked deps.
334
+
335
+ ## Slices
336
+
337
+ - [ ] **S01: First** \`risk:low\` \`depends:[]\`
338
+ > After this: S01 done.
339
+
340
+ - [ ] **S02: Second** \`risk:low\` \`depends:[S01]\`
341
+ > After this: S02 done.
342
+ `);
343
+
344
+ // S01 has a plan with incomplete task — it's the active slice
345
+ writePlan(base1, 'M001', 'S01', `# S01: First
346
+
347
+ **Goal:** First slice.
348
+ **Demo:** Tests pass.
349
+
350
+ ## Tasks
351
+
352
+ - [ ] **T01: Incomplete** \`est:10m\`
353
+ Still working.
354
+ `);
355
+
356
+ const state1 = await deriveState(base1);
357
+
358
+ assertEq(state1.phase, 'executing', 'blocked-A: phase is executing (S01 active)');
359
+ assertEq(state1.activeSlice?.id, 'S01', 'blocked-A: activeSlice is S01');
360
+ } finally {
361
+ cleanup(base1);
362
+ }
363
+
364
+ // Case B: S01 depends on nonexistent S99 → truly blocked
365
+ const base2 = createFixtureBase();
366
+ try {
367
+ writeRoadmap(base2, 'M001', `# M001: Test Milestone
368
+
369
+ **Vision:** Test truly blocked.
370
+
371
+ ## Slices
372
+
373
+ - [ ] **S01: Blocked** \`risk:low\` \`depends:[S99]\`
374
+ > After this: Done.
375
+ `);
376
+
377
+ const state2 = await deriveState(base2);
378
+
379
+ assertEq(state2.phase, 'blocked', 'blocked-B: phase is blocked');
380
+ assertEq(state2.activeSlice, null, 'blocked-B: activeSlice is null');
381
+ assert(state2.blockers.length > 0, 'blocked-B: blockers array is non-empty');
382
+ } finally {
383
+ cleanup(base2);
384
+ }
385
+ }
386
+
387
+ // ─── Test 9: multi-milestone registry ──────────────────────────────────
388
+ console.log('\n=== multi-milestone registry ===');
389
+ {
390
+ const base = createFixtureBase();
391
+ try {
392
+ // M001: complete (all slices done)
393
+ writeRoadmap(base, 'M001', `# M001: First Milestone
394
+
395
+ **Vision:** Already done.
396
+
397
+ ## Slices
398
+
399
+ - [x] **S01: Done** \`risk:low\` \`depends:[]\`
400
+ > After this: Done.
401
+ `);
402
+
403
+ writeMilestoneSummary(base, 'M001', `# M001 Summary\n\nFirst milestone complete.`);
404
+
405
+ // M002: active (has incomplete slices)
406
+ writeRoadmap(base, 'M002', `# M002: Second Milestone
407
+
408
+ **Vision:** Currently active.
409
+
410
+ ## Slices
411
+
412
+ - [ ] **S01: In Progress** \`risk:low\` \`depends:[]\`
413
+ > After this: Done.
414
+ `);
415
+
416
+ // M003: just a dir (no roadmap → pending since M002 is already active)
417
+ mkdirSync(join(base, '.gsd', 'milestones', 'M003'), { recursive: true });
418
+
419
+ const state = await deriveState(base);
420
+
421
+ assertEq(state.registry.length, 3, 'multi-ms: registry has 3 entries');
422
+ assertEq(state.registry[0]?.id, 'M001', 'multi-ms: registry[0] is M001');
423
+ assertEq(state.registry[0]?.status, 'complete', 'multi-ms: M001 is complete');
424
+ assertEq(state.registry[1]?.id, 'M002', 'multi-ms: registry[1] is M002');
425
+ assertEq(state.registry[1]?.status, 'active', 'multi-ms: M002 is active');
426
+ assertEq(state.registry[2]?.id, 'M003', 'multi-ms: registry[2] is M003');
427
+ assertEq(state.registry[2]?.status, 'pending', 'multi-ms: M003 is pending');
428
+ assertEq(state.activeMilestone?.id, 'M002', 'multi-ms: activeMilestone is M002');
429
+ assertEq(state.progress?.milestones?.done, 1, 'multi-ms: milestones done = 1');
430
+ assertEq(state.progress?.milestones?.total, 3, 'multi-ms: milestones total = 3');
431
+ } finally {
432
+ cleanup(base);
433
+ }
434
+ }
435
+
436
+ // ─── Test 10: requirements integration ─────────────────────────────────
437
+ console.log('\n=== requirements integration ===');
438
+ {
439
+ const base = createFixtureBase();
440
+ try {
441
+ writeRequirements(base, `# Requirements
442
+
443
+ ## Active
444
+
445
+ ### R001 — First Active Requirement
446
+ - Status: active
447
+ - Description: Something active.
448
+
449
+ ### R002 — Second Active Requirement
450
+ - Status: active
451
+ - Description: Another active one.
452
+
453
+ ## Validated
454
+
455
+ ### R003 — Validated Requirement
456
+ - Status: validated
457
+ - Description: Already validated.
458
+
459
+ ## Deferred
460
+
461
+ ### R004 — Deferred Requirement
462
+ - Status: deferred
463
+ - Description: Pushed back.
464
+
465
+ ### R005 — Another Deferred
466
+ - Status: deferred
467
+ - Description: Also deferred.
468
+
469
+ ## Out of Scope
470
+
471
+ ### R006 — Out of Scope Requirement
472
+ - Status: out-of-scope
473
+ - Description: Not doing this.
474
+ `);
475
+
476
+ // Need at least an empty milestones dir for deriveState
477
+ const state = await deriveState(base);
478
+
479
+ assert(state.requirements !== undefined, 'requirements: requirements object exists');
480
+ assertEq(state.requirements?.active, 2, 'requirements: active = 2');
481
+ assertEq(state.requirements?.validated, 1, 'requirements: validated = 1');
482
+ assertEq(state.requirements?.deferred, 2, 'requirements: deferred = 2');
483
+ assertEq(state.requirements?.outOfScope, 1, 'requirements: outOfScope = 1');
484
+ assertEq(state.requirements?.total, 6, 'requirements: total = 6 (sum of all)');
485
+ } finally {
486
+ cleanup(base);
487
+ }
488
+ }
489
+
490
+ // ─── Test 11: all slices [x], no summary → completing-milestone ────────
491
+ console.log('\n=== all slices [x], no summary → completing-milestone ===');
492
+ {
493
+ const base = createFixtureBase();
494
+ try {
495
+ writeRoadmap(base, 'M001', `# M001: Test Milestone
496
+
497
+ **Vision:** Test completing-milestone phase.
498
+
499
+ ## Slices
500
+
501
+ - [x] **S01: First Done** \`risk:low\` \`depends:[]\`
502
+ > After this: S01 complete.
503
+
504
+ - [x] **S02: Second Done** \`risk:low\` \`depends:[S01]\`
505
+ > After this: S02 complete.
506
+ `);
507
+
508
+ const state = await deriveState(base);
509
+
510
+ assertEq(state.phase, 'completing-milestone', 'completing-ms: phase is completing-milestone');
511
+ assert(state.activeMilestone !== null, 'completing-ms: activeMilestone is not null');
512
+ assertEq(state.activeMilestone?.id, 'M001', 'completing-ms: activeMilestone id is M001');
513
+ assertEq(state.activeSlice, null, 'completing-ms: activeSlice is null');
514
+ assertEq(state.activeTask, null, 'completing-ms: activeTask is null');
515
+ assertEq(state.registry.length, 1, 'completing-ms: registry has 1 entry');
516
+ assertEq(state.registry[0]?.status, 'active', 'completing-ms: registry[0] status is active (not complete)');
517
+ assertEq(state.progress?.slices?.done, 2, 'completing-ms: slices done = 2');
518
+ assertEq(state.progress?.slices?.total, 2, 'completing-ms: slices total = 2');
519
+ assert(
520
+ state.nextAction.toLowerCase().includes('summary') || state.nextAction.toLowerCase().includes('complete'),
521
+ 'completing-ms: nextAction mentions summary or complete'
522
+ );
523
+ } finally {
524
+ cleanup(base);
525
+ }
526
+ }
527
+
528
+ // ─── Test 12: all slices [x], summary exists → complete ───────────────
529
+ console.log('\n=== all slices [x], summary exists → complete ===');
530
+ {
531
+ const base = createFixtureBase();
532
+ try {
533
+ writeRoadmap(base, 'M001', `# M001: Test Milestone
534
+
535
+ **Vision:** Test that summary presence means complete.
536
+
537
+ ## Slices
538
+
539
+ - [x] **S01: Done** \`risk:low\` \`depends:[]\`
540
+ > After this: Done.
541
+ `);
542
+
543
+ writeMilestoneSummary(base, 'M001', `# M001 Summary\n\nMilestone is complete.`);
544
+
545
+ const state = await deriveState(base);
546
+
547
+ assertEq(state.phase, 'complete', 'summary-exists: phase is complete');
548
+ assertEq(state.registry.length, 1, 'summary-exists: registry has 1 entry');
549
+ assertEq(state.registry[0]?.status, 'complete', 'summary-exists: registry[0] status is complete');
550
+ assertEq(state.activeSlice, null, 'summary-exists: activeSlice is null');
551
+ assertEq(state.activeTask, null, 'summary-exists: activeTask is null');
552
+ } finally {
553
+ cleanup(base);
554
+ }
555
+ }
556
+
557
+ // ─── Test 13: multi-milestone completing-milestone ─────────────────────
558
+ console.log('\n=== multi-milestone completing-milestone ===');
559
+ {
560
+ const base = createFixtureBase();
561
+ try {
562
+ // M001: all slices done + summary exists → complete
563
+ writeRoadmap(base, 'M001', `# M001: First Milestone
564
+
565
+ **Vision:** Already complete with summary.
566
+
567
+ ## Slices
568
+
569
+ - [x] **S01: Done** \`risk:low\` \`depends:[]\`
570
+ > After this: Done.
571
+ `);
572
+ writeMilestoneSummary(base, 'M001', `# M001 Summary\n\nFirst milestone complete.`);
573
+
574
+ // M002: all slices done, no summary → completing-milestone
575
+ writeRoadmap(base, 'M002', `# M002: Second Milestone
576
+
577
+ **Vision:** All slices done but no summary.
578
+
579
+ ## Slices
580
+
581
+ - [x] **S01: Done** \`risk:low\` \`depends:[]\`
582
+ > After this: Done.
583
+
584
+ - [x] **S02: Also Done** \`risk:low\` \`depends:[S01]\`
585
+ > After this: Done.
586
+ `);
587
+
588
+ // M003: has incomplete slices → pending (M002 is active)
589
+ writeRoadmap(base, 'M003', `# M003: Third Milestone
590
+
591
+ **Vision:** Not yet started.
592
+
593
+ ## Slices
594
+
595
+ - [ ] **S01: Not Started** \`risk:low\` \`depends:[]\`
596
+ > After this: Done.
597
+ `);
598
+
599
+ const state = await deriveState(base);
600
+
601
+ assertEq(state.phase, 'completing-milestone', 'multi-completing: phase is completing-milestone');
602
+ assertEq(state.activeMilestone?.id, 'M002', 'multi-completing: activeMilestone is M002');
603
+ assertEq(state.activeSlice, null, 'multi-completing: activeSlice is null');
604
+ assertEq(state.activeTask, null, 'multi-completing: activeTask is null');
605
+ assertEq(state.registry.length, 3, 'multi-completing: registry has 3 entries');
606
+ assertEq(state.registry[0]?.id, 'M001', 'multi-completing: registry[0] is M001');
607
+ assertEq(state.registry[0]?.status, 'complete', 'multi-completing: M001 is complete');
608
+ assertEq(state.registry[1]?.id, 'M002', 'multi-completing: registry[1] is M002');
609
+ assertEq(state.registry[1]?.status, 'active', 'multi-completing: M002 is active (completing-milestone)');
610
+ assertEq(state.registry[2]?.id, 'M003', 'multi-completing: registry[2] is M003');
611
+ assertEq(state.registry[2]?.status, 'pending', 'multi-completing: M003 is pending');
612
+ assertEq(state.progress?.milestones?.done, 1, 'multi-completing: milestones done = 1');
613
+ assertEq(state.progress?.milestones?.total, 3, 'multi-completing: milestones total = 3');
614
+ assertEq(state.progress?.slices?.done, 2, 'multi-completing: slices done = 2');
615
+ assertEq(state.progress?.slices?.total, 2, 'multi-completing: slices total = 2');
616
+ } finally {
617
+ cleanup(base);
618
+ }
619
+ }
620
+
621
+ // ═══ Milestone with summary but no roadmap → complete ═══════════════════
622
+ {
623
+ console.log('\n=== milestone with summary and no roadmap → complete ===');
624
+ const base = createFixtureBase();
625
+ try {
626
+ // M001, M002: completed milestones with summaries but no roadmaps
627
+ const m1dir = join(base, '.gsd', 'milestones', 'M001');
628
+ mkdirSync(m1dir, { recursive: true });
629
+ writeFileSync(join(m1dir, 'M001-SUMMARY.md'), '---\nid: M001\n---\n# Bootstrap\nDone.');
630
+
631
+ const m2dir = join(base, '.gsd', 'milestones', 'M002');
632
+ mkdirSync(m2dir, { recursive: true });
633
+ writeFileSync(join(m2dir, 'M002-SUMMARY.md'), '---\nid: M002\n---\n# Core Features\nDone.');
634
+
635
+ // M003: active milestone with a roadmap
636
+ writeRoadmap(base, 'M003', '# M003: Polish\n## Slices\n- [ ] **S01: Cleanup**');
637
+
638
+ const state = await deriveState(base);
639
+
640
+ assertEq(state.phase, 'planning', 'summary-no-roadmap: phase is planning (active is M003)');
641
+ assertEq(state.activeMilestone?.id, 'M003', 'summary-no-roadmap: active milestone is M003');
642
+ assertEq(state.activeMilestone?.title, 'Polish', 'summary-no-roadmap: active title is Polish');
643
+ assertEq(state.registry.length, 3, 'summary-no-roadmap: registry has 3 entries');
644
+ assertEq(state.registry[0]?.status, 'complete', 'summary-no-roadmap: M001 is complete');
645
+ assertEq(state.registry[0]?.title, 'Bootstrap', 'summary-no-roadmap: M001 title from summary');
646
+ assertEq(state.registry[1]?.status, 'complete', 'summary-no-roadmap: M002 is complete');
647
+ assertEq(state.registry[1]?.title, 'Core Features', 'summary-no-roadmap: M002 title from summary');
648
+ assertEq(state.registry[2]?.status, 'active', 'summary-no-roadmap: M003 is active');
649
+ assertEq(state.progress?.milestones?.done, 2, 'summary-no-roadmap: milestones done = 2');
650
+ assertEq(state.progress?.milestones?.total, 3, 'summary-no-roadmap: milestones total = 3');
651
+ } finally {
652
+ cleanup(base);
653
+ }
654
+ }
655
+
656
+ // ═══ All milestones have summary but no roadmap → complete ═════════════
657
+ {
658
+ console.log('\n=== all milestones summary-only → complete ===');
659
+ const base = createFixtureBase();
660
+ try {
661
+ const m1dir = join(base, '.gsd', 'milestones', 'M001');
662
+ mkdirSync(m1dir, { recursive: true });
663
+ writeFileSync(join(m1dir, 'M001-SUMMARY.md'), '---\ntitle: Done\n---\nAll done.');
664
+
665
+ const state = await deriveState(base);
666
+ assertEq(state.phase, 'complete', 'all-summary-only: phase is complete');
667
+ assertEq(state.registry[0]?.status, 'complete', 'all-summary-only: M001 is complete');
668
+ } finally {
669
+ cleanup(base);
670
+ }
671
+ }
672
+
673
+ // ═════════════════════════════════════════════════════════════════════════
674
+ // Results
675
+ // ═════════════════════════════════════════════════════════════════════════
676
+
677
+ console.log(`\n${'='.repeat(40)}`);
678
+ console.log(`Results: ${passed} passed, ${failed} failed`);
679
+ if (failed > 0) {
680
+ process.exit(1);
681
+ } else {
682
+ console.log('All tests passed ✓');
683
+ }
684
+ }
685
+
686
+ main().catch((error) => {
687
+ console.error(error);
688
+ process.exit(1);
689
+ });