@event4u/agent-config 11.0.0 → 12.1.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 (312) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +173 -229
  3. package/agents/templates/.ai-council.yml.example +46 -3
  4. package/dist/agent-src/commands/agent-handoff.md +0 -1
  5. package/dist/agent-src/commands/agent-status.md +0 -1
  6. package/dist/agent-src/commands/agents/audit.md +0 -1
  7. package/dist/agent-src/commands/agents/init.md +0 -1
  8. package/dist/agent-src/commands/agents/optimize.md +0 -1
  9. package/dist/agent-src/commands/agents/user/accept.md +0 -1
  10. package/dist/agent-src/commands/agents/user/delete.md +0 -1
  11. package/dist/agent-src/commands/agents/user/init.md +0 -1
  12. package/dist/agent-src/commands/agents/user/review.md +0 -1
  13. package/dist/agent-src/commands/agents/user/show.md +0 -1
  14. package/dist/agent-src/commands/agents/user/update.md +0 -1
  15. package/dist/agent-src/commands/agents/user.md +0 -1
  16. package/dist/agent-src/commands/agents.md +0 -1
  17. package/dist/agent-src/commands/analytics/prune.md +0 -1
  18. package/dist/agent-src/commands/analytics/show.md +0 -1
  19. package/dist/agent-src/commands/analytics.md +0 -1
  20. package/dist/agent-src/commands/analyze/conformance.md +0 -1
  21. package/dist/agent-src/commands/analyze/decision.md +0 -1
  22. package/dist/agent-src/commands/analyze/inbox.md +0 -1
  23. package/dist/agent-src/commands/analyze/incident.md +0 -1
  24. package/dist/agent-src/commands/analyze/near-miss.md +0 -1
  25. package/dist/agent-src/commands/analyze/postmortem.md +0 -1
  26. package/dist/agent-src/commands/analyze/premortem.md +0 -1
  27. package/dist/agent-src/commands/analyze/reference-repo.md +0 -1
  28. package/dist/agent-src/commands/analyze.md +0 -1
  29. package/dist/agent-src/commands/brand/identity.md +0 -1
  30. package/dist/agent-src/commands/brand/review.md +0 -1
  31. package/dist/agent-src/commands/brand/strategy.md +0 -1
  32. package/dist/agent-src/commands/brand/tokens.md +0 -1
  33. package/dist/agent-src/commands/brand/voice.md +0 -1
  34. package/dist/agent-src/commands/brand.md +0 -1
  35. package/dist/agent-src/commands/bug/fix.md +0 -1
  36. package/dist/agent-src/commands/bug/investigate.md +0 -1
  37. package/dist/agent-src/commands/bug.md +0 -1
  38. package/dist/agent-src/commands/challenge-me/vision.md +0 -1
  39. package/dist/agent-src/commands/challenge-me/with-docs.md +0 -1
  40. package/dist/agent-src/commands/challenge-me.md +0 -1
  41. package/dist/agent-src/commands/chat-history/import.md +0 -1
  42. package/dist/agent-src/commands/chat-history.md +0 -1
  43. package/dist/agent-src/commands/check-current-md.md +0 -1
  44. package/dist/agent-src/commands/commit/in-chunks.md +0 -1
  45. package/dist/agent-src/commands/commit.md +0 -1
  46. package/dist/agent-src/commands/condense.md +0 -1
  47. package/dist/agent-src/commands/context/create.md +0 -1
  48. package/dist/agent-src/commands/context/refactor.md +0 -1
  49. package/dist/agent-src/commands/context.md +0 -1
  50. package/dist/agent-src/commands/contribution-precheck.md +0 -1
  51. package/dist/agent-src/commands/cost/profile.md +0 -1
  52. package/dist/agent-src/commands/cost/report.md +0 -1
  53. package/dist/agent-src/commands/cost.md +0 -1
  54. package/dist/agent-src/commands/council/analysis.md +0 -1
  55. package/dist/agent-src/commands/council/debate.md +0 -1
  56. package/dist/agent-src/commands/council/default.md +0 -1
  57. package/dist/agent-src/commands/council/design.md +0 -1
  58. package/dist/agent-src/commands/council/optimize.md +0 -1
  59. package/dist/agent-src/commands/council/pr.md +0 -1
  60. package/dist/agent-src/commands/council.md +0 -1
  61. package/dist/agent-src/commands/design-system/capture.md +0 -1
  62. package/dist/agent-src/commands/design-system/generate.md +0 -1
  63. package/dist/agent-src/commands/design-system/import.md +0 -1
  64. package/dist/agent-src/commands/design-system.md +0 -1
  65. package/dist/agent-src/commands/estimate-ticket.md +0 -1
  66. package/dist/agent-src/commands/explain-run.md +0 -1
  67. package/dist/agent-src/commands/feature/dev.md +0 -1
  68. package/dist/agent-src/commands/feature/explore.md +0 -1
  69. package/dist/agent-src/commands/feature/plan.md +0 -1
  70. package/dist/agent-src/commands/feature/refactor.md +0 -1
  71. package/dist/agent-src/commands/feature/roadmap.md +0 -1
  72. package/dist/agent-src/commands/feature.md +0 -1
  73. package/dist/agent-src/commands/fix/ci.md +0 -1
  74. package/dist/agent-src/commands/fix/comments.md +0 -1
  75. package/dist/agent-src/commands/fix/portability.md +0 -1
  76. package/dist/agent-src/commands/fix/pr-comments-loop.md +0 -1
  77. package/dist/agent-src/commands/fix/pr-comments.md +0 -1
  78. package/dist/agent-src/commands/fix/quality.md +0 -1
  79. package/dist/agent-src/commands/fix/refs.md +0 -1
  80. package/dist/agent-src/commands/fix/route.md +0 -1
  81. package/dist/agent-src/commands/fix/seeder.md +0 -1
  82. package/dist/agent-src/commands/fix.md +0 -1
  83. package/dist/agent-src/commands/ghostwriter/delete.md +0 -1
  84. package/dist/agent-src/commands/ghostwriter/fetch.md +0 -1
  85. package/dist/agent-src/commands/ghostwriter/list.md +0 -1
  86. package/dist/agent-src/commands/ghostwriter/show.md +0 -1
  87. package/dist/agent-src/commands/ghostwriter/write.md +0 -1
  88. package/dist/agent-src/commands/ghostwriter.md +0 -1
  89. package/dist/agent-src/commands/grill-me.md +0 -1
  90. package/dist/agent-src/commands/humanize.md +0 -1
  91. package/dist/agent-src/commands/image/analyse.md +0 -1
  92. package/dist/agent-src/commands/image/create.md +0 -1
  93. package/dist/agent-src/commands/image/verify.md +0 -1
  94. package/dist/agent-src/commands/image.md +0 -1
  95. package/dist/agent-src/commands/implement-ticket.md +0 -1
  96. package/dist/agent-src/commands/jira-ticket.md +0 -1
  97. package/dist/agent-src/commands/judge/on-diff.md +0 -1
  98. package/dist/agent-src/commands/judge/solo.md +0 -1
  99. package/dist/agent-src/commands/judge/steps.md +0 -1
  100. package/dist/agent-src/commands/judge.md +0 -1
  101. package/dist/agent-src/commands/knowledge/cross-repo.md +0 -1
  102. package/dist/agent-src/commands/knowledge/forget.md +0 -1
  103. package/dist/agent-src/commands/knowledge/ingest.md +0 -1
  104. package/dist/agent-src/commands/knowledge/list.md +0 -1
  105. package/dist/agent-src/commands/knowledge.md +0 -1
  106. package/dist/agent-src/commands/memory/add.md +0 -1
  107. package/dist/agent-src/commands/memory/learn-low-impact.md +0 -1
  108. package/dist/agent-src/commands/memory/load.md +0 -1
  109. package/dist/agent-src/commands/memory/mine-session.md +0 -1
  110. package/dist/agent-src/commands/memory/promote.md +0 -1
  111. package/dist/agent-src/commands/memory/propose.md +0 -1
  112. package/dist/agent-src/commands/memory.md +0 -1
  113. package/dist/agent-src/commands/mission/upgrade.md +0 -1
  114. package/dist/agent-src/commands/mode.md +0 -1
  115. package/dist/agent-src/commands/module/create.md +0 -1
  116. package/dist/agent-src/commands/module/explore.md +0 -1
  117. package/dist/agent-src/commands/module.md +0 -1
  118. package/dist/agent-src/commands/optimize/agents-dir.md +0 -1
  119. package/dist/agent-src/commands/optimize/augmentignore.md +0 -1
  120. package/dist/agent-src/commands/optimize/deep.md +0 -1
  121. package/dist/agent-src/commands/optimize/project.md +0 -1
  122. package/dist/agent-src/commands/optimize/prompt.md +0 -1
  123. package/dist/agent-src/commands/optimize/rtk.md +0 -1
  124. package/dist/agent-src/commands/optimize/skills.md +0 -1
  125. package/dist/agent-src/commands/optimize.md +0 -1
  126. package/dist/agent-src/commands/orchestrate.md +0 -1
  127. package/dist/agent-src/commands/override/create.md +0 -1
  128. package/dist/agent-src/commands/override/manage.md +0 -1
  129. package/dist/agent-src/commands/override.md +0 -1
  130. package/dist/agent-src/commands/package/reset.md +0 -1
  131. package/dist/agent-src/commands/package/test.md +0 -1
  132. package/dist/agent-src/commands/package.md +0 -1
  133. package/dist/agent-src/commands/post-as/ghostwriter.md +0 -1
  134. package/dist/agent-src/commands/post-as/me.md +0 -1
  135. package/dist/agent-src/commands/post-as.md +0 -1
  136. package/dist/agent-src/commands/pr/create/description-only.md +0 -1
  137. package/dist/agent-src/commands/pr/create.md +23 -9
  138. package/dist/agent-src/commands/prediction-pool.md +0 -1
  139. package/dist/agent-src/commands/prepare-for-review.md +0 -1
  140. package/dist/agent-src/commands/profile/activate.md +0 -1
  141. package/dist/agent-src/commands/profile/deactivate.md +0 -1
  142. package/dist/agent-src/commands/profile/show.md +0 -1
  143. package/dist/agent-src/commands/profile.md +0 -1
  144. package/dist/agent-src/commands/project/analyze.md +0 -1
  145. package/dist/agent-src/commands/project/health.md +0 -1
  146. package/dist/agent-src/commands/project.md +0 -1
  147. package/dist/agent-src/commands/refine-ticket.md +0 -1
  148. package/dist/agent-src/commands/research/deep.md +0 -1
  149. package/dist/agent-src/commands/research/report.md +0 -1
  150. package/dist/agent-src/commands/research.md +0 -1
  151. package/dist/agent-src/commands/review/changes.md +0 -1
  152. package/dist/agent-src/commands/review/routing.md +0 -1
  153. package/dist/agent-src/commands/review.md +0 -1
  154. package/dist/agent-src/commands/roadmap/ai-council.md +0 -1
  155. package/dist/agent-src/commands/roadmap/create.md +0 -1
  156. package/dist/agent-src/commands/roadmap/materialize.md +0 -1
  157. package/dist/agent-src/commands/roadmap/next.md +0 -1
  158. package/dist/agent-src/commands/roadmap/process-full.md +0 -1
  159. package/dist/agent-src/commands/roadmap/process-phase.md +0 -1
  160. package/dist/agent-src/commands/roadmap/process-step.md +0 -1
  161. package/dist/agent-src/commands/roadmap.md +0 -1
  162. package/dist/agent-src/commands/rule-compliance-audit.md +0 -1
  163. package/dist/agent-src/commands/security-audit-config.md +0 -1
  164. package/dist/agent-src/commands/skill/preview.md +0 -1
  165. package/dist/agent-src/commands/skill.md +0 -1
  166. package/dist/agent-src/commands/skills/discover.md +0 -1
  167. package/dist/agent-src/commands/skills.md +0 -1
  168. package/dist/agent-src/commands/sync/agent-settings.md +0 -1
  169. package/dist/agent-src/commands/sync/gitignore/fix.md +0 -1
  170. package/dist/agent-src/commands/sync/gitignore.md +0 -1
  171. package/dist/agent-src/commands/sync.md +0 -1
  172. package/dist/agent-src/commands/tdd/green.md +0 -1
  173. package/dist/agent-src/commands/tdd/red.md +0 -1
  174. package/dist/agent-src/commands/tdd/refactor.md +0 -1
  175. package/dist/agent-src/commands/tdd.md +0 -1
  176. package/dist/agent-src/commands/team/adversarial.md +0 -1
  177. package/dist/agent-src/commands/team/delegate.md +0 -1
  178. package/dist/agent-src/commands/team/review.md +0 -1
  179. package/dist/agent-src/commands/team/status.md +0 -1
  180. package/dist/agent-src/commands/team-knowledge/bootstrap.md +0 -1
  181. package/dist/agent-src/commands/team-knowledge/consolidate.md +0 -1
  182. package/dist/agent-src/commands/team-knowledge.md +0 -1
  183. package/dist/agent-src/commands/team.md +0 -1
  184. package/dist/agent-src/commands/tests/create.md +0 -1
  185. package/dist/agent-src/commands/tests/e2e-heal.md +0 -1
  186. package/dist/agent-src/commands/tests/e2e-plan.md +0 -1
  187. package/dist/agent-src/commands/tests/execute.md +0 -1
  188. package/dist/agent-src/commands/tests.md +0 -1
  189. package/dist/agent-src/commands/threat-model.md +0 -1
  190. package/dist/agent-src/commands/update-form-request-messages.md +0 -1
  191. package/dist/agent-src/commands/upstream-contribute.md +0 -1
  192. package/dist/agent-src/commands/video/from-script.md +0 -1
  193. package/dist/agent-src/commands/video/from-song.md +0 -1
  194. package/dist/agent-src/commands/video/scene.md +0 -1
  195. package/dist/agent-src/commands/video/stitch.md +0 -1
  196. package/dist/agent-src/commands/video/storyboard.md +0 -1
  197. package/dist/agent-src/commands/video.md +0 -1
  198. package/dist/agent-src/commands/work.md +0 -1
  199. package/dist/agent-src/commands/worktree/cleanup.md +0 -1
  200. package/dist/agent-src/commands/worktree/create.md +0 -1
  201. package/dist/agent-src/commands/worktree/status.md +0 -1
  202. package/dist/agent-src/commands/worktree/verify.md +0 -1
  203. package/dist/agent-src/commands/worktree.md +0 -1
  204. package/dist/agent-src/contexts/model-recommendations.md +6 -4
  205. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  206. package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
  207. package/dist/agent-src/scripts/update_roadmap_progress.ts +32 -1
  208. package/dist/agent-src/skills/docx-authoring/SKILL.md +1 -0
  209. package/dist/agent-src/skills/git-workflow/SKILL.md +9 -1
  210. package/dist/agent-src/skills/pdf-tools/SKILL.md +1 -0
  211. package/dist/agent-src/skills/roadmap-writing/SKILL.md +7 -7
  212. package/dist/agent-src/skills/skill-writing/SKILL.md +35 -0
  213. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  214. package/dist/agent-src/templates/roadmaps.md +41 -3
  215. package/dist/cli/commands/commands.candidates.test.js +1 -1
  216. package/dist/cli/commands/commands.candidates.test.js.map +1 -1
  217. package/dist/cli/commands/commands.js +9 -15
  218. package/dist/cli/commands/commands.js.map +1 -1
  219. package/dist/cli/commands/profiles.test.js +7 -7
  220. package/dist/cli/commands/profiles.test.js.map +1 -1
  221. package/dist/cli/discovery/loadManifest.js.map +1 -1
  222. package/dist/cli/main.js +2 -2
  223. package/dist/cli/main.js.map +1 -1
  224. package/dist/cli/registry.js +1 -0
  225. package/dist/cli/registry.js.map +1 -1
  226. package/dist/cli-delegate/chunk-K34QTSD5.js +3895 -0
  227. package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-LKZ5DOYW.js} +557 -297
  228. package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-O52B4F5S.js} +123 -2338
  229. package/dist/cli-delegate/cmd_conformance.js +18 -6
  230. package/dist/cli-delegate/cmd_doctor.js +2 -2
  231. package/dist/cli-delegate/cmd_export.js +1 -1
  232. package/dist/cli-delegate/cmd_handoff.js +1 -1
  233. package/dist/cli-delegate/cmd_prune.js +1 -1
  234. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  235. package/dist/cli-delegate/cmd_sync.js +1 -1
  236. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  237. package/dist/cli-delegate/cmd_validate.js +1 -1
  238. package/dist/discovery/deprecation-report.md +1 -1
  239. package/dist/discovery/discovery-manifest.json +213 -412
  240. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  241. package/dist/discovery/discovery-manifest.summary.md +2 -2
  242. package/dist/discovery/dormancy-report.md +1 -1
  243. package/dist/discovery/orphan-report.md +1 -1
  244. package/dist/discovery/packs.json +3 -3
  245. package/dist/discovery/trust-report.md +1 -1
  246. package/dist/discovery/workspaces.json +3 -3
  247. package/dist/hooks/dispatch.js +2588 -1082
  248. package/dist/install/install.mjs +744 -482
  249. package/dist/mcp/registry-manifest.json +3 -3
  250. package/dist/mcp/server.json +2 -2
  251. package/dist/mcp/server.mjs +159 -15
  252. package/dist/router.json +1 -1
  253. package/dist/scripts/_lib/model_tier.js +9 -0
  254. package/dist/scripts/_lib/model_tier.js.map +1 -1
  255. package/dist/scripts/condense.js +50 -0
  256. package/dist/scripts/condense.js.map +1 -1
  257. package/docs/MIGRATION.md +21 -1
  258. package/docs/contracts/settings-classes.md +41 -0
  259. package/package.json +1 -1
  260. package/src/agent-src/scripts/roadmap_gates.ts +30 -2
  261. package/src/agent-src/scripts/update_roadmap_progress.ts +32 -1
  262. package/src/config/agent-settings.template.yml +34 -0
  263. package/src/config/evaluator-budgets.json +4 -4
  264. package/src/config/gate-coverage.yml +39 -9
  265. package/src/config/gate-violation-baselines.json +9 -3
  266. package/src/config/hook-token-budget.json +7 -1
  267. package/src/config/preamble-payload-budget.json +28 -0
  268. package/src/scripts/_dispatch.bash +52 -5
  269. package/src/scripts/_lib/git_common_dir.ts +173 -0
  270. package/src/scripts/_lib/model_tier.ts +9 -0
  271. package/src/scripts/_lib/release_highlights.ts +108 -7
  272. package/src/scripts/_lib/skill_catalogue.ts +862 -0
  273. package/src/scripts/ai_council/clients.ts +182 -3
  274. package/src/scripts/ai_council/config.ts +16 -1
  275. package/src/scripts/ai_council/low_impact.ts +14 -1
  276. package/src/scripts/ai_council/orchestrator.ts +14 -1
  277. package/src/scripts/ai_council/transport_resolver.ts +32 -2
  278. package/src/scripts/audit_command_surface.ts +21 -15
  279. package/src/scripts/build_catalog_index.ts +1 -1
  280. package/src/scripts/build_discovery_manifest.ts +30 -34
  281. package/src/scripts/capture_skill_catalogue.ts +185 -328
  282. package/src/scripts/check_branch_freshness.ts +217 -62
  283. package/src/scripts/check_release_published.ts +6 -2
  284. package/src/scripts/check_release_trunk_sync.ts +18 -2
  285. package/src/scripts/condense.ts +50 -1
  286. package/src/scripts/dispatch_r2_reviewer.ts +15 -5
  287. package/src/scripts/evidence_report.ts +15 -4
  288. package/src/scripts/gen_discovery_baseline.ts +3 -3
  289. package/src/scripts/hook_manifest.yaml +67 -13
  290. package/src/scripts/hooks/concern_registry.ts +2 -0
  291. package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
  292. package/src/scripts/hooks/subagent_ledger_hook.ts +17 -4
  293. package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
  294. package/src/scripts/hot_context_hook.ts +13 -1
  295. package/src/scripts/install.ts +129 -0
  296. package/src/scripts/janitor.ts +77 -2
  297. package/src/scripts/lint_command_tiers.ts +29 -75
  298. package/src/scripts/lint_originality.ts +101 -14
  299. package/src/scripts/lint_plan_risk_register.ts +22 -9
  300. package/src/scripts/lint_roadmap_blockers.ts +109 -6
  301. package/src/scripts/lint_scheduled_deprecations.ts +738 -0
  302. package/src/scripts/migration_status.ts +17 -1
  303. package/src/scripts/probe_review_binding_drift.ts +731 -0
  304. package/src/scripts/release.ts +95 -1
  305. package/src/scripts/schemas/command.schema.json +6 -11
  306. package/src/scripts/schemas/skill.schema.json +12 -2
  307. package/src/scripts/schemas/subagent.schema.json +2 -2
  308. package/src/scripts/session_eol_report.ts +81 -0
  309. package/src/scripts/skill_linter.ts +82 -2
  310. package/src/scripts/workspace_doctor.ts +464 -0
  311. package/src/scripts/worktree_cleanup_check.ts +113 -8
  312. package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
