@event4u/agent-config 9.13.0 → 9.15.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 (280) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +113 -67
  3. package/README.md +9 -1
  4. package/dist/agent-src/commands/agents/audit.md +7 -7
  5. package/dist/agent-src/commands/agents/optimize.md +1 -1
  6. package/dist/agent-src/commands/analyze/reference-repo.md +1 -1
  7. package/dist/agent-src/commands/chat-history.md +1 -0
  8. package/dist/agent-src/commands/context.md +1 -0
  9. package/dist/agent-src/commands/council.md +1 -1
  10. package/dist/agent-src/commands/ghostwriter.md +1 -1
  11. package/dist/agent-src/commands/memory.md +2 -1
  12. package/dist/agent-src/commands/module.md +1 -0
  13. package/dist/agent-src/commands/optimize/agents-dir.md +1 -1
  14. package/dist/agent-src/commands/optimize/deep.md +150 -0
  15. package/dist/agent-src/commands/optimize/skills.md +5 -5
  16. package/dist/agent-src/commands/optimize.md +12 -6
  17. package/dist/agent-src/commands/override.md +1 -0
  18. package/dist/agent-src/commands/prediction-pool.md +4 -4
  19. package/dist/agent-src/commands/research/deep.md +1 -1
  20. package/dist/agent-src/commands/roadmap.md +2 -1
  21. package/dist/agent-src/commands/rule-compliance-audit.md +1 -1
  22. package/dist/agent-src/commands/tdd.md +1 -0
  23. package/dist/agent-src/commands/tests.md +1 -0
  24. package/dist/agent-src/commands/upstream-contribute.md +5 -5
  25. package/dist/agent-src/commands/video/from-script.md +1 -1
  26. package/dist/agent-src/commands/video/from-song.md +1 -1
  27. package/dist/agent-src/commands/video/scene.md +1 -1
  28. package/dist/agent-src/commands/video/stitch.md +1 -1
  29. package/dist/agent-src/commands/video/storyboard.md +1 -1
  30. package/dist/agent-src/commands/worktree/create.md +2 -1
  31. package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
  32. package/dist/agent-src/contexts/contracts/artifact-engagement-flow.md +5 -5
  33. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +3 -3
  34. package/dist/agent-src/contexts/contracts/emergency-triage-block.md +2 -2
  35. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +4 -3
  36. package/dist/agent-src/contexts/execution/subagent-routing.md +19 -0
  37. package/dist/agent-src/contexts/override-system.md +1 -1
  38. package/dist/agent-src/ghostwriter/fictional-fixture-v1.md +1 -1
  39. package/dist/agent-src/rules/augment-edit-discipline.md +1 -1
  40. package/dist/agent-src/rules/autonomous-execution.md +2 -1
  41. package/dist/agent-src/rules/brand-consistency.md +1 -0
  42. package/dist/agent-src/rules/brand-source-of-truth.md +1 -0
  43. package/dist/agent-src/rules/code-comment-discipline.md +2 -0
  44. package/dist/agent-src/rules/communication-through-line.md +1 -0
  45. package/dist/agent-src/rules/content-quoting-floor.md +1 -0
  46. package/dist/agent-src/rules/cross-source-consistency.md +0 -1
  47. package/dist/agent-src/rules/downstream-changes.md +1 -0
  48. package/dist/agent-src/rules/external-code-graph-interop.md +1 -0
  49. package/dist/agent-src/rules/external-reference-deep-dive.md +1 -0
  50. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
  51. package/dist/agent-src/rules/finance-safety-floor.md +0 -1
  52. package/dist/agent-src/rules/icon-consistency.md +1 -0
  53. package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
  54. package/dist/agent-src/rules/improve-before-implement.md +1 -1
  55. package/dist/agent-src/rules/invite-challenge.md +1 -0
  56. package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
  57. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  58. package/dist/agent-src/rules/markdown-safe-codeblocks.md +1 -0
  59. package/dist/agent-src/rules/media-governance-routing.md +4 -3
  60. package/dist/agent-src/rules/media-sync-ground-truth.md +1 -0
  61. package/dist/agent-src/rules/no-attribution-footers.md +1 -0
  62. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +1 -0
  63. package/dist/agent-src/rules/no-pr-progress-comments.md +1 -0
  64. package/dist/agent-src/rules/onboarding-gate.md +1 -0
  65. package/dist/agent-src/rules/output-discipline.md +1 -0
  66. package/dist/agent-src/rules/prefer-enums-over-literals.md +1 -0
  67. package/dist/agent-src/rules/preservation-guard.md +1 -0
  68. package/dist/agent-src/rules/question-not-instruction.md +1 -0
  69. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +1 -0
  70. package/dist/agent-src/rules/role-mode-adherence.md +1 -0
  71. package/dist/agent-src/rules/runtime-safety.md +1 -0
  72. package/dist/agent-src/rules/security-sensitive-stop.md +1 -0
  73. package/dist/agent-src/rules/session-canary.md +9 -2
  74. package/dist/agent-src/rules/source-confidentiality.md +1 -0
  75. package/dist/agent-src/rules/source-of-truth.md +1 -0
  76. package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -0
  77. package/dist/agent-src/rules/think-before-action.md +1 -0
  78. package/dist/agent-src/rules/token-budget-discipline.md +1 -0
  79. package/dist/agent-src/rules/token-efficiency.md +1 -0
  80. package/dist/agent-src/rules/token-optimizer-maintenance.md +5 -5
  81. package/dist/agent-src/rules/tool-safety.md +1 -0
  82. package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
  83. package/dist/agent-src/rules/user-interaction.md +1 -0
  84. package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
  85. package/dist/agent-src/skills/adr-create/SKILL.md +1 -1
  86. package/dist/agent-src/skills/check-refs/SKILL.md +1 -1
  87. package/dist/agent-src/skills/competitive-positioning/SKILL.md +1 -1
  88. package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
  89. package/dist/agent-src/skills/context-authoring/SKILL.md +1 -1
  90. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +1 -1
  91. package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
  92. package/dist/agent-src/skills/github-ci/SKILL.md +1 -0
  93. package/dist/agent-src/skills/guideline-writing/SKILL.md +1 -1
  94. package/dist/agent-src/skills/lint-skills/SKILL.md +1 -1
  95. package/dist/agent-src/skills/readme-reviewer/SKILL.md +1 -1
  96. package/dist/agent-src/skills/roadmap-management/SKILL.md +8 -1
  97. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  98. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  99. package/dist/agent-src/skills/skill-writing/SKILL.md +4 -4
  100. package/dist/agent-src/skills/token-optimizer/SKILL.md +4 -4
  101. package/dist/agent-src/skills/upstream-contribute/SKILL.md +6 -6
  102. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -0
  103. package/dist/agent-src/templates/AGENTS.md +1 -1
  104. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  105. package/dist/agent-src/templates/command.md +1 -1
  106. package/dist/agent-src/templates/copilot-instructions.md +3 -3
  107. package/dist/agent-src/templates/rule.md +2 -2
  108. package/dist/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
  109. package/dist/agent-src/templates/skill-archive-note.md +5 -5
  110. package/dist/agent-src/templates/skill.md +1 -1
  111. package/dist/cli/agent-config.js +66 -19
  112. package/dist/cli/agent-config.js.map +1 -1
  113. package/dist/cli/main.js +9 -4
  114. package/dist/cli/main.js.map +1 -1
  115. package/dist/cli/python/workspace_drive.js +2 -0
  116. package/dist/cli/python/workspace_drive.js.map +1 -1
  117. package/dist/cli/python/workspace_drive_health.js +2 -0
  118. package/dist/cli/python/workspace_drive_health.js.map +1 -1
  119. package/dist/cli/python/workspace_hosts.js +2 -0
  120. package/dist/cli/python/workspace_hosts.js.map +1 -1
  121. package/dist/cli/python/workspace_sessions.js +2 -0
  122. package/dist/cli/python/workspace_sessions.js.map +1 -1
  123. package/dist/cli/registry.js +1 -0
  124. package/dist/cli/registry.js.map +1 -1
  125. package/dist/cli-delegate/{chunk-A3HJ6NTI.js → chunk-63WSP6YZ.js} +2 -1
  126. package/dist/cli-delegate/{chunk-EJYIGZ3T.js → chunk-BKO2AZ7Z.js} +1 -1
  127. package/dist/cli-delegate/{chunk-QFKQLQ7X.js → chunk-H574KOUH.js} +340 -462
  128. package/dist/cli-delegate/chunk-NXFWFAYV.js +339 -0
  129. package/dist/cli-delegate/{chunk-TEGJU2JX.js → chunk-ZXXJLL5K.js} +2 -2
  130. package/dist/cli-delegate/cmd_conformance.js +29 -23
  131. package/dist/cli-delegate/cmd_doctor.js +3 -3
  132. package/dist/cli-delegate/cmd_export.js +4 -4
  133. package/dist/cli-delegate/cmd_preflight.js +2 -2
  134. package/dist/cli-delegate/cmd_prune.js +4 -4
  135. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  136. package/dist/cli-delegate/cmd_sync.js +4 -4
  137. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  138. package/dist/cli-delegate/cmd_validate.js +4 -4
  139. package/dist/discovery/deprecation-report.md +1 -1
  140. package/dist/discovery/discovery-manifest.json +163 -103
  141. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  142. package/dist/discovery/discovery-manifest.summary.md +3 -3
  143. package/dist/discovery/orphan-report.md +1 -1
  144. package/dist/discovery/packs.json +6 -5
  145. package/dist/discovery/trust-report.md +3 -3
  146. package/dist/discovery/workspaces.json +5 -4
  147. package/dist/hooks/dispatch.js +729 -430
  148. package/dist/install/install.mjs +700 -625
  149. package/dist/install/rule_scope.js +18 -2
  150. package/dist/install/rule_scope.js.map +1 -1
  151. package/dist/mcp/registry-manifest.json +3 -3
  152. package/dist/mcp/server.json +2 -2
  153. package/dist/mcp/server.mjs +564 -424
  154. package/dist/router.json +1 -1
  155. package/dist/scripts/_lib/claude_settings_hooks.js +30 -5
  156. package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
  157. package/dist/scripts/_lib/scoped_projection.js +223 -0
  158. package/dist/scripts/_lib/scoped_projection.js.map +1 -0
  159. package/dist/scripts/_lib/surface_tiers.js +125 -0
  160. package/dist/scripts/_lib/surface_tiers.js.map +1 -0
  161. package/dist/scripts/_lib/token_count.js +166 -0
  162. package/dist/scripts/_lib/token_count.js.map +1 -0
  163. package/dist/scripts/_lib/trigger_eval_floors.js +8 -0
  164. package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -1
  165. package/dist/scripts/condense.js +139 -9
  166. package/dist/scripts/condense.js.map +1 -1
  167. package/dist/scripts/project_thin_rules.js +450 -0
  168. package/dist/scripts/project_thin_rules.js.map +1 -0
  169. package/dist/server/routes/workspace.js +2 -0
  170. package/dist/server/routes/workspace.js.map +1 -1
  171. package/dist/server/schemas/settings.js +9 -3
  172. package/dist/server/schemas/settings.js.map +1 -1
  173. package/dist/ui/assets/index-DJY_vQVn.js +40 -0
  174. package/dist/ui/assets/index-DJY_vQVn.js.map +1 -0
  175. package/dist/ui/index.html +1 -1
  176. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  177. package/package.json +11 -11
  178. package/src/agent-src/templates/AGENTS.md +1 -1
  179. package/src/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
  180. package/src/config/agent-settings.template.yml +59 -11
  181. package/src/config/ci-local-parity.yml +8 -0
  182. package/src/config/discovery/unassigned-artefacts.yml +23 -25
  183. package/src/config/evaluator-budgets.json +35 -5
  184. package/src/config/gate-coverage.yml +147 -0
  185. package/src/config/gate-violation-baselines.json +25 -0
  186. package/src/install/rule_scope.ts +27 -2
  187. package/src/scripts/_cli/cmd_conformance.ts +13 -24
  188. package/src/scripts/_dispatch.bash +10 -0
  189. package/src/scripts/_lib/claude_settings_hooks.ts +30 -4
  190. package/src/scripts/_lib/exec_evidence.ts +5 -0
  191. package/src/scripts/_lib/gate_baseline.ts +213 -0
  192. package/src/scripts/_lib/scoped_projection.ts +262 -0
  193. package/src/scripts/_lib/tier_budget_routing.ts +234 -0
  194. package/src/scripts/_lib/tool_adapter_registry.ts +8 -1
  195. package/src/scripts/_lib/trigger_eval_floors.ts +9 -0
  196. package/src/scripts/_lib/trigger_routers.ts +17 -5
  197. package/src/scripts/adr/regenerate_index.ts +4 -3
  198. package/src/scripts/ai-video/lib/parse-blueprint.sh +1 -1
  199. package/src/scripts/ai_council/compile_corpus.ts +1 -1
  200. package/src/scripts/audit_cloud_compatibility.ts +37 -2
  201. package/src/scripts/audit_initial_context.ts +53 -19
  202. package/src/scripts/audit_likelihood.ts +31 -2
  203. package/src/scripts/audit_user_type_axis.ts +58 -11
  204. package/src/scripts/bench_hook_latency.ts +172 -41
  205. package/src/scripts/build_mcp_catalog.ts +12 -1
  206. package/src/scripts/check_always_budget.ts +122 -11
  207. package/src/scripts/check_artefact_count_messaging.ts +128 -15
  208. package/src/scripts/check_augment_description_cap.ts +24 -3
  209. package/src/scripts/check_ci_strict_superset.ts +150 -0
  210. package/src/scripts/check_cluster_patterns.ts +100 -0
  211. package/src/scripts/check_consumer_scope_flip.ts +26 -4
  212. package/src/scripts/check_evaluator_budgets.ts +198 -19
  213. package/src/scripts/check_gate_coverage.ts +337 -1
  214. package/src/scripts/check_generator_output_coverage.ts +2 -1
  215. package/src/scripts/check_iron_law_prominence.ts +1 -1
  216. package/src/scripts/check_kernel_rule_bundle.ts +1 -1
  217. package/src/scripts/check_no_new_legacy_path.ts +167 -3
  218. package/src/scripts/check_pack_size.ts +18 -4
  219. package/src/scripts/check_portability.ts +37 -7
  220. package/src/scripts/check_references.ts +19 -3
  221. package/src/scripts/check_release_pr_shape.ts +13 -0
  222. package/src/scripts/check_token_regression.ts +9 -2
  223. package/src/scripts/condense.ts +155 -12
  224. package/src/scripts/cost/budget.mjs +63 -2
  225. package/src/scripts/count_scoped_projection.ts +104 -0
  226. package/src/scripts/evaluator_umbrella.sh +34 -1
  227. package/src/scripts/generate_capability_matrix.ts +8 -0
  228. package/src/scripts/generate_router_coverage_from_matrix.ts +155 -0
  229. package/src/scripts/hook_manifest.yaml +17 -3
  230. package/src/scripts/hooks/block_config_weakening.ts +337 -0
  231. package/src/scripts/hooks/concern_registry.ts +2 -0
  232. package/src/scripts/hooks/probe_throttle.ts +75 -0
  233. package/src/scripts/install-hooks.sh +24 -2
  234. package/src/scripts/install.ts +15 -92
  235. package/src/scripts/lint_agents_md.ts +9 -3
  236. package/src/scripts/lint_archived_skills.ts +1 -1
  237. package/src/scripts/lint_artefact_frontmatter.ts +185 -37
  238. package/src/scripts/lint_command_routing.ts +21 -52
  239. package/src/scripts/lint_command_verbs.ts +47 -28
  240. package/src/scripts/lint_context_spine_usage.ts +38 -12
  241. package/src/scripts/lint_framework_leakage.ts +25 -6
  242. package/src/scripts/lint_handoffs.ts +49 -2
  243. package/src/scripts/lint_media_policy_linkage.ts +62 -30
  244. package/src/scripts/lint_namespace.ts +92 -7
  245. package/src/scripts/lint_new_skill_gate.ts +39 -31
  246. package/src/scripts/lint_originality_shingles.ts +7 -1
  247. package/src/scripts/lint_override_kernel_guard.ts +21 -3
  248. package/src/scripts/lint_pack_boundaries.ts +461 -91
  249. package/src/scripts/lint_pack_dependencies.ts +24 -57
  250. package/src/scripts/lint_persona_governance.ts +1 -1
  251. package/src/scripts/lint_role_experiences.ts +34 -5
  252. package/src/scripts/lint_trigger_precision.ts +142 -0
  253. package/src/scripts/mcp_server/consumer_tool_catalog.json +12 -201
  254. package/src/scripts/mcp_server/tools.ts +1 -1
  255. package/src/scripts/prepack-check.mjs +40 -1
  256. package/src/scripts/prepack_router_targets.d.mts +7 -0
  257. package/src/scripts/prepack_router_targets.mjs +93 -0
  258. package/src/scripts/print_required_checks.ts +72 -18
  259. package/src/scripts/profile_staleness_hook.ts +12 -0
  260. package/src/scripts/record_evaluator_measurements.mjs +105 -0
  261. package/src/scripts/recruit_preflight.sh +1 -1
  262. package/src/scripts/release.ts +105 -1
  263. package/src/scripts/router_target_paths.d.mts +4 -0
  264. package/src/scripts/router_target_paths.mjs +69 -0
  265. package/src/scripts/router_telemetry.ts +52 -1
  266. package/src/scripts/routing_doctor.ts +485 -0
  267. package/src/scripts/rule_trigger_eval.ts +403 -0
  268. package/src/scripts/schemas/rule.schema.json +5 -1
  269. package/src/scripts/session_canary_hook.ts +96 -5
  270. package/src/scripts/skill_linter.ts +90 -42
  271. package/src/scripts/smoke/skills.sh +1 -1
  272. package/src/scripts/sweep_dead_scan_roots.ts +356 -10
  273. package/src/scripts/trigger_coverage.ts +13 -2
  274. package/src/scripts/update_counts.ts +91 -12
  275. package/src/scripts/validate_telegraph_carveouts.ts +1 -1
  276. package/src/scripts/wrapper_freshness_hook.ts +14 -0
  277. package/src/server/schemas/settings.ts +19 -5
  278. package/dist/cli-delegate/chunk-TJIAM5FW.js +0 -133
  279. package/dist/ui/assets/index-BpSQt5vm.js +0 -40
  280. package/dist/ui/assets/index-BpSQt5vm.js.map +0 -1
