@event4u/agent-config 16.2.0 → 16.3.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 (332) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +89 -237
  5. package/README.md +14 -12
  6. package/dist/agent-src/commands/analyze/inbox.md +1 -1
  7. package/dist/agent-src/commands/analyze/roadmap-repos.md +1 -1
  8. package/dist/agent-src/commands/bug/fix.md +17 -2
  9. package/dist/agent-src/commands/feature/plan.md +1 -1
  10. package/dist/agent-src/commands/feature/roadmap.md +1 -1
  11. package/dist/agent-src/commands/fix/ci.md +1 -1
  12. package/dist/agent-src/commands/implement-ticket.md +1 -1
  13. package/dist/agent-src/commands/jira-ticket.md +1 -1
  14. package/dist/agent-src/commands/roadmap/create.md +1 -1
  15. package/dist/agent-src/commands/roadmap/materialize.md +1 -1
  16. package/dist/agent-src/commands/roadmap/process-full.md +4 -4
  17. package/dist/agent-src/commands/roadmap/resolve-blockers.md +361 -0
  18. package/dist/agent-src/commands/roadmap/triage-parked.md +325 -0
  19. package/dist/agent-src/commands/roadmap.md +18 -4
  20. package/dist/agent-src/commands/tests/create.md +17 -3
  21. package/dist/agent-src/commands/tests/execute.md +22 -5
  22. package/dist/agent-src/contexts/authority/decision-revisit-gate-enforcement.md +43 -0
  23. package/dist/agent-src/contexts/communication/rules-auto/design-fidelity-enforcement.md +24 -0
  24. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +6 -6
  25. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +32 -2
  26. package/dist/agent-src/contexts/execution/session-canary-enforcement-history.md +67 -0
  27. package/dist/agent-src/contexts/execution/toolchain-resolver.md +101 -7
  28. package/dist/agent-src/rules/active-remediation.md +1 -1
  29. package/dist/agent-src/rules/autonomous-execution.md +1 -0
  30. package/dist/agent-src/rules/broken-access-control.md +1 -0
  31. package/dist/agent-src/rules/command-suggestion-policy.md +1 -0
  32. package/dist/agent-src/rules/context-hygiene.md +1 -1
  33. package/dist/agent-src/rules/decision-revisit-gate.md +16 -33
  34. package/dist/agent-src/rules/design-fidelity.md +13 -16
  35. package/dist/agent-src/rules/doc-screenshot-hygiene.md +1 -0
  36. package/dist/agent-src/rules/domain-safety-disclaimer.md +1 -0
  37. package/dist/agent-src/rules/domain-safety-pii.md +1 -0
  38. package/dist/agent-src/rules/domain-safety-retention.md +1 -0
  39. package/dist/agent-src/rules/engineering-safety-floor.md +1 -0
  40. package/dist/agent-src/rules/evaluator-independence.md +3 -2
  41. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
  42. package/dist/agent-src/rules/finance-safety-floor.md +1 -0
  43. package/dist/agent-src/rules/git-history-discipline.md +1 -0
  44. package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
  45. package/dist/agent-src/rules/legal-safety-floor.md +3 -2
  46. package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
  47. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  48. package/dist/agent-src/rules/media-governance-routing.md +14 -17
  49. package/dist/agent-src/rules/persona-governance.md +2 -2
  50. package/dist/agent-src/rules/provider-lifecycle-discipline.md +1 -1
  51. package/dist/agent-src/rules/question-not-instruction.md +1 -0
  52. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  53. package/dist/agent-src/rules/runtime-safety.md +1 -0
  54. package/dist/agent-src/rules/secret-vcs-guard.md +1 -0
  55. package/dist/agent-src/rules/security-sensitive-stop.md +3 -2
  56. package/dist/agent-src/rules/session-canary.md +8 -62
  57. package/dist/agent-src/rules/settings-ask-protocol.md +1 -0
  58. package/dist/agent-src/rules/source-confidentiality.md +1 -0
  59. package/dist/agent-src/rules/strategy-safety-floor.md +1 -0
  60. package/dist/agent-src/rules/tool-safety.md +1 -0
  61. package/dist/agent-src/rules/untrusted-input-defense.md +1 -0
  62. package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
  63. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
  64. package/dist/agent-src/scripts/deferral_carry.ts +339 -0
  65. package/dist/agent-src/scripts/update_roadmap_progress.ts +4 -1
  66. package/dist/agent-src/skills/api-design/data/manifest.json +2 -2
  67. package/dist/agent-src/skills/api-testing/SKILL.md +17 -0
  68. package/dist/agent-src/skills/code-intelligence/SKILL.md +14 -2
  69. package/dist/agent-src/skills/design-system-capture/references/import-procedure.md +1 -1
  70. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +2 -2
  71. package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
  72. package/dist/agent-src/skills/iconography/SKILL.md +1 -1
  73. package/dist/agent-src/skills/js-library-packaging/SKILL.md +2 -2
  74. package/dist/agent-src/skills/monorepo-workspace/SKILL.md +1 -1
  75. package/dist/agent-src/skills/quality-tools/SKILL.md +11 -1
  76. package/dist/agent-src/skills/quality-tools/references/go-tools.md +76 -0
  77. package/dist/agent-src/skills/quality-tools/references/python-tools.md +104 -0
  78. package/dist/agent-src/skills/review-routing/SKILL.md +2 -2
  79. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  80. package/dist/agent-src/skills/storybook-workshop/SKILL.md +3 -3
  81. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  82. package/dist/agent-src/skills/test-case-discovery/SKILL.md +7 -0
  83. package/dist/agent-src/skills/test-driven-development/SKILL.md +17 -0
  84. package/dist/agent-src/skills/test-performance/SKILL.md +19 -1
  85. package/dist/agent-src/skills/ui-component-architect/SKILL.md +2 -2
  86. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  87. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
  88. package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
  89. package/dist/cli/registry.js +1 -1
  90. package/dist/cli-delegate/archive_completed_roadmaps.js +92 -12
  91. package/dist/cli-delegate/check_update_banner.js +12 -1
  92. package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
  93. package/dist/cli-delegate/{chunk-M2DPGFZ4.js → chunk-6V6TA25H.js} +22 -152
  94. package/dist/cli-delegate/chunk-7IQE3D5S.js +191 -0
  95. package/dist/cli-delegate/{chunk-6KFEC3G7.js → chunk-CQWX336L.js} +1 -1
  96. package/dist/cli-delegate/chunk-DEIV7XP7.js +108 -0
  97. package/dist/cli-delegate/chunk-EEHDXSYD.js +2040 -0
  98. package/dist/cli-delegate/{chunk-P6ZHP5W2.js → chunk-GKE7ZVFA.js} +4 -4
  99. package/dist/cli-delegate/chunk-JQOM7TTM.js +1201 -0
  100. package/dist/cli-delegate/{chunk-IGD6ERX6.js → chunk-JQVYKIZA.js} +10 -0
  101. package/dist/cli-delegate/{chunk-FC75PGJE.js → chunk-JY2I2DXV.js} +12 -1
  102. package/dist/cli-delegate/chunk-KB5FZ655.js +208 -0
  103. package/dist/cli-delegate/{chunk-7RQP4UDH.js → chunk-KR6EM23A.js} +235 -1455
  104. package/dist/cli-delegate/{chunk-LTYVIY76.js → chunk-LIYTLH6O.js} +1 -1
  105. package/dist/cli-delegate/{chunk-YJ72UIAK.js → chunk-NQ7WT43P.js} +1186 -244
  106. package/dist/cli-delegate/chunk-NXEILF2K.js +2160 -0
  107. package/dist/cli-delegate/{chunk-5R7HOLAF.js → chunk-NYX3L2PZ.js} +1 -1
  108. package/dist/cli-delegate/{chunk-4E2W63DA.js → chunk-OKJUIAPS.js} +62 -1
  109. package/dist/cli-delegate/{chunk-QAJS5YWY.js → chunk-PHD7OOLE.js} +2 -1
  110. package/dist/cli-delegate/{chunk-RGNIWTEA.js → chunk-QLDHIPWR.js} +1 -1
  111. package/dist/cli-delegate/{chunk-7XGBX6ZH.js → chunk-RQNNW7IF.js} +1314 -560
  112. package/dist/cli-delegate/{chunk-4XFP2Y3E.js → chunk-S4UMZNES.js} +1 -1
  113. package/dist/cli-delegate/{chunk-6KM6ORRY.js → chunk-S4YWNRKO.js} +24 -8
  114. package/dist/cli-delegate/{chunk-FP36SWJD.js → chunk-TRI6DQZ6.js} +2781 -340
  115. package/dist/cli-delegate/chunk-ULQRUR23.js +208 -0
  116. package/dist/cli-delegate/{chunk-NUHLI2RX.js → chunk-YCU3TIXH.js} +2 -0
  117. package/dist/cli-delegate/chunk-ZBZV2G5R.js +210 -0
  118. package/dist/cli-delegate/{chunk-WGXTVKCQ.js → chunk-ZUZKZYCN.js} +5 -3
  119. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  120. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  121. package/dist/cli-delegate/cmd_conformance.js +1167 -234
  122. package/dist/cli-delegate/cmd_doctor.js +13 -6
  123. package/dist/cli-delegate/cmd_doctor_neighbours.js +23 -0
  124. package/dist/cli-delegate/cmd_explain.js +5 -5
  125. package/dist/cli-delegate/cmd_export.js +11 -8
  126. package/dist/cli-delegate/cmd_fleet.js +3 -3
  127. package/dist/cli-delegate/cmd_handoff.js +10 -7
  128. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  129. package/dist/cli-delegate/cmd_memory_get.js +3 -1
  130. package/dist/cli-delegate/cmd_migrate.js +2 -2
  131. package/dist/cli-delegate/cmd_packs_active.js +3 -3
  132. package/dist/cli-delegate/cmd_preflight.js +6 -6
  133. package/dist/cli-delegate/cmd_prune.js +11 -8
  134. package/dist/cli-delegate/cmd_refresh.js +2 -1
  135. package/dist/cli-delegate/cmd_route_audit.js +8 -6
  136. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  137. package/dist/cli-delegate/cmd_session_recycle.js +4 -3
  138. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  139. package/dist/cli-delegate/cmd_settings_migrate.js +11 -8
  140. package/dist/cli-delegate/cmd_settings_set.js +4 -4
  141. package/dist/cli-delegate/cmd_sync.js +11 -8
  142. package/dist/cli-delegate/cmd_uninstall.js +11 -8
  143. package/dist/cli-delegate/cmd_update.js +1 -1
  144. package/dist/cli-delegate/cmd_upgrade.js +3 -2
  145. package/dist/cli-delegate/cmd_validate.js +11 -8
  146. package/dist/cli-delegate/cmd_versions.js +1 -1
  147. package/dist/cli-delegate/deferral_carry.js +16 -0
  148. package/dist/cli-delegate/gate_execute.js +2 -2
  149. package/dist/cli-delegate/roadmap_gates.js +5 -5
  150. package/dist/cli-delegate/update_roadmap_progress.js +1 -1
  151. package/dist/discovery/deprecation-report.md +1 -1
  152. package/dist/discovery/discovery-manifest.json +127 -75
  153. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  154. package/dist/discovery/discovery-manifest.summary.md +3 -3
  155. package/dist/discovery/dormancy-report.md +1 -1
  156. package/dist/discovery/orphan-report.md +1 -1
  157. package/dist/discovery/packs.json +7 -5
  158. package/dist/discovery/trust-report.md +2 -2
  159. package/dist/discovery/workspaces.json +6 -4
  160. package/dist/hooks/dispatch.js +5357 -6656
  161. package/dist/hooks/dispatch.sha256 +1 -1
  162. package/dist/install/claudePathsPlan.js +47 -6
  163. package/dist/install/claudeRuleRewrite.js +7 -0
  164. package/dist/install/emit_host_rules_cli.js +39 -36
  165. package/dist/install/globalRuleScope.js +87 -0
  166. package/dist/install/host_rule_ownership.js +160 -0
  167. package/dist/install/install.mjs +2080 -1024
  168. package/dist/install/installThinLayer.js +261 -0
  169. package/dist/install/installedRuleLinks.js +176 -0
  170. package/dist/install/wizard-plan.js +24 -0
  171. package/dist/mcp/registry-manifest.json +3 -3
  172. package/dist/mcp/server.json +2 -2
  173. package/dist/mcp/server.mjs +31965 -27154
  174. package/dist/scripts/_lib/agent_settings.js +11 -0
  175. package/dist/scripts/_lib/claude_settings_hooks.js +10 -8
  176. package/dist/scripts/_lib/installed_layer.js +378 -0
  177. package/dist/scripts/_lib/lean_projection_mode.js +218 -15
  178. package/dist/scripts/_lib/rule_consequence_class.js +45 -0
  179. package/dist/scripts/_lib/rule_injection.js +67 -10
  180. package/dist/scripts/_lib/rule_law_section.js +122 -0
  181. package/dist/scripts/_lib/thin_rules.js +458 -0
  182. package/dist/scripts/_lib/windsurf_render.js +60 -0
  183. package/dist/scripts/condense.js +14 -26
  184. package/dist/scripts/project_thin_rules.js +14 -294
  185. package/dist/server/schemas/settings.js +6 -0
  186. package/dist/shared/skillRanking.js +74 -13
  187. package/docs/contracts/settings-classes.md +7 -5
  188. package/package.json +1 -1
  189. package/src/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
  190. package/src/agent-src/scripts/deferral_carry.ts +339 -0
  191. package/src/agent-src/scripts/update_roadmap_progress.ts +4 -1
  192. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
  193. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
  194. package/src/config/agent-settings.template.yml +62 -22
  195. package/src/config/continuity-surface.json +7 -0
  196. package/src/config/evaluator-budgets.json +4 -4
  197. package/src/config/gate-coverage.yml +96 -3
  198. package/src/config/gate-violation-baselines.json +17 -9
  199. package/src/config/hook-bundle-budget.json +25 -0
  200. package/src/config/hook-token-budget.json +6 -1
  201. package/src/config/host-instruction-limits.json +33 -0
  202. package/src/config/routing-coverage-seed.json +24 -3
  203. package/src/config/rule-activation-census.json +7 -3
  204. package/src/config/rule-consequence-class.json +161 -0
  205. package/src/config/rule-law-ceilings.json +99 -0
  206. package/src/install/claudePathsPlan.ts +46 -4
  207. package/src/install/claudeRuleRewrite.ts +10 -1
  208. package/src/install/emit_host_rules_cli.ts +43 -34
  209. package/src/install/globalRuleScope.ts +109 -0
  210. package/src/install/host_rule_ownership.ts +170 -0
  211. package/src/install/installThinLayer.ts +312 -0
  212. package/src/install/installedRuleLinks.ts +220 -0
  213. package/src/install/wizard-plan.ts +24 -0
  214. package/src/scripts/_cli/cmd_doctor.ts +23 -71
  215. package/src/scripts/_cli/cmd_doctor_neighbours.ts +293 -0
  216. package/src/scripts/_dispatch.bash +16 -2
  217. package/src/scripts/_lib/agent_settings.ts +11 -0
  218. package/src/scripts/_lib/claude_settings_hooks.ts +10 -8
  219. package/src/scripts/_lib/estate_offsets.ts +241 -0
  220. package/src/scripts/_lib/exemption_shape.ts +143 -0
  221. package/src/scripts/_lib/fnv.ts +23 -0
  222. package/src/scripts/_lib/foreign_scan.ts +106 -0
  223. package/src/scripts/_lib/gitignore_block.ts +314 -0
  224. package/src/scripts/_lib/global_deploy_inventory.ts +32 -0
  225. package/src/scripts/_lib/graph_feeder_record.ts +214 -0
  226. package/src/scripts/_lib/hook_settings.ts +73 -0
  227. package/src/scripts/_lib/host_capability.ts +40 -19
  228. package/src/scripts/_lib/host_hook_merge.ts +165 -0
  229. package/src/scripts/_lib/installed_layer.ts +538 -0
  230. package/src/scripts/_lib/json_merge.ts +82 -0
  231. package/src/scripts/_lib/json_pointers.ts +93 -11
  232. package/src/scripts/_lib/lean_projection_mode.ts +275 -15
  233. package/src/scripts/_lib/machine_wake.ts +16 -3
  234. package/src/scripts/_lib/neighbour_census.ts +760 -0
  235. package/src/scripts/_lib/neighbour_scan.ts +122 -0
  236. package/src/scripts/_lib/neighbour_tool_use.ts +189 -0
  237. package/src/scripts/_lib/prompt_shape.ts +46 -0
  238. package/src/scripts/_lib/release_migration_gate.ts +19 -3
  239. package/src/scripts/_lib/reserved_name_sweep.ts +73 -0
  240. package/src/scripts/_lib/review_coverage.ts +134 -0
  241. package/src/scripts/_lib/rule_consequence_class.ts +71 -0
  242. package/src/scripts/_lib/rule_injection.ts +130 -10
  243. package/src/scripts/_lib/rule_law_section.ts +147 -0
  244. package/src/scripts/_lib/rule_layer_overlap.ts +58 -0
  245. package/src/scripts/_lib/skill_origin.ts +254 -0
  246. package/src/scripts/_lib/structured_ask.ts +64 -21
  247. package/src/scripts/_lib/thin_rules.ts +540 -0
  248. package/src/scripts/_lib/touched_file_quality.ts +434 -0
  249. package/src/scripts/_lib/turn_end_refusals.ts +77 -2
  250. package/src/scripts/_lib/windsurf_render.ts +63 -0
  251. package/src/scripts/ai_council/cli_call_budget.ts +13 -5
  252. package/src/scripts/ai_council/clients.ts +26 -57
  253. package/src/scripts/ai_council/config.ts +1 -1
  254. package/src/scripts/ai_council/vendor_defaults.ts +45 -0
  255. package/src/scripts/audit_skill_overlap.ts +261 -0
  256. package/src/scripts/before_complete_hook.ts +243 -12
  257. package/src/scripts/bench_touched_file_quality.ts +238 -0
  258. package/src/scripts/check_enforcement_matrix.ts +377 -20
  259. package/src/scripts/check_estate_count.ts +130 -153
  260. package/src/scripts/check_finding_dispositions.ts +116 -4
  261. package/src/scripts/check_hook_bundle_composition.ts +370 -0
  262. package/src/scripts/check_kernel_edit_ratified.ts +49 -11
  263. package/src/scripts/check_memory.ts +7 -1
  264. package/src/scripts/check_release_highlights.ts +10 -0
  265. package/src/scripts/check_release_pr_shape.ts +38 -15
  266. package/src/scripts/check_standing_rule_delivery.ts +33 -13
  267. package/src/scripts/check_verify_expectation_delta.ts +521 -0
  268. package/src/scripts/code_graph/detect.ts +127 -7
  269. package/src/scripts/code_graph/foreign.ts +194 -0
  270. package/src/scripts/code_graph/query.ts +217 -4
  271. package/src/scripts/code_graph/types.ts +26 -2
  272. package/src/scripts/code_graph/validate.ts +3 -1
  273. package/src/scripts/code_graph/verbs.ts +103 -0
  274. package/src/scripts/compile_hook_manifest.ts +33 -1
  275. package/src/scripts/condense.ts +14 -27
  276. package/src/scripts/estate_exemption_shape.ts +202 -0
  277. package/src/scripts/generate_host_cost_table.ts +1 -1
  278. package/src/scripts/hook_manifest.json +1 -1
  279. package/src/scripts/hook_manifest.yaml +150 -9
  280. package/src/scripts/hooks/block_config_weakening.ts +67 -2
  281. package/src/scripts/hooks/block_plumbing_writes.ts +29 -1
  282. package/src/scripts/hooks/code_graph_context_hook.ts +252 -23
  283. package/src/scripts/hooks/concern_failure_policy.ts +280 -0
  284. package/src/scripts/hooks/dispatch_hook.ts +52 -52
  285. package/src/scripts/hooks/exit_codes.ts +2 -1
  286. package/src/scripts/hooks/host_lowering.json +1 -1
  287. package/src/scripts/hooks/host_lowering.ts +19 -1
  288. package/src/scripts/hooks/one_question_per_ask_hook.ts +41 -25
  289. package/src/scripts/hooks/payload_stub.ts +10 -1
  290. package/src/scripts/hooks/rule_inject_hook.ts +757 -63
  291. package/src/scripts/hooks/table_fingerprint.ts +21 -0
  292. package/src/scripts/hooks/telemetry_usage_hook.ts +64 -8
  293. package/src/scripts/hooks/turn_end_gate_hook.ts +84 -0
  294. package/src/scripts/hooks_status.ts +112 -0
  295. package/src/scripts/injection_scan_hook.ts +7 -0
  296. package/src/scripts/install.ts +72 -185
  297. package/src/scripts/installed_layer_report.ts +103 -0
  298. package/src/scripts/language_mirror_hook.ts +15 -6
  299. package/src/scripts/learning_sidecar.ts +119 -8
  300. package/src/scripts/lint_hook_manifest.ts +53 -0
  301. package/src/scripts/lint_major_migration_sections.ts +156 -26
  302. package/src/scripts/lint_pack_boundaries.ts +63 -5
  303. package/src/scripts/lint_rule_law_section.ts +517 -0
  304. package/src/scripts/lint_workflow_security.ts +161 -5
  305. package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -0
  306. package/src/scripts/mcp_server/graph_tools.ts +72 -3
  307. package/src/scripts/measure_routing_signal.ts +54 -16
  308. package/src/scripts/measure_skill_ranker_baseline.ts +216 -8
  309. package/src/scripts/memory_learn_hook.ts +91 -6
  310. package/src/scripts/model_rule_injection.ts +52 -5
  311. package/src/scripts/project_thin_rules.ts +34 -327
  312. package/src/scripts/report_evidence_temperature.ts +657 -0
  313. package/src/scripts/report_installed_rule_links.ts +157 -0
  314. package/src/scripts/report_label_agreement.ts +233 -0
  315. package/src/scripts/report_obligation_mechanism.ts +230 -0
  316. package/src/scripts/report_single_token_triggers.ts +277 -0
  317. package/src/scripts/report_skill_ranker_confusion.ts +442 -0
  318. package/src/scripts/report_standing_payload_by_host.ts +188 -15
  319. package/src/scripts/report_verification_record_coverage.ts +306 -0
  320. package/src/scripts/rule_activation_census.ts +5 -2
  321. package/src/scripts/rule_trigger_eval.ts +9 -9
  322. package/src/scripts/schemas/review-findings.schema.json +3 -2
  323. package/src/scripts/schemas/rule.schema.json +5 -0
  324. package/src/scripts/self_review_gate.ts +62 -7
  325. package/src/scripts/skill_tools/score_skill_relevance.ts +290 -25
  326. package/src/scripts/sweep_skill_ranker_signals.ts +129 -0
  327. package/src/scripts/sync_gitignore.ts +29 -235
  328. package/src/scripts/trigger_eval_grandfather.json +0 -5
  329. package/src/server/schemas/settings.ts +10 -0
  330. package/src/shared/skillRanking.ts +117 -11
  331. package/dist/cli-delegate/chunk-JNMVLWLR.js +0 -4457
  332. package/dist/cli-delegate/chunk-YDPILCOL.js +0 -152
