@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
@@ -212,6 +212,11 @@
212
212
  },
213
213
  "description": "What mechanically enforces this rule. `validator:<script>` and `test:<path>` must resolve on disk AND be wired into a taskfile, workflow, or the hook manifest — a script that exists but runs nowhere resolves to unwired, not covered. `hook:<name>` must be registered in hook_manifest.yaml with fail_closed: true; a hook that never blocks is `observer:`, not `validator:`. `instruction-only: <reason>` declares an honestly model-carried obligation and is counted as a recorded gap — the reason is MANDATORY in the pattern, because a bare marker is a claim with no triage record behind it and resolves to `missing`, which the --check ratchet reds on. The bare `none` spelling it replaces stays legal for exactly one reason: `non-destructive-by-default` still carries it and is a kernel rule that block_kernel_rule_writes denies agent writes to, so retiring the value outright would make a CI-green tree unreachable for any agent. Resolved, not trusted, by check_enforcement_coverage.ts."
214
214
  },
215
+ "consequence_class": {
216
+ "type": "string",
217
+ "enum": ["irreversible-external", "security-boundary", "authority-bypass"],
218
+ "description": "Which clause of the high-consequence criterion this rule meets, declared only by rules that meet one (road-to-rule-laws-that-can-stand Phase 2). THE CRITERION, and it is meant to be applied by reading the rule's own law section rather than its name: a rule is high-consequence when violating the obligation its law states would (a) `irreversible-external` \u2014 perform an action that leaves this repository and that this agent cannot undo: a deploy, a published egress, a paid render, a rewrite of published history, a destroyed record; or (b) `security-boundary` \u2014 cross a security or data-exposure boundary: authorization, secrets, private or personal data, confidentiality, or untrusted content reaching a privileged path; or (c) `authority-bypass` \u2014 take, or let the agent take, a decision reserved to the user, the owner, or an independent evaluator. A rule whose violation costs quality, tokens, consistency, consistency of style, or rework is NOT high-consequence however important it is \u2014 that reading is what makes the class a measurement rather than a list of favourites. Exactly one clause per rule: where two apply, the declared one is the clause the rule's own law text names. The class is what the criterion produces: every member, every near-miss considered and rejected with its reason, and every member whose law cannot yet stand in a stub are all recorded in src/config/rule-consequence-class.json and checked by tests/scripts/rule_consequence_class.test.ts. Consumed by project_thin_rules, which ships a class member as a stub plus its own law section, byte-copied and digest-checked."
219
+ },
215
220
  "obligation_frequency": {
216
221
  "type": "string",
217
222
  "enum": ["per-edit", "per-turn", "per-task", "per-session", "per-event", "per-commit", "per-file-write", "none"],
@@ -34,7 +34,13 @@ import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs
34
34
  import path from 'node:path';
35
35
  import { fileURLToPath } from 'node:url';
36
36
 
37
- import { AnthropicClient, load_anthropic_key } from './ai_council/clients.js';
37
+ import { CLI_CONSUMER_SELF_REVIEW } from './ai_council/cli_call_budget.js';
38
+ import {
39
+ AnthropicClient,
40
+ AnthropicCliClient,
41
+ type ExternalAIClient,
42
+ load_anthropic_key,
43
+ } from './ai_council/clients.js';
38
44
  import { independenceFields } from './_lib/review_independence.js';
39
45
 
40
46
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -1012,6 +1018,49 @@ function resolveKey(): string | null {
1012
1018
  }
1013
1019
  }
1014
1020
 
