@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,254 @@
1
+ /**
2
+ * Tests for Kata metrics disk I/O — init, snapshot, load/save cycle.
3
+ * Uses a temp directory to avoid touching real .kata/ state.
4
+ */
5
+
6
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { tmpdir } from "node:os";
9
+ import {
10
+ initMetrics,
11
+ resetMetrics,
12
+ getLedger,
13
+ snapshotUnitMetrics,
14
+ type MetricsLedger,
15
+ } from "../metrics.js";
16
+
17
+ let passed = 0;
18
+ let failed = 0;
19
+
20
+ function assert(condition: boolean, message: string): void {
21
+ if (condition) {
22
+ passed++;
23
+ } else {
24
+ failed++;
25
+ console.error(` FAIL: ${message}`);
26
+ }
27
+ }
28
+
29
+ function assertEq<T>(actual: T, expected: T, message: string): void {
30
+ if (JSON.stringify(actual) === JSON.stringify(expected)) {
31
+ passed++;
32
+ } else {
33
+ failed++;
34
+ console.error(
35
+ ` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
36
+ );
37
+ }
38
+ }
39
+
40
+ // ─── Setup ────────────────────────────────────────────────────────────────────
41
+
42
+ const tmpBase = mkdtempSync(join(tmpdir(), "kata-metrics-test-"));
43
+ mkdirSync(join(tmpBase, ".kata"), { recursive: true });
44
+
45
+ // Mock ExtensionContext with session entries
46
+ function mockCtx(messages: any[] = []): any {
47
+ const entries = messages.map((msg, i) => ({
48
+ type: "message",
49
+ id: `entry-${i}`,
50
+ parentId: i > 0 ? `entry-${i - 1}` : null,
51
+ timestamp: new Date().toISOString(),
52
+ message: msg,
53
+ }));
54
+ return {
55
+ sessionManager: {
56
+ getEntries: () => entries,
57
+ },
58
+ model: { id: "claude-sonnet-4-20250514" },
59
+ };
60
+ }
61
+
62
+ // ─── Tests ────────────────────────────────────────────────────────────────────
63
+
64
+ console.log("\n=== initMetrics / getLedger ===");
65
+
66
+ {
67
+ resetMetrics();
68
+ assert(getLedger() === null, "ledger null before init");
69
+
70
+ initMetrics(tmpBase);
71
+ const ledger = getLedger();
72
+ assert(ledger !== null, "ledger not null after init");
73
+ assertEq(ledger!.version, 1, "version is 1");
74
+ assertEq(ledger!.units.length, 0, "no units initially");
75
+ }
76
+
77
+ console.log("\n=== snapshotUnitMetrics ===");
78
+
79
+ {
80
+ resetMetrics();
81
+ initMetrics(tmpBase);
82
+
83
+ // Simulate a session with assistant messages containing usage data
84
+ const ctx = mockCtx([
85
+ { role: "user", content: "Do the thing" },
86
+ {
87
+ role: "assistant",
88
+ content: [
89
+ { type: "text", text: "I'll do the thing" },
90
+ { type: "tool_call", id: "tc1", name: "bash", input: {} },
91
+ ],
92
+ usage: {
93
+ input: 5000,
94
+ output: 2000,
95
+ cacheRead: 3000,
96
+ cacheWrite: 500,
97
+ totalTokens: 10500,
98
+ cost: {
99
+ input: 0.015,
100
+ output: 0.03,
101
+ cacheRead: 0.003,
102
+ cacheWrite: 0.002,
103
+ total: 0.05,
104
+ },
105
+ },
106
+ },
107
+ {
108
+ role: "toolResult",
109
+ toolCallId: "tc1",
110
+ content: [{ type: "text", text: "ok" }],
111
+ },
112
+ {
113
+ role: "assistant",
114
+ content: [{ type: "text", text: "Done!" }],
115
+ usage: {
116
+ input: 8000,
117
+ output: 1000,
118
+ cacheRead: 6000,
119
+ cacheWrite: 200,
120
+ totalTokens: 15200,
121
+ cost: {
122
+ input: 0.024,
123
+ output: 0.015,
124
+ cacheRead: 0.006,
125
+ cacheWrite: 0.001,
126
+ total: 0.046,
127
+ },
128
+ },
129
+ },
130
+ ]);
131
+
132
+ const unit = snapshotUnitMetrics(
133
+ ctx,
134
+ "execute-task",
135
+ "M001/S01/T01",
136
+ Date.now() - 5000,
137
+ "claude-sonnet-4-20250514",
138
+ );
139
+
140
+ assert(unit !== null, "unit returned");
141
+ assertEq(unit!.type, "execute-task", "type");
142
+ assertEq(unit!.id, "M001/S01/T01", "id");
143
+ assertEq(unit!.tokens.input, 13000, "input tokens (5000+8000)");
144
+ assertEq(unit!.tokens.output, 3000, "output tokens (2000+1000)");
145
+ assertEq(unit!.tokens.cacheRead, 9000, "cacheRead (3000+6000)");
146
+ assertEq(unit!.tokens.total, 25700, "total tokens (10500+15200)");
147
+ assert(
148
+ Math.abs(unit!.cost - 0.096) < 0.001,
149
+ `cost ~0.096 (got ${unit!.cost})`,
150
+ );
151
+ assertEq(unit!.toolCalls, 1, "1 tool call");
152
+ assertEq(unit!.assistantMessages, 2, "2 assistant messages");
153
+ assertEq(unit!.userMessages, 1, "1 user message");
154
+
155
+ // Verify ledger persisted
156
+ const ledger = getLedger()!;
157
+ assertEq(ledger.units.length, 1, "1 unit in ledger");
158
+ }
159
+
160
+ console.log("\n=== Persistence across init/reset cycles ===");
161
+
162
+ {
163
+ // Reset and re-init — should load from disk
164
+ resetMetrics();
165
+ initMetrics(tmpBase);
166
+
167
+ const ledger = getLedger()!;
168
+ assertEq(ledger.units.length, 1, "unit survived reset+init");
169
+ assertEq(ledger.units[0].id, "M001/S01/T01", "correct unit ID");
170
+
171
+ // Add another unit
172
+ const ctx = mockCtx([
173
+ {
174
+ role: "assistant",
175
+ content: [{ type: "text", text: "Research complete" }],
176
+ usage: {
177
+ input: 3000,
178
+ output: 1500,
179
+ cacheRead: 1000,
180
+ cacheWrite: 300,
181
+ totalTokens: 5800,
182
+ cost: {
183
+ input: 0.009,
184
+ output: 0.023,
185
+ cacheRead: 0.001,
186
+ cacheWrite: 0.001,
187
+ total: 0.034,
188
+ },
189
+ },
190
+ },
191
+ ]);
192
+
193
+ snapshotUnitMetrics(
194
+ ctx,
195
+ "research-slice",
196
+ "M001/S02",
197
+ Date.now() - 3000,
198
+ "claude-sonnet-4-20250514",
199
+ );
200
+
201
+ // Verify both units persisted
202
+ resetMetrics();
203
+ initMetrics(tmpBase);
204
+ const final = getLedger()!;
205
+ assertEq(final.units.length, 2, "2 units after second snapshot");
206
+ }
207
+
208
+ console.log("\n=== File content verification ===");
209
+
210
+ {
211
+ const raw = readFileSync(join(tmpBase, ".kata", "metrics.json"), "utf-8");
212
+ const parsed: MetricsLedger = JSON.parse(raw);
213
+ assertEq(parsed.version, 1, "file version is 1");
214
+ assertEq(parsed.units.length, 2, "file has 2 units");
215
+ assert(parsed.projectStartedAt > 0, "projectStartedAt is set");
216
+ }
217
+
218
+ console.log("\n=== Empty session handling ===");
219
+
220
+ {
221
+ resetMetrics();
222
+ initMetrics(tmpBase);
223
+
224
+ // Empty session — no messages
225
+ const ctx = mockCtx([]);
226
+ const unit = snapshotUnitMetrics(
227
+ ctx,
228
+ "plan-slice",
229
+ "M001/S01",
230
+ Date.now(),
231
+ "test-model",
232
+ );
233
+ assert(unit === null, "returns null for empty session");
234
+
235
+ // Ledger shouldn't have grown
236
+ assertEq(
237
+ getLedger()!.units.length,
238
+ 2,
239
+ "still 2 units (empty session not added)",
240
+ );
241
+ }
242
+
243
+ // ─── Cleanup ──────────────────────────────────────────────────────────────────
244
+
245
+ resetMetrics();
246
+ rmSync(tmpBase, { recursive: true, force: true });
247
+
248
+ console.log(`\n${"=".repeat(40)}`);
249
+ console.log(`Results: ${passed} passed, ${failed} failed`);
250
+ if (failed > 0) {
251
+ process.exit(1);
252
+ } else {
253
+ console.log("All tests passed ✓");
254
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Tests for Kata metrics aggregation logic.
3
+ * Tests the pure functions — no file I/O, no extension context.
4
+ */
5
+
6
+ import {
7
+ type UnitMetrics,
8
+ type TokenCounts,
9
+ classifyUnitPhase,
10
+ aggregateByPhase,
11
+ aggregateBySlice,
12
+ aggregateByModel,
13
+ getProjectTotals,
14
+ formatCost,
15
+ formatTokenCount,
16
+ } from "../metrics.js";
17
+
18
+ // ─── Test helpers ─────────────────────────────────────────────────────────────
19
+
20
+ function makeUnit(overrides: Partial<UnitMetrics> = {}): UnitMetrics {
21
+ return {
22
+ type: "execute-task",
23
+ id: "M001/S01/T01",
24
+ model: "claude-sonnet-4-20250514",
25
+ startedAt: 1000,
26
+ finishedAt: 2000,
27
+ tokens: { input: 1000, output: 500, cacheRead: 200, cacheWrite: 100, total: 1800 },
28
+ cost: 0.05,
29
+ toolCalls: 3,
30
+ assistantMessages: 2,
31
+ userMessages: 1,
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ let passed = 0;
37
+ let failed = 0;
38
+
39
+ function assert(condition: boolean, message: string): void {
40
+ if (condition) {
41
+ passed++;
42
+ } else {
43
+ failed++;
44
+ console.error(` FAIL: ${message}`);
45
+ }
46
+ }
47
+
48
+ function assertEq<T>(actual: T, expected: T, message: string): void {
49
+ if (actual === expected) {
50
+ passed++;
51
+ } else {
52
+ failed++;
53
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
54
+ }
55
+ }
56
+
57
+ function assertClose(actual: number, expected: number, tolerance: number, message: string): void {
58
+ if (Math.abs(actual - expected) <= tolerance) {
59
+ passed++;
60
+ } else {
61
+ failed++;
62
+ console.error(` FAIL: ${message} — expected ~${expected}, got ${actual}`);
63
+ }
64
+ }
65
+
66
+ // ─── Phase classification ─────────────────────────────────────────────────────
67
+
68
+ console.log("\n=== classifyUnitPhase ===");
69
+
70
+ assertEq(classifyUnitPhase("research-milestone"), "research", "research-milestone → research");
71
+ assertEq(classifyUnitPhase("research-slice"), "research", "research-slice → research");
72
+ assertEq(classifyUnitPhase("plan-milestone"), "planning", "plan-milestone → planning");
73
+ assertEq(classifyUnitPhase("plan-slice"), "planning", "plan-slice → planning");
74
+ assertEq(classifyUnitPhase("execute-task"), "execution", "execute-task → execution");
75
+ assertEq(classifyUnitPhase("complete-slice"), "completion", "complete-slice → completion");
76
+ assertEq(classifyUnitPhase("reassess-roadmap"), "reassessment", "reassess-roadmap → reassessment");
77
+ assertEq(classifyUnitPhase("unknown-thing"), "execution", "unknown → execution (fallback)");
78
+
79
+ // ─── getProjectTotals ─────────────────────────────────────────────────────────
80
+
81
+ console.log("\n=== getProjectTotals ===");
82
+
83
+ {
84
+ const units = [
85
+ makeUnit({ tokens: { input: 1000, output: 500, cacheRead: 200, cacheWrite: 100, total: 1800 }, cost: 0.05, toolCalls: 3, startedAt: 1000, finishedAt: 2000 }),
86
+ makeUnit({ tokens: { input: 2000, output: 1000, cacheRead: 400, cacheWrite: 200, total: 3600 }, cost: 0.10, toolCalls: 5, startedAt: 2000, finishedAt: 4000 }),
87
+ ];
88
+ const totals = getProjectTotals(units);
89
+
90
+ assertEq(totals.units, 2, "total units");
91
+ assertEq(totals.tokens.input, 3000, "total input tokens");
92
+ assertEq(totals.tokens.output, 1500, "total output tokens");
93
+ assertEq(totals.tokens.cacheRead, 600, "total cacheRead");
94
+ assertEq(totals.tokens.cacheWrite, 300, "total cacheWrite");
95
+ assertEq(totals.tokens.total, 5400, "total tokens");
96
+ assertClose(totals.cost, 0.15, 0.001, "total cost");
97
+ assertEq(totals.toolCalls, 8, "total tool calls");
98
+ assertEq(totals.duration, 3000, "total duration");
99
+ }
100
+
101
+ {
102
+ const totals = getProjectTotals([]);
103
+ assertEq(totals.units, 0, "empty: zero units");
104
+ assertEq(totals.cost, 0, "empty: zero cost");
105
+ assertEq(totals.tokens.total, 0, "empty: zero tokens");
106
+ }
107
+
108
+ // ─── aggregateByPhase ─────────────────────────────────────────────────────────
109
+
110
+ console.log("\n=== aggregateByPhase ===");
111
+
112
+ {
113
+ const units = [
114
+ makeUnit({ type: "research-milestone", cost: 0.02 }),
115
+ makeUnit({ type: "research-slice", cost: 0.03 }),
116
+ makeUnit({ type: "plan-milestone", cost: 0.01 }),
117
+ makeUnit({ type: "plan-slice", cost: 0.02 }),
118
+ makeUnit({ type: "execute-task", cost: 0.10 }),
119
+ makeUnit({ type: "execute-task", cost: 0.08 }),
120
+ makeUnit({ type: "complete-slice", cost: 0.01 }),
121
+ makeUnit({ type: "reassess-roadmap", cost: 0.005 }),
122
+ ];
123
+ const phases = aggregateByPhase(units);
124
+
125
+ assertEq(phases.length, 5, "5 phases");
126
+ assertEq(phases[0].phase, "research", "first phase is research");
127
+ assertEq(phases[0].units, 2, "2 research units");
128
+ assertClose(phases[0].cost, 0.05, 0.001, "research cost");
129
+
130
+ assertEq(phases[1].phase, "planning", "second phase is planning");
131
+ assertEq(phases[1].units, 2, "2 planning units");
132
+
133
+ assertEq(phases[2].phase, "execution", "third phase is execution");
134
+ assertEq(phases[2].units, 2, "2 execution units");
135
+ assertClose(phases[2].cost, 0.18, 0.001, "execution cost");
136
+
137
+ assertEq(phases[3].phase, "completion", "fourth phase is completion");
138
+ assertEq(phases[4].phase, "reassessment", "fifth phase is reassessment");
139
+ }
140
+
141
+ // ─── aggregateBySlice ─────────────────────────────────────────────────────────
142
+
143
+ console.log("\n=== aggregateBySlice ===");
144
+
145
+ {
146
+ const units = [
147
+ makeUnit({ id: "M001/S01/T01", cost: 0.05 }),
148
+ makeUnit({ id: "M001/S01/T02", cost: 0.04 }),
149
+ makeUnit({ id: "M001/S02/T01", cost: 0.10 }),
150
+ makeUnit({ id: "M001", type: "research-milestone", cost: 0.02 }),
151
+ ];
152
+ const slices = aggregateBySlice(units);
153
+
154
+ assertEq(slices.length, 3, "3 slice groups");
155
+
156
+ const s01 = slices.find(s => s.sliceId === "M001/S01");
157
+ assert(!!s01, "M001/S01 exists");
158
+ assertEq(s01!.units, 2, "M001/S01 has 2 units");
159
+ assertClose(s01!.cost, 0.09, 0.001, "M001/S01 cost");
160
+
161
+ const s02 = slices.find(s => s.sliceId === "M001/S02");
162
+ assert(!!s02, "M001/S02 exists");
163
+ assertEq(s02!.units, 1, "M001/S02 has 1 unit");
164
+
165
+ const mLevel = slices.find(s => s.sliceId === "M001");
166
+ assert(!!mLevel, "M001 (milestone-level) exists");
167
+ }
168
+
169
+ // ─── aggregateByModel ─────────────────────────────────────────────────────────
170
+
171
+ console.log("\n=== aggregateByModel ===");
172
+
173
+ {
174
+ const units = [
175
+ makeUnit({ model: "claude-sonnet-4-20250514", cost: 0.05 }),
176
+ makeUnit({ model: "claude-sonnet-4-20250514", cost: 0.04 }),
177
+ makeUnit({ model: "claude-opus-4-20250514", cost: 0.30 }),
178
+ ];
179
+ const models = aggregateByModel(units);
180
+
181
+ assertEq(models.length, 2, "2 models");
182
+ // Sorted by cost desc — opus should be first
183
+ assertEq(models[0].model, "claude-opus-4-20250514", "opus first (higher cost)");
184
+ assertClose(models[0].cost, 0.30, 0.001, "opus cost");
185
+ assertEq(models[1].model, "claude-sonnet-4-20250514", "sonnet second");
186
+ assertEq(models[1].units, 2, "sonnet has 2 units");
187
+ }
188
+
189
+ // ─── formatCost ───────────────────────────────────────────────────────────────
190
+
191
+ console.log("\n=== formatCost ===");
192
+
193
+ assertEq(formatCost(0), "$0.0000", "zero cost");
194
+ assertEq(formatCost(0.001), "$0.0010", "sub-cent cost");
195
+ assertEq(formatCost(0.05), "$0.050", "5 cents");
196
+ assertEq(formatCost(1.50), "$1.50", "dollar+");
197
+ assertEq(formatCost(14.20), "$14.20", "double digits");
198
+
199
+ // ─── formatTokenCount ─────────────────────────────────────────────────────────
200
+
201
+ console.log("\n=== formatTokenCount ===");
202
+
203
+ assertEq(formatTokenCount(0), "0", "zero tokens");
204
+ assertEq(formatTokenCount(500), "500", "sub-k");
205
+ assertEq(formatTokenCount(1500), "1.5k", "1.5k");
206
+ assertEq(formatTokenCount(150000), "150.0k", "150k");
207
+ assertEq(formatTokenCount(1500000), "1.50M", "1.5M");
208
+
209
+ // ─── Summary ──────────────────────────────────────────────────────────────────
210
+
211
+ console.log(`\n${"=".repeat(40)}`);
212
+ console.log(`Results: ${passed} passed, ${failed} failed`);
213
+ if (failed > 0) {
214
+ process.exit(1);
215
+ } else {
216
+ console.log("All tests passed ✓");
217
+ }