@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,317 @@
1
+ import {
2
+ mkdtempSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { join, dirname } from "node:path";
9
+ import { tmpdir } from "node:os";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ // loadPrompt reads from ~/.kata-cli/agent/extensions/kata/prompts/ (main checkout).
13
+ // In a worktree the file may not exist there yet, so we resolve prompts
14
+ // relative to this test file's location (the worktree copy).
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const worktreePromptsDir = join(__dirname, "..", "prompts");
17
+
18
+ let passed = 0;
19
+ let failed = 0;
20
+
21
+ function assert(condition: boolean, message: string): void {
22
+ if (condition) {
23
+ passed++;
24
+ } else {
25
+ failed++;
26
+ console.error(` FAIL: ${message}`);
27
+ }
28
+ }
29
+
30
+ function assertEq<T>(actual: T, expected: T, message: string): void {
31
+ if (JSON.stringify(actual) === JSON.stringify(expected)) {
32
+ passed++;
33
+ } else {
34
+ failed++;
35
+ console.error(
36
+ ` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
37
+ );
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Load a prompt template from the worktree prompts directory
43
+ * and apply variable substitution (mirrors loadPrompt logic).
44
+ */
45
+ function loadPromptFromWorktree(
46
+ name: string,
47
+ vars: Record<string, string> = {},
48
+ ): string {
49
+ const path = join(worktreePromptsDir, `${name}.md`);
50
+ let content = readFileSync(path, "utf-8");
51
+ for (const [key, value] of Object.entries(vars)) {
52
+ content = content.replaceAll(`{{${key}}}`, value);
53
+ }
54
+ return content.trim();
55
+ }
56
+
57
+ // ─── Fixture Helpers ───────────────────────────────────────────────────────
58
+
59
+ function createFixtureBase(): string {
60
+ const base = mkdtempSync(join(tmpdir(), "kata-complete-ms-test-"));
61
+ mkdirSync(join(base, ".kata", "milestones"), { recursive: true });
62
+ return base;
63
+ }
64
+
65
+ function writeRoadmap(base: string, mid: string, content: string): void {
66
+ const dir = join(base, ".kata", "milestones", mid);
67
+ mkdirSync(dir, { recursive: true });
68
+ writeFileSync(join(dir, `${mid}-ROADMAP.md`), content);
69
+ }
70
+
71
+ function writeMilestoneSummary(
72
+ base: string,
73
+ mid: string,
74
+ content: string,
75
+ ): void {
76
+ const dir = join(base, ".kata", "milestones", mid);
77
+ mkdirSync(dir, { recursive: true });
78
+ writeFileSync(join(dir, `${mid}-SUMMARY.md`), content);
79
+ }
80
+
81
+ function cleanup(base: string): void {
82
+ rmSync(base, { recursive: true, force: true });
83
+ }
84
+
85
+ // ═══════════════════════════════════════════════════════════════════════════
86
+ // Tests
87
+ // ═══════════════════════════════════════════════════════════════════════════
88
+
89
+ async function main(): Promise<void> {
90
+ // ─── Prompt Template Loading ───────────────────────────────────────────
91
+ console.log("\n=== complete-milestone prompt template exists ===");
92
+ {
93
+ let result: string;
94
+ let threw = false;
95
+ try {
96
+ result = loadPromptFromWorktree("complete-milestone", {
97
+ milestoneId: "M001",
98
+ milestoneTitle: "Test Milestone",
99
+ roadmapPath: ".kata/milestones/M001/M001-ROADMAP.md",
100
+ inlinedContext: "test context block",
101
+ });
102
+ } catch (err) {
103
+ threw = true;
104
+ result = "";
105
+ console.error(` ERROR: loadPrompt threw: ${err}`);
106
+ }
107
+
108
+ assert(!threw, "loadPrompt does not throw for complete-milestone");
109
+ assert(
110
+ typeof result === "string" && result.length > 0,
111
+ "loadPrompt returns a non-empty string",
112
+ );
113
+ }
114
+
115
+ // ─── Variable Substitution ─────────────────────────────────────────────
116
+ console.log("\n=== prompt variable substitution ===");
117
+ {
118
+ const prompt = loadPromptFromWorktree("complete-milestone", {
119
+ milestoneId: "M001",
120
+ milestoneTitle: "Integration Feature",
121
+ roadmapPath: ".kata/milestones/M001/M001-ROADMAP.md",
122
+ inlinedContext: "--- inlined slice summaries and context ---",
123
+ });
124
+
125
+ assert(prompt.includes("M001"), "prompt contains milestoneId 'M001'");
126
+ assert(
127
+ prompt.includes("Integration Feature"),
128
+ "prompt contains milestoneTitle",
129
+ );
130
+ assert(
131
+ prompt.includes(".kata/milestones/M001/M001-ROADMAP.md"),
132
+ "prompt contains roadmapPath",
133
+ );
134
+ assert(
135
+ prompt.includes("--- inlined slice summaries and context ---"),
136
+ "prompt contains inlinedContext",
137
+ );
138
+ assert(
139
+ !prompt.includes("{{milestoneId}}"),
140
+ "no un-substituted {{milestoneId}}",
141
+ );
142
+ assert(
143
+ !prompt.includes("{{milestoneTitle}}"),
144
+ "no un-substituted {{milestoneTitle}}",
145
+ );
146
+ assert(
147
+ !prompt.includes("{{roadmapPath}}"),
148
+ "no un-substituted {{roadmapPath}}",
149
+ );
150
+ assert(
151
+ !prompt.includes("{{inlinedContext}}"),
152
+ "no un-substituted {{inlinedContext}}",
153
+ );
154
+ }
155
+
156
+ // ─── Prompt Content Integrity ──────────────────────────────────────────
157
+ console.log("\n=== prompt content integrity ===");
158
+ {
159
+ const prompt = loadPromptFromWorktree("complete-milestone", {
160
+ milestoneId: "M002",
161
+ milestoneTitle: "Completion Workflow",
162
+ roadmapPath: ".kata/milestones/M002/M002-ROADMAP.md",
163
+ inlinedContext: "context",
164
+ });
165
+
166
+ assert(
167
+ prompt.includes("Complete Milestone"),
168
+ "prompt contains 'Complete Milestone' heading",
169
+ );
170
+ assert(
171
+ prompt.includes("success criter") || prompt.includes("success criteria"),
172
+ "prompt mentions success criteria verification",
173
+ );
174
+ assert(
175
+ prompt.includes("milestone-summary") ||
176
+ prompt.includes("milestoneSummary"),
177
+ "prompt references milestone summary artifact",
178
+ );
179
+ assert(
180
+ prompt.includes("Milestone M002 complete"),
181
+ "prompt contains completion sentinel for M002",
182
+ );
183
+ }
184
+
185
+ // ─── diagnoseExpectedArtifact behavior ─────────────────────────────────
186
+ // Since diagnoseExpectedArtifact is not exported from auto.ts, we test
187
+ // the same logic by reimplementing the switch case for complete-milestone
188
+ // and verifying against known path patterns.
189
+ console.log(
190
+ "\n=== diagnoseExpectedArtifact logic for complete-milestone ===",
191
+ );
192
+ {
193
+ // Import the path helpers used by diagnoseExpectedArtifact
194
+ const { relMilestoneFile } = await import("../paths.ts");
195
+
196
+ // Simulate diagnoseExpectedArtifact("complete-milestone", "M001", base) logic
197
+ const base = createFixtureBase();
198
+ try {
199
+ writeRoadmap(
200
+ base,
201
+ "M001",
202
+ `# M001\n\n## Slices\n- [x] **S01: Done** \`risk:low\` \`depends:[]\`\n > After this: done\n`,
203
+ );
204
+
205
+ const unitType = "complete-milestone";
206
+ const unitId = "M001";
207
+ const parts = unitId.split("/");
208
+ const mid = parts[0]!;
209
+
210
+ // This is the exact logic from diagnoseExpectedArtifact for "complete-milestone"
211
+ const result = `${relMilestoneFile(base, mid, "SUMMARY")} (milestone summary)`;
212
+
213
+ assert(typeof result === "string", "diagnose returns a string");
214
+ assert(result.includes("SUMMARY"), "diagnose result mentions SUMMARY");
215
+ assert(
216
+ result.includes("milestone"),
217
+ "diagnose result mentions milestone",
218
+ );
219
+ assert(
220
+ result.includes("M001"),
221
+ "diagnose result includes the milestone ID",
222
+ );
223
+ } finally {
224
+ cleanup(base);
225
+ }
226
+ }
227
+
228
+ // ─── deriveState integration: completing-milestone dispatches correctly ─
229
+ console.log("\n=== deriveState completing-milestone integration ===");
230
+ {
231
+ const { deriveState, isMilestoneComplete } = await import("../state.ts");
232
+ const { parseRoadmap } = await import("../files.ts");
233
+
234
+ const base = createFixtureBase();
235
+ try {
236
+ writeRoadmap(
237
+ base,
238
+ "M001",
239
+ `# M001: Integration Test
240
+
241
+ **Vision:** Test completing-milestone flow.
242
+
243
+ ## Slices
244
+
245
+ - [x] **S01: Slice One** \`risk:low\` \`depends:[]\`
246
+ > After this: done.
247
+
248
+ - [x] **S02: Slice Two** \`risk:low\` \`depends:[S01]\`
249
+ > After this: done.
250
+ `,
251
+ );
252
+
253
+ // Verify isMilestoneComplete returns true
254
+ const { loadFile } = await import("../files.ts");
255
+ const roadmapPath = join(
256
+ base,
257
+ ".kata",
258
+ "milestones",
259
+ "M001",
260
+ "M001-ROADMAP.md",
261
+ );
262
+ const roadmapContent = await loadFile(roadmapPath);
263
+ const roadmap = parseRoadmap(roadmapContent!);
264
+ assert(
265
+ isMilestoneComplete(roadmap),
266
+ "isMilestoneComplete returns true when all slices are [x]",
267
+ );
268
+
269
+ // Verify deriveState returns completing-milestone phase
270
+ const state = await deriveState(base);
271
+ assertEq(
272
+ state.phase,
273
+ "completing-milestone",
274
+ "deriveState returns completing-milestone when all slices done, no summary",
275
+ );
276
+ assertEq(state.activeMilestone?.id, "M001", "active milestone is M001");
277
+ assertEq(
278
+ state.activeSlice,
279
+ null,
280
+ "no active slice in completing-milestone",
281
+ );
282
+
283
+ // Now add the summary and verify it transitions to complete
284
+ writeMilestoneSummary(base, "M001", "# M001 Summary\n\nDone.");
285
+ const stateAfter = await deriveState(base);
286
+ assertEq(
287
+ stateAfter.phase,
288
+ "complete",
289
+ "deriveState returns complete after summary exists",
290
+ );
291
+ assertEq(
292
+ stateAfter.registry[0]?.status,
293
+ "complete",
294
+ "registry shows complete status",
295
+ );
296
+ } finally {
297
+ cleanup(base);
298
+ }
299
+ }
300
+
301
+ // ═════════════════════════════════════════════════════════════════════════
302
+ // Results
303
+ // ═════════════════════════════════════════════════════════════════════════
304
+
305
+ console.log(`\n${"=".repeat(40)}`);
306
+ console.log(`Results: ${passed} passed, ${failed} failed`);
307
+ if (failed > 0) {
308
+ process.exit(1);
309
+ } else {
310
+ console.log("All tests passed ✓");
311
+ }
312
+ }
313
+
314
+ main().catch((error) => {
315
+ console.error(error);
316
+ process.exit(1);
317
+ });
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Contract tests for `formatCostProjection`.
3
+ * Tests the pure function — no file I/O, no extension context.
4
+ *
5
+ * This test intentionally fails at import time (or on first assertion)
6
+ * because `formatCostProjection` does not yet exist in metrics.ts.
7
+ * That failure confirms the test runs against real code. (T01 state)
8
+ */
9
+
10
+ import {
11
+ type SliceAggregate,
12
+ formatCostProjection,
13
+ } from "../metrics.js";
14
+
15
+ // ─── Test helpers ─────────────────────────────────────────────────────────────
16
+
17
+ function makeSliceAggregate(sliceId: string, cost: number): SliceAggregate {
18
+ return {
19
+ sliceId,
20
+ units: 1,
21
+ tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
22
+ cost,
23
+ duration: 1000,
24
+ };
25
+ }
26
+
27
+ let passed = 0;
28
+ let failed = 0;
29
+
30
+ function assert(condition: boolean, message: string): void {
31
+ if (condition) {
32
+ passed++;
33
+ } else {
34
+ failed++;
35
+ console.error(` FAIL: ${message}`);
36
+ }
37
+ }
38
+
39
+ function assertEq<T>(actual: T, expected: T, message: string): void {
40
+ if (actual === expected) {
41
+ passed++;
42
+ } else {
43
+ failed++;
44
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
45
+ }
46
+ }
47
+
48
+ // ─── formatCostProjection ─────────────────────────────────────────────────────
49
+
50
+ console.log("\n=== formatCostProjection ===");
51
+
52
+ // 1. Zero completed slices → empty result
53
+ {
54
+ const result = formatCostProjection([], 3);
55
+ assertEq(result.length, 0, "zero slices → empty array");
56
+ }
57
+
58
+ // 2. One slice → suppressed (need ≥2 to project reliably)
59
+ {
60
+ const result = formatCostProjection([makeSliceAggregate("M001/S01", 0.10)], 3);
61
+ assertEq(result.length, 0, "one slice → suppressed (no projection shown)");
62
+ }
63
+
64
+ // 3. Two slices → projection shown (result.length > 0)
65
+ {
66
+ const slices = [
67
+ makeSliceAggregate("M001/S01", 0.10),
68
+ makeSliceAggregate("M001/S02", 0.10),
69
+ ];
70
+ const result = formatCostProjection(slices, 5);
71
+ assert(result.length > 0, "two slices → projection shown");
72
+ }
73
+
74
+ // 4. Two-slice result: result[0] contains "$" (cost is formatted)
75
+ {
76
+ const slices = [
77
+ makeSliceAggregate("M001/S01", 0.10),
78
+ makeSliceAggregate("M001/S02", 0.10),
79
+ ];
80
+ const result = formatCostProjection(slices, 5);
81
+ assert(result.length > 0 && result[0].includes("$"), "projection line contains \"$\"");
82
+ }
83
+
84
+ // 5. Budget ceiling hit: total $0.20 >= ceiling $0.05 → line contains "ceiling"
85
+ {
86
+ const slices = [
87
+ makeSliceAggregate("M001/S01", 0.10),
88
+ makeSliceAggregate("M001/S02", 0.10),
89
+ ];
90
+ const result = formatCostProjection(slices, 5, 0.05);
91
+ const hasCeilingLine = result.some(
92
+ line => line.toLowerCase().includes("ceiling")
93
+ );
94
+ assert(hasCeilingLine, "ceiling warning appears when total ($0.20) >= ceiling ($0.05)");
95
+ }
96
+
97
+ // 6. Budget ceiling not hit: total $0.20 < ceiling $100.00 → no ceiling line
98
+ {
99
+ const slices = [
100
+ makeSliceAggregate("M001/S01", 0.10),
101
+ makeSliceAggregate("M001/S02", 0.10),
102
+ ];
103
+ const result = formatCostProjection(slices, 5, 100.00);
104
+ const hasCeilingLine = result.some(
105
+ line => line.toLowerCase().includes("ceiling")
106
+ );
107
+ assert(!hasCeilingLine, "no ceiling warning when total ($0.20) < ceiling ($100.00)");
108
+ }
109
+
110
+ // 7. No ceiling arg → no ceiling line
111
+ {
112
+ const slices = [
113
+ makeSliceAggregate("M001/S01", 0.10),
114
+ makeSliceAggregate("M001/S02", 0.10),
115
+ ];
116
+ const result = formatCostProjection(slices, 5);
117
+ const hasCeilingLine = result.some(
118
+ line => line.toLowerCase().includes("ceiling")
119
+ );
120
+ assert(!hasCeilingLine, "no ceiling warning when no ceiling is set");
121
+ }
122
+
123
+ // 8. Rounding: avg $0.10 × 5 remaining = $0.50 → result[0] contains "$0.50"
124
+ {
125
+ const slices = [
126
+ makeSliceAggregate("M001/S01", 0.10),
127
+ makeSliceAggregate("M001/S02", 0.10),
128
+ ];
129
+ const result = formatCostProjection(slices, 5);
130
+ const hasRoundedCost = result.some(line => line.includes("$0.50"));
131
+ assert(hasRoundedCost, "projected cost $0.50 (avg $0.10 × 5 remaining) appears in output");
132
+ }
133
+
134
+ // 9. Bare milestone entries excluded from average:
135
+ // makeSliceAggregate('M001', 5.00) has no "/" in sliceId → excluded from avg calc.
136
+ // Only M001/S01 ($0.10) and M001/S02 ($0.10) count → avg $0.10 × 3 remaining = $0.30
137
+ {
138
+ const slices = [
139
+ makeSliceAggregate("M001", 5.00), // bare milestone — must be excluded
140
+ makeSliceAggregate("M001/S01", 0.10),
141
+ makeSliceAggregate("M001/S02", 0.10),
142
+ ];
143
+ const result = formatCostProjection(slices, 3);
144
+ const hasCorrectProjection = result.some(line => line.includes("$0.30"));
145
+ assert(
146
+ hasCorrectProjection,
147
+ "bare milestone entry excluded from avg: projection shows $0.30 (avg $0.10 × 3), not $1.83 (including $5.00 entry)"
148
+ );
149
+ }
150
+
151
+ // ─── Summary ──────────────────────────────────────────────────────────────────
152
+
153
+ console.log(`\n${"=".repeat(40)}`);
154
+ console.log(`Results: ${passed} passed, ${failed} failed`);
155
+ if (failed > 0) {
156
+ console.error(`${failed} test(s) failed`);
157
+ process.exit(1);
158
+ } else {
159
+ console.log("All tests passed ✓");
160
+ }