@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
@@ -88,6 +88,7 @@ import * as user_global_paths from '../_lib/user_global_paths.js';
88
88
  import { jsonDumpsIndent2 as _jsonDumpsIndent2 } from './_py_json.js';
89
89
  import * as budget from './cli_call_budget.js';
90
90
  import { appendEvent } from './events_log.js';
91
+ import { OPENAI_CLI_VENDOR_DEFAULT } from './vendor_defaults.js';
91
92
 
92
93
  export const ANTHROPIC_KEY_FILENAME = 'anthropic.key';
93
94
  export const OPENAI_KEY_FILENAME = 'openai.key';
@@ -124,23 +125,12 @@ export const DEFAULT_PERPLEXITY_MODEL = 'sonar-pro';
124
125
  // divergence is a one-line change, not a hunt through inline literals.
125
126
  // (`xai` / `perplexity` CLI reuse their API constants: their CLIs are
126
127
  // community wrappers around the same paid API, so the values do not diverge.)
127
- /**
128
- * Sentinel: let the codex CLI pick its own model, by omitting `--model`.
129
- *
130
- * This is not a placeholder for a value nobody looked up. Measured
131
- * 2026-08-15 against `codex exec --json` on a ChatGPT-account (subscription)
132
- * transport, every explicitly named candidate was refused with
133
- * `400 invalid_request_error: The '<model>' model is not supported when using
134
- * Codex with a ChatGPT account.` — `gpt-4o`, `gpt-5` (this constant's previous
135
- * value) and `gpt-5.1-codex` alike. Omitting the flag answered normally.
136
- *
137
- * So the only value known to work on this transport is "whatever the CLI
138
- * chooses", and pinning ANY name here would re-break the seat the next time
139
- * the vendor rotates its lineup. The label is carried into records as-is so a
140
- * reader can see that no model was pinned, rather than a name being implied
141
- * that nobody verified.
142
- */
143
- export const OPENAI_CLI_VENDOR_DEFAULT = 'codex-default';
128
+ // `OPENAI_CLI_VENDOR_DEFAULT` lives in `vendor_defaults.ts`, re-exported here
129
+ // so every existing importer keeps resolving. It moved because `config.ts`
130
+ // wanted that one string and was charged this whole module for it in the
131
+ // composed hook bundle — 51,523 bytes on every dispatch; the reasoning and the
132
+ // measurement are in that file's header.
133
+ export { OPENAI_CLI_VENDOR_DEFAULT } from './vendor_defaults.js';
144
134
 
