@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.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +56 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +95 -0
- package/dist/resource-loader.d.ts +18 -0
- package/dist/resource-loader.js +50 -0
- package/dist/wizard.d.ts +15 -0
- package/dist/wizard.js +159 -0
- package/package.json +50 -21
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +45 -0
- package/src/resources/AGENTS.md +108 -0
- package/src/resources/KATA-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2758 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4916 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/github/formatters.ts +207 -0
- package/src/resources/extensions/github/gh-api.ts +537 -0
- package/src/resources/extensions/github/index.ts +778 -0
- package/src/resources/extensions/kata/activity-log.ts +88 -0
- package/src/resources/extensions/kata/auto.ts +2786 -0
- package/src/resources/extensions/kata/commands.ts +355 -0
- package/src/resources/extensions/kata/crash-recovery.ts +85 -0
- package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/kata/doctor.ts +683 -0
- package/src/resources/extensions/kata/files.ts +730 -0
- package/src/resources/extensions/kata/gitignore.ts +165 -0
- package/src/resources/extensions/kata/guided-flow.ts +976 -0
- package/src/resources/extensions/kata/index.ts +556 -0
- package/src/resources/extensions/kata/metrics.ts +397 -0
- package/src/resources/extensions/kata/observability-validator.ts +408 -0
- package/src/resources/extensions/kata/package.json +11 -0
- package/src/resources/extensions/kata/paths.ts +346 -0
- package/src/resources/extensions/kata/preferences.ts +695 -0
- package/src/resources/extensions/kata/prompt-loader.ts +50 -0
- package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/kata/prompts/discuss.md +151 -0
- package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
- package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/kata/prompts/queue.md +85 -0
- package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
- package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
- package/src/resources/extensions/kata/prompts/system.md +341 -0
- package/src/resources/extensions/kata/session-forensics.ts +550 -0
- package/src/resources/extensions/kata/skill-discovery.ts +137 -0
- package/src/resources/extensions/kata/state.ts +509 -0
- package/src/resources/extensions/kata/templates/context.md +76 -0
- package/src/resources/extensions/kata/templates/decisions.md +8 -0
- package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/kata/templates/plan.md +133 -0
- package/src/resources/extensions/kata/templates/preferences.md +15 -0
- package/src/resources/extensions/kata/templates/project.md +31 -0
- package/src/resources/extensions/kata/templates/reassessment.md +28 -0
- package/src/resources/extensions/kata/templates/requirements.md +81 -0
- package/src/resources/extensions/kata/templates/research.md +46 -0
- package/src/resources/extensions/kata/templates/roadmap.md +118 -0
- package/src/resources/extensions/kata/templates/slice-context.md +58 -0
- package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
- package/src/resources/extensions/kata/templates/state.md +19 -0
- package/src/resources/extensions/kata/templates/task-plan.md +52 -0
- package/src/resources/extensions/kata/templates/task-summary.md +57 -0
- package/src/resources/extensions/kata/templates/uat.md +54 -0
- package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
- package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
- package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
- package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
- package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
- package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
- package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
- package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
- package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
- package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
- package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
- package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
- package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
- package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
- package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
- package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
- package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
- package/src/resources/extensions/kata/types.ts +159 -0
- package/src/resources/extensions/kata/unit-runtime.ts +163 -0
- package/src/resources/extensions/kata/workspace-index.ts +203 -0
- package/src/resources/extensions/kata/worktree.ts +182 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +68 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -0
- package/src/resources/extensions/shared/interview-ui.ts +822 -0
- package/src/resources/extensions/shared/next-action-ui.ts +235 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +92 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1293 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
- package/dist/commands/task.d.ts +0 -9
- package/dist/commands/task.d.ts.map +0 -1
- package/dist/commands/task.js +0 -129
- package/dist/commands/task.js.map +0 -1
- package/dist/commands/task.test.d.ts +0 -2
- package/dist/commands/task.test.d.ts.map +0 -1
- package/dist/commands/task.test.js +0 -169
- package/dist/commands/task.test.js.map +0 -1
- package/dist/e2e/task-e2e.test.d.ts +0 -2
- package/dist/e2e/task-e2e.test.d.ts.map +0 -1
- package/dist/e2e/task-e2e.test.js +0 -173
- package/dist/e2e/task-e2e.test.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -93
- package/dist/index.js.map +0 -1
- package/dist/slug.d.ts +0 -2
- package/dist/slug.d.ts.map +0 -1
- package/dist/slug.js +0 -12
- package/dist/slug.js.map +0 -1
- package/dist/slug.test.d.ts +0 -2
- package/dist/slug.test.d.ts.map +0 -1
- package/dist/slug.test.js +0 -32
- package/dist/slug.test.js.map +0 -1
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kata Metrics — Token & Cost Tracking
|
|
3
|
+
*
|
|
4
|
+
* Accumulates per-unit usage data across auto-mode sessions.
|
|
5
|
+
* Data is extracted from session entries before each context wipe,
|
|
6
|
+
* written to .kata/metrics.json, and surfaced in the dashboard.
|
|
7
|
+
*
|
|
8
|
+
* Data flow:
|
|
9
|
+
* 1. Before newSession() wipes context, snapshotUnitMetrics() scans
|
|
10
|
+
* session entries for AssistantMessage usage data
|
|
11
|
+
* 2. The unit record is appended to the in-memory ledger and flushed to disk
|
|
12
|
+
* 3. The dashboard overlay and progress widget read from the in-memory ledger
|
|
13
|
+
* 4. On crash recovery or fresh start, the ledger is loaded from disk
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
19
|
+
import { kataRoot } from "./paths.js";
|
|
20
|
+
|
|
21
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export interface TokenCounts {
|
|
24
|
+
input: number;
|
|
25
|
+
output: number;
|
|
26
|
+
cacheRead: number;
|
|
27
|
+
cacheWrite: number;
|
|
28
|
+
total: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UnitMetrics {
|
|
32
|
+
type: string; // e.g. "research-milestone", "execute-task"
|
|
33
|
+
id: string; // e.g. "M001/S01/T01"
|
|
34
|
+
model: string; // model ID used
|
|
35
|
+
startedAt: number; // ms timestamp
|
|
36
|
+
finishedAt: number; // ms timestamp
|
|
37
|
+
tokens: TokenCounts;
|
|
38
|
+
cost: number; // total USD cost
|
|
39
|
+
toolCalls: number;
|
|
40
|
+
assistantMessages: number;
|
|
41
|
+
userMessages: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface MetricsLedger {
|
|
45
|
+
version: 1;
|
|
46
|
+
projectStartedAt: number;
|
|
47
|
+
units: UnitMetrics[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Phase classification ─────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export type MetricsPhase =
|
|
53
|
+
| "research"
|
|
54
|
+
| "planning"
|
|
55
|
+
| "execution"
|
|
56
|
+
| "completion"
|
|
57
|
+
| "reassessment";
|
|
58
|
+
|
|
59
|
+
export function classifyUnitPhase(unitType: string): MetricsPhase {
|
|
60
|
+
switch (unitType) {
|
|
61
|
+
case "research-milestone":
|
|
62
|
+
case "research-slice":
|
|
63
|
+
return "research";
|
|
64
|
+
case "plan-milestone":
|
|
65
|
+
case "plan-slice":
|
|
66
|
+
return "planning";
|
|
67
|
+
case "execute-task":
|
|
68
|
+
return "execution";
|
|
69
|
+
case "complete-slice":
|
|
70
|
+
return "completion";
|
|
71
|
+
case "reassess-roadmap":
|
|
72
|
+
return "reassessment";
|
|
73
|
+
default:
|
|
74
|
+
return "execution";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── In-memory state ──────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
let ledger: MetricsLedger | null = null;
|
|
81
|
+
let basePath: string = "";
|
|
82
|
+
|
|
83
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Initialize the metrics system for a given project.
|
|
87
|
+
* Loads existing ledger from disk if present.
|
|
88
|
+
*/
|
|
89
|
+
export function initMetrics(base: string): void {
|
|
90
|
+
basePath = base;
|
|
91
|
+
ledger = loadLedger(base);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Reset in-memory state. Called when auto-mode stops.
|
|
96
|
+
*/
|
|
97
|
+
export function resetMetrics(): void {
|
|
98
|
+
ledger = null;
|
|
99
|
+
basePath = "";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Snapshot usage metrics from the current session before it's wiped.
|
|
104
|
+
* Scans session entries for AssistantMessage usage data.
|
|
105
|
+
*/
|
|
106
|
+
export function snapshotUnitMetrics(
|
|
107
|
+
ctx: ExtensionContext,
|
|
108
|
+
unitType: string,
|
|
109
|
+
unitId: string,
|
|
110
|
+
startedAt: number,
|
|
111
|
+
model: string,
|
|
112
|
+
): UnitMetrics | null {
|
|
113
|
+
if (!ledger) return null;
|
|
114
|
+
|
|
115
|
+
const entries = ctx.sessionManager.getEntries();
|
|
116
|
+
if (!entries || entries.length === 0) return null;
|
|
117
|
+
|
|
118
|
+
const tokens: TokenCounts = {
|
|
119
|
+
input: 0,
|
|
120
|
+
output: 0,
|
|
121
|
+
cacheRead: 0,
|
|
122
|
+
cacheWrite: 0,
|
|
123
|
+
total: 0,
|
|
124
|
+
};
|
|
125
|
+
let cost = 0;
|
|
126
|
+
let toolCalls = 0;
|
|
127
|
+
let assistantMessages = 0;
|
|
128
|
+
let userMessages = 0;
|
|
129
|
+
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
if (entry.type !== "message") continue;
|
|
132
|
+
const msg = (entry as any).message;
|
|
133
|
+
if (!msg) continue;
|
|
134
|
+
|
|
135
|
+
if (msg.role === "assistant") {
|
|
136
|
+
assistantMessages++;
|
|
137
|
+
if (msg.usage) {
|
|
138
|
+
tokens.input += msg.usage.input ?? 0;
|
|
139
|
+
tokens.output += msg.usage.output ?? 0;
|
|
140
|
+
tokens.cacheRead += msg.usage.cacheRead ?? 0;
|
|
141
|
+
tokens.cacheWrite += msg.usage.cacheWrite ?? 0;
|
|
142
|
+
tokens.total += msg.usage.totalTokens ?? 0;
|
|
143
|
+
if (msg.usage.cost) {
|
|
144
|
+
cost += msg.usage.cost.total ?? 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Count tool calls in this message
|
|
148
|
+
if (msg.content && Array.isArray(msg.content)) {
|
|
149
|
+
for (const block of msg.content) {
|
|
150
|
+
if (block.type === "tool_call") toolCalls++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} else if (msg.role === "user") {
|
|
154
|
+
userMessages++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const unit: UnitMetrics = {
|
|
159
|
+
type: unitType,
|
|
160
|
+
id: unitId,
|
|
161
|
+
model,
|
|
162
|
+
startedAt,
|
|
163
|
+
finishedAt: Date.now(),
|
|
164
|
+
tokens,
|
|
165
|
+
cost,
|
|
166
|
+
toolCalls,
|
|
167
|
+
assistantMessages,
|
|
168
|
+
userMessages,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
ledger.units.push(unit);
|
|
172
|
+
saveLedger(basePath, ledger);
|
|
173
|
+
|
|
174
|
+
return unit;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get the current ledger (read-only).
|
|
179
|
+
*/
|
|
180
|
+
export function getLedger(): MetricsLedger | null {
|
|
181
|
+
return ledger;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Aggregation helpers ──────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
export interface PhaseAggregate {
|
|
187
|
+
phase: MetricsPhase;
|
|
188
|
+
units: number;
|
|
189
|
+
tokens: TokenCounts;
|
|
190
|
+
cost: number;
|
|
191
|
+
duration: number; // ms
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface SliceAggregate {
|
|
195
|
+
sliceId: string;
|
|
196
|
+
units: number;
|
|
197
|
+
tokens: TokenCounts;
|
|
198
|
+
cost: number;
|
|
199
|
+
duration: number;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface ModelAggregate {
|
|
203
|
+
model: string;
|
|
204
|
+
units: number;
|
|
205
|
+
tokens: TokenCounts;
|
|
206
|
+
cost: number;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface ProjectTotals {
|
|
210
|
+
units: number;
|
|
211
|
+
tokens: TokenCounts;
|
|
212
|
+
cost: number;
|
|
213
|
+
duration: number;
|
|
214
|
+
toolCalls: number;
|
|
215
|
+
assistantMessages: number;
|
|
216
|
+
userMessages: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function emptyTokens(): TokenCounts {
|
|
220
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function addTokens(a: TokenCounts, b: TokenCounts): TokenCounts {
|
|
224
|
+
return {
|
|
225
|
+
input: a.input + b.input,
|
|
226
|
+
output: a.output + b.output,
|
|
227
|
+
cacheRead: a.cacheRead + b.cacheRead,
|
|
228
|
+
cacheWrite: a.cacheWrite + b.cacheWrite,
|
|
229
|
+
total: a.total + b.total,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function aggregateByPhase(units: UnitMetrics[]): PhaseAggregate[] {
|
|
234
|
+
const map = new Map<MetricsPhase, PhaseAggregate>();
|
|
235
|
+
for (const u of units) {
|
|
236
|
+
const phase = classifyUnitPhase(u.type);
|
|
237
|
+
let agg = map.get(phase);
|
|
238
|
+
if (!agg) {
|
|
239
|
+
agg = { phase, units: 0, tokens: emptyTokens(), cost: 0, duration: 0 };
|
|
240
|
+
map.set(phase, agg);
|
|
241
|
+
}
|
|
242
|
+
agg.units++;
|
|
243
|
+
agg.tokens = addTokens(agg.tokens, u.tokens);
|
|
244
|
+
agg.cost += u.cost;
|
|
245
|
+
agg.duration += u.finishedAt - u.startedAt;
|
|
246
|
+
}
|
|
247
|
+
// Return in a stable order
|
|
248
|
+
const order: MetricsPhase[] = [
|
|
249
|
+
"research",
|
|
250
|
+
"planning",
|
|
251
|
+
"execution",
|
|
252
|
+
"completion",
|
|
253
|
+
"reassessment",
|
|
254
|
+
];
|
|
255
|
+
return order.map((p) => map.get(p)).filter((a): a is PhaseAggregate => !!a);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function aggregateBySlice(units: UnitMetrics[]): SliceAggregate[] {
|
|
259
|
+
const map = new Map<string, SliceAggregate>();
|
|
260
|
+
for (const u of units) {
|
|
261
|
+
const parts = u.id.split("/");
|
|
262
|
+
// Slice ID is parts[0]/parts[1] if it exists, else parts[0]
|
|
263
|
+
const sliceId = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
264
|
+
let agg = map.get(sliceId);
|
|
265
|
+
if (!agg) {
|
|
266
|
+
agg = { sliceId, units: 0, tokens: emptyTokens(), cost: 0, duration: 0 };
|
|
267
|
+
map.set(sliceId, agg);
|
|
268
|
+
}
|
|
269
|
+
agg.units++;
|
|
270
|
+
agg.tokens = addTokens(agg.tokens, u.tokens);
|
|
271
|
+
agg.cost += u.cost;
|
|
272
|
+
agg.duration += u.finishedAt - u.startedAt;
|
|
273
|
+
}
|
|
274
|
+
return Array.from(map.values()).sort((a, b) =>
|
|
275
|
+
a.sliceId.localeCompare(b.sliceId),
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function aggregateByModel(units: UnitMetrics[]): ModelAggregate[] {
|
|
280
|
+
const map = new Map<string, ModelAggregate>();
|
|
281
|
+
for (const u of units) {
|
|
282
|
+
let agg = map.get(u.model);
|
|
283
|
+
if (!agg) {
|
|
284
|
+
agg = { model: u.model, units: 0, tokens: emptyTokens(), cost: 0 };
|
|
285
|
+
map.set(u.model, agg);
|
|
286
|
+
}
|
|
287
|
+
agg.units++;
|
|
288
|
+
agg.tokens = addTokens(agg.tokens, u.tokens);
|
|
289
|
+
agg.cost += u.cost;
|
|
290
|
+
}
|
|
291
|
+
return Array.from(map.values()).sort((a, b) => b.cost - a.cost);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function getProjectTotals(units: UnitMetrics[]): ProjectTotals {
|
|
295
|
+
const totals: ProjectTotals = {
|
|
296
|
+
units: units.length,
|
|
297
|
+
tokens: emptyTokens(),
|
|
298
|
+
cost: 0,
|
|
299
|
+
duration: 0,
|
|
300
|
+
toolCalls: 0,
|
|
301
|
+
assistantMessages: 0,
|
|
302
|
+
userMessages: 0,
|
|
303
|
+
};
|
|
304
|
+
for (const u of units) {
|
|
305
|
+
totals.tokens = addTokens(totals.tokens, u.tokens);
|
|
306
|
+
totals.cost += u.cost;
|
|
307
|
+
totals.duration += u.finishedAt - u.startedAt;
|
|
308
|
+
totals.toolCalls += u.toolCalls;
|
|
309
|
+
totals.assistantMessages += u.assistantMessages;
|
|
310
|
+
totals.userMessages += u.userMessages;
|
|
311
|
+
}
|
|
312
|
+
return totals;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ─── Formatting helpers ───────────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
export function formatCost(cost: number): string {
|
|
318
|
+
if (cost < 0.01) return `$${cost.toFixed(4)}`;
|
|
319
|
+
if (cost < 1) return `$${cost.toFixed(3)}`;
|
|
320
|
+
return `$${cost.toFixed(2)}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Compute a projected remaining cost based on completed slice averages.
|
|
325
|
+
*
|
|
326
|
+
* Filters to slice-level entries (sliceId contains "/") to exclude bare milestone
|
|
327
|
+
* aggregates from the average. Returns [] when fewer than 2 slice-level entries
|
|
328
|
+
* exist (insufficient data for a reliable projection).
|
|
329
|
+
*
|
|
330
|
+
* If `budgetCeiling` is provided and `totalCost >= budgetCeiling`, a warning line
|
|
331
|
+
* is appended to the result.
|
|
332
|
+
*/
|
|
333
|
+
export function formatCostProjection(
|
|
334
|
+
completedSlices: SliceAggregate[],
|
|
335
|
+
remainingCount: number,
|
|
336
|
+
budgetCeiling?: number,
|
|
337
|
+
): string[] {
|
|
338
|
+
const sliceLevel = completedSlices.filter((s) => s.sliceId.includes("/"));
|
|
339
|
+
if (sliceLevel.length < 2) return [];
|
|
340
|
+
|
|
341
|
+
const totalCost = sliceLevel.reduce((sum, s) => sum + s.cost, 0);
|
|
342
|
+
const avgCost = totalCost / sliceLevel.length;
|
|
343
|
+
const projected = avgCost * remainingCount;
|
|
344
|
+
|
|
345
|
+
const projLine = `Projected remaining: ${formatCost(projected)} (${formatCost(avgCost)}/slice avg × ${remainingCount} remaining)`;
|
|
346
|
+
const result: string[] = [projLine];
|
|
347
|
+
|
|
348
|
+
if (budgetCeiling !== undefined && totalCost >= budgetCeiling) {
|
|
349
|
+
result.push(
|
|
350
|
+
`Budget ceiling ${formatCost(budgetCeiling)} reached (spent ${formatCost(totalCost)})`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function formatTokenCount(count: number): string {
|
|
358
|
+
if (count < 1000) return `${count}`;
|
|
359
|
+
if (count < 1_000_000) return `${(count / 1000).toFixed(1)}k`;
|
|
360
|
+
return `${(count / 1_000_000).toFixed(2)}M`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ─── Disk I/O ─────────────────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
function metricsPath(base: string): string {
|
|
366
|
+
return join(kataRoot(base), "metrics.json");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function loadLedger(base: string): MetricsLedger {
|
|
370
|
+
try {
|
|
371
|
+
const raw = readFileSync(metricsPath(base), "utf-8");
|
|
372
|
+
const parsed = JSON.parse(raw);
|
|
373
|
+
if (parsed.version === 1 && Array.isArray(parsed.units)) {
|
|
374
|
+
return parsed as MetricsLedger;
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
// File doesn't exist or is corrupt — start fresh
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
version: 1,
|
|
381
|
+
projectStartedAt: Date.now(),
|
|
382
|
+
units: [],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function saveLedger(base: string, data: MetricsLedger): void {
|
|
387
|
+
try {
|
|
388
|
+
mkdirSync(kataRoot(base), { recursive: true });
|
|
389
|
+
writeFileSync(
|
|
390
|
+
metricsPath(base),
|
|
391
|
+
JSON.stringify(data, null, 2) + "\n",
|
|
392
|
+
"utf-8",
|
|
393
|
+
);
|
|
394
|
+
} catch {
|
|
395
|
+
// Don't let metrics failures break auto-mode
|
|
396
|
+
}
|
|
397
|
+
}
|