@event4u/agent-config 12.0.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 (148) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +273 -0
  3. package/README.md +1 -1
  4. package/agents/templates/.ai-council.yml.example +46 -3
  5. package/dist/agent-src/commands/cost/report.md +23 -0
  6. package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
  7. package/dist/agent-src/contexts/model-recommendations.md +6 -4
  8. package/dist/agent-src/contexts/subagent-configuration.md +69 -7
  9. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  10. package/dist/agent-src/rules/design-fidelity.md +1 -1
  11. package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
  12. package/dist/agent-src/rules/evaluator-independence.md +27 -5
  13. package/dist/agent-src/rules/git-history-discipline.md +5 -1
  14. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  15. package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
  16. package/dist/agent-src/rules/ui-audit-gate.md +9 -4
  17. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  18. package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
  19. package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -1
  20. package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
  21. package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
  22. package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
  23. package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
  24. package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
  25. package/dist/agent-src/skills/roadmap-writing/SKILL.md +11 -11
  26. package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
  27. package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/agent-src/templates/roadmaps.md +41 -3
  30. package/dist/cli/registry.js +1 -0
  31. package/dist/cli/registry.js.map +1 -1
  32. package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-3B72LSZL.js} +123 -2338
  33. package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
  34. package/dist/cli-delegate/chunk-N7FEST6D.js +3895 -0
  35. package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-XURJ4AQC.js} +566 -298
  36. package/dist/cli-delegate/cmd_conformance.js +4 -4
  37. package/dist/cli-delegate/cmd_doctor.js +2 -2
  38. package/dist/cli-delegate/cmd_export.js +2 -2
  39. package/dist/cli-delegate/cmd_handoff.js +1 -1
  40. package/dist/cli-delegate/cmd_prune.js +2 -2
  41. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  42. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  43. package/dist/cli-delegate/cmd_sync.js +2 -2
  44. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  45. package/dist/cli-delegate/cmd_validate.js +2 -2
  46. package/dist/discovery/deprecation-report.md +1 -1
  47. package/dist/discovery/discovery-manifest.json +44 -23
  48. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  49. package/dist/discovery/discovery-manifest.summary.md +3 -3
  50. package/dist/discovery/dormancy-report.md +1 -1
  51. package/dist/discovery/orphan-report.md +1 -1
  52. package/dist/discovery/packs.json +6 -5
  53. package/dist/discovery/trust-report.md +2 -2
  54. package/dist/discovery/workspaces.json +5 -4
  55. package/dist/hooks/dispatch.js +10625 -8541
  56. package/dist/install/install.mjs +754 -484
  57. package/dist/mcp/registry-manifest.json +3 -3
  58. package/dist/mcp/server.json +2 -2
  59. package/dist/mcp/server.mjs +1302 -167
  60. package/dist/router.json +1 -1
  61. package/dist/scripts/_lib/model_tier.js +9 -0
  62. package/dist/scripts/_lib/model_tier.js.map +1 -1
  63. package/dist/scripts/condense.js +50 -0
  64. package/dist/scripts/condense.js.map +1 -1
  65. package/dist/server/schemas/settings.js +2 -2
  66. package/dist/server/schemas/settings.js.map +1 -1
  67. package/docs/MIGRATION.md +20 -1
  68. package/docs/guidelines/design-fidelity-mechanics.md +21 -0
  69. package/docs/guidelines/design-handover-extraction.md +87 -0
  70. package/docs/guidelines/prompt-templates.md +24 -1
  71. package/package.json +1 -1
  72. package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  73. package/src/agent-src/scripts/roadmap_gates.ts +30 -2
  74. package/src/agent-src/scripts/update_roadmap_progress.ts +47 -1
  75. package/src/config/evaluator-budgets.json +9 -8
  76. package/src/config/gate-coverage.yml +63 -11
  77. package/src/config/gate-violation-baselines.json +12 -0
  78. package/src/config/hook-token-budget.json +15 -2
  79. package/src/config/preamble-payload-budget.json +28 -0
  80. package/src/scripts/_dispatch.bash +10 -0
  81. package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
  82. package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
  83. package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
  84. package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
  85. package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
  86. package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
  87. package/src/scripts/_lib/git_common_dir.ts +173 -0
  88. package/src/scripts/_lib/model_tier.ts +9 -0
  89. package/src/scripts/_lib/release_highlights.ts +108 -7
  90. package/src/scripts/_lib/skill_catalogue.ts +1115 -0
  91. package/src/scripts/_lib/tier_budget_routing.ts +31 -331
  92. package/src/scripts/ai_council/clients.ts +197 -18
  93. package/src/scripts/ai_council/config.ts +16 -1
  94. package/src/scripts/ai_council/low_impact.ts +14 -1
  95. package/src/scripts/ai_council/orchestrator.ts +14 -1
  96. package/src/scripts/ai_council/transport_resolver.ts +32 -2
  97. package/src/scripts/bench_ab_v2_complexity.ts +252 -0
  98. package/src/scripts/bench_ab_v2_run.ts +3 -42
  99. package/src/scripts/bench_ab_v2_safety.ts +296 -0
  100. package/src/scripts/bench_ab_v2_search.ts +343 -0
  101. package/src/scripts/bench_ab_v2_stats.ts +191 -0
  102. package/src/scripts/build_archive_index.ts +383 -0
  103. package/src/scripts/capture_skill_catalogue.ts +279 -317
  104. package/src/scripts/check_branch_freshness.ts +7 -1
  105. package/src/scripts/check_release_published.ts +6 -2
  106. package/src/scripts/check_release_trunk_sync.ts +18 -2
  107. package/src/scripts/check_source_size_budget.ts +428 -0
  108. package/src/scripts/check_suppression_hygiene.ts +5 -1
  109. package/src/scripts/condense.ts +50 -1
  110. package/src/scripts/cost/backfill_rates.mjs +298 -0
  111. package/src/scripts/cost/budget.mjs +12 -32
  112. package/src/scripts/cost_summary.ts +25 -0
  113. package/src/scripts/dispatch_r2_reviewer.ts +15 -5
  114. package/src/scripts/evidence_report.ts +15 -4
  115. package/src/scripts/external_sources_denylist.json +1 -0
  116. package/src/scripts/hook_manifest.yaml +75 -15
  117. package/src/scripts/hooks/concern_registry.ts +4 -0
  118. package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
  119. package/src/scripts/hooks/skill_route_hook.ts +226 -0
  120. package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
  121. package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
  122. package/src/scripts/hot_context_hook.ts +13 -1
  123. package/src/scripts/install.ts +129 -0
  124. package/src/scripts/janitor.ts +77 -2
  125. package/src/scripts/lint_design_antipattern_parity.ts +40 -6
  126. package/src/scripts/lint_harvest_provenance.ts +35 -1
  127. package/src/scripts/lint_plan_risk_register.ts +22 -9
  128. package/src/scripts/lint_roadmap_blockers.ts +109 -6
  129. package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
  130. package/src/scripts/lint_scheduled_deprecations.ts +738 -0
  131. package/src/scripts/lint_skill_router_head.ts +21 -0
  132. package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
  133. package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
  134. package/src/scripts/mcp_server/tools.ts +87 -0
  135. package/src/scripts/migration_status.ts +17 -1
  136. package/src/scripts/probe_review_binding_drift.ts +731 -0
  137. package/src/scripts/release.ts +100 -6
  138. package/src/scripts/routing_doctor.ts +50 -13
  139. package/src/scripts/schemas/command.schema.json +3 -3
  140. package/src/scripts/schemas/skill.schema.json +20 -2
  141. package/src/scripts/schemas/subagent.schema.json +2 -2
  142. package/src/scripts/session_eol_report.ts +81 -0
  143. package/src/scripts/trigger_coverage.ts +224 -3
  144. package/src/scripts/workspace_doctor.ts +464 -0
  145. package/src/scripts/worktree_cleanup_check.ts +2 -1
  146. package/src/server/schemas/settings.ts +2 -2
  147. package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
  148. 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,
