create-harness-vibe-coding 0.8.13 → 0.8.16
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/CHANGELOG.md +165 -0
- package/README-CN.md +41 -9
- package/README.md +39 -9
- package/package.json +2 -1
- package/src/generator.js +601 -472
- package/src/index.js +4 -4
- package/templates/common/.claude/agents/architect-manager.md +1 -0
- package/templates/common/.claude/agents/architect.md +2 -1
- package/templates/common/.claude/agents/codebase-explorer.md +1 -0
- package/templates/common/.claude/agents/context-master.md +2 -1
- package/templates/common/.claude/agents/debugger.md +1 -0
- package/templates/common/.claude/agents/docs-researcher.md +2 -1
- package/templates/common/.claude/agents/explore-manager.md +1 -0
- package/templates/common/.claude/agents/implement-manager.md +1 -0
- package/templates/common/.claude/agents/implementer.md +1 -0
- package/templates/common/.claude/agents/memory-master.md +2 -1
- package/templates/common/.claude/agents/planner.md +3 -2
- package/templates/common/.claude/agents/reflector.md +1 -0
- package/templates/common/.claude/agents/researcher.md +1 -0
- package/templates/common/.claude/agents/review-manager.md +1 -0
- package/templates/common/.claude/agents/reviewer.md +2 -1
- package/templates/common/.claude/agents/task-scribe.md +1 -0
- package/templates/common/.claude/agents/tdd-guide.md +5 -4
- package/templates/common/.claude/agents/test-writer.md +6 -5
- package/templates/common/.claude/agents/verifier.md +1 -0
- package/templates/common/.claude/commands/wf-help.md +1 -0
- package/templates/common/.claude/commands/wf-update.md +68 -10
- package/templates/common/.claude/rules/ecc/common.md +6 -5
- package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +12 -6
- package/templates/common/.claude/skills/tdd/SKILL.md +5 -5
- package/templates/common/.claude/skills/wf/SKILL.md +13 -5
- package/templates/common/.claude/skills/wf-agents-docs/SKILL.md +119 -0
- package/templates/common/.claude/skills/wf-auto/SKILL.md +27 -7
- package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +12 -5
- package/templates/common/.claude/skills/wf-learn/SKILL.md +6 -0
- package/templates/common/.claude/skills/wf-max/SKILL.md +22 -7
- package/templates/common/.claude/skills/wf-readme/SKILL.md +8 -2
- package/templates/common/.claude/skills/wf-remove/SKILL.md +6 -0
- package/templates/common/.claude/skills/wf-review/SKILL.md +10 -3
- package/templates/common/.claude/skills/wf-update/SKILL.md +41 -4
- package/templates/common/.harness-version +290 -128
- package/templates/common/.opencode/agents/architect-manager.md +1 -0
- package/templates/common/.opencode/agents/architect.md +2 -1
- package/templates/common/.opencode/agents/codebase-explorer.md +1 -0
- package/templates/common/.opencode/agents/context-master.md +2 -1
- package/templates/common/.opencode/agents/debugger.md +1 -0
- package/templates/common/.opencode/agents/docs-researcher.md +2 -1
- package/templates/common/.opencode/agents/explore-manager.md +1 -0
- package/templates/common/.opencode/agents/implement-manager.md +1 -0
- package/templates/common/.opencode/agents/implementer.md +1 -0
- package/templates/common/.opencode/agents/memory-master.md +2 -1
- package/templates/common/.opencode/agents/planner.md +3 -2
- package/templates/common/.opencode/agents/reflector.md +1 -0
- package/templates/common/.opencode/agents/researcher.md +1 -0
- package/templates/common/.opencode/agents/review-manager.md +1 -0
- package/templates/common/.opencode/agents/reviewer.md +2 -1
- package/templates/common/.opencode/agents/task-scribe.md +1 -0
- package/templates/common/.opencode/agents/tdd-guide.md +5 -4
- package/templates/common/.opencode/agents/test-writer.md +6 -5
- package/templates/common/.opencode/agents/verifier.md +1 -0
- package/templates/common/.opencode/commands/wf-auto-spark.md +3 -2
- package/templates/common/.opencode/commands/wf-auto.md +3 -2
- package/templates/common/.opencode/commands/wf-help.md +1 -0
- package/templates/common/.opencode/commands/wf-learn.md +3 -2
- package/templates/common/.opencode/commands/wf-max.md +3 -2
- package/templates/common/.opencode/commands/wf-readme.md +3 -2
- package/templates/common/.opencode/commands/wf-remove.md +3 -2
- package/templates/common/.opencode/commands/wf-review.md +3 -2
- package/templates/common/.opencode/commands/wf-update.md +68 -10
- package/templates/common/.opencode/commands/wf.md +3 -2
- package/templates/common/CLAUDE.md +14 -12
- package/templates/common/Harness/MEMORY.md +21 -18
- package/templates/common/Harness/README.md +41 -39
- package/templates/common/Harness/ownership.manifest.json +815 -0
- package/templates/common/Harness/{architecture.md → project/architecture.md} +1 -1
- package/templates/common/Harness/research/README.md +3 -3
- package/templates/common/Harness/scripts/context-budget.mjs +95 -0
- package/templates/common/Harness/scripts/l2-cache-telemetry.mjs +703 -0
- package/templates/common/Harness/scripts/scan-clean.mjs +13 -3
- package/templates/common/Harness/scripts/validate-harness.mjs +332 -175
- package/templates/common/Harness/scripts/wf-remove.mjs +60 -34
- package/templates/common/Harness/scripts/wf-update-check.mjs +355 -45
- package/templates/common/Harness/settings.json +43 -0
- package/templates/common/Harness/{ECC-GUIDE.md → specs/guides/ECC-GUIDE.md} +4 -4
- package/templates/common/Harness/{SETUP.md → specs/guides/SETUP.md} +32 -35
- package/templates/common/Harness/{extension.md → specs/guides/extension.md} +3 -3
- package/templates/common/Harness/{lifecycle.md → specs/guides/lifecycle.md} +2 -2
- package/templates/common/Harness/{agent-workflow.md → specs/runtime/agent-workflow.md} +6 -6
- package/templates/common/Harness/{context-loading.md → specs/runtime/context-loading.md} +85 -21
- package/templates/common/Harness/{dispatch.md → specs/runtime/dispatch.md} +3 -2
- package/templates/common/Harness/{subagents.md → specs/runtime/subagents.md} +10 -5
- package/templates/common/Harness/{WF-AUTO-SPARK.md → specs/workflows/WF-AUTO-SPARK.md} +2 -2
- package/templates/common/Harness/{WF-AUTO.md → specs/workflows/WF-AUTO.md} +6 -1
- package/templates/common/Harness/{WF-KERNEL.md → specs/workflows/WF-KERNEL.md} +10 -0
- package/templates/common/Harness/{WF-MAX.md → specs/workflows/WF-MAX.md} +10 -5
- package/templates/common/Harness/{WF-STATE.md → specs/workflows/WF-STATE.md} +5 -0
- package/templates/common/Harness/{WF.md → specs/workflows/WF.md} +12 -1
- package/templates/common/README.md +8 -6
- package/templates/common/memory/startup-hints.md +19 -17
- package/templates/optional/skills/browser-e2e/.claude/skills/browser-e2e/SKILL.md +1 -1
- package/templates/optional/skills/browser-e2e/.claude/skills/wf-browser/SKILL.md +7 -0
- package/templates/optional/skills/browser-e2e/.opencode/commands/wf-browser.md +3 -2
- package/templates/optional/skills/browser-e2e/Harness/workflows/browser-e2e.md +4 -4
- package/templates/optional/skills/github-pr-review/.claude/skills/github-pr-review/SKILL.md +1 -1
- package/templates/optional/skills/python-backend/.claude/skills/python-backend/SKILL.md +1 -1
- package/templates/optional/skills/ts-react-frontend/.claude/skills/ts-react-frontend/SKILL.md +1 -1
- package/templates/optional/skills/ui-ux-review/.claude/skills/ui-ux-review/SKILL.md +1 -1
- /package/templates/common/Harness/{ACCEPTANCE_PROTOCOL.md → specs/protocols/ACCEPTANCE_PROTOCOL.md} +0 -0
- /package/templates/common/Harness/{AGENT_ISOLATION.md → specs/protocols/AGENT_ISOLATION.md} +0 -0
- /package/templates/common/Harness/{DEBUG_PROTOCOL.md → specs/protocols/DEBUG_PROTOCOL.md} +0 -0
- /package/templates/common/Harness/{HARNESS_BRIDGE.md → specs/protocols/HARNESS_BRIDGE.md} +0 -0
- /package/templates/common/Harness/{MEMORY_PROTOCOL.md → specs/protocols/MEMORY_PROTOCOL.md} +0 -0
- /package/templates/common/Harness/{TASK_ARCHIVE.md → specs/protocols/TASK_ARCHIVE.md} +0 -0
- /package/templates/common/Harness/{TDD-GUIDE.md → specs/protocols/TDD-GUIDE.md} +0 -0
- /package/templates/common/Harness/{WF-AUTO-ANGLES.md → specs/workflows/WF-AUTO-ANGLES.md} +0 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const SCHEMA = 'harness-l2-cache-telemetry@1';
|
|
9
|
+
const DEFAULT_GROUPS = ['provider-control', 'harness-thin', 'wf-light'];
|
|
10
|
+
const OFFICIAL_DOCS = [
|
|
11
|
+
'https://code.claude.com/docs/en/cli-reference',
|
|
12
|
+
'https://code.claude.com/docs/en/prompt-caching',
|
|
13
|
+
'https://code.claude.com/docs/en/statusline',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const GROUPS = {
|
|
17
|
+
'provider-control': {
|
|
18
|
+
description: 'Claude Code provider-cache control with project customizations disabled.',
|
|
19
|
+
safeMode: true,
|
|
20
|
+
disallowedTools: '*',
|
|
21
|
+
maxTurns: 1,
|
|
22
|
+
prompts: [
|
|
23
|
+
'Answer exactly: provider cache control cold turn.',
|
|
24
|
+
'Do not use tools. Answer exactly: cache_read_input_tokens.',
|
|
25
|
+
'Do not use tools. Answer exactly: structured JSON usage is the source of truth; status text is only a visual aid.',
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
'harness-thin': {
|
|
29
|
+
description: 'Harness thin startup route: CLAUDE.md plus startup hints only.',
|
|
30
|
+
tools: 'Read',
|
|
31
|
+
maxTurns: 4,
|
|
32
|
+
prompts: [
|
|
33
|
+
'Read only CLAUDE.md and Harness/memory/startup-hints.md. In two short sentences, state the normal startup route and what must not be loaded. Do not modify files.',
|
|
34
|
+
'Do not read files or run tools. Answer exactly: cache_read_input_tokens.',
|
|
35
|
+
'Do not read files or run tools. Answer exactly: No, L0/L1 cannot claim real cache-hit improvement without L2 provider usage telemetry.',
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
'wf-light': {
|
|
39
|
+
description: 'Explicit /wf route with stable Harness router and WF-light documents.',
|
|
40
|
+
tools: 'Read,Grep,Glob',
|
|
41
|
+
maxTurns: 6,
|
|
42
|
+
prompts: [
|
|
43
|
+
'/wf Read only CLAUDE.md, Harness/MEMORY.md, Harness/README.md, Harness/specs/workflows/WF.md, Harness/specs/workflows/WF-KERNEL.md, and Harness/specs/runtime/context-loading.md. Return exactly three bullets: route used, cache-first boundary, no-edit confirmation.',
|
|
44
|
+
'Do not read files or run tools. Based on this same session, one sentence: name the stable-prefix file categories.',
|
|
45
|
+
'Do not read files or run tools. Answer exactly: cache_read_input_tokens should stay high when the prefix is stable.',
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function usage() {
|
|
51
|
+
return `Usage:
|
|
52
|
+
node Harness/scripts/l2-cache-telemetry.mjs [options]
|
|
53
|
+
|
|
54
|
+
Options:
|
|
55
|
+
--dry-run Print the planned Claude Code calls only.
|
|
56
|
+
--from-file <path> Summarize an existing telemetry JSON file.
|
|
57
|
+
--json Print JSON instead of text.
|
|
58
|
+
--out <path> Output path for a real run.
|
|
59
|
+
--groups <ids> Comma list: ${Object.keys(GROUPS).join(', ')}.
|
|
60
|
+
--turns <n> Turns per group. Default: 3.
|
|
61
|
+
--turn-budget-usd <n> Per Claude call budget. Default: 0.35.
|
|
62
|
+
--total-budget-usd <n> Stop after total reported cost passes this. Default: 1.20.
|
|
63
|
+
--claude-command <command> Claude executable. Default: claude.cmd on Windows, claude elsewhere.
|
|
64
|
+
--root <path> Working directory. Default: current directory.
|
|
65
|
+
--model <model> Optional Claude model flag.
|
|
66
|
+
--timeout-ms <n> Per turn timeout. Default: 180000.
|
|
67
|
+
--include-raw Store full raw Claude JSON result for each turn.
|
|
68
|
+
--exclude-dynamic-system-prompt-sections
|
|
69
|
+
Add Claude Code's cache-friendly CLI flag; recorded in the report.
|
|
70
|
+
--claim-min-warm-turns <n> Warm turns required before README measured-claim gate. Default: 10.
|
|
71
|
+
--claim-min-read-ratio <n> Warm median read ratio threshold. Default: 95.
|
|
72
|
+
--claim-min-uplift-points <n> Improvement threshold over provider-control. Default: 2.
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readArg(argv, index, name) {
|
|
77
|
+
if (index + 1 >= argv.length) {
|
|
78
|
+
throw new Error(`${name} requires a value`);
|
|
79
|
+
}
|
|
80
|
+
return argv[index + 1];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function numberArg(value, name, min = 0) {
|
|
84
|
+
const parsed = Number(value);
|
|
85
|
+
if (!Number.isFinite(parsed) || parsed < min) {
|
|
86
|
+
throw new Error(`${name} must be a number >= ${min}`);
|
|
87
|
+
}
|
|
88
|
+
return parsed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function intArg(value, name, min = 1) {
|
|
92
|
+
const parsed = Number(value);
|
|
93
|
+
if (!Number.isInteger(parsed) || parsed < min) {
|
|
94
|
+
throw new Error(`${name} must be an integer >= ${min}`);
|
|
95
|
+
}
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseArgs(argv) {
|
|
100
|
+
const options = {
|
|
101
|
+
dryRun: false,
|
|
102
|
+
json: false,
|
|
103
|
+
fromFile: '',
|
|
104
|
+
outputPath: '',
|
|
105
|
+
groups: [...DEFAULT_GROUPS],
|
|
106
|
+
turnsPerGroup: 3,
|
|
107
|
+
turnBudgetUsd: 0.35,
|
|
108
|
+
totalBudgetUsd: 1.20,
|
|
109
|
+
claudeCommand: process.platform === 'win32' ? 'claude.cmd' : 'claude',
|
|
110
|
+
root: process.cwd(),
|
|
111
|
+
model: '',
|
|
112
|
+
timeoutMs: 180000,
|
|
113
|
+
includeRaw: false,
|
|
114
|
+
excludeDynamicSystemPromptSections: false,
|
|
115
|
+
claimMinWarmTurns: 10,
|
|
116
|
+
claimMinReadRatio: 95,
|
|
117
|
+
claimMinUpliftPoints: 2,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < argv.length; i++) {
|
|
121
|
+
const arg = argv[i];
|
|
122
|
+
if (arg === '--help' || arg === '-h') {
|
|
123
|
+
console.log(usage());
|
|
124
|
+
process.exit(0);
|
|
125
|
+
} else if (arg === '--dry-run') {
|
|
126
|
+
options.dryRun = true;
|
|
127
|
+
} else if (arg === '--json') {
|
|
128
|
+
options.json = true;
|
|
129
|
+
} else if (arg === '--from-file') {
|
|
130
|
+
options.fromFile = path.resolve(readArg(argv, i, arg));
|
|
131
|
+
i++;
|
|
132
|
+
} else if (arg === '--out') {
|
|
133
|
+
options.outputPath = path.resolve(readArg(argv, i, arg));
|
|
134
|
+
i++;
|
|
135
|
+
} else if (arg === '--groups') {
|
|
136
|
+
options.groups = readArg(argv, i, arg).split(',').map(item => item.trim()).filter(Boolean);
|
|
137
|
+
i++;
|
|
138
|
+
} else if (arg === '--turns') {
|
|
139
|
+
options.turnsPerGroup = intArg(readArg(argv, i, arg), arg, 1);
|
|
140
|
+
i++;
|
|
141
|
+
} else if (arg === '--turn-budget-usd') {
|
|
142
|
+
options.turnBudgetUsd = numberArg(readArg(argv, i, arg), arg, 0.01);
|
|
143
|
+
i++;
|
|
144
|
+
} else if (arg === '--total-budget-usd') {
|
|
145
|
+
options.totalBudgetUsd = numberArg(readArg(argv, i, arg), arg, 0.01);
|
|
146
|
+
i++;
|
|
147
|
+
} else if (arg === '--claude-command') {
|
|
148
|
+
options.claudeCommand = readArg(argv, i, arg);
|
|
149
|
+
i++;
|
|
150
|
+
} else if (arg === '--root') {
|
|
151
|
+
options.root = path.resolve(readArg(argv, i, arg));
|
|
152
|
+
i++;
|
|
153
|
+
} else if (arg === '--model') {
|
|
154
|
+
options.model = readArg(argv, i, arg);
|
|
155
|
+
i++;
|
|
156
|
+
} else if (arg === '--timeout-ms') {
|
|
157
|
+
options.timeoutMs = intArg(readArg(argv, i, arg), arg, 1000);
|
|
158
|
+
i++;
|
|
159
|
+
} else if (arg === '--include-raw') {
|
|
160
|
+
options.includeRaw = true;
|
|
161
|
+
} else if (arg === '--exclude-dynamic-system-prompt-sections') {
|
|
162
|
+
options.excludeDynamicSystemPromptSections = true;
|
|
163
|
+
} else if (arg === '--claim-min-warm-turns') {
|
|
164
|
+
options.claimMinWarmTurns = intArg(readArg(argv, i, arg), arg, 1);
|
|
165
|
+
i++;
|
|
166
|
+
} else if (arg === '--claim-min-read-ratio') {
|
|
167
|
+
options.claimMinReadRatio = numberArg(readArg(argv, i, arg), arg, 0);
|
|
168
|
+
i++;
|
|
169
|
+
} else if (arg === '--claim-min-uplift-points') {
|
|
170
|
+
options.claimMinUpliftPoints = numberArg(readArg(argv, i, arg), arg, 0);
|
|
171
|
+
i++;
|
|
172
|
+
} else {
|
|
173
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const id of options.groups) {
|
|
178
|
+
if (!GROUPS[id]) throw new Error(`Unknown group: ${id}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!options.outputPath) options.outputPath = defaultOutputPath();
|
|
182
|
+
return options;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function defaultOutputPath() {
|
|
186
|
+
const stamp = new Date().toISOString().replace(/[-:.]/g, '').replace('T', '-').replace('Z', 'Z');
|
|
187
|
+
return path.join(os.homedir(), '.claude', 'cache-telemetry', `harness-l2-${stamp}.json`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function round(value, decimals = 1) {
|
|
191
|
+
const scale = 10 ** decimals;
|
|
192
|
+
return Math.round((value + Number.EPSILON) * scale) / scale;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function numeric(value) {
|
|
196
|
+
const parsed = Number(value);
|
|
197
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function sha256(text) {
|
|
201
|
+
return crypto.createHash('sha256').update(text).digest('hex');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function promptFor(group, turn) {
|
|
205
|
+
if (turn < group.prompts.length) return group.prompts[turn];
|
|
206
|
+
return 'Do not read files or run tools. In one sentence, restate the cache-first boundary for this same session.';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function claudeArgs(group, options, sessionId) {
|
|
210
|
+
const args = [
|
|
211
|
+
'-p',
|
|
212
|
+
'--output-format',
|
|
213
|
+
'json',
|
|
214
|
+
'--strict-mcp-config',
|
|
215
|
+
'--max-budget-usd',
|
|
216
|
+
String(options.turnBudgetUsd),
|
|
217
|
+
'--max-turns',
|
|
218
|
+
String(group.maxTurns),
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
if (options.excludeDynamicSystemPromptSections) {
|
|
222
|
+
args.push('--exclude-dynamic-system-prompt-sections');
|
|
223
|
+
}
|
|
224
|
+
if (options.model) args.push('--model', options.model);
|
|
225
|
+
if (group.safeMode) args.push('--safe-mode');
|
|
226
|
+
if (group.tools) args.push('--tools', group.tools);
|
|
227
|
+
if (group.disallowedTools) args.push('--disallowedTools', group.disallowedTools);
|
|
228
|
+
if (sessionId) args.push('--resume', sessionId);
|
|
229
|
+
|
|
230
|
+
return args;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function plan(options) {
|
|
234
|
+
return {
|
|
235
|
+
groups: options.groups.map(id => {
|
|
236
|
+
const group = GROUPS[id];
|
|
237
|
+
return {
|
|
238
|
+
id,
|
|
239
|
+
description: group.description,
|
|
240
|
+
turns: Array.from({ length: options.turnsPerGroup }, (_, turn) => {
|
|
241
|
+
const sessionId = turn === 0 ? '' : '<session-id-from-cold-turn>';
|
|
242
|
+
const prompt = promptFor(group, turn);
|
|
243
|
+
return {
|
|
244
|
+
turn,
|
|
245
|
+
command: options.claudeCommand,
|
|
246
|
+
args: claudeArgs(group, options, sessionId),
|
|
247
|
+
prompt,
|
|
248
|
+
promptSha256: sha256(prompt),
|
|
249
|
+
};
|
|
250
|
+
}),
|
|
251
|
+
};
|
|
252
|
+
}),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function sourceEvidence() {
|
|
257
|
+
return {
|
|
258
|
+
localHelpRequired: true,
|
|
259
|
+
localHelpCommand: 'claude --help',
|
|
260
|
+
officialDocs: OFFICIAL_DOCS,
|
|
261
|
+
telemetryFields: [
|
|
262
|
+
'usage.cache_read_input_tokens',
|
|
263
|
+
'usage.cache_creation_input_tokens',
|
|
264
|
+
'context_window.current_usage.cache_read_input_tokens',
|
|
265
|
+
'context_window.current_usage.cache_creation_input_tokens',
|
|
266
|
+
],
|
|
267
|
+
formula: 'cache_read_input_tokens / (input_tokens + cache_creation_input_tokens + cache_read_input_tokens)',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function usageFrom(raw) {
|
|
272
|
+
const usage = raw?.usage
|
|
273
|
+
|| raw?.message?.usage
|
|
274
|
+
|| raw?.context_window?.current_usage
|
|
275
|
+
|| raw?.raw?.usage
|
|
276
|
+
|| raw?.raw?.context_window?.current_usage
|
|
277
|
+
|| {};
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
input_tokens: numeric(usage.input_tokens ?? raw?.input_tokens),
|
|
281
|
+
output_tokens: numeric(usage.output_tokens ?? raw?.output_tokens),
|
|
282
|
+
cache_creation_input_tokens: numeric(usage.cache_creation_input_tokens ?? raw?.cache_creation_input_tokens),
|
|
283
|
+
cache_read_input_tokens: numeric(usage.cache_read_input_tokens ?? raw?.cache_read_input_tokens),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function hasTelemetry(raw, usage) {
|
|
288
|
+
const usageSource = raw?.usage
|
|
289
|
+
|| raw?.message?.usage
|
|
290
|
+
|| raw?.context_window?.current_usage
|
|
291
|
+
|| raw?.raw?.usage
|
|
292
|
+
|| raw?.raw?.context_window?.current_usage
|
|
293
|
+
|| raw;
|
|
294
|
+
return ['input_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens']
|
|
295
|
+
.every(key => Object.prototype.hasOwnProperty.call(usageSource || {}, key)
|
|
296
|
+
&& Number.isFinite(Number(usageSource[key])));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function ratioFromUsage(usage) {
|
|
300
|
+
const total = usage.input_tokens + usage.cache_creation_input_tokens + usage.cache_read_input_tokens;
|
|
301
|
+
return {
|
|
302
|
+
totalInputTokens: total,
|
|
303
|
+
readRatioPercent: total > 0 ? round((usage.cache_read_input_tokens / total) * 100, 1) : 0,
|
|
304
|
+
creationRatioPercent: total > 0 ? round((usage.cache_creation_input_tokens / total) * 100, 1) : 0,
|
|
305
|
+
freshRatioPercent: total > 0 ? round((usage.input_tokens / total) * 100, 1) : 0,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function inferGroup(raw, index) {
|
|
310
|
+
if (raw.group) return raw.group;
|
|
311
|
+
if (typeof raw.label === 'string') {
|
|
312
|
+
for (const id of Object.keys(GROUPS)) {
|
|
313
|
+
if (raw.label.startsWith(id)) return id;
|
|
314
|
+
}
|
|
315
|
+
if (raw.label.startsWith('safe-baseline')) return 'provider-control';
|
|
316
|
+
if (raw.label.startsWith('harness-strict')) return 'harness-thin';
|
|
317
|
+
if (raw.label.startsWith('wf-approx')) return 'wf-light';
|
|
318
|
+
}
|
|
319
|
+
return `unknown-${index}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function inferTurn(raw, index, groupRuns) {
|
|
323
|
+
if (Number.isInteger(raw.turn)) return raw.turn;
|
|
324
|
+
if (typeof raw.label === 'string') {
|
|
325
|
+
const match = raw.label.match(/-(\d+)$/);
|
|
326
|
+
if (match) return Number(match[1]) - 1;
|
|
327
|
+
}
|
|
328
|
+
return groupRuns.get(inferGroup(raw, index)) || 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function normalizeRun(raw, index, groupRuns = new Map()) {
|
|
332
|
+
const group = inferGroup(raw, index);
|
|
333
|
+
const turn = inferTurn(raw, index, groupRuns);
|
|
334
|
+
groupRuns.set(group, turn + 1);
|
|
335
|
+
|
|
336
|
+
const usage = usageFrom(raw);
|
|
337
|
+
const ratios = ratioFromUsage(usage);
|
|
338
|
+
const telemetryPresent = hasTelemetry(raw, usage);
|
|
339
|
+
const explicitSuccess = Object.prototype.hasOwnProperty.call(raw, 'success') ? Boolean(raw.success) : null;
|
|
340
|
+
const rawSubtype = raw.subtype || raw.terminal_reason || raw.raw?.subtype || raw.raw?.terminal_reason || '';
|
|
341
|
+
const subtypeSuccess = rawSubtype ? !/error|fail|budget|timeout/i.test(rawSubtype) : true;
|
|
342
|
+
const success = explicitSuccess === null ? subtypeSuccess : explicitSuccess;
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
group,
|
|
346
|
+
turn,
|
|
347
|
+
label: raw.label || `${group}-${turn + 1}`,
|
|
348
|
+
session_id: raw.session_id || raw.sessionId || raw.raw?.session_id || '',
|
|
349
|
+
success,
|
|
350
|
+
telemetryPresent,
|
|
351
|
+
usage,
|
|
352
|
+
totalInputTokens: ratios.totalInputTokens,
|
|
353
|
+
readRatioPercent: ratios.readRatioPercent,
|
|
354
|
+
creationRatioPercent: ratios.creationRatioPercent,
|
|
355
|
+
freshRatioPercent: ratios.freshRatioPercent,
|
|
356
|
+
total_cost_usd: numeric(raw.total_cost_usd ?? raw.cost_usd ?? raw.raw?.total_cost_usd ?? raw.raw?.cost_usd),
|
|
357
|
+
duration_ms: raw.duration_ms === null ? null : numeric(raw.duration_ms ?? raw.durationMs ?? raw.raw?.duration_ms),
|
|
358
|
+
terminal_reason: raw.terminal_reason || raw.raw?.terminal_reason || '',
|
|
359
|
+
subtype: raw.subtype || raw.raw?.subtype || '',
|
|
360
|
+
error: raw.error || '',
|
|
361
|
+
promptSha256: raw.promptSha256 || '',
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function median(values) {
|
|
366
|
+
if (values.length === 0) return null;
|
|
367
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
368
|
+
const mid = Math.floor(sorted.length / 2);
|
|
369
|
+
return sorted.length % 2 === 0 ? round((sorted[mid - 1] + sorted[mid]) / 2, 1) : sorted[mid];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function percentile(values, pct) {
|
|
373
|
+
if (values.length === 0) return null;
|
|
374
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
375
|
+
const idx = Math.ceil((pct / 100) * sorted.length) - 1;
|
|
376
|
+
return sorted[Math.max(0, Math.min(sorted.length - 1, idx))];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function mean(values, decimals = 1) {
|
|
380
|
+
if (values.length === 0) return null;
|
|
381
|
+
return round(values.reduce((sum, value) => sum + value, 0) / values.length, decimals);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function summarizeRuns(rawRuns, settings) {
|
|
385
|
+
const groupRuns = new Map();
|
|
386
|
+
const runs = rawRuns.map((run, index) => normalizeRun(run, index, groupRuns));
|
|
387
|
+
const byGroup = {};
|
|
388
|
+
|
|
389
|
+
for (const run of runs) {
|
|
390
|
+
byGroup[run.group] ||= [];
|
|
391
|
+
byGroup[run.group].push(run);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const groups = {};
|
|
395
|
+
for (const [group, items] of Object.entries(byGroup)) {
|
|
396
|
+
const warm = items.filter(item => item.turn > 0 && item.telemetryPresent);
|
|
397
|
+
const warmReadRatios = warm.map(item => item.readRatioPercent);
|
|
398
|
+
const durations = items.map(item => item.duration_ms).filter(value => Number.isFinite(value) && value > 0);
|
|
399
|
+
const warmDurations = warm.map(item => item.duration_ms).filter(value => Number.isFinite(value) && value > 0);
|
|
400
|
+
const successCount = items.filter(item => item.success).length;
|
|
401
|
+
const telemetryCount = items.filter(item => item.telemetryPresent).length;
|
|
402
|
+
|
|
403
|
+
groups[group] = {
|
|
404
|
+
totalTurns: items.length,
|
|
405
|
+
successfulTurns: successCount,
|
|
406
|
+
successRate: items.length ? round(successCount / items.length, 3) : 0,
|
|
407
|
+
telemetryTurns: telemetryCount,
|
|
408
|
+
warmTurns: warm.length,
|
|
409
|
+
coldReadRatioPercent: items.find(item => item.turn === 0)?.readRatioPercent ?? null,
|
|
410
|
+
warmReadRatioPercent: warmReadRatios,
|
|
411
|
+
warmMinReadRatioPercent: warmReadRatios.length ? Math.min(...warmReadRatios) : null,
|
|
412
|
+
warmMedianReadRatioPercent: median(warmReadRatios),
|
|
413
|
+
warmMeanReadRatioPercent: mean(warmReadRatios),
|
|
414
|
+
warmMaxReadRatioPercent: warmReadRatios.length ? Math.max(...warmReadRatios) : null,
|
|
415
|
+
medianDurationMs: median(durations),
|
|
416
|
+
p95DurationMs: percentile(durations, 95),
|
|
417
|
+
maxDurationMs: durations.length ? Math.max(...durations) : null,
|
|
418
|
+
warmMedianDurationMs: median(warmDurations),
|
|
419
|
+
warmP95DurationMs: percentile(warmDurations, 95),
|
|
420
|
+
totalCostUsd: round(items.reduce((sum, item) => sum + item.total_cost_usd, 0), 6),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const totalCostUsd = round(runs.reduce((sum, run) => sum + run.total_cost_usd, 0), 6);
|
|
425
|
+
return {
|
|
426
|
+
totalTurns: runs.length,
|
|
427
|
+
totalCostUsd,
|
|
428
|
+
groups,
|
|
429
|
+
claimGate: claimGate(groups, settings),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function claimGate(groups, settings) {
|
|
434
|
+
const measuredReasons = [];
|
|
435
|
+
const improvementReasons = [];
|
|
436
|
+
const harnessGroups = ['harness-thin', 'wf-light'].filter(group => groups[group]);
|
|
437
|
+
|
|
438
|
+
if (harnessGroups.length === 0) {
|
|
439
|
+
measuredReasons.push('no Harness telemetry group found');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (const group of harnessGroups) {
|
|
443
|
+
const summary = groups[group];
|
|
444
|
+
if (summary.warmTurns < settings.claimMinWarmTurns) {
|
|
445
|
+
measuredReasons.push(`${group} needs at least ${settings.claimMinWarmTurns} warm telemetry turns`);
|
|
446
|
+
}
|
|
447
|
+
if (summary.successRate < 0.95) {
|
|
448
|
+
measuredReasons.push(`${group} success rate ${summary.successRate} is below 0.95`);
|
|
449
|
+
}
|
|
450
|
+
if (summary.telemetryTurns < summary.totalTurns) {
|
|
451
|
+
measuredReasons.push(`${group} has turns without L2 usage telemetry`);
|
|
452
|
+
}
|
|
453
|
+
if ((summary.warmMedianReadRatioPercent ?? 0) < settings.claimMinReadRatio) {
|
|
454
|
+
measuredReasons.push(`${group} warm median read ratio is below ${settings.claimMinReadRatio}%`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (!groups['provider-control']) {
|
|
459
|
+
improvementReasons.push('provider-control group is required before claiming improvement');
|
|
460
|
+
} else {
|
|
461
|
+
const control = groups['provider-control'].warmMedianReadRatioPercent;
|
|
462
|
+
if (control === null) improvementReasons.push('provider-control has no warm telemetry turns');
|
|
463
|
+
for (const group of harnessGroups) {
|
|
464
|
+
const harness = groups[group].warmMedianReadRatioPercent;
|
|
465
|
+
if (harness === null) continue;
|
|
466
|
+
if (control !== null && harness - control < settings.claimMinUpliftPoints) {
|
|
467
|
+
improvementReasons.push(`${group} uplift over provider-control is below ${settings.claimMinUpliftPoints} percentage points`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const measuredWarmCacheClaimAllowed = measuredReasons.length === 0;
|
|
473
|
+
const improvementClaimAllowed = measuredWarmCacheClaimAllowed && improvementReasons.length === 0;
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
measuredWarmCacheClaimAllowed,
|
|
477
|
+
improvementClaimAllowed,
|
|
478
|
+
reasons: [...measuredReasons, ...improvementReasons],
|
|
479
|
+
thresholds: {
|
|
480
|
+
minWarmTurnsPerHarnessGroup: settings.claimMinWarmTurns,
|
|
481
|
+
minWarmMedianReadRatioPercent: settings.claimMinReadRatio,
|
|
482
|
+
minUpliftPointsOverProviderControl: settings.claimMinUpliftPoints,
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function parseClaudeJson(stdout) {
|
|
488
|
+
const trimmed = String(stdout || '').trim();
|
|
489
|
+
if (!trimmed) return { parsed: null, error: 'empty stdout' };
|
|
490
|
+
try {
|
|
491
|
+
return { parsed: JSON.parse(trimmed), error: '' };
|
|
492
|
+
} catch (err) {
|
|
493
|
+
const first = trimmed.indexOf('{');
|
|
494
|
+
const last = trimmed.lastIndexOf('}');
|
|
495
|
+
if (first >= 0 && last > first) {
|
|
496
|
+
try {
|
|
497
|
+
return { parsed: JSON.parse(trimmed.slice(first, last + 1)), error: '' };
|
|
498
|
+
} catch {
|
|
499
|
+
// Fall through to the original parse error.
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return { parsed: null, error: err.message };
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function truncate(text, max = 4000) {
|
|
507
|
+
const value = String(text || '');
|
|
508
|
+
return value.length > max ? `${value.slice(0, max)}...<truncated>` : value;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function spawnClaude(command, args, options) {
|
|
512
|
+
if (process.platform === 'win32') {
|
|
513
|
+
return spawnSync('cmd.exe', ['/d', '/s', '/c', command, ...args], options);
|
|
514
|
+
}
|
|
515
|
+
return spawnSync(command, args, options);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function runClaudeTurn({ options, groupId, turn, sessionId }) {
|
|
519
|
+
const group = GROUPS[groupId];
|
|
520
|
+
const prompt = promptFor(group, turn);
|
|
521
|
+
const args = claudeArgs(group, options, sessionId);
|
|
522
|
+
const started = Date.now();
|
|
523
|
+
const child = spawnClaude(options.claudeCommand, args, {
|
|
524
|
+
cwd: options.root,
|
|
525
|
+
input: prompt,
|
|
526
|
+
encoding: 'utf8',
|
|
527
|
+
timeout: options.timeoutMs,
|
|
528
|
+
windowsHide: true,
|
|
529
|
+
env: { ...process.env, NO_COLOR: '1' },
|
|
530
|
+
});
|
|
531
|
+
const durationMs = Date.now() - started;
|
|
532
|
+
const parseResult = parseClaudeJson(child.stdout);
|
|
533
|
+
const parsed = parseResult.parsed || {};
|
|
534
|
+
const usage = usageFrom(parsed);
|
|
535
|
+
const ratios = ratioFromUsage(usage);
|
|
536
|
+
const telemetryPresent = parseResult.parsed ? hasTelemetry(parsed, usage) : false;
|
|
537
|
+
const exitCode = child.status;
|
|
538
|
+
const timedOut = child.error?.code === 'ETIMEDOUT';
|
|
539
|
+
const subtype = parsed.subtype || parsed.terminal_reason || '';
|
|
540
|
+
const success = exitCode === 0
|
|
541
|
+
&& Boolean(parseResult.parsed)
|
|
542
|
+
&& telemetryPresent
|
|
543
|
+
&& !parsed.is_error
|
|
544
|
+
&& !/error|fail|budget|timeout/i.test(subtype)
|
|
545
|
+
&& !timedOut;
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
group: groupId,
|
|
549
|
+
turn,
|
|
550
|
+
label: `${groupId}-${turn + 1}`,
|
|
551
|
+
command: options.claudeCommand,
|
|
552
|
+
spawnVia: process.platform === 'win32' ? 'cmd.exe /d /s /c' : 'direct',
|
|
553
|
+
args,
|
|
554
|
+
promptSha256: sha256(prompt),
|
|
555
|
+
session_id: parsed.session_id || '',
|
|
556
|
+
success,
|
|
557
|
+
telemetryPresent,
|
|
558
|
+
usage,
|
|
559
|
+
totalInputTokens: ratios.totalInputTokens,
|
|
560
|
+
readRatioPercent: ratios.readRatioPercent,
|
|
561
|
+
creationRatioPercent: ratios.creationRatioPercent,
|
|
562
|
+
freshRatioPercent: ratios.freshRatioPercent,
|
|
563
|
+
total_cost_usd: numeric(parsed.total_cost_usd ?? parsed.cost_usd),
|
|
564
|
+
duration_ms: numeric(parsed.duration_ms) || durationMs,
|
|
565
|
+
terminal_reason: parsed.terminal_reason || '',
|
|
566
|
+
subtype: parsed.subtype || '',
|
|
567
|
+
error: child.error?.message || parseResult.error || (exitCode === 0 ? '' : truncate(child.stderr)),
|
|
568
|
+
resultPreview: parseResult.parsed ? truncate(parsed.result, 500) : '',
|
|
569
|
+
raw: options.includeRaw && parseResult.parsed ? parsed : undefined,
|
|
570
|
+
stdout: parseResult.parsed ? undefined : truncate(child.stdout),
|
|
571
|
+
stderr: parseResult.parsed ? truncate(child.stderr, 1000) : truncate(child.stderr),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function loadRunsFromFile(file) {
|
|
576
|
+
const body = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
577
|
+
if (Array.isArray(body)) return body;
|
|
578
|
+
if (Array.isArray(body.runs)) return body.runs;
|
|
579
|
+
throw new Error(`No runs[] array found in ${file}`);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function settingsForReport(options) {
|
|
583
|
+
return {
|
|
584
|
+
groups: options.groups,
|
|
585
|
+
turnsPerGroup: options.turnsPerGroup,
|
|
586
|
+
turnBudgetUsd: options.turnBudgetUsd,
|
|
587
|
+
totalBudgetUsd: options.totalBudgetUsd,
|
|
588
|
+
claudeCommand: options.claudeCommand,
|
|
589
|
+
root: options.root,
|
|
590
|
+
model: options.model,
|
|
591
|
+
timeoutMs: options.timeoutMs,
|
|
592
|
+
includeRaw: options.includeRaw,
|
|
593
|
+
excludeDynamicSystemPromptSections: options.excludeDynamicSystemPromptSections,
|
|
594
|
+
claimMinWarmTurns: options.claimMinWarmTurns,
|
|
595
|
+
claimMinReadRatio: options.claimMinReadRatio,
|
|
596
|
+
claimMinUpliftPoints: options.claimMinUpliftPoints,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function baseReport(options, mode) {
|
|
601
|
+
return {
|
|
602
|
+
schema: SCHEMA,
|
|
603
|
+
mode,
|
|
604
|
+
generatedAt: new Date().toISOString(),
|
|
605
|
+
outputPath: options.outputPath,
|
|
606
|
+
settings: settingsForReport(options),
|
|
607
|
+
sourceEvidence: sourceEvidence(),
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function realRun(options) {
|
|
612
|
+
const runs = [];
|
|
613
|
+
let reportedCost = 0;
|
|
614
|
+
let stoppedReason = '';
|
|
615
|
+
|
|
616
|
+
for (const groupId of options.groups) {
|
|
617
|
+
let sessionId = '';
|
|
618
|
+
for (let turn = 0; turn < options.turnsPerGroup; turn++) {
|
|
619
|
+
const run = runClaudeTurn({ options, groupId, turn, sessionId });
|
|
620
|
+
runs.push(run);
|
|
621
|
+
reportedCost = round(reportedCost + run.total_cost_usd, 6);
|
|
622
|
+
|
|
623
|
+
if (!sessionId && run.session_id) sessionId = run.session_id;
|
|
624
|
+
if (turn === 0 && !sessionId) {
|
|
625
|
+
stoppedReason = `${groupId} cold turn did not return a session_id`;
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
if (reportedCost >= options.totalBudgetUsd) {
|
|
629
|
+
stoppedReason = `reported total cost ${reportedCost} reached --total-budget-usd ${options.totalBudgetUsd}`;
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (stoppedReason) break;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const report = {
|
|
637
|
+
...baseReport(options, 'run'),
|
|
638
|
+
plan: plan(options),
|
|
639
|
+
runs,
|
|
640
|
+
summary: summarizeRuns(runs, options),
|
|
641
|
+
stoppedReason,
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
fs.mkdirSync(path.dirname(options.outputPath), { recursive: true });
|
|
645
|
+
fs.writeFileSync(options.outputPath, JSON.stringify(report, null, 2) + '\n', 'utf8');
|
|
646
|
+
return report;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function printText(report) {
|
|
650
|
+
if (report.mode === 'dry-run') {
|
|
651
|
+
console.log(`Dry run: ${report.plan.groups.length} group(s), ${report.settings.turnsPerGroup} turn(s) each.`);
|
|
652
|
+
console.log(`Output path for real run: ${report.outputPath}`);
|
|
653
|
+
for (const group of report.plan.groups) {
|
|
654
|
+
console.log(`- ${group.id}: ${group.description}`);
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
console.log(`L2 telemetry ${report.mode}: ${report.summary.totalTurns} turn(s), $${report.summary.totalCostUsd}`);
|
|
660
|
+
for (const [group, summary] of Object.entries(report.summary.groups)) {
|
|
661
|
+
console.log(
|
|
662
|
+
`- ${group}: success ${summary.successfulTurns}/${summary.totalTurns}, ` +
|
|
663
|
+
`warm median read ${summary.warmMedianReadRatioPercent ?? 'n/a'}%, ` +
|
|
664
|
+
`warm turns ${summary.warmTurns}`,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
console.log(`Measured README claim allowed: ${report.summary.claimGate.measuredWarmCacheClaimAllowed}`);
|
|
668
|
+
console.log(`Improvement README claim allowed: ${report.summary.claimGate.improvementClaimAllowed}`);
|
|
669
|
+
if (report.summary.claimGate.reasons.length) {
|
|
670
|
+
console.log('Claim gate reasons:');
|
|
671
|
+
for (const reason of report.summary.claimGate.reasons) console.log(`- ${reason}`);
|
|
672
|
+
}
|
|
673
|
+
if (report.mode === 'run') console.log(`Report written: ${report.outputPath}`);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
const options = parseArgs(process.argv.slice(2));
|
|
678
|
+
let report;
|
|
679
|
+
|
|
680
|
+
if (options.fromFile) {
|
|
681
|
+
const runs = loadRunsFromFile(options.fromFile);
|
|
682
|
+
report = {
|
|
683
|
+
...baseReport(options, 'from-file'),
|
|
684
|
+
inputPath: options.fromFile,
|
|
685
|
+
runs: runs.map((run, index) => normalizeRun(run, index)),
|
|
686
|
+
summary: summarizeRuns(runs, options),
|
|
687
|
+
};
|
|
688
|
+
} else if (options.dryRun) {
|
|
689
|
+
report = {
|
|
690
|
+
...baseReport(options, 'dry-run'),
|
|
691
|
+
plan: plan(options),
|
|
692
|
+
summary: summarizeRuns([], options),
|
|
693
|
+
};
|
|
694
|
+
} else {
|
|
695
|
+
report = realRun(options);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
699
|
+
else printText(report);
|
|
700
|
+
} catch (err) {
|
|
701
|
+
console.error(`l2-cache-telemetry failed: ${err.message}`);
|
|
702
|
+
process.exit(1);
|
|
703
|
+
}
|