1021
+ /** The transport a reviewer was reached on, for the run's own report. */
1022
+ interface Reviewer {
1023
+ client: ExternalAIClient;
1024
+ transport: 'cli' | 'api';
1025
+ how: string;
1026
+ }
1027
+
1028
+ /**
1029
+ * Resolve the review client CLI-FIRST — the subscription transport before the
1030
+ * metered one, which is the ordering the rest of this suite already uses for
1031
+ * coding work and which the council has used since it grew a `mode: cli`.
1032
+ *
1033
+ * The ordering is not a preference. An API key is a METERED resource with a
1034
+ * balance that runs out, and when it does the gate reports NEUTRAL — a release
1035
+ * then carries no review at all. That is not hypothetical: it is how 16.3.0
1036
+ * stalled, with six chunks returning `credit balance is too low` and the
1037
+ * release step refusing for want of a findings artefact. A vendor CLI backed by
1038
+ * a subscription has no balance to exhaust, so trying it first removes the one
1039
+ * failure mode that has actually produced an unreviewed release here.
1040
+ *
1041
+ * `AnthropicCliClient`'s constructor THROWS when `claude` is not on PATH, which
1042
+ * is the probe: a hosted GitHub runner has no vendor CLI and falls straight
1043
+ * through to the API transport, so this change costs CI nothing and buys every
1044
+ * local and self-hosted run a free review. The catch is deliberately broad —
1045
+ * anything that goes wrong reaching for the CLI resolves to the API path rather
1046
+ * than to a NEUTRAL, because a reviewed release beats a diagnosed one.
1047
+ *
1048
+ * Calls book against `CLI_CONSUMER_SELF_REVIEW` so the daily CLI counter can
1049
+ * tell a review apart from a council round; without the attribution the gate's
1050
+ * six chunks would read as unexplained council spend.
1051
+ */
1052
+ function resolveReviewer(): Reviewer | null {
1053
+ try {
1054
+ const client = new AnthropicCliClient({ consumer: CLI_CONSUMER_SELF_REVIEW });
1055
+ return { client, transport: 'cli', how: `vendor CLI (${client.binary}), subscription` };
1056
+ } catch {
1057
+ // No `claude` on PATH, or the client refused to construct. Fall through.
1058
+ }
1059
+ const key = resolveKey();
1060
+ if (!key) return null;
1061
+ return { client: new AnthropicClient({ api_key: key }), transport: 'api', how: 'ANTHROPIC_API_KEY' };
1062
+ }
1063
+
1015
1064
  // ── CLI ───────────────────────────────────────────────────────────────