@@ -91,6 +91,20 @@ concerns:
91
91
  script: src/scripts/hooks/block_kernel_rule_writes.ts
92
92
  args: []
93
93
  fail_closed: true
94
+ # road-to-renewal-leverage Phase 2 — "fix the code, not the config".
95
+ # PreToolUse guard over the recorded allowlist-growth antipattern
96
+ # (autonomous-execution § Antipattern): warns as an allowlist grows within a
97
+ # session and BLOCKS past the cap that rule already states in words (>20
98
+ # entries in one session = the linter is wrong). Baselines and budget
99
+ # thresholds warn only — whether a rising count is weakening or a legitimate
100
+ # ratchet reset is not decidable from the edit alone. Council 2026-08-02
101
+ # rejected inventing a "fix-loop is active" predicate; the SESSION is the
102
+ # window the recorded rule already names. fail_closed: false — a malformed
103
+ # envelope must never block an unrelated edit.
104
+ block-config-weakening:
105
+ script: src/scripts/hooks/block_config_weakening.ts
106
+ args: []
107
+ fail_closed: false
94
108
  # token-saving Phase 3 — PreToolUse RTK-wrap nudge. DETERMINISTIC: keys off a
95
109
  # live `which rtk` probe (not the self-reported flag). When rtk is installed