@@ -266,6 +266,17 @@ export const MERGEABLE_KEYS = [
266
266
  // GUI; whitelisting decides only whether the value SURVIVES the merge
267
267
  // (ADR-271, road-to-design-intent-conformance 2.3).
268
268
  'design.fidelity_mode',
269
+ // `lean_projection.*` is a per-MACHINE setting and the install it governs is
270
+ // a user-scope one: the layer being thinned is `~/.claude/rules`, which
271
+ // belongs to the machine and not to any checkout. Without these two rows a
272
+ // consumer who wrote the opt-in into the user-global file got no error, no
273
+ // warning and no effect — the same silent-filter defect the two entries
274
+ // above repair, reaching a third key, and unsatisfiable-by-construction on
275
+ // an ADR-020 global-only install, where no project layer exists to carry it.
276
+ // Whitelisting decides only whether a value SURVIVES the merge: it sets
277
+ // nothing, and `installerThinsHost` still refuses a template-only value.
278
+ 'lean_projection.mode',
279
+ 'lean_projection.hosts',
269
280
  ];
270
281
  /** The shipped template, relative to the package root. */
271
282
  const TEMPLATE_RELATIVE = path.join('src', 'config', 'agent-settings.template.yml');
@@ -70,17 +70,19 @@ export function build_claude_hook_matrix(manifest_path) {
70
70
  const claude_events = (platforms['claude'] ?? {}) || {};
71
71
  const aliasesAll = (manifest['native_event_aliases'] ?? {});
72
72
  const aliases = (aliasesAll['claude'] ?? {}) || {};
73
- const ac_to_native = {};
74
- for (const [native, ac] of Object.entries(aliases)) {
75
- ac_to_native[String(ac)] = native;
76
- }
73
+ // Iterating the ALIAS map, not the event map, because the relation is
74
+ // many-natives-to-one-event and the inversion this replaced silently lost
75
+ // every native but the last. `PostToolUse` and `PostToolUseFailure` both
76
+ // lower to `post_tool_use`; building `ac_to_native` would have kept one of
77
+ // them and UNBOUND the other, turning a routing fix into a routing
78
+ // regression that no existing test could see. Pinned by
79
+ // `before_complete_failure_envelope.test.ts` ("both natives stay bound").
77
80
  const matrix = {};
78
- for (const [ac_event, concerns] of Object.entries(claude_events)) {
81
+ for (const [native, ac_raw] of Object.entries(aliases)) {
82
+ const ac_event = String(ac_raw);
83
+ const concerns = claude_events[ac_event];
79
84
  if (!concerns || (Array.isArray(concerns) && concerns.length === 0))
80
85
  continue;
81
- const native = ac_to_native[ac_event];
82
- if (native === undefined)
83
- continue;
84
86
  // Bundle fast path (road-to-hook-latency-repair Phase 2): invoke the
85
87
  // precompiled dispatcher bundle directly — no bash shim, no CLI boot
86
88
  // (measured ~450-500 ms CLI vs ~110 ms bundle per event on a 1-vCPU
@@ -0,0 +1,378 @@
1
+ /**
2
+ * What a host actually loads, measured in the host's own unit (step 0.1 of
3
+ * `road-to-a-rule-carrier-that-works-outside-the-repo`).
4
+ *
5
+ * THE QUESTION NOTHING IN THIS TREE COULD ANSWER. Every existing figure for the
6
+ * installed layer counts raw bytes of whole files: `censusRuleDir` sums
7
+ * `statSync().size`, `charsAtRoot` does the same for a directory, and
8
+ * `check_preamble_payload_budget` reads both. A host does not load frontmatter
9
+ * as instruction — it is the routing surface, which the router has already
10
+ * read — and it does not render HTML comments, which are authoring scaffolding.
11
+ * So the number a reader wants when they ask "how much instruction is this
12
+ * consumer under" has never been computed here.
13
+ *
14
+ * THE NUMBER IS THEREFORE NOT COMPARABLE WITH ANY PUBLISHED BASELINE, and that
15
+ * is stated here rather than discovered by someone holding the two side by
16
+ * side. `chars` below is strictly smaller than the byte figures in
17
+ * `budgets.yml`, `rule-activation-census.json` or the payload ratchet, and the
18
+ * gap is not a constant — it is whatever that particular corpus spends on
19
+ * frontmatter and comments.
20
+ *
21
+ * ONE LIBRARY, FOUR CONSUMERS. The report CLI is the only one wired in this
22
+ * change (Phase 0 makes no behavior change), and the shape is the reason the
23
+ * other three named in step 0.1 — CI, `doctor`, the upgrade receipt — can use
24
+ * it without a second reader: every input is a PARAMETER. `home`, `projectRoot`
25
+ * and `manifestPath` are supplied by the caller, never read off the process, so
26
+ * a test, a temporary `HOME` and a real machine are the same call with
27
+ * different arguments. That is also what makes the step's "install into a
28
+ * temporary HOME" reachable at all, and it is the fact that refutes
29
+ * `check_standing_rule_delivery`'s header: a user-scope install IS observable
30
+ * from CI, because the installer takes `HOME` from the environment.
31
+ *
32
+ * WHAT `package_owned` AND `foreign` MEAN, because the honest answer is three
33
+ * valued and the report has two columns. `recordedOwnership` distinguishes
34
+ * `recorded-unchanged`, `recorded-modified` and `unknown`, where `unknown` is
35
+ * explicitly "this tree cannot say" rather than "foreign". The report folds the
36
+ * first two into `package_owned` — both are files the manifest claims — and
37
+ * reports the third as `foreign`, which therefore reads "not claimed by this
38
+ * install", never "written by someone else". A machine with no manifest reports
39
+ * every file foreign, which is the correct reading of no evidence and is why
40
+ * {@link InstalledLayerReport.manifest_present} is in the output.
41
+ */
42
+ import * as fs from 'node:fs';
43
+ import * as path from 'node:path';
44
+ import { fileURLToPath } from 'node:url';
45
+ import { GLOBAL_RULE_DIRS, PROJECT_RULE_DIRS, globalRuleLayerPath, } from '../../install/globalRuleLayers.js';
46
+ import { NO_RECORDED_HASHES, readRecordedHashes } from '../../install/recordedOwnership.js';
47
+ import { ruleBody } from './rule_law_section.js';
48
+ /** How many files the per-directory ranking carries. Step 0.1 fixes it at 20. */
49
+ export const TOP_FILES = 20;
50
+ /**
51
+ * The fraction of a published limit at which the report warns.
52
+ *
53
+ * 0.8 is step 1.4's number, not a tuned one. It is a WARNING threshold and
54
+ * never a gate: crossing it changes what the report says and nothing else, so
55
+ * no install fails here.
56
+ */
57
+ export const LIMIT_WARN_FRACTION = 0.8;
58
+ /**
59
+ * Whether the host loads this file on every session rather than on a path match.
60
+ *
61
+ * `paths:` at the start of a line is the whole test, and it is the host's own
62
+ * convention rather than this package's: `claudeRuleRewrite.ts` states it from
63
+ * the emitting side — "with it a rule loads on a path match, without it the
64
+ * rule loads unconditionally". `rule_activation_census.projection_reading` asks
65
+ * the installed tree the same question with the same regex, so a file counted
66
+ * unconditional here is counted unconditional there.
67
+ *
68
+ * SOURCE-SIDE CLASSIFICATION IS A DIFFERENT QUESTION and deliberately not used.
69
+ * `classify_rule` reads `alwaysApply` and `type: always` to tell `always` from
70
+ * `unconditional`, which matters when you are deciding what to emit. This
71
+ * reads what WAS emitted, where the distinction has already been spent: an
72
+ * Iron-Law rule and an untriggered tier-2 rule both arrive without `paths:` and
73
+ * both load every session, which is the only property the host acts on.
74
+ */
75
+ export function isUnconditional(text) {
76
+ return !/^paths:/m.test(text);
77
+ }
78
+ /**
79
+ * Measure one directory.
80
+ *
81
+ * `.md` only, matching `censusRuleDir` and `topRulesByCost`, so the FILE count
82
+ * is comparable with the existing byte censuses even though the character count
83
+ * is not. Cursor's `.mdc` tree is therefore read as zero files, which is the
84
+ * same blind spot every other census in this tree has and is not fixed here —
85
+ * fixing it in one reader and not the others would make two counts that
86
+ * disagree, which is worse than one that is openly partial.
87
+ */
88
+ export function readLayer(host, scope, dir, recorded) {
89
+ const empty = {
90
+ files: 0,
91
+ unconditional: 0,
92
+ chars: 0,
93
+ package_owned: 0,
94
+ foreign: 0,
95
+ package_owned_chars: 0,
96
+ foreign_chars: 0,
97
+ top: [],
98
+ };
99
+ let names;
100
+ try {
101
+ names = fs.readdirSync(dir);
102
+ }
103
+ catch {
104
+ return { host, scope, dir, present: false, ...empty };
105
+ }
106
+ const readings = [];
107
+ for (const name of names.sort()) {
108
+ if (!name.endsWith('.md'))
109
+ continue;
110
+ const abs = path.join(dir, name);
111
+ let text;
112
+ try {
113
+ const st = fs.statSync(abs);
114
+ if (!st.isFile())
115
+ continue;
116
+ text = fs.readFileSync(abs, 'utf-8');
117
+ }
118
+ catch {
119
+ continue; // a file that cannot be read is not a file the host loads
120
+ }
121
+ // RECORDED-BUT-MODIFIED IS STILL RECORDED. The report's two columns
122
+ // answer "does this install claim the file"; `classifyOwnership` answers
123
+ // the finer question the upgrade path needs — whether the bytes still
124
+ // match — and folding that in here would give a column three meanings.
125
+ const owned = recorded.has(abs);
126
+ readings.push({
127
+ file: name,
128
+ // THE STRIP IS THE PROJECTOR'S OWN. `ruleBody` is what
129
+ // `project_thin_rules` and the delivery carrier both call, so "what
130
+ // a host loads" and "what a delivery carries" cannot drift into two
131
+ // spellings of the same measurement.
132
+ chars: ruleBody(text).length,
133
+ unconditional: isUnconditional(text),
134
+ package_owned: owned,
135
+ });
136
+ }
137
+ const top = [...readings]
138
+ // RANKED BY THE SAME MEASURE AS THE TOTAL, which is why
139
+ // `preamble_byte_census.topRulesByCost` is not reused here: it ranks by
140
+ // `statSync().size`, so its list and this total would be in different
141
+ // units and the largest file by one could be absent from the other.
142
+ .sort((a, b) => b.chars - a.chars || a.file.localeCompare(b.file))
143
+ .slice(0, TOP_FILES);
144
+ return {
145
+ host,
146
+ scope,
147
+ dir,
148
+ present: true,
149
+ files: readings.length,
150
+ unconditional: readings.filter((r) => r.unconditional).length,
151
+ chars: readings.reduce((n, r) => n + r.chars, 0),
152
+ package_owned: readings.filter((r) => r.package_owned).length,
153
+ foreign: readings.filter((r) => !r.package_owned).length,
154
+ package_owned_chars: readings.reduce((n, r) => n + (r.package_owned ? r.chars : 0), 0),
155
+ foreign_chars: readings.reduce((n, r) => n + (r.package_owned ? 0 : r.chars), 0),
156
+ top,
157
+ };
158
+ }
159
+ /**
160
+ * Where the limits live, derived from this module rather than from `cwd`.
161
+ *
162
+ * The two `..` hops climb from `src/scripts/_lib` to `src/`, which is where
163
+ * `config/` lives — NOT to the package root, though the comment said so until
164
+ * 2026-10-05. The result is correct and the arithmetic stated for it was not,
165
+ * which is the half a future reader would trust when changing the hops.
166
+ *
167
+ * `fileURLToPath`, NOT `new URL(...).pathname` — the claimed parity with
168
+ * `default_template_path()` was only half true until 2026-10-05. A `pathname`
169
+ * percent-encodes, so a package under a directory with a space resolved to
170
+ * `/Users/x/My%20Projects/...`, and on Windows it yields a leading-slash drive
171
+ * path. Either way the read fails, `loadHostInstructionLimits` swallows it and
172
+ * returns an empty map, and EVERY host then reads `unpublished` with `warn`
173
+ * false — the budget measurement degrading to "not measured" with no signal,
174
+ * which is the one failure mode this file's own limit table was written to
175
+ * avoid.
176
+ */
177
+ export function defaultHostLimitsPath() {
178
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'config', 'host-instruction-limits.json');
179
+ }
180
+ /**
181
+ * Read the published-limit table.
182
+ *
183
+ * TOLERANT IN ONE DIRECTION ONLY. An unreadable or malformed file yields an
184
+ * EMPTY table, so every host reads `unpublished` and nothing warns — a reading
185
+ * this tree cannot stand behind must never become a warning it can. A present
186
+ * entry whose `limit` is not a positive finite number is treated the same way,
187
+ * because a `0` or a string would otherwise make every install look saturated.
188
+ */
189
+ export function loadHostInstructionLimits(file = defaultHostLimitsPath()) {
190
+ const out = new Map();
191
+ let raw;
192
+ try {
193
+ raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
194
+ }
195
+ catch {
196
+ return out; // no table ⇒ no limits ⇒ no warnings
197
+ }
198
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
199
+ return out;
200
+ for (const [host, v] of Object.entries(raw)) {
201
+ if (host.startsWith('_'))
202
+ continue; // `_comment` and friends are not hosts
203
+ if (typeof v !== 'object' || v === null || Array.isArray(v))
204
+ continue;
205
+ const e = v;
206
+ const n = e.limit;
207
+ const limit = typeof n === 'number' && Number.isFinite(n) && n > 0 ? n : null;
208
+ const k = e.kind;
209
+ const kind = limit === null
210
+ ? 'unpublished'
211
+ : k === 'vendor-published' || k === 'reported'
212
+ ? k
213
+ : 'reported'; // a cited figure with no stated kind is the weaker one
214
+ out.set(host, { limit, kind });
215
+ }
216
+ return out;
217
+ }
218
+ /**
219
+ * Fold the per-directory readings into one row per host, against its limit.
220
+ *
221
+ * Exported so a caller holding layers it built itself — the install receipt,
222
+ * which measures before and after — can produce the same rows without
223
+ * re-reading the tree.
224
+ */
225
+ export function buildHostLimitRows(layers, limits) {
226
+ const byHost = new Map();
227
+ for (const l of layers) {
228
+ if (!l.present)
229
+ continue;
230
+ const acc = byHost.get(l.host) ?? { chars: 0, owned: 0, foreign: 0 };
231
+ acc.chars += l.chars;
232
+ acc.owned += l.package_owned_chars;
233
+ acc.foreign += l.foreign_chars;
234
+ byHost.set(l.host, acc);
235
+ }
236
+ // EVERY host that has a directory gets a row, present or not — a host whose
237
+ // layer is absent reads 0 against its limit, which is a real answer and the
238
+ // one step 3.5 asks for ("one row per host with a published limit").
239
+ for (const l of layers)
240
+ if (!byHost.has(l.host))
241
+ byHost.set(l.host, { chars: 0, owned: 0, foreign: 0 });
242
+ // AND every host the LIMITS table knows, whether or not a layer reached it.
243
+ // Rows were layer-driven while the requirement is limit-driven, so a limit
244
+ // recorded for a host the layer map does not list disappeared silently —
245
+ // the two sources agree today, which means they can only diverge in the
246
+ // direction the code did not cover.
247
+ for (const [host, e] of limits)
248
+ if (e.limit !== null && !byHost.has(host)) {
249
+ byHost.set(host, { chars: 0, owned: 0, foreign: 0 });
250
+ }
251
+ const rows = [];
252
+ for (const host of [...byHost.keys()].sort()) {
253
+ const a = byHost.get(host);
254
+ const e = limits.get(host) ?? { limit: null, kind: 'unpublished' };
255
+ const fraction = e.limit === null ? null : a.chars / e.limit;
256
+ rows.push({
257
+ host,
258
+ limit: e.limit,
259
+ kind: e.kind,
260
+ chars: a.chars,
261
+ package_owned_chars: a.owned,
262
+ foreign_chars: a.foreign,
263
+ fraction,
264
+ warn: fraction !== null && fraction >= LIMIT_WARN_FRACTION,
265
+ });
266
+ }
267
+ return rows;
268
+ }
269
+ /**
270
+ * Read every host rule directory in both scopes.
271
+ *
272
+ * BOTH SCOPES, BECAUSE THE HOST UNIONS THEM. A reading of the global layer
273
+ * alone would understate a consumer whose project carries its own rules, and
274
+ * the carrier's own scope filter reads the union for the same reason
275
+ * (`rule_layer_overlap.hostRuleLayerIds`).
276
+ */
277
+ export function buildInstalledLayerReport(opts) {
278
+ const manifestPath = opts.manifestPath ?? null;
279
+ const recorded = manifestPath === null
280
+ ? NO_RECORDED_HASHES
281
+ : readRecordedHashes(manifestPath, opts.projectRoot);
282
+ const layers = [];
283
+ for (const host of Object.keys(GLOBAL_RULE_DIRS).sort()) {
284
+ const dir = globalRuleLayerPath(host, opts.home);
285
+ if (dir === null)
286
+ continue;
287
+ layers.push(readLayer(host, 'global', dir, recorded));
288
+ }
289
+ for (const rel of Object.keys(PROJECT_RULE_DIRS).sort()) {
290
+ const host = PROJECT_RULE_DIRS[rel];
291
+ layers.push(readLayer(host, 'project', path.join(opts.projectRoot, rel), recorded));
292
+ }
293
+ const totals = layers.reduce((acc, l) => ({
294
+ files: acc.files + l.files,
295
+ unconditional: acc.unconditional + l.unconditional,
296
+ chars: acc.chars + l.chars,
297
+ package_owned: acc.package_owned + l.package_owned,
298
+ foreign: acc.foreign + l.foreign,
299
+ package_owned_chars: acc.package_owned_chars + l.package_owned_chars,
300
+ foreign_chars: acc.foreign_chars + l.foreign_chars,
301
+ }), {
302
+ files: 0,
303
+ unconditional: 0,
304
+ chars: 0,
305
+ package_owned: 0,
306
+ foreign: 0,
307
+ package_owned_chars: 0,
308
+ foreign_chars: 0,
309
+ });
310
+ const limits = buildHostLimitRows(layers, loadHostInstructionLimits(opts.hostLimitsPath ?? defaultHostLimitsPath()));
311
+ return {
312
+ home: opts.home,
313
+ project_root: opts.projectRoot,
314
+ host_version: opts.hostVersion ?? 'unknown',
315
+ manifest_present: manifestPath !== null && fs.existsSync(manifestPath),
316
+ layers,
317
+ limits,
318
+ totals,
319
+ };
320
+ }
321
+ /** The report as lines, for a terminal. One directory per block. */
322
+ export function renderInstalledLayerReport(report) {
323
+ const out = [];
324
+ out.push(`installed layer — host ${report.host_version}, HOME ${report.home}`);
325
+ out.push(`project ${report.project_root}`);
326
+ out.push(report.manifest_present
327
+ ? 'ownership: read from the installed-tools manifest'
328
+ : 'ownership: NO manifest resolved — every file reads foreign, which is no evidence rather than a finding');
329
+ for (const l of report.layers) {
330
+ if (!l.present) {
331
+ out.push(` ${l.host} (${l.scope}) — absent: ${l.dir}`);
332
+ continue;
333
+ }
334
+ out.push(` ${l.host} (${l.scope}) — ${String(l.files)} files, ` +
335
+ `${String(l.unconditional)} unconditional, ${String(l.chars)} chars, ` +
336
+ `${String(l.package_owned)} package-owned / ${String(l.foreign)} foreign`);
337
+ for (const f of l.top) {
338
+ out.push(` ${String(f.chars).padStart(7)} ${f.unconditional ? 'uncond' : 'scoped'} ${f.file}`);
339
+ }
340
+ }
341
+ const t = report.totals;
342
+ out.push(` TOTAL — ${String(t.files)} files, ${String(t.unconditional)} unconditional, ` +
343
+ `${String(t.chars)} chars, ${String(t.package_owned)} package-owned / ${String(t.foreign)} foreign`);
344
+ out.push(...renderHostLimitRows(report.limits));
345
+ return out;
346
+ }
347
+ /**
348
+ * The budget block — one row per host, against its published limit.
349
+ *
350
+ * Extracted so the REPORT and the INSTALL RECEIPT print the same bytes. They
351
+ * answer the same question at two moments, and a consumer comparing a receipt
352
+ * against a later report must not have to decide whether a wording difference
353
+ * means a measurement difference.
354
+ */
355
+ export function renderHostLimitRows(rows) {
356
+ // ONE ROW PER HOST, against its published limit — steps 1.4 and 3.5. The
357
+ // combined figure is what the row is about: a consumer's own instruction
358
+ // files sit in the same budget as this package's, so a package-owned number
359
+ // alone would understate what the host is actually holding.
360
+ const out = [
361
+ 'host instruction budgets — package-owned + foreign against the published limit',
362
+ ];
363
+ for (const r of rows) {
364
+ const split = `${String(r.package_owned_chars)} package-owned + ` +
365
+ `${String(r.foreign_chars)} foreign = ${String(r.chars)} chars`;
366
+ if (r.limit === null) {
367
+ out.push(` ${r.host} — ${split}; no published limit recorded — not measured`);
368
+ continue;
369
+ }
370
+ const pct = (r.fraction * 100).toFixed(1);
371
+ out.push(` ${r.host} — ${split}; ${pct}% of ${String(r.limit)} (${r.kind})` +
372
+ (r.warn
373
+ ? ` — WARNING: at or past ${String(Math.round(LIMIT_WARN_FRACTION * 100))}% of the published limit`
374
+ : ''));
375
+ }
376
+ return out;
377
+ }
378
+ //# sourceMappingURL=installed_layer.js.map
@@ -1,20 +1,64 @@
1
1
  /**
2
- * `lean_projection.mode` — ONE definition of what the three modes mean.
3
- *
4
- * The projector (`condense.ts`) resolves the value with a real YAML parse; the
5
- * delivery concern (`hooks/rule_inject_hook.ts`) resolves it with an
6
- * indentation-shaped read, because a hook must never fail a tool call because a
7
- * parser could not load. Two readers is unavoidable. Two *normalisations* is
8
- * not, and would be the defect worth preventing: a projector that writes thin
9
- * files while the concern believes the mode is off delivers pointers and no
10
- * bodies, which is exactly the 36.2 % arm the delivery mode exists to replace.
11
- *
12
- * So each side supplies the raw string from its own reader and this module
13
- * decides what it means. Anything unrecognised — absent key, typo, `null`,
14
- * a non-string — is `eager-all`: the parser fallback, which is NOT the value
15
- * the template ships (see the type below), because a mode nobody can spell
16
- * must never silently thin the standing corpus.
2
+ * `lean_projection.mode` — ONE definition of what the three modes mean, and
3
+ * since step 1.2 of `road-to-a-rule-carrier-that-works-outside-the-repo`, ONE
4
+ * reader that produces it.
5
+ *
6
+ * ── What this module used to be, and why that was not enough ──
7
+ *
8
+ * It owned the NORMALISATION and left each caller its own READER: the projector
9
+ * (`condense.ts`) resolved the value with a real YAML parse over the full
10
+ * settings cascade; the delivery concern (`hooks/rule_inject_hook.ts`) resolved
11
+ * it with an indentation-shaped read of `<cwd>/.agent-settings.yml` alone. The
12
+ * header argued that two readers were unavoidable and that two *normalisations*
13
+ * were the only thing worth preventing.
14
+ *
15
+ * Measured on 2026-10-02, that split was not a stylistic difference — it was a
16
+ * disagreement about WHICH FILE. The installer writes settings to
17
+ * `<root>/agents/settings/.agent-settings.yml` (`agent_settings.ts`'s canonical
18
+ * write target; `install.ts:4108`). The concern read `<root>/.agent-settings.yml`,
19
+ * the LEGACY location, and nothing else. On a normal install the concern
20
+ * therefore read a file that does not exist, got `''`, normalised it to
21
+ * `eager-all`, and closed its own gate — while the projector, reading the
22
+ * canonical file, had written thin stubs. That is the pointer arm the delivery
23
+ * mode exists to replace, reached by configuration rather than by choice, and
24
+ * it is a SECOND independent cause of the silence step 1.1 repaired: a consumer
25
+ * that fixed the package root alone would still have received nothing.
26
+ *
27
+ * The user-global layer was invisible to the concern for the same reason, which
28
+ * matters more than it sounds: ADR-020 installs are global-only, so for those
29
+ * consumers the ONLY layer carrying a mode is one the concern never opened.
30
+ *
31
+ * ── D3: one resolver, not a parity test between three ──
32
+ *
33
+ * The roadmap's D3 records the choice and the reason — a parity test keeps
34
+ * three readers that can drift again, and this module already existed to be the
35
+ * one. `resolveLeanProjection` below is that resolver. It reads the layers in
36
+ * the order `load_agent_settings` defines (template base, user-global,
37
+ * project), through `project_settings_path`, which prefers the canonical file
38
+ * and falls back to the legacy one, so a hand-edited root file still works.
39
+ *
40
+ * ── Why the template path is passed in rather than resolved ──
41
+ *
42
+ * `agent_settings.default_template_path()` derives the package root from
43
+ * `import.meta.url` three directories up. That is correct for a module at
44
+ * `<pkg>/src/scripts/_lib/`; inside the composed hook bundle `import.meta.url`
45
+ * is `<pkg>/dist/hooks/dispatch.js`, so the same arithmetic yields the PARENT
46
+ * of the package and the template read misses. Verified in the built bundle,
47
+ * not inferred. With no template the base layer is `{}` and an unconfigured
48
+ * consumer resolves to `eager-all` — the silence again, one layer lower. So a
49
+ * caller that knows the real package root (the carrier does: the dispatcher
50
+ * passes it) hands it over, and the resolver reads the template from there.
51
+ *
52
+ * ── What has NOT changed ──
53
+ *
54
+ * Anything unrecognised — absent key, typo, `null`, a non-string — is still
55
+ * `eager-all`: the parser fallback, which is NOT the value the template ships
56
+ * (see the type below), because a mode nobody can spell must never silently
57
+ * thin the standing corpus. That fallback now fires only when no layer carried
58
+ * a value at all, rather than whenever the concern looked in the wrong place.
17
59
  */
60
+ import * as path from 'node:path';
61
+ import { load_agent_settings } from './agent_settings.js';
18
62
  export const DEFAULT_LEAN_PROJECTION_MODE = 'eager-all';
19
63
  /** Map a raw settings value onto a mode. Unrecognised → `eager-all`. */
20
64
  export function normalizeLeanProjectionMode(raw) {
@@ -146,4 +190,163 @@ export function describeDroppedHosts(res) {
146
190
  export function thinsHost(mode, hosts, hostId) {
147
191
  return writesThinFiles(mode) && hosts.includes(hostId);
148
192
  }
193
+ /**
194
+ * THE resolver. Every surface that asks "which delivery mode" calls this.
195
+ *
196
+ * Tolerant like everything else on a hook path: `load_agent_settings` answers
197
+ * with defaults on an unreadable or malformed file and never throws, and the
198
+ * normalisers above turn anything unrecognised into the safe value. A caller
199
+ * on a hot path pays ONE cascade read per prompt; `modeExplicit` is a lazy
200
+ * getter, so the second, template-isolated read happens only for a caller that
201
+ * actually reads provenance — which today is the installer and nothing else.
202
+ */
203
+ export function resolveLeanProjection(opts = {}) {
204
+ // The LEGACY root path, deliberately, and not `project_settings_path`'s
205
+ // either-or pick. `load_agent_settings` expands whatever it is given into
206
+ // `[the file, <its dir>/agents/settings/.agent-settings.yml, <its dir>/
207
+ // agents/settings/.agent-settings.local.yml]` and merges them deepest-wins,
208
+ // so handing it the root path reads BOTH the legacy and the canonical file
209
+ // — which is strictly more than picking one, and is what a tree carrying
210
+ // both should resolve to. It also keeps one export out of this module's
211
+ // import list, which is runtime bytes in the composed hook bundle.
212
+ const file = opts.settingsPath ?? path.join(opts.projectRoot ?? process.cwd(), '.agent-settings.yml');
213
+ const tpl = opts.packageRoot == null
214
+ ? null
215
+ : path.join(opts.packageRoot, 'src', 'config', 'agent-settings.template.yml');
216
+ let lean;
217
+ try {
218
+ lean = load_agent_settings(tpl === null ? { project_path: file } : { project_path: file, template_path: tpl })['lean_projection'];
219
+ }
220
+ catch {
221
+ /* a settings layer nothing can read must never fail a turn */
222
+ }
223
+ const obj = typeof lean === 'object' && lean !== null && !Array.isArray(lean) ? lean : {};
224
+ const o = obj;
225
+ return {
226
+ mode: normalizeLeanProjectionMode(o['mode'] ?? ''),
227
+ hosts: resolveLeanProjectionHosts(o['hosts']),
228
+ // LAZY, and the hot path is the reason. Provenance needs a SECOND,
229
+ // template-isolated cascade, and only the installer reads it — the
230
+ // delivery concern on `user_prompt_submit` destructures `{ mode, hosts }`
231
+ // and discards this field, so computing it eagerly doubled the per-prompt
232
+ // settings read for an answer nobody asked for. A getter keeps the
233
+ // field's shape (`readonly modeExplicit: boolean`) and charges only the
234
+ // caller that touches it.
235
+ get modeExplicit() {
236
+ return rawExplicitLeanProjectionMode(opts) !== '';
237
+ },
238
+ };
239
+ }
240
+ /**
241
+ * A path no tree contains, so `template_defaults` contributes nothing.
242
+ *
243
+ * This is the mechanism `load_agent_settings` documents for the purpose —
244
+ * "a test or tool that pins every other input can pin the defaults base too,
245
+ * and point it at a nonexistent file to isolate the cascade" — used here for a
246
+ * tool rather than a test. It is a reserved device filename on every platform
247
+ * this package runs on and is never created, so the read fails and the loader's
248
+ * own tolerance turns it into `{}`.
249
+ *
250
+ * The alternative — re-reading the YAML layers by hand — was rejected because
251
+ * it would be a SECOND reader of the same key, which is the precise defect step
252
+ * 1.2 of the carrier roadmap existed to remove. Isolating the base keeps one
253
+ * reader and changes only which layers it is given.
254
+ */
255
+ const NO_TEMPLATE_LAYER = path.join(path.sep, 'dev', 'null', 'agent-config-absent-template.yml');
256
+ /**
257
+ * The RAW `lean_projection.mode` string from the user-controlled layers only,
258
+ * or `''` when no such layer carries it.
259
+ *
260
+ * Raw and un-normalised on purpose: {@link normalizeLeanProjectionMode} maps
261
+ * everything unrecognised — `''` included — onto `eager-all`, so a normalised
262
+ * answer cannot tell "the user chose eager-all" from "the user chose nothing".
263
+ * Provenance needs exactly that distinction, so it has to be read before the
264
+ * normaliser runs.
265
+ */
266
+ export function rawExplicitLeanProjectionMode(opts = {}) {
267
+ const file = opts.settingsPath ?? path.join(opts.projectRoot ?? process.cwd(), '.agent-settings.yml');
268
+ let lean;
269
+ try {
270
+ lean = load_agent_settings({ project_path: file, template_path: NO_TEMPLATE_LAYER })['lean_projection'];
271
+ }
272
+ catch {
273
+ return ''; // a layer nothing can read states no preference
274
+ }
275
+ if (typeof lean !== 'object' || lean === null || Array.isArray(lean))
276
+ return '';
277
+ const m = lean['mode'];
278
+ return typeof m === 'string' ? m.trim() : '';
279
+ }
280
+ /**
281
+ * Did a human ask for this mode, and is it the one the caller is gated on?
282
+ *
283
+ * The question an installer actually has. `modeExplicit` alone is not enough —
284
+ * a consumer who explicitly set `eager-all` has set the key explicitly and must
285
+ * NOT be thinned — so the two conditions are folded here rather than left to
286
+ * every caller to remember to combine.
287
+ */
288
+ export function leanProjectionModeChosen(mode, opts = {}) {
289
+ const raw = rawExplicitLeanProjectionMode(opts);
290
+ return raw !== '' && normalizeLeanProjectionMode(raw) === mode;
291
+ }
292
+ /**
293
+ * May the INSTALLER thin this host's rule tree?
294
+ *
295
+ * The host-aware sibling of {@link leanProjectionModeChosen}, and the predicate
296
+ * `road-to-an-installed-layer-that-is-thinned` step 1.1 keys on. It folds the
297
+ * three conditions that must hold together, once, so no caller has to remember
298
+ * to combine them:
299
+ *
300
+ * 1. **A human asked.** `modeExplicit` — false whenever the only layer carrying
301
+ * the key is the shipped template, which already says `delivery`. Reading the
302
+ * template as consent would thin every consumer's `~/.claude/rules` as a side
303
+ * effect of wiring a reader, which is the owner-reserved default flip (that
304
+ * roadmap's blocker, decision D4).
305
+ * 2. **The chosen mode actually writes stubs.** A consumer who explicitly chose
306
+ * `eager-all` has set the key and must NOT be thinned on the strength of
307
+ * having an opinion.
308
+ * 3. **This host is in scope.** The same `hosts:` list the projector honours, so
309
+ * installer and projector cannot disagree about which trees are thinned.
310
+ *
311
+ * ONE `resolveLeanProjection` call, which is why this is not two
312
+ * {@link leanProjectionModeChosen} calls: that one answers "is the chosen mode
313
+ * exactly X" and knows nothing about `hosts`, so a caller combining it with a
314
+ * host check by hand would re-create the fold this function exists to own.
315
+ *
316
+ * That is one CALL and two cascade READS here, because reading `modeExplicit`
317
+ * triggers its lazy getter and a second, template-isolated load. The installer
318
+ * runs once per deploy and does not care; the cost is named rather than rounded
319
+ * down because the same resolver serves the per-prompt delivery concern, which
320
+ * does NOT read provenance and therefore does not pay it.
321
+ */
322
+ export function installerThinsHost(hostId, opts = {}) {
323
+ const { mode, hosts, modeExplicit } = resolveLeanProjection({
324
+ ...opts,
325
+ settingsPath: opts.settingsPath ?? NO_PROJECT_LAYER,
326
+ });
327
+ return modeExplicit && thinsHost(mode, hosts.hosts, hostId);
328
+ }
329
+ /**
330
+ * A path no tree contains, so the PROJECT cascade contributes nothing.
331
+ *
332
+ * The same device {@link NO_TEMPLATE_LAYER} uses, pointed at the other end of
333
+ * the cascade, and the reason is scope rather than isolation: the thing
334
+ * {@link installerThinsHost} gates is a mutation of `~/.claude/rules`, which
335
+ * belongs to the MACHINE. A per-checkout value must not decide it, in either
336
+ * direction — a project saying `delivery` would thin the layer every other
337
+ * project on that machine reads, and a project saying `eager-all` would
338
+ * suppress an opt-in the user wrote user-globally, because the project cascade
339
+ * merges last and wins.
340
+ *
341
+ * `load_agent_settings` reads `user_global_settings_paths()` regardless of what
342
+ * `project_path` it is given, so pointing the project layer at nothing leaves
343
+ * exactly the machine-global layers — which is the scope the installer's
344
+ * sibling `_resolve_global_rule_scope` already resolves for the same deploy.
345
+ * A caller may still pass an explicit `settingsPath` (tests pin one). A
346
+ * `projectRoot` handed to {@link installerThinsHost} is DISCARDED — the `??`
347
+ * chain terminates at `settingsPath` before `projectRoot` is consulted — which
348
+ * is the intended behaviour and is stated here because a caller passing it and
349
+ * expecting it honoured would otherwise get no signal.
350
+ */
351
+ const NO_PROJECT_LAYER = path.join(path.sep, 'dev', 'null', 'agent-config-absent-project-settings.yml');
149
352
  //# sourceMappingURL=lean_projection_mode.js.map