@@ -222,13 +222,23 @@ export const REVIEW_SCOPE_NAME_ONLY_FLAGS: readonly string[] = [
222
222
  '-O/dev/null',
223
223
  ];
224
224
 
225
- /** `git diff` argv for the review scope (patch body), config-pinned. */
226
- export function reviewScopeDiffArgs(base: string): string[] {
225
+ /**
226
+ * `git diff` argv for the review scope (patch body), config-pinned.
227
+ *
228
+ * `head` defaults to `HEAD` — the only value the dispatcher and the validator
229
+ * ever pass, since both bind the scope being dispatched *now*. It is a
230
+ * parameter so that an out-of-band measurement can re-derive the scope at a
231
+ * HISTORICAL revision through this same definition instead of assembling a
232
+ * second `git diff` argv beside it: a private copy would drift from the
233
+ * normative flag list above the first time a flag changes, and then report a
234
+ * hash no gate would ever produce.
235
+ */
236
+ export function reviewScopeDiffArgs(base: string, head = 'HEAD'): string[] {
227
237
  return [
228
238
  ...REVIEW_SCOPE_GIT_CONFIG,
229
239
  'diff',
230
240
  ...REVIEW_SCOPE_DIFF_FLAGS,
231
- `${base}...HEAD`,
241
+ `${base}...${head}`,
232
242
  '--',
233
243
  ':/',
234
244
  ...REVIEW_SCOPE_EXCLUDES,
@@ -236,12 +246,12 @@ export function reviewScopeDiffArgs(base: string): string[] {
236
246
  }
237
247
 
238
248
  /** `git diff --name-only` argv for the review scope (changed-file list), config-pinned. */
239
- export function reviewScopeNameOnlyArgs(base: string): string[] {
249
+ export function reviewScopeNameOnlyArgs(base: string, head = 'HEAD'): string[] {
240
250
  return [
241
251
  ...REVIEW_SCOPE_GIT_CONFIG,
242
252
  'diff',
243
253
  ...REVIEW_SCOPE_NAME_ONLY_FLAGS,
244
- `${base}...HEAD`,
254
+ `${base}...${head}`,
245
255
  '--',
246
256
  ':/',
247
257
  ...REVIEW_SCOPE_EXCLUDES,
@@ -29,6 +29,8 @@ import * as path from 'node:path';
29
29
  import process from 'node:process';
30
30
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
31
 
32
+ import { workspaceIdentity } from './_lib/git_common_dir.js';
33
+
32
34
  // argparse `prog` is hardcoded to "evidence_report.py" in the retired Python implementation,
33
35
  // so usage strings stay byte-identical regardless of the `.py`/`.ts` filename.
34
36
  const PROG = 'evidence_report.py';
@@ -39,15 +41,24 @@ const DESCRIPTION = 'Evidence Report template automation.';
39
41
  // Repo-root resolution
40
42
  // ---------------------------------------------------------------------------
41
43
 
42
- /** Return repo root via git, falling back to relative-path heuristic. */
44
+ /**
45
+ * Return repo root, falling back to the relative-path heuristic.
46
+ *
47
+ * Census row R4. The previous implementation spawned
48
+ * `git rev-parse --show-toplevel` with **no `cwd` and no env scrub**, so an
49
+ * inherited `GIT_DIR` (every git hook exports one) silently answered about the
50
+ * hook's repository instead of this one. `workspaceIdentity()` reads files, so
51
+ * there is no child process to redirect. The fallback is unchanged, and in a
52
+ * plain invocation both forms return the same path.
53
+ */
43
54
  function _repo_root(): string {
44
- const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf-8' });
45
- if (result.error || result.status !== 0 || typeof result.stdout !== 'string') {
55
+ const root = workspaceIdentity().repoRoot;
56
+ if (!root.resolved) {
46
57
  // Script lives at src/scripts/; repo root is two levels up.
47
58
  const here = fileURLToPath(import.meta.url);
48
59
  return path.resolve(path.dirname(here), '..', '..');
49
60
  }
50
- return result.stdout.trim();
61
+ return root.value;
51
62
  }
52
63
 
53
64
  function _git_head_short(): string {
@@ -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
@@ -556,6 +578,30 @@ concerns:
556
578
  args: []
557
579
  fail_closed: false
558
580
  severity: advisory
581
+ # road-to-inbox-harvest-2026-08-d-context-ledger Phase 1 Step 1.1 — the
582
+ # tool-result byte counter. `hook-token-budget.json` registers the gap it
583
+ # closes in its own words: bytes-into-context from tool RESULTS are
584
+ # instrumented nowhere, so every context-cost claim in this tree is made
585
+ # against a denominator missing its dominant term.
586
+ #
587
+ # CAPTURE ONLY. It appends `{ts, tool, bytes, measurable}` to a gitignored
588
+ # census file and emits NOTHING to the model — zero injection bytes, which
589
+ # is why it carries a metric registration rather than a per-concern byte
590
+ # cap. The threshold question (when is a result too large) is deliberately
591
+ # unanswered: the distribution has never been observed, and the house rule
592
+ # is baseline before mechanism.
593
+ #
594
+ # The record type has four fields and none can hold free text — a tool
595
+ # NAME, a byte COUNT, a timestamp and a measurable flag. Tool results
596
+ # routinely carry file contents and command output; none of it can reach
597
+ # this output because the output has no field able to carry it.
598
+ #
599
+ # Never blocks, never warns: exit 0 on every path. fail_closed: false.
600
+ tool-result-bytes:
601
+ script: src/scripts/hooks/tool_result_bytes_hook.ts
602
+ args: []
603
+ fail_closed: false
604
+ severity: advisory
559
605
  # road-to-subagent-lifecycle-integrity Phase 3 Step 1 — spawn guard, SHADOW
560
606
  # ONLY, per docs/contracts/concern-activation-policy.md. On every Agent/Task
561
607
  # call it appends one `spawn_guard_shadow` line recording what a guard WOULD
@@ -615,7 +661,11 @@ roles:
615
661
  # Orchestrator-only concerns (Phase 2.3): a worker never talks to the
616
662
  # user, never dispatches sub-workers, never runs the council, and its
617
663
  # complaints route via the orchestrator.
618
- 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]
619
669
 
620
670
  platforms:
621
671
  augment:
@@ -623,22 +673,32 @@ platforms:
623
673
  session_end: [chat-history, memory-learn, session-register]
624
674
  stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
625
675
  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]
626
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
676
+ 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]
627
677
 
628
678
  claude:
629
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]
630
680
  session_end: [chat-history, memory-learn, session-register]
631
681
  stop: [chat-history, hot-context, verify-before-complete, team-review-gate, end-review-nudge, turn-end-gate, self-repair, session-register, session-eol]
632
- 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]
633
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]
634
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape, language-mirror]
635
- # round-5 § 6.1 — the ONLY binding on this slot in the tree. `pre_compact`
636
- # sets a pin-lost marker; the `post_tool_use` entry above re-emits the pin
637
- # exactly once while that marker is set, then clears it. One extra
638
- # injection per compaction EVENT — never one per tool call, which is the
639
- # shape § 6.2 refuses. claude only: it is the sole verified platform, and
640
- # the measured compaction defect is a Claude Code transcript fact.
641
- pre_compact: [language-mirror]
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]
685
+ # `language-mirror` — round-5 § 6.1. `pre_compact` sets a pin-lost marker;
686
+ # the `post_tool_use` entry above re-emits the pin exactly once while that
687
+ # marker is set, then clears it. One extra injection per compaction EVENT —
688
+ # never one per tool call, which is the shape § 6.2 refuses. claude only: it
689
+ # is the sole verified platform, and the measured compaction defect is a
690
+ # Claude Code transcript fact. (It was the ONLY binding on this slot until
691
+ # the entry below; that sentence is retired rather than left to rot.)
692
+ #
693
+ # `hot-context` — road-to-inbox-harvest-2026-08-d-context-ledger Step 2.1.
694
+ # This is the one slot that fires WHILE state is being destroyed, and it
695
+ # carried no WRITER at all: the cache a post-compaction `session_start`
696
+ # restores was written at the last `stop`, so everything the session did
697
+ # between that stop and the compaction was lost with it. The concern is
698
+ # already bound on `stop` and `session_start`; this adds the write at the
699
+ # moment the loss actually happens. Deterministic rebuild, emits nothing
700
+ # here — the 4,096-byte row it carries is for its session_start restore.
701
+ pre_compact: [language-mirror, hot-context]
642
702
  # road-to-subagent-lifecycle-integrity Phase 1 Step 1. `claude` is the only