96
110
  # and the agent is about to run a single verbose CLI command that is NOT
@@ -211,7 +225,7 @@ platforms:
211
225
  session_start: [chat-history, hot-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
212
226
  session_end: [chat-history, memory-learn]
213
227
  stop: [chat-history, hot-context, verify-before-complete]
214
- pre_tool_use: [block-no-verify, block-kernel-rule-writes, rtk-wrap, design-slop, code-graph-nudge]
228
+ pre_tool_use: [block-no-verify, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
215
229
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
216
230
 
217
231
  claude:
@@ -219,7 +233,7 @@ platforms:
219
233
  session_end: [chat-history, memory-learn]
220
234
  stop: [chat-history, hot-context, verify-before-complete, team-review-gate]
221
235
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
222
- pre_tool_use: [block-no-verify, block-kernel-rule-writes, rtk-wrap, design-slop, code-graph-nudge]
236
+ pre_tool_use: [block-no-verify, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
223
237
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
224
238
 
225
239
  # Cowork — the Claude desktop app's local-agent-mode runtime, built
@@ -241,7 +255,7 @@ platforms:
241
255
  session_end: [chat-history, memory-learn]
242
256
  stop: [chat-history, hot-context, verify-before-complete]
243
257
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
244
- pre_tool_use: [block-no-verify, block-kernel-rule-writes, rtk-wrap, design-slop, code-graph-nudge]
258
+ pre_tool_use: [block-no-verify, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
245
259
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
246
260
 
247
261
  # Phase 7.5 — Cursor. `.cursor/hooks.json` (project) is read by the
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+ // MIGRATE: precompiled-hook-layer — carry this hook when road-to-credible-install
3
+ // Phase 1 precompiles the hook path (touch-once preserved at migration).
4
+ /**
5
+ * PreToolUse guard: "fix the code, not the config."
6
+ *
7
+ * The recorded antipattern (`autonomous-execution` § Antipattern —
8
+ * allowlist-growth as silent budget bypass): during a fix loop the agent
9
+ * weakens the gate instead of the code, growing a lint allowlist entry by
10
+ * entry until the linter no longer objects. The rule already states the
11
+ * threshold in words —
12
+ *
13
+ * ALLOWLIST > 20 ENTRIES IN ONE SESSION = THE LINTER IS WRONG.
14
+ * STOP. PROPOSE LINTER REDESIGN OR REMOVAL.
15
+ *
16
+ * — and two linters cite it in their own failure messages. Nothing enforced
17
+ * it: by the time CI sees the diff the session has already spent its budget
18
+ * on the wrong fix. This hook moves that stated threshold to tool-call time.
19
+ *
20
+ * Deliberately NOT built (council 2026-08-02, anthropic/claude-sonnet-4-5 +
21
+ * openai/gpt-4o):
22
+ *
23
+ * - **No invented "fix-loop is active" predicate.** Both members rejected it
24
+ * as speculative — no such state exists in the repo and defining entry/exit
25
+ * invites false positives in both directions. The SESSION is the window the
26
+ * recorded rule already names, so the session id off the hook envelope is
27
+ * the predicate, and it is not an invention.
28
+ * - **No blocking on baselines or budgets.** Whether a violation-baseline
29
+ * count rising is weakening or a legitimate ratchet reset after a refactor
30
+ * is not mechanically decidable from the edit alone. Those surfaces warn
31
+ * (exit 2) and stay a review decision; only allowlist growth — mechanical,
32
+ * countable, and already carrying a stated numeric threshold — blocks.
33
+ *
34
+ * Cumulative within a session, per allowlist file. Small additions warn from
35
+ * the first one so the count is visible while it is still cheap to change
36
+ * course; crossing the recorded cap blocks.
37
+ *
38
+ * Exit codes (docs/contracts/hook-architecture-v1.md):
39
+ * 0 — allow
40
+ * 1 — block
41
+ * 2 — warn
42
+ */
43
+
44
+ import * as fs from 'node:fs';
45
+ import * as path from 'node:path';
46
+ import { fileURLToPath, pathToFileURL } from 'node:url';
47
+
48
+ import { EDIT_TOOLS } from '../minimal_safe_diff_hook.js';
49
+ import { readHookStdin } from './hook_stdin.js';
50
+
51
+ const _HERE = fileURLToPath(import.meta.url);
52
+
53
+ const EXIT_ALLOW = 0;
54
+ const EXIT_BLOCK = 1;
55
+ const EXIT_WARN = 2;
56
+
57
+ /**
58
+ * The stated cap from `autonomous-execution` § Antipattern. Crossing it in one
59
+ * session is defined there as the third validation-target failure — the point
60
+ * at which the linter, not the content, is wrong.
61
+ */
62
+ export const SESSION_ENTRY_CAP = 20;
63
+
64
+ /** Warn from this many cumulative added entries so the count stays visible. */
65
+ export const SESSION_WARN_AT = 5;
66
+
67
+ type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
68
+ type JsonObject = { [k: string]: JsonValue };
69
+
70
+ function _isObject(v: JsonValue | undefined): v is JsonObject {
71
+ return v !== null && v !== undefined && typeof v === 'object' && !Array.isArray(v);
72
+ }
73
+
74
+ const _PATH_KEYS: readonly string[] = ['file_path', 'path', 'target_file', 'filePath', 'notebook_path'];
75
+
76
+ /**
77
+ * Config surfaces this guard recognises.
78
+ *
79
+ * `allowlist` is the blocking class — an entry count is a plain number and the
80
+ * recorded rule already fixes its threshold. `advisory` covers the surfaces
81
+ * where "weakening" needs context a tool call does not carry (a baseline count
82
+ * may legitimately reset after a refactor); those warn and never block.
83
+ */
84
+ export type ConfigKind = 'allowlist' | 'advisory' | null;
85
+
86
+ /** Classify a target path into a config surface, or null for everything else. */
87
+ export function classify_target(p: string): ConfigKind {
88
+ const posix = p.split(path.sep).join('/');
89
+ const base = posix.split('/').pop() ?? '';
90
+ if (/_allowlist\.(json|txt)$/.test(base) || /^allowlist[-_.]/.test(base)) {
91
+ return 'allowlist';
92
+ }
93
+ if (/(^|-)baselines?\.json$/.test(base)) {
94
+ return 'advisory';
95
+ }
96
+ if (/-budget(s)?\.(json|ya?ml)$/.test(base) || base === 'budgets.yml') {
97
+ return 'advisory';
98
+ }
99
+ return null;
100
+ }
101
+
102
+ /**
103
+ * Count allowlist entries in a blob.
104
+ *
105
+ * JSON: every string leaf in the structure (arrays of paths, and
106
+ * `{"rule": ["path", …]}` maps alike) — parsing beats a line count because a
107
+ * reformat would otherwise read as growth. Falls back to non-blank,
108
+ * non-comment lines for `.txt` allowlists and for JSON fragments that do not
109
+ * parse on their own (an `Edit` payload is rarely a whole document).
110
+ */
111
+ export function count_entries(text: string): number {
112
+ const trimmed = text.trim();
113
+ if (trimmed) {
114
+ try {
115
+ let n = 0;
116
+ const walk = (v: unknown): void => {
117
+ if (typeof v === 'string') {
118
+ n += 1;
119
+ } else if (Array.isArray(v)) {
120
+ v.forEach(walk);
121
+ } else if (v !== null && typeof v === 'object') {
122
+ Object.values(v as Record<string, unknown>).forEach(walk);
123
+ }
124
+ };
125
+ walk(JSON.parse(trimmed));
126
+ return n;
127
+ } catch {
128
+ /* not a standalone JSON document — fall through */
129
+ }
130
+ }
131
+ return text
132
+ .split(/\r?\n/)
133
+ .map((l) => l.trim())
134
+ .filter((l) => l.length > 0 && !l.startsWith('#') && !l.startsWith('//')).length;
135
+ }
136
+
137
+ /**
138
+ * Entries this single tool call adds to an allowlist.
139
+ *
140
+ * `Write` carries the whole new document, so the delta is measured against
141
+ * what is on disk. `Edit` carries a replacement pair, so the delta is the pair
142
+ * itself. Negative results (a shrinking allowlist — the direction this guard
143
+ * wants) collapse to 0: removals must never bank credit against a later
144
+ * addition.
145
+ */
146
+ export function added_entries(
147
+ tool_input: JsonObject,
148
+ on_disk: string | null,
149
+ ): number {
150
+ const content = tool_input['content'];
151
+ if (typeof content === 'string') {
152
+ const before = on_disk === null ? 0 : count_entries(on_disk);
153
+ return Math.max(0, count_entries(content) - before);
154
+ }
155
+ const oldS = tool_input['old_string'] ?? tool_input['oldStr'] ?? tool_input['old_str'];
156
+ const newS = tool_input['new_string'] ?? tool_input['newStr'] ?? tool_input['new_str'];
157
+ if (typeof newS === 'string') {
158
+ const before = typeof oldS === 'string' ? count_entries(oldS) : 0;
159
+ return Math.max(0, count_entries(newS) - before);
160
+ }
161
+ return 0;
162
+ }
163
+
164
+ export interface Decision {
165
+ action: 'allow' | 'warn' | 'block';
166
+ reason: string;
167
+ }
168
+
169
+ /** Decide from the cumulative session total for one allowlist file. */
170
+ export function decide(kind: ConfigKind, rel_path: string, session_total: number, added: number): Decision {
171
+ if (kind === null || added <= 0) {
172
+ return { action: 'allow', reason: '' };
173
+ }
174
+ if (kind === 'advisory') {
175
+ return {
176
+ action: 'warn',
177
+ reason:
178
+ `${rel_path}: this edit loosens a gate threshold. Whether that is a ` +
179
+ 'legitimate ratchet reset or the config being bent around a failing ' +
180
+ 'check is a review decision — state which in the diff.',
181
+ };
182
+ }
183
+ if (session_total > SESSION_ENTRY_CAP) {
184
+ return {
185
+ action: 'block',
186
+ reason:
187
+ `${rel_path}: ${session_total} allowlist entries added this session ` +
188
+ `(cap ${SESSION_ENTRY_CAP}). Per autonomous-execution § Antipattern — ` +
189
+ 'allowlist-growth as silent budget bypass, crossing this cap means the ' +
190
+ 'LINTER is wrong, not the content. Fix the code or redesign the check; ' +
191
+ 'do not expand the allowlist further.\n' +
192
+ ' Legitimate override is a human action outside this session: land the ' +
193
+ 'linter redesign, or remove the `block-config-weakening` entry in ' +
194
+ 'src/scripts/hook_manifest.yaml.',
195
+ };
196
+ }
197
+ if (session_total >= SESSION_WARN_AT) {
198
+ return {
199
+ action: 'warn',
200
+ reason:
201
+ `${rel_path}: ${session_total} allowlist entries added this session ` +
202
+ `(cap ${SESSION_ENTRY_CAP}). Fix the code, not the config.`,
203
+ };
204
+ }
205
+ return { action: 'allow', reason: '' };
206
+ }
207
+
208
+ // --- session state -------------------------------------------------------
209
+
210
+ function _state_file(root: string): string {
211
+ return path.join(root, 'agents', 'runtime', 'state', 'config-weakening.json');
212
+ }
213
+
214
+ /** Read the cumulative count, add `added`, persist, and return the new total. */
215
+ export function bump_session(root: string, session: string, key: string, added: number): number {
216
+ const f = _state_file(root);
217
+ let data: Record<string, Record<string, number>> = {};
218
+ try {
219
+ data = JSON.parse(fs.readFileSync(f, 'utf-8')) as Record<string, Record<string, number>>;
220
+ } catch {
221
+ data = {};
222
+ }
223
+ const bucket = data[session] ?? {};
224
+ const total = (bucket[key] ?? 0) + added;
225
+ bucket[key] = total;
226
+ data[session] = bucket;
227
+ try {
228
+ fs.mkdirSync(path.dirname(f), { recursive: true });
229
+ fs.writeFileSync(f, `${JSON.stringify(data, null, 2)}\n`, 'utf-8');
230
+ } catch {
231
+ /* state is an optimisation, never a gate — a read-only tree still decides */
232
+ }
233
+ return total;
234
+ }
235
+
236
+ // --- entry ---------------------------------------------------------------
237
+
238
+ function _extract(envelope: JsonObject): { tool: string; paths: string[]; ti: JsonObject | null } {
239
+ const payload = _isObject(envelope['payload']) ? envelope['payload'] : envelope;
240
+ const nameVal =
241
+ payload['tool_name'] ?? payload['toolName'] ?? payload['tool'] ?? envelope['tool_name'] ?? envelope['tool'];
242
+ const tool = typeof nameVal === 'string' ? nameVal : '';
243
+ const ti = _isObject(payload['tool_input'])
244
+ ? payload['tool_input']
245
+ : _isObject(envelope['tool_input'])
246
+ ? envelope['tool_input']
247
+ : null;
248
+ const paths: string[] = [];
249
+ if (ti !== null) {
250
+ for (const key of _PATH_KEYS) {
251
+ const v = ti[key];
252
+ if (typeof v === 'string' && v) {
253
+ paths.push(v);
254
+ }
255
+ }
256
+ }
257
+ return { tool, paths, ti };
258
+ }
259
+
260
+ export function main(): number {
261
+ let envelope: JsonValue;
262
+ try {
263
+ const raw = readHookStdin();
264
+ envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
265
+ } catch {
266
+ return EXIT_ALLOW;
267
+ }
268
+ if (!_isObject(envelope)) {
269
+ return EXIT_ALLOW;
270
+ }
271
+
272
+ const { tool, paths, ti } = _extract(envelope);
273
+ if (!tool || !EDIT_TOOLS.has(tool) || ti === null) {
274
+ return EXIT_ALLOW;
275
+ }
276
+
277
+ const cwd = envelope['cwd'];
278
+ const pr = envelope['workspace_root'] ?? envelope['project_root'];
279
+ const root = typeof cwd === 'string' && cwd ? cwd : typeof pr === 'string' && pr ? pr : '.';
280
+ const sidRaw = envelope['session_id'] ?? envelope['sessionId'];
281
+ const session = typeof sidRaw === 'string' && sidRaw ? sidRaw : 'default';
282
+
283
+ for (const p of paths) {
284
+ const kind = classify_target(p);
285
+ if (kind === null) {
286
+ continue;
287
+ }
288
+ const abs = path.isAbsolute(p) ? p : path.join(root, p);
289
+ let on_disk: string | null = null;
290
+ try {
291
+ on_disk = fs.readFileSync(abs, 'utf-8');
292
+ } catch {
293
+ on_disk = null;
294
+ }
295
+ const added = added_entries(ti, on_disk);
296
+ if (added <= 0) {
297
+ continue;
298
+ }
299
+ const rel = path.relative(root, abs) || p;
300
+ const total = kind === 'allowlist' ? bump_session(root, session, rel, added) : added;
301
+ const d = decide(kind, rel, total, added);
302
+ if (d.action === 'block') {
303
+ process.stderr.write(`block-config-weakening: BLOCKED — ${d.reason}\n`);
304
+ return EXIT_BLOCK;
305
+ }
306
+ if (d.action === 'warn') {
307
+ process.stdout.write(`${JSON.stringify({ decision: 'warn', reason: d.reason })}\n`);
308
+ return EXIT_WARN;
309
+ }
310
+ }
311
+ return EXIT_ALLOW;
312
+ }
313
+
314
+ // Bundle-safety: never auto-run when inlined into an esbuild bundle, where
315
+ // every module shares the bundle's `import.meta.url`.
316
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
317
+ function _isCliEntry(): boolean {
318
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
319
+ return false;
320
+ }
321
+ if (process.argv[1] === undefined) {
322
+ return false;
323
+ }
324
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
325
+ if (import.meta.url === argvUrl) {
326
+ return true;
327
+ }
328
+ try {
329
+ return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
330
+ } catch {
331
+ return false;
332
+ }
333
+ }
334
+
335
+ if (_isCliEntry() || process.argv[1] === _HERE) {
336
+ process.exit(main());
337
+ }
@@ -42,6 +42,7 @@ import { main as prUrlReminderMain } from '../pr_url_reminder_hook.js';
42
42
  import { main as sessionCanaryMain } from '../session_canary_hook.js';
43
43
  import { main as blockNoVerifyMain } from './block_no_verify.js';
44
44
  import { main as blockKernelRuleWritesMain } from './block_kernel_rule_writes.js';
45
+ import { main as blockConfigWeakeningMain } from './block_config_weakening.js';
45
46
  import { main as rtkWrapMain } from './rtk_wrap_hook.js';
46
47
  import { main as designSlopMain } from './design_slop_hook.js';
47
48
  import { main as codeGraphNudgeMain } from './code_graph_nudge_hook.js';
@@ -69,6 +70,7 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
69
70
  'src/scripts/session_canary_hook.ts': sessionCanaryMain as ConcernMain,
70
71
  'src/scripts/hooks/block_no_verify.ts': blockNoVerifyMain as ConcernMain,
71
72
  'src/scripts/hooks/block_kernel_rule_writes.ts': blockKernelRuleWritesMain as ConcernMain,
73
+ 'src/scripts/hooks/block_config_weakening.ts': blockConfigWeakeningMain as ConcernMain,
72
74
  'src/scripts/hooks/rtk_wrap_hook.ts': rtkWrapMain as ConcernMain,
73
75
  'src/scripts/hooks/design_slop_hook.ts': designSlopMain as ConcernMain,
74
76
  'src/scripts/hooks/code_graph_nudge_hook.ts': codeGraphNudgeMain as ConcernMain,
@@ -0,0 +1,75 @@
1
+ /**
2
+ * probe_throttle — daily throttle for session_start probe concerns
3
+ * (road-to-hook-latency-repair Phase 2, SessionStart fan-out lever).
4
+ *
5
+ * `profile-staleness`, `wrapper-freshness`, and `surface-probe` are
6
+ * defense-in-depth probes whose findings do not change within a day; running
7
+ * them on EVERY session_start pays their cost per session for no new
8
+ * information. Each probe checks a per-concern stamp file under
9
+ * `agents/runtime/state/probe-throttle/` and skips when the stamp's mtime is
10
+ * from the same LOCAL calendar day (a skipped probe re-runs tomorrow).
11
+ *
12
+ * Deliberate simplicity (roadmap lock — "an mtime file, not a daemon"):
13
+ * - The stamp carries NO semantics beyond its mtime. Deleting the file or
14
+ * the directory only changes WHEN a probe re-runs, never what it answers.
15
+ * - Same-day equality is skew-safe: a stamp dated in the future or the past
16
+ * is simply "not today", so the probe runs and re-stamps — there is no
17
+ * lockout state (council 2026-08-03).
18
+ * - Fail-open both ways: an unreadable stamp means "run the probe"; an
19
+ * unwritable state dir means the probe simply runs again next session.
20
+ * - Replay runs (AGENT_CONFIG_REPLAY=1) bypass the throttle and never
21
+ * write stamps — fixture re-execution and the latency bench must be
22
+ * deterministic, not dependent on yesterday's stamps (state_io replay
23
+ * contract).
24
+ */
25
+
26
+ import fs from 'node:fs';
27
+ import path from 'node:path';
28
+
29
+ import { is_replay_mode } from './state_io.js';
30
+
31
+ const STATE_SUBDIR = ['agents', 'runtime', 'state', 'probe-throttle'] as const;
32
+
33
+ function _stampPath(projectRoot: string, concern: string): string {
34
+ return path.join(projectRoot, ...STATE_SUBDIR, `${concern}.stamp`);
35
+ }
36
+
37
+ /** Local-calendar-day key (YYYY-MM-DD in local time, not UTC). */
38
+ function _localDay(d: Date): string {
39
+ const y = d.getFullYear();
40
+ const m = String(d.getMonth() + 1).padStart(2, '0');
41
+ const day = String(d.getDate()).padStart(2, '0');
42
+ return `${y}-${m}-${day}`;
43
+ }
44
+
45
+ /**
46
+ * True when the probe already ran today (same LOCAL calendar day as the
47
+ * stamp's mtime) and may be skipped. Replay mode and any read problem
48
+ * return false — the probe runs.
49
+ */
50
+ export function probeRanToday(projectRoot: string, concern: string): boolean {
51
+ if (is_replay_mode()) return false;
52
+ try {
53
+ const st = fs.statSync(_stampPath(projectRoot, concern));
54
+ return _localDay(st.mtime) === _localDay(new Date());
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Record that the probe ran now. Fail-silent — an unwritable state dir just
62
+ * means the probe runs again next session. No-op in replay mode.
63
+ */
64
+ export function stampProbeRun(projectRoot: string, concern: string): void {
65
+ if (is_replay_mode()) return;
66
+ try {
67
+ const p = _stampPath(projectRoot, concern);
68
+ fs.mkdirSync(path.dirname(p), { recursive: true });
69
+ // Content is a human-readable timestamp for debugging only — the
70
+ // mtime is the sole semantic carrier.
71
+ fs.writeFileSync(p, `${new Date().toISOString()}\n`);
72
+ } catch {
73
+ /* fail-silent */
74
+ }
75
+ }
@@ -266,10 +266,20 @@ write_chat_history_hook "post-rewrite" "git:post-rewrite"
266
266
  # `.augment/`, etc. projections until they remember to run `task sync`.
267
267
  # These hooks bridge that gap: fast idempotent re-projection.
268
268
  #
269
+ # The same staleness bites the compiled CLI: the Claude/agent hooks run
270
+ # `./agent-config` → `dist/cli/` (gitignored tsc output). A pull that adds
271
+ # or moves a TS module leaves `dist/` stale and EVERY subsequent tool-use
272
+ # hook fails with ERR_MODULE_NOT_FOUND until someone rebuilds. So when the
273
+ # range touches TS sources, rebuild via `npm run build:cli` — tsc only,
274
+ # deliberately NOT the full `npm run build`: the install-bundle step
275
+ # rewrites the committed `dist/install/install.mjs` and has produced
276
+ # spurious tracked diffs from worktrees before.
277
+ #
269
278
  # Bypass: `git pull --no-verify` does not exist, but devs can disable the
270
279
  # hooks per-command via `git -c core.hooksPath=/dev/null ...` or by
271
280
  # editing the file. Runtime ~200 ms when nothing relevant changed
272
- # (path-diff check exits early); ~2 s on full re-projection.
281
+ # (path-diff check exits early); ~2 s on full re-projection; ~2 s extra
282
+ # for the tsc rebuild when TS sources changed.
273
283
 
274
284
  append_auto_sync_block() {
275
285
  local name="$1"
@@ -294,13 +304,25 @@ elif [ "$name" = "post-checkout" ]; then
294
304
  fi
295
305
 
296
306
  if [ -n "\$prev" ] && [ -n "\$new" ] && [ "\$prev" != "\$new" ]; then
297
- if git diff --name-only "\$prev" "\$new" 2>/dev/null | \\
307
+ changed="\$(git diff --name-only "\$prev" "\$new" 2>/dev/null)"
308
+ if printf '%s\n' "\$changed" | \\
298
309
  grep -qE '^(dist/agent-src/|\\.agent-src\\.uncondensed/|src/scripts/condense\\.ts|\\.agent-tools\\.yml|Taskfile\\.yml)'; then
299
310
  if command -v task >/dev/null 2>&1; then
300
311
  task sync >/dev/null 2>&1 || true
301
312
  task generate-tools >/dev/null 2>&1 || true
302
313
  fi
303
314
  fi
315
+ # TS sources changed → the gitignored tsc output (dist/cli, dist/scripts,
316
+ # …) is stale and the ./agent-config hook chain breaks with
317
+ # ERR_MODULE_NOT_FOUND. Rebuild tsc output only (build:cli), never the
318
+ # full build (install-bundle rewrites committed dist/install/install.mjs).
319
+ # Skip silently without node_modules (fresh clone / bare worktree).
320
+ if printf '%s\n' "\$changed" | \\
321
+ grep -qE '^(src/(cli|server|shared|install|scripts)/|tsconfig\\.json|package\\.json)'; then
322
+ if command -v npm >/dev/null 2>&1 && [ -d node_modules ]; then
323
+ npm run build:cli >/dev/null 2>&1 || true
324
+ fi
325
+ fi
304
326
  fi
305
327
  EOF
306
328
  }
@@ -74,6 +74,7 @@ import type * as YamlModule from 'yaml';
74
74
  import { build_merge_entries } from './_lib/json_pointers.js';
75
75
  import { is_claude_builtin_name } from './_lib/claude_builtin_names.js';
76
76
  import * as installed_lock from './_lib/installed_lock.js';
77
+ import * as scoped_projection from './_lib/scoped_projection.js';
77
78
  import * as surface_tiers from './_lib/surface_tiers.js';
78
79
  import * as global_deploy_inventory from './_lib/global_deploy_inventory.js';
79
80
  import * as installed_tools from './_lib/installed_tools.js';
@@ -3344,89 +3345,16 @@ function _prune_lab_modules(
3344
3345
  // `requires` graph). Mirrors the core-only lab prune above — same
3345
3346
  // `_prune_modules_by` mechanics, a different predicate.
3346
3347
 
3347
- /** `workspaces` values whose packs are ALWAYS active under `scoped` mode. */
3348
- const SCOPED_ACTIVE_WORKSPACES: ReadonlySet<string> = new Set(['engineering', 'agent-config-maintainer']);
3349
-
3350
- interface PackRecord {
3351
- id: string;
3352
- workspaces: string[];
3353
- requires: string[];
3354
- }
3355
-
3356
- /**
3357
- * Parse `src/config/discovery/packs.yml` into `{id, workspaces, requires}`
3358
- * records. `requires` falls back to the legacy `requires_hint` field name
3359
- * when `requires` is absent.
3360
- *
3361
- * Deliberately THROWS on a missing file or a parse result that is not a
3362
- * list — an empty active-pack set would be the LEAST safe fallback here
3363
- * (it would prune every tagged artefact, the opposite of the fail-safe
3364
- * contract). The caller wraps this in a try/catch that restores the full
3365
- * tree on any error instead.
3366
- */
3367
- function _load_packs_registry(package_root: string): PackRecord[] {
3368
- const vocab_path = path.join(package_root, 'src', 'config', 'discovery', 'packs.yml');
3369
- const data = yamlSafeLoad(readText(vocab_path));
3370
- if (!Array.isArray(data)) {
3371
- throw new Error(`packs.yml did not parse to a list: ${vocab_path}`);
3372
- }
3373
- const out: PackRecord[] = [];
3374
- for (const entry of data) {
3375
- if (!_isPlainObject(entry)) continue;
3376
- const id = entry['id'];
3377
- if (typeof id !== 'string' || id === '') continue;
3378
- const workspaces_raw = entry['workspaces'];
3379
- const workspaces = Array.isArray(workspaces_raw)
3380
- ? workspaces_raw.filter((w): w is string => typeof w === 'string')
3381
- : [];
3382
- const requires_raw = entry['requires'] ?? entry['requires_hint'];
3383
- const requires = Array.isArray(requires_raw)
3384
- ? requires_raw.filter((r): r is string => typeof r === 'string')
3385
- : [];
3386
- out.push({ id, workspaces, requires });
3387
- }
3388
- return out;
3389
- }
3390
-
3391
- /**
3392
- * Active pack id set for `scoped` mode: every pack whose `workspaces`
3393
- * intersects `SCOPED_ACTIVE_WORKSPACES`, unioned with `runtime_active_packs`,
3394
- * expanded over the `requires` graph (transitive closure).
3348
+ /*
3349
+ * The scoped-projection predicate lives in `_lib/scoped_projection.ts` so the
3350
+ * installer and `count_scoped_projection.ts` — which produces the number the
3351
+ * claims ledger publishes — run the SAME code rather than the same rule
3352
+ * written twice. Re-exported under the existing private names to keep the
3353
+ * prune tests' import surface unchanged.
3395
3354
  */
3396
- function _compute_active_pack_ids(
3397
- packs: readonly PackRecord[],
3398
- runtime_active_packs: readonly string[],
3399
- ): Set<string> {
3400
- const by_id = new Map<string, PackRecord>();
3401
- for (const p of packs) by_id.set(p.id, p);
3402
-
3403
- const active = new Set<string>();
3404
- for (const p of packs) {
3405
- if (p.workspaces.some((w) => SCOPED_ACTIVE_WORKSPACES.has(w))) {
3406
- active.add(p.id);
3407
- }
3408
- }
3409
- for (const id of runtime_active_packs) {
3410
- active.add(id);
3411
- }
3412
-
3413
- let frontier = [...active];
3414
- while (frontier.length > 0) {
3415
- const next: string[] = [];
3416
- for (const id of frontier) {
3417
- const rec = by_id.get(id);
3418
- if (rec === undefined) continue;
3419
- for (const dep of rec.requires) {
3420
- if (!active.has(dep)) {
3421
- active.add(dep);
3422
- next.push(dep);
3423
- }
3424
- }
3425
- }
3426
- frontier = next;
3427
- }
3428
- return active;
3429
- }
3355
+ type PackRecord = scoped_projection.PackRecord;
3356
+ const _load_packs_registry = scoped_projection.load_packs_registry;
3357
+ const _compute_active_pack_ids = scoped_projection.compute_active_pack_ids;
3430
3358
 
3431
3359
  /**
3432
3360
  * Read the GLOBAL settings document that governs a global deploy, if one
@@ -3506,7 +3434,7 @@ function _resolve_global_rule_scope(package_root: string): RuleScope {
3506
3434
  // contradict, and this is the documented upgrade-compat path.
3507
3435
  if (settings_path === null) {
3508
3436
  try {
3509
- return ruleScopeFromSettings(_load_default_settings(package_root));
3437
+ return ruleScopeFromSettings(_load_default_settings(package_root), package_root);
3510
3438
  } catch {
3511
3439
  return LEGACY_ALL;
3512
3440
  }
@@ -3539,7 +3467,7 @@ function _resolve_global_rule_scope(package_root: string): RuleScope {
3539
3467
  return LEGACY_ALL;
3540
3468
  }
3541
3469
  try {
3542
- return ruleScopeFromSettings(parsed as Record<string, unknown>);
3470
+ return ruleScopeFromSettings(parsed as Record<string, unknown>, package_root);
3543
3471
  } catch (e) {
3544
3472
  warn(
3545
3473
  `could not derive rule scope from ${settings_path} (${String(e)}) — ` +
@@ -3574,14 +3502,9 @@ function _prune_scoped_modules(
3574
3502
  deploy_results: Record<string, DeployResult>,
3575
3503
  active_ids: ReadonlySet<string>,
3576
3504
  ): [number, Record<string, DeployResult>] {
3577
- return _prune_modules_by(deploy_results, (p) => {
3578
- const packs = surface_tiers.frontmatter_packs(p);
3579
- if (packs.size === 0) return false;
3580
- for (const id of packs) {
3581
- if (active_ids.has(id)) return false;
3582
- }
3583
- return true;
3584
- });
3505
+ return _prune_modules_by(deploy_results, (p) =>
3506
+ scoped_projection.is_pruned_under_scoped(p, active_ids),
3507
+ );
3585
3508
  }
3586
3509
 
3587
3510
  function install_global(