@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
@@ -45,7 +45,18 @@ import * as fs from 'node:fs';
45
45
  import * as path from 'node:path';
46
46
  import { fileURLToPath, pathToFileURL } from 'node:url';
47
47
 
48
- import * as yaml from 'js-yaml';
48
+ // `yaml`, not `js-yaml`, and the choice is about the BUNDLE rather than about
49
+ // this file. Every concern is inlined into one `dist/hooks/dispatch.js` that
50
+ // every dispatch loads, and this import was the only `js-yaml` one left on that
51
+ // path while six other modules — `dispatch_hook.ts` and `host_lowering.ts`
52
+ // among them — already pulled `yaml`. Measured on 2026-10-01 from the esbuild
53
+ // metafile: `js-yaml` contributed 95,846 bytes (6.13 %) of a 1,564,211-byte
54
+ // bundle, paid on every hook event, for one call site. The behavioural delta is
55
+ // normalised in `parseSettingsDoc` below — three constraints, each MEASURED
56
+ // against the old reader rather than read off documentation — and pinned both by
57
+ // the characterization tests written before the swap and by a differential test
58
+ // that runs BOTH readers over one corpus and asserts the same refusal set.
59
+ import { parseDocument as parseYamlDocument } from 'yaml';
49
60
 
50
61
  import {
51
62
  buildSettingsClassIndex,
@@ -179,10 +190,64 @@ export function leafPaths(value: unknown, prefix = ''): Map<string, string> {
179
190
  * `null` is NOT "no keys changed" — the caller treats it as an unreadable
180
191
  * corpus and refuses, because a guard that allows whatever it cannot parse is
181
192
  * bypassed by making the file unparseable for one call.
193
+ *
194
+ * THE YAML READER IS CONSTRAINED, and all three constraints exist to keep the
195
+ * refusal set the same as `js-yaml`'s rather than to express a preference.
196
+ * Each was MEASURED against the old reader over a shared corpus, which the
197
+ * differential test re-runs; the equivalence is not argued from documentation.
198
+ *
199
+ * · `schema: 'core'` + `resolveKnownTags: false` — WITHOUT THIS, THE SWAP
200
+ * WOULD HAVE WEAKENED THE GUARD. `js-yaml` v5's default CORE schema does
201
+ * not carry the YAML 1.1 tags, so `!!timestamp`, `!!binary`, `!!set`,
202
+ * `!!omap`, `!!pairs` and any unknown `!custom` tag THREW — the document
203
+ * was unparseable and the guard refused. The `yaml` package resolves them
204
+ * by default, so the same document would have parsed and the guard would
205
+ * have gone on to allow an edit it previously refused. Found by an
206
+ * independent council review (openai/codex-default, 2026-10-01) and
207
+ * confirmed by running both readers: six tagged inputs, `js-yaml` throws on
208
+ * all six, default `yaml` returns a value for all six.
209
+ * · A WARNING IS A REFUSAL. `yaml` reports an unresolvable tag as a warning
210
+ * and keeps parsing where `js-yaml` throws, so treating warnings as success
211
+ * would re-open the same hole one tag at a time. `parseDocument` is used
212
+ * rather than `parse` precisely to see them.
213
+ * · NULLISH IS UNPARSEABLE. `js-yaml` threw on an empty document and returned
214
+ * `null` for a value-less one (`~`, `null`); both landed on refusal. `yaml`
215
+ * returns `null` for all three instead of throwing on the first, so without
216
+ * this collapse an EMPTIED settings file would arrive at the caller as a
217
+ * parsed document with no keys — the one shape that turns "I cannot read
218
+ * this" into "nothing changed".
219
+ *
220
+ * · `merge: false` — PINNED, NOT FIXING AN OBSERVED DIFFERENCE. The `yaml`
221
+ * package documents `merge` as defaulting to the document's YAML VERSION:
222
+ * true for 1.1, false for 1.2. A second review round (openai/codex-default)
223
+ * flagged that a `%YAML 1.1` directive could therefore re-enable merge
224
+ * resolution past the schema pin, which would expose keys the old reader
225
+ * left nested under a literal `<<`. Measured, and it does NOT reproduce
226
+ * here — under `schema: 'core'` a `%YAML 1.1` document reads
227
+ * `{"personal":{"<<":{…}}}` identically with and without this option, same
228
+ * as `js-yaml`. It is set anyway because the cost is a word and the finding
229
+ * named a real documented coupling between two options: a future default
230
+ * cannot flip it silently. An option set without an observed failure behind
231
+ * it is said so rather than implied.
232
+ *
233
+ * A merge key (`<<: *anchor`) was raised as a difference by the other seat and
234
+ * is NOT one: both readers leave `<<` literal at these versions, with and
235
+ * without a version directive. The same differential corpus pins both forms, so
236
+ * the question is settled by a reading rather than re-litigated from
237
+ * documentation next time.
182
238
  */
183
239
  export function parseSettingsDoc(text: string, rel: string): unknown | null {
240
+ if (rel.endsWith('.json')) {
241
+ try {
242
+ return (JSON.parse(text) as unknown) ?? null;
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
184
247
  try {
185
- return rel.endsWith('.json') ? (JSON.parse(text) as unknown) : yaml.load(text);
248
+ const doc = parseYamlDocument(text, { schema: 'core', resolveKnownTags: false, merge: false });
249
+ if (doc.errors.length > 0 || doc.warnings.length > 0) return null;
250
+ return (doc.toJS() as unknown) ?? null;
186
251
  } catch {
187
252
  return null;
188
253
  }
@@ -24,8 +24,16 @@
24
24
  * invisible in a source review.
25
25
  * - `hooks/hooks.json` — the host binding file, written by `condense.ts` via
26
26
  * `task sync`. A hand edit here silently unbinds a guard.
27
+ * - `src/scripts/hook_manifest.json` and `src/scripts/hooks/host_lowering.json`
28
+ * — the compiled plumbing tables, written by `compile_hook_manifest`
29
+ * (road-to-blocking-severities 2.2). These are the ones the RUNTIME actually
30
+ * reads: `dispatch_hook._load_yaml` and `host_lowering.resolveTable` serve
31
+ * the compiled body in preference to the YAML whenever its fingerprint
32
+ * matches, so an edited table is what every host is told about which concern
33
+ * runs and which slot can refuse, while the reviewed YAML beside it still
34
+ * says something else.
27
35
  *
28
- * Both are matched under any parent directory, because a consumer install
36
+ * All four are matched under any parent directory, because a consumer install
29
37
  * carries them under its own project root rather than under this repository's.
30
38
  *
31
39
  * WHAT STILL PASSES, and why that is not a hole.
@@ -94,15 +102,35 @@ function _isObject(v: JsonValue | undefined): v is JsonObject {
94
102
  export const PLUMBING_BUILD_OUTPUTS: readonly string[] = [
95
103
  'dist/hooks/dispatch.js',
96
104
  'hooks/hooks.json',
105
+ 'src/scripts/hook_manifest.json',
106
+ 'src/scripts/hooks/host_lowering.json',
97
107
  ];
98
108
 
99
109
  /**
100
110
  * Where each output comes from, printed in the deny so the reader is told what
101
111
  * to run instead of only what not to do.
112
+ *
113
+ * road-to-blocking-severities 2.2 added the two compiled tables. They are the
114
+ * same KIND of file as the first two — written by a build, never by hand — and
115
+ * until this step they were refused by nothing, which mattered more here than
116
+ * it would have for an ordinary generated artifact: `dispatch_hook._load_yaml`
117
+ * and `host_lowering.resolveTable` both serve the compiled body IN PREFERENCE
118
+ * to the YAML whenever its fingerprint matches, so an edited JSON is what every
119
+ * host is told about which concern runs and which slot can refuse.
120
+ *
121
+ * Their YAML sources stay OUT of this set and keep carrying a ratification
122
+ * record instead — a hand edit there is legitimate and is exactly how the
123
+ * plumbing is maintained. The split is the one the guard's header already
124
+ * draws: a deny for files with no legitimate hand edit, a record for files
125
+ * whose hand edits are the point.
102
126
  */
103
127
  const REGENERATED_BY: Readonly<Record<string, string>> = {
104
128
  'dist/hooks/dispatch.js': 'npm run build:hooks',
105
129
  'hooks/hooks.json': 'task sync',
130
+ 'src/scripts/hook_manifest.json':
131
+ './scripts-run src/scripts/compile_hook_manifest --table manifest',
132
+ 'src/scripts/hooks/host_lowering.json':
133
+ './scripts-run src/scripts/compile_hook_manifest --table host-lowering',
106
134
  };
107
135
 
108
136
  // Keys across platforms that carry the tool's target file path.
@@ -31,10 +31,15 @@
31
31
  * "per-host `enforced_by` resolved from the table, not from a host name" means
32
32
  * in practice.
33
33
  *
34
- * Once per session (a latch keyed by session id), so it cannot nag.
34
+ * Once per distinct SEARCH TARGET, capped at five lines per session (step 2.2),
35
+ * so it cannot nag. It was once-per-session until step 2.1 added `Bash`: with
36
+ * shell searches in scope a single line covered the first `grep` of a session
37
+ * and nothing after it, which is the opposite of the problem — the state that
38
+ * matters is the state at the question being asked.
35
39
  * fail_closed: false — every error path returns allow; a context line must
36
40
  * never break a tool call.
37
41
  */
42
+ import { createHash } from 'node:crypto';
38
43
  import fs from 'node:fs';
39
44
  import path from 'node:path';
40
45
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -55,13 +60,127 @@ function isObject(v: unknown): v is JsonObject {
55
60
  interface ToolIntent {
56
61
  isSearch: boolean;
57
62
  isCodeRead: boolean;
63
+ /**
64
+ * What this call is ABOUT — a Grep/Glob pattern, a read path, or the search
65
+ * term of a shell search. The per-target latch key (step 2.2): the same
66
+ * question asked twice is one question, and a hook that answered it once
67
+ * has nothing to add the second time.
68
+ *
69
+ * Empty when the call is neither a search nor a code read, and never the
70
+ * raw command line of an arbitrary Bash call — see {@link bashSearchToken}.
71
+ */
72
+ target: string;
73
+ }
74
+
75
+ /**
76
+ * The shell search heads this hook recognises.
77
+ *
78
+ * A closed list on purpose. "Any Bash command might be a search" is the reading
79
+ * that makes the hook chatty — Risk-Register rank 2 — so a command whose head
80
+ * is not here is silent AND does not latch, exactly as a non-code `Read` is.
81
+ */
82
+ const SEARCH_HEADS = new Set(['grep', 'rg', 'ag', 'find', 'ack', 'fd']);
83
+
84
+ /**
85
+ * Short options that consume the NEXT word as their value.
86
+ *
87
+ * Without this the value is read as the search term: a completion review of
88
+ * step 2.1 measured `grep -A 5 foo` keying the latch on `"5"` and
89
+ * `rg -g *.ts alpha` keying on `"*.ts"` — which also silenced a second,
90
+ * genuinely different search behind the same glob. A latch keyed on an option's
91
+ * argument is worse than no latch: it collapses distinct questions onto one slot.
92
+ */
93
+ const VALUE_TAKING = new Set([
94
+ '-A', '-B', '-C', '-m', '-e', '-f', '-g', '-t', '-T', '-d', '--include',
95
+ '--exclude', '--glob', '--max-count', '--after-context', '--before-context',
96
+ '--context', '--regexp', '--file', '--type', '--maxdepth', '--depth',
97
+ ]);
98
+
99
+ /** Asking a tool for its own usage is not a search, and must not spend a slot. */
100
+ const NOT_A_SEARCH = new Set(['--help', '-h', '--version', '-V', '--usage']);
101
+
102
+ /** `find` carries its subject in `-name`, not in the first positional. */
103
+ const FIND_SUBJECT = new Set(['-name', '-iname', '-path', '-ipath', '-regex']);
104
+
105
+ /**
106
+ * The search term inside ONE pipeline segment, or `null`.
107
+ *
108
+ * Split out from {@link bashSearchToken} so a pipeline can be scanned segment
109
+ * by segment: `cat x | grep needle` is a search, and reading only the head of
110
+ * the whole command missed every one of them. The same review measured that.
111
+ */
112
+ function segmentSearchToken(words: readonly string[]): string | null {
113
+ if (words.length === 0) return null;
114
+ let i = 0;
115
+ let head = (words[0] as string).split('/').pop() as string;
116
+ if (head === 'git') {
117
+ // `git -C <path> grep <term>` is the form this tree's own token-efficiency
118
+ // rule mandates, and a `words[1] === 'grep'` test missed all of it. Scan
119
+ // past git's own options to the subcommand.
120
+ let j = 1;
121
+ while (j < words.length && (words[j] as string).startsWith('-')) {
122
+ j += VALUE_TAKING.has(words[j] as string) || words[j] === '-C' ? 2 : 1;
123
+ }
124
+ if (words[j] !== 'grep') return null;
125
+ head = 'git grep';
126
+ i = j + 1;
127
+ } else if (SEARCH_HEADS.has(head)) {
128
+ i = 1;
129
+ } else {
130
+ return null;
131
+ }
132
+ const rest = words.slice(i);
133
+ if (rest.some((w) => NOT_A_SEARCH.has(w))) return null;
134
+ if (head === 'find') {
135
+ const at = rest.findIndex((w) => FIND_SUBJECT.has(w));
136
+ if (at !== -1 && at + 1 < rest.length) return strip(rest[at + 1] as string);
137
+ }
138
+ for (let k = 0; k < rest.length; k += 1) {
139
+ const w = rest[k] as string;
140
+ if (w.startsWith('-')) {
141
+ if (VALUE_TAKING.has(w)) k += 1; // its value is not the subject
142
+ continue;
143
+ }
144
+ return strip(w);
145
+ }
146
+ return head;
147
+ }
148
+
149
+ function strip(w: string): string {
150
+ return w.replace(/^["']|["']$/g, '');
151
+ }
152
+
153
+ /**
154
+ * The thing a shell search is searching FOR, or `null` when the command is not
155
+ * a structure search at all.
156
+ *
157
+ * Reads EVERY pipeline segment, not just the first: `cat x | grep needle` and
158
+ * `… | rg symbol` are searches, and they are the common shape. The first
159
+ * segment that is a search wins, so one line yields one latch key.
160
+ *
161
+ * `git grep` is the one two-word head and is resolved past git's own options.
162
+ * Failing a positional subject the key is the command's head, which collapses a
163
+ * flag-only search onto one slot rather than leaking a command line into a
164
+ * state file — and the key is stored as a digest in any case
165
+ * ({@link latchKey}).
166
+ */
167
+ export function bashSearchToken(command: string): string | null {
168
+ for (const segment of command.split(/\||&&|;/)) {
169
+ const words = segment.trim().split(/\s+/).filter((w) => w !== '');
170
+ const hit = segmentSearchToken(words);
171
+ if (hit !== null) return hit;
172
+ }
173
+ return null;
58
174
  }
59
175
 
60
176
  /**
61
177
  * Best-effort read of the intercepted tool + its target from the envelope.
62
178
  *
63
- * Carried over from the nudge unchanged, and the manifest's `tools:` filter is
64
- * pinned against exactly this branch surface — Grep, Glob, Read.
179
+ * The manifest's `tools:` filter is pinned against exactly this branch surface
180
+ * — Grep, Glob, Read, Bash. `Bash` joined it in step 2.1 for the reason the
181
+ * step gives: the searches an agent actually runs in this tree are shell
182
+ * searches, so a matcher that saw only the structured search tools was watching
183
+ * the door nobody uses.
65
184
  */
66
185
  export function classifyTool(envelope: JsonObject): ToolIntent {
67
186
  const payload = isObject(envelope['payload']) ? envelope['payload'] : envelope;
@@ -71,13 +190,29 @@ export function classifyTool(envelope: JsonObject): ToolIntent {
71
190
  const ti = (isObject(payload['tool_input']) ? payload['tool_input'] : envelope['tool_input']) as
72
191
  | JsonObject
73
192
  | undefined;
74
- const isSearch = name === 'Grep' || name === 'Glob';
75
- let isCodeRead = false;
193
+ const silent: ToolIntent = { isSearch: false, isCodeRead: false, target: '' };
194
+
195
+ if (name === 'Grep' || name === 'Glob') {
196
+ const pat = isObject(ti) ? (ti['pattern'] ?? ti['query']) : undefined;
197
+ return { isSearch: true, isCodeRead: false, target: typeof pat === 'string' ? pat : name };
198
+ }
76
199
  if (name === 'Read' && isObject(ti)) {
77
200
  const fp = ti['file_path'] ?? ti['path'];
78
- if (typeof fp === 'string') isCodeRead = CODE_EXT.test(fp);
201
+ if (typeof fp === 'string' && CODE_EXT.test(fp)) {
202
+ return { isSearch: false, isCodeRead: true, target: fp };
203
+ }
204
+ return silent;
205
+ }
206
+ if (name === 'Bash' && isObject(ti)) {
207
+ const cmd = ti['command'];
208
+ if (typeof cmd !== 'string') return silent;
209
+ const token = bashSearchToken(cmd);
210
+ // Any other Bash command is silent AND does not latch: it consumed no
211
+ // slot, so the next real search still gets its line.
212
+ if (token === null) return silent;
213
+ return { isSearch: true, isCodeRead: false, target: token };
79
214
  }
80
- return { isSearch, isCodeRead };
215
+ return silent;
81
216
  }
82
217
 
83
218
  function sessionId(envelope: JsonObject): string {
@@ -88,33 +223,117 @@ function sessionId(envelope: JsonObject): string {
88
223
  function latchFile(root: string): string {
89
224
  return path.join(root, 'agents', 'runtime', 'state', 'code-graph-context.json');
90
225
  }
91
- function alreadySpoke(root: string, session: string): boolean {
226
+
227
+ /**
228
+ * At most this many context lines in one session (step 2.2).
229
+ *
230
+ * The cap is the chattiness bound, and it is the half that does not depend on
231
+ * the agent's behaviour: per-target alone is unbounded, because a session that
232
+ * greps thirty distinct symbols would hear thirty lines and the consumer would
233
+ * turn the hook off. Five is a stated default, not a measurement — a session
234
+ * that has been told the graph's state five times has been told.
235
+ */
236
+ export const MAX_CONTEXT_LINES_PER_SESSION = 5;
237
+
238
+ /** How many sessions the latch file retains before evicting the oldest. */
239
+ export const MAX_SESSIONS_LATCHED = 200;
240
+
241
+ /**
242
+ * What the latch file records for a target: a truncated digest, never the
243
+ * target itself.
244
+ *
245
+ * A council review of step 2.1 (2026-10-01, anthropic + openai) asked for one
246
+ * concrete thing before the shell tool was routed here — that command text is
247
+ * neither persisted nor carried into diagnostics. Routing on the shell tool
248
+ * makes the target a search TERM an operator typed, and a term can be a
249
+ * customer name, a token fragment or an internal hostname. Nothing downstream
250
+ * needs to read it back: the latch answers "have I spoken about this one", and
251
+ * equality over a digest answers that exactly as well.
252
+ *
253
+ * Not a security boundary and not claimed as one — a short digest of a short
254
+ * term is guessable by anyone who can enumerate terms. It is the same
255
+ * PII-exclusion-by-construction move `domain-safety-pii` § Surface 2 asks for:
256
+ * the file is shaped so the plaintext is not in it to leak.
257
+ */
258
+ export function latchKey(target: string): string {
259
+ return createHash('sha256').update(target).digest('hex').slice(0, 16);
260
+ }
261
+
262
+ /**
263
+ * Should this target get a line, given what this session already heard?
264
+ *
265
+ * Pure, so the two rules it encodes can be tested without a filesystem: a
266
+ * repeat is silent at any count, and a NEW target is silent once the cap is
267
+ * reached. Takes the target in the clear and hashes here, so no caller can
268
+ * forget to.
269
+ */
270
+ export function speaksFor(spoken: readonly string[], target: string): boolean {
271
+ if (spoken.includes(latchKey(target))) return false;
272
+ return spoken.length < MAX_CONTEXT_LINES_PER_SESSION;
273
+ }
274
+
275
+ function readLatch(root: string): Record<string, string[]> {
92
276
  try {
93
- const state = JSON.parse(fs.readFileSync(latchFile(root), 'utf-8')) as Record<string, boolean>;
94
- return state[session] === true;
277
+ const raw = JSON.parse(fs.readFileSync(latchFile(root), 'utf-8')) as Record<string, unknown>;
278
+ const out: Record<string, string[]> = {};
279
+ for (const [k, v] of Object.entries(raw)) {
280
+ // A pre-2.2 file stored `true` per session. Read it as "this session
281
+ // is done", which is what it meant — never as an empty list, which
282
+ // would hand an upgraded session a fresh budget it already spent.
283
+ if (Array.isArray(v)) out[k] = v.filter((x): x is string => typeof x === 'string');
284
+ else if (v === true) out[k] = Array.from({ length: MAX_CONTEXT_LINES_PER_SESSION }, (_, i) => `legacy:${String(i)}`);
285
+ }
286
+ return out;
95
287
  } catch {
96
- return false;
288
+ return {};
97
289
  }
98
290
  }
99
- function latch(root: string, session: string): void {
291
+
292
+ /**
293
+ * The cheap read-only half, so the `git status` probe behind `graphState` runs
294
+ * only for a call that could still produce a line (Risk-Register rank 3).
295
+ *
296
+ * Advisory only — {@link latchTarget} re-reads and remains the authority. A
297
+ * disagreement between the two can only come from a concurrent write, and it
298
+ * resolves toward silence, which is the safe direction for a context line.
299
+ */
300
+ export function wouldSpeak(root: string, session: string, target: string): boolean {
301
+ return speaksFor(readLatch(root)[session] ?? [], target);
302
+ }
303
+
304
+ /**
305
+ * Decide and record in one step, because the two must not drift apart: a reader
306
+ * that says yes and a writer that records something else is how a cap leaks.
307
+ * Returns whether the caller may speak.
308
+ */
309
+ export function latchTarget(root: string, session: string, target: string): boolean {
310
+ const state = readLatch(root);
311
+ const spoken = state[session] ?? [];
312
+ if (!speaksFor(spoken, target)) return false;
100
313
  try {
101
- const p = latchFile(root);
102
- let state: Record<string, boolean> = {};
103
- try {
104
- state = JSON.parse(fs.readFileSync(p, 'utf-8')) as Record<string, boolean>;
105
- } catch {
106
- /* fresh */
314
+ state[session] = [...spoken, latchKey(target)];
315
+ // Nothing prunes a finished session, so without this the file
316
+ // accumulated up to five digests per session id for the life of the
317
+ // repository — a review's finding, and the sibling instrument added in
318
+ // the same change caps itself and says why local-and-gitignored is not
319
+ // the same as free. Insertion order is the eviction order: the oldest
320
+ // session is the one least likely to speak again, and re-latching a
321
+ // dropped session costs one extra line, never a wrong answer.
322
+ const ids = Object.keys(state);
323
+ for (const old of ids.slice(0, Math.max(0, ids.length - MAX_SESSIONS_LATCHED))) {
324
+ if (old !== session) delete state[old];
107
325
  }
108
- state[session] = true;
326
+ const p = latchFile(root);
109
327
  fs.mkdirSync(path.dirname(p), { recursive: true });
110
328
  fs.writeFileSync(p, JSON.stringify(state));
111
329
  } catch {
112
330
  /* fail-open: a persistence failure must not break the tool call */
113
331
  }
332
+ return true;
114
333
  }
115
334
 
116
335
  /**
117
- * The three-state staleness token this hook reports.
336
+ * The four-state staleness token this hook reports.
118
337
  *
119
338
  * Re-exported, not defined here: Phase 3's verbs must print the same token, and
120
339
  * an engine module importing this hook to learn it would invert the dependency
@@ -139,6 +358,13 @@ export function contextLine(state: GraphState): string {
139
358
  'grep is the other path — say which one answered.'
140
359
  );
141
360
  }
361
+ if (state === 'edited') {
362
+ return (
363
+ 'code-graph: level with HEAD, but indexed files are edited in the working tree. ' +
364
+ 'Relationship answers miss anything written since the last build — ' +
365
+ '`agent-config code-graph refresh` picks the edits up, or use grep and say so.'
366
+ );
367
+ }
142
368
  const n = state.slice('behind:'.length);
143
369
  return (
144
370
  `code-graph: ${n} commit(s) behind. Relationship answers may miss anything newer — ` +
@@ -160,11 +386,14 @@ export function main(): number {
160
386
  const pr = envelope['workspace_root'] ?? envelope['project_root'];
161
387
  const root = typeof cwd === 'string' && cwd ? cwd : typeof pr === 'string' && pr ? pr : '.';
162
388
 
163
- const { isSearch, isCodeRead } = classifyTool(envelope);
389
+ const { isSearch, isCodeRead, target } = classifyTool(envelope);
164
390
  if (!isSearch && !isCodeRead) return EXIT_ALLOW;
165
391
 
166
392
  const session = sessionId(envelope);
167
- if (alreadySpoke(root, session)) return EXIT_ALLOW; // once per session
393
+ // Once per distinct target, at most five per session (2.2). Checked BEFORE
394
+ // `graphState`, because that call now runs a `git status` probe and a line
395
+ // this session will not emit must not pay for one.
396
+ if (!wouldSpeak(root, session, target)) return EXIT_ALLOW;
168
397
 
169
398
  let state: GraphState;
170
399
  try {
@@ -176,7 +405,7 @@ export function main(): number {
176
405
  // so a session that builds a graph mid-flight still gets the line once.
177
406
  if (state === 'absent') return EXIT_ALLOW;
178
407
 
179
- latch(root, session);
408
+ if (!latchTarget(root, session, target)) return EXIT_ALLOW;
180
409
  process.stdout.write(`${JSON.stringify({ decision: 'warn', reason: contextLine(state) })}\n`);
181
410
  return EXIT_WARN;
182
411
  }