@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.
Files changed (105) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +151 -0
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/cost/report.md +23 -0
  5. package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
  6. package/dist/agent-src/contexts/subagent-configuration.md +69 -7
  7. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  8. package/dist/agent-src/rules/design-fidelity.md +1 -1
  9. package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
  10. package/dist/agent-src/rules/evaluator-independence.md +27 -5
  11. package/dist/agent-src/rules/git-history-discipline.md +5 -1
  12. package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
  13. package/dist/agent-src/rules/ui-audit-gate.md +9 -4
  14. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  15. package/dist/agent-src/scripts/update_roadmap_progress.ts +17 -2
  16. package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
  17. package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
  18. package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
  19. package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
  20. package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
  21. package/dist/agent-src/skills/roadmap-writing/SKILL.md +4 -4
  22. package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
  23. package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
  24. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  25. package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-3B72LSZL.js} +1 -1
  26. package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
  27. package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-N7FEST6D.js} +17 -17
  28. package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
  29. package/dist/cli-delegate/cmd_conformance.js +2 -2
  30. package/dist/cli-delegate/cmd_doctor.js +2 -2
  31. package/dist/cli-delegate/cmd_export.js +2 -2
  32. package/dist/cli-delegate/cmd_handoff.js +1 -1
  33. package/dist/cli-delegate/cmd_prune.js +2 -2
  34. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  35. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  36. package/dist/cli-delegate/cmd_sync.js +2 -2
  37. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  38. package/dist/cli-delegate/cmd_validate.js +2 -2
  39. package/dist/discovery/deprecation-report.md +1 -1
  40. package/dist/discovery/discovery-manifest.json +43 -22
  41. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  42. package/dist/discovery/discovery-manifest.summary.md +3 -3
  43. package/dist/discovery/dormancy-report.md +1 -1
  44. package/dist/discovery/orphan-report.md +1 -1
  45. package/dist/discovery/packs.json +6 -5
  46. package/dist/discovery/trust-report.md +2 -2
  47. package/dist/discovery/workspaces.json +5 -4
  48. package/dist/hooks/dispatch.js +936 -346
  49. package/dist/install/install.mjs +12 -4
  50. package/dist/mcp/registry-manifest.json +3 -3
  51. package/dist/mcp/server.json +2 -2
  52. package/dist/mcp/server.mjs +1192 -160
  53. package/dist/router.json +1 -1
  54. package/dist/server/schemas/settings.js +2 -2
  55. package/dist/server/schemas/settings.js.map +1 -1
  56. package/docs/guidelines/design-fidelity-mechanics.md +21 -0
  57. package/docs/guidelines/design-handover-extraction.md +87 -0
  58. package/docs/guidelines/prompt-templates.md +24 -1
  59. package/package.json +1 -1
  60. package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  61. package/src/agent-src/scripts/update_roadmap_progress.ts +17 -2
  62. package/src/config/evaluator-budgets.json +5 -4
  63. package/src/config/gate-coverage.yml +33 -11
  64. package/src/config/gate-violation-baselines.json +9 -3
  65. package/src/config/hook-token-budget.json +8 -1
  66. package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
  67. package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
  68. package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
  69. package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
  70. package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
  71. package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
  72. package/src/scripts/_lib/skill_catalogue.ts +258 -5
  73. package/src/scripts/_lib/tier_budget_routing.ts +31 -331
  74. package/src/scripts/ai_council/clients.ts +21 -21
  75. package/src/scripts/bench_ab_v2_complexity.ts +252 -0
  76. package/src/scripts/bench_ab_v2_run.ts +3 -42
  77. package/src/scripts/bench_ab_v2_safety.ts +296 -0
  78. package/src/scripts/bench_ab_v2_search.ts +343 -0
  79. package/src/scripts/bench_ab_v2_stats.ts +191 -0
  80. package/src/scripts/build_archive_index.ts +383 -0
  81. package/src/scripts/capture_skill_catalogue.ts +107 -2
  82. package/src/scripts/check_source_size_budget.ts +428 -0
  83. package/src/scripts/check_suppression_hygiene.ts +5 -1
  84. package/src/scripts/cost/backfill_rates.mjs +298 -0
  85. package/src/scripts/cost/budget.mjs +12 -32
  86. package/src/scripts/cost_summary.ts +25 -0
  87. package/src/scripts/external_sources_denylist.json +1 -0
  88. package/src/scripts/hook_manifest.yaml +28 -2
  89. package/src/scripts/hooks/concern_registry.ts +2 -0
  90. package/src/scripts/hooks/skill_route_hook.ts +226 -0
  91. package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
  92. package/src/scripts/lint_design_antipattern_parity.ts +40 -6
  93. package/src/scripts/lint_harvest_provenance.ts +35 -1
  94. package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
  95. package/src/scripts/lint_skill_router_head.ts +21 -0
  96. package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
  97. package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
  98. package/src/scripts/mcp_server/tools.ts +87 -0
  99. package/src/scripts/release.ts +5 -5
  100. package/src/scripts/routing_doctor.ts +50 -13
  101. package/src/scripts/schemas/command.schema.json +1 -1
  102. package/src/scripts/schemas/skill.schema.json +18 -1
  103. package/src/scripts/trigger_coverage.ts +224 -3
  104. package/src/server/schemas/settings.ts +2 -2
  105. 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, join } from 'node:path';
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:`). Pending reserves from tier-reserves.jsonl count only
204
- // within the shared reserve TTL — reserves are race protection, not spend
205
- // accounting. The TTL comes from src/config/budget-routing.json, the SAME
206
- // source acquireBudgetPermit reads, so the two readers cannot diverge
207
- // (external review 2026-08-03, Finding 1b).
208
-
209
- function loadReserveTtlMs() {
210
- try {
211
- const cfgPath = join(dirname(new URL(import.meta.url).pathname), '..', '..', 'config', 'budget-routing.json');
212
- const raw = JSON.parse(readFileSync(cfgPath, 'utf-8'));
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 + reserved < ceiling;
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
- drop: [delegation-nudge, end-review-nudge, council-availability, team-review-gate, self-repair, session-eol]
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,