@@ -247,19 +247,19 @@ export function main(): number {
247
247
  out.push('');
248
248
 
249
249
  out.push('## Sample commands (first 10)\n');
250
- out.push('| File | name | tier | cluster |');
250
+ out.push('| File | name | visibility | cluster |');
251
251
  out.push('|---|---|---|---|');
252
252
  for (const c of commands.slice(0, 10)) {
253
253
  const fm = _frontmatter(c);
254
254
  const rel = _relativeTo(c, SRC);
255
- out.push(`| \`${rel}\` | ${fm.name ?? ''} | ${fm.tier ?? ''} | ${fm.cluster ?? ''} |`);
255
+ out.push(`| \`${rel}\` | ${fm.name ?? ''} | ${fm.visibility ?? ''} | ${fm.cluster ?? ''} |`);
256
256
  }
257
257
  out.push('');
258
258
 
259
259
  out.push('## Notes\n');
260
260
  out.push('- Every skill carries `domain:` today — Phase 4 mapping uses it as primary signal.');
261
261
  out.push('- Rules use `type` (always | auto) + `tier` (safety-floor | kernel | …); no `domain:` today.');
262
- out.push('- Commands use `tier` + `cluster`; no `domain:` today.');
262
+ out.push('- Commands use `visibility` + `cluster`; no `domain:` today.');
263
263
  out.push('- Templates have no consistent frontmatter; Phase 4 quarantines them in `unassigned-artefacts.yml`.');
264
264
  out.push('');
265
265
 
@@ -556,6 +556,30 @@ concerns:
556
556
  args: []
557
557
  fail_closed: false
558
558
  severity: advisory
559
+ # road-to-inbox-harvest-2026-08-d-context-ledger Phase 1 Step 1.1 — the
560
+ # tool-result byte counter. `hook-token-budget.json` registers the gap it
561
+ # closes in its own words: bytes-into-context from tool RESULTS are
562
+ # instrumented nowhere, so every context-cost claim in this tree is made
563
+ # against a denominator missing its dominant term.
564
+ #
565
+ # CAPTURE ONLY. It appends `{ts, tool, bytes, measurable}` to a gitignored
566
+ # census file and emits NOTHING to the model — zero injection bytes, which
567
+ # is why it carries a metric registration rather than a per-concern byte
568
+ # cap. The threshold question (when is a result too large) is deliberately
569
+ # unanswered: the distribution has never been observed, and the house rule
570
+ # is baseline before mechanism.
571
+ #
572
+ # The record type has four fields and none can hold free text — a tool
573
+ # NAME, a byte COUNT, a timestamp and a measurable flag. Tool results
574
+ # routinely carry file contents and command output; none of it can reach
575
+ # this output because the output has no field able to carry it.
576
+ #
577
+ # Never blocks, never warns: exit 0 on every path. fail_closed: false.
578
+ tool-result-bytes:
579
+ script: src/scripts/hooks/tool_result_bytes_hook.ts
580
+ args: []
581
+ fail_closed: false
582
+ severity: advisory
559
583
  # road-to-subagent-lifecycle-integrity Phase 3 Step 1 — spawn guard, SHADOW
560
584
  # ONLY, per docs/contracts/concern-activation-policy.md. On every Agent/Task
561
585
  # call it appends one `spawn_guard_shadow` line recording what a guard WOULD
@@ -590,6 +614,26 @@ concerns:
590
614
  # blocker: in-process Agent-tool subagents inherit the host process env and
591
615
  # cannot be marked per-spawn; the axis binds where the suite launches a CLI
592
616
  # session itself.
617
+ #
618
+ # Superseded in part, 2026-08-14 (road-to-subagent-lifecycle-integrity Phase 4
619
+ # Step 2). The env half above still stands — nothing marks an in-process spawn
620
+ # per-process. What has changed is that env is no longer the only candidate
621
+ # channel: `subagent_start` / `subagent_stop` are bound on this host
622
+ # (2.1.229, Phase 0 Step 1) and their payload carries `agent_id`, which
623
+ # `subagent-ledger` correlates on today. So a payload-bound role axis —
624
+ # `agent_id` present ⇒ role `worker` — is a live proposal (Phase 4 Step 1),
625
+ # not a closed question.
626
+ # It is NOT implemented, and the reason is a missing measurement rather than a
627
+ # missing decision: whether `agent_id` reaches a `PreToolUse` / `PostToolUse`
628
+ # payload inside a subagent is unverified on this host. That is Phase 0 Step 4,
629
+ # blocked on `raw-capture-needs-host-env` (the capture itself is already
630
+ # shipped — `dispatch_hook._maybe_capture_payload`). A neighbouring assertion
631
+ # from the same spike already came back negative: `agent_type` does NOT arrive
632
+ # on `SubagentStop` (18 of 25 stop records null), so the documented-field
633
+ # presence is not assumed for `agent_id` either.
634
+ # Until that spike lands the env path is the only one wired, and the
635
+ # pre_tool_use exemption above is unaffected either way — a payload-resolved
636
+ # `worker` still cannot drop a pre_tool_use concern.
593
637
  roles:
594
638
  worker:
595
639
  # Orchestrator-only concerns (Phase 2.3): a worker never talks to the
@@ -603,7 +647,7 @@ platforms:
603
647
  session_end: [chat-history, memory-learn, session-register]
604
648
  stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
605
649
  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]