145
135
  /**
146
136
  * Models MEASURED to be refused by the codex subscription transport, with the
@@ -1011,6 +1001,7 @@ export const CLI_CALLS_FILENAME = 'cli-calls.json';
1011
1001
  export {
1012
1002
  CLI_CALLS_ATTRIBUTION_SUFFIX,
1013
1003
  CLI_CONSUMER_COUNCIL,
1004
+ CLI_CONSUMER_SELF_REVIEW,
1014
1005
  CLI_CONSUMER_TEAM,
1015
1006
  CLI_CONSUMER_UNKNOWN,
1016
1007
  QUOTA_SOURCE_LOCAL_BUDGET,
@@ -1678,16 +1669,22 @@ export function _foldSystemPrompt(system_prompt: string, user_prompt: string): s
1678
1669
  export class AnthropicCliClient extends CliClient {
1679
1670
  override subscription_label = 'claude-pro';
1680
1671
 
1672
+ // `...opts` rather than a field list, in all five CLI subclasses, and the
1673
+ // spread IS the fix rather than a tidy. Each of these constructors used to
1674
+ // name six option fields explicitly and forward those — so when
1675
+ // `CliClientOptions` grew a seventh, `consumer`, every subclass silently
1676
+ // dropped it. Both construction sites passed it, `CliClient` read
1677
+ // `opts.consumer ?? CLI_CONSUMER_UNKNOWN`, and the sidecar recorded
1678
+ // `unknown` for every CLI call ever booked: measured 2026-10-05 as
1679
+ // `{"anthropic":{"unknown":2},"openai":{"unknown":2}}` after a council
1680
+ // round. A field list can forget a field. A spread cannot, so the eighth
1681
+ // option needs no edit here and cannot repeat this.
1682
+ //
1683
+ // `model` stays explicit because it is the one field each subclass
1684
+ // overrides with its own pinned default; everything else passes through.
1681
1685
  constructor(opts: CliClientOptions = {}) {
1682
1686
  super(
1683
- {
1684
- model: opts.model ?? DEFAULT_ANTHROPIC_CLI_MODEL,
1685
- binary: opts.binary,
1686
- timeout_seconds: opts.timeout_seconds,
1687
- max_calls_per_day: opts.max_calls_per_day,
1688
- warn_at: opts.warn_at,
1689
- cli_calls_path: opts.cli_calls_path,
1690
- },
1687
+ { ...opts, model: opts.model ?? DEFAULT_ANTHROPIC_CLI_MODEL },
1691
1688
  { name: 'anthropic', default_binary: 'claude' },
1692
1689
  );
1693
1690
  }
@@ -1817,14 +1814,7 @@ export class OpenAICliClient extends CliClient {
1817
1814
 
1818
1815
  constructor(opts: CliClientOptions = {}) {
1819
1816
  super(
1820
- {
1821
- model: opts.model ?? DEFAULT_OPENAI_CLI_MODEL,
1822
- binary: opts.binary,
1823
- timeout_seconds: opts.timeout_seconds,
1824
- max_calls_per_day: opts.max_calls_per_day,
1825
- warn_at: opts.warn_at,
1826
- cli_calls_path: opts.cli_calls_path,
1827
- },
1817
+ { ...opts, model: opts.model ?? DEFAULT_OPENAI_CLI_MODEL },
1828
1818
  { name: 'openai', default_binary: 'codex' },
1829
1819
  );
1830
1820
  }
@@ -2064,14 +2054,7 @@ export class GeminiCliClient extends CliClient {
2064
2054
 
2065
2055
  constructor(opts: CliClientOptions = {}) {
2066
2056
  super(
2067
- {
2068
- model: opts.model ?? DEFAULT_GEMINI_CLI_MODEL,
2069
- binary: opts.binary,
2070
- timeout_seconds: opts.timeout_seconds,
2071
- max_calls_per_day: opts.max_calls_per_day,
2072
- warn_at: opts.warn_at,
2073
- cli_calls_path: opts.cli_calls_path,
2074
- },
2057
+ { ...opts, model: opts.model ?? DEFAULT_GEMINI_CLI_MODEL },
2075
2058
  { name: 'gemini', default_binary: 'gemini' },
2076
2059
  );
2077
2060
  }
@@ -2217,14 +2200,7 @@ export class XAICliClient extends CliClient {
2217
2200
 
2218
2201
  constructor(opts: CliClientOptions = {}) {
2219
2202
  super(
2220
- {
2221
- model: opts.model ?? DEFAULT_XAI_MODEL,
2222
- binary: opts.binary,
2223
- timeout_seconds: opts.timeout_seconds,
2224
- max_calls_per_day: opts.max_calls_per_day,
2225
- warn_at: opts.warn_at,
2226
- cli_calls_path: opts.cli_calls_path,
2227
- },
2203
+ { ...opts, model: opts.model ?? DEFAULT_XAI_MODEL },
2228
2204
  { name: 'xai', default_binary: 'grok' },
2229
2205
  );
2230
2206
  }
@@ -2289,14 +2265,7 @@ export class PerplexityCliClient extends CliClient {
2289
2265
 
2290
2266
  constructor(opts: CliClientOptions = {}) {
2291
2267
  super(
2292
- {
2293
- model: opts.model ?? DEFAULT_PERPLEXITY_MODEL,
2294
- binary: opts.binary,
2295
- timeout_seconds: opts.timeout_seconds,
2296
- max_calls_per_day: opts.max_calls_per_day,
2297
- warn_at: opts.warn_at,
2298
- cli_calls_path: opts.cli_calls_path,
2299
- },
2268
+ { ...opts, model: opts.model ?? DEFAULT_PERPLEXITY_MODEL },
2300
2269
  { name: 'perplexity', default_binary: 'perplexity' },
2301
2270
  );
2302
2271
  }
@@ -82,7 +82,7 @@ import * as budget from './cli_call_budget.js';
82
82
  // `import type` and is erased at build, so there is no runtime cycle today.
83
83
  // Converting it to a value import — or adding any other runtime import from
84
84
  // quorum into config — creates a real ESM cycle in the loader's init path.
85
- import { OPENAI_CLI_VENDOR_DEFAULT } from './clients.js';
85
+ import { OPENAI_CLI_VENDOR_DEFAULT } from './vendor_defaults.js';
86
86
  import { parseSeatFields, type PolicyExclusion } from './seat_policy.js';
87
87
  import type { ContentClass } from './content_ceiling.js';
88
88
  import { SOLO_FLOOR_MIN_PRESENT } from './quorum.js';
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Vendor-CLI sentinel values that are DATA, carried in a module with no
3
+ * dependencies of its own.
4
+ *
5
+ * WHY THIS FILE EXISTS, measured rather than argued. `OPENAI_CLI_VENDOR_DEFAULT`
6
+ * is a single string, and it used to live in `clients.ts` beside the transport
7
+ * classes that compare against it. `ai_council/config.ts` imports that one
8
+ * string and nothing else from `clients.ts`; `config.ts` is reachable from
9
+ * `council-availability`, which is bound on `session_start`. So every hook
10
+ * dispatch on every slot carried the whole council transport layer —
11
+ * `clients.ts` and its closure, **51,523 bytes** of the composed
12
+ * `dist/hooks/dispatch.js` — to compare one string against another. Breaking
13
+ * that one import edge removes exactly that, with no capability removed
14
+ * anywhere: every entry point that actually talks to a vendor CLI imports
15
+ * `clients.ts` directly and still does.
16
+ *
17
+ * The bytes are not an abstraction. `src/config/hook-bundle-budget.json` caps
18
+ * the composed bundle, the cap is a shrink-only ratchet, and on 2026-10-02 the
19
+ * tree stood 75 bytes under it — a state in which no concern anywhere could add
20
+ * a line. This module is how `road-to-a-rule-carrier-that-works-outside-the-repo`
21
+ * Phase 1 paid for its own runtime code instead of asking for a raise.
22
+ *
23
+ * WHAT BELONGS HERE: a vendor-facing constant that a CONFIGURATION reader needs
24
+ * and a TRANSPORT reader also happens to use. Nothing that imports anything.
25
+ * The moment this file gains an import it stops being the cheap half of the
26
+ * split and the split stops being worth its own file.
27
+ */
28
+
29
+ /**
30
+ * Sentinel: let the codex CLI pick its own model, by omitting `--model`.
31
+ *
32
+ * This is not a placeholder for a value nobody looked up. Measured
33
+ * 2026-08-15 against `codex exec --json` on a ChatGPT-account (subscription)
34
+ * transport, every explicitly named candidate was refused with
35
+ * `400 invalid_request_error: The '<model>' model is not supported when using
36
+ * Codex with a ChatGPT account.` — `gpt-4o`, `gpt-5` (this constant's previous
37
+ * value) and `gpt-5.1-codex` alike. Omitting the flag answered normally.
38
+ *
39
+ * So the only value known to work on this transport is "whatever the CLI
40
+ * chooses", and pinning ANY name here would re-break the seat the next time
41
+ * the vendor rotates its lineup. The label is carried into records as-is so a
42
+ * reader can see that no model was pinned, rather than a name being implied
43
+ * that nobody verified.
44
+ */
45
+ export const OPENAI_CLI_VENDOR_DEFAULT = 'codex-default';
@@ -30,6 +30,7 @@
30
30
  * quietly until the gate means nothing. Cross-pack pairs never block — a