1016
1065
  export function main(argv: string[]): 0 | 2 {
1017
1066
  const dryRun = argv.includes('--dry-run');
@@ -1087,22 +1136,28 @@ export function main(argv: string[]): 0 | 2 {
1087
1136
  return 0;
1088
1137
  }
1089
1138
 
1090
- const key = resolveKey();
1091
- if (!key) {
1139
+ const reviewer = resolveReviewer();
1140
+ if (!reviewer) {
1092
1141
  // Explicit NEUTRAL state — never a bare green that reads as "reviewed".
1142
+ // BOTH transports are named, because naming only the API one is what
1143
+ // made the CLI path invisible: an operator reading this warning set the
1144
+ // secret and never learned a logged-in `claude` would also have served.
1093
1145
  process.stdout.write(
1094
- '::warning::self-review-gate NEUTRAL — no ANTHROPIC_API_KEY configured, NOTHING was reviewed. ' +
1095
- 'Set the repo secret to enable the live dogfooded review.\n',
1146
+ '::warning::self-review-gate NEUTRAL — no reviewer reachable (no `claude` CLI on PATH, ' +
1147
+ 'no ANTHROPIC_API_KEY), NOTHING was reviewed. Log in to the vendor CLI or set the ' +
1148
+ 'repo secret to enable the live dogfooded review.\n',
1096
1149
  );
1097
1150
  const summary = process.env.GITHUB_STEP_SUMMARY;
1098
1151
  if (summary) {
1099
1152
  appendFileSync(
1100
1153
  summary,
1101
- '### Self-review gate: NEUTRAL\n\nNo `ANTHROPIC_API_KEY` secret — nothing was reviewed. This is not a pass.\n',
1154
+ '### Self-review gate: NEUTRAL\n\nNo reviewer reachable — neither the `claude` CLI ' +
1155
+ '(subscription) nor `ANTHROPIC_API_KEY` (metered). Nothing was reviewed. This is not a pass.\n',
1102
1156
  );
1103
1157
  }
1104
1158
  return 0;
1105
1159
  }
1160
+ process.stdout.write(`::notice::self-review-gate — transport: ${reviewer.transport} (${reviewer.how}).\n`);
1106
1161
 
1107
1162
  // Belt-and-suspenders: the WHOLE live path is wrapped so that ANYTHING going
1108
1163
  // wrong — no API credit / balance (HTTP 402), rate-limit (429), network error,
@@ -1111,7 +1166,7 @@ export function main(argv: string[]): 0 | 2 {
1111
1166
  // gate is advisory by design. (Only an --enforce run with real blocking
1112
1167
  // findings returns 2 — an error is not a finding.)
1113
1168
  try {
1114
- const client = new AnthropicClient({ api_key: key });
1169
+ const client = reviewer.client;
1115
1170
  const systemPrompt = buildSystemPrompt(plan.release, baseRef);
1116
1171
  const partition = plan.partition;
1117
1172
 
@@ -39,11 +39,14 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
39
39
  // a second copy (road-to-skill-delivery-over-mcp risk 6). Everything below is
40
40
  // the disk half — globbing, frontmatter, the CLI — which stays here.
41
41
  import {
42
+ buildTermStats,
42
43
  scoreSkill,
43
44
  skillTerms as _sharedSkillTerms,
44
45
  tokenize as _sharedTokenize,
45
46
  triggerTextFromFlatLines,
47
+ widensTermSource,
46
48
  type RankOptions,
49
+ type TermStats,
47
50
  } from '../../shared/skillRanking.js';
48
51
 
49
52
  const _HERE = fileURLToPath(import.meta.url);
@@ -67,6 +70,20 @@ export const ROOT = path.resolve(path.dirname(_HERE), '..', '..', '..');
67
70
  * CLI turns that into a distinct exit code rather than an empty list.
68
71
  */
69
72
  import { DEFAULT_CATALOGUE_ROOTS, resolveSkillCatalogueRoots } from '../_lib/skill_catalogue.js';
73
+ // Authorship + scan primitives only. Deliberately NOT `neighbour_census.js`,
74
+ // whose module graph reaches the host-hook merger, the secret detector and the
75
+ // install-ownership reader — this module is inlined into the shared hook bundle,
76
+ // where every byte is paid by every concern on every dispatch.
77
+ import {
78
+ fileDigest,
79
+ makeSkillOriginResolver,
80
+ qualifiedSkillName,
81
+ readScanCache,
82
+ scanCachePath,
83
+ scanVerdict,
84
+ type ScanCache,
85
+ type SkillOrigin,
86
+ } from '../_lib/skill_origin.js';
70
87
 
71
88
  /**
72
89
  * Every readable catalogue root, in precedence order — possibly empty.
@@ -100,7 +117,17 @@ export interface Frontmatter {
100
117
  export function _parse_frontmatter(filePath: string): Frontmatter {
101
118
  // text.read_text(errors="replace") — Node `utf-8` decode replaces the same
102
119
  // way for the inputs these tools see (well-formed SKILL.md files).
103
- const text = fs.readFileSync(filePath, 'utf-8');
120
+ return _parse_frontmatter_text(fs.readFileSync(filePath, 'utf-8'));
121
+ }
122
+
123
+ /**
124
+ * The same reader over text already in hand.
125
+ *
126
+ * The loader needs the body as well as the frontmatter, and reading each
127
+ * SKILL.md twice to get them would double the per-call I/O of a ranker that
128
+ * runs on a hook path.
129
+ */
130
+ export function _parse_frontmatter_text(text: string): Frontmatter {
104
131
  if (!text.startsWith('---')) {
105
132
  return {};
106
133
  }
@@ -164,38 +191,204 @@ function _rstrip(s: string): string {
164
191
 
165
192
  export interface Skill {
166
193
  name: string;
194
+ /**
195
+ * Where the skill came from — `package` for ours, `project` / `home` for a
196
+ * neighbour another installer put in a root this ranker unions in.
197
+ */
198
+ origin: SkillOrigin;
199
+ /** What a route line prints: `name` for ours, `origin:name` otherwise. */
200
+ qualified: string;
201
+ /**
202
+ * Finding kind when a neighbour body did not pass the shape scan, else
203
+ * `null`. Such a skill is still ranked — by its NAME only; see
204
+ * {@link _load_skills}.
205
+ */
206
+ unscanned: string | null;
167
207
  description: string;
168
208
  personas: string[];
169
209
  terms: Set<string>;
170
210
  /** `triggers[].keyword` / `.phrase` prose. Indexed only under keyword-v2. */
171
211
  triggerText: string[];
212
+ /** The body's `## When to use` section. Indexed only behind its own flag. */
213
+ whenToUseText: string;
214
+ /** The body's `##` / `###` heading lines. Indexed only behind its own flag. */
215
+ headingText: string[];
216
+ }
217
+
218
+ /** Everything after the frontmatter block, or the whole text when there is none. */
219
+ export function _body_of(text: string): string {
220
+ if (!text.startsWith('---')) return text;
221
+ const end = text.indexOf('\n---', 3);
222
+ if (end === -1) return text;
223
+ return text.slice(end + 4);
172
224
  }
173
225
 
174
226
  /**
175
- * Load one root, or several with the FIRST occurrence of a name winning.
227
+ * The `## When to use` section, and every `##` / `###` heading, out of a body.
228
+ *
229
+ * Line-oriented, and bounded in two ways that a first version got wrong in the
230
+ * same direction — it indexed something other than what this block describes,
231
+ * which makes a measurement of the FLAG a measurement of the PARSER:
176
232
  *
177
- * Precedence by name, never by root: every root is read, and a collision
178
- * resolves to the earliest root that carries it — `resolveSkillCatalogueRoots`
179
- * owns what that order means. Reading only the first root is the defect this
180
- * replaced; silently ranking one name twice would be the next one.
233
+ * - the section ends at the next heading of the SAME OR A HIGHER level, so a
234
+ * `###` subsection of `## When to use` stays inside it. Ending at any
235
+ * heading truncated the capture at the first subsection;
236
+ * - a line inside a fenced code block is never a heading. These skills are
237
+ * documentation and routinely fence markdown samples, so a `## …` line in a
238
+ * fence was both harvested as a topic and used to terminate the section.
239
+ *
240
+ * Without the first bound a skill whose whole file is one section could still
241
+ * not put its procedure into an index whose flag says "when to use", because a
242
+ * sibling `##` ends it.
243
+ */
244
+ export function _body_signals(body: string): { whenToUse: string; headings: string[] } {
245
+ const headings: string[] = [];
246
+ const when: string[] = [];
247
+ let whenLevel = 0;
248
+ let fenceMark: string | null = null;
249
+ let fenceLen = 0;
250
+ for (const raw of body.split('\n')) {
251
+ const line = raw.trimEnd();
252
+ const f = /^\s*(`{3,}|~{3,})(.*)$/.exec(line);
253
+ if (f) {
254
+ const run = f[1] as string;
255
+ const mark = run[0] as string;
256
+ if (fenceMark === null) {
257
+ fenceMark = mark;
258
+ fenceLen = run.length;
259
+ // The DELIMITER and its info string are not prose. Pushing them
260
+ // put the language tag of every fenced sample (`bash`, `yaml`)
261
+ // into the index, which is the parser leaking into the signal in
262
+ // the opposite direction from the truncation this block fixed.
263
+ continue;
264
+ }
265
+ // A closing fence has to be at least as long as the opening one and
266
+ // use the same marker; otherwise a ``` inside a ```` block would
267
+ // close it early.
268
+ if (mark === fenceMark && run.length >= fenceLen && (f[2] as string).trim() === '') {
269
+ fenceMark = null;
270
+ fenceLen = 0;
271
+ }
272
+ continue;
273
+ }
274
+ if (fenceMark !== null) {
275
+ if (whenLevel > 0) when.push(raw);
276
+ continue;
277
+ }
278
+ // `#{1,6}`, not `#{2,6}`: a level-1 heading is strictly HIGHER than the
279
+ // section and must close it. Matching only `##`+ let a `# New Section`
280
+ // and everything after it leak into an open capture.
281
+ const h = /^(#{1,6})\s+(.*)$/.exec(line);
282
+ if (h) {
283
+ const level = (h[1] as string).length;
284
+ const title = (h[2] as string).trim();
285
+ if (level >= 2 && level <= 3) headings.push(title);
286
+ if (whenLevel > 0 && level <= whenLevel) {
287
+ whenLevel = 0;
288
+ } else if (whenLevel > 0) {
289
+ // A SUBSECTION TITLE inside `## When to use` is part of that
290
+ // section's text. Skipping it indexed the prose under a `### Do
291
+ // NOT use when` while dropping the words that say what it is.
292
+ when.push(title);
293
+ }
294
+ // Only OPEN a section when none is open. Assigning unconditionally
295
+ // let a `### When to use it on a monorepo` overwrite the outer
296
+ // level 2 with 3, after which the next sibling `###` satisfied
297
+ // `level <= whenLevel` and closed the section early — silently
298
+ // dropping everything from that sibling to the real end.
299
+ if (whenLevel === 0 && /^when to use\b/i.test(title)) whenLevel = level;
300
+ continue;
301
+ }
302
+ if (whenLevel > 0) when.push(raw);
303
+ }
304
+ return { whenToUse: when.join(' ').trim(), headings };
305
+ }
306
+
307
+ /**
308
+ * Everything a load needs to know about who owns a file and whether its body was
309
+ * scanned. Built once per `rank` call, never per file.
181
310
  */
182
- function _load_skills_across(roots: readonly string[]): Skill[] {
311
+ export interface NeighbourContext {
312
+ origin: (skillMdPath: string) => SkillOrigin;
313
+ scans: ScanCache | null;
314
+ }
315
+
316
+ /**
317
+ * The context for a workspace: manifest-backed origins plus the scan cache.
318
+ *
319
+ * Built once per `rank` call rather than per file — one manifest read and one
320
+ * cache read for a catalogue of several hundred entries.
321
+ */
322
+ export function defaultNeighbourContext(workspaceRoot: string = ROOT): NeighbourContext {
323
+ return {
324
+ origin: makeSkillOriginResolver(workspaceRoot, { packageRoot: ROOT }),
325
+ scans: readScanCache(scanCachePath(workspaceRoot)),
326
+ };
327
+ }
328
+
329
+ /**
330
+ * Load one root, or several, with the FIRST occurrence of a QUALIFIED name winning.
331
+ *
332
+ * **It used to dedupe on the bare name, and that was the defect.** Every root is
333
+ * read, so a second package's `design-system` in `~/.claude/skills` and ours both
334
+ * reached this loop — and whichever root came second was dropped, silently, with
335
+ * the surviving row printing one bare name. The reader could not tell which skill
336
+ * the route line meant, and in the direction where the neighbour came first, ours
337
+ * was simply gone.
338
+ *
339
+ * Deduping on the qualified name keeps the only collision that is genuinely a
340
+ * duplicate — the same PACKAGE skill reached through two roots, which is the
341
+ * authored tree beside its own projection — and lets a collision between two
342
+ * different owners rank twice, which is the thing a reader needs to see. A
343
+ * no-manifest tree labels everything `package` (`makeOriginResolver`), so there
344
+ * the behavior is byte-identical to the bare-name dedupe it replaced.
345
+ */
346
+ function _load_skills_across(
347
+ roots: readonly string[],
348
+ opts: RankOptions = {},
349
+ ctx?: NeighbourContext,
350
+ ): Skill[] {
351
+ const context = ctx ?? defaultNeighbourContext();
183
352
  const out: Skill[] = [];
184
353
  const seen = new Set<string>();
185
354
  for (const root of roots) {
186
- for (const s of _load_skills(root)) {
187
- if (seen.has(s.name)) continue;
188
- seen.add(s.name);
355
+ for (const s of _load_skills(root, opts, context)) {
356
+ if (seen.has(s.qualified)) continue;
357
+ seen.add(s.qualified);
189
358
  out.push(s);
190
359
  }
191
360
  }
192
361
  return out;
193
362
  }
194
363
 
195
- function _load_skills(skillsDir: string): Skill[] {
364
+ /**
365
+ * One root's skills.
366
+ *
367
+ * A NEIGHBOUR whose body did not pass the shape scan is loaded with every term
368
+ * source but its name emptied — description, triggers, `## When to use`,
369
+ * headings and personas — so it ranks by name and nothing from the file can
370
+ * reach the index or, through it, the injected route line. It is NOT dropped:
371
+ * a skill hidden for a finding is a skill the reader cannot weigh, and the
372
+ * finding kind rides along on `unscanned` instead.
373
+ *
374
+ * "Did not pass" includes "was never scanned". `scanVerdict` refuses a missing
375
+ * census record and a stale digest as firmly as a real finding, which is what
376
+ * makes this a floor rather than a preference — a gate that passes on missing
377
+ * evidence is satisfied by deleting the evidence.
378
+ */
379
+ function _load_skills(
380
+ skillsDir: string,
381
+ opts: RankOptions = {},
382
+ ctx: NeighbourContext = defaultNeighbourContext(),
383
+ ): Skill[] {
384
+ // The body is parsed only when a flag indexes it. Under keyword-v1 the
385
+ // loader's work is exactly what it always was, which is what keeps the
386
+ // default path's per-prompt cost comparable to the pre-flag reading.
387
+ const wantsBody = Boolean(opts.includeWhenToUse || opts.includeHeadings);
196
388
  const skills: Skill[] = [];
197
389
  for (const skillMd of _globSkillMd(skillsDir)) {
198
- const fm = _parse_frontmatter(skillMd);
390
+ const text = fs.readFileSync(skillMd, 'utf-8');
391
+ const fm = _parse_frontmatter_text(text);
199
392
  const rawName = fm['name'];
200
393
  const name = _truthyStr(rawName) ? String(rawName) : path.basename(path.dirname(skillMd));
201
394
  const rawDesc = fm['description'];
@@ -209,12 +402,41 @@ function _load_skills(skillsDir: string): Skill[] {
209
402
  const triggerText = Array.isArray(rawTriggers)
210
403
  ? triggerTextFromFlatLines(rawTriggers as string[])
211
404
  : [];
405
+ const { whenToUse, headings } = wantsBody
406
+ ? _body_signals(_body_of(text))
407
+ : { whenToUse: '', headings: [] as string[] };
408
+ const origin = ctx.origin(skillMd);
409
+ const qualified = qualifiedSkillName(name, origin);
410
+ const verdict =
411
+ origin === 'package'
412
+ ? ({ ok: true } as const)
413
+ : scanVerdict(ctx.scans, qualified, fileDigest(skillMd));
414
+ if (!verdict.ok) {
415
+ skills.push({
416
+ name,
417
+ origin,
418
+ qualified,
419
+ unscanned: verdict.kind,
420
+ description: '',
421
+ personas: [],
422
+ terms: _tokenize(name),
423
+ triggerText: [],
424
+ whenToUseText: '',
425
+ headingText: [],
426
+ });
427
+ continue;
428
+ }
212
429
  skills.push({
213
430
  name,
431
+ origin,
432
+ qualified,
433
+ unscanned: null,
214
434
  description: desc,
215
435
  personas: personaList,
216
436
  terms: _tokenize(name + ' ' + desc),
217
437
  triggerText,
438
+ whenToUseText: whenToUse,
439
+ headingText: headings,
218
440
  });
219
441
  }
220
442
  return skills;
@@ -234,7 +456,7 @@ function _truthyStr(v: string | string[] | undefined): boolean {
234
456
  * Python sorts `Path` objects component-wise. Since every match is
235
457
  * `<dir>/SKILL.md`, sorting by the directory component reproduces that order.
236
458
  */
237
- function _globSkillMd(root: string): string[] {
459
+ export function _globSkillMd(root: string): string[] {
238
460
  let names: string[];
239
461
  try {
240
462
  names = fs.readdirSync(root);
@@ -257,38 +479,81 @@ function _globSkillMd(root: string): string[] {
257
479
  return dirs.map((name) => path.join(root, name, 'SKILL.md'));
258
480
  }
259
481
 
260
- function _score(taskTerms: Set<string>, skill: Skill, opts: RankOptions = {}): number {
261
- // Single-sourced in `src/shared/skillRanking.ts`. Under keyword-v1 (the
262
- // default) `skill.terms` is already `tokenize(name + ' ' + description)`, so
263
- // the precomputed set is reused unchanged; keyword-v2 re-derives the term
264
- // set with the skill's trigger prose folded in.
265
- const rankable = {
482
+ function _rankable(skill: Skill): {
483
+ name: string;
484
+ description: string;
485
+ personas: string[];
486
+ triggerText: string[];
487
+ whenToUseText: string;
488
+ headingText: string[];
489
+ } {
490
+ return {
266
491
  name: skill.name,
267
492
  description: skill.description,
268
493
  personas: skill.personas,
269
494
  triggerText: skill.triggerText,
495
+ whenToUseText: skill.whenToUseText,
496
+ headingText: skill.headingText,
270
497
  };
271
- const terms = opts.includeTriggers ? _sharedSkillTerms(rankable, opts) : skill.terms;
272
- return scoreSkill(taskTerms, rankable, terms);
498
+ }
499
+
500
+ /**
501
+ * The indexed term set for one skill.
502
+ *
503
+ * With no term-source flag set, `skill.terms` is already
504
+ * `tokenize(name + ' ' + description)` and the precomputed set is reused
505
+ * unchanged; a flag that widens the source re-derives it with that source
506
+ * folded in. Separated from `_score` so the caller can build each set ONCE and
507
+ * share it between the document-frequency pass and the scoring loop.
508
+ */
509
+ function _terms(skill: Skill, opts: RankOptions): ReadonlySet<string> {
510
+ return widensTermSource(opts) ? _sharedSkillTerms(_rankable(skill), opts) : skill.terms;
511
+ }
512
+
513
+ function _score(
514
+ taskTerms: Set<string>,
515
+ skill: Skill,
516
+ terms: ReadonlySet<string>,
517
+ opts: RankOptions = {},
518
+ stats?: TermStats,
519
+ ): number {
520
+ // Single-sourced in `src/shared/skillRanking.ts`.
521
+ return scoreSkill(taskTerms, _rankable(skill), terms, opts, stats);
273
522
  }
274
523
 
275
524
  export type RankRow = [string, number, string[]];
276
525
 
277
526
  /**
278
527
  * @param skillsDir one root, or several to rank across (see `_load_skills_across`).
528
+ * @param ctx origin + scan context; omitted, it is built from {@link ROOT}.
529
+ *
530
+ * Row names are QUALIFIED (`project:design-system`) for a neighbour and bare for
531
+ * ours, so two same-named skills are two distinguishable rows rather than one
532
+ * ambiguous one. On a tree with no installed-tools manifest every skill resolves
533
+ * to `package` and every row is bare, which is what it was before.
279
534
  */
280
535
  export function rank(
281
536
  task: string,
282
537
  skillsDir: string | readonly string[],
283
538
  opts: RankOptions = {},
539
+ ctx?: NeighbourContext,
284
540
  ): RankRow[] {
285
541
  const taskTerms = _tokenize(task);
286
- const skills = _load_skills_across(typeof skillsDir === 'string' ? [skillsDir] : skillsDir);
542
+ const skills = _load_skills_across(
543
+ typeof skillsDir === 'string' ? [skillsDir] : skillsDir,
544
+ opts,
545
+ ctx,
546
+ );
547
+ // ONE term set per skill, shared between the document-frequency pass and the
548
+ // scoring loop — the two used to tokenize the whole catalogue separately.
549
+ const termSets = skills.map((s) => _terms(s, opts));
550
+ const stats = opts.idfWeighting ? buildTermStats(skills.map(_rankable), termSets) : undefined;
287
551
  const rows: RankRow[] = [];
288
- for (const s of skills) {
289
- const score = _score(taskTerms, s, opts);
552
+ for (let i = 0; i < skills.length; i += 1) {
553
+ const s = skills[i] as Skill;
554
+ const score = _score(taskTerms, s, termSets[i] as ReadonlySet<string>, opts, stats);
290
555
  if (score > 0) {
291
- rows.push([s.name, score, [...s.personas]]);
556
+ rows.push([s.qualified, score, [...s.personas]]);
292
557
  }
293
558
  }
294
559
  // rows.sort(key=lambda r: (-r[1], r[0])) — Python stable tuple sort.
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * sweep_skill_ranker_signals — every candidate signal, alone, on both slices.
4
+ *
5
+ * `road-to-a-ranker-that-routes` 2.2 asks for top-1, top-3 and MRR with
6
+ * intervals for each candidate flag measured ALONE, and the flags are a table in
7
+ * `measure_skill_ranker_baseline.RANKER_LABELS`. This script is the thing that
8
+ * produces that table, and it exists as a committed script rather than a
9
+ * one-off because a number nobody can regenerate is a claim rather than a
10
+ * measurement.
11
+ *
12
+ * IT PRINTS BOTH SLICES, and the two are read for different purposes. The
13
+ * TUNING reading is what a signal may be CHOSEN on. The SEALED reading is the
14
+ * only one a lift may be CLAIMED on. Printing only the sealed column would
15
+ * invite choosing on it, which is the one thing the partition exists to stop;
16
+ * printing only the tuning column would leave the claim unsupported.
17
+ *
18
+ * Usage:
19
+ * ./scripts-run src/scripts/sweep_skill_ranker_signals
20
+ * ./scripts-run src/scripts/sweep_skill_ranker_signals keyword-v1 idf
21
+ * ./scripts-run src/scripts/sweep_skill_ranker_signals --json
22
+ */
23
+
24
+ import * as path from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ import { wilsonInterval } from './_lib/capture_rate.js';
28
+ import {
29
+ RANKER_LABELS,
30
+ REPO,
31
+ SKILLS_DIR,
32
+ type SliceName,
33
+ isKnownRanker,
34
+ partitionBySlice,
35
+ readMatrixLabelledPrompts,
36
+ } from './measure_skill_ranker_baseline.js';
37
+ import { meanReciprocalRank, readRows } from './report_skill_ranker_confusion.js';
38
+
39
+ export interface SweepRow {
40
+ label: string;
41
+ slice: SliceName;
42
+ n: number;
43
+ top1: number;
44
+ top1_ci95: { lower: number; upper: number };
45
+ top3: number;
46
+ top3_ci95: { lower: number; upper: number };
47
+ mrr: number;
48
+ }
49
+
50
+ function round3(n: number): number {
51
+ return Math.round(n * 1000) / 1000;
52
+ }
53
+
54
+ function ci(successes: number, trials: number): { lower: number; upper: number } {
55
+ const raw = wilsonInterval(successes, trials);
56
+ return { lower: round3(raw.lower), upper: round3(raw.upper) };
57
+ }
58
+
59
+ /**
60
+ * Every configuration on both slices, ONE ranking pass per prompt.
61
+ *
62
+ * `rank()` re-globs and re-reads the whole SKILL.md catalogue on every call, so
63
+ * it is the dominant cost by a wide margin and ranking each prompt twice doubles
64
+ * the whole sweep. The earlier shape took top-1/top-3 from `measureAccuracy` and
65
+ * MRR from a second pass in `readRows`, which also derived the row set from two
66
+ * independent corpus reads — so a future divergence between them would have been
67
+ * silent rather than loud. All three rates now come off the same readings of the
68
+ * same rows.
69
+ */
70
+ export function sweep(labels: readonly string[], repo = REPO, skillsDir = SKILLS_DIR): SweepRow[] {
71
+ const whole = readMatrixLabelledPrompts(repo);
72
+ const out: SweepRow[] = [];
73
+ for (const label of labels) {
74
+ for (const slice of ['tuning', 'sealed'] as const) {
75
+ const rows = partitionBySlice(whole, slice);
76
+ const readings = readRows(rows, skillsDir, label);
77
+ const n = readings.length;
78
+ const t1 = readings.filter((r) => r.rankOfExpected === 1).length;
79
+ const t3 = readings.filter((r) => r.rankOfExpected >= 1 && r.rankOfExpected <= 3).length;
80
+ out.push({
81
+ label,
82
+ slice,
83
+ n,
84
+ top1: n ? round3(t1 / n) : 0,
85
+ top1_ci95: ci(t1, n),
86
+ top3: n ? round3(t3 / n) : 0,
87
+ top3_ci95: ci(t3, n),
88
+ mrr: round3(meanReciprocalRank(readings)),
89
+ });
90
+ }
91
+ }
92
+ return out;
93
+ }
94
+
95
+ export function renderTable(rows: readonly SweepRow[]): string {
96
+ const L = ['| configuration | slice | n | top-1 | 95 % CI | top-3 | 95 % CI | MRR |', '|---|---|---:|---:|---|---:|---|---:|'];
97
+ for (const r of rows) {
98
+ L.push(
99
+ `| \`${r.label}\` | ${r.slice} | ${String(r.n)} | ${r.top1.toFixed(3)} | ` +
100
+ `${r.top1_ci95.lower.toFixed(3)} – ${r.top1_ci95.upper.toFixed(3)} | ${r.top3.toFixed(3)} | ` +
101
+ `${r.top3_ci95.lower.toFixed(3)} – ${r.top3_ci95.upper.toFixed(3)} | ${r.mrr.toFixed(3)} |`,
102
+ );
103
+ }
104
+ return `${L.join('\n')}\n`;
105
+ }
106
+
107
+ export function main(argv: readonly string[]): number {
108
+ const json = argv.includes('--json');
109
+ const named = argv.filter((a) => !a.startsWith('--'));
110
+ const labels = named.length > 0 ? named : Object.keys(RANKER_LABELS);
111
+ // `isKnownRanker` (Object.hasOwn), not `in`: `in` walks the prototype chain,
112
+ // so `constructor` and `toString` would pass here while `Object.keys` below
113
+ // prints a set that does not contain them — two answers to one question.
114
+ const unknown = labels.filter((l) => !isKnownRanker(l));
115
+ if (unknown.length > 0) {
116
+ process.stderr.write(
117
+ `sweep_skill_ranker_signals: unknown configuration(s) ${unknown.join(', ')}; ` +
118
+ `known: ${Object.keys(RANKER_LABELS).join(', ')}\n`,
119
+ );
120
+ return 2;
121
+ }
122
+ const rows = sweep(labels);
123
+ process.stdout.write(json ? `${JSON.stringify(rows, null, 2)}\n` : renderTable(rows));
124
+ return 0;
125
+ }
126
+
127
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
128
+ process.exit(main(process.argv.slice(2)));
129
+ }