@kata-sh/cli 0.1.0 → 0.1.1

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 (199) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -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 +56 -0
  7. package/dist/loader.d.ts +2 -0
  8. package/dist/loader.js +95 -0
  9. package/dist/resource-loader.d.ts +18 -0
  10. package/dist/resource-loader.js +50 -0
  11. package/dist/wizard.d.ts +15 -0
  12. package/dist/wizard.js +159 -0
  13. package/package.json +50 -21
  14. package/pkg/dist/modes/interactive/theme/dark.json +85 -0
  15. package/pkg/dist/modes/interactive/theme/light.json +84 -0
  16. package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
  17. package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
  18. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  19. package/pkg/dist/modes/interactive/theme/theme.js +949 -0
  20. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
  21. package/pkg/package.json +8 -0
  22. package/scripts/postinstall.js +45 -0
  23. package/src/resources/AGENTS.md +108 -0
  24. package/src/resources/KATA-WORKFLOW.md +661 -0
  25. package/src/resources/agents/researcher.md +29 -0
  26. package/src/resources/agents/scout.md +56 -0
  27. package/src/resources/agents/worker.md +31 -0
  28. package/src/resources/extensions/ask-user-questions.ts +200 -0
  29. package/src/resources/extensions/bg-shell/index.ts +2758 -0
  30. package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
  31. package/src/resources/extensions/browser-tools/core.js +1057 -0
  32. package/src/resources/extensions/browser-tools/index.ts +4916 -0
  33. package/src/resources/extensions/browser-tools/package.json +20 -0
  34. package/src/resources/extensions/context7/index.ts +428 -0
  35. package/src/resources/extensions/context7/package.json +11 -0
  36. package/src/resources/extensions/get-secrets-from-user.ts +352 -0
  37. package/src/resources/extensions/github/formatters.ts +207 -0
  38. package/src/resources/extensions/github/gh-api.ts +537 -0
  39. package/src/resources/extensions/github/index.ts +778 -0
  40. package/src/resources/extensions/kata/activity-log.ts +88 -0
  41. package/src/resources/extensions/kata/auto.ts +2786 -0
  42. package/src/resources/extensions/kata/commands.ts +355 -0
  43. package/src/resources/extensions/kata/crash-recovery.ts +85 -0
  44. package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
  45. package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
  46. package/src/resources/extensions/kata/doctor.ts +683 -0
  47. package/src/resources/extensions/kata/files.ts +730 -0
  48. package/src/resources/extensions/kata/gitignore.ts +165 -0
  49. package/src/resources/extensions/kata/guided-flow.ts +976 -0
  50. package/src/resources/extensions/kata/index.ts +556 -0
  51. package/src/resources/extensions/kata/metrics.ts +397 -0
  52. package/src/resources/extensions/kata/observability-validator.ts +408 -0
  53. package/src/resources/extensions/kata/package.json +11 -0
  54. package/src/resources/extensions/kata/paths.ts +346 -0
  55. package/src/resources/extensions/kata/preferences.ts +695 -0
  56. package/src/resources/extensions/kata/prompt-loader.ts +50 -0
  57. package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
  58. package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
  59. package/src/resources/extensions/kata/prompts/discuss.md +151 -0
  60. package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
  61. package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
  62. package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
  63. package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
  64. package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
  65. package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
  66. package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
  67. package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
  68. package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
  69. package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
  70. package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
  71. package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
  72. package/src/resources/extensions/kata/prompts/queue.md +85 -0
  73. package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
  74. package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
  75. package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
  76. package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
  77. package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
  78. package/src/resources/extensions/kata/prompts/system.md +341 -0
  79. package/src/resources/extensions/kata/session-forensics.ts +550 -0
  80. package/src/resources/extensions/kata/skill-discovery.ts +137 -0
  81. package/src/resources/extensions/kata/state.ts +509 -0
  82. package/src/resources/extensions/kata/templates/context.md +76 -0
  83. package/src/resources/extensions/kata/templates/decisions.md +8 -0
  84. package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
  85. package/src/resources/extensions/kata/templates/plan.md +133 -0
  86. package/src/resources/extensions/kata/templates/preferences.md +15 -0
  87. package/src/resources/extensions/kata/templates/project.md +31 -0
  88. package/src/resources/extensions/kata/templates/reassessment.md +28 -0
  89. package/src/resources/extensions/kata/templates/requirements.md +81 -0
  90. package/src/resources/extensions/kata/templates/research.md +46 -0
  91. package/src/resources/extensions/kata/templates/roadmap.md +118 -0
  92. package/src/resources/extensions/kata/templates/slice-context.md +58 -0
  93. package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
  94. package/src/resources/extensions/kata/templates/state.md +19 -0
  95. package/src/resources/extensions/kata/templates/task-plan.md +52 -0
  96. package/src/resources/extensions/kata/templates/task-summary.md +57 -0
  97. package/src/resources/extensions/kata/templates/uat.md +54 -0
  98. package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
  99. package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
  100. package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
  101. package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
  102. package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
  103. package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
  104. package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
  105. package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
  106. package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
  107. package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
  108. package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
  109. package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
  110. package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
  111. package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
  112. package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
  113. package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
  114. package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
  115. package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
  116. package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
  117. package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
  118. package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
  119. package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
  120. package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
  121. package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
  122. package/src/resources/extensions/kata/types.ts +159 -0
  123. package/src/resources/extensions/kata/unit-runtime.ts +163 -0
  124. package/src/resources/extensions/kata/workspace-index.ts +203 -0
  125. package/src/resources/extensions/kata/worktree.ts +182 -0
  126. package/src/resources/extensions/mac-tools/index.ts +852 -0
  127. package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
  128. package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
  129. package/src/resources/extensions/search-the-web/cache.ts +78 -0
  130. package/src/resources/extensions/search-the-web/format.ts +258 -0
  131. package/src/resources/extensions/search-the-web/http.ts +238 -0
  132. package/src/resources/extensions/search-the-web/index.ts +68 -0
  133. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
  134. package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
  135. package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
  136. package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
  137. package/src/resources/extensions/shared/confirm-ui.ts +126 -0
  138. package/src/resources/extensions/shared/interview-ui.ts +822 -0
  139. package/src/resources/extensions/shared/next-action-ui.ts +235 -0
  140. package/src/resources/extensions/shared/progress-widget.ts +282 -0
  141. package/src/resources/extensions/shared/thinking-widget.ts +107 -0
  142. package/src/resources/extensions/shared/ui.ts +400 -0
  143. package/src/resources/extensions/shared/wizard-ui.ts +551 -0
  144. package/src/resources/extensions/slash-commands/audit.ts +92 -0
  145. package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
  146. package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
  147. package/src/resources/extensions/slash-commands/index.ts +12 -0
  148. package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
  149. package/src/resources/extensions/subagent/agents.ts +126 -0
  150. package/src/resources/extensions/subagent/index.ts +1293 -0
  151. package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
  152. package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
  153. package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
  154. package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
  155. package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
  156. package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
  157. package/src/resources/skills/frontend-design/SKILL.md +45 -0
  158. package/src/resources/skills/swiftui/SKILL.md +208 -0
  159. package/src/resources/skills/swiftui/references/animations.md +921 -0
  160. package/src/resources/skills/swiftui/references/architecture.md +1561 -0
  161. package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
  162. package/src/resources/skills/swiftui/references/navigation.md +1492 -0
  163. package/src/resources/skills/swiftui/references/networking-async.md +214 -0
  164. package/src/resources/skills/swiftui/references/performance.md +1706 -0
  165. package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
  166. package/src/resources/skills/swiftui/references/state-management.md +1443 -0
  167. package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
  168. package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
  169. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
  170. package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
  171. package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
  172. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
  173. package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
  174. package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
  175. package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
  176. package/dist/commands/task.d.ts +0 -9
  177. package/dist/commands/task.d.ts.map +0 -1
  178. package/dist/commands/task.js +0 -129
  179. package/dist/commands/task.js.map +0 -1
  180. package/dist/commands/task.test.d.ts +0 -2
  181. package/dist/commands/task.test.d.ts.map +0 -1
  182. package/dist/commands/task.test.js +0 -169
  183. package/dist/commands/task.test.js.map +0 -1
  184. package/dist/e2e/task-e2e.test.d.ts +0 -2
  185. package/dist/e2e/task-e2e.test.d.ts.map +0 -1
  186. package/dist/e2e/task-e2e.test.js +0 -173
  187. package/dist/e2e/task-e2e.test.js.map +0 -1
  188. package/dist/index.d.ts +0 -3
  189. package/dist/index.d.ts.map +0 -1
  190. package/dist/index.js +0 -93
  191. package/dist/index.js.map +0 -1
  192. package/dist/slug.d.ts +0 -2
  193. package/dist/slug.d.ts.map +0 -1
  194. package/dist/slug.js +0 -12
  195. package/dist/slug.js.map +0 -1
  196. package/dist/slug.test.d.ts +0 -2
  197. package/dist/slug.test.d.ts.map +0 -1
  198. package/dist/slug.test.js +0 -32
  199. package/dist/slug.test.js.map +0 -1