31
31
  * cross-pack merge changes install shape and is a different decision.
32
32
  */
33
+ import { createHash } from 'node:crypto';
33
34
  import * as fs from 'node:fs';
34
35
  import * as path from 'node:path';
35
36
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -47,6 +48,7 @@ export const OUT_MD_NAME = 'skill-overlap.md';
47
48
  export const OUT_JSON = path.join(REPORT_DIR, OUT_JSON_NAME);
48
49
  export const OUT_MD = path.join(REPORT_DIR, OUT_MD_NAME);
49
50
  export const ALLOWLIST = path.join(ROOT, 'src', 'scripts', 'audit_skill_overlap_allowlist.json');
51
+ export const CROSS_CACHE_NAME = 'neighbour-overlap.json';
50
52
  export const ALLOWLIST_CAP = 20;
51
53
  // re.compile(r"^---\n(.*?)\n---", re.DOTALL) — used with .search (anywhere).
52
54
  const FM_RE = /^---\n([\s\S]*?)\n---/m;
@@ -68,6 +70,8 @@ type Json = any;
68
70
  interface Skill {
69
71
  name: string;
70
72
  relpath: string;
73
+ /** Absolute `SKILL.md` path — the key a cross-root result is returned under. */
74
+ abspath: string;
71
75
  packs: Set<string>;
72
76
  vector: Map<string, number>;
73
77
  }