606
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
650
+ 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]
607
651
 
608
652
  claude:
609
653
  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]
@@ -611,14 +655,24 @@ platforms:
611
655
  stop: [chat-history, hot-context, verify-before-complete, team-review-gate, end-review-nudge, turn-end-gate, self-repair, session-register, session-eol]
612
656
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, delegation-nudge, git-authorization, session-canary, self-repair, session-register]
613
657
  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]
614
- 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]
615
- # round-5 § 6.1 — the ONLY binding on this slot in the tree. `pre_compact`
616
- # sets a pin-lost marker; the `post_tool_use` entry above re-emits the pin
617
- # exactly once while that marker is set, then clears it. One extra
618
- # injection per compaction EVENT — never one per tool call, which is the
619
- # shape § 6.2 refuses. claude only: it is the sole verified platform, and
620
- # the measured compaction defect is a Claude Code transcript fact.
621
- pre_compact: [language-mirror]
658
+ 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
+ # `language-mirror` — round-5 § 6.1. `pre_compact` sets a pin-lost marker;
660
+ # the `post_tool_use` entry above re-emits the pin exactly once while that
661
+ # marker is set, then clears it. One extra injection per compaction EVENT —
662
+ # never one per tool call, which is the shape § 6.2 refuses. claude only: it
663
+ # is the sole verified platform, and the measured compaction defect is a
664
+ # Claude Code transcript fact. (It was the ONLY binding on this slot until
665
+ # the entry below; that sentence is retired rather than left to rot.)
666
+ #
667
+ # `hot-context` — road-to-inbox-harvest-2026-08-d-context-ledger Step 2.1.
668
+ # This is the one slot that fires WHILE state is being destroyed, and it
669
+ # carried no WRITER at all: the cache a post-compaction `session_start`
670
+ # restores was written at the last `stop`, so everything the session did
671
+ # between that stop and the compaction was lost with it. The concern is
672
+ # already bound on `stop` and `session_start`; this adds the write at the
673
+ # moment the loss actually happens. Deterministic rebuild, emits nothing
674
+ # here — the 4,096-byte row it carries is for its session_start restore.
675
+ pre_compact: [language-mirror, hot-context]
622
676
  # road-to-subagent-lifecycle-integrity Phase 1 Step 1. `claude` is the only
