@event4u/agent-config 12.1.0 → 13.0.0
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +151 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/cost/report.md +23 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
- package/dist/agent-src/contexts/subagent-configuration.md +69 -7
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
- package/dist/agent-src/rules/evaluator-independence.md +27 -5
- package/dist/agent-src/rules/git-history-discipline.md +5 -1
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +9 -4
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +17 -2
- package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +4 -4
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-3B72LSZL.js} +1 -1
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-N7FEST6D.js} +17 -17
- package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +43 -22
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +936 -346
- package/dist/install/install.mjs +12 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1192 -160
- package/dist/router.json +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/guidelines/design-fidelity-mechanics.md +21 -0
- package/docs/guidelines/design-handover-extraction.md +87 -0
- package/docs/guidelines/prompt-templates.md +24 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +17 -2
- package/src/config/evaluator-budgets.json +5 -4
- package/src/config/gate-coverage.yml +33 -11
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/hook-token-budget.json +8 -1
- package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
- package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
- package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
- package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
- package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
- package/src/scripts/_lib/skill_catalogue.ts +258 -5
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- package/src/scripts/ai_council/clients.ts +21 -21
- package/src/scripts/bench_ab_v2_complexity.ts +252 -0
- package/src/scripts/bench_ab_v2_run.ts +3 -42
- package/src/scripts/bench_ab_v2_safety.ts +296 -0
- package/src/scripts/bench_ab_v2_search.ts +343 -0
- package/src/scripts/bench_ab_v2_stats.ts +191 -0
- package/src/scripts/build_archive_index.ts +383 -0
- package/src/scripts/capture_skill_catalogue.ts +107 -2
- package/src/scripts/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/cost/backfill_rates.mjs +298 -0
- package/src/scripts/cost/budget.mjs +12 -32
- package/src/scripts/cost_summary.ts +25 -0
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +28 -2
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_skill_router_head.ts +21 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
- package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
- package/src/scripts/mcp_server/tools.ts +87 -0
- package/src/scripts/release.ts +5 -5
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/schemas/command.schema.json +1 -1
- package/src/scripts/schemas/skill.schema.json +18 -1
- package/src/scripts/trigger_coverage.ts +224 -3
- package/src/server/schemas/settings.ts +2 -2
- package/src/config/budget-routing.json +0 -7
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cost-backfill — re-price ledger rows that `track.mjs` flagged `rate_missing`,
|
|
3
|
+
// using a rate table supplied for the model ids that had none at capture time.
|
|
4
|
+
//
|
|
5
|
+
// Why this exists: `track.mjs` prices an unrecognised model at $0 rather than
|
|
6
|
+
// throwing, because losing a whole session of real token counts over one
|
|
7
|
+
// unknown id is worse than an understated total. It keeps the token counts and
|
|
8
|
+
// sets `rate_missing` precisely so the cost can be repaired later. This is that
|
|
9
|
+
// repair pass.
|
|
10
|
+
//
|
|
11
|
+
// BUILT AGAINST AN OBSERVED ROW, NOT A GUESSED ONE. The shape below was read
|
|
12
|
+
// off a real flagged row produced from this repo's own transcripts on
|
|
13
|
+
// 2026-08-16 (`claude-fable-5`, 468 messages, 236,695,963 cache-read tokens,
|
|
14
|
+
// cost 0). Two limits are properties of that observed shape, and both are
|
|
15
|
+
// stated rather than papered over — a re-priced row that looks exact while
|
|
16
|
+
// resting on an invented split would be worse than the zero it replaces:
|
|
17
|
+
//
|
|
18
|
+
// 1. THE CACHE-WRITE TTL SPLIT IS NOT RETAINED ON THE ROW. `track.mjs`
|
|
19
|
+
// prices `ephemeral_5m` and `ephemeral_1h` cache writes at different
|
|
20
|
+
// rates (1.25x / 2x of input), but `byModel[m]` aggregates both into one
|
|
21
|
+
// `cache_creation_input_tokens` tally. A backfill therefore prices every
|
|
22
|
+
// cache write at the 5m rate — Anthropic's default TTL, and the same
|
|
23
|
+
// fallback `costForUsage` already applies to any unaccounted remainder.
|
|
24
|
+
// A session that really used 1h writes is under-priced by the difference.
|
|
25
|
+
// Recorded per row as `cache_ttl_assumed: "5m"`.
|
|
26
|
+
//
|
|
27
|
+
// 2. `byBucket[*].cost_usd` IS NOT REPAIRED. The row records per-bucket
|
|
28
|
+
// totals and per-model totals, but not per-bucket-per-model tokens, so
|
|
29
|
+
// the recovered cost cannot be attributed across main vs subagent. The
|
|
30
|
+
// alternative is not a better number but an invented one — the same
|
|
31
|
+
// honest-limit stance `cost-summary-schema` takes for `by_date`. The
|
|
32
|
+
// bucket figures stay as captured and the report says so.
|
|
33
|
+
//
|
|
34
|
+
// Rates file (JSON), either form per model id:
|
|
35
|
+
// { "claude-fable-5": "opus" } // reuse a known tier's rates
|
|
36
|
+
// { "claude-fable-5": { "input": 5, "output": 25, // explicit USD per 1M
|
|
37
|
+
// "cache_write_5m": 6.25, "cache_read": 0.5 } }
|
|
38
|
+
// Every rate must be a finite number >= 0, and an all-zero table is refused.
|
|
39
|
+
// `cache_write_1h` is accepted but unused — see RATE_KEYS.
|
|
40
|
+
//
|
|
41
|
+
// Usage: node src/scripts/cost/backfill_rates.mjs --rates <file.json>
|
|
42
|
+
// [--store <ledger.jsonl>] [--apply]
|
|
43
|
+
// Env:
|
|
44
|
+
// BACKFILL_STORE=<path> ledger to re-price (default: agents/cost-tracking/sessions.jsonl)
|
|
45
|
+
// BACKFILL_RATES=<path> rates file (same as --rates)
|
|
46
|
+
// BACKFILL_APPLY=1 write the result (same as --apply); default is a dry run
|
|
47
|
+
// BACKFILL_QUIET=1 suppress the report; exit code and writes are unchanged
|
|
48
|
+
|
|
49
|
+
import { readFileSync, writeFileSync, renameSync, existsSync } from 'node:fs';
|
|
50
|
+
|
|
51
|
+
const DEFAULT_STORE = 'agents/cost-tracking/sessions.jsonl';
|
|
52
|
+
|
|
53
|
+
// Mirrored from track.mjs PRICING. It is a copy rather than an import for the
|
|
54
|
+
// reason that file's own header gives: track.mjs exports nothing and calls
|
|
55
|
+
// main() at module scope, so importing it would scan the developer's real
|
|
56
|
+
// ~/.claude as a side effect. Keep the two in step.
|
|
57
|
+
const PRICING = {
|
|
58
|
+
haiku: { input: 0.25, output: 1.25, cache_write_5m: 0.30, cache_write_1h: 0.50, cache_read: 0.03 },
|
|
59
|
+
sonnet: { input: 3.00, output: 15.00, cache_write_5m: 3.75, cache_write_1h: 6.00, cache_read: 0.30 },
|
|
60
|
+
opus: { input: 15.00, output: 75.00, cache_write_5m: 18.75, cache_write_1h: 30.00, cache_read: 1.50 },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// The keys `costForModelEntry` actually reads. `cache_write_1h` is deliberately
|
|
64
|
+
// NOT here: the row does not retain the TTL split (honest limit 1), so a 1h
|
|
65
|
+
// rate cannot affect any output, and requiring a number that provably changes
|
|
66
|
+
// nothing is a usability wart rather than a safeguard. It is accepted when
|
|
67
|
+
// present — a tier alias carries one — and ignored. A future 1h-aware pass
|
|
68
|
+
// would need the producer to keep the split first, at which point this list is
|
|
69
|
+
// where the key goes back.
|
|
70
|
+
const RATE_KEYS = ['input', 'output', 'cache_write_5m', 'cache_read'];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A usable rate is finite and non-negative.
|
|
74
|
+
* `typeof x === 'number'` is NOT enough, and the gap is not theoretical: JSON
|
|
75
|
+
* parses `1e999` to `Infinity`, which prices to `null` in the written row;
|
|
76
|
+
* a negative rate writes a negative total. Both then clear `rate_missing`,
|
|
77
|
+
* which is worse than the silent zero 2.4 removed — the row now carries a
|
|
78
|
+
* confident wrong figure with its own warning switched off, and the token
|
|
79
|
+
* counts that made a re-price possible have already been overwritten.
|
|
80
|
+
*/
|
|
81
|
+
function isUsableRate(v) {
|
|
82
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve one rates-file entry to `{ tier, rates }`.
|
|
87
|
+
* A string is a tier alias; an object is an explicit table. Anything else, a
|
|
88
|
+
* table missing a key, an unusable value, or an all-zero table is rejected by
|
|
89
|
+
* name — a silently-defaulted rate would reintroduce the understatement this
|
|
90
|
+
* pass exists to remove.
|
|
91
|
+
*/
|
|
92
|
+
function resolveRate(model, spec) {
|
|
93
|
+
if (typeof spec === 'string') {
|
|
94
|
+
const rates = PRICING[spec];
|
|
95
|
+
if (!rates) throw new Error(`rates["${model}"]: unknown tier alias "${spec}" (known: ${Object.keys(PRICING).join(', ')})`);
|
|
96
|
+
return { tier: spec, rates };
|
|
97
|
+
}
|
|
98
|
+
if (spec && typeof spec === 'object') {
|
|
99
|
+
const missing = RATE_KEYS.filter((k) => spec[k] === undefined);
|
|
100
|
+
if (missing.length) throw new Error(`rates["${model}"]: missing ${missing.join(', ')}`);
|
|
101
|
+
const unusable = RATE_KEYS.filter((k) => !isUsableRate(spec[k]));
|
|
102
|
+
if (unusable.length) throw new Error(`rates["${model}"]: ${unusable.map((k) => `${k}=${JSON.stringify(spec[k])}`).join(', ')} — a rate must be a finite number >= 0`);
|
|
103
|
+
// An all-zero table is not a price, it is the absence of one, and no tier
|
|
104
|
+
// in PRICING is free. Accepting it would re-create the exact case the flag
|
|
105
|
+
// exists to distinguish: a $0 row that reads as cheap work.
|
|
106
|
+
if (RATE_KEYS.every((k) => spec[k] === 0)) throw new Error(`rates["${model}"]: an all-zero table is not a price — it re-creates the $0 row rate_missing exists to flag`);
|
|
107
|
+
return { tier: 'backfilled', rates: spec };
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`rates["${model}"]: expected a tier alias or a rate table, got ${typeof spec}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Cost for one `byModel` entry at the supplied rates.
|
|
114
|
+
* Cache writes price at the 5m rate — see honest limit 1 in the header.
|
|
115
|
+
*/
|
|
116
|
+
function costForModelEntry(entry, rates) {
|
|
117
|
+
return (entry.input_tokens || 0) / 1e6 * rates.input
|
|
118
|
+
+ (entry.output_tokens || 0) / 1e6 * rates.output
|
|
119
|
+
+ (entry.cache_creation_input_tokens || 0) / 1e6 * rates.cache_write_5m
|
|
120
|
+
+ (entry.cache_read_input_tokens || 0) / 1e6 * rates.cache_read;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Mirrors track.mjs's flag condition: a row with no billable tokens costs 0 at any rate. */
|
|
124
|
+
function billableTokens(entry) {
|
|
125
|
+
if (!entry) return 0;
|
|
126
|
+
return (entry.input_tokens || 0) + (entry.output_tokens || 0)
|
|
127
|
+
+ (entry.cache_creation_input_tokens || 0) + (entry.cache_read_input_tokens || 0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Re-price one row in place. Returns the per-model repairs made, or [] when
|
|
132
|
+
* the row is not flagged, carries no supplied rate, or was already backfilled
|
|
133
|
+
* for that model — which is what makes a second run a no-op rather than a
|
|
134
|
+
* double count.
|
|
135
|
+
*/
|
|
136
|
+
export function backfillRow(row, rateTable) {
|
|
137
|
+
if (!row || row.rate_missing !== true) return [];
|
|
138
|
+
const already = new Set((row.rate_backfill || []).map((b) => b.model));
|
|
139
|
+
const repairs = [];
|
|
140
|
+
|
|
141
|
+
for (const model of row.rate_missing_models || []) {
|
|
142
|
+
if (already.has(model)) continue;
|
|
143
|
+
const spec = rateTable[model];
|
|
144
|
+
if (!spec) continue;
|
|
145
|
+
const entry = (row.byModel || {})[model];
|
|
146
|
+
if (!entry || billableTokens(entry) <= 0) continue;
|
|
147
|
+
|
|
148
|
+
const { tier, rates } = spec;
|
|
149
|
+
const cost = costForModelEntry(entry, rates);
|
|
150
|
+
const previous = entry.cost_usd || 0;
|
|
151
|
+
const delta = cost - previous;
|
|
152
|
+
|
|
153
|
+
entry.cost_usd = cost;
|
|
154
|
+
entry.tier = tier;
|
|
155
|
+
row.byTier = row.byTier || {};
|
|
156
|
+
row.byTier[tier] = (row.byTier[tier] || 0) + delta;
|
|
157
|
+
row.total_cost_usd = (row.total_cost_usd || 0) + delta;
|
|
158
|
+
|
|
159
|
+
repairs.push({ model, tier, cost_usd: cost, delta_usd: delta });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!repairs.length) return [];
|
|
163
|
+
|
|
164
|
+
row.rate_backfill = [
|
|
165
|
+
...(row.rate_backfill || []),
|
|
166
|
+
...repairs.map((r) => ({
|
|
167
|
+
model: r.model,
|
|
168
|
+
tier: r.tier,
|
|
169
|
+
cost_usd: r.cost_usd,
|
|
170
|
+
cache_ttl_assumed: '5m',
|
|
171
|
+
bucket_split_repaired: false,
|
|
172
|
+
at: new Date().toISOString(),
|
|
173
|
+
})),
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
// Whatever is still unpriced keeps the flag. A row re-priced for one model
|
|
177
|
+
// and not another is still understated, and saying otherwise is the exact
|
|
178
|
+
// silent zero 2.4 removed.
|
|
179
|
+
const remaining = (row.rate_missing_models || []).filter(
|
|
180
|
+
(m) => !row.rate_backfill.some((b) => b.model === m),
|
|
181
|
+
);
|
|
182
|
+
row.rate_missing_models = remaining;
|
|
183
|
+
row.rate_missing = remaining.length > 0;
|
|
184
|
+
|
|
185
|
+
return repairs;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseArgs(argv) {
|
|
189
|
+
const out = { rates: process.env.BACKFILL_RATES || null, store: process.env.BACKFILL_STORE || DEFAULT_STORE, apply: process.env.BACKFILL_APPLY === '1' };
|
|
190
|
+
for (let i = 0; i < argv.length; i++) {
|
|
191
|
+
const a = argv[i];
|
|
192
|
+
if (a === '--apply') out.apply = true;
|
|
193
|
+
else if (a === '--rates') out.rates = argv[++i];
|
|
194
|
+
else if (a === '--store') out.store = argv[++i];
|
|
195
|
+
else if (a.startsWith('--rates=')) out.rates = a.slice('--rates='.length);
|
|
196
|
+
else if (a.startsWith('--store=')) out.store = a.slice('--store='.length);
|
|
197
|
+
else throw new Error(`unknown argument: ${a}`);
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function main() {
|
|
203
|
+
let args;
|
|
204
|
+
try {
|
|
205
|
+
args = parseArgs(process.argv.slice(2));
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.error(`cost-backfill: ${e.message}`);
|
|
208
|
+
process.exit(2);
|
|
209
|
+
}
|
|
210
|
+
if (!args.rates) {
|
|
211
|
+
console.error('cost-backfill: --rates <file.json> is required (or BACKFILL_RATES)');
|
|
212
|
+
process.exit(2);
|
|
213
|
+
}
|
|
214
|
+
if (!existsSync(args.rates)) {
|
|
215
|
+
console.error(`cost-backfill: rates file not found: ${args.rates}`);
|
|
216
|
+
process.exit(2);
|
|
217
|
+
}
|
|
218
|
+
if (!existsSync(args.store)) {
|
|
219
|
+
console.error(`cost-backfill: ledger not found: ${args.store}`);
|
|
220
|
+
process.exit(2);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let rateTable;
|
|
224
|
+
try {
|
|
225
|
+
const raw = JSON.parse(readFileSync(args.rates, 'utf-8'));
|
|
226
|
+
rateTable = Object.fromEntries(Object.entries(raw).map(([m, spec]) => [m, resolveRate(m, spec)]));
|
|
227
|
+
} catch (e) {
|
|
228
|
+
console.error(`cost-backfill: ${e.message}`);
|
|
229
|
+
process.exit(2);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const before = readFileSync(args.store, 'utf-8');
|
|
233
|
+
const lines = before.split('\n').filter((l) => l.trim().length > 0);
|
|
234
|
+
const rows = [];
|
|
235
|
+
const repaired = [];
|
|
236
|
+
let flaggedRows = 0;
|
|
237
|
+
|
|
238
|
+
for (const line of lines) {
|
|
239
|
+
let row;
|
|
240
|
+
try {
|
|
241
|
+
row = JSON.parse(line);
|
|
242
|
+
} catch {
|
|
243
|
+
// A malformed line is preserved verbatim rather than dropped: this pass
|
|
244
|
+
// repairs prices, it does not curate the ledger.
|
|
245
|
+
rows.push(line);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (row.rate_missing === true) flaggedRows++;
|
|
249
|
+
const repairs = backfillRow(row, rateTable);
|
|
250
|
+
if (repairs.length) repaired.push({ sessionId: row.sessionId, repairs, stillMissing: row.rate_missing_models });
|
|
251
|
+
rows.push(JSON.stringify(row));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (args.apply && repaired.length) {
|
|
255
|
+
// This is a read-modify-REWRITE against a store whose producer APPENDS
|
|
256
|
+
// (`track.mjs` uses appendFileSync). temp-then-rename makes the write
|
|
257
|
+
// atomic; it does not make the read-write pair safe — a `track.mjs` run
|
|
258
|
+
// landing in between would be silently discarded by the rename. There is
|
|
259
|
+
// no lock, so the honest move is to detect the collision and refuse rather
|
|
260
|
+
// than to overwrite: on an append-only cost ledger a lost row is silent
|
|
261
|
+
// data loss, and re-running the pass afterwards costs nothing.
|
|
262
|
+
if (readFileSync(args.store, 'utf-8') !== before) {
|
|
263
|
+
console.error('cost-backfill: the ledger changed while this pass was running — refusing to overwrite. Re-run it.');
|
|
264
|
+
process.exit(2);
|
|
265
|
+
}
|
|
266
|
+
const tmp = `${args.store}.backfill.tmp`;
|
|
267
|
+
writeFileSync(tmp, rows.join('\n') + '\n');
|
|
268
|
+
renameSync(tmp, args.store);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (process.env.BACKFILL_QUIET === '1') return;
|
|
272
|
+
|
|
273
|
+
const total = repaired.reduce((s, r) => s + r.repairs.reduce((t, x) => t + x.delta_usd, 0), 0);
|
|
274
|
+
console.log(`# cost-backfill — ${args.apply ? 'APPLIED' : 'DRY RUN (pass --apply to write)'}`);
|
|
275
|
+
console.log('');
|
|
276
|
+
console.log(`Ledger: \`${args.store}\` · rows ${lines.length} · flagged ${flaggedRows} · re-priced ${repaired.length}`);
|
|
277
|
+
console.log('');
|
|
278
|
+
if (!repaired.length) {
|
|
279
|
+
console.log('No row was re-priced — either nothing is flagged, or no rate was supplied for the flagged ids.');
|
|
280
|
+
} else {
|
|
281
|
+
console.log('| Session | Model | Tier | Recovered | Still missing |');
|
|
282
|
+
console.log('|---|---|---|---:|---|');
|
|
283
|
+
for (const r of repaired) {
|
|
284
|
+
for (const x of r.repairs) {
|
|
285
|
+
console.log(`| \`${(r.sessionId || '').slice(0, 8)}\` | \`${x.model}\` | ${x.tier} | $${x.delta_usd.toFixed(6)} | ${r.stillMissing.length ? r.stillMissing.join(', ') : '—'} |`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
console.log('');
|
|
289
|
+
console.log(`**Recovered total: $${total.toFixed(6)}** — previously reported as $0.`);
|
|
290
|
+
}
|
|
291
|
+
console.log('');
|
|
292
|
+
console.log('Two limits of the recovered figure, both properties of the stored row:');
|
|
293
|
+
console.log('- Cache writes price at the **5m** rate; the row does not retain the 5m/1h TTL split, so a session that used 1h writes stays under-priced by the difference.');
|
|
294
|
+
console.log('- `byBucket[*].cost_usd` is **not** repaired — the row carries no per-bucket-per-model tokens, so the recovered cost cannot be attributed to main vs subagent.');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Importable for tests; runs only when invoked directly, unlike track.mjs.
|
|
298
|
+
if (process.argv[1] && process.argv[1].endsWith('backfill_rates.mjs')) main();
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Env: BUDGET_STORE, BUDGET_CONFIG, BUDGET_PERIOD={today|week|month|all}, BUDGET_QUIET=1
|
|
11
11
|
|
|
12
12
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
13
|
-
import { dirname
|
|
13
|
+
import { dirname } from 'node:path';
|
|
14
14
|
|
|
15
15
|
const STORE = process.env.BUDGET_STORE || 'agents/cost-tracking/sessions.jsonl';
|
|
16
16
|
const CONFIG = process.env.BUDGET_CONFIG || 'agents/cost-tracking/budget.json';
|
|
@@ -200,20 +200,16 @@ function cmdCheck() {
|
|
|
200
200
|
|
|
201
201
|
// --- Per-tier budget state (docs/contracts/budget-routing.md) -------------
|
|
202
202
|
// v1 window for per-tier ceilings: rolling 24h for SPEND (ledger entries
|
|
203
|
-
// tagged `tier:`).
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (Number.isFinite(raw.reserve_ttl_ms)) return raw.reserve_ttl_ms;
|
|
214
|
-
} catch { /* fall through to the safe default below */ }
|
|
215
|
-
return 600000;
|
|
216
|
-
}
|
|
203
|
+
// tagged `tier:`).
|
|
204
|
+
//
|
|
205
|
+
// The pending-RESERVE half was removed 2026-08-16 with the permit lifecycle it
|
|
206
|
+
// belonged to (converged council verdict, anthropic + openai 2 of 2; migration
|
|
207
|
+
// record in the contract above). This reader summed `tier-reserves.jsonl`, whose
|
|
208
|
+
// only writer was `acquireBudgetPermit` — a function with no production caller —
|
|
209
|
+
// so `reserved_usd` was provably always 0 here, and a reader could not tell that
|
|
210
|
+
// zero from "nothing is currently reserved". The shared-TTL config it loaded
|
|
211
|
+
// (src/config/budget-routing.json) existed solely to keep the two reserve
|
|
212
|
+
// readers in step and went with them.
|
|
217
213
|
|
|
218
214
|
function loadPerTierCeilings() {
|
|
219
215
|
const out = { cheap: null, medium: null, strong: null };
|
|
@@ -245,29 +241,13 @@ function cmdTier(rest) {
|
|
|
245
241
|
const spent = loadSessions()
|
|
246
242
|
.filter((s) => s.tier === tier && inWindow(s.capturedAt || s.endedAt || s.ts))
|
|
247
243
|
.reduce((acc, s) => acc + (Number(s.total_cost_usd ?? s.cost_usd ?? 0) || 0), 0);
|
|
248
|
-
let reserved = 0;
|
|
249
|
-
const reserveTtl = loadReserveTtlMs();
|
|
250
|
-
const reservePath = join(dirname(STORE), 'tier-reserves.jsonl');
|
|
251
|
-
if (existsSync(reservePath)) {
|
|
252
|
-
for (const line of readFileSync(reservePath, 'utf-8').split('\n')) {
|
|
253
|
-
if (!line.trim()) continue;
|
|
254
|
-
try {
|
|
255
|
-
const e = JSON.parse(line);
|
|
256
|
-
if (e.tier === tier && e.status === 'pending' && (now - e.ts_ms) < reserveTtl) {
|
|
257
|
-
reserved += Number(e.est_usd || 0) || 0;
|
|
258
|
-
}
|
|
259
|
-
} catch { /* skip malformed line */ }
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
244
|
const ceiling = loadPerTierCeilings()[tier];
|
|
263
|
-
const available = ceiling === null ? true : spent
|
|
245
|
+
const available = ceiling === null ? true : spent < ceiling;
|
|
264
246
|
console.log(JSON.stringify({
|
|
265
247
|
tier,
|
|
266
248
|
window: 'rolling-24h',
|
|
267
|
-
reserve_ttl_ms: reserveTtl,
|
|
268
249
|
ceiling_usd: ceiling,
|
|
269
250
|
spent_usd: spent,
|
|
270
|
-
reserved_usd: reserved,
|
|
271
251
|
available,
|
|
272
252
|
}));
|
|
273
253
|
}
|
|
@@ -167,6 +167,15 @@ export function aggregate(rows: Row[]): Json {
|
|
|
167
167
|
const by_model = new Map<string, ModelBucket>();
|
|
168
168
|
const by_date = new Map<string, KvBucket>();
|
|
169
169
|
const rate_missing_models = new Set<string>();
|
|
170
|
+
// Re-priced rows, counted at the totals layer only — deliberately NOT
|
|
171
|
+
// per grouping. `rate_missing_sessions` propagates into every grouping
|
|
172
|
+
// because a flagged row makes THAT grouping's total a floor. A backfilled
|
|
173
|
+
// row is the opposite: it no longer understates anything, so nothing about
|
|
174
|
+
// a slice changes. What changes is the provenance of the summary as a
|
|
175
|
+
// whole — part of it is priced at operator-supplied rates rather than
|
|
176
|
+
// measured — and that is a totals-level fact.
|
|
177
|
+
const rate_backfilled_models = new Set<string>();
|
|
178
|
+
let rate_backfilled_sessions = 0;
|
|
170
179
|
const totals = _zero_kv();
|
|
171
180
|
|
|
172
181
|
for (const row of rows) {
|
|
@@ -185,6 +194,16 @@ export function aggregate(rows: Row[]): Json {
|
|
|
185
194
|
for (const id of _strList(row['rate_missing_models'])) {
|
|
186
195
|
rate_missing_models.add(id);
|
|
187
196
|
}
|
|
197
|
+
const backfill = row['rate_backfill'];
|
|
198
|
+
if (Array.isArray(backfill) && backfill.length > 0) {
|
|
199
|
+
rate_backfilled_sessions += 1;
|
|
200
|
+
for (const entry of backfill) {
|
|
201
|
+
if (entry && typeof entry === 'object') {
|
|
202
|
+
const id = _str((entry as Record<string, unknown>)['model'] ?? '');
|
|
203
|
+
if (id) rate_backfilled_models.add(id);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
188
207
|
|
|
189
208
|
const sessBucket = _getOr(by_sess, sid, _zero_kv);
|
|
190
209
|
const convBucket = _getOr(by_conv, cid, _zero_kv);
|
|
@@ -231,6 +250,12 @@ export function aggregate(rows: Row[]): Json {
|
|
|
231
250
|
// aggregated past: a non-zero count means total_cost_usd is a floor.
|
|
232
251
|
rate_missing_sessions: totals.rate_missing_sessions,
|
|
233
252
|
rate_missing_models: [...rate_missing_models].sort(),
|
|
253
|
+
// The re-priced qualification. Without it, `rate_missing: false`
|
|
254
|
+
// on a repaired row and on a row that was never flagged aggregate
|
|
255
|
+
// identically, so a total carrying operator-supplied estimates
|
|
256
|
+
// reads as fully measured.
|
|
257
|
+
rate_backfilled_sessions,
|
|
258
|
+
rate_backfilled_models: [...rate_backfilled_models].sort(),
|
|
234
259
|
},
|
|
235
260
|
by_session: _sortedEntries(by_sess).map(([k, v]) => ({
|
|
236
261
|
key: k,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"_README": "Denylist for check_no_external_sources.py — blocks readable inspiration/harvest/comparison source names from re-entering the tracked tree. See rule source-confidentiality + the 2026-06-13 source-confidentiality sweep. Recommending an integrated tool is allowed; recording that we copied/derived/were-inspired-by an external source is not. To intentionally retain a source link, encrypt it via src/scripts/_lib/link_crypto.py.",
|
|
3
3
|
"deny": [
|
|
4
4
|
"kdcllc",
|
|
5
|
+
"nidhinjs",
|
|
5
6
|
"\\bmicrock\\b",
|
|
6
7
|
"obra/superpowers",
|
|
7
8
|
"composiohq",
|
|
@@ -434,6 +434,28 @@ concerns:
|
|
|
434
434
|
args: []
|
|
435
435
|
fail_closed: false
|
|
436
436
|
severity: advisory
|
|
437
|
+
# road-to-inbox-harvest-2026-08-d-runtime-skill-routing Phase 2 — binds the
|
|
438
|
+
# deterministic task→skill ranker, which had shipped with ZERO production
|
|
439
|
+
# callers: `score_skill_relevance.rank` and its `suggest_skill_for_task`
|
|
440
|
+
# wrapper were reachable only from their CLIs and from `routing:doctor`.
|
|
441
|
+
# Ranks the SUBMITTED prompt over the installed skill catalogue and injects
|
|
442
|
+
# at most one line naming the top-3 skill ids and their scores — POINTERS,
|
|
443
|
+
# never bodies, never a load instruction. Same delivery as `delegation-nudge`
|
|
444
|
+
# above: `{decision:"warn", reason, additional_context}` at exit 2, reduced
|
|
445
|
+
# to a real exit of 0 by `host_semantics.emitFor`'s warn branch, so it never
|
|
446
|
+
# blocks a turn.
|
|
447
|
+
#
|
|
448
|
+
# Silence is the default and its floor is MEASURED, not chosen: `rank`
|
|
449
|
+
# returns something on nearly every real prompt, so the trigger is a top-1
|
|
450
|
+
# score at or above 30/100 — the p90 of the routing-matrix corpus (496 prompt
|
|
451
|
+
# lines; median 18, p75 28, max 70), firing on 13.9% of them. A median floor
|
|
452
|
+
# would speak on half of all turns, which is the per-turn-reminder shape this
|
|
453
|
+
# estate has already measured failing. fail_closed: false.
|
|
454
|
+
skill-route:
|
|
455
|
+
script: src/scripts/hooks/skill_route_hook.ts
|
|
456
|
+
args: []
|
|
457
|
+
fail_closed: false
|
|
458
|
+
severity: advisory
|
|
437
459
|
# road-to-orchestrator-discipline-carriers Phase 5 (F4-lite) — end-review
|
|
438
460
|
# nudge. Fires ONLY when the session mutated tracked+untracked, non-doc
|
|
439
461
|
# files beyond MUTATION_LINE_THRESHOLD (50 lines; untracked files added
|
|
@@ -639,7 +661,11 @@ roles:
|
|
|
639
661
|
# Orchestrator-only concerns (Phase 2.3): a worker never talks to the
|
|
640
662
|
# user, never dispatches sub-workers, never runs the council, and its
|
|
641
663
|
# complaints route via the orchestrator.
|
|
642
|
-
|
|
664
|
+
# `skill-route` joins the drop list for the same reason `delegation-nudge`
|
|
665
|
+
# is on it: a worker was handed a bounded slice by an orchestrator that
|
|
666
|
+
# already chose the approach, so a routing pointer there advises nobody who
|
|
667
|
+
# can act on it and costs the dispatch anyway.
|
|
668
|
+
drop: [delegation-nudge, skill-route, end-review-nudge, council-availability, team-review-gate, self-repair, session-eol]
|
|
643
669
|
|
|
644
670
|
platforms:
|
|
645
671
|
augment:
|
|
@@ -653,7 +679,7 @@ platforms:
|
|
|
653
679
|
session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
654
680
|
session_end: [chat-history, memory-learn, session-register]
|
|
655
681
|
stop: [chat-history, hot-context, verify-before-complete, team-review-gate, end-review-nudge, turn-end-gate, self-repair, session-register, session-eol]
|
|
656
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, delegation-nudge, git-authorization, session-canary, self-repair, session-register]
|
|
682
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, delegation-nudge, skill-route, git-authorization, session-canary, self-repair, session-register]
|
|
657
683
|
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, reread-guard, spawn-guard-shadow]
|
|
658
684
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, tool-result-bytes, edit-shape, language-mirror]
|
|
659
685
|
# `language-mirror` — round-5 § 6.1. `pre_compact` sets a pin-lost marker;
|
|
@@ -57,6 +57,7 @@ import { main as codeGraphNudgeMain } from './code_graph_nudge_hook.js';
|
|
|
57
57
|
import { main as uiRouteNudgeMain } from './ui_route_nudge_hook.js';
|
|
58
58
|
import { main as orchestrationRecordMain } from './orchestration_record_hook.js';
|
|
59
59
|
import { main as delegationNudgeMain } from './delegation_nudge_hook.js';
|
|
60
|
+
import { main as skillRouteMain } from './skill_route_hook.js';
|
|
60
61
|
import { main as endReviewNudgeMain } from './end_review_nudge_hook.js';
|
|
61
62
|
import { main as turnEndGateMain } from './turn_end_gate_hook.js';
|
|
62
63
|
import { main as editShapeMain } from './edit_shape_hook.js';
|
|
@@ -104,6 +105,7 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
|
|
|
104
105
|
'src/scripts/hooks/ui_route_nudge_hook.ts': uiRouteNudgeMain as ConcernMain,
|
|
105
106
|
'src/scripts/hooks/orchestration_record_hook.ts': orchestrationRecordMain as ConcernMain,
|
|
106
107
|
'src/scripts/hooks/delegation_nudge_hook.ts': delegationNudgeMain as ConcernMain,
|
|
108
|
+
'src/scripts/hooks/skill_route_hook.ts': skillRouteMain as ConcernMain,
|
|
107
109
|
'src/scripts/hooks/end_review_nudge_hook.ts': endReviewNudgeMain as ConcernMain,
|
|
108
110
|
'src/scripts/hooks/turn_end_gate_hook.ts': turnEndGateMain as ConcernMain,
|
|
109
111
|
'src/scripts/hooks/edit_shape_hook.ts': editShapeMain as ConcernMain,
|