@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,1257 @@
1
+ import { parseRoadmap, parsePlan, parseSummary, parseContinue, parseRequirementCounts } from '../files.ts';
2
+
3
+ let passed = 0;
4
+ let failed = 0;
5
+
6
+ function assert(condition: boolean, message: string): void {
7
+ if (condition) passed++;
8
+ else {
9
+ failed++;
10
+ console.error(` FAIL: ${message}`);
11
+ }
12
+ }
13
+
14
+ function assertEq<T>(actual: T, expected: T, message: string): void {
15
+ if (JSON.stringify(actual) === JSON.stringify(expected)) passed++;
16
+ else {
17
+ failed++;
18
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
19
+ }
20
+ }
21
+
22
+ // ═══════════════════════════════════════════════════════════════════════════
23
+ // parseRoadmap tests
24
+ // ═══════════════════════════════════════════════════════════════════════════
25
+
26
+ console.log('\n=== parseRoadmap: full roadmap ===');
27
+ {
28
+ const content = `# M001: Kata Extension — Hierarchical Planning
29
+
30
+ **Vision:** Build a structured planning system for coding agents.
31
+
32
+ **Success Criteria:**
33
+ - All parsers have test coverage
34
+ - Round-trip formatting preserves data
35
+ - State derivation works correctly
36
+
37
+ ---
38
+
39
+ ## Slices
40
+
41
+ - [x] **S01: Types + File I/O** \`risk:low\` \`depends:[]\`
42
+ > After this: All types defined and parsers work.
43
+
44
+ - [ ] **S02: State Derivation** \`risk:medium\` \`depends:[S01]\`
45
+ > After this: Dashboard shows real-time state.
46
+
47
+ - [ ] **S03: Auto Mode** \`risk:high\` \`depends:[S01, S02]\`
48
+ > After this: Agent can execute tasks automatically.
49
+
50
+ ---
51
+
52
+ ## Boundary Map
53
+
54
+ ### S01 → S02
55
+ \`\`\`
56
+ Produces:
57
+ types.ts — all type definitions
58
+ files.ts — parser and formatter functions
59
+
60
+ Consumes from S02:
61
+ nothing
62
+ \`\`\`
63
+
64
+ ### S02 → S03
65
+ \`\`\`
66
+ Produces:
67
+ state.ts — deriveState function
68
+
69
+ Consumes from S03:
70
+ auto-mode entry points
71
+ \`\`\`
72
+ `;
73
+
74
+ const r = parseRoadmap(content);
75
+
76
+ assertEq(r.title, 'M001: Kata Extension — Hierarchical Planning', 'roadmap title');
77
+ assertEq(r.vision, 'Build a structured planning system for coding agents.', 'roadmap vision');
78
+ assertEq(r.successCriteria.length, 3, 'success criteria count');
79
+ assertEq(r.successCriteria[0], 'All parsers have test coverage', 'first success criterion');
80
+ assertEq(r.successCriteria[2], 'State derivation works correctly', 'third success criterion');
81
+
82
+ // Slices
83
+ assertEq(r.slices.length, 3, 'slice count');
84
+
85
+ assertEq(r.slices[0].id, 'S01', 'S01 id');
86
+ assertEq(r.slices[0].title, 'Types + File I/O', 'S01 title');
87
+ assertEq(r.slices[0].risk, 'low', 'S01 risk');
88
+ assertEq(r.slices[0].depends, [], 'S01 depends');
89
+ assertEq(r.slices[0].done, true, 'S01 done');
90
+ assertEq(r.slices[0].demo, 'All types defined and parsers work.', 'S01 demo');
91
+
92
+ assertEq(r.slices[1].id, 'S02', 'S02 id');
93
+ assertEq(r.slices[1].title, 'State Derivation', 'S02 title');
94
+ assertEq(r.slices[1].risk, 'medium', 'S02 risk');
95
+ assertEq(r.slices[1].depends, ['S01'], 'S02 depends');
96
+ assertEq(r.slices[1].done, false, 'S02 done');
97
+
98
+ assertEq(r.slices[2].id, 'S03', 'S03 id');
99
+ assertEq(r.slices[2].risk, 'high', 'S03 risk');
100
+ assertEq(r.slices[2].depends, ['S01', 'S02'], 'S03 depends');
101
+ assertEq(r.slices[2].done, false, 'S03 done');
102
+
103
+ // Boundary map
104
+ assertEq(r.boundaryMap.length, 2, 'boundary map entry count');
105
+ assertEq(r.boundaryMap[0].fromSlice, 'S01', 'bm[0] from');
106
+ assertEq(r.boundaryMap[0].toSlice, 'S02', 'bm[0] to');
107
+ assert(r.boundaryMap[0].produces.includes('types.ts'), 'bm[0] produces mentions types.ts');
108
+ assertEq(r.boundaryMap[1].fromSlice, 'S02', 'bm[1] from');
109
+ assertEq(r.boundaryMap[1].toSlice, 'S03', 'bm[1] to');
110
+ }
111
+
112
+ console.log('\n=== parseRoadmap: empty slices section ===');
113
+ {
114
+ const content = `# M002: Empty Milestone
115
+
116
+ **Vision:** Nothing yet.
117
+
118
+ ## Slices
119
+
120
+ ## Boundary Map
121
+ `;
122
+
123
+ const r = parseRoadmap(content);
124
+ assertEq(r.title, 'M002: Empty Milestone', 'title with empty slices');
125
+ assertEq(r.slices.length, 0, 'no slices parsed');
126
+ assertEq(r.boundaryMap.length, 0, 'no boundary map entries');
127
+ }
128
+
129
+ console.log('\n=== parseRoadmap: malformed checkbox lines ===');
130
+ {
131
+ // Lines that don't match the expected bold pattern should be skipped
132
+ const content = `# M003: Malformed
133
+
134
+ **Vision:** Test malformed lines.
135
+
136
+ ## Slices
137
+
138
+ - [ ] S01: Missing bold markers \`risk:low\` \`depends:[]\`
139
+ - [x] **S02: Valid Slice** \`risk:medium\` \`depends:[]\`
140
+ > After this: Works.
141
+ - [ ] Not a checkbox at all
142
+ Some random text
143
+ - [x] **S03: Another Valid** \`risk:high\` \`depends:[S02]\`
144
+ > After this: Also works.
145
+ `;
146
+
147
+ const r = parseRoadmap(content);
148
+ // Only S02 and S03 should be parsed (malformed lines without bold markers are skipped)
149
+ assertEq(r.slices.length, 2, 'only valid slices parsed from malformed input');
150
+ assertEq(r.slices[0].id, 'S02', 'first valid slice is S02');
151
+ assertEq(r.slices[0].done, true, 'S02 done');
152
+ assertEq(r.slices[1].id, 'S03', 'second valid slice is S03');
153
+ assertEq(r.slices[1].depends, ['S02'], 'S03 depends on S02');
154
+ }
155
+
156
+ console.log('\n=== parseRoadmap: lowercase vs uppercase X for done ===');
157
+ {
158
+ const content = `# M004: Case Test
159
+
160
+ **Vision:** Test X case sensitivity.
161
+
162
+ ## Slices
163
+
164
+ - [x] **S01: Lowercase x** \`risk:low\` \`depends:[]\`
165
+ > After this: done.
166
+
167
+ - [X] **S02: Uppercase X** \`risk:low\` \`depends:[]\`
168
+ > After this: also done.
169
+
170
+ - [ ] **S03: Not Done** \`risk:low\` \`depends:[]\`
171
+ > After this: not yet.
172
+ `;
173
+
174
+ const r = parseRoadmap(content);
175
+ assertEq(r.slices.length, 3, 'all three slices parsed');
176
+ assertEq(r.slices[0].done, true, 'lowercase x is done');
177
+ assertEq(r.slices[1].done, true, 'uppercase X is done');
178
+ assertEq(r.slices[2].done, false, 'space is not done');
179
+ }
180
+
181
+ console.log('\n=== parseRoadmap: missing boundary map ===');
182
+ {
183
+ const content = `# M005: No Boundary Map
184
+
185
+ **Vision:** A roadmap without a boundary map section.
186
+
187
+ **Success Criteria:**
188
+ - One criterion
189
+
190
+ ---
191
+
192
+ ## Slices
193
+
194
+ - [ ] **S01: Only Slice** \`risk:low\` \`depends:[]\`
195
+ > After this: Done.
196
+ `;
197
+
198
+ const r = parseRoadmap(content);
199
+ assertEq(r.title, 'M005: No Boundary Map', 'title');
200
+ assertEq(r.slices.length, 1, 'one slice');
201
+ assertEq(r.boundaryMap.length, 0, 'empty boundary map when section missing');
202
+ assertEq(r.successCriteria.length, 1, 'one success criterion');
203
+ }
204
+
205
+ console.log('\n=== parseRoadmap: no sections at all ===');
206
+ {
207
+ const content = `# M006: Bare Minimum
208
+
209
+ Just a title and nothing else.
210
+ `;
211
+
212
+ const r = parseRoadmap(content);
213
+ assertEq(r.title, 'M006: Bare Minimum', 'title from bare roadmap');
214
+ assertEq(r.vision, '', 'empty vision');
215
+ assertEq(r.successCriteria.length, 0, 'no success criteria');
216
+ assertEq(r.slices.length, 0, 'no slices');
217
+ assertEq(r.boundaryMap.length, 0, 'no boundary map');
218
+ }
219
+
220
+ console.log('\n=== parseRoadmap: slice with no demo blockquote ===');
221
+ {
222
+ const content = `# M007: No Demo
223
+
224
+ **Vision:** Testing slices without demo lines.
225
+
226
+ ## Slices
227
+
228
+ - [ ] **S01: No Demo Here** \`risk:medium\` \`depends:[]\`
229
+ - [ ] **S02: Also No Demo** \`risk:low\` \`depends:[S01]\`
230
+ `;
231
+
232
+ const r = parseRoadmap(content);
233
+ assertEq(r.slices.length, 2, 'two slices without demos');
234
+ assertEq(r.slices[0].demo, '', 'S01 demo empty');
235
+ assertEq(r.slices[1].demo, '', 'S02 demo empty');
236
+ }
237
+
238
+ console.log('\n=== parseRoadmap: missing risk defaults to low ===');
239
+ {
240
+ const content = `# M008: Default Risk
241
+
242
+ **Vision:** Test default risk.
243
+
244
+ ## Slices
245
+
246
+ - [ ] **S01: No Risk Tag** \`depends:[]\`
247
+ > After this: done.
248
+ `;
249
+
250
+ const r = parseRoadmap(content);
251
+ assertEq(r.slices.length, 1, 'one slice');
252
+ assertEq(r.slices[0].risk, 'low', 'default risk is low');
253
+ }
254
+
255
+ // ═══════════════════════════════════════════════════════════════════════════
256
+ // parsePlan tests
257
+ // ═══════════════════════════════════════════════════════════════════════════
258
+
259
+ console.log('\n=== parsePlan: full plan ===');
260
+ {
261
+ const content = `# S01: Parser Test Suite
262
+
263
+ **Goal:** All 5 parsers have test coverage with edge cases.
264
+ **Demo:** \`node --test tests/parsers.test.ts\` passes with zero failures.
265
+
266
+ ## Must-Haves
267
+
268
+ - parseRoadmap tests cover happy path and edge cases
269
+ - parsePlan tests cover happy path and edge cases
270
+ - All existing tests still pass
271
+
272
+ ## Tasks
273
+
274
+ - [ ] **T01: Test parseRoadmap and parsePlan** \`est:45m\`
275
+ Create tests/parsers.test.ts with comprehensive tests for the two most complex parsers.
276
+
277
+ - [x] **T02: Test parseSummary and parseContinue** \`est:35m\`
278
+ Extend tests/parsers.test.ts with tests for the remaining parsers.
279
+
280
+ ## Files Likely Touched
281
+
282
+ - \`tests/parsers.test.ts\` — new test file
283
+ - \`types.ts\` — add observability_surfaces
284
+ - \`files.ts\` — update parseSummary
285
+ `;
286
+
287
+ const p = parsePlan(content);
288
+
289
+ assertEq(p.id, 'S01', 'plan id');
290
+ assertEq(p.title, 'Parser Test Suite', 'plan title');
291
+ assertEq(p.goal, 'All 5 parsers have test coverage with edge cases.', 'plan goal');
292
+ assertEq(p.demo, '`node --test tests/parsers.test.ts` passes with zero failures.', 'plan demo');
293
+
294
+ // Must-haves
295
+ assertEq(p.mustHaves.length, 3, 'must-have count');
296
+ assertEq(p.mustHaves[0], 'parseRoadmap tests cover happy path and edge cases', 'first must-have');
297
+
298
+ // Tasks
299
+ assertEq(p.tasks.length, 2, 'task count');
300
+
301
+ assertEq(p.tasks[0].id, 'T01', 'T01 id');
302
+ assertEq(p.tasks[0].title, 'Test parseRoadmap and parsePlan', 'T01 title');
303
+ assertEq(p.tasks[0].done, false, 'T01 not done');
304
+ assert(p.tasks[0].description.includes('comprehensive tests'), 'T01 description content');
305
+
306
+ assertEq(p.tasks[1].id, 'T02', 'T02 id');
307
+ assertEq(p.tasks[1].title, 'Test parseSummary and parseContinue', 'T02 title');
308
+ assertEq(p.tasks[1].done, true, 'T02 done');
309
+
310
+ // Files likely touched
311
+ assertEq(p.filesLikelyTouched.length, 3, 'files likely touched count');
312
+ assert(p.filesLikelyTouched[0].includes('tests/parsers.test.ts'), 'first file');
313
+ }
314
+
315
+ console.log('\n=== parsePlan: multi-line task description concatenation ===');
316
+ {
317
+ const content = `# S02: Multi-line Test
318
+
319
+ **Goal:** Test multi-line descriptions.
320
+ **Demo:** Descriptions are concatenated.
321
+
322
+ ## Must-Haves
323
+
324
+ - Multi-line works
325
+
326
+ ## Tasks
327
+
328
+ - [ ] **T01: Multi-line Task** \`est:30m\`
329
+ First line of description.
330
+ Second line of description.
331
+ Third line of description.
332
+
333
+ - [ ] **T02: Single Line** \`est:10m\`
334
+ Just one line.
335
+
336
+ ## Files Likely Touched
337
+
338
+ - \`foo.ts\`
339
+ `;
340
+
341
+ const p = parsePlan(content);
342
+
343
+ assertEq(p.tasks.length, 2, 'two tasks');
344
+ // Multi-line descriptions should be concatenated with spaces
345
+ assert(p.tasks[0].description.includes('First line'), 'T01 desc has first line');
346
+ assert(p.tasks[0].description.includes('Second line'), 'T01 desc has second line');
347
+ assert(p.tasks[0].description.includes('Third line'), 'T01 desc has third line');
348
+ // Verify concatenation with space separator
349
+ assert(p.tasks[0].description.includes('description. Second'), 'lines joined with space');
350
+
351
+ assertEq(p.tasks[1].description, 'Just one line.', 'T02 single-line desc');
352
+ }
353
+
354
+ console.log('\n=== parsePlan: task with missing estimate ===');
355
+ {
356
+ const content = `# S03: No Estimate
357
+
358
+ **Goal:** Handle tasks without estimates.
359
+ **Demo:** Parser doesn't crash.
360
+
361
+ ## Tasks
362
+
363
+ - [ ] **T01: No Estimate Task**
364
+ A task without an estimate backtick.
365
+
366
+ - [ ] **T02: Has Estimate** \`est:20m\`
367
+ This one has an estimate.
368
+ `;
369
+
370
+ const p = parsePlan(content);
371
+
372
+ assertEq(p.tasks.length, 2, 'two tasks parsed');
373
+ assertEq(p.tasks[0].id, 'T01', 'T01 id');
374
+ assertEq(p.tasks[0].title, 'No Estimate Task', 'T01 title without estimate');
375
+ assertEq(p.tasks[0].done, false, 'T01 not done');
376
+ // The estimate backtick text appears in description if present, but parser doesn't crash without it
377
+ assertEq(p.tasks[1].id, 'T02', 'T02 id');
378
+ }
379
+
380
+ console.log('\n=== parsePlan: empty tasks section ===');
381
+ {
382
+ const content = `# S04: Empty Tasks
383
+
384
+ **Goal:** No tasks yet.
385
+ **Demo:** Nothing.
386
+
387
+ ## Must-Haves
388
+
389
+ - Something
390
+
391
+ ## Tasks
392
+
393
+ ## Files Likely Touched
394
+
395
+ - \`nothing.ts\`
396
+ `;
397
+
398
+ const p = parsePlan(content);
399
+
400
+ assertEq(p.id, 'S04', 'plan id with empty tasks');
401
+ assertEq(p.tasks.length, 0, 'no tasks');
402
+ assertEq(p.mustHaves.length, 1, 'one must-have');
403
+ assertEq(p.filesLikelyTouched.length, 1, 'one file');
404
+ }
405
+
406
+ console.log('\n=== parsePlan: no H1 ===');
407
+ {
408
+ const content = `**Goal:** A plan without a heading.
409
+ **Demo:** Still parses.
410
+
411
+ ## Tasks
412
+
413
+ - [ ] **T01: Orphan Task** \`est:5m\`
414
+ A task in a headingless plan.
415
+ `;
416
+
417
+ const p = parsePlan(content);
418
+
419
+ assertEq(p.id, '', 'empty id without H1');
420
+ assertEq(p.title, '', 'empty title without H1');
421
+ assertEq(p.goal, 'A plan without a heading.', 'goal still parsed');
422
+ assertEq(p.tasks.length, 1, 'task still parsed');
423
+ assertEq(p.tasks[0].id, 'T01', 'task id');
424
+ }
425
+
426
+ console.log('\n=== parsePlan: task estimate backtick in description ===');
427
+ {
428
+ // The `est:45m` text appears after the bold closing but before the description lines
429
+ // It should end up as part of the description or be ignored gracefully
430
+ const content = `# S05: Estimate Handling
431
+
432
+ **Goal:** Test estimate text handling.
433
+ **Demo:** Works.
434
+
435
+ ## Tasks
436
+
437
+ - [ ] **T01: With Estimate** \`est:45m\`
438
+ Main description here.
439
+ `;
440
+
441
+ const p = parsePlan(content);
442
+ assertEq(p.tasks.length, 1, 'one task');
443
+ assertEq(p.tasks[0].id, 'T01', 'task id');
444
+ assertEq(p.tasks[0].title, 'With Estimate', 'title excludes estimate');
445
+ // The `est:45m` backtick text after ** is not part of the title or description
446
+ // It's on the same line after the regex match captures, so it's in the remainder
447
+ // The description should be the continuation lines
448
+ assert(p.tasks[0].description.includes('Main description'), 'description from continuation line');
449
+ }
450
+
451
+ console.log('\n=== parsePlan: uppercase X for done ===');
452
+ {
453
+ const content = `# S06: Case Test
454
+
455
+ **Goal:** Test case.
456
+ **Demo:** Works.
457
+
458
+ ## Tasks
459
+
460
+ - [X] **T01: Uppercase Done** \`est:5m\`
461
+ Done with uppercase X.
462
+
463
+ - [x] **T02: Lowercase Done** \`est:5m\`
464
+ Done with lowercase x.
465
+ `;
466
+
467
+ const p = parsePlan(content);
468
+ assertEq(p.tasks[0].done, true, 'uppercase X is done');
469
+ assertEq(p.tasks[1].done, true, 'lowercase x is done');
470
+ }
471
+
472
+ console.log('\n=== parsePlan: no Must-Haves section ===');
473
+ {
474
+ const content = `# S07: No Must-Haves
475
+
476
+ **Goal:** Test missing must-haves.
477
+ **Demo:** Parser handles it.
478
+
479
+ ## Tasks
480
+
481
+ - [ ] **T01: Only Task** \`est:10m\`
482
+ The only task.
483
+ `;
484
+
485
+ const p = parsePlan(content);
486
+ assertEq(p.mustHaves.length, 0, 'empty must-haves');
487
+ assertEq(p.tasks.length, 1, 'task still parsed');
488
+ }
489
+
490
+ console.log('\n=== parsePlan: no Files Likely Touched section ===');
491
+ {
492
+ const content = `# S08: No Files
493
+
494
+ **Goal:** Test missing files section.
495
+ **Demo:** Parser handles it.
496
+
497
+ ## Tasks
498
+
499
+ - [ ] **T01: Task** \`est:10m\`
500
+ Description.
501
+ `;
502
+
503
+ const p = parsePlan(content);
504
+ assertEq(p.filesLikelyTouched.length, 0, 'empty files likely touched');
505
+ }
506
+
507
+ console.log('\n=== parsePlan: old-format task entries (no sublines) ===');
508
+ {
509
+ const content = `# S09: Old Format
510
+
511
+ **Goal:** Test old-format compatibility.
512
+ **Demo:** Parser handles entries without sublines.
513
+
514
+ ## Tasks
515
+
516
+ - [ ] **T01: Classic Task** \`est:10m\`
517
+ Just a plain description with no labeled sublines.
518
+ `;
519
+
520
+ const p = parsePlan(content);
521
+ assertEq(p.tasks.length, 1, 'one task parsed');
522
+ assertEq(p.tasks[0].id, 'T01', 'task id');
523
+ assertEq(p.tasks[0].title, 'Classic Task', 'task title');
524
+ assertEq(p.tasks[0].done, false, 'task not done');
525
+ assertEq(p.tasks[0].files, undefined, 'files is undefined for old-format entry');
526
+ assertEq(p.tasks[0].verify, undefined, 'verify is undefined for old-format entry');
527
+ }
528
+
529
+ console.log('\n=== parsePlan: new-format task entries with Files and Verify sublines ===');
530
+ {
531
+ const content = `# S10: New Format
532
+
533
+ **Goal:** Test new-format subline extraction.
534
+ **Demo:** Parser extracts Files and Verify correctly.
535
+
536
+ ## Tasks
537
+
538
+ - [ ] **T01: Modern Task** \`est:15m\`
539
+ - Why: because we need typed plan entries
540
+ - Files: \`types.ts\`, \`files.ts\`
541
+ - Verify: run the test suite
542
+ `;
543
+
544
+ const p = parsePlan(content);
545
+ assertEq(p.tasks.length, 1, 'one task parsed');
546
+ assertEq(p.tasks[0].id, 'T01', 'task id');
547
+ assert(Array.isArray(p.tasks[0].files), 'files is an array');
548
+ assertEq(p.tasks[0].files!.length, 2, 'files array has two entries');
549
+ assertEq(p.tasks[0].files![0], 'types.ts', 'first file is types.ts');
550
+ assertEq(p.tasks[0].files![1], 'files.ts', 'second file is files.ts');
551
+ assertEq(p.tasks[0].verify, 'run the test suite', 'verify string extracted correctly');
552
+ assert(p.tasks[0].description.includes('Why: because we need typed plan entries'), 'Why line accumulates into description');
553
+ }
554
+
555
+ // ═══════════════════════════════════════════════════════════════════════════
556
+ // parseSummary tests
557
+ // ═══════════════════════════════════════════════════════════════════════════
558
+
559
+ console.log('\n=== parseSummary: full summary with all frontmatter fields ===');
560
+ {
561
+ const content = `---
562
+ id: T01
563
+ parent: S01
564
+ milestone: M001
565
+ provides:
566
+ - parseRoadmap test coverage
567
+ - parsePlan test coverage
568
+ requires:
569
+ - slice: S00
570
+ provides: type definitions
571
+ - slice: S02
572
+ provides: state derivation
573
+ affects:
574
+ - auto-mode dispatch
575
+ key_files:
576
+ - tests/parsers.test.ts
577
+ - files.ts
578
+ key_decisions:
579
+ - Use manual assert pattern
580
+ patterns_established:
581
+ - parsers.test.ts is the canonical test location
582
+ drill_down_paths:
583
+ - tests/parsers.test.ts for assertion details
584
+ observability_surfaces:
585
+ - test pass/fail output from node --test
586
+ - exit code 1 on failure
587
+ duration: 23min
588
+ verification_result: pass
589
+ retries: 0
590
+ completed_at: 2025-03-10T08:00:00Z
591
+ ---
592
+
593
+ # T01: Test parseRoadmap and parsePlan
594
+
595
+ **Created parsers.test.ts with 98 assertions across 16 test groups.**
596
+
597
+ ## What Happened
598
+
599
+ Added comprehensive tests for parseRoadmap and parsePlan.
600
+
601
+ ## Deviations
602
+
603
+ None.
604
+
605
+ ## Files Created/Modified
606
+
607
+ - \`tests/parsers.test.ts\` — new test file with 98 assertions
608
+ - \`types.ts\` — added observability_surfaces field
609
+ - \`files.ts\` — updated parseSummary extraction
610
+ `;
611
+
612
+ const s = parseSummary(content);
613
+
614
+ // Frontmatter fields
615
+ assertEq(s.frontmatter.id, 'T01', 'summary id');
616
+ assertEq(s.frontmatter.parent, 'S01', 'summary parent');
617
+ assertEq(s.frontmatter.milestone, 'M001', 'summary milestone');
618
+ assertEq(s.frontmatter.provides.length, 2, 'provides count');
619
+ assertEq(s.frontmatter.provides[0], 'parseRoadmap test coverage', 'first provides');
620
+ assertEq(s.frontmatter.provides[1], 'parsePlan test coverage', 'second provides');
621
+
622
+ // requires (nested objects)
623
+ assertEq(s.frontmatter.requires.length, 2, 'requires count');
624
+ assertEq(s.frontmatter.requires[0].slice, 'S00', 'first requires slice');
625
+ assertEq(s.frontmatter.requires[0].provides, 'type definitions', 'first requires provides');
626
+ assertEq(s.frontmatter.requires[1].slice, 'S02', 'second requires slice');
627
+ assertEq(s.frontmatter.requires[1].provides, 'state derivation', 'second requires provides');
628
+
629
+ assertEq(s.frontmatter.affects.length, 1, 'affects count');
630
+ assertEq(s.frontmatter.affects[0], 'auto-mode dispatch', 'affects value');
631
+ assertEq(s.frontmatter.key_files.length, 2, 'key_files count');
632
+ assertEq(s.frontmatter.key_decisions.length, 1, 'key_decisions count');
633
+ assertEq(s.frontmatter.patterns_established.length, 1, 'patterns_established count');
634
+ assertEq(s.frontmatter.drill_down_paths.length, 1, 'drill_down_paths count');
635
+
636
+ // observability_surfaces extraction
637
+ assertEq(s.frontmatter.observability_surfaces.length, 2, 'observability_surfaces count');
638
+ assertEq(s.frontmatter.observability_surfaces[0], 'test pass/fail output from node --test', 'first observability surface');
639
+ assertEq(s.frontmatter.observability_surfaces[1], 'exit code 1 on failure', 'second observability surface');
640
+
641
+ assertEq(s.frontmatter.duration, '23min', 'duration');
642
+ assertEq(s.frontmatter.verification_result, 'pass', 'verification_result');
643
+ assertEq(s.frontmatter.completed_at, '2025-03-10T08:00:00Z', 'completed_at');
644
+
645
+ // Body fields
646
+ assertEq(s.title, 'T01: Test parseRoadmap and parsePlan', 'summary title');
647
+ assertEq(s.oneLiner, 'Created parsers.test.ts with 98 assertions across 16 test groups.', 'one-liner');
648
+ assert(s.whatHappened.includes('comprehensive tests'), 'whatHappened content');
649
+ assertEq(s.deviations, 'None.', 'deviations');
650
+
651
+ // Files modified
652
+ assertEq(s.filesModified.length, 3, 'filesModified count');
653
+ assertEq(s.filesModified[0].path, 'tests/parsers.test.ts', 'first file path');
654
+ assert(s.filesModified[0].description.includes('98 assertions'), 'first file description');
655
+ assertEq(s.filesModified[1].path, 'types.ts', 'second file path');
656
+ assertEq(s.filesModified[2].path, 'files.ts', 'third file path');
657
+ }
658
+
659
+ console.log('\n=== parseSummary: one-liner extraction (bold-wrapped line after H1) ===');
660
+ {
661
+ const content = `# S01: Parser Test Suite
662
+
663
+ **All 5 parsers have test coverage with edge cases.**
664
+
665
+ ## What Happened
666
+
667
+ Things happened.
668
+ `;
669
+
670
+ const s = parseSummary(content);
671
+ assertEq(s.title, 'S01: Parser Test Suite', 'title');
672
+ assertEq(s.oneLiner, 'All 5 parsers have test coverage with edge cases.', 'bold one-liner');
673
+ }
674
+
675
+ console.log('\n=== parseSummary: non-bold paragraph after H1 (empty one-liner) ===');
676
+ {
677
+ const content = `# T02: Some Task
678
+
679
+ This is just a regular paragraph, not bold.
680
+
681
+ ## What Happened
682
+
683
+ Did stuff.
684
+ `;
685
+
686
+ const s = parseSummary(content);
687
+ assertEq(s.title, 'T02: Some Task', 'title');
688
+ assertEq(s.oneLiner, '', 'non-bold line results in empty one-liner');
689
+ }
690
+
691
+ console.log('\n=== parseSummary: files-modified parsing (backtick path — description format) ===');
692
+ {
693
+ const content = `# T03: File Changes
694
+
695
+ **One-liner.**
696
+
697
+ ## Files Created/Modified
698
+
699
+ - \`src/index.ts\` — main entry point
700
+ - \`src/utils.ts\` — utility functions
701
+ - \`README.md\` — updated docs
702
+ `;
703
+
704
+ const s = parseSummary(content);
705
+ assertEq(s.filesModified.length, 3, 'three files');
706
+ assertEq(s.filesModified[0].path, 'src/index.ts', 'first path');
707
+ assertEq(s.filesModified[0].description, 'main entry point', 'first description');
708
+ assertEq(s.filesModified[1].path, 'src/utils.ts', 'second path');
709
+ assertEq(s.filesModified[2].path, 'README.md', 'third path');
710
+ }
711
+
712
+ console.log('\n=== parseSummary: missing frontmatter (safe defaults) ===');
713
+ {
714
+ const content = `# T04: No Frontmatter
715
+
716
+ **Did something.**
717
+
718
+ ## What Happened
719
+
720
+ No frontmatter at all.
721
+ `;
722
+
723
+ const s = parseSummary(content);
724
+ assertEq(s.frontmatter.id, '', 'default id empty');
725
+ assertEq(s.frontmatter.parent, '', 'default parent empty');
726
+ assertEq(s.frontmatter.milestone, '', 'default milestone empty');
727
+ assertEq(s.frontmatter.provides.length, 0, 'default provides empty');
728
+ assertEq(s.frontmatter.requires.length, 0, 'default requires empty');
729
+ assertEq(s.frontmatter.affects.length, 0, 'default affects empty');
730
+ assertEq(s.frontmatter.key_files.length, 0, 'default key_files empty');
731
+ assertEq(s.frontmatter.key_decisions.length, 0, 'default key_decisions empty');
732
+ assertEq(s.frontmatter.patterns_established.length, 0, 'default patterns_established empty');
733
+ assertEq(s.frontmatter.drill_down_paths.length, 0, 'default drill_down_paths empty');
734
+ assertEq(s.frontmatter.observability_surfaces.length, 0, 'default observability_surfaces empty');
735
+ assertEq(s.frontmatter.duration, '', 'default duration empty');
736
+ assertEq(s.frontmatter.verification_result, 'untested', 'default verification_result');
737
+ assertEq(s.frontmatter.completed_at, '', 'default completed_at empty');
738
+ assertEq(s.title, 'T04: No Frontmatter', 'title still parsed');
739
+ assertEq(s.oneLiner, 'Did something.', 'one-liner still parsed');
740
+ }
741
+
742
+ console.log('\n=== parseSummary: empty body ===');
743
+ {
744
+ const content = `---
745
+ id: T05
746
+ parent: S01
747
+ milestone: M001
748
+ ---
749
+ `;
750
+
751
+ const s = parseSummary(content);
752
+ assertEq(s.frontmatter.id, 'T05', 'id from frontmatter');
753
+ assertEq(s.title, '', 'empty title');
754
+ assertEq(s.oneLiner, '', 'empty one-liner');
755
+ assertEq(s.whatHappened, '', 'empty whatHappened');
756
+ assertEq(s.deviations, '', 'empty deviations');
757
+ assertEq(s.filesModified.length, 0, 'no files modified');
758
+ }
759
+
760
+ console.log('\n=== parseSummary: summary with requires array (nested objects) ===');
761
+ {
762
+ const content = `---
763
+ id: T06
764
+ parent: S02
765
+ milestone: M001
766
+ requires:
767
+ - slice: S01
768
+ provides: parser functions
769
+ - slice: S00
770
+ provides: core types
771
+ - slice: S03
772
+ provides: state engine
773
+ provides: []
774
+ affects: []
775
+ key_files: []
776
+ key_decisions: []
777
+ patterns_established: []
778
+ drill_down_paths: []
779
+ observability_surfaces: []
780
+ duration: 10min
781
+ verification_result: pass
782
+ retries: 1
783
+ completed_at: 2025-03-10T09:00:00Z
784
+ ---
785
+
786
+ # T06: Nested Requires
787
+
788
+ **Test nested requires parsing.**
789
+
790
+ ## What Happened
791
+
792
+ Tested.
793
+ `;
794
+
795
+ const s = parseSummary(content);
796
+ assertEq(s.frontmatter.requires.length, 3, 'three requires entries');
797
+ assertEq(s.frontmatter.requires[0].slice, 'S01', 'first requires slice');
798
+ assertEq(s.frontmatter.requires[0].provides, 'parser functions', 'first requires provides');
799
+ assertEq(s.frontmatter.requires[1].slice, 'S00', 'second requires slice');
800
+ assertEq(s.frontmatter.requires[2].slice, 'S03', 'third requires slice');
801
+ assertEq(s.frontmatter.requires[2].provides, 'state engine', 'third requires provides');
802
+ }
803
+
804
+ // ═══════════════════════════════════════════════════════════════════════════
805
+ // parseContinue tests
806
+ // ═══════════════════════════════════════════════════════════════════════════
807
+
808
+ console.log('\n=== parseContinue: full continue file with all frontmatter fields ===');
809
+ {
810
+ const content = `---
811
+ milestone: M001
812
+ slice: S01
813
+ task: T02
814
+ step: 3
815
+ total_steps: 5
816
+ status: in_progress
817
+ saved_at: 2025-03-10T08:30:00Z
818
+ ---
819
+
820
+ ## Completed Work
821
+
822
+ Steps 1-3 are done. Created test file and wrote assertions.
823
+
824
+ ## Remaining Work
825
+
826
+ Steps 4-5: run tests and check regressions.
827
+
828
+ ## Decisions Made
829
+
830
+ Used manual assert pattern instead of node:assert.
831
+
832
+ ## Context
833
+
834
+ Working in the kata-s01 worktree. All imports use .ts extensions.
835
+
836
+ ## Next Action
837
+
838
+ Run the full test suite with node --test.
839
+ `;
840
+
841
+ const c = parseContinue(content);
842
+
843
+ // Frontmatter
844
+ assertEq(c.frontmatter.milestone, 'M001', 'continue milestone');
845
+ assertEq(c.frontmatter.slice, 'S01', 'continue slice');
846
+ assertEq(c.frontmatter.task, 'T02', 'continue task');
847
+ assertEq(c.frontmatter.step, 3, 'continue step');
848
+ assertEq(c.frontmatter.totalSteps, 5, 'continue totalSteps');
849
+ assertEq(c.frontmatter.status, 'in_progress', 'continue status');
850
+ assertEq(c.frontmatter.savedAt, '2025-03-10T08:30:00Z', 'continue savedAt');
851
+
852
+ // Body sections
853
+ assert(c.completedWork.includes('Steps 1-3 are done'), 'completedWork content');
854
+ assert(c.remainingWork.includes('Steps 4-5'), 'remainingWork content');
855
+ assert(c.decisions.includes('manual assert pattern'), 'decisions content');
856
+ assert(c.context.includes('kata-s01 worktree'), 'context content');
857
+ assert(c.nextAction.includes('node --test'), 'nextAction content');
858
+ }
859
+
860
+ console.log('\n=== parseContinue: string step/totalSteps parsed as integers ===');
861
+ {
862
+ const content = `---
863
+ milestone: M002
864
+ slice: S03
865
+ task: T01
866
+ step: 7
867
+ total_steps: 12
868
+ status: in_progress
869
+ saved_at: 2025-03-10T10:00:00Z
870
+ ---
871
+
872
+ ## Completed Work
873
+
874
+ Some work.
875
+
876
+ ## Remaining Work
877
+
878
+ More work.
879
+
880
+ ## Decisions Made
881
+
882
+ None.
883
+
884
+ ## Context
885
+
886
+ None.
887
+
888
+ ## Next Action
889
+
890
+ Continue.
891
+ `;
892
+
893
+ const c = parseContinue(content);
894
+ assertEq(c.frontmatter.step, 7, 'step parsed as integer 7');
895
+ assertEq(c.frontmatter.totalSteps, 12, 'totalSteps parsed as integer 12');
896
+ assertEq(typeof c.frontmatter.step, 'number', 'step is number type');
897
+ assertEq(typeof c.frontmatter.totalSteps, 'number', 'totalSteps is number type');
898
+ }
899
+
900
+ console.log('\n=== parseContinue: NaN step values (non-numeric strings) ===');
901
+ {
902
+ const content = `---
903
+ milestone: M001
904
+ slice: S01
905
+ task: T01
906
+ step: abc
907
+ total_steps: xyz
908
+ status: in_progress
909
+ saved_at: 2025-03-10T10:00:00Z
910
+ ---
911
+
912
+ ## Completed Work
913
+
914
+ Work.
915
+
916
+ ## Remaining Work
917
+
918
+ Work.
919
+
920
+ ## Decisions Made
921
+
922
+ None.
923
+
924
+ ## Context
925
+
926
+ None.
927
+
928
+ ## Next Action
929
+
930
+ Do things.
931
+ `;
932
+
933
+ const c = parseContinue(content);
934
+ // parseInt("abc") returns NaN; the parser || 0 fallback should give 0
935
+ // Actually, looking at parser: typeof fm.step === 'string' ? parseInt(fm.step) : ...
936
+ // parseInt("abc") = NaN, and NaN || 0 doesn't work because NaN is falsy only in boolean context
937
+ // But the parser uses: typeof fm.step === 'string' ? parseInt(fm.step) : (fm.step as number) || 0
938
+ // parseInt returns NaN which is a number, not 0 — let's verify
939
+ const stepIsNaN = Number.isNaN(c.frontmatter.step);
940
+ const totalIsNaN = Number.isNaN(c.frontmatter.totalSteps);
941
+ // The parser does parseInt which returns NaN for non-numeric strings
942
+ // There's no || 0 fallback on the parseInt path, so NaN is expected
943
+ assert(stepIsNaN, 'NaN step when non-numeric string');
944
+ assert(totalIsNaN, 'NaN totalSteps when non-numeric string');
945
+ }
946
+
947
+ console.log('\n=== parseContinue: all three status variants ===');
948
+ {
949
+ for (const status of ['in_progress', 'interrupted', 'compacted'] as const) {
950
+ const content = `---
951
+ milestone: M001
952
+ slice: S01
953
+ task: T01
954
+ step: 1
955
+ total_steps: 3
956
+ status: ${status}
957
+ saved_at: 2025-03-10T10:00:00Z
958
+ ---
959
+
960
+ ## Completed Work
961
+
962
+ Work.
963
+ `;
964
+
965
+ const c = parseContinue(content);
966
+ assertEq(c.frontmatter.status, status, `status variant: ${status}`);
967
+ }
968
+ }
969
+
970
+ console.log('\n=== parseContinue: missing frontmatter ===');
971
+ {
972
+ const content = `## Completed Work
973
+
974
+ Some work done.
975
+
976
+ ## Remaining Work
977
+
978
+ More to do.
979
+
980
+ ## Decisions Made
981
+
982
+ A decision.
983
+
984
+ ## Context
985
+
986
+ Some context.
987
+
988
+ ## Next Action
989
+
990
+ Next thing.
991
+ `;
992
+
993
+ const c = parseContinue(content);
994
+ assertEq(c.frontmatter.milestone, '', 'default milestone empty');
995
+ assertEq(c.frontmatter.slice, '', 'default slice empty');
996
+ assertEq(c.frontmatter.task, '', 'default task empty');
997
+ assertEq(c.frontmatter.step, 0, 'default step 0');
998
+ assertEq(c.frontmatter.totalSteps, 0, 'default totalSteps 0');
999
+ assertEq(c.frontmatter.status, 'in_progress', 'default status in_progress');
1000
+ assertEq(c.frontmatter.savedAt, '', 'default savedAt empty');
1001
+
1002
+ // Body sections still parse
1003
+ assert(c.completedWork.includes('Some work done'), 'completedWork without frontmatter');
1004
+ assert(c.remainingWork.includes('More to do'), 'remainingWork without frontmatter');
1005
+ assert(c.decisions.includes('A decision'), 'decisions without frontmatter');
1006
+ assert(c.context.includes('Some context'), 'context without frontmatter');
1007
+ assert(c.nextAction.includes('Next thing'), 'nextAction without frontmatter');
1008
+ }
1009
+
1010
+ console.log('\n=== parseContinue: body section extraction ===');
1011
+ {
1012
+ const content = `---
1013
+ milestone: M001
1014
+ slice: S01
1015
+ task: T03
1016
+ step: 2
1017
+ total_steps: 4
1018
+ status: interrupted
1019
+ saved_at: 2025-03-10T11:00:00Z
1020
+ ---
1021
+
1022
+ ## Completed Work
1023
+
1024
+ First paragraph of completed work.
1025
+ Second paragraph continuing the explanation.
1026
+
1027
+ ## Remaining Work
1028
+
1029
+ Need to finish step 3 and step 4.
1030
+
1031
+ ## Decisions Made
1032
+
1033
+ Decided to use approach A over approach B because of performance.
1034
+
1035
+ ## Context
1036
+
1037
+ Running in worktree. Node 22 required. TypeScript strict mode.
1038
+
1039
+ ## Next Action
1040
+
1041
+ Pick up at step 3: run the integration tests.
1042
+ `;
1043
+
1044
+ const c = parseContinue(content);
1045
+ assert(c.completedWork.includes('First paragraph'), 'completedWork first paragraph');
1046
+ assert(c.completedWork.includes('Second paragraph'), 'completedWork second paragraph');
1047
+ assert(c.remainingWork.includes('step 3 and step 4'), 'remainingWork detail');
1048
+ assert(c.decisions.includes('approach A over approach B'), 'decisions detail');
1049
+ assert(c.context.includes('Node 22 required'), 'context detail');
1050
+ assert(c.nextAction.includes('step 3: run the integration tests'), 'nextAction detail');
1051
+ }
1052
+
1053
+ console.log('\n=== parseContinue: total_steps vs totalSteps key support ===');
1054
+ {
1055
+ // Test total_steps (snake_case) — the primary format
1056
+ const content1 = `---
1057
+ milestone: M001
1058
+ slice: S01
1059
+ task: T01
1060
+ step: 2
1061
+ total_steps: 8
1062
+ status: in_progress
1063
+ saved_at: 2025-03-10T12:00:00Z
1064
+ ---
1065
+
1066
+ ## Completed Work
1067
+
1068
+ Work.
1069
+ `;
1070
+
1071
+ const c1 = parseContinue(content1);
1072
+ assertEq(c1.frontmatter.totalSteps, 8, 'total_steps snake_case works');
1073
+
1074
+ // Test totalSteps (camelCase) — the fallback
1075
+ const content2 = `---
1076
+ milestone: M001
1077
+ slice: S01
1078
+ task: T01
1079
+ step: 2
1080
+ totalSteps: 6
1081
+ status: in_progress
1082
+ saved_at: 2025-03-10T12:00:00Z
1083
+ ---
1084
+
1085
+ ## Completed Work
1086
+
1087
+ Work.
1088
+ `;
1089
+
1090
+ const c2 = parseContinue(content2);
1091
+ assertEq(c2.frontmatter.totalSteps, 6, 'totalSteps camelCase works');
1092
+ }
1093
+
1094
+ // ═══════════════════════════════════════════════════════════════════════════
1095
+ // parseRequirementCounts tests
1096
+ // ═══════════════════════════════════════════════════════════════════════════
1097
+
1098
+ console.log('\n=== parseRequirementCounts: full requirements file ===');
1099
+ {
1100
+ const content = `# Requirements
1101
+
1102
+ ## Active
1103
+
1104
+ ### R001 — User authentication
1105
+ - Status: active
1106
+
1107
+ ### R002 — Dashboard rendering
1108
+ - Status: blocked
1109
+
1110
+ ### R003 — API rate limiting
1111
+ - Status: active
1112
+
1113
+ ## Validated
1114
+
1115
+ ### R010 — Parser test coverage
1116
+ - Status: validated
1117
+
1118
+ ### R011 — Type system
1119
+ - Status: validated
1120
+
1121
+ ## Deferred
1122
+
1123
+ ### R020 — Admin panel
1124
+ - Status: deferred
1125
+
1126
+ ## Out of Scope
1127
+
1128
+ ### R030 — Mobile app
1129
+ - Status: out-of-scope
1130
+
1131
+ ### R031 — Desktop app
1132
+ - Status: out-of-scope
1133
+ `;
1134
+
1135
+ const counts = parseRequirementCounts(content);
1136
+ assertEq(counts.active, 3, 'active count');
1137
+ assertEq(counts.validated, 2, 'validated count');
1138
+ assertEq(counts.deferred, 1, 'deferred count');
1139
+ assertEq(counts.outOfScope, 2, 'outOfScope count');
1140
+ assertEq(counts.blocked, 1, 'blocked count');
1141
+ assertEq(counts.total, 8, 'total is sum of active+validated+deferred+outOfScope');
1142
+ }
1143
+
1144
+ console.log('\n=== parseRequirementCounts: null input returns all zeros ===');
1145
+ {
1146
+ const counts = parseRequirementCounts(null);
1147
+ assertEq(counts.active, 0, 'null active');
1148
+ assertEq(counts.validated, 0, 'null validated');
1149
+ assertEq(counts.deferred, 0, 'null deferred');
1150
+ assertEq(counts.outOfScope, 0, 'null outOfScope');
1151
+ assertEq(counts.blocked, 0, 'null blocked');
1152
+ assertEq(counts.total, 0, 'null total');
1153
+ }
1154
+
1155
+ console.log('\n=== parseRequirementCounts: empty sections return zero counts ===');
1156
+ {
1157
+ const content = `# Requirements
1158
+
1159
+ ## Active
1160
+
1161
+ ## Validated
1162
+
1163
+ ## Deferred
1164
+
1165
+ ## Out of Scope
1166
+ `;
1167
+
1168
+ const counts = parseRequirementCounts(content);
1169
+ assertEq(counts.active, 0, 'empty active');
1170
+ assertEq(counts.validated, 0, 'empty validated');
1171
+ assertEq(counts.deferred, 0, 'empty deferred');
1172
+ assertEq(counts.outOfScope, 0, 'empty outOfScope');
1173
+ assertEq(counts.blocked, 0, 'empty blocked');
1174
+ assertEq(counts.total, 0, 'empty total');
1175
+ }
1176
+
1177
+ console.log('\n=== parseRequirementCounts: blocked status counting ===');
1178
+ {
1179
+ const content = `# Requirements
1180
+
1181
+ ## Active
1182
+
1183
+ ### R001 — Blocked thing
1184
+ - Status: blocked
1185
+
1186
+ ### R002 — Another blocked thing
1187
+ - Status: blocked
1188
+
1189
+ ### R003 — Active thing
1190
+ - Status: active
1191
+
1192
+ ## Validated
1193
+
1194
+ ## Deferred
1195
+
1196
+ ### R020 — Blocked deferred
1197
+ - Status: blocked
1198
+
1199
+ ## Out of Scope
1200
+ `;
1201
+
1202
+ const counts = parseRequirementCounts(content);
1203
+ assertEq(counts.active, 3, 'active includes blocked items in Active section');
1204
+ assertEq(counts.blocked, 3, 'blocked counts all blocked statuses across sections');
1205
+ assertEq(counts.deferred, 1, 'deferred section count');
1206
+ }
1207
+
1208
+ console.log('\n=== parseRequirementCounts: total is sum of all section counts ===');
1209
+ {
1210
+ const content = `# Requirements
1211
+
1212
+ ## Active
1213
+
1214
+ ### R001 — One
1215
+ - Status: active
1216
+
1217
+ ## Validated
1218
+
1219
+ ### R010 — Two
1220
+ - Status: validated
1221
+
1222
+ ### R011 — Three
1223
+ - Status: validated
1224
+
1225
+ ## Deferred
1226
+
1227
+ ### R020 — Four
1228
+ - Status: deferred
1229
+
1230
+ ### R021 — Five
1231
+ - Status: deferred
1232
+
1233
+ ### R022 — Six
1234
+ - Status: deferred
1235
+
1236
+ ## Out of Scope
1237
+
1238
+ ### R030 — Seven
1239
+ - Status: out-of-scope
1240
+ `;
1241
+
1242
+ const counts = parseRequirementCounts(content);
1243
+ assertEq(counts.active, 1, 'one active');
1244
+ assertEq(counts.validated, 2, 'two validated');
1245
+ assertEq(counts.deferred, 3, 'three deferred');
1246
+ assertEq(counts.outOfScope, 1, 'one outOfScope');
1247
+ assertEq(counts.total, 7, 'total = 1 + 2 + 3 + 1');
1248
+ assertEq(counts.total, counts.active + counts.validated + counts.deferred + counts.outOfScope, 'total is exact sum');
1249
+ }
1250
+
1251
+ // ═══════════════════════════════════════════════════════════════════════════
1252
+ // Results
1253
+ // ═══════════════════════════════════════════════════════════════════════════
1254
+
1255
+ console.log(`\nResults: ${passed} passed, ${failed} failed`);
1256
+ if (failed > 0) process.exit(1);
1257
+ console.log('All tests passed ✓');