643
703
  # platform where both native events were measured present on the installed
644
704
  # binary (2.1.229 — see the evidence page cited on the alias rows below).
@@ -675,7 +735,7 @@ platforms:
675
735
  stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
676
736
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
677
737
  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]
678
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
738
+ 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]
679
739
  # subagent-ledger IS bound here, unlike the two nudges above, and the
680
740
  # difference is the delivery channel rather than the platform: the nudges
681
741
  # depend on stdout reaching the model, which this trampoline discards by
@@ -698,7 +758,7 @@ platforms:
698
758
  session_end: [chat-history, memory-learn, session-register]
699
759
  stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
700
760
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
701
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
761
+ 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]
702
762
 
703
763
  # Phase 7.6 — Cline. Hooks live under `.clinerules/hooks/<HookName>`
704
764
  # (project, no file extension, must be executable per Cline docs) or
@@ -713,7 +773,7 @@ platforms:
713
773
  session_end: [chat-history, memory-learn, session-register]
714
774
  stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
715
775
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
716
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
776
+ 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]
717
777
 
718
778
  # Phase 7.7 — Windsurf (Cascade). Hooks live at `.windsurf/hooks.json`
719
779
  # (project) or `~/.codeium/windsurf/hooks.json` (user). Cascade has
@@ -749,7 +809,7 @@ platforms:
749
809
  session_end: [chat-history, memory-learn, session-register]
750
810
  stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
751
811
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
752
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
812
+ 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]
753
813
 
754
814
  # Phase 7.9 — Copilot has no hook surface. Concerns route through
755
815
  # rule-only fallback; the dispatcher silently no-ops on --platform copilot.