@kata-sh/cli 0.1.0 → 0.1.2

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,730 @@
1
+ // Kata Extension — File Parsing and I/O
2
+ // Parsers for roadmap, plan, summary, and continue files.
3
+ // Used by state derivation and the status widget.
4
+ // Pure functions, zero Pi dependencies — uses only Node built-ins.
5
+
6
+ import { promises as fs, readdirSync } from 'node:fs';
7
+ import { dirname } from 'node:path';
8
+ import { milestonesDir, resolveMilestoneFile, relMilestoneFile } from './paths.js';
9
+
10
+ import type {
11
+ Roadmap, RoadmapSliceEntry, BoundaryMapEntry, RiskLevel,
12
+ SlicePlan, TaskPlanEntry,
13
+ Summary, SummaryFrontmatter, SummaryRequires, FileModified,
14
+ Continue, ContinueFrontmatter, ContinueStatus,
15
+ RequirementCounts,
16
+ } from './types.ts';
17
+
18
+ // ─── Helpers ───────────────────────────────────────────────────────────────
19
+
20
+ /**
21
+ * Split markdown content into frontmatter (YAML-like) and body.
22
+ * Returns [frontmatterLines, body] where frontmatterLines is null if no frontmatter.
23
+ */
24
+ function splitFrontmatter(content: string): [string[] | null, string] {
25
+ const trimmed = content.trimStart();
26
+ if (!trimmed.startsWith('---')) return [null, content];
27
+
28
+ const afterFirst = trimmed.indexOf('\n');
29
+ if (afterFirst === -1) return [null, content];
30
+
31
+ const rest = trimmed.slice(afterFirst + 1);
32
+ const endIdx = rest.indexOf('\n---');
33
+ if (endIdx === -1) return [null, content];
34
+
35
+ const fmLines = rest.slice(0, endIdx).split('\n');
36
+ const body = rest.slice(endIdx + 4).replace(/^\n+/, '');
37
+ return [fmLines, body];
38
+ }
39
+
40
+ /**
41
+ * Parse YAML-like frontmatter lines into a flat key-value map.
42
+ * Handles simple scalars and arrays (lines starting with " - ").
43
+ * Handles nested objects like requires (lines with " key: value").
44
+ */
45
+ function parseFrontmatterMap(lines: string[]): Record<string, unknown> {
46
+ const result: Record<string, unknown> = {};
47
+ let currentKey: string | null = null;
48
+ let currentArray: unknown[] | null = null;
49
+ let currentObj: Record<string, string> | null = null;
50
+
51
+ for (const line of lines) {
52
+ // Nested object property (4-space indent with key: value)
53
+ const nestedMatch = line.match(/^ (\w[\w_]*)\s*:\s*(.*)$/);
54
+ if (nestedMatch && currentArray && currentObj) {
55
+ currentObj[nestedMatch[1]] = nestedMatch[2].trim();
56
+ continue;
57
+ }
58
+
59
+ // Array item (2-space indent)
60
+ const arrayMatch = line.match(/^ - (.*)$/);
61
+ if (arrayMatch && currentKey) {
62
+ // If there's a pending nested object, push it
63
+ if (currentObj && Object.keys(currentObj).length > 0) {
64
+ currentArray!.push(currentObj);
65
+ }
66
+ currentObj = null;
67
+
68
+ const val = arrayMatch[1].trim();
69
+ if (!currentArray) currentArray = [];
70
+
71
+ // Check if this array item starts a nested object (e.g. "- slice: S00")
72
+ const nestedStart = val.match(/^(\w[\w_]*)\s*:\s*(.*)$/);
73
+ if (nestedStart) {
74
+ currentObj = { [nestedStart[1]]: nestedStart[2].trim() };
75
+ } else {
76
+ currentArray.push(val);
77
+ }
78
+ continue;
79
+ }
80
+
81
+ // Flush previous key
82
+ if (currentKey) {
83
+ if (currentObj && Object.keys(currentObj).length > 0 && currentArray) {
84
+ currentArray.push(currentObj);
85
+ currentObj = null;
86
+ }
87
+ if (currentArray) {
88
+ result[currentKey] = currentArray;
89
+ }
90
+ currentArray = null;
91
+ }
92
+
93
+ // Top-level key: value
94
+ const kvMatch = line.match(/^(\w[\w_]*)\s*:\s*(.*)$/);
95
+ if (kvMatch) {
96
+ currentKey = kvMatch[1];
97
+ const val = kvMatch[2].trim();
98
+
99
+ if (val === '' || val === '[]') {
100
+ currentArray = [];
101
+ } else if (val.startsWith('[') && val.endsWith(']')) {
102
+ const inner = val.slice(1, -1).trim();
103
+ result[currentKey] = inner ? inner.split(',').map(s => s.trim()) : [];
104
+ currentKey = null;
105
+ } else {
106
+ result[currentKey] = val;
107
+ currentKey = null;
108
+ }
109
+ }
110
+ }
111
+
112
+ // Flush final key
113
+ if (currentKey) {
114
+ if (currentObj && Object.keys(currentObj).length > 0 && currentArray) {
115
+ currentArray.push(currentObj);
116
+ currentObj = null;
117
+ }
118
+ if (currentArray) {
119
+ result[currentKey] = currentArray;
120
+ }
121
+ }
122
+
123
+ return result;
124
+ }
125
+
126
+ /** Extract the text after a heading at a given level, up to the next heading of same or higher level. */
127
+ function extractSection(body: string, heading: string, level: number = 2): string | null {
128
+ const prefix = '#'.repeat(level) + ' ';
129
+ const regex = new RegExp(`^${prefix}${escapeRegex(heading)}\\s*$`, 'm');
130
+ const match = regex.exec(body);
131
+ if (!match) return null;
132
+
133
+ const start = match.index + match[0].length;
134
+ const rest = body.slice(start);
135
+
136
+ const nextHeading = rest.match(new RegExp(`^#{1,${level}} `, 'm'));
137
+ const end = nextHeading ? nextHeading.index! : rest.length;
138
+
139
+ return rest.slice(0, end).trim();
140
+ }
141
+
142
+ /** Extract all sections at a given level, returning heading → content map. */
143
+ function extractAllSections(body: string, level: number = 2): Map<string, string> {
144
+ const prefix = '#'.repeat(level) + ' ';
145
+ const regex = new RegExp(`^${prefix}(.+)$`, 'gm');
146
+ const sections = new Map<string, string>();
147
+ const matches = [...body.matchAll(regex)];
148
+
149
+ for (let i = 0; i < matches.length; i++) {
150
+ const heading = matches[i][1].trim();
151
+ const start = matches[i].index! + matches[i][0].length;
152
+ const end = i + 1 < matches.length ? matches[i + 1].index! : body.length;
153
+ sections.set(heading, body.slice(start, end).trim());
154
+ }
155
+
156
+ return sections;
157
+ }
158
+
159
+ function escapeRegex(s: string): string {
160
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
161
+ }
162
+
163
+ /** Parse bullet list items from a text block. */
164
+ function parseBullets(text: string): string[] {
165
+ return text.split('\n')
166
+ .map(l => l.replace(/^\s*[-*]\s+/, '').trim())
167
+ .filter(l => l.length > 0 && !l.startsWith('#'));
168
+ }
169
+
170
+ /** Extract key: value from bold-prefixed lines like "**Key:** Value" */
171
+ function extractBoldField(text: string, key: string): string | null {
172
+ const regex = new RegExp(`^\\*\\*${escapeRegex(key)}:\\*\\*\\s*(.+)$`, 'm');
173
+ const match = regex.exec(text);
174
+ return match ? match[1].trim() : null;
175
+ }
176
+
177
+ // ─── Roadmap Parser ────────────────────────────────────────────────────────
178
+
179
+ export function parseRoadmap(content: string): Roadmap {
180
+ const lines = content.split('\n');
181
+
182
+ const h1 = lines.find(l => l.startsWith('# '));
183
+ const title = h1 ? h1.slice(2).trim() : '';
184
+ const vision = extractBoldField(content, 'Vision') || '';
185
+
186
+ const scSection = extractSection(content, 'Success Criteria', 2) ||
187
+ (() => {
188
+ const idx = content.indexOf('**Success Criteria:**');
189
+ if (idx === -1) return '';
190
+ const rest = content.slice(idx);
191
+ const nextSection = rest.indexOf('\n---');
192
+ const block = rest.slice(0, nextSection === -1 ? undefined : nextSection);
193
+ const firstNewline = block.indexOf('\n');
194
+ return firstNewline === -1 ? '' : block.slice(firstNewline + 1);
195
+ })();
196
+ const successCriteria = scSection ? parseBullets(scSection) : [];
197
+
198
+ // Slices
199
+ const slicesSection = extractSection(content, 'Slices');
200
+ const slices: RoadmapSliceEntry[] = [];
201
+
202
+ if (slicesSection) {
203
+ const checkboxItems = slicesSection.split('\n');
204
+ let currentSlice: RoadmapSliceEntry | null = null;
205
+
206
+ for (const line of checkboxItems) {
207
+ const cbMatch = line.match(/^-\s+\[([ xX])\]\s+\*\*(\w+):\s+(.+?)\*\*\s*(.*)/);
208
+ if (cbMatch) {
209
+ if (currentSlice) slices.push(currentSlice);
210
+
211
+ const done = cbMatch[1].toLowerCase() === 'x';
212
+ const id = cbMatch[2];
213
+ const sliceTitle = cbMatch[3];
214
+ const rest = cbMatch[4];
215
+
216
+ const riskMatch = rest.match(/`risk:(\w+)`/);
217
+ const risk = (riskMatch ? riskMatch[1] : 'low') as RiskLevel;
218
+
219
+ const depsMatch = rest.match(/`depends:\[([^\]]*)\]`/);
220
+ const depends = depsMatch && depsMatch[1].trim()
221
+ ? depsMatch[1].split(',').map(s => s.trim())
222
+ : [];
223
+
224
+ currentSlice = { id, title: sliceTitle, risk, depends, done, demo: '' };
225
+ } else if (currentSlice && line.trim().startsWith('>')) {
226
+ const demoText = line.trim().replace(/^>\s*/, '').replace(/^After this:\s*/i, '');
227
+ currentSlice.demo = demoText;
228
+ }
229
+ }
230
+ if (currentSlice) slices.push(currentSlice);
231
+ }
232
+
233
+ // Boundary map
234
+ const boundaryMap: BoundaryMapEntry[] = [];
235
+ const bmSection = extractSection(content, 'Boundary Map');
236
+
237
+ if (bmSection) {
238
+ const h3Sections = extractAllSections(bmSection, 3);
239
+ for (const [heading, sectionContent] of h3Sections) {
240
+ const arrowMatch = heading.match(/^(\S+)\s*→\s*(\S+)/);
241
+ if (!arrowMatch) continue;
242
+
243
+ const fromSlice = arrowMatch[1];
244
+ const toSlice = arrowMatch[2];
245
+
246
+ let produces = '';
247
+ let consumes = '';
248
+
249
+ const prodMatch = sectionContent.match(/^Produces:\s*\n([\s\S]*?)(?=^Consumes|$)/m);
250
+ if (prodMatch) produces = prodMatch[1].trim();
251
+
252
+ const consMatch = sectionContent.match(/^Consumes[^:]*:\s*\n?([\s\S]*?)$/m);
253
+ if (consMatch) consumes = consMatch[1].trim();
254
+ if (!consumes) {
255
+ const singleCons = sectionContent.match(/^Consumes[^:]*:\s*(.+)$/m);
256
+ if (singleCons) consumes = singleCons[1].trim();
257
+ }
258
+
259
+ boundaryMap.push({ fromSlice, toSlice, produces, consumes });
260
+ }
261
+ }
262
+
263
+ return { title, vision, successCriteria, slices, boundaryMap };
264
+ }
265
+
266
+ // ─── Slice Plan Parser ─────────────────────────────────────────────────────
267
+
268
+ export function parsePlan(content: string): SlicePlan {
269
+ const lines = content.split('\n');
270
+
271
+ const h1 = lines.find(l => l.startsWith('# '));
272
+ let id = '';
273
+ let title = '';
274
+ if (h1) {
275
+ const match = h1.match(/^#\s+(\w+):\s+(.+)/);
276
+ if (match) {
277
+ id = match[1];
278
+ title = match[2].trim();
279
+ } else {
280
+ title = h1.slice(2).trim();
281
+ }
282
+ }
283
+
284
+ const goal = extractBoldField(content, 'Goal') || '';
285
+ const demo = extractBoldField(content, 'Demo') || '';
286
+
287
+ const mhSection = extractSection(content, 'Must-Haves');
288
+ const mustHaves = mhSection ? parseBullets(mhSection) : [];
289
+
290
+ const tasksSection = extractSection(content, 'Tasks');
291
+ const tasks: TaskPlanEntry[] = [];
292
+
293
+ if (tasksSection) {
294
+ const taskLines = tasksSection.split('\n');
295
+ let currentTask: TaskPlanEntry | null = null;
296
+
297
+ for (const line of taskLines) {
298
+ const cbMatch = line.match(/^-\s+\[([ xX])\]\s+\*\*(\w+):\s+(.+?)\*\*\s*(.*)/);
299
+ if (cbMatch) {
300
+ if (currentTask) tasks.push(currentTask);
301
+
302
+ const rest = cbMatch[4] || '';
303
+ const estMatch = rest.match(/`est:([^`]+)`/);
304
+ const estimate = estMatch ? estMatch[1] : '';
305
+
306
+ currentTask = {
307
+ id: cbMatch[2],
308
+ title: cbMatch[3],
309
+ description: '',
310
+ done: cbMatch[1].toLowerCase() === 'x',
311
+ estimate,
312
+ };
313
+ } else if (currentTask && line.match(/^\s*-\s+Files:\s*(.*)/)) {
314
+ const filesMatch = line.match(/^\s*-\s+Files:\s*(.*)/);
315
+ if (filesMatch) {
316
+ currentTask.files = filesMatch[1]
317
+ .split(',')
318
+ .map(f => f.replace(/`/g, '').trim())
319
+ .filter(f => f.length > 0);
320
+ }
321
+ } else if (currentTask && line.match(/^\s*-\s+Verify:\s*(.*)/)) {
322
+ const verifyMatch = line.match(/^\s*-\s+Verify:\s*(.*)/);
323
+ if (verifyMatch) {
324
+ currentTask.verify = verifyMatch[1].trim();
325
+ }
326
+ } else if (currentTask && line.trim() && !line.startsWith('#')) {
327
+ const desc = line.trim();
328
+ if (desc) {
329
+ currentTask.description = currentTask.description
330
+ ? currentTask.description + ' ' + desc
331
+ : desc;
332
+ }
333
+ }
334
+ }
335
+ if (currentTask) tasks.push(currentTask);
336
+ }
337
+
338
+ const filesSection = extractSection(content, 'Files Likely Touched');
339
+ const filesLikelyTouched = filesSection ? parseBullets(filesSection) : [];
340
+
341
+ return { id, title, goal, demo, mustHaves, tasks, filesLikelyTouched };
342
+ }
343
+
344
+ // ─── Summary Parser ────────────────────────────────────────────────────────
345
+
346
+ export function parseSummary(content: string): Summary {
347
+ const [fmLines, body] = splitFrontmatter(content);
348
+
349
+ const fm = fmLines ? parseFrontmatterMap(fmLines) : {};
350
+ const frontmatter: SummaryFrontmatter = {
351
+ id: (fm.id as string) || '',
352
+ parent: (fm.parent as string) || '',
353
+ milestone: (fm.milestone as string) || '',
354
+ provides: (fm.provides as string[]) || [],
355
+ requires: ((fm.requires as Array<Record<string, string>>) || []).map(r => ({
356
+ slice: r.slice || '',
357
+ provides: r.provides || '',
358
+ })),
359
+ affects: (fm.affects as string[]) || [],
360
+ key_files: (fm.key_files as string[]) || [],
361
+ key_decisions: (fm.key_decisions as string[]) || [],
362
+ patterns_established: (fm.patterns_established as string[]) || [],
363
+ drill_down_paths: (fm.drill_down_paths as string[]) || [],
364
+ observability_surfaces: (fm.observability_surfaces as string[]) || [],
365
+ duration: (fm.duration as string) || '',
366
+ verification_result: (fm.verification_result as string) || 'untested',
367
+ completed_at: (fm.completed_at as string) || '',
368
+ blocker_discovered: fm.blocker_discovered === 'true' || fm.blocker_discovered === true,
369
+ };
370
+
371
+ const bodyLines = body.split('\n');
372
+ const h1 = bodyLines.find(l => l.startsWith('# '));
373
+ const title = h1 ? h1.slice(2).trim() : '';
374
+
375
+ const h1Idx = bodyLines.indexOf(h1 || '');
376
+ let oneLiner = '';
377
+ for (let i = h1Idx + 1; i < bodyLines.length; i++) {
378
+ const line = bodyLines[i].trim();
379
+ if (!line) continue;
380
+ if (line.startsWith('**') && line.endsWith('**')) {
381
+ oneLiner = line.slice(2, -2);
382
+ }
383
+ break;
384
+ }
385
+
386
+ const whatHappened = extractSection(body, 'What Happened') || '';
387
+ const deviations = extractSection(body, 'Deviations') || '';
388
+
389
+ const filesSection = extractSection(body, 'Files Created/Modified') || extractSection(body, 'Files Modified');
390
+ const filesModified: FileModified[] = [];
391
+ if (filesSection) {
392
+ for (const line of filesSection.split('\n')) {
393
+ const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
394
+ if (!trimmed || trimmed.startsWith('#')) continue;
395
+
396
+ const fileMatch = trimmed.match(/^`([^`]+)`\s*[—–-]\s*(.+)/);
397
+ if (fileMatch) {
398
+ filesModified.push({ path: fileMatch[1], description: fileMatch[2].trim() });
399
+ }
400
+ }
401
+ }
402
+
403
+ return { frontmatter, title, oneLiner, whatHappened, deviations, filesModified };
404
+ }
405
+
406
+ // ─── Continue Parser ───────────────────────────────────────────────────────
407
+
408
+ export function parseContinue(content: string): Continue {
409
+ const [fmLines, body] = splitFrontmatter(content);
410
+
411
+ const fm = fmLines ? parseFrontmatterMap(fmLines) : {};
412
+ const frontmatter: ContinueFrontmatter = {
413
+ milestone: (fm.milestone as string) || '',
414
+ slice: (fm.slice as string) || '',
415
+ task: (fm.task as string) || '',
416
+ step: typeof fm.step === 'string' ? parseInt(fm.step) : (fm.step as number) || 0,
417
+ totalSteps: typeof fm.total_steps === 'string' ? parseInt(fm.total_steps) : (fm.total_steps as number) ||
418
+ (typeof fm.totalSteps === 'string' ? parseInt(fm.totalSteps) : (fm.totalSteps as number) || 0),
419
+ status: ((fm.status as string) || 'in_progress') as ContinueStatus,
420
+ savedAt: (fm.saved_at as string) || (fm.savedAt as string) || '',
421
+ };
422
+
423
+ const completedWork = extractSection(body, 'Completed Work') || '';
424
+ const remainingWork = extractSection(body, 'Remaining Work') || '';
425
+ const decisions = extractSection(body, 'Decisions Made') || '';
426
+ const context = extractSection(body, 'Context') || '';
427
+ const nextAction = extractSection(body, 'Next Action') || '';
428
+
429
+ return { frontmatter, completedWork, remainingWork, decisions, context, nextAction };
430
+ }
431
+
432
+ // ─── Continue Formatter ────────────────────────────────────────────────────
433
+
434
+ function formatFrontmatter(data: Record<string, unknown>): string {
435
+ const lines: string[] = ['---'];
436
+
437
+ for (const [key, value] of Object.entries(data)) {
438
+ if (value === undefined || value === null) continue;
439
+
440
+ if (Array.isArray(value)) {
441
+ if (value.length === 0) {
442
+ lines.push(`${key}: []`);
443
+ } else if (typeof value[0] === 'object' && value[0] !== null) {
444
+ lines.push(`${key}:`);
445
+ for (const obj of value) {
446
+ const entries = Object.entries(obj as Record<string, unknown>);
447
+ if (entries.length > 0) {
448
+ lines.push(` - ${entries[0][0]}: ${entries[0][1]}`);
449
+ for (let i = 1; i < entries.length; i++) {
450
+ lines.push(` ${entries[i][0]}: ${entries[i][1]}`);
451
+ }
452
+ }
453
+ }
454
+ } else {
455
+ lines.push(`${key}:`);
456
+ for (const item of value) {
457
+ lines.push(` - ${item}`);
458
+ }
459
+ }
460
+ } else {
461
+ lines.push(`${key}: ${value}`);
462
+ }
463
+ }
464
+
465
+ lines.push('---');
466
+ return lines.join('\n');
467
+ }
468
+
469
+ export function formatContinue(cont: Continue): string {
470
+ const fm = cont.frontmatter;
471
+ const fmData: Record<string, unknown> = {
472
+ milestone: fm.milestone,
473
+ slice: fm.slice,
474
+ task: fm.task,
475
+ step: fm.step,
476
+ total_steps: fm.totalSteps,
477
+ status: fm.status,
478
+ saved_at: fm.savedAt,
479
+ };
480
+
481
+ const lines: string[] = [];
482
+ lines.push(formatFrontmatter(fmData));
483
+ lines.push('');
484
+ lines.push('## Completed Work');
485
+ lines.push(cont.completedWork);
486
+ lines.push('');
487
+ lines.push('## Remaining Work');
488
+ lines.push(cont.remainingWork);
489
+ lines.push('');
490
+ lines.push('## Decisions Made');
491
+ lines.push(cont.decisions);
492
+ lines.push('');
493
+ lines.push('## Context');
494
+ lines.push(cont.context);
495
+ lines.push('');
496
+ lines.push('## Next Action');
497
+ lines.push(cont.nextAction);
498
+
499
+ return lines.join('\n');
500
+ }
501
+
502
+ // ─── File I/O ──────────────────────────────────────────────────────────────
503
+
504
+ /**
505
+ * Load a file from disk. Returns content string or null if file doesn't exist.
506
+ */
507
+ export async function loadFile(path: string): Promise<string | null> {
508
+ try {
509
+ return await fs.readFile(path, 'utf-8');
510
+ } catch (err: unknown) {
511
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
512
+ throw err;
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Save content to a file atomically (write to temp, then rename).
518
+ * Creates parent directories if needed.
519
+ */
520
+ export async function saveFile(path: string, content: string): Promise<void> {
521
+ const dir = dirname(path);
522
+ await fs.mkdir(dir, { recursive: true });
523
+
524
+ const tmpPath = path + '.tmp';
525
+ await fs.writeFile(tmpPath, content, 'utf-8');
526
+ await fs.rename(tmpPath, path);
527
+ }
528
+
529
+ export function parseRequirementCounts(content: string | null): RequirementCounts {
530
+ const counts: RequirementCounts = {
531
+ active: 0,
532
+ validated: 0,
533
+ deferred: 0,
534
+ outOfScope: 0,
535
+ blocked: 0,
536
+ total: 0,
537
+ };
538
+
539
+ if (!content) return counts;
540
+
541
+ const sections = [
542
+ { key: 'active', heading: 'Active' },
543
+ { key: 'validated', heading: 'Validated' },
544
+ { key: 'deferred', heading: 'Deferred' },
545
+ { key: 'outOfScope', heading: 'Out of Scope' },
546
+ ] as const;
547
+
548
+ for (const section of sections) {
549
+ const text = extractSection(content, section.heading, 2);
550
+ if (!text) continue;
551
+ const matches = text.match(/^###\s+R\d+\s+—/gm);
552
+ counts[section.key] = matches ? matches.length : 0;
553
+ }
554
+
555
+ const blockedMatches = content.match(/^-\s+Status:\s+blocked\s*$/gim);
556
+ counts.blocked = blockedMatches ? blockedMatches.length : 0;
557
+ counts.total = counts.active + counts.validated + counts.deferred + counts.outOfScope;
558
+ return counts;
559
+ }
560
+
561
+ // ─── Task Plan Must-Haves Parser ───────────────────────────────────────────
562
+
563
+ /**
564
+ * Parse must-have items from a task plan's `## Must-Haves` section.
565
+ * Returns structured items with checkbox state. Handles YAML frontmatter,
566
+ * all common checkbox variants (`[ ]`, `[x]`, `[X]`), plain bullets (no checkbox),
567
+ * and indented variants. Returns empty array when the section is missing or empty.
568
+ */
569
+ export function parseTaskPlanMustHaves(content: string): Array<{ text: string; checked: boolean }> {
570
+ const [, body] = splitFrontmatter(content);
571
+ const sectionText = extractSection(body, 'Must-Haves');
572
+ if (!sectionText) return [];
573
+
574
+ const bullets = parseBullets(sectionText);
575
+ if (bullets.length === 0) return [];
576
+
577
+ return bullets.map(line => {
578
+ const cbMatch = line.match(/^\[([xX ])\]\s+(.+)/);
579
+ if (cbMatch) {
580
+ return {
581
+ text: cbMatch[2].trim(),
582
+ checked: cbMatch[1].toLowerCase() === 'x',
583
+ };
584
+ }
585
+ // No checkbox — treat as unchecked with full line as text
586
+ return { text: line.trim(), checked: false };
587
+ });
588
+ }
589
+
590
+ // ─── Must-Have Summary Matching ────────────────────────────────────────────
591
+
592
+ /** Common short words to exclude from substring matching. */
593
+ const COMMON_WORDS = new Set([
594
+ 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'her',
595
+ 'was', 'one', 'our', 'out', 'has', 'its', 'let', 'say', 'she', 'too', 'use',
596
+ 'with', 'have', 'from', 'this', 'that', 'they', 'been', 'each', 'when', 'will',
597
+ 'does', 'into', 'also', 'than', 'them', 'then', 'some', 'what', 'only', 'just',
598
+ 'more', 'make', 'like', 'made', 'over', 'such', 'take', 'most', 'very', 'must',
599
+ 'file', 'test', 'tests', 'task', 'new', 'add', 'added', 'existing',
600
+ ]);
601
+
602
+ /**
603
+ * Count how many must-have items are mentioned in a summary.
604
+ *
605
+ * Matching heuristic per must-have:
606
+ * 1. Extract all backtick-enclosed code tokens (e.g. `inspectFoo`).
607
+ * If any code token appears case-insensitively in the summary, count as mentioned.
608
+ * 2. If no code tokens exist, check if any significant word (≥4 chars, not a common word)
609
+ * from the must-have text appears in the summary (case-insensitive).
610
+ *
611
+ * Returns the count of must-haves that had at least one match.
612
+ */
613
+ export function countMustHavesMentionedInSummary(
614
+ mustHaves: Array<{ text: string; checked: boolean }>,
615
+ summaryContent: string,
616
+ ): number {
617
+ if (!summaryContent || mustHaves.length === 0) return 0;
618
+
619
+ const summaryLower = summaryContent.toLowerCase();
620
+ let count = 0;
621
+
622
+ for (const mh of mustHaves) {
623
+ // Extract backtick-enclosed code tokens
624
+ const codeTokens: string[] = [];
625
+ const codeRegex = /`([^`]+)`/g;
626
+ let match: RegExpExecArray | null;
627
+ while ((match = codeRegex.exec(mh.text)) !== null) {
628
+ codeTokens.push(match[1]);
629
+ }
630
+
631
+ if (codeTokens.length > 0) {
632
+ // Strategy 1: any code token found in summary (case-insensitive)
633
+ const found = codeTokens.some(token => summaryLower.includes(token.toLowerCase()));
634
+ if (found) count++;
635
+ } else {
636
+ // Strategy 2: significant substring matching
637
+ // Split into words, keep words ≥4 chars that aren't common
638
+ const words = mh.text.replace(/[^\w\s]/g, ' ').split(/\s+/).filter(w =>
639
+ w.length >= 4 && !COMMON_WORDS.has(w.toLowerCase())
640
+ );
641
+ const found = words.some(word => summaryLower.includes(word.toLowerCase()));
642
+ if (found) count++;
643
+ }
644
+ }
645
+
646
+ return count;
647
+ }
648
+
649
+ // ─── UAT Type Extractor ────────────────────────────────────────────────────
650
+
651
+ /**
652
+ * The four UAT classification types recognised by Kata auto-mode.
653
+ * `undefined` is returned (not this union) when no type can be determined.
654
+ */
655
+ export type UatType = 'artifact-driven' | 'live-runtime' | 'human-experience' | 'mixed';
656
+
657
+ /**
658
+ * Extract the UAT type from a UAT file's raw content.
659
+ *
660
+ * UAT files have no YAML frontmatter — pass raw file content directly.
661
+ * Classification is leading-keyword-only: e.g. `mixed (artifact-driven + live-runtime)` → `'mixed'`.
662
+ *
663
+ * Returns `undefined` when:
664
+ * - the `## UAT Type` section is absent
665
+ * - no `UAT mode:` bullet is found in the section
666
+ * - the value does not start with a recognised keyword
667
+ */
668
+ export function extractUatType(content: string): UatType | undefined {
669
+ const sectionText = extractSection(content, 'UAT Type');
670
+ if (!sectionText) return undefined;
671
+
672
+ const bullets = parseBullets(sectionText);
673
+ const modeBullet = bullets.find(b => b.startsWith('UAT mode:'));
674
+ if (!modeBullet) return undefined;
675
+
676
+ const rawValue = modeBullet.slice('UAT mode:'.length).trim().toLowerCase();
677
+
678
+ if (rawValue.startsWith('artifact-driven')) return 'artifact-driven';
679
+ if (rawValue.startsWith('live-runtime')) return 'live-runtime';
680
+ if (rawValue.startsWith('human-experience')) return 'human-experience';
681
+ if (rawValue.startsWith('mixed')) return 'mixed';
682
+
683
+ return undefined;
684
+ }
685
+
686
+ /**
687
+ * Extract the `depends_on` list from M00x-CONTEXT.md YAML frontmatter.
688
+ * Returns [] when: content is null, no frontmatter block, field absent, or field is empty.
689
+ * Normalizes each dep ID to uppercase (e.g. 'm001' → 'M001').
690
+ */
691
+ export function parseContextDependsOn(content: string | null): string[] {
692
+ if (!content) return [];
693
+ const [fmLines] = splitFrontmatter(content);
694
+ if (!fmLines) return [];
695
+ const fm = parseFrontmatterMap(fmLines);
696
+ const raw = fm['depends_on'];
697
+ if (!Array.isArray(raw) || raw.length === 0) return [];
698
+ return (raw as string[]).map(s => String(s).toUpperCase().trim()).filter(Boolean);
699
+ }
700
+
701
+ /**
702
+ * Inline the prior milestone's SUMMARY.md as context for the current milestone's planning prompt.
703
+ * Returns null when: (1) `mid` is the first milestone, (2) prior milestone has no SUMMARY file.
704
+ *
705
+ * Scans the milestones directory using the same readdirSync + sort + M\d+ match pattern
706
+ * as findMilestoneIds in state.ts.
707
+ */
708
+ export async function inlinePriorMilestoneSummary(mid: string, base: string): Promise<string | null> {
709
+ const dir = milestonesDir(base);
710
+ let sorted: string[];
711
+ try {
712
+ sorted = readdirSync(dir, { withFileTypes: true })
713
+ .filter(d => d.isDirectory())
714
+ .map(d => {
715
+ const match = d.name.match(/^(M\d+)/);
716
+ return match ? match[1] : d.name;
717
+ })
718
+ .sort();
719
+ } catch {
720
+ return null;
721
+ }
722
+ const idx = sorted.indexOf(mid);
723
+ if (idx <= 0) return null;
724
+ const prevMid = sorted[idx - 1];
725
+ const absPath = resolveMilestoneFile(base, prevMid, "SUMMARY");
726
+ const relPath = relMilestoneFile(base, prevMid, "SUMMARY");
727
+ const content = absPath ? await loadFile(absPath) : null;
728
+ if (!content) return null;
729
+ return `### Prior Milestone Summary\nSource: \`${relPath}\`\n\n${content.trim()}`;
730
+ }