623
677
  # platform where both native events were measured present on the installed
624
678
  # binary (2.1.229 — see the evidence page cited on the alias rows below).
@@ -655,7 +709,7 @@ platforms:
655
709
  stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
656
710
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
657
711
  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
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
712
+ 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]
659
713
  # subagent-ledger IS bound here, unlike the two nudges above, and the
660
714
  # difference is the delivery channel rather than the platform: the nudges
661
715
  # depend on stdout reaching the model, which this trampoline discards by
@@ -678,7 +732,7 @@ platforms:
678
732
  session_end: [chat-history, memory-learn, session-register]
679
733
  stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
680
734
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
681
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
735
+ 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]
682
736
 
683
737
  # Phase 7.6 — Cline. Hooks live under `.clinerules/hooks/<HookName>`
684
738
  # (project, no file extension, must be executable per Cline docs) or
@@ -693,7 +747,7 @@ platforms:
693
747
  session_end: [chat-history, memory-learn, session-register]
694
748
  stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
695
749
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
696
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
750
+ 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]
697
751
 
698
752
  # Phase 7.7 — Windsurf (Cascade). Hooks live at `.windsurf/hooks.json`
699
753
  # (project) or `~/.codeium/windsurf/hooks.json` (user). Cascade has
@@ -729,7 +783,7 @@ platforms:
729
783
  session_end: [chat-history, memory-learn, session-register]