@@ -0,0 +1,1013 @@
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(
25
+ ` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
26
+ );
27
+ }
28
+ }
29
+
30
+ // ─── Fixture Helpers ───────────────────────────────────────────────────────
31
+
32
+ function createFixtureBase(): string {
33
+ const base = mkdtempSync(join(tmpdir(), "kata-state-test-"));
34
+ mkdirSync(join(base, ".kata", "milestones"), { recursive: true });
35
+ return base;
36
+ }
37
+
38
+ function writeRoadmap(base: string, mid: string, content: string): void {
39
+ const dir = join(base, ".kata", "milestones", mid);
40
+ mkdirSync(dir, { recursive: true });
41
+ writeFileSync(join(dir, `${mid}-ROADMAP.md`), content);
42
+ }
43
+
44
+ function writePlan(
45
+ base: string,
46
+ mid: string,
47
+ sid: string,
48
+ content: string,
49
+ ): void {
50
+ const dir = join(base, ".kata", "milestones", mid, "slices", sid);
51
+ mkdirSync(join(dir, "tasks"), { recursive: true });
52
+ writeFileSync(join(dir, `${sid}-PLAN.md`), content);
53
+ }
54
+
55
+ function writeContinue(
56
+ base: string,
57
+ mid: string,
58
+ sid: string,
59
+ content: string,
60
+ ): void {
61
+ const dir = join(base, ".kata", "milestones", mid, "slices", sid);
62
+ mkdirSync(dir, { recursive: true });
63
+ writeFileSync(join(dir, `${sid}-CONTINUE.md`), content);
64
+ }
65
+
66
+ function writeMilestoneSummary(
67
+ base: string,
68
+ mid: string,
69
+ content: string,
70
+ ): void {
71
+ const dir = join(base, ".kata", "milestones", mid);
72
+ mkdirSync(dir, { recursive: true });
73
+ writeFileSync(join(dir, `${mid}-SUMMARY.md`), content);
74
+ }
75
+
76
+ function writeRequirements(base: string, content: string): void {
77
+ writeFileSync(join(base, ".kata", "REQUIREMENTS.md"), content);
78
+ }
79
+
80
+ function cleanup(base: string): void {
81
+ rmSync(base, { recursive: true, force: true });
82
+ }
83
+
84
+ // ═══════════════════════════════════════════════════════════════════════════
85
+ // Test Groups
86
+ // ═══════════════════════════════════════════════════════════════════════════
87
+
88
+ async function main(): Promise<void> {
89
+ // ─── Test 1: empty milestones dir → pre-planning ───────────────────────
90
+ console.log("\n=== empty milestones dir → pre-planning ===");
91
+ {
92
+ const base = createFixtureBase();
93
+ try {
94
+ const state = await deriveState(base);
95
+
96
+ assertEq(state.phase, "pre-planning", "phase is pre-planning");
97
+ assertEq(state.activeMilestone, null, "activeMilestone is null");
98
+ assertEq(state.activeSlice, null, "activeSlice is null");
99
+ assertEq(state.activeTask, null, "activeTask is null");
100
+ assertEq(state.registry, [], "registry is empty");
101
+ assertEq(state.progress?.milestones?.done, 0, "milestones done = 0");
102
+ assertEq(state.progress?.milestones?.total, 0, "milestones total = 0");
103
+ } finally {
104
+ cleanup(base);
105
+ }
106
+ }
107
+
108
+ // ─── Test 2: milestone dir exists but no roadmap → pre-planning ────────
109
+ console.log("\n=== milestone dir exists but no roadmap → pre-planning ===");
110
+ {
111
+ const base = createFixtureBase();
112
+ try {
113
+ // Create M001 directory but no roadmap file
114
+ mkdirSync(join(base, ".kata", "milestones", "M001"), { recursive: true });
115
+
116
+ const state = await deriveState(base);
117
+
118
+ assertEq(state.phase, "pre-planning", "phase is pre-planning");
119
+ assert(state.activeMilestone !== null, "activeMilestone is not null");
120
+ assertEq(state.activeMilestone?.id, "M001", "activeMilestone id is M001");
121
+ assertEq(state.activeSlice, null, "activeSlice is null");
122
+ assertEq(state.activeTask, null, "activeTask is null");
123
+ assertEq(state.registry.length, 1, "registry has 1 entry");
124
+ assertEq(
125
+ state.registry[0]?.status,
126
+ "active",
127
+ "registry entry status is active",
128
+ );
129
+ } finally {
130
+ cleanup(base);
131
+ }
132
+ }
133
+
134
+ // ─── Test 3: roadmap with incomplete slice, no plan → planning ─────────
135
+ console.log("\n=== roadmap with incomplete slice, no plan → planning ===");
136
+ {
137
+ const base = createFixtureBase();
138
+ try {
139
+ writeRoadmap(
140
+ base,
141
+ "M001",
142
+ `# M001: Test Milestone
143
+
144
+ **Vision:** Test planning phase.
145
+
146
+ ## Slices
147
+
148
+ - [ ] **S01: Test Slice** \`risk:low\` \`depends:[]\`
149
+ > After this: Slice is done.
150
+ `,
151
+ );
152
+
153
+ const state = await deriveState(base);
154
+
155
+ assertEq(state.phase, "planning", "phase is planning");
156
+ assert(state.activeSlice !== null, "activeSlice is not null");
157
+ assertEq(state.activeSlice?.id, "S01", "activeSlice id is S01");
158
+ assertEq(state.activeTask, null, "activeTask is null");
159
+ assertEq(state.progress?.slices?.done, 0, "slices done = 0");
160
+ assertEq(state.progress?.slices?.total, 1, "slices total = 1");
161
+ } finally {
162
+ cleanup(base);
163
+ }
164
+ }
165
+
166
+ // ─── Test 4: roadmap + plan with incomplete tasks → executing ──────────
167
+ console.log("\n=== roadmap + plan with incomplete tasks → executing ===");
168
+ {
169
+ const base = createFixtureBase();
170
+ try {
171
+ writeRoadmap(
172
+ base,
173
+ "M001",
174
+ `# M001: Test Milestone
175
+
176
+ **Vision:** Test executing phase.
177
+
178
+ ## Slices
179
+
180
+ - [ ] **S01: Test Slice** \`risk:low\` \`depends:[]\`
181
+ > After this: Slice is done.
182
+ `,
183
+ );
184
+
185
+ writePlan(
186
+ base,
187
+ "M001",
188
+ "S01",
189
+ `# S01: Test Slice
190
+
191
+ **Goal:** Test executing.
192
+ **Demo:** Tests pass.
193
+
194
+ ## Tasks
195
+
196
+ - [ ] **T01: First** \`est:10m\`
197
+ First task description.
198
+
199
+ - [ ] **T02: Second** \`est:10m\`
200
+ Second task description.
201
+ `,
202
+ );
203
+
204
+ const state = await deriveState(base);
205
+
206
+ assertEq(state.phase, "executing", "phase is executing");
207
+ assert(state.activeTask !== null, "activeTask is not null");
208
+ assertEq(state.activeTask?.id, "T01", "activeTask id is T01");
209
+ assertEq(state.progress?.tasks?.done, 0, "tasks done = 0");
210
+ assertEq(state.progress?.tasks?.total, 2, "tasks total = 2");
211
+ } finally {
212
+ cleanup(base);
213
+ }
214
+ }
215
+
216
+ // ─── Test 5: executing + continue file → resume message ─────────────
217
+ console.log("\n=== executing + continue file → resume message ===");
218
+ {
219
+ const base = createFixtureBase();
220
+ try {
221
+ writeRoadmap(
222
+ base,
223
+ "M001",
224
+ `# M001: Test Milestone
225
+
226
+ **Vision:** Test interrupted resume.
227
+
228
+ ## Slices
229
+
230
+ - [ ] **S01: Test Slice** \`risk:low\` \`depends:[]\`
231
+ > After this: Slice is done.
232
+ `,
233
+ );
234
+
235
+ writePlan(
236
+ base,
237
+ "M001",
238
+ "S01",
239
+ `# S01: Test Slice
240
+
241
+ **Goal:** Test interrupted.
242
+ **Demo:** Tests pass.
243
+
244
+ ## Tasks
245
+
246
+ - [ ] **T01: First Task** \`est:10m\`
247
+ First task description.
248
+ `,
249
+ );
250
+
251
+ writeContinue(
252
+ base,
253
+ "M001",
254
+ "S01",
255
+ `---
256
+ milestone: M001
257
+ slice: S01
258
+ task: T01
259
+ step: 2
260
+ totalSteps: 5
261
+ status: interrupted
262
+ savedAt: 2026-03-10T10:00:00Z
263
+ ---
264
+
265
+ # Continue: T01
266
+
267
+ ## Completed Work
268
+ Steps 1 done.
269
+
270
+ ## Remaining Work
271
+ Steps 2-5.
272
+
273
+ ## Next Action
274
+ Continue from step 2.
275
+ `,
276
+ );
277
+
278
+ const state = await deriveState(base);
279
+
280
+ assertEq(state.phase, "executing", "interrupted: phase is executing");
281
+ assert(state.activeTask !== null, "interrupted: activeTask is not null");
282
+ assertEq(
283
+ state.activeTask?.id,
284
+ "T01",
285
+ "interrupted: activeTask id is T01",
286
+ );
287
+ assert(
288
+ state.nextAction.includes("Resume") ||
289
+ state.nextAction.includes("resume") ||
290
+ state.nextAction.includes("continue.md"),
291
+ "interrupted: nextAction mentions Resume/resume/continue.md",
292
+ );
293
+ } finally {
294
+ cleanup(base);
295
+ }
296
+ }
297
+
298
+ // ─── Test 6: all tasks done, slice not [x] → summarizing ──────────────
299
+ console.log("\n=== all tasks done, slice not [x] → summarizing ===");
300
+ {
301
+ const base = createFixtureBase();
302
+ try {
303
+ writeRoadmap(
304
+ base,
305
+ "M001",
306
+ `# M001: Test Milestone
307
+
308
+ **Vision:** Test summarizing phase.
309
+
310
+ ## Slices
311
+
312
+ - [ ] **S01: Test Slice** \`risk:low\` \`depends:[]\`
313
+ > After this: Slice is done.
314
+ `,
315
+ );
316
+
317
+ writePlan(
318
+ base,
319
+ "M001",
320
+ "S01",
321
+ `# S01: Test Slice
322
+
323
+ **Goal:** Test summarizing.
324
+ **Demo:** Tests pass.
325
+
326
+ ## Tasks
327
+
328
+ - [x] **T01: First Done** \`est:10m\`
329
+ Already completed.
330
+
331
+ - [x] **T02: Second Done** \`est:10m\`
332
+ Also completed.
333
+ `,
334
+ );
335
+
336
+ const state = await deriveState(base);
337
+
338
+ assertEq(state.phase, "summarizing", "summarizing: phase is summarizing");
339
+ assert(
340
+ state.activeSlice !== null,
341
+ "summarizing: activeSlice is not null",
342
+ );
343
+ assertEq(
344
+ state.activeSlice?.id,
345
+ "S01",
346
+ "summarizing: activeSlice id is S01",
347
+ );
348
+ assertEq(state.activeTask, null, "summarizing: activeTask is null");
349
+ assert(
350
+ state.nextAction.toLowerCase().includes("summary") ||
351
+ state.nextAction.toLowerCase().includes("complete"),
352
+ "summarizing: nextAction mentions summary or complete",
353
+ );
354
+ assertEq(state.progress?.tasks?.done, 2, "summarizing: tasks done = 2");
355
+ assertEq(state.progress?.tasks?.total, 2, "summarizing: tasks total = 2");
356
+ } finally {
357
+ cleanup(base);
358
+ }
359
+ }
360
+
361
+ // ─── Test 7: all milestones complete → complete ────────────────────────
362
+ console.log("\n=== all milestones complete → complete ===");
363
+ {
364
+ const base = createFixtureBase();
365
+ try {
366
+ writeRoadmap(
367
+ base,
368
+ "M001",
369
+ `# M001: Test Milestone
370
+
371
+ **Vision:** Test complete phase.
372
+
373
+ ## Slices
374
+
375
+ - [x] **S01: Done Slice** \`risk:low\` \`depends:[]\`
376
+ > After this: Done.
377
+ `,
378
+ );
379
+
380
+ writeMilestoneSummary(
381
+ base,
382
+ "M001",
383
+ `# M001 Summary\n\nMilestone complete.`,
384
+ );
385
+
386
+ const state = await deriveState(base);
387
+
388
+ assertEq(state.phase, "complete", "complete: phase is complete");
389
+ assertEq(state.activeSlice, null, "complete: activeSlice is null");
390
+ assertEq(state.activeTask, null, "complete: activeTask is null");
391
+ assert(
392
+ state.nextAction.toLowerCase().includes("complete"),
393
+ "complete: nextAction mentions complete",
394
+ );
395
+ assertEq(state.registry.length, 1, "complete: registry has 1 entry");
396
+ assertEq(
397
+ state.registry[0]?.status,
398
+ "complete",
399
+ "complete: registry[0] status is complete",
400
+ );
401
+ } finally {
402
+ cleanup(base);
403
+ }
404
+ }
405
+
406
+ // ─── Test 8: blocked dependencies ──────────────────────────────────────
407
+ console.log("\n=== blocked dependencies ===");
408
+ {
409
+ // Case A: S01 active (deps satisfied), S02 blocked on S01
410
+ const base1 = createFixtureBase();
411
+ try {
412
+ writeRoadmap(
413
+ base1,
414
+ "M001",
415
+ `# M001: Test Milestone
416
+
417
+ **Vision:** Test blocked deps.
418
+
419
+ ## Slices
420
+
421
+ - [ ] **S01: First** \`risk:low\` \`depends:[]\`
422
+ > After this: S01 done.
423
+
424
+ - [ ] **S02: Second** \`risk:low\` \`depends:[S01]\`
425
+ > After this: S02 done.
426
+ `,
427
+ );
428
+
429
+ // S01 has a plan with incomplete task — it's the active slice
430
+ writePlan(
431
+ base1,
432
+ "M001",
433
+ "S01",
434
+ `# S01: First
435
+
436
+ **Goal:** First slice.
437
+ **Demo:** Tests pass.
438
+
439
+ ## Tasks
440
+
441
+ - [ ] **T01: Incomplete** \`est:10m\`
442
+ Still working.
443
+ `,
444
+ );
445
+
446
+ const state1 = await deriveState(base1);
447
+
448
+ assertEq(
449
+ state1.phase,
450
+ "executing",
451
+ "blocked-A: phase is executing (S01 active)",
452
+ );
453
+ assertEq(state1.activeSlice?.id, "S01", "blocked-A: activeSlice is S01");
454
+ } finally {
455
+ cleanup(base1);
456
+ }
457
+
458
+ // Case B: S01 depends on nonexistent S99 → truly blocked
459
+ const base2 = createFixtureBase();
460
+ try {
461
+ writeRoadmap(
462
+ base2,
463
+ "M001",
464
+ `# M001: Test Milestone
465
+
466
+ **Vision:** Test truly blocked.
467
+
468
+ ## Slices
469
+
470
+ - [ ] **S01: Blocked** \`risk:low\` \`depends:[S99]\`
471
+ > After this: Done.
472
+ `,
473
+ );
474
+
475
+ const state2 = await deriveState(base2);
476
+
477
+ assertEq(state2.phase, "blocked", "blocked-B: phase is blocked");
478
+ assertEq(state2.activeSlice, null, "blocked-B: activeSlice is null");
479
+ assert(
480
+ state2.blockers.length > 0,
481
+ "blocked-B: blockers array is non-empty",
482
+ );
483
+ } finally {
484
+ cleanup(base2);
485
+ }
486
+ }
487
+
488
+ // ─── Test 9: multi-milestone registry ──────────────────────────────────
489
+ console.log("\n=== multi-milestone registry ===");
490
+ {
491
+ const base = createFixtureBase();
492
+ try {
493
+ // M001: complete (all slices done)
494
+ writeRoadmap(
495
+ base,
496
+ "M001",
497
+ `# M001: First Milestone
498
+
499
+ **Vision:** Already done.
500
+
501
+ ## Slices
502
+
503
+ - [x] **S01: Done** \`risk:low\` \`depends:[]\`
504
+ > After this: Done.
505
+ `,
506
+ );
507
+
508
+ writeMilestoneSummary(
509
+ base,
510
+ "M001",
511
+ `# M001 Summary\n\nFirst milestone complete.`,
512
+ );
513
+
514
+ // M002: active (has incomplete slices)
515
+ writeRoadmap(
516
+ base,
517
+ "M002",
518
+ `# M002: Second Milestone
519
+
520
+ **Vision:** Currently active.
521
+
522
+ ## Slices
523
+
524
+ - [ ] **S01: In Progress** \`risk:low\` \`depends:[]\`
525
+ > After this: Done.
526
+ `,
527
+ );
528
+
529
+ // M003: just a dir (no roadmap → pending since M002 is already active)
530
+ mkdirSync(join(base, ".kata", "milestones", "M003"), { recursive: true });
531
+
532
+ const state = await deriveState(base);
533
+
534
+ assertEq(state.registry.length, 3, "multi-ms: registry has 3 entries");
535
+ assertEq(state.registry[0]?.id, "M001", "multi-ms: registry[0] is M001");
536
+ assertEq(
537
+ state.registry[0]?.status,
538
+ "complete",
539
+ "multi-ms: M001 is complete",
540
+ );
541
+ assertEq(state.registry[1]?.id, "M002", "multi-ms: registry[1] is M002");
542
+ assertEq(state.registry[1]?.status, "active", "multi-ms: M002 is active");
543
+ assertEq(state.registry[2]?.id, "M003", "multi-ms: registry[2] is M003");
544
+ assertEq(
545
+ state.registry[2]?.status,
546
+ "pending",
547
+ "multi-ms: M003 is pending",
548
+ );
549
+ assertEq(
550
+ state.activeMilestone?.id,
551
+ "M002",
552
+ "multi-ms: activeMilestone is M002",
553
+ );
554
+ assertEq(
555
+ state.progress?.milestones?.done,
556
+ 1,
557
+ "multi-ms: milestones done = 1",
558
+ );
559
+ assertEq(
560
+ state.progress?.milestones?.total,
561
+ 3,
562
+ "multi-ms: milestones total = 3",
563
+ );
564
+ } finally {
565
+ cleanup(base);
566
+ }
567
+ }
568
+
569
+ // ─── Test 10: requirements integration ─────────────────────────────────
570
+ console.log("\n=== requirements integration ===");
571
+ {
572
+ const base = createFixtureBase();
573
+ try {
574
+ writeRequirements(
575
+ base,
576
+ `# Requirements
577
+
578
+ ## Active
579
+
580
+ ### R001 — First Active Requirement
581
+ - Status: active
582
+ - Description: Something active.
583
+
584
+ ### R002 — Second Active Requirement
585
+ - Status: active
586
+ - Description: Another active one.
587
+
588
+ ## Validated
589
+
590
+ ### R003 — Validated Requirement
591
+ - Status: validated
592
+ - Description: Already validated.
593
+
594
+ ## Deferred
595
+
596
+ ### R004 — Deferred Requirement
597
+ - Status: deferred
598
+ - Description: Pushed back.
599
+
600
+ ### R005 — Another Deferred
601
+ - Status: deferred
602
+ - Description: Also deferred.
603
+
604
+ ## Out of Scope
605
+
606
+ ### R006 — Out of Scope Requirement
607
+ - Status: out-of-scope
608
+ - Description: Not doing this.
609
+ `,
610
+ );
611
+
612
+ // Need at least an empty milestones dir for deriveState
613
+ const state = await deriveState(base);
614
+
615
+ assert(
616
+ state.requirements !== undefined,
617
+ "requirements: requirements object exists",
618
+ );
619
+ assertEq(state.requirements?.active, 2, "requirements: active = 2");
620
+ assertEq(state.requirements?.validated, 1, "requirements: validated = 1");
621
+ assertEq(state.requirements?.deferred, 2, "requirements: deferred = 2");
622
+ assertEq(
623
+ state.requirements?.outOfScope,
624
+ 1,
625
+ "requirements: outOfScope = 1",
626
+ );
627
+ assertEq(
628
+ state.requirements?.total,
629
+ 6,
630
+ "requirements: total = 6 (sum of all)",
631
+ );
632
+ } finally {
633
+ cleanup(base);
634
+ }
635
+ }
636
+
637
+ // ─── Test 11: all slices [x], no summary → completing-milestone ────────
638
+ console.log("\n=== all slices [x], no summary → completing-milestone ===");
639
+ {
640
+ const base = createFixtureBase();
641
+ try {
642
+ writeRoadmap(
643
+ base,
644
+ "M001",
645
+ `# M001: Test Milestone
646
+
647
+ **Vision:** Test completing-milestone phase.
648
+
649
+ ## Slices
650
+
651
+ - [x] **S01: First Done** \`risk:low\` \`depends:[]\`
652
+ > After this: S01 complete.
653
+
654
+ - [x] **S02: Second Done** \`risk:low\` \`depends:[S01]\`
655
+ > After this: S02 complete.
656
+ `,
657
+ );
658
+
659
+ const state = await deriveState(base);
660
+
661
+ assertEq(
662
+ state.phase,
663
+ "completing-milestone",
664
+ "completing-ms: phase is completing-milestone",
665
+ );
666
+ assert(
667
+ state.activeMilestone !== null,
668
+ "completing-ms: activeMilestone is not null",
669
+ );
670
+ assertEq(
671
+ state.activeMilestone?.id,
672
+ "M001",
673
+ "completing-ms: activeMilestone id is M001",
674
+ );
675
+ assertEq(state.activeSlice, null, "completing-ms: activeSlice is null");
676
+ assertEq(state.activeTask, null, "completing-ms: activeTask is null");
677
+ assertEq(state.registry.length, 1, "completing-ms: registry has 1 entry");
678
+ assertEq(
679
+ state.registry[0]?.status,
680
+ "active",
681
+ "completing-ms: registry[0] status is active (not complete)",
682
+ );
683
+ assertEq(
684
+ state.progress?.slices?.done,
685
+ 2,
686
+ "completing-ms: slices done = 2",
687
+ );
688
+ assertEq(
689
+ state.progress?.slices?.total,
690
+ 2,
691
+ "completing-ms: slices total = 2",
692
+ );
693
+ assert(
694
+ state.nextAction.toLowerCase().includes("summary") ||
695
+ state.nextAction.toLowerCase().includes("complete"),
696
+ "completing-ms: nextAction mentions summary or complete",
697
+ );
698
+ } finally {
699
+ cleanup(base);
700
+ }
701
+ }
702
+
703
+ // ─── Test 12: all slices [x], summary exists → complete ───────────────
704
+ console.log("\n=== all slices [x], summary exists → complete ===");
705
+ {
706
+ const base = createFixtureBase();
707
+ try {
708
+ writeRoadmap(
709
+ base,
710
+ "M001",
711
+ `# M001: Test Milestone
712
+
713
+ **Vision:** Test that summary presence means complete.
714
+
715
+ ## Slices
716
+
717
+ - [x] **S01: Done** \`risk:low\` \`depends:[]\`
718
+ > After this: Done.
719
+ `,
720
+ );
721
+
722
+ writeMilestoneSummary(
723
+ base,
724
+ "M001",
725
+ `# M001 Summary\n\nMilestone is complete.`,
726
+ );
727
+
728
+ const state = await deriveState(base);
729
+
730
+ assertEq(state.phase, "complete", "summary-exists: phase is complete");
731
+ assertEq(
732
+ state.registry.length,
733
+ 1,
734
+ "summary-exists: registry has 1 entry",
735
+ );
736
+ assertEq(
737
+ state.registry[0]?.status,
738
+ "complete",
739
+ "summary-exists: registry[0] status is complete",
740
+ );
741
+ assertEq(state.activeSlice, null, "summary-exists: activeSlice is null");
742
+ assertEq(state.activeTask, null, "summary-exists: activeTask is null");
743
+ } finally {
744
+ cleanup(base);
745
+ }
746
+ }
747
+
748
+ // ─── Test 13: multi-milestone completing-milestone ─────────────────────
749
+ console.log("\n=== multi-milestone completing-milestone ===");
750
+ {
751
+ const base = createFixtureBase();
752
+ try {
753
+ // M001: all slices done + summary exists → complete
754
+ writeRoadmap(
755
+ base,
756
+ "M001",
757
+ `# M001: First Milestone
758
+
759
+ **Vision:** Already complete with summary.
760
+
761
+ ## Slices
762
+
763
+ - [x] **S01: Done** \`risk:low\` \`depends:[]\`
764
+ > After this: Done.
765
+ `,
766
+ );
767
+ writeMilestoneSummary(
768
+ base,
769
+ "M001",
770
+ `# M001 Summary\n\nFirst milestone complete.`,
771
+ );
772
+
773
+ // M002: all slices done, no summary → completing-milestone
774
+ writeRoadmap(
775
+ base,
776
+ "M002",
777
+ `# M002: Second Milestone
778
+
779
+ **Vision:** All slices done but no summary.
780
+
781
+ ## Slices
782
+
783
+ - [x] **S01: Done** \`risk:low\` \`depends:[]\`
784
+ > After this: Done.
785
+
786
+ - [x] **S02: Also Done** \`risk:low\` \`depends:[S01]\`
787
+ > After this: Done.
788
+ `,
789
+ );
790
+
791
+ // M003: has incomplete slices → pending (M002 is active)
792
+ writeRoadmap(
793
+ base,
794
+ "M003",
795
+ `# M003: Third Milestone
796
+
797
+ **Vision:** Not yet started.
798
+
799
+ ## Slices
800
+
801
+ - [ ] **S01: Not Started** \`risk:low\` \`depends:[]\`
802
+ > After this: Done.
803
+ `,
804
+ );
805
+
806
+ const state = await deriveState(base);
807
+
808
+ assertEq(
809
+ state.phase,
810
+ "completing-milestone",
811
+ "multi-completing: phase is completing-milestone",
812
+ );
813
+ assertEq(
814
+ state.activeMilestone?.id,
815
+ "M002",
816
+ "multi-completing: activeMilestone is M002",
817
+ );
818
+ assertEq(
819
+ state.activeSlice,
820
+ null,
821
+ "multi-completing: activeSlice is null",
822
+ );
823
+ assertEq(state.activeTask, null, "multi-completing: activeTask is null");
824
+ assertEq(
825
+ state.registry.length,
826
+ 3,
827
+ "multi-completing: registry has 3 entries",
828
+ );
829
+ assertEq(
830
+ state.registry[0]?.id,
831
+ "M001",
832
+ "multi-completing: registry[0] is M001",
833
+ );
834
+ assertEq(
835
+ state.registry[0]?.status,
836
+ "complete",
837
+ "multi-completing: M001 is complete",
838
+ );
839
+ assertEq(
840
+ state.registry[1]?.id,
841
+ "M002",
842
+ "multi-completing: registry[1] is M002",
843
+ );
844
+ assertEq(
845
+ state.registry[1]?.status,
846
+ "active",
847
+ "multi-completing: M002 is active (completing-milestone)",
848
+ );
849
+ assertEq(
850
+ state.registry[2]?.id,
851
+ "M003",
852
+ "multi-completing: registry[2] is M003",
853
+ );
854
+ assertEq(
855
+ state.registry[2]?.status,
856
+ "pending",
857
+ "multi-completing: M003 is pending",
858
+ );
859
+ assertEq(
860
+ state.progress?.milestones?.done,
861
+ 1,
862
+ "multi-completing: milestones done = 1",
863
+ );
864
+ assertEq(
865
+ state.progress?.milestones?.total,
866
+ 3,
867
+ "multi-completing: milestones total = 3",
868
+ );
869
+ assertEq(
870
+ state.progress?.slices?.done,
871
+ 2,
872
+ "multi-completing: slices done = 2",
873
+ );
874
+ assertEq(
875
+ state.progress?.slices?.total,
876
+ 2,
877
+ "multi-completing: slices total = 2",
878
+ );
879
+ } finally {
880
+ cleanup(base);
881
+ }
882
+ }
883
+
884
+ // ═══ Milestone with summary but no roadmap → complete ═══════════════════
885
+ {
886
+ console.log("\n=== milestone with summary and no roadmap → complete ===");
887
+ const base = createFixtureBase();
888
+ try {
889
+ // M001, M002: completed milestones with summaries but no roadmaps
890
+ const m1dir = join(base, ".kata", "milestones", "M001");
891
+ mkdirSync(m1dir, { recursive: true });
892
+ writeFileSync(
893
+ join(m1dir, "M001-SUMMARY.md"),
894
+ "---\nid: M001\n---\n# Bootstrap\nDone.",
895
+ );
896
+
897
+ const m2dir = join(base, ".kata", "milestones", "M002");
898
+ mkdirSync(m2dir, { recursive: true });
899
+ writeFileSync(
900
+ join(m2dir, "M002-SUMMARY.md"),
901
+ "---\nid: M002\n---\n# Core Features\nDone.",
902
+ );
903
+
904
+ // M003: active milestone with a roadmap
905
+ writeRoadmap(
906
+ base,
907
+ "M003",
908
+ "# M003: Polish\n## Slices\n- [ ] **S01: Cleanup**",
909
+ );
910
+
911
+ const state = await deriveState(base);
912
+
913
+ assertEq(
914
+ state.phase,
915
+ "planning",
916
+ "summary-no-roadmap: phase is planning (active is M003)",
917
+ );
918
+ assertEq(
919
+ state.activeMilestone?.id,
920
+ "M003",
921
+ "summary-no-roadmap: active milestone is M003",
922
+ );
923
+ assertEq(
924
+ state.activeMilestone?.title,
925
+ "Polish",
926
+ "summary-no-roadmap: active title is Polish",
927
+ );
928
+ assertEq(
929
+ state.registry.length,
930
+ 3,
931
+ "summary-no-roadmap: registry has 3 entries",
932
+ );
933
+ assertEq(
934
+ state.registry[0]?.status,
935
+ "complete",
936
+ "summary-no-roadmap: M001 is complete",
937
+ );
938
+ assertEq(
939
+ state.registry[0]?.title,
940
+ "Bootstrap",
941
+ "summary-no-roadmap: M001 title from summary",
942
+ );
943
+ assertEq(
944
+ state.registry[1]?.status,
945
+ "complete",
946
+ "summary-no-roadmap: M002 is complete",
947
+ );
948
+ assertEq(
949
+ state.registry[1]?.title,
950
+ "Core Features",
951
+ "summary-no-roadmap: M002 title from summary",
952
+ );
953
+ assertEq(
954
+ state.registry[2]?.status,
955
+ "active",
956
+ "summary-no-roadmap: M003 is active",
957
+ );
958
+ assertEq(
959
+ state.progress?.milestones?.done,
960
+ 2,
961
+ "summary-no-roadmap: milestones done = 2",
962
+ );
963
+ assertEq(
964
+ state.progress?.milestones?.total,
965
+ 3,
966
+ "summary-no-roadmap: milestones total = 3",
967
+ );
968
+ } finally {
969
+ cleanup(base);
970
+ }
971
+ }
972
+
973
+ // ═══ All milestones have summary but no roadmap → complete ═════════════
974
+ {
975
+ console.log("\n=== all milestones summary-only → complete ===");
976
+ const base = createFixtureBase();
977
+ try {
978
+ const m1dir = join(base, ".kata", "milestones", "M001");
979
+ mkdirSync(m1dir, { recursive: true });
980
+ writeFileSync(
981
+ join(m1dir, "M001-SUMMARY.md"),
982
+ "---\ntitle: Done\n---\nAll done.",
983
+ );
984
+
985
+ const state = await deriveState(base);
986
+ assertEq(state.phase, "complete", "all-summary-only: phase is complete");
987
+ assertEq(
988
+ state.registry[0]?.status,
989
+ "complete",
990
+ "all-summary-only: M001 is complete",
991
+ );
992
+ } finally {
993
+ cleanup(base);
994
+ }
995
+ }
996
+
997
+ // ═════════════════════════════════════════════════════════════════════════
998
+ // Results
999
+ // ═════════════════════════════════════════════════════════════════════════
1000
+
1001
+ console.log(`\n${"=".repeat(40)}`);
1002
+ console.log(`Results: ${passed} passed, ${failed} failed`);
1003
+ if (failed > 0) {
1004
+ process.exit(1);
1005
+ } else {
1006
+ console.log("All tests passed ✓");
1007
+ }
1008
+ }
1009
+
1010
+ main().catch((error) => {
1011
+ console.error(error);
1012
+ process.exit(1);
1013
+ });