@@ -178,6 +182,7 @@ export function _parse(md: string, field: Field = 'body'): Skill {
178
182
  return {
179
183
  name: name ? String(name) : path.basename(path.dirname(md)),
180
184
  relpath: _relPosix(md, ROOT),
185
+ abspath: path.resolve(md),
181
186
  packs: Array.isArray(packsVal) ? new Set(packsVal.map((x) => String(x))) : new Set(),
182
187
  vector: _keyword_vector(source),
183
188
  };
@@ -322,6 +327,193 @@ export function find_pairs(skills: Skill[], threshold: number): Pair[] {
322
327
  return stableSort(pairs, (p) => [p.same_domain ? 0 : 1, -p.similarity]);
323
328
  }
324
329
 
330
+ /**
331
+ * One neighbour skill against one of ours, at or above the threshold.
332
+ *
333
+ * `neighbour_path` is absolute, because the two sides live under different roots
334
+ * and a repo-relative path would be a `../..` string that no caller can key on.
335
+ */
336
+ export interface CrossPair {
337
+ neighbour: string;
338
+ neighbour_path: string;
339
+ ours: string;
340
+ similarity: number;
341
+ }
342
+
343
+ /**
344
+ * Cross-root pairs: every NEIGHBOUR skill against every one of OURS.
345
+ *
346
+ * NOT `find_pairs` over a merged list, and the difference is the whole point of
347
+ * the mode. `find_pairs` is `combinations(skills, 2)` over one corpus, so a
348
+ * merged list would also compare our skills against each other — re-deriving the
349
+ * same-corpus report under a different name, at the cost of the pairs the caller
350
+ * actually asked for being buried in it. This emits exactly the cross product,
351
+ * which is also the cheaper half: |neighbours| × |ours| rather than C(n+m, 2).
352
+ *
353
+ * The THRESHOLD is the caller's and defaults to {@link OVERLAP_THRESHOLD} — the
354
+ * same 0.70 every historical same-corpus report is calibrated against. Reusing it
355
+ * is deliberate: a second, softer bar invented for neighbours would make
356
+ * `compat: overlapping` mean something no existing number can be compared to.
357
+ */
358
+ export function crossRootPairs(
359
+ neighbourRoot: string,
360
+ ourRoot: string,
361
+ threshold: number = OVERLAP_THRESHOLD,
362
+ field: Field = 'body',
363
+ ): CrossPair[] {
364
+ return crossPairsForFiles(skillMdsUnder(neighbourRoot), ourRoot, threshold, field);
365
+ }
366
+
367
+ /** Non-archived `SKILL.md` paths under a root, in the collector's own order. */
368
+ export function skillMdsUnder(root: string): string[] {
369
+ return _rglobSkillMd(root).filter((md) => !md.split(path.sep).includes('_archive'));
370
+ }
371
+
372
+ /**
373
+ * The same cross product, over an EXPLICIT neighbour file list.
374
+ *
375
+ * This is the shape the census actually needs, and the root-taking wrapper above
376
+ * is the convenience layer rather than the other way round. A census's neighbour
377
+ * skills are the UNCLAIMED entries of two roots that also hold claimed ones —
378
+ * `agent-config install` writes our own skills into `~/.claude/skills` — so
379
+ * pairing a whole root would compare this package's installed copies against its
380
+ * own source tree and report the install as an overlap.
381
+ */
382
+ export function crossPairsForFiles(
383
+ neighbourFiles: readonly string[],
384
+ ourRoot: string,
385
+ threshold: number = OVERLAP_THRESHOLD,
386
+ field: Field = 'body',
387
+ ): CrossPair[] {
388
+ const theirs: Skill[] = [];
389
+ for (const md of neighbourFiles) {
390
+ try {
391
+ theirs.push(_parse(md, field));
392
+ } catch {
393
+ // Unreadable neighbour file: not a pair, and not a crash.
394
+ }
395
+ }
396
+ const ours = collect(ourRoot, field);
397
+ const out: CrossPair[] = [];
398
+ for (const n of theirs) {
399
+ for (const o of ours) {
400
+ const sim = _cosine(n.vector, o.vector);
401
+ if (sim < threshold) continue;
402
+ out.push({
403
+ neighbour: n.name,
404
+ neighbour_path: n.abspath,
405
+ ours: o.name,
406
+ similarity: pyRound(sim, 3),
407
+ });
408
+ }
409
+ }
410
+ return stableSort(out, (p) => [-p.similarity]);
411
+ }
412
+
413
+ /**
414
+ * Cross-root pairs folded the way the census reads them: highest-scoring hit per
415
+ * neighbour `SKILL.md`, with every our-skill name that crossed the bar.
416
+ */
417
+ export function crossRootOverlapMap(
418
+ pairs: readonly CrossPair[],
419
+ ): Map<string, { names: string[]; similarity: number }> {
420
+ const out = new Map<string, { names: string[]; similarity: number }>();
421
+ for (const p of pairs) {
422
+ const hit = out.get(p.neighbour_path);
423
+ if (hit === undefined) {
424
+ out.set(p.neighbour_path, { names: [p.ours], similarity: p.similarity });
425
+ continue;
426
+ }
427
+ if (!hit.names.includes(p.ours)) hit.names.push(p.ours);
428
+ if (p.similarity > hit.similarity) hit.similarity = p.similarity;
429
+ }
430
+ for (const hit of out.values()) hit.names.sort();
431
+ return out;
432
+ }
433
+
434
+ /**
435
+ * The census digest the cross-root cache is keyed by.
436
+ *
437
+ * Over the INPUTS and nothing else: every skill's absolute path and the sha256
438
+ * of its bytes, on both sides, plus the threshold and the field. Risk-register
439
+ * row 3 is why a cache exists at all — a pairwise pass per prompt is not a
440
+ * hook-budget shape — and keying it on the bytes is what makes "a changed digest
441
+ * rescans first" true rather than aspirational. A key over NAMES alone would
442
+ * hand a stale score to a body that was rewritten underneath it.
443
+ */
444
+ export function censusDigest(
445
+ neighbourFiles: readonly string[],
446
+ ourRoot: string,
447
+ threshold: number,
448
+ field: Field = 'body',
449
+ ): string {
450
+ const h = createHash('sha256');
451
+ h.update(`${String(threshold)}\n${field}\n`);
452
+ const sides: ReadonlyArray<readonly string[]> = [
453
+ [...neighbourFiles].sort(),
454
+ skillMdsUnder(ourRoot),
455
+ ];
456
+ for (const side of sides) {
457
+ h.update('side\n');
458
+ for (const md of side) {
459
+ let bytes: Buffer;
460
+ try {
461
+ bytes = fs.readFileSync(md);
462
+ } catch {
463
+ continue;
464
+ }
465
+ h.update(path.resolve(md));
466
+ h.update('\0');
467
+ h.update(createHash('sha256').update(bytes).digest('hex'));
468
+ h.update('\n');
469
+ }
470
+ }
471
+ return h.digest('hex');
472
+ }
473
+
474
+ export interface CrossCache {
475
+ schema_version: 1;
476
+ digest: string;
477
+ threshold: number;
478
+ pairs: CrossPair[];
479
+ }
480
+
481
+ /**
482
+ * Cross-root pairs, recomputed only when the census digest moved.
483
+ *
484
+ * A cache miss is never silent: the result says `cached: false`, so a caller
485
+ * reporting "this came from the cache" cannot be reporting it about a pass that
486
+ * actually ran. A write failure is not fatal — the pairs are correct either way,
487
+ * and refusing to answer because a report directory is read-only would turn a
488
+ * cache into a dependency.
489
+ */
490
+ export function cachedCrossRootPairs(
491
+ neighbourFiles: readonly string[],
492
+ ourRoot: string,
493
+ cacheFile: string,
494
+ threshold: number = OVERLAP_THRESHOLD,
495
+ field: Field = 'body',
496
+ ): { pairs: CrossPair[]; digest: string; cached: boolean } {
497
+ const digest = censusDigest(neighbourFiles, ourRoot, threshold, field);
498
+ try {
499
+ const parsed = JSON.parse(fs.readFileSync(cacheFile, 'utf-8')) as Partial<CrossCache>;
500
+ if (parsed.digest === digest && Array.isArray(parsed.pairs)) {
501
+ return { pairs: parsed.pairs as CrossPair[], digest, cached: true };
502
+ }
503
+ } catch {
504
+ // No cache, or an unreadable one: compute.
505
+ }
506
+ const pairs = crossPairsForFiles(neighbourFiles, ourRoot, threshold, field);
507
+ const payload: CrossCache = { schema_version: 1, digest, threshold, pairs };
508
+ try {
509
+ fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
510
+ fs.writeFileSync(cacheFile, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
511
+ } catch {
512
+ // A cache that cannot be written is still a correct answer.
513
+ }
514
+ return { pairs, digest, cached: false };
515
+ }
516
+
325
517
  /** Stable sort by a tuple-of-numbers key (Python tuple compare). */
326
518
  function stableSort<T>(arr: T[], key: (x: T) => number[]): T[] {
327
519
  return arr
@@ -484,6 +676,9 @@ class FloatTag {
484
676
  export function main(argv: string[] | null = null): number {
485
677
  const args = parse_args(argv ?? process.argv.slice(2));
486
678
  const root = args.root ?? _default_skill_root();
679
+ if (args.neighbourRoot !== undefined) {
680
+ return _mainCrossRoot(args, root);
681
+ }
487
682
  const skills = collect(root, args.field);
488
683
  try {
489
684
  assertScanned({
@@ -570,6 +765,55 @@ export function main(argv: string[] | null = null): number {
570
765
  return 1;
571
766
  }
572
767
 
768
+ /**
769
+ * `--neighbour-root` — the cross-root pair mode.
770
+ *
771
+ * A SECOND QUESTION, not a second answer to the first. The same-corpus report
772
+ * asks "are two of our skills so alike that one should absorb the other", and a
773
+ * merge is the action it leads to. This asks "is a skill another package
774
+ * installed close enough to one of ours that a reader of the route line needs to
775
+ * know", and NOTHING is merged, removed or suppressed on the answer — the
776
+ * neighbour is not ours to merge. So it writes its own artifact and leaves
777
+ * `skill-overlap.json` alone.
778
+ *
779
+ * Exit 0 always. A finding here is a report; `--strict` belongs to the
780
+ * same-corpus mode, where the pair is between two files this package owns.
781
+ */
782
+ function _mainCrossRoot(args: Args, ourRoot: string): number {
783
+ const neighbourRoot = args.neighbourRoot as string;
784
+ const outDir = args.outDir ?? REPORT_DIR;
785
+ const cacheFile = args.crossCache ?? path.join(outDir, CROSS_CACHE_NAME);
786
+ const { pairs, digest, cached } = cachedCrossRootPairs(
787
+ skillMdsUnder(neighbourRoot),
788
+ ourRoot,
789
+ cacheFile,
790
+ args.threshold,
791
+ args.field,
792
+ );
793
+ const theirs = collect(neighbourRoot, args.field);
794
+ // The counts line the gate-coverage contract asks for, on stderr like its
795
+ // sibling, so the report body on stdout stays machine-readable.
796
+ process.stderr.write(`scanned: ${String(theirs.length)}\n`);
797
+ if (!args.quiet) {
798
+ process.stdout.write(
799
+ `\u2705 Neighbour overlap: ${theirs.length} neighbour skill(s) \u00d7 ` +
800
+ `${collect(ourRoot, args.field).length} of ours, ${pairs.length} pair(s) ` +
801
+ `\u2265 ${_pct(args.threshold)} (${cached ? 'from cache' : 'recomputed'}).\n`,
802
+ );
803
+ process.stdout.write(` census digest: ${digest.slice(0, 12)}\n`);
804
+ process.stdout.write(` cache: ${_relPosix(cacheFile, ROOT)}\n`);
805
+ }
806
+ const byPath = crossRootOverlapMap(pairs);
807
+ for (const skill of theirs) {
808
+ const hit = byPath.get(skill.abspath);
809
+ if (hit === undefined) continue;
810
+ process.stdout.write(
811
+ ` ${hit.similarity.toFixed(3)} ${skill.name} also: ${hit.names.join(', ')}\n`,
812
+ );
813
+ }
814
+ return 0;
815
+ }
816
+
573
817
  /** Order-independent key for an unordered pair. */
574
818
  export function _pairKey(a: string, b: string): string {
575
819
  return a < b ? `${a}::${b}` : `${b}::${a}`;
@@ -662,6 +906,10 @@ interface Args {
662
906
  strict: boolean;
663
907
  /** Allowlist path override (tests); `undefined` = the shipped file. */
664
908
  allowlist?: string;
909
+ /** Cross-root mode: the NEIGHBOUR tree to pair against `--root`. */
910
+ neighbourRoot?: string;
911
+ /** Cross-root cache path; `undefined` = `<out-dir>/neighbour-overlap.json`. */
912
+ crossCache?: string;
665
913
  /** Which text the vector is built from. `description` is NON-canonical. */
666
914
  field: Field;
667
915
  }
@@ -698,6 +946,19 @@ export function parse_args(argv: string[]): Args {
698
946
  } else if (a === '--threshold' || a.startsWith('--threshold=')) {
699
947
  const inline = a.startsWith('--threshold=') ? a.slice('--threshold='.length) : undefined;
700
948
  args.threshold = Number(takeValue('--threshold', inline, () => argv[++i]));
949
+ } else if (a === '--neighbour-root' || a.startsWith('--neighbour-root=')) {
950
+ const inline = a.startsWith('--neighbour-root=')
951
+ ? a.slice('--neighbour-root='.length)
952
+ : undefined;
953
+ args.neighbourRoot = path.resolve(
954
+ ROOT,
955
+ takeValue('--neighbour-root', inline, () => argv[++i]),
956
+ );
957
+ } else if (a === '--cross-cache' || a.startsWith('--cross-cache=')) {
958
+ const inline = a.startsWith('--cross-cache=')
959
+ ? a.slice('--cross-cache='.length)
960
+ : undefined;
961
+ args.crossCache = path.resolve(ROOT, takeValue('--cross-cache', inline, () => argv[++i]));
701
962
  } else if (a === '--root' || a.startsWith('--root=')) {
702
963
  const inline = a.startsWith('--root=') ? a.slice('--root='.length) : undefined;
703
964
  args.root = path.resolve(ROOT, takeValue('--root', inline, () => argv[++i]));