730
784
  stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
731
785
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
732
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
786
+ 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]
733
787
 
734
788
  # Phase 7.9 — Copilot has no hook surface. Concerns route through
735
789
  # rule-only fallback; the dispatcher silently no-ops on --platform copilot.
@@ -63,6 +63,7 @@ import { main as editShapeMain } from './edit_shape_hook.js';
63
63
  import { main as rereadGuardMain } from './reread_guard_hook.js';
64
64
  import { main as sessionEolMain } from './session_eol_hook.js';
65
65
  import { main as subagentLedgerMain } from './subagent_ledger_hook.js';
66
+ import { main as toolResultBytesMain } from './tool_result_bytes_hook.js';
66
67
  import { main as spawnGuardShadowMain } from './spawn_guard_shadow_hook.js';
67
68
 
68
69
  /** A concern `main` — argv-taking or not; both shapes exist. */
@@ -109,5 +110,6 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
109
110
  'src/scripts/hooks/reread_guard_hook.ts': rereadGuardMain as ConcernMain,
110
111
  'src/scripts/hooks/session_eol_hook.ts': sessionEolMain as ConcernMain,
111
112
  'src/scripts/hooks/subagent_ledger_hook.ts': subagentLedgerMain as ConcernMain,
113
+ 'src/scripts/hooks/tool_result_bytes_hook.ts': toolResultBytesMain as ConcernMain,
112
114
  'src/scripts/hooks/spawn_guard_shadow_hook.ts': spawnGuardShadowMain as ConcernMain,
113
115
  };
@@ -243,6 +243,27 @@ export function buildRecordInput(facts: DispatchFacts, ts: string, id: string):
243
243
 
244
244
  if (!facts.isAsync) {
245
245
  if (facts.resolvedModel) {
246
+ // road-to-inbox-harvest-2026-08-d-top-band-model-economy Step 1.1.
247
+ //
248
+ // The raw id goes into the field the schema already reserves for it
249
+ // ("the model id the provider reported SERVING"). Without it, a
250
+ // dispatch that ran on a band the three-tier vocabulary cannot
251
+ // express is byte-identical in the record to one where the host
252
+ // reported no model at all: `extractModelFamily` returns null for
253
+ // both, so `tiers` is omitted for both.
254
+ //
255
+ // That collapse is precisely what blocks ADR-035's reopen
256
+ // condition. "Did a vendor ship a band the three tiers cannot
257
+ // express" is answerable only if an unmapped id is distinguishable
258
+ // from an absent one, and recording the served id separates them:
259
+ // unmapped ⇒ `model_served` set with `tiers` absent; unreported ⇒
260
+ // both absent.
261
+ //
262
+ // Vendor-neutral by construction: the id is stored verbatim as an
263
+ // opaque host string. No band above `high` is named and no mapping
264
+ // is invented — naming a fourth band is the decision this
265
+ // measurement exists to inform, never one it may pre-empt.
266
+ input.model_served = facts.resolvedModel;
246
267
  const family = extractModelFamily(facts.resolvedModel);
247
268
  if (family) input.tiers = [family];
248
269
  }
@@ -73,8 +73,9 @@
73
73
  * a `subagent_reaped` line is written for it. That line is not bookkeeping —
74
74
  * it is the closest thing this instrument has to a direct observation of a
75
75
  * never-returning dispatch, which is why the leak is reported rather than
76
- * quietly swept. `session_id` rides on the record so a future sweep can scope
77
- * by session; nothing reads it yet and it is not claimed to.
76
+ * quietly swept. `session_id` rides on the open record AND on both dispatch
77
+ * lines so a rate can be scoped by session; nothing reads it yet and it is not
78
+ * claimed to.
78
79
  *
79
80
  * ── Depth and concurrency (Step 3) ────────────────────────────────────────
80
81
  *
@@ -598,6 +599,13 @@ function handleStart(root: string, payload: JsonObject, nowIso: string, sessionI
598
599
  event: 'subagent_start',
599
600
  ts: nowIso,
600
601
  ref,
602
+ // Phase 1 Step 4 correction (b): without this the dispatch denominator
603
+ // is not per-session. Three sessions share one ledger file, so a rate
604
+ // computed over the file aggregates strangers — the measured window
605
+ // held 7 starts against 25 stops, one of them an agent type this
606
+ // session never dispatched. The open record already carried it; the
607
+ // appended line did not, and the line is what a rate is computed from.
608
+ session_id: sessionId,
601
609
  agent_type: rec.agent_type,
602
610
  parent_ref: parentRef,
603
611
  depth,
@@ -606,7 +614,7 @@ function handleStart(root: string, payload: JsonObject, nowIso: string, sessionI
606
614
  });
607
615
  }
