@event4u/agent-config 12.1.0 → 14.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 (185) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +164 -186
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/cost/report.md +23 -0
  5. package/dist/agent-src/commands/evals/roadmap.json +8 -0
  6. package/dist/agent-src/commands/fix/ci.md +39 -1
  7. package/dist/agent-src/commands/pr/create.md +65 -4
  8. package/dist/agent-src/commands/roadmap/next.md +28 -5
  9. package/dist/agent-src/commands/work.md +16 -0
  10. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
  11. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
  12. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
  13. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
  14. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
  15. package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
  16. package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
  17. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
  18. package/dist/agent-src/contexts/subagent-configuration.md +69 -7
  19. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  20. package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
  21. package/dist/agent-src/rules/design-fidelity.md +1 -1
  22. package/dist/agent-src/rules/design-review-after-ui-write.md +36 -14
  23. package/dist/agent-src/rules/evaluator-independence.md +27 -5
  24. package/dist/agent-src/rules/git-history-discipline.md +5 -1
  25. package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
  26. package/dist/agent-src/rules/ui-audit-gate.md +34 -12
  27. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  28. package/dist/agent-src/scripts/gate_execute.ts +395 -0
  29. package/dist/agent-src/scripts/resume_probe.ts +422 -0
  30. package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
  31. package/dist/agent-src/scripts/update_roadmap_progress.ts +70 -4
  32. package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
  33. package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
  34. package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
  35. package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
  36. package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
  37. package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
  38. package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
  39. package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
  40. package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
  41. package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
  42. package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
  43. package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
  44. package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
  45. package/dist/agent-src/skills/roadmap-writing/SKILL.md +5 -5
  46. package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
  47. package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
  48. package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
  49. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  50. package/dist/agent-src/templates/roadmaps.md +43 -3
  51. package/dist/cli/registry.js +1 -0
  52. package/dist/cli/registry.js.map +1 -1
  53. package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-7PZ4FSZB.js} +368 -191
  54. package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
  55. package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-F3YUF7SA.js} +1 -1
  56. package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
  57. package/dist/cli-delegate/cmd_conformance.js +224 -104
  58. package/dist/cli-delegate/cmd_doctor.js +2 -2
  59. package/dist/cli-delegate/cmd_export.js +2 -2
  60. package/dist/cli-delegate/cmd_handoff.js +1 -1
  61. package/dist/cli-delegate/cmd_prune.js +2 -2
  62. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  63. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  64. package/dist/cli-delegate/cmd_sync.js +2 -2
  65. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  66. package/dist/cli-delegate/cmd_validate.js +2 -2
  67. package/dist/discovery/deprecation-report.md +1 -1
  68. package/dist/discovery/discovery-manifest.json +73 -30
  69. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  70. package/dist/discovery/discovery-manifest.summary.md +3 -3
  71. package/dist/discovery/dormancy-report.md +1 -1
  72. package/dist/discovery/orphan-report.md +1 -1
  73. package/dist/discovery/packs.json +7 -5
  74. package/dist/discovery/trust-report.md +3 -3
  75. package/dist/discovery/workspaces.json +6 -4
  76. package/dist/hooks/dispatch.js +2653 -1365
  77. package/dist/install/install.mjs +12 -4
  78. package/dist/mcp/registry-manifest.json +3 -3
  79. package/dist/mcp/server.json +2 -2
  80. package/dist/mcp/server.mjs +2679 -887
  81. package/dist/router.json +1 -1
  82. package/dist/scripts/condense.js +1 -1
  83. package/dist/scripts/condense.js.map +1 -1
  84. package/dist/server/schemas/settings.js +2 -2
  85. package/dist/server/schemas/settings.js.map +1 -1
  86. package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
  87. package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
  88. package/dist/ui/index.html +1 -1
  89. package/docs/guidelines/design-fidelity-mechanics.md +21 -0
  90. package/docs/guidelines/design-handover-extraction.md +87 -0
  91. package/docs/guidelines/prompt-templates.md +24 -1
  92. package/package.json +1 -1
  93. package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  94. package/src/agent-src/scripts/gate_execute.ts +395 -0
  95. package/src/agent-src/scripts/resume_probe.ts +422 -0
  96. package/src/agent-src/scripts/roadmap_gates.ts +138 -4
  97. package/src/agent-src/scripts/update_roadmap_progress.ts +70 -4
  98. package/src/config/ci-local-parity.yml +13 -0
  99. package/src/config/evaluator-budgets.json +9 -8
  100. package/src/config/gate-coverage.yml +140 -12
  101. package/src/config/gate-violation-baselines.json +9 -3
  102. package/src/config/hook-token-budget.json +8 -1
  103. package/src/config/rule-activation-census.json +41 -0
  104. package/src/config/rule-stub-ceilings.json +52 -0
  105. package/src/scripts/_dispatch.bash +4 -0
  106. package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
  107. package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
  108. package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
  109. package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
  110. package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
  111. package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
  112. package/src/scripts/_lib/evaluator_contract.ts +159 -0
  113. package/src/scripts/_lib/gh_transient.ts +80 -0
  114. package/src/scripts/_lib/release_material.ts +16 -5
  115. package/src/scripts/_lib/review_independence.ts +111 -0
  116. package/src/scripts/_lib/session_register.ts +14 -0
  117. package/src/scripts/_lib/skill_catalogue.ts +278 -5
  118. package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
  119. package/src/scripts/_lib/tier_budget_routing.ts +31 -331
  120. package/src/scripts/_lib/turn_end_refusals.ts +482 -0
  121. package/src/scripts/ai_council/_py_json.ts +96 -0
  122. package/src/scripts/ai_council/cli_call_budget.ts +357 -0
  123. package/src/scripts/ai_council/clients.ts +101 -164
  124. package/src/scripts/ai_council/config.ts +25 -11
  125. package/src/scripts/ai_council/probe_store.ts +196 -0
  126. package/src/scripts/ai_council/qualification.ts +425 -0
  127. package/src/scripts/ai_council/qualification_wiring.ts +243 -0
  128. package/src/scripts/ai_council/quorum_wiring.ts +211 -0
  129. package/src/scripts/ai_team/team_dispatch.ts +40 -6
  130. package/src/scripts/bench_ab_v2_complexity.ts +252 -0
  131. package/src/scripts/bench_ab_v2_run.ts +3 -42
  132. package/src/scripts/bench_ab_v2_safety.ts +296 -0
  133. package/src/scripts/bench_ab_v2_search.ts +343 -0
  134. package/src/scripts/bench_ab_v2_stats.ts +191 -0
  135. package/src/scripts/build_archive_index.ts +383 -0
  136. package/src/scripts/capture_skill_catalogue.ts +369 -4
  137. package/src/scripts/check_evaluator_schema.ts +249 -0
  138. package/src/scripts/check_pr_ci_current.ts +409 -0
  139. package/src/scripts/check_review_schema.ts +195 -0
  140. package/src/scripts/check_rule_activation_census.ts +451 -0
  141. package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
  142. package/src/scripts/check_source_size_budget.ts +428 -0
  143. package/src/scripts/check_suppression_hygiene.ts +5 -1
  144. package/src/scripts/condense.ts +1 -1
  145. package/src/scripts/cost/backfill_rates.mjs +298 -0
  146. package/src/scripts/cost/budget.mjs +12 -32
  147. package/src/scripts/cost_summary.ts +25 -0
  148. package/src/scripts/council_cli.ts +141 -191
  149. package/src/scripts/dispatch_r2_reviewer.ts +8 -1
  150. package/src/scripts/external_sources_denylist.json +1 -0
  151. package/src/scripts/hook_manifest.yaml +99 -6
  152. package/src/scripts/hooks/concern_registry.ts +6 -0
  153. package/src/scripts/hooks/dispatch_hook.ts +43 -1
  154. package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
  155. package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
  156. package/src/scripts/hooks/skill_route_hook.ts +226 -0
  157. package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
  158. package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
  159. package/src/scripts/hooks_doctor.ts +112 -0
  160. package/src/scripts/interruption_report.ts +443 -0
  161. package/src/scripts/lint_design_antipattern_parity.ts +40 -6
  162. package/src/scripts/lint_evidence_artifacts.ts +359 -0
  163. package/src/scripts/lint_harvest_provenance.ts +35 -1
  164. package/src/scripts/lint_roadmap_blockers.ts +65 -0
  165. package/src/scripts/lint_roadmap_complexity.ts +7 -1
  166. package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
  167. package/src/scripts/lint_skill_router_head.ts +21 -0
  168. package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
  169. package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
  170. package/src/scripts/mcp_server/tools.ts +87 -0
  171. package/src/scripts/release.ts +43 -58
  172. package/src/scripts/routing_doctor.ts +50 -13
  173. package/src/scripts/rule_activation_census.ts +292 -0
  174. package/src/scripts/schemas/command.schema.json +1 -1
  175. package/src/scripts/schemas/evaluator-output.schema.json +60 -0
  176. package/src/scripts/schemas/review-findings.schema.json +31 -0
  177. package/src/scripts/schemas/skill.schema.json +18 -1
  178. package/src/scripts/self_review_gate.ts +6 -0
  179. package/src/scripts/session_register_hook.ts +30 -1
  180. package/src/scripts/sync_pr_branch.ts +271 -0
  181. package/src/scripts/test_council_qualification.ts +213 -0
  182. package/src/scripts/trigger_coverage.ts +224 -3
  183. package/src/server/schemas/settings.ts +2 -2
  184. package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
  185. 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,