@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
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The shape scan a neighbour skill body passes before any of it may be indexed.
3
+ *
4
+ * SCAN BEFORE INJECT. A foreign `SKILL.md` is a file another package put on
5
+ * disk, and the ranker reaches it through the catalogue union. Nothing about
6
+ * being in `~/.claude/skills` makes its prose safe to fold into a term index
7
+ * whose output names skills to a model — the body is untrusted content in the
8
+ * sense `untrusted-input-defense` means it, and the floor here is the same one
9
+ * that rule states: it is data until something has looked at it.
10
+ *
11
+ * THE SHAPES ARE THE ONES THIS SUITE ALREADY SCANS ITS OWN CORPUS WITH. Four
12
+ * `security_lint` checks, reused as functions rather than re-implemented
13
+ * (decision D3 of the lane): instruction smuggling, hidden Unicode, mixed-script
14
+ * confusables and dangerous frontmatter. Re-declaring their patterns here would
15
+ * create a second copy to drift against the first, and the first is the one CI
16
+ * keeps honest.
17
+ *
18
+ * ANY FINDING REFUSES THE BODY — severity is NOT the discriminator. In the
19
+ * corpus linters, `HIGH` fails a build and `MED` warns, because the corpus is
20
+ * this package's own prose and a warning there is a maintainer's to weigh. A
21
+ * neighbour body is nobody's to weigh at rank time, and the cost of refusing is
22
+ * known and small: the skill still RANKS, by its name, with the finding kind
23
+ * printed. Risk-register row 4 is exactly this trade and resolves the same way —
24
+ * a shell snippet can be an installer skill's whole point, so the skill is never
25
+ * hidden, only un-indexed.
26
+ *
27
+ * WHY THIS IS NOT IMPORTED BY THE RANKER. It pulls four linter modules and,
28
+ * through one of them, `node:child_process`. The ranker runs inside the shared
29
+ * hook bundle on `user_prompt_submit`, where every byte is paid by every concern
30
+ * on every dispatch and `check_hook_bundle_composition` holds the ceiling. So
31
+ * the scan runs in the census CLI and its verdict reaches the ranker as a cached
32
+ * record — see `neighbour_census.scanVerdict`, where a missing record is a
33
+ * refusal rather than a pass.
34
+ */
35
+ import * as fs from 'node:fs';
36
+ import * as path from 'node:path';
37
+
38
+ import * as sl from './security_lint.js';
39
+ import { fileDigest, type ScanCache, type ScanRecord } from './skill_origin.js';
40
+ import { _scan as scanSmuggling } from '../lint_instruction_smuggling.js';
41
+ import { _scan as scanHiddenUnicode } from '../lint_hidden_unicode.js';
42
+ import { _scan as scanConfusables } from '../lint_confusables.js';
43
+ import { _scan as scanFrontmatter } from '../lint_skill_frontmatter_safety.js';
44
+
45
+ /** The shape checks, in the order their findings are reported. */
46
+ const SHAPES: ReadonlyArray<readonly [string, (sf: sl.ScannedFile) => sl.Finding[]]> = [
47
+ ['instruction-smuggling', scanSmuggling],
48
+ ['dangerous-frontmatter', scanFrontmatter],
49
+ ['hidden-unicode', scanHiddenUnicode],
50
+ ['mixed-script-confusable', scanConfusables],
51
+ ];
52
+
53
+ export interface ScanResult {
54
+ /** `null` when every shape passed; otherwise the first finding's check id. */
55
+ kind: string | null;
56
+ /** Every finding, for a report that wants more than the first kind. */
57
+ findings: sl.Finding[];
58
+ }
59
+
60
+ /**
61
+ * Scan one `SKILL.md`.
62
+ *
63
+ * An unreadable file is a refusal, not a pass (`unreadable`): a body nobody
64
+ * could open is in exactly the state this gate exists to keep out of the index,
65
+ * and returning `kind: null` for it would make an IO error look like a clean
66
+ * scan. A THROWING check is reported under its own id rather than swallowed, for
67
+ * the same reason.
68
+ */
69
+ export function scanNeighbourBody(skillMdPath: string): ScanResult {
70
+ let sf: sl.ScannedFile;
71
+ try {
72
+ sf = sl.scan_file(skillMdPath);
73
+ } catch {
74
+ return { kind: 'unreadable', findings: [] };
75
+ }
76
+ const findings: sl.Finding[] = [];
77
+ for (const [id, scan] of SHAPES) {
78
+ try {
79
+ findings.push(...scan(sf));
80
+ } catch {
81
+ findings.push(
82
+ new sl.Finding(sf.rel, 0, id, 'HIGH', 'shape check threw on this body', 1.0),
83
+ );
84
+ }
85
+ }
86
+ return { kind: findings.length === 0 ? null : (findings[0] as sl.Finding).check, findings };
87
+ }
88
+
89
+ /**
90
+ * Record what the scan found, for the ranker to read on the hook path.
91
+ *
92
+ * The digest is the FILE's, not the finding's: the ranker re-digests the body it
93
+ * is about to index and compares, so a body edited after its scan reads as
94
+ * `digest-changed` and goes back to name-only ranking until the census runs
95
+ * again. That compare is the whole mechanism behind "a changed digest rescans
96
+ * first" — without it a one-time pass would keep clearing a file forever.
97
+ *
98
+ * A write failure is reported to the caller rather than thrown: the scan is
99
+ * correct either way, and a read-only report directory must not fail a census.
100
+ */
101
+ export function writeScanCache(file: string, entries: readonly ScanRecord[]): boolean {
102
+ const payload: ScanCache = {
103
+ schema_version: 1,
104
+ entries: [...entries].sort((a, b) => (a.qualified < b.qualified ? -1 : 1)),
105
+ };
106
+ try {
107
+ fs.mkdirSync(path.dirname(file), { recursive: true });
108
+ fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
109
+ return true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ /** A `ScanRecord` for one file, digest included. */
116
+ export function scanRecordFor(qualified: string, skillMdPath: string): ScanRecord {
117
+ return {
118
+ qualified,
119
+ digest: fileDigest(skillMdPath) ?? '',
120
+ unscanned: scanNeighbourBody(skillMdPath).kind,
121
+ };
122
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * The foreign-MCP-tool-name store: one writer on the post-tool hook path, one
3
+ * reader in the neighbour census.
4
+ *
5
+ * WHY A MODULE OF ITS OWN, AND NOT A CONSTANT IN EITHER SIDE. The writer lives
6
+ * in `hooks/telemetry_usage_hook.ts`, which is inlined into the shared hook
7
+ * bundle `check_hook_bundle_composition` caps; the reader lives in
8
+ * `_lib/neighbour_census.ts`, which is not. Putting the pair here lets esbuild
9
+ * tree-shake {@link toolsUsedByServer} out of the hook bundle while both sides
10
+ * still name one path — a constant copied into two files is a store that
11
+ * silently splits in two the first time one copy is edited.
12
+ *
13
+ * WHAT IS RECORDED, AND WHAT IS NOT. One map of `mcp__<server>__<tool>` to the
14
+ * ISO date it was last seen. No arguments, no responses, no session id, no
15
+ * project path beyond the file's own location. The names recorded are names the
16
+ * consumer's own `.mcp.json` already lists, and the file is written under
17
+ * `agents/runtime/`, which is gitignored and local-only — nothing here is sent
18
+ * anywhere, and no transport reads it.
19
+ *
20
+ * NOT GATED ON THE TELEMETRY OPT-IN, DELIBERATELY. `telemetry_usage_hook`'s
21
+ * Class-A capture is default-off because it feeds an outbound spool. This store
22
+ * feeds `doctor neighbours`, a local report the consumer runs on their own tree.
23
+ * Gating it on an org telemetry switch would make the census print `0` on every
24
+ * install that never enabled one — a number that looks like a measurement and is
25
+ * an instrumentation artifact, which is the exact failure the usage hook's own
26
+ * header records from the collector it replaced.
27
+ *
28
+ * GROWTH, DECLARED (scale-discipline R-A7). The store is a map keyed by tool
29
+ * NAME, not an append log: one entry per distinct `mcp__…` name the consumer
30
+ * has ever called, each rewritten in place at most once per day. The bound is
31
+ * therefore the size of the neighbour's tool surface — tens of entries, not a
32
+ * series — so it needs no TTL job. The reader applies the window; entries older
33
+ * than it stay on disk and stop counting, which is why a stale entry is inert
34
+ * rather than wrong.
35
+ */
36
+ import * as fs from 'node:fs';
37
+ import * as path from 'node:path';
38
+
39
+ /** Host-side prefix of every MCP tool name: `mcp__<server>__<tool>`. */
40
+ export const MCP_TOOL_PREFIX = 'mcp__';
41
+
42
+ /**
43
+ * The store, relative to the project root. Gitignored; see the header.
44
+ *
45
+ * A literal with forward slashes rather than `path.join`, and the reason is
46
+ * measured rather than stylistic: `path.join` here is a top-level call, so the
47
+ * `node:path` namespace import survives into the hook bundle even though every
48
+ * function in this module that needs it is tree-shaken out of that bundle.
49
+ * `path.join(root, REL)` at each call site normalises the separator on Windows,
50
+ * so the only thing lost is the authoring convention.
51
+ */
52
+ export const FOREIGN_TOOL_USE_REL = 'agents/runtime/neighbour-tool-use.json';
53
+
54
+ /** The census window. 30 days, matching the step that asked for the number. */
55
+ export const TOOL_USE_WINDOW_DAYS = 30;
56
+
57
+ /** The filename the writer's root is the directory of. */
58
+ const SETTINGS_FILENAME = '.agent-settings.yml';
59
+
60
+ /** `YYYY-MM-DD` in UTC — the granularity the store keys on. */
61
+ export function usageDay(now: Date): string {
62
+ return now.toISOString().slice(0, 10);
63
+ }
64
+
65
+ const DAY_RE = /^\d{4}-\d{2}-\d{2}$/u;
66
+
67
+ /**
68
+ * Is this store value a day the window comparison can be trusted on?
69
+ *
70
+ * The comparison is lexicographic, which is exactly right for `YYYY-MM-DD` and
71
+ * quietly wrong for anything else: `'TODO'`, `'unknown'` and `'hand edited'`
72
+ * all sort ABOVE a real cutoff, so without this check a hand-edited or
73
+ * half-written store inflates the count instead of being ignored. The reader's
74
+ * promise is that a malformed store yields nothing, and a malformed VALUE is
75
+ * the likelier half of that.
76
+ */
77
+ export function isUsageDay(value: unknown): value is string {
78
+ return typeof value === 'string' && DAY_RE.test(value);
79
+ }
80
+
81
+ /**
82
+ * The `.mcp.json` key as it appears inside a tool name.
83
+ *
84
+ * Hosts sanitise the key before embedding it — a server registered as
85
+ * `claude.ai Claude Docs` arrives as `mcp__claude_ai_Claude_Docs__…`. Normalising
86
+ * BOTH sides the same way is what keeps a matched count from quietly being a
87
+ * zero: an exact-key match would report `0` for every server whose name carries
88
+ * a dot or a space, which reads as "never used" rather than as "never matched".
89
+ */
90
+ export function serverSegment(key: string): string {
91
+ return key.replace(/[^A-Za-z0-9_-]/gu, '_');
92
+ }
93
+
94
+ /**
95
+ * Does `tool` belong to the server whose sanitised key is `segment`?
96
+ *
97
+ * Driven by the KNOWN segment rather than by parsing the tool name, and that
98
+ * direction is the whole correctness argument. Parsing — split at the first
99
+ * `__` after the prefix — is ambiguous exactly where {@link serverSegment} is
100
+ * lossy: `Acme Inc. Tools` sanitises to `Acme_Inc__Tools`, whose own name
101
+ * contains the separator, so a parser reads the server as `Acme_Inc` and the
102
+ * census reports `0` for a server in daily use. That is the same never-matched
103
+ * zero `serverSegment` exists to prevent, reintroduced one layer down. Matching
104
+ * against the keys `.mcp.json` actually lists cannot be ambiguous, because the
105
+ * candidate set is finite and known.
106
+ */
107
+ export function toolBelongsTo(tool: string, segment: string): boolean {
108
+ if (!tool.startsWith(MCP_TOOL_PREFIX)) return false;
109
+ const rest = tool.slice(MCP_TOOL_PREFIX.length);
110
+ return rest === segment || rest.startsWith(`${segment}__`);
111
+ }
112
+
113
+ /**
114
+ * The directory the store lives under, from any directory inside the project.
115
+ *
116
+ * The WRITER roots the store at the directory holding `.agent-settings.yml`
117
+ * (`telemetry_usage_hook.readSettingsFor`), because a session started in a
118
+ * subdirectory would otherwise scatter one store per directory it happened to
119
+ * start in. The reader has to resolve the same way or it looks in the wrong
120
+ * place: a monorepo with settings at the repo root and `.mcp.json` under
121
+ * `packages/web` would report `0` for a server in daily use. The two walks are
122
+ * pinned equal by a fixture rather than by assertion.
123
+ *
124
+ * No settings file on any ancestor → `start`, matching the writer's own
125
+ * fallback.
126
+ */
127
+ export function resolveStoreRoot(start: string): string {
128
+ let dir = path.resolve(start);
129
+ for (;;) {
130
+ try {
131
+ if (fs.statSync(path.join(dir, SETTINGS_FILENAME)).isFile()) return dir;
132
+ } catch {
133
+ // Not here — keep walking.
134
+ }
135
+ const parent = path.dirname(dir);
136
+ if (parent === dir) return path.resolve(start);
137
+ dir = parent;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Distinct tools seen per server inside the window, keyed by server segment.
143
+ *
144
+ * `segments` is the closed candidate set — the sanitised `.mcp.json` keys. Every
145
+ * one of them appears in the result, so a server with no sightings reads `0`
146
+ * rather than being absent. Longest segment wins where one is a prefix of
147
+ * another, so a tool is counted once.
148
+ *
149
+ * A missing, unreadable or malformed store yields all-zero — true of what was
150
+ * observed. It does not mean the server was unused, and the census says so next
151
+ * to the number.
152
+ */
153
+ export function toolsUsedByServer(
154
+ projectRoot: string,
155
+ now: Date,
156
+ segments: Iterable<string>,
157
+ ): Map<string, number> {
158
+ const known = [...new Set(segments)].sort((a, b) => b.length - a.length);
159
+ const counts = new Map<string, number>(known.map((s) => [s, 0]));
160
+ if (known.length === 0) return counts;
161
+
162
+ let decoded: unknown;
163
+ try {
164
+ const root = resolveStoreRoot(projectRoot);
165
+ decoded = JSON.parse(fs.readFileSync(path.join(root, FOREIGN_TOOL_USE_REL), 'utf-8'));
166
+ } catch {
167
+ return counts;
168
+ }
169
+ if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) return counts;
170
+
171
+ const today = usageDay(now);
172
+ // Inclusive of both ends, so the span is WINDOW days and not WINDOW+1: a
173
+ // floor computed at `now - 30d` would count 31 distinct days under a field
174
+ // named `tools_used_30d`.
175
+ const floor = usageDay(new Date(now.getTime() - (TOOL_USE_WINDOW_DAYS - 1) * 86_400_000));
176
+
177
+ for (const [tool, seen] of Object.entries(decoded as Record<string, unknown>)) {
178
+ // A future date is as much a sign of a bad write as a malformed one,
179
+ // and without the upper bound a single `9999-01-01` counts forever.
180
+ if (!isUsageDay(seen) || seen < floor || seen > today) continue;
181
+ for (const segment of known) {
182
+ if (toolBelongsTo(tool, segment)) {
183
+ counts.set(segment, (counts.get(segment) ?? 0) + 1);
184
+ break;
185
+ }
186
+ }
187
+ }
188
+ return counts;
189
+ }
@@ -41,6 +41,50 @@
41
41
  * over-filtering would silently stop pinning real prompts, which is worse.
42
42
  * Measured across 1 540 user-role entries: 509 open at character zero with a
43
43
  * marker, and in 0 of them does human text survive wrapper removal.
44
+ *
45
+ * `<agent-message` added 2026-10-03, by the same under-filtering it warns
46
+ * about: a subagent hand-back reaches this slot as `<agent-message from="…">`,
47
+ * and 12 of the 153 hook-sourced user records in this project's capture open
48
+ * with it. It pinned a German session to `en`, after which the turn-end gate
49
+ * refused each German reply — a reply language derived from a frame nobody
50
+ * typed. Its literal stops before the `>` because the element carries an
51
+ * attribute; the anchor at character zero is unchanged.
52
+ *
53
+ * **The fourth shape has NO envelope tag, and it is the exception this file's
54
+ * own "structural, not linguistic" rule has to name rather than quietly break.**
55
+ * The host resumes a rate-limited session with a plain sentence —
56
+ * `Your claude.ai usage limit has reset. Continue the task you were working
57
+ * on…` — carrying no element, no bracket and no stamp. 5 of the 153
58
+ * hook-sourced user records in this project's capture open with it. Measured
59
+ * 2026-10-03: it pinned a German session to `en` with `prompt_chars: 148`,
60
+ * which is that sentence's exact length, and every German reply for the rest
61
+ * of the run was then refused by the turn-end gate as wrong-language.
62
+ *
63
+ * Matching prose is what the paragraph above warns against, so what makes this
64
+ * one admissible is stated rather than assumed: it is a FIXED host string, not
65
+ * a linguistic pattern, and the character-zero anchor does the same work a tag
66
+ * would — a human asking *why did I get "Your claude.ai usage limit has
67
+ * reset"* does not open with it. A test pins that direction. If the host ever
68
+ * reworded the sentence this guard would silently stop matching, which is the
69
+ * honest failure mode: it under-filters back to today's behaviour rather than
70
+ * misfiring on a real turn.
71
+ *
72
+ * It is deliberately NOT added to `machine_wake.ts`. There the same
73
+ * misclassification retains an authorization instead of mis-pinning a reply,
74
+ * and a prose match is too weak a warrant for that consequence — see the
75
+ * asymmetry note below, which this is the first real test of.
76
+ *
77
+ * **A SECOND VOCABULARY ANSWERS THIS SAME QUESTION AND THE TWO HAVE DRIFTED.**
78
+ * `_lib/machine_wake.ts`'s `humanTypedThisTurn` guards per-turn STATE on this
79
+ * slot (the git-authorization ledger, the suggestion latch) while this one
80
+ * guards the language pin. As of this change both know `<task-notification>`,
81
+ * `<system-reminder>` and `<agent-message`; only this one knows
82
+ * `[SYSTEM NOTIFICATION` and `<local-command-caveat>`. That asymmetry is NOT
83
+ * obviously a defect and was deliberately left: mistaking a real turn for a
84
+ * machine one costs a mis-pinned reply here and a RETAINED AUTHORIZATION
85
+ * there, so the two sides do not want the same error budget — and
86
+ * `<local-command-caveat>` in particular precedes a real typed prompt. Unify
87
+ * them only with that asymmetry answered, never as tidying.
44
88
  */
45
89
  export function isSyntheticPrompt(prompt: string): boolean {
46
90
  const head = prompt.trimStart().slice(0, 400);
@@ -49,6 +93,8 @@ export function isSyntheticPrompt(prompt: string): boolean {
49
93
  head.startsWith("<task-notification>") ||
50
94
  head.startsWith("<system-reminder>") ||
51
95
  head.startsWith("<local-command-caveat>") ||
96
+ head.startsWith("<agent-message") ||
97
+ head.startsWith("Your claude.ai usage limit has reset") ||
52
98
  /^\[SYSTEM NOTIFICATION\b/.test(head)
53
99
  );
54
100
  }
@@ -24,6 +24,7 @@ import * as path from 'node:path';
24
24
  import { REPO_ROOT } from '../release_env.js';
25
25
  import { die, run, type RunResult } from '../release_publication.js';
26
26
  import {
27
+ BREAKING_INDEX_PATH,
27
28
  MIGRATION_PATH as MAJOR_MIGRATION_PATH,
28
29
  pendingMajorFinding,
29
30
  } from '../lint_major_migration_sections.js';
@@ -148,8 +149,15 @@ export function assert_scheduled_deprecations_clear(
148
149
  * real run will refuse and returns, because `--dry-run exits 0` is a contract
149
150
  * this file's own tests assert.
150
151
  *
152
+ * A major owes TWO records and this seam reads both: the migration section and
153
+ * the by-major row in `BREAKING_CHANGES.md`. Checking only the first at cut
154
+ * time while the lint checks both would let a cut create the exact state the
155
+ * lint then reds `main` over — the gate arriving one merge too late, which is
156
+ * the shape `pendingMajorFinding` exists to avoid in the first place.
157
+ *
151
158
  * @param migrationReader Seam for the file read, so a test can drive the
152
159
  * refusal without mutating a tracked file.
160
+ * @param indexReader The same seam for the breaking-changes index.
153
161
  */
154
162
  export function assert_major_migration_section(
155
163
  target: string,
@@ -157,18 +165,23 @@ export function assert_major_migration_section(
157
165
  opts: { previewOnly?: boolean } = {},
158
166
  migrationReader: () => string = () =>
159
167
  fs.readFileSync(path.join(REPO_ROOT, MAJOR_MIGRATION_PATH), 'utf-8'),
168
+ indexReader: () => string = () =>
169
+ fs.readFileSync(path.join(REPO_ROOT, BREAKING_INDEX_PATH), 'utf-8'),
160
170
  ): void {
161
171
  let migration: string;
172
+ let index: string;
162
173
  try {
163
174
  migration = migrationReader();
175
+ index = indexReader();
164
176
  } catch (exc) {
165
177
  die(
166
- `refusing the ${target} cut: cannot read ${MAJOR_MIGRATION_PATH} ` +
178
+ `refusing the ${target} cut: cannot read ${MAJOR_MIGRATION_PATH} or ` +
179
+ `${BREAKING_INDEX_PATH} ` +
167
180
  `(${exc instanceof Error ? exc.message : String(exc)}). That is an environment ` +
168
181
  'failure, not a finding — fix the checkout, then re-run.',
169
182
  );
170
183
  }
171
- const finding = pendingMajorFinding(target, changelog_entry, migration);
184
+ const finding = pendingMajorFinding(target, changelog_entry, migration, index);
172
185
  if (finding === null) {
173
186
  return;
174
187
  }
@@ -180,5 +193,8 @@ export function assert_major_migration_section(
180
193
  );
181
194
  return;
182
195
  }
183
- die(`refusing the ${target} cut: ${MAJOR_MIGRATION_PATH} has no section for ${target}.`);
196
+ die(
197
+ `refusing the ${target} cut: ${MAJOR_MIGRATION_PATH} or ${BREAKING_INDEX_PATH} ` +
198
+ `does not name ${target}.`,
199
+ );
184
200
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The reserved-name sweep over flat Claude Code command files.
3
+ *
4
+ * A flat command whose slug shadows a Claude Code built-in never ships on the
5
+ * claude-code anchor — the suite complements the host, it does not overlay its
6
+ * built-ins. The sweep that enforces this used to delete EVERY builtin-named
7
+ * `.md` in the anchor's `commands/` directory, including one another agent
8
+ * package or the consumer had put there. The consumer's own `/review` command
9
+ * disappeared at install time with no message, because the sweep's only
10
+ * question was the filename.
11
+ *
12
+ * The question it asks now is ownership: a file is removed when this package
13
+ * wrote it — the current deploy's own written set, or a path a previous
14
+ * install recorded in the installed-tools manifest. Anything else is FOREIGN:
15
+ * it stays on disk and is reported, so the consumer learns that a built-in
16
+ * name is shadowed by someone and can decide, rather than finding their file
17
+ * gone.
18
+ *
19
+ * Both halves of the sweep's original purpose survive: ours never ships under
20
+ * a reserved name, and the suite still never overlays a built-in with its own
21
+ * file. What it no longer does is enforce that on a file that is not its own.
22
+ */
23
+
24
+ import * as fs from 'node:fs';
25
+ import * as path from 'node:path';
26
+
27
+ export interface ReservedSweepResult {
28
+ /** Slugs of OUR files removed because the name is a host built-in. */
29
+ reserved: string[];
30
+ /** Slugs left in place because nothing proves this package wrote them. */
31
+ foreign: string[];
32
+ }
33
+
34
+ /**
35
+ * Remove our builtin-named flat commands from `commandsDir`; keep foreign ones.
36
+ *
37
+ * Both ownership sets are anchor-relative, exactly as the deploy inventory
38
+ * records them. `currentFiles` is this deploy's written set — the live half,
39
+ * and the only one that exists on a first install. `recordedFiles` is what a
40
+ * previous deploy recorded under the same anchor, so a builtin-named file of
41
+ * ours left behind by an older version is still reaped rather than becoming
42
+ * permanently unprovable. A removed entry is dropped from `currentFiles` so
43
+ * the inventory record stays consistent with the tree, exactly as before.
44
+ */
45
+ export function sweepReservedNames(
46
+ commandsDir: string,
47
+ currentFiles: Set<string>,
48
+ isBuiltinName: (slug: string) => boolean,
49
+ recordedFiles: ReadonlySet<string>,
50
+ ): ReservedSweepResult {
51
+ const reserved: string[] = [];
52
+ const foreign: string[] = [];
53
+ let entries: string[] = [];
54
+ try {
55
+ entries = fs.readdirSync(commandsDir).filter((f) => f.endsWith('.md'));
56
+ } catch {
57
+ return { reserved, foreign };
58
+ }
59
+ for (const fname of entries.sort()) {
60
+ const slug = fname.slice(0, -'.md'.length);
61
+ if (!isBuiltinName(slug)) continue;
62
+ const rel = `commands/${fname}`;
63
+ const ours = currentFiles.has(rel) || recordedFiles.has(rel);
64
+ if (!ours) {
65
+ foreign.push(slug);
66
+ continue;
67
+ }
68
+ fs.rmSync(path.join(commandsDir, fname), { force: true });
69
+ currentFiles.delete(rel);
70
+ reserved.push(slug);
71
+ }
72
+ return { reserved, foreign };
73
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * How much of a release the self-review actually read, stated where the record
3
+ * is read.
4
+ *
5
+ * `self_review_gate` writes a `coverage` block — `{chunks, filesReviewed,
6
+ * filesTotal, unreviewed[]}` — into the findings artifact, `--ingest` carries it
7
+ * into the per-release ledger named by {@link LEDGER_DIR}, and from there NOBODY
8
+ * reads `filesReviewed` back. The 16.2.0 ledger records `filesReviewed: 65,
9
+ * filesTotal: 678`: the review saw under a tenth of the change. Every consumer
10
+ * of that record — the disposition gate's success line, the release-highlights
11
+ * gate, a human opening the file — saw output that said nothing about it.
12
+ *
13
+ * A finding count over 9.6 % of a diff and the same count over all of it are
14
+ * different claims, and the record spelled them identically.
15
+ *
16
+ * A LABEL, NEVER A FLOOR, and the distinction is the decision.
17
+ *
18
+ * Nothing here refuses. `coverageLabel` returns a string or `null`; no caller
19
+ * turns it into an exit code, and none may. Raising what a review reads costs
20
+ * one more paid model request per chunk per cut, which is a spend decision
21
+ * belonging to the owner. A gate that failed a cut on partial coverage would
22
+ * force that decision through the back door — the operator would either pay or
23
+ * be unable to release — which is exactly the move the roadmap's decision D1
24
+ * records as rejected.
25
+ *
26
+ * The honest cost of that choice, stated rather than left to be discovered: a
27
+ * release can ship with 9.6 % coverage and this module will only have said so.
28
+ * Saying so is the whole claim.
29
+ *
30
+ * It lives in its own module because two consumers read it — `check_finding_dispositions` on the ledger and
31
+ * `check_release_highlights` beside the Known-limitations field — and a label
32
+ * the two spell differently is a label a reader cannot compare across the two
33
+ * places they meet it. One definition is also the seam the unit tests use, so
34
+ * the arithmetic is checked without either CLI in the loop.
35
+ */
36
+ import * as fs from 'node:fs';
37
+ import * as path from 'node:path';
38
+
39
+ /** Where the per-release ledgers live, relative to the repository root. */
40
+ export const LEDGER_DIR = path.join('agents', 'evidence', 'release-findings');
41
+
42
+ /**
43
+ * The coverage block as the gate writes it. Every field optional: this type
44
+ * describes an artifact read back off disk, which may be from an older writer
45
+ * or hand-edited, so the reader's job is to decide whether it can answer rather
46
+ * than to assume it can.
47
+ */
48
+ export interface ReviewCoverage {
49
+ readonly chunks?: unknown;
50
+ readonly filesReviewed?: unknown;
51
+ readonly filesTotal?: unknown;
52
+ readonly unreviewed?: unknown;
53
+ }
54
+
55
+ function _finite(v: unknown): number | null {
56
+ return typeof v === 'number' && Number.isFinite(v) ? v : null;
57
+ }
58
+
59
+ /**
60
+ * The one sentence both gates print, or `null` when there is nothing to say.
61
+ *
62
+ * `null` covers three cases deliberately collapsed into one, because they lead
63
+ * to the same output: full coverage (nothing to warn about), an absent block (an
64
+ * older ledger, written before the gate recorded coverage), and a malformed one
65
+ * (a string where a number belongs, a zero total). A reader who sees no label
66
+ * learns "this record makes no partial-coverage claim" — which is true of all
67
+ * three — and never a fabricated percentage derived from a field that was not
68
+ * a number.
69
+ *
70
+ * `filesTotal === 0` is `null` rather than `0 of 0 (partial)`: a change set with
71
+ * no files was fully read by any definition, and dividing by it says nothing.
72
+ */
73
+ export function coverageLabel(coverage: unknown): string | null {
74
+ if (coverage === null || typeof coverage !== 'object') {
75
+ return null;
76
+ }
77
+ const c = coverage as ReviewCoverage;
78
+ const reviewed = _finite(c.filesReviewed);
79
+ const total = _finite(c.filesTotal);
80
+ if (reviewed === null || total === null || total <= 0 || reviewed >= total) {
81
+ return null;
82
+ }
83
+ return `self-review read ${String(reviewed)} of ${String(total)} changed files (partial)`;
84
+ }
85
+
86
+ /**
87
+ * The `coverage` block of a committed ledger, or `undefined`.
88
+ *
89
+ * Never throws. A missing ledger is the normal state of an in-flight release and
90
+ * a corrupt one is already the business of the gate that parses it properly —
91
+ * a label is not the place to discover either, and a reader that threw here
92
+ * would turn an informational line into a crash on a release branch.
93
+ */
94
+ export function readLedgerCoverage(root: string, release: string): unknown {
95
+ const file = path.join(root, LEDGER_DIR, `${release}.json`);
96
+ try {
97
+ const parsed: unknown = JSON.parse(fs.readFileSync(file, 'utf-8'));
98
+ if (parsed === null || typeof parsed !== 'object') {
99
+ return undefined;
100
+ }
101
+ return (parsed as Record<string, unknown>)['coverage'];
102
+ } catch {
103
+ return undefined;
104
+ }
105
+ }
106
+
107
+ /** `coverageLabel` over the committed ledger for `release`. */
108
+ export function ledgerCoverageLabel(root: string, release: string): string | null {
109
+ return coverageLabel(readLedgerCoverage(root, release));
110
+ }
111
+
112
+ /**
113
+ * The same label as a Known-limitations line, for the release-highlights gate.
114
+ *
115
+ * It carries the label plus the sentence that keeps a reader from mistaking it
116
+ * for a refusal, because the field it prints beside is one the gate CAN refuse
117
+ * on. Without that sentence the next person to read the output reasonably
118
+ * concludes the gate is about to start failing cuts on coverage, and the cheapest
119
+ * way to make the message go away becomes raising the ceiling — which is the
120
+ * owner's spend decision, taken by whoever happened to read the line.
121
+ *
122
+ * Returns `null` when there is nothing to say, so the caller prints nothing
123
+ * rather than a reassuring sentence about a number it does not have.
124
+ */
125
+ export function coverageLimitationNote(label: string | null): string | null {
126
+ if (label === null) {
127
+ return null;
128
+ }
129
+ return (
130
+ `ℹ️ ${label} — a limitation of the RECORD, not a verdict on the release. ` +
131
+ 'Stated, never enforced:\n what the review reads is a spend decision and this ' +
132
+ 'gate does not take it.'
133
+ );
134
+ }