608
616
 
609
- function handleStop(root: string, payload: JsonObject, nowIso: string): void {
617
+ function handleStop(root: string, payload: JsonObject, nowIso: string, sessionId: string | null): void {
610
618
  const agentId = str(payload, 'agent_id', 'agentId');
611
619
  if (agentId === null) {
612
620
  appendUnidentified(root, 'subagent_stop', nowIso, payload);
@@ -631,6 +639,11 @@ function handleStop(root: string, payload: JsonObject, nowIso: string): void {
631
639
  event: 'subagent_stop',
632
640
  ts: nowIso,
633
641
  ref,
642
+ // The session this stop was OBSERVED in, read from the same envelope
643
+ // position as the start's — never back-filled from `rec`, which would
644
+ // relabel a cross-session stop as belonging to the dispatching session
645
+ // and hide exactly the mismatch this field exists to expose.
646
+ session_id: sessionId,
634
647
  agent_type: rec?.agent_type ?? str(payload, 'agent_type', 'agentType', 'subagent_type', 'subagentType'),
635
648
  depth: rec?.depth ?? null,
636
649
  depth_basis: rec?.depth_basis ?? null,
@@ -661,7 +674,7 @@ export function processEnvelope(envelope: JsonValue, consumerRoot: string): numb
661
674
  reapStaleOpenRecords(consumerRoot, nowIso);
662
675
 
663
676
  if (event === 'subagent_start') handleStart(consumerRoot, payload, nowIso, sessionId);
664
- else handleStop(consumerRoot, payload, nowIso);
677
+ else handleStop(consumerRoot, payload, nowIso, sessionId);
665
678
  } catch {
666
679
  // Malformed payload, unreadable disk, anything — never disturb the run.
667
680
  return EXIT_ALLOW;
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Tool-result byte counter — capture only, no behaviour change
4
+ * (road-to-inbox-harvest-2026-08-d-context-ledger Phase 1, Step 1.1).
5
+ *
6
+ * Why this exists. `hook-token-budget.json` registers the gap in its own
7
+ * words: *"bytes-into-context from tool RESULTS is not instrumented
8
+ * anywhere (the census covers hook payloads only)"*. The injection census
9
+ * measures what HOOKS put into context — a few kilobytes per turn under a
10
+ * per-slot cap. It does not measure what TOOLS put into context, which is
11
+ * the larger number by any plausible margin and the one nobody can cite.
12
+ * Every claim about context cost in this tree is therefore made against a
13
+ * denominator that excludes its own dominant term.
14
+ *
15
+ * This concern is the instrument and ONLY the instrument. It appends; it
16
+ * never decides, never advises, and emits nothing to the model. The
17
+ * threshold question — when is a tool result too large — is deliberately
18
+ * NOT answered here: the distribution has never been observed, and a
19
+ * threshold set before the distribution is a number invented rather than
20
+ * measured. That ordering is the house rule this repository has paid to
21
+ * learn more than once.
22
+ *
23
+ * ── What is recorded, and what is refused ──────────────────────────────
24
+ *
25
+ * PRIVACY BY CONSTRUCTION — never widen this file to record content.
26
+ *
27
+ * The record type has exactly three fields and NONE of them can hold free
28
+ * text: a timestamp, a tool NAME (an id-shaped enum — `Read`, `Bash`,
29
+ * `Grep` — the same single host string `orchestration_record_hook` already
30
+ * records), and a BYTE COUNT. A tool result routinely carries file
31
+ * contents, command output, secrets in an env dump, and customer data from
32
+ * an API response. None of it reaches this file's output, because the
33
+ * output has no field able to carry it. A record shape that cannot hold a
34
+ * secret has no scrubber that can fail — the same construction
35
+ * `domain-safety-pii` § Surface 2 prescribes for logs and the reason the
36
+ * sibling `subagent-ledger` refuses `last_assistant_message` in any form.
37
+ *
38
+ * The result is measured, never read: `_resultBytes` computes a length and
39
+ * discards the value in the same expression.
40
+ *
41
+ * ── Always exit 0 ─────────────────────────────────────────────────────
42
+ *
43
+ * A `warn` (exit 2) is read as a hard BLOCK on this host, and this concern
44
+ * has nothing to say to the model. Every path returns EXIT_ALLOW —
45
+ * malformed envelope, unparseable payload, unwritable disk. An instrument
46
+ * that can fail a turn is a mechanism, and this is not one yet.
47
+ *
48
+ * ── Absence is recorded, not inferred ─────────────────────────────────
49
+ *
50
+ * A payload carrying no readable result writes a line with `bytes: null`
51
+ * and `measurable: false` rather than writing nothing. The two cases are
52
+ * otherwise byte-identical in the output — "this host does not carry tool
53
+ * results in its PostToolUse payload" and "no tools ran" would produce the
54
+ * same empty file, and the first is a finding about the instrument's own
55
+ * coverage that the baseline has to be able to see. This is the
56
+ * instrument-goes-quiet failure the sibling ledger's round-1 review fixed
57
+ * one layer down, applied here before it can be shipped.
58
+ */
59
+ import * as fs from 'node:fs';
60
+ import * as path from 'node:path';
61
+ import { fileURLToPath, pathToFileURL } from 'node:url';
62
+
63
+ import { is_replay_mode } from './state_io.js';
64
+ import { readHookStdin } from './hook_stdin.js';
65
+
66
+ const EXIT_ALLOW = 0;
67
+
68
+ type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
69
+ type JsonObject = { [k: string]: JsonValue };
70
+
71
+ function isObject(v: unknown): v is JsonObject {
72
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
73
+ }
74
+
75
+ /**
76
+ * Census file, relative to the consumer repo root. Sibling of
77
+ * `injection-census.jsonl` in the same directory and the same
78
+ * `{ts, …, bytes}` line shape — one family, two units: that file counts
79
+ * bytes this suite injects, this one counts bytes the host's tools do.
80
+ * Gitignored via `/agents/runtime/`.
81
+ */
82
+ export const CENSUS_FILE = path.join('agents', 'runtime', 'state', 'tool-result-census.jsonl');
83
+
84
+ /** The payload keys a host may use for a tool's result, widest first. */
85
+ const RESULT_KEYS = ['tool_response', 'toolResponse', 'tool_result', 'toolUseResult'] as const;
86
+
87
+ /**
88
+ * UTF-8 byte length of a tool result, or `null` when the payload carries
89
+ * none under any known key.
90
+ *
91
+ * A result is an object on most tools and a bare string on some (`Bash`
92
+ * returns its stdout directly — see the sibling hook's own test corpus), so
93
+ * both shapes are measured. The sibling's `extractToolResult` returns
94
+ * `JsonObject | null` and would report a bare-string result as absent,
95
+ * which is correct for its purpose (it reads named usage fields) and wrong
96
+ * for this one: a 400 KB command output is exactly the case this counter
97
+ * exists to see, and it is precisely the shape that extractor drops.
98
+ *
99
+ * Bytes, not characters: a multibyte result costs what it costs on the
100
+ * wire, and `String.length` would under-report it.
101
+ */
102
+ export function _resultBytes(payload: JsonObject): number | null {
103
+ for (const key of RESULT_KEYS) {
104
+ const v = payload[key];
105
+ if (v === undefined || v === null) continue;
106
+ if (typeof v === 'string') return Buffer.byteLength(v, 'utf8');
107
+ try {
108
+ return Buffer.byteLength(JSON.stringify(v), 'utf8');
109
+ } catch {
110
+ // Circular or otherwise unserialisable — unmeasurable, not zero.
111
+ return null;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /** The tool's name, read from the same positions the sibling hook reads. */
118
+ export function _toolName(payload: JsonObject, envelope: JsonObject): string | null {
119
+ const candidates = [
120
+ payload['tool_name'],
121
+ payload['toolName'],
122
+ payload['tool'],
123
+ envelope['tool_name'],
124
+ envelope['tool'],
125
+ ];
126
+ for (const v of candidates) {
127
+ if (typeof v === 'string' && v) return v;
128
+ }
129
+ return null;
130
+ }
131
+
132
+ function unwrapPayload(envelope: JsonObject): JsonObject {
133
+ const inner = envelope['payload'];
134
+ return isObject(inner) ? inner : envelope;
135
+ }
136
+
137
+ /** `${ts.slice(0,7)}` is NOT used here — the census is one flat file, as its sibling is. */
138
+ function censusFile(consumerRoot: string): string {
139
+ return path.join(consumerRoot, CENSUS_FILE);
140
+ }
141
+
142
+ export interface CensusLine {
143
+ ts: string;
144
+ tool: string | null;
145
+ bytes: number | null;
146
+ /** `false` ⇒ the payload carried no readable result. See the header. */
147
+ measurable: boolean;
148
+ }
149
+
150
+ export function _appendCensusLine(consumerRoot: string, line: CensusLine): void {
151
+ if (is_replay_mode()) return;
152
+ const file = censusFile(consumerRoot);
153
+ fs.mkdirSync(path.dirname(file), { recursive: true });
154
+ fs.appendFileSync(file, `${JSON.stringify(line)}\n`, 'utf8');
155
+ }
156
+
157
+ /** Process an ALREADY-PARSED dispatcher envelope. Always returns EXIT_ALLOW. */
158
+ export function processEnvelope(envelope: JsonValue, consumerRoot: string): number {
159
+ try {
160
+ if (!isObject(envelope)) return EXIT_ALLOW;
161
+ if (envelope['event'] !== 'post_tool_use') return EXIT_ALLOW;
162
+
163
+ const payload = unwrapPayload(envelope);
164
+ const bytes = _resultBytes(payload);
165
+
166
+ _appendCensusLine(consumerRoot, {
167
+ ts: new Date().toISOString(),
168
+ tool: _toolName(payload, envelope),
169
+ bytes,
170
+ measurable: bytes !== null,
171
+ });
172
+ } catch {
173
+ // Malformed payload, unwritable disk, anything — never disturb the run.
174
+ return EXIT_ALLOW;
175
+ }
176
+ return EXIT_ALLOW;
177
+ }
178
+
179
+ /** Resolve the consumer repo root, same precedence as the sibling ledger. */
180
+ export function resolveConsumerRoot(envelope: JsonValue): string {
181
+ if (isObject(envelope)) {
182
+ for (const key of ['workspace_root', 'project_root']) {
183
+ const v = envelope[key];
184
+ if (typeof v === 'string' && v) return v;
185
+ }
186
+ const cwd = unwrapPayload(envelope)['cwd'];
187
+ if (typeof cwd === 'string' && cwd) return cwd;
188
+ }
189
+ return process.cwd();
190
+ }
191
+
192
+ export function main(): number {
193
+ const raw = readHookStdin();
194
+ let envelope: JsonValue = {};
195
+ try {
196
+ envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
197
+ } catch {
198
+ return EXIT_ALLOW;
199
+ }
200
+ return processEnvelope(envelope, resolveConsumerRoot(envelope));
201
+ }
202
+
203
+ // Bundle-safety: never auto-run when inlined into an esbuild bundle, where
204
+ // every module shares the bundle's `import.meta.url`.
205
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
206
+ function _isCliEntry(): boolean {
207
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
208
+ return false;
209
+ }
210
+ if (process.argv[1] === undefined) return false;
211
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
212
+ if (import.meta.url === argvUrl) return true;
213
+ try {
214
+ return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
215
+ } catch {
216
+ return false;
217
+ }
218
+ }
219
+ if (_isCliEntry()) process.exit(main());
@@ -379,7 +379,19 @@ export function main(): number {
379
379
  if (_is_replay_mode()) {
380
380
  return 0; // replay fixtures: read-only, no state mutation
381
381
  }
382
- if (event === 'stop' || event === 'session_end') {
382
+ // `pre_compact` joins the write set (road-to-inbox-harvest-2026-08-d-
383
+ // context-ledger Step 2.1). It is the only slot that fires WHILE state
384
+ // is being destroyed, and until now it carried no writer at all: the
385
+ // cache a post-compaction `session_start` restores was written at the
386
+ // last `stop`, so everything the session did since then was lost with
387
+ // the compaction. Writing here makes the restore reflect the moment
388
+ // before the loss instead of the moment before the last turn.
389
+ //
390
+ // Cheap and idempotent by construction: `_write_hot_context` is a
391
+ // deterministic rebuild from the history file, so an extra fire costs
392
+ // one rebuild and can never produce a worse cache than the older one it
393
+ // replaces. It emits nothing — the restore side is unchanged.
394
+ if (event === 'stop' || event === 'session_end' || event === 'pre_compact') {
383
395
  _write_hot_context(root);
384
396
  } else if (event === 'session_start') {
385
397
  const source = String(payload.source ?? '');
@@ -81,6 +81,12 @@ import * as rule_layer_overlap from './_lib/rule_layer_overlap.js';
81
81
  import * as installed_tools from './_lib/installed_tools.js';
82
82
  import { collect_drift, format_drift_report } from './_lib/install_drift.js';
83
83
  import * as user_global_paths from './_lib/user_global_paths.js';
84
+ // The LIBRARY half, deliberately not `capture_skill_catalogue.js`: that module
85
+ // is a CLI entry with a top-level `process.exit()`, and esbuild would bundle
86
+ // that exit into the installer a consumer loads (`check_installer_import_purity`
87
+ // catches exactly this). The lib has no entry and no exit.
88
+ import * as catalogue from './_lib/skill_catalogue.js';
89
+ import { isInteractiveSession, type SessionProbe } from '../shared/interactiveContext.js';
84
90
  import * as claude_desktop_bundler from './_lib/claude_desktop_bundler.js';
85
91
  import * as claude_settings_hooks from './_lib/claude_settings_hooks.js';
86
92
  import { find_project_root_with_anchor, load_agent_settings } from './_lib/agent_settings.js';
@@ -3899,6 +3905,32 @@ function install_global(
3899
3905
  if (!state.QUIET) warn(`settings-surface snapshot failed — ${String(e)}`);
3900
3906
  }
3901
3907
 
3908
+ // Catalogue-budget warning (road-to-skill-catalogue-budget Phase 1).
3909
+ // Some hosts silently truncate the skill catalogue they were handed: the
3910
+ // deploy reports success, and the model never sees most of what was
3911
+ // deployed. A host that publishes its own dropped count has a MEASURED
3912
+ // ceiling; one that does not gets no warning and no invented number.
3913
+ // Warn only — over-shipping is the safe direction, and a consumer who
3914
+ // accepts truncation is making a legitimate choice.
3915
+ if (!state.QUIET) {
3916
+ try {
3917
+ for (const line of _catalogue_truncation_warnings(deploy_results, project_root)) {
3918
+ warn(line);
3919
+ }
3920
+ const notice = _scoped_migration_notice(deploy_results, project_root, package_root, {
3921
+ env: process.env,
3922
+ stdinTty: process.stdin.isTTY === true,
3923
+ stdoutTty: process.stdout.isTTY === true,
3924
+ });
3925
+ if (notice.length > 0) {
3926
+ process.stdout.write('\n');
3927
+ for (const line of notice) info(line);
3928
+ }
3929
+ } catch {
3930
+ // A measurement must never sink an install that otherwise worked.
3931
+ }
3932
+ }
3933
+
3902
3934
  if (!state.QUIET) {
3903
3935
  process.stdout.write('\n');
3904
3936
  success(`Global install completed (v${installed_version}).`);
@@ -3907,6 +3939,103 @@ function install_global(
3907
3939
  return 0;
3908
3940
  }
3909
3941
 
3942
+ /**
3943
+ * One line per deployed host whose catalogue exceeds a ceiling this machine has
3944
+ * actually measured. Empty when nothing has been measured — the observation log
3945
+ * lives where captures accumulate, and an install on a machine that never ran
3946
+ * one has no basis to claim a limit.
3947
+ *
3948
+ * Returns lines rather than printing them so the decision is testable without
3949
+ * capturing stdout.
3950
+ */
3951
+ export function _catalogue_truncation_warnings(
3952
+ deploy_results: Record<string, DeployResult>,
3953
+ project_root: string | null,
3954
+ ): string[] {
3955
+ const log_candidates = [
3956
+ path.join(user_global_paths.event4u_root(), 'state', 'skill-catalogue.jsonl'),
3957
+ ...(project_root ? [path.join(project_root, catalogue.OBSERVATION_LOG)] : []),
3958
+ ];
3959
+ const records = log_candidates.flatMap((p) => catalogue.readObservationLog(p));
3960
+ if (records.length === 0) return [];
3961
+
3962
+ const limits = catalogue.knownHostLimits(records);
3963
+ if (limits.size === 0) return [];
3964
+
3965
+ const lines: string[] = [];
3966
+ for (const tool_id of Object.keys(deploy_results).sort()) {
3967
+ const [, , status] = deploy_results[tool_id] as DeployResult;
3968
+ if (status !== 'deployed') continue;
3969
+ const limit = limits.get(tool_id);
3970
+ if (limit === undefined) continue;
3971
+ const anchor_raw = USER_SCOPE_PATHS[tool_id];
3972
+ if (!anchor_raw) continue;
3973
+
3974
+ const volume = catalogue.measureCatalogueVolume(tool_id, expanduser(anchor_raw));
3975
+ const warning = catalogue.catalogueLimitWarning(volume, limit);
3976
+ if (warning !== null) lines.push(warning);
3977
+ }
3978
+ return lines;
3979
+ }
3980
+
3981
+ /**
3982
+ * The scoped-projection migration notice for an EXISTING install.
3983
+ *
3984
+ * Shape decided by AI council 2026-08-15 (2/2 converged, both members present):
3985
+ * option B — the existing warning PLUS an interactive notice that persists
3986
+ * NOTHING and names the manual edit and the GUI route. `projection.mode` is
3987
+ * settings class C, so no agent may write it; a notice that offered a CLI
3988
+ * write would send the reader to a command guaranteed to refuse them.
3989
+ *
3990
+ * Eligibility and delivery are separate on purpose. A non-interactive run can
3991
+ * be fully eligible; it simply gets the warning and no notice, and the two
3992
+ * outcomes stay distinguishable instead of collapsing into "nothing printed".
3993
+ *
3994
+ * Known and accepted: with nothing persisted, a qualifying install sees this
3995
+ * on every deploy. The council named that as the strongest argument for doing
3996
+ * nothing at all, and it does not outweigh making a measured, silent
3997
+ * capability loss an explicit human decision.
3998
+ */
3999
+ export function _scoped_migration_notice(
4000
+ deploy_results: Record<string, DeployResult>,
4001
+ project_root: string | null,
4002
+ package_root: string,
4003
+ probe: SessionProbe,
4004
+ ): string[] {
4005
+ if (!isInteractiveSession(probe)) return [];
4006
+
4007
+ const log_candidates = [
4008
+ path.join(user_global_paths.event4u_root(), 'state', 'skill-catalogue.jsonl'),
4009
+ ...(project_root ? [path.join(project_root, catalogue.OBSERVATION_LOG)] : []),
4010
+ ];
4011
+ const limits = catalogue.knownHostLimits(log_candidates.flatMap((p) => catalogue.readObservationLog(p)));
4012
+ if (limits.size === 0) return [];
4013
+
4014
+ const resolved = _resolve_scoped_projection(package_root);
4015
+ const settings_path =
4016
+ _resolve_global_settings_path() ??
4017
+ path.join(user_global_paths.event4u_root(), 'settings', SETTINGS_FILE);
4018
+
4019
+ for (const tool_id of Object.keys(deploy_results).sort()) {
4020
+ const [, , status] = deploy_results[tool_id] as DeployResult;
4021
+ if (status !== 'deployed') continue;
4022
+ const anchor_raw = USER_SCOPE_PATHS[tool_id];
4023
+ if (!anchor_raw) continue;
4024
+
4025
+ const volume = catalogue.measureCatalogueVolume(tool_id, expanduser(anchor_raw));
4026
+ const eligibility = catalogue.migrationEligibility(
4027
+ tool_id,
4028
+ resolved.mode,
4029
+ volume.skillEntries,
4030
+ limits,
4031
+ );
4032
+ if (eligibility.eligible) {
4033
+ return catalogue.migrationPromptLines(tool_id, eligibility, settings_path);
4034
+ }
4035
+ }
4036
+ return [];
4037
+ }
4038
+
3910
4039
  const SETTINGS_SURFACE_REL = path.join('state', 'settings-surface.json');
3911
4040
  const SETTINGS_DELTA_REL = path.join('state', 'settings-delta.json');
3912
4041