@event4u/agent-config 16.2.0 → 16.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +89 -237
  5. package/README.md +14 -12
  6. package/dist/agent-src/commands/analyze/inbox.md +1 -1
  7. package/dist/agent-src/commands/analyze/roadmap-repos.md +1 -1
  8. package/dist/agent-src/commands/bug/fix.md +17 -2
  9. package/dist/agent-src/commands/feature/plan.md +1 -1
  10. package/dist/agent-src/commands/feature/roadmap.md +1 -1
  11. package/dist/agent-src/commands/fix/ci.md +1 -1
  12. package/dist/agent-src/commands/implement-ticket.md +1 -1
  13. package/dist/agent-src/commands/jira-ticket.md +1 -1
  14. package/dist/agent-src/commands/roadmap/create.md +1 -1
  15. package/dist/agent-src/commands/roadmap/materialize.md +1 -1
  16. package/dist/agent-src/commands/roadmap/process-full.md +4 -4
  17. package/dist/agent-src/commands/roadmap/resolve-blockers.md +361 -0
  18. package/dist/agent-src/commands/roadmap/triage-parked.md +325 -0
  19. package/dist/agent-src/commands/roadmap.md +18 -4
  20. package/dist/agent-src/commands/tests/create.md +17 -3
  21. package/dist/agent-src/commands/tests/execute.md +22 -5
  22. package/dist/agent-src/contexts/authority/decision-revisit-gate-enforcement.md +43 -0
  23. package/dist/agent-src/contexts/communication/rules-auto/design-fidelity-enforcement.md +24 -0
  24. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +6 -6
  25. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +32 -2
  26. package/dist/agent-src/contexts/execution/session-canary-enforcement-history.md +67 -0
  27. package/dist/agent-src/contexts/execution/toolchain-resolver.md +101 -7
  28. package/dist/agent-src/rules/active-remediation.md +1 -1
  29. package/dist/agent-src/rules/autonomous-execution.md +1 -0
  30. package/dist/agent-src/rules/broken-access-control.md +1 -0
  31. package/dist/agent-src/rules/command-suggestion-policy.md +1 -0
  32. package/dist/agent-src/rules/context-hygiene.md +1 -1
  33. package/dist/agent-src/rules/decision-revisit-gate.md +16 -33
  34. package/dist/agent-src/rules/design-fidelity.md +13 -16
  35. package/dist/agent-src/rules/doc-screenshot-hygiene.md +1 -0
  36. package/dist/agent-src/rules/domain-safety-disclaimer.md +1 -0
  37. package/dist/agent-src/rules/domain-safety-pii.md +1 -0
  38. package/dist/agent-src/rules/domain-safety-retention.md +1 -0
  39. package/dist/agent-src/rules/engineering-safety-floor.md +1 -0
  40. package/dist/agent-src/rules/evaluator-independence.md +3 -2
  41. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
  42. package/dist/agent-src/rules/finance-safety-floor.md +1 -0
  43. package/dist/agent-src/rules/git-history-discipline.md +1 -0
  44. package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
  45. package/dist/agent-src/rules/legal-safety-floor.md +3 -2
  46. package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
  47. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  48. package/dist/agent-src/rules/media-governance-routing.md +14 -17
  49. package/dist/agent-src/rules/persona-governance.md +2 -2
  50. package/dist/agent-src/rules/provider-lifecycle-discipline.md +1 -1
  51. package/dist/agent-src/rules/question-not-instruction.md +1 -0
  52. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  53. package/dist/agent-src/rules/runtime-safety.md +1 -0
  54. package/dist/agent-src/rules/secret-vcs-guard.md +1 -0
  55. package/dist/agent-src/rules/security-sensitive-stop.md +3 -2
  56. package/dist/agent-src/rules/session-canary.md +8 -62
  57. package/dist/agent-src/rules/settings-ask-protocol.md +1 -0
  58. package/dist/agent-src/rules/source-confidentiality.md +1 -0
  59. package/dist/agent-src/rules/strategy-safety-floor.md +1 -0
  60. package/dist/agent-src/rules/tool-safety.md +1 -0
  61. package/dist/agent-src/rules/untrusted-input-defense.md +1 -0
  62. package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
  63. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
  64. package/dist/agent-src/scripts/deferral_carry.ts +339 -0
  65. package/dist/agent-src/scripts/update_roadmap_progress.ts +4 -1
  66. package/dist/agent-src/skills/api-design/data/manifest.json +2 -2
  67. package/dist/agent-src/skills/api-testing/SKILL.md +17 -0
  68. package/dist/agent-src/skills/code-intelligence/SKILL.md +14 -2
  69. package/dist/agent-src/skills/design-system-capture/references/import-procedure.md +1 -1
  70. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +2 -2
  71. package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
  72. package/dist/agent-src/skills/iconography/SKILL.md +1 -1
  73. package/dist/agent-src/skills/js-library-packaging/SKILL.md +2 -2
  74. package/dist/agent-src/skills/monorepo-workspace/SKILL.md +1 -1
  75. package/dist/agent-src/skills/quality-tools/SKILL.md +11 -1
  76. package/dist/agent-src/skills/quality-tools/references/go-tools.md +76 -0
  77. package/dist/agent-src/skills/quality-tools/references/python-tools.md +104 -0
  78. package/dist/agent-src/skills/review-routing/SKILL.md +2 -2
  79. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  80. package/dist/agent-src/skills/storybook-workshop/SKILL.md +3 -3
  81. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  82. package/dist/agent-src/skills/test-case-discovery/SKILL.md +7 -0
  83. package/dist/agent-src/skills/test-driven-development/SKILL.md +17 -0
  84. package/dist/agent-src/skills/test-performance/SKILL.md +19 -1
  85. package/dist/agent-src/skills/ui-component-architect/SKILL.md +2 -2
  86. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  87. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
  88. package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
  89. package/dist/cli/registry.js +1 -1
  90. package/dist/cli-delegate/archive_completed_roadmaps.js +92 -12
  91. package/dist/cli-delegate/check_update_banner.js +12 -1
  92. package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
  93. package/dist/cli-delegate/{chunk-M2DPGFZ4.js → chunk-6V6TA25H.js} +22 -152
  94. package/dist/cli-delegate/chunk-7IQE3D5S.js +191 -0
  95. package/dist/cli-delegate/{chunk-6KFEC3G7.js → chunk-CQWX336L.js} +1 -1
  96. package/dist/cli-delegate/chunk-DEIV7XP7.js +108 -0
  97. package/dist/cli-delegate/chunk-EEHDXSYD.js +2040 -0
  98. package/dist/cli-delegate/{chunk-P6ZHP5W2.js → chunk-GKE7ZVFA.js} +4 -4
  99. package/dist/cli-delegate/chunk-JQOM7TTM.js +1201 -0
  100. package/dist/cli-delegate/{chunk-IGD6ERX6.js → chunk-JQVYKIZA.js} +10 -0
  101. package/dist/cli-delegate/{chunk-FC75PGJE.js → chunk-JY2I2DXV.js} +12 -1
  102. package/dist/cli-delegate/chunk-KB5FZ655.js +208 -0
  103. package/dist/cli-delegate/{chunk-7RQP4UDH.js → chunk-KR6EM23A.js} +235 -1455
  104. package/dist/cli-delegate/{chunk-LTYVIY76.js → chunk-LIYTLH6O.js} +1 -1
  105. package/dist/cli-delegate/{chunk-YJ72UIAK.js → chunk-NQ7WT43P.js} +1186 -244
  106. package/dist/cli-delegate/chunk-NXEILF2K.js +2160 -0
  107. package/dist/cli-delegate/{chunk-5R7HOLAF.js → chunk-NYX3L2PZ.js} +1 -1
  108. package/dist/cli-delegate/{chunk-4E2W63DA.js → chunk-OKJUIAPS.js} +62 -1
  109. package/dist/cli-delegate/{chunk-QAJS5YWY.js → chunk-PHD7OOLE.js} +2 -1
  110. package/dist/cli-delegate/{chunk-RGNIWTEA.js → chunk-QLDHIPWR.js} +1 -1
  111. package/dist/cli-delegate/{chunk-7XGBX6ZH.js → chunk-RQNNW7IF.js} +1314 -560
  112. package/dist/cli-delegate/{chunk-4XFP2Y3E.js → chunk-S4UMZNES.js} +1 -1
  113. package/dist/cli-delegate/{chunk-6KM6ORRY.js → chunk-S4YWNRKO.js} +24 -8
  114. package/dist/cli-delegate/{chunk-FP36SWJD.js → chunk-TRI6DQZ6.js} +2781 -340
  115. package/dist/cli-delegate/chunk-ULQRUR23.js +208 -0
  116. package/dist/cli-delegate/{chunk-NUHLI2RX.js → chunk-YCU3TIXH.js} +2 -0
  117. package/dist/cli-delegate/chunk-ZBZV2G5R.js +210 -0
  118. package/dist/cli-delegate/{chunk-WGXTVKCQ.js → chunk-ZUZKZYCN.js} +5 -3
  119. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  120. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  121. package/dist/cli-delegate/cmd_conformance.js +1167 -234
  122. package/dist/cli-delegate/cmd_doctor.js +13 -6
  123. package/dist/cli-delegate/cmd_doctor_neighbours.js +23 -0
  124. package/dist/cli-delegate/cmd_explain.js +5 -5
  125. package/dist/cli-delegate/cmd_export.js +11 -8
  126. package/dist/cli-delegate/cmd_fleet.js +3 -3
  127. package/dist/cli-delegate/cmd_handoff.js +10 -7
  128. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  129. package/dist/cli-delegate/cmd_memory_get.js +3 -1
  130. package/dist/cli-delegate/cmd_migrate.js +2 -2
  131. package/dist/cli-delegate/cmd_packs_active.js +3 -3
  132. package/dist/cli-delegate/cmd_preflight.js +6 -6
  133. package/dist/cli-delegate/cmd_prune.js +11 -8
  134. package/dist/cli-delegate/cmd_refresh.js +2 -1
  135. package/dist/cli-delegate/cmd_route_audit.js +8 -6
  136. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  137. package/dist/cli-delegate/cmd_session_recycle.js +4 -3
  138. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  139. package/dist/cli-delegate/cmd_settings_migrate.js +11 -8
  140. package/dist/cli-delegate/cmd_settings_set.js +4 -4
  141. package/dist/cli-delegate/cmd_sync.js +11 -8
  142. package/dist/cli-delegate/cmd_uninstall.js +11 -8
  143. package/dist/cli-delegate/cmd_update.js +1 -1
  144. package/dist/cli-delegate/cmd_upgrade.js +3 -2
  145. package/dist/cli-delegate/cmd_validate.js +11 -8
  146. package/dist/cli-delegate/cmd_versions.js +1 -1
  147. package/dist/cli-delegate/deferral_carry.js +16 -0
  148. package/dist/cli-delegate/gate_execute.js +2 -2
  149. package/dist/cli-delegate/roadmap_gates.js +5 -5
  150. package/dist/cli-delegate/update_roadmap_progress.js +1 -1
  151. package/dist/discovery/deprecation-report.md +1 -1
  152. package/dist/discovery/discovery-manifest.json +127 -75
  153. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  154. package/dist/discovery/discovery-manifest.summary.md +3 -3
  155. package/dist/discovery/dormancy-report.md +1 -1
  156. package/dist/discovery/orphan-report.md +1 -1
  157. package/dist/discovery/packs.json +7 -5
  158. package/dist/discovery/trust-report.md +2 -2
  159. package/dist/discovery/workspaces.json +6 -4
  160. package/dist/hooks/dispatch.js +5357 -6656
  161. package/dist/hooks/dispatch.sha256 +1 -1
  162. package/dist/install/claudePathsPlan.js +47 -6
  163. package/dist/install/claudeRuleRewrite.js +7 -0
  164. package/dist/install/emit_host_rules_cli.js +39 -36
  165. package/dist/install/globalRuleScope.js +87 -0
  166. package/dist/install/host_rule_ownership.js +160 -0
  167. package/dist/install/install.mjs +2080 -1024
  168. package/dist/install/installThinLayer.js +261 -0
  169. package/dist/install/installedRuleLinks.js +176 -0
  170. package/dist/install/wizard-plan.js +24 -0
  171. package/dist/mcp/registry-manifest.json +3 -3
  172. package/dist/mcp/server.json +2 -2
  173. package/dist/mcp/server.mjs +31965 -27154
  174. package/dist/scripts/_lib/agent_settings.js +11 -0
  175. package/dist/scripts/_lib/claude_settings_hooks.js +10 -8
  176. package/dist/scripts/_lib/installed_layer.js +378 -0
  177. package/dist/scripts/_lib/lean_projection_mode.js +218 -15
  178. package/dist/scripts/_lib/rule_consequence_class.js +45 -0
  179. package/dist/scripts/_lib/rule_injection.js +67 -10
  180. package/dist/scripts/_lib/rule_law_section.js +122 -0
  181. package/dist/scripts/_lib/thin_rules.js +458 -0
  182. package/dist/scripts/_lib/windsurf_render.js +60 -0
  183. package/dist/scripts/condense.js +14 -26
  184. package/dist/scripts/project_thin_rules.js +14 -294
  185. package/dist/server/schemas/settings.js +6 -0
  186. package/dist/shared/skillRanking.js +74 -13
  187. package/docs/contracts/settings-classes.md +7 -5
  188. package/package.json +1 -1
  189. package/src/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
  190. package/src/agent-src/scripts/deferral_carry.ts +339 -0
  191. package/src/agent-src/scripts/update_roadmap_progress.ts +4 -1
  192. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
  193. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
  194. package/src/config/agent-settings.template.yml +62 -22
  195. package/src/config/continuity-surface.json +7 -0
  196. package/src/config/evaluator-budgets.json +4 -4
  197. package/src/config/gate-coverage.yml +96 -3
  198. package/src/config/gate-violation-baselines.json +17 -9
  199. package/src/config/hook-bundle-budget.json +25 -0
  200. package/src/config/hook-token-budget.json +6 -1
  201. package/src/config/host-instruction-limits.json +33 -0
  202. package/src/config/routing-coverage-seed.json +24 -3
  203. package/src/config/rule-activation-census.json +7 -3
  204. package/src/config/rule-consequence-class.json +161 -0
  205. package/src/config/rule-law-ceilings.json +99 -0
  206. package/src/install/claudePathsPlan.ts +46 -4
  207. package/src/install/claudeRuleRewrite.ts +10 -1
  208. package/src/install/emit_host_rules_cli.ts +43 -34
  209. package/src/install/globalRuleScope.ts +109 -0
  210. package/src/install/host_rule_ownership.ts +170 -0
  211. package/src/install/installThinLayer.ts +312 -0
  212. package/src/install/installedRuleLinks.ts +220 -0
  213. package/src/install/wizard-plan.ts +24 -0
  214. package/src/scripts/_cli/cmd_doctor.ts +23 -71
  215. package/src/scripts/_cli/cmd_doctor_neighbours.ts +293 -0
  216. package/src/scripts/_dispatch.bash +16 -2
  217. package/src/scripts/_lib/agent_settings.ts +11 -0
  218. package/src/scripts/_lib/claude_settings_hooks.ts +10 -8
  219. package/src/scripts/_lib/estate_offsets.ts +241 -0
  220. package/src/scripts/_lib/exemption_shape.ts +143 -0
  221. package/src/scripts/_lib/fnv.ts +23 -0
  222. package/src/scripts/_lib/foreign_scan.ts +106 -0
  223. package/src/scripts/_lib/gitignore_block.ts +314 -0
  224. package/src/scripts/_lib/global_deploy_inventory.ts +32 -0
  225. package/src/scripts/_lib/graph_feeder_record.ts +214 -0
  226. package/src/scripts/_lib/hook_settings.ts +73 -0
  227. package/src/scripts/_lib/host_capability.ts +40 -19
  228. package/src/scripts/_lib/host_hook_merge.ts +165 -0
  229. package/src/scripts/_lib/installed_layer.ts +538 -0
  230. package/src/scripts/_lib/json_merge.ts +82 -0
  231. package/src/scripts/_lib/json_pointers.ts +93 -11
  232. package/src/scripts/_lib/lean_projection_mode.ts +275 -15
  233. package/src/scripts/_lib/machine_wake.ts +16 -3
  234. package/src/scripts/_lib/neighbour_census.ts +760 -0
  235. package/src/scripts/_lib/neighbour_scan.ts +122 -0
  236. package/src/scripts/_lib/neighbour_tool_use.ts +189 -0
  237. package/src/scripts/_lib/prompt_shape.ts +46 -0
  238. package/src/scripts/_lib/release_migration_gate.ts +19 -3
  239. package/src/scripts/_lib/reserved_name_sweep.ts +73 -0
  240. package/src/scripts/_lib/review_coverage.ts +134 -0
  241. package/src/scripts/_lib/rule_consequence_class.ts +71 -0
  242. package/src/scripts/_lib/rule_injection.ts +130 -10
  243. package/src/scripts/_lib/rule_law_section.ts +147 -0
  244. package/src/scripts/_lib/rule_layer_overlap.ts +58 -0
  245. package/src/scripts/_lib/skill_origin.ts +254 -0
  246. package/src/scripts/_lib/structured_ask.ts +64 -21
  247. package/src/scripts/_lib/thin_rules.ts +540 -0
  248. package/src/scripts/_lib/touched_file_quality.ts +434 -0
  249. package/src/scripts/_lib/turn_end_refusals.ts +77 -2
  250. package/src/scripts/_lib/windsurf_render.ts +63 -0
  251. package/src/scripts/ai_council/cli_call_budget.ts +13 -5
  252. package/src/scripts/ai_council/clients.ts +26 -57
  253. package/src/scripts/ai_council/config.ts +1 -1
  254. package/src/scripts/ai_council/vendor_defaults.ts +45 -0
  255. package/src/scripts/audit_skill_overlap.ts +261 -0
  256. package/src/scripts/before_complete_hook.ts +243 -12
  257. package/src/scripts/bench_touched_file_quality.ts +238 -0
  258. package/src/scripts/check_enforcement_matrix.ts +377 -20
  259. package/src/scripts/check_estate_count.ts +130 -153
  260. package/src/scripts/check_finding_dispositions.ts +116 -4
  261. package/src/scripts/check_hook_bundle_composition.ts +370 -0
  262. package/src/scripts/check_kernel_edit_ratified.ts +49 -11
  263. package/src/scripts/check_memory.ts +7 -1
  264. package/src/scripts/check_release_highlights.ts +10 -0
  265. package/src/scripts/check_release_pr_shape.ts +38 -15
  266. package/src/scripts/check_standing_rule_delivery.ts +33 -13
  267. package/src/scripts/check_verify_expectation_delta.ts +521 -0
  268. package/src/scripts/code_graph/detect.ts +127 -7
  269. package/src/scripts/code_graph/foreign.ts +194 -0
  270. package/src/scripts/code_graph/query.ts +217 -4
  271. package/src/scripts/code_graph/types.ts +26 -2
  272. package/src/scripts/code_graph/validate.ts +3 -1
  273. package/src/scripts/code_graph/verbs.ts +103 -0
  274. package/src/scripts/compile_hook_manifest.ts +33 -1
  275. package/src/scripts/condense.ts +14 -27
  276. package/src/scripts/estate_exemption_shape.ts +202 -0
  277. package/src/scripts/generate_host_cost_table.ts +1 -1
  278. package/src/scripts/hook_manifest.json +1 -1
  279. package/src/scripts/hook_manifest.yaml +150 -9
  280. package/src/scripts/hooks/block_config_weakening.ts +67 -2
  281. package/src/scripts/hooks/block_plumbing_writes.ts +29 -1
  282. package/src/scripts/hooks/code_graph_context_hook.ts +252 -23
  283. package/src/scripts/hooks/concern_failure_policy.ts +280 -0
  284. package/src/scripts/hooks/dispatch_hook.ts +52 -52
  285. package/src/scripts/hooks/exit_codes.ts +2 -1
  286. package/src/scripts/hooks/host_lowering.json +1 -1
  287. package/src/scripts/hooks/host_lowering.ts +19 -1
  288. package/src/scripts/hooks/one_question_per_ask_hook.ts +41 -25
  289. package/src/scripts/hooks/payload_stub.ts +10 -1
  290. package/src/scripts/hooks/rule_inject_hook.ts +757 -63
  291. package/src/scripts/hooks/table_fingerprint.ts +21 -0
  292. package/src/scripts/hooks/telemetry_usage_hook.ts +64 -8
  293. package/src/scripts/hooks/turn_end_gate_hook.ts +84 -0
  294. package/src/scripts/hooks_status.ts +112 -0
  295. package/src/scripts/injection_scan_hook.ts +7 -0
  296. package/src/scripts/install.ts +72 -185
  297. package/src/scripts/installed_layer_report.ts +103 -0
  298. package/src/scripts/language_mirror_hook.ts +15 -6
  299. package/src/scripts/learning_sidecar.ts +119 -8
  300. package/src/scripts/lint_hook_manifest.ts +53 -0
  301. package/src/scripts/lint_major_migration_sections.ts +156 -26
  302. package/src/scripts/lint_pack_boundaries.ts +63 -5
  303. package/src/scripts/lint_rule_law_section.ts +517 -0
  304. package/src/scripts/lint_workflow_security.ts +161 -5
  305. package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -0
  306. package/src/scripts/mcp_server/graph_tools.ts +72 -3
  307. package/src/scripts/measure_routing_signal.ts +54 -16
  308. package/src/scripts/measure_skill_ranker_baseline.ts +216 -8
  309. package/src/scripts/memory_learn_hook.ts +91 -6
  310. package/src/scripts/model_rule_injection.ts +52 -5
  311. package/src/scripts/project_thin_rules.ts +34 -327
  312. package/src/scripts/report_evidence_temperature.ts +657 -0
  313. package/src/scripts/report_installed_rule_links.ts +157 -0
  314. package/src/scripts/report_label_agreement.ts +233 -0
  315. package/src/scripts/report_obligation_mechanism.ts +230 -0
  316. package/src/scripts/report_single_token_triggers.ts +277 -0
  317. package/src/scripts/report_skill_ranker_confusion.ts +442 -0
  318. package/src/scripts/report_standing_payload_by_host.ts +188 -15
  319. package/src/scripts/report_verification_record_coverage.ts +306 -0
  320. package/src/scripts/rule_activation_census.ts +5 -2
  321. package/src/scripts/rule_trigger_eval.ts +9 -9
  322. package/src/scripts/schemas/review-findings.schema.json +3 -2
  323. package/src/scripts/schemas/rule.schema.json +5 -0
  324. package/src/scripts/self_review_gate.ts +62 -7
  325. package/src/scripts/skill_tools/score_skill_relevance.ts +290 -25
  326. package/src/scripts/sweep_skill_ranker_signals.ts +129 -0
  327. package/src/scripts/sync_gitignore.ts +29 -235
  328. package/src/scripts/trigger_eval_grandfather.json +0 -5
  329. package/src/server/schemas/settings.ts +10 -0
  330. package/src/shared/skillRanking.ts +117 -11
  331. package/dist/cli-delegate/chunk-JNMVLWLR.js +0 -4457
  332. package/dist/cli-delegate/chunk-YDPILCOL.js +0 -152
@@ -0,0 +1,280 @@
1
+ /**
2
+ * What a concern's DECLARED SEVERITY means when the concern produced no verdict.
3
+ *
4
+ * Extracted from `dispatch_hook.ts` on 2026-10-03, and not for tidiness.
5
+ * `check_source_size_budget` measured that file at exactly 1,500 lines on
6
+ * `main` — sitting precisely on the cap — and step 3.3's additions put it 203
7
+ * lines over. The ratchet turns one way, and the doctrine every entry in
8
+ * `gate-violation-baselines.json` records is that a MOVE gives the budget back
9
+ * where raising the baseline spends it. The same move
10
+ * `_lib/concern_sla_window.ts` made out of `bench_hook_latency.ts`.
11
+ *
12
+ * It is also the better boundary on its own terms. Everything here answers one
13
+ * question — what a concern's declaration permits and requires when the
14
+ * concern itself said nothing — and none of it is about RUNNING a concern,
15
+ * which is what the dispatcher does. `stdin_failure_policy.ts` is its sibling:
16
+ * the same question for a payload the dispatcher could not read.
17
+ *
18
+ * The dispatcher re-exports every symbol here, so callers and tests that
19
+ * imported them from `dispatch_hook.js` are unaffected.
20
+ */
21
+ import { EXIT_ALLOW, EXIT_BLOCK, EXIT_ERROR } from './exit_codes.js';
22
+
23
+ type JsonObject = Record<string, unknown>;
24
+
25
+ /**
26
+ * P0.2 (road-to-rule-coherence) — is this concern declared advisory?
27
+ *
28
+ * An advisory concern MUST never produce a BLOCK verdict on any host. Four
29
+ * PreToolUse concerns document themselves as advisory in prose
30
+ * (`design_slop_hook`: "FLAGS, NEVER A BLOCK") while the transport happily
31
+ * turned their WARN into a host-level deny. Prose is not enforcement: the
32
+ * manifest now declares severity and the dispatcher enforces the ceiling.
33
+ */
34
+ export function _is_advisory(concern: JsonObject): boolean {
35
+ return String(concern["severity"] ?? "").trim().toLowerCase() === "advisory";
36
+ }
37
+
38
+ /**
39
+ * Is this concern declared `severity: blocking`?
40
+ *
41
+ * The positive twin of `_is_advisory`, and NOT its negation. A concern whose
42
+ * `severity` is absent, misspelt or any third value is neither — and step 3.3
43
+ * below promotes a crash to a refusal only on an EXPLICIT `blocking`, so an
44
+ * undeclared concern keeps the historical fail-open rather than inheriting a
45
+ * refusal from a typo. `lint_hook_manifest` requires the key, so the middle
46
+ * case should not exist; this reads it as the safe direction anyway, because
47
+ * the cost of the two mistakes is not symmetric.
48
+ */
49
+ export function _is_blocking(concern: JsonObject): boolean {
50
+ return String(concern["severity"] ?? "").trim().toLowerCase() === "blocking";
51
+ }
52
+
53
+ /**
54
+ * What an `rc >= 3` (crash / could-not-decide) becomes — step 3.3 of
55
+ * `road-to-a-kernel-that-guards-its-plumbing`.
56
+ *
57
+ * WHAT CHANGED, AND WHAT AUTHORISES IT.
58
+ * Until this step the resolution read the `fail_closed:` FLAG: a crash blocked
59
+ * when the concern opted in and allowed otherwise. Three of nine
60
+ * `severity: blocking` concerns carried the flag, so six guards declared
61
+ * blocking in the manifest — the declaration that AUTHORISES a refusal, and the
62
+ * one `_is_advisory` already enforces as a ceiling — and then allowed the call
63
+ * through whenever they crashed. A guard that refuses when it works and permits
64
+ * when it breaks is not a guard: a crash is exactly the moment the check did
65
+ * not happen, so it is the moment the answer matters most.
66
+ *
67
+ * So `severity` now decides both directions. It is the ceiling (an advisory
68
+ * concern may never block) AND the floor (a blocking concern that could not
69
+ * decide refuses). `fail_closed:` is no longer consulted on this branch — all
70
+ * three concerns carrying it are `severity: blocking`, so no verdict moves
71
+ * because it stopped being read here.
72
+ *
73
+ * WHAT A SLOW CONCERN IS NOT, ON EITHER PATH.
74
+ * This branch resolves a concern that produced NO VERDICT. It is not a latency
75
+ * gate and no reading of `concern_sla_ms` reaches it. On the default in-process
76
+ * route a kill-timeout cannot preempt synchronous code, so an overrun there is
77
+ * a POST-HOC observation about a concern that did in fact answer — refusing it
78
+ * afterwards would deny a call whose guard passed, buying no enforcement while
79
+ * spending exactly the availability risk Risk 1 of the owning roadmap holds
80
+ * down. On the spawn route the timeout stays the historical 30 s, because the
81
+ * registered SLA measures in-process concern work and is three to fifteen times
82
+ * smaller than interpreter startup alone; `SPAWN_TIMEOUT_MS` carries that
83
+ * measurement and the probe that refused the substitution.
84
+ *
85
+ * So `sla_ms x 3` remains what the warn-only window made it: a number the bench
86
+ * prints beside each measured p95, gating nothing. Its readings are recorded
87
+ * with the owning plan and no count is repeated here — a review of an earlier
88
+ * revision found two different tallies in one diff, because a number had been
89
+ * pasted into an implementation comment while the record kept accumulating.
90
+ * The readings validate the bound as an OBSERVATION, which is the only thing
91
+ * this step asks of them.
92
+ *
93
+ * The discriminator for this branch is whether a VERDICT EXISTS. A crash and a
94
+ * killed spawn leave none, so they fail closed. A slow success leaves one, so
95
+ * it is honoured, however slow it was.
96
+ *
97
+ * THE ONE-RETRY ESCAPE, AND WHY THE STOP SLOT NEEDS IT.
98
+ * `turn-end-gate` and `run-continuation` are `severity: blocking` on `stop`,
99
+ * and `tests/hooks/concern_severity.test.ts` records the hazard in as many
100
+ * words: a turn-end gate that fails closed "does not degrade, it wedges the
101
+ * session". Neither carries `skip_on_refusal_retry`, so without this clause a
102
+ * DETERMINISTIC crash in either would refuse every Stop, indefinitely, with no
103
+ * escape the user can reach.
104
+ *
105
+ * So the promotion is spent ONCE. On the retry the host itself marks
106
+ * (`stop_hook_active`, read by `_is_refusal_retry`) a crash falls back to
107
+ * fail-open and the turn ends. The legitimate refusal path is untouched: a
108
+ * concern that DECIDED to refuse returns `EXIT_BLOCK` and never reaches this
109
+ * branch. What a broken concern loses is only the power to refuse forever on
110
+ * the strength of being broken.
111
+ *
112
+ * `pre_tool_use` is untouched by the clause — `_is_refusal_retry` is false on
113
+ * every event but `stop` — and that asymmetry is deliberate. A crashing
114
+ * tool-call guard refuses every attempt, which is the right answer there: the
115
+ * user can run a different command and nothing is wedged, whereas a turn that
116
+ * cannot end leaves no move at all.
117
+ */
118
+ export function _resolve_execution_failure(
119
+ concern: JsonObject,
120
+ refusal_retry: boolean,
121
+ ): number {
122
+ if (!_is_blocking(concern)) return EXIT_ALLOW;
123
+ if (refusal_retry) return EXIT_ALLOW;
124
+ return EXIT_BLOCK;
125
+ }
126
+
127
+ /**
128
+ * The spawn kill-timeout, and why `concern_sla_ms` is NOT what sets it.
129
+ *
130
+ * Step 3.3 of `road-to-a-kernel-that-guards-its-plumbing` reads "timeout for a
131
+ * blocking concern becomes `sla_ms x 3`". That clause is NOT landed, and the
132
+ * reason is a unit mismatch that is measurable rather than arguable.
133
+ *
134
+ * `concern_sla_ms` is derived from the per-concern `duration_ms` this file
135
+ * reports into the timings sink, which on the in-process route brackets
136
+ * `main_fn(argsList)` and nothing else — the concern's OWN work. The registered
137
+ * rows are 0.564 to 1.587 ms, so `sla_ms x 3` is 1.7 to 4.8 ms.
138
+ *
139
+ * A `spawnSync` timeout bounds something else entirely: fork, interpreter
140
+ * start, module graph load, and only then the same work. The bench's own
141
+ * control row measures the interpreter term alone at p95 17 ms on the 1 vCPU
142
+ * reference class and 26 ms on the GitHub runner — three to fifteen times the
143
+ * whole proposed bound, before the concern has run a line.
144
+ *
145
+ * Wiring it was tried on this branch and probed against the real dispatcher.
146
+ * With `AGENT_CONFIG_HOOKS_ISOLATED=1` on `claude/pre_tool_use`, all six
147
+ * blocking concerns returned `ETIMEDOUT`, left no verdict, and — resolved by
148
+ * `_resolve_execution_failure` — turned the dispatch into a deny: exit 2 on an
149
+ * ordinary `Read`. That is Risk 1 of the owning roadmap ("fail-closed wedges
150
+ * slow hosts") firing on every host rather than a slow one, reached through a
151
+ * documented escape hatch. The two changes are individually defensible and
152
+ * lethal together, which is exactly the shape a measured bound is supposed to
153
+ * prevent.
154
+ *
155
+ * So the bound stays where the window put it: an observation
156
+ * `src/scripts/_lib/concern_sla_window.ts` prints on every bench run, gating
157
+ * nothing. The spawn path keeps the historical 30 s. Re-wiring it needs a
158
+ * SPAWN-path measurement — which this tree does not have, because the sink it
159
+ * would come from records the in-process number — and not a second reading of
160
+ * the one registered here.
161
+ *
162
+ * `spawn_path_keeps_the_historical_timeout` in `dispatch_hook.test.ts` pins
163
+ * this: it dispatches a blocking concern whose NAME carries a registered SLA
164
+ * row down the spawn path and asserts the call is allowed. Under the bound it
165
+ * reds.
166
+ */
167
+ export const SPAWN_TIMEOUT_MS = 30000;
168
+
169
+ /**
170
+ * The signal name a killed child reports, or the honest placeholder.
171
+ *
172
+ * A SIGNALLED child has `status === null` and `signal` set, and reaches the
173
+ * dispatcher's spawn branch with no `proc.error` whenever the signal came from
174
+ * outside this process — an OOM kill, a SIGTERM from a supervisor, a crash in
175
+ * the child's own runtime. `proc.status ?? 0` read that as exit 0, i.e. ALLOW,
176
+ * which is a fail-OPEN at the one moment the error band exists to fail closed:
177
+ * the concern was killed, so it decided nothing. The coalescing default was
178
+ * harmless while the band meant fail-open for six of nine blocking concerns;
179
+ * with the severity resolution it is the difference between a guard that
180
+ * refuses when it is killed and one that waves the call through. Found by an
181
+ * independent review, 2026-10-03, latent since the spawn path was written.
182
+ *
183
+ * `null` for both fields is possible and is reported as an unknown signal
184
+ * rather than guessed at — a refusal whose cause reads "unknown" is still
185
+ * actionable, and one whose cause is invented is not.
186
+ */
187
+ export function signalLabel(signal: NodeJS.Signals | null): string {
188
+ return signal === null ? 'unknown signal' : signal;
189
+ }
190
+
191
+ /**
192
+ * What an operator is told when a blocking concern refuses without deciding.
193
+ *
194
+ * Printed where the refusal is produced rather than left in a source comment.
195
+ * Both review seats of 2026-10-03 named the same gap: on `pre_tool_use` a
196
+ * deterministically crashing blocking concern refuses every tool call, and the
197
+ * operator cannot discover the escape hatch from inside a wedged session.
198
+ * `stop` is bounded by the one-retry clause and needs no such line, so the
199
+ * caller prints this only where the denial can repeat.
200
+ */
201
+ export function noVerdictRefusalNotice(name: string): string {
202
+ return (
203
+ `dispatch_hook: '${name}' is severity: blocking and produced no ` +
204
+ `verdict, so this call is refused. If it keeps failing, ` +
205
+ `AGENT_CONFIG_HOOKS_ISOLATED=1 runs concerns out of process and ` +
206
+ `agents/runtime/state/dispatch-issues.jsonl names the cause.\n`
207
+ );
208
+ }
209
+
210
+ /**
211
+ * What a finished `spawnSync` actually said, in the vocabulary of verdicts.
212
+ *
213
+ * Three ways a spawned concern produces NO VERDICT, and until 2026-10-03 only
214
+ * two of them were recognised as such:
215
+ *
216
+ * - **`proc.error`** — an OSError, or the kill-timeout expiring. Always a
217
+ * no-verdict and always was.
218
+ * - **`status === null` with a signal** — the child was killed from outside
219
+ * this process: an OOM kill, a supervisor SIGTERM, an abort in the child's
220
+ * own runtime. `proc.status ?? 0` read this as exit 0, i.e. ALLOW, which is
221
+ * a fail-OPEN at the one moment the error band exists to fail closed. The
222
+ * coalescing default was harmless while the band meant fail-open for six of
223
+ * nine blocking concerns; with the severity resolution it is the difference
224
+ * between a guard that refuses when it is killed and one that waves the call
225
+ * through. Found by an independent review, latent since the spawn path was
226
+ * written.
227
+ * - **an exit code in the error band** — a child that simply exited 3. This
228
+ * logged nothing until step 3.3: `proc.error` covers the first case, the
229
+ * in-process route logs its own throw, and this fell between the two. That
230
+ * was survivable while the band meant fail-open; now it can produce a
231
+ * refusal, and a refusal whose cause is in no record is one nobody can act
232
+ * on.
233
+ *
234
+ * Returning a classification rather than writing the log here keeps this module
235
+ * free of I/O — the caller owns `log_dispatch_issue` and the workspace it needs,
236
+ * and this owns what the three cases MEAN.
237
+ */
238
+ export interface SpawnClassification {
239
+ /** The exit code the dispatcher should carry forward. */
240
+ readonly rc: number;
241
+ /** `execution_failed` detail to record, or `null` when there is nothing to record. */
242
+ readonly issueDetail: string | null;
243
+ /** True when the concern produced no verdict and its own stdout must be discarded. */
244
+ readonly noVerdict: boolean;
245
+ /** Prefix for the stderr line, or `null` to pass the child's stderr through unchanged. */
246
+ readonly stderrNote: string | null;
247
+ }
248
+
249
+ export function classifySpawnResult(
250
+ error: Error | undefined,
251
+ status: number | null,
252
+ signal: NodeJS.Signals | null,
253
+ ): SpawnClassification {
254
+ if (error !== undefined) {
255
+ const err = error as NodeJS.ErrnoException;
256
+ const typeName = err.code === 'ETIMEDOUT' ? 'TimeoutExpired' : err.name || 'OSError';
257
+ return {
258
+ rc: EXIT_ERROR,
259
+ issueDetail: `${typeName}: ${err.message}`,
260
+ noVerdict: true,
261
+ stderrNote: err.message,
262
+ };
263
+ }
264
+ if (status === null) {
265
+ const sig = signalLabel(signal);
266
+ return {
267
+ rc: EXIT_ERROR,
268
+ issueDetail: `concern was terminated by ${sig} without a verdict`,
269
+ noVerdict: true,
270
+ stderrNote: `terminated by ${sig}`,
271
+ };
272
+ }
273
+ return {
274
+ rc: status,
275
+ issueDetail:
276
+ status >= EXIT_ERROR ? `concern exited ${String(status)} without a verdict` : null,
277
+ noVerdict: false,
278
+ stderrNote: null,
279
+ };
280
+ }
@@ -12,7 +12,7 @@
12
12
  * Per `docs/contracts/hook-architecture-v1.md`. Reads `src/scripts/hook_manifest.yaml`,
13
13
  * resolves which concerns fire on the given (platform, event) tuple, and runs each
14
14
  * sequentially with the stdin envelope contract. Reduces concern exit codes per the
15
- * spec (0=allow, 1=block, 2=warn, ≥3=error → fail-open unless concern is fail_closed).
15
+ * spec (0=allow, 1=block, 2=warn, ≥3=error → resolved by declared `severity`).
16
16
  *
17
17
  * Invocation:
18
18
  *
@@ -59,6 +59,13 @@ import { _concern_body_classes, planPayloadShapes } from "./payload_stub.js";
59
59
  import { resolveSessionRole, type SessionRole } from "../_lib/session_role.js";
60
60
  import { stdinReadFailure, denyOnStdinFailure } from './stdin_failure_policy.js';
61
61
  export { stdinReadFailure, denyOnStdinFailure, _is_fail_closed_blocking } from './stdin_failure_policy.js';
62
+ // Severity policy — what a declaration means when the concern said nothing.
63
+ // Re-exported so the move out of this file left every importer unaffected.
64
+ import {
65
+ _is_advisory, _is_blocking, _resolve_execution_failure,
66
+ classifySpawnResult, noVerdictRefusalNotice, SPAWN_TIMEOUT_MS,
67
+ } from './concern_failure_policy.js';
68
+ export { _is_advisory, _is_blocking, _resolve_execution_failure };
62
69
  import { _py_json_dumps } from './py_json_dumps.js';
63
70
  import { _fallback_yaml } from './fallback_yaml.js';
64
71
  import { detectSurface } from '../_lib/surface.js';
@@ -128,18 +135,6 @@ export function _severity_for(rc: number): string {
128
135
  return _SEVERITY_BY_EXIT[rc] ?? "error";
129
136
  }
130
137
 
131
- /**
132
- * P0.2 (road-to-rule-coherence) — is this concern declared advisory?
133
- *
134
- * An advisory concern MUST never produce a BLOCK verdict on any host. Four
135
- * PreToolUse concerns document themselves as advisory in prose
136
- * (`design_slop_hook`: "FLAGS, NEVER A BLOCK") while the transport happily
137
- * turned their WARN into a host-level deny. Prose is not enforcement: the
138
- * manifest now declares severity and the dispatcher enforces the ceiling.
139
- */
140
- export function _is_advisory(concern: JsonObject): boolean {
141
- return String(concern["severity"] ?? "").trim().toLowerCase() === "advisory";
142
- }
143
138
 
144
139
  function _now_iso(): string {
145
140
  // Python: datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ").
@@ -198,32 +193,25 @@ export function _parse_concern_stdout(stdout_text: string): JsonObject {
198
193
  * path (`yaml.safe_load(text) or {}`); version 1.1 matches PyYAML.safe_load.
199
194
  */
200
195
  /**
201
- * Cheap content fingerprint of a manifest source — FNV-1a 32-bit plus the byte
202
- * length, hex.
196
+ * Content fingerprint of a manifest source — delegated to `tableFingerprint`,
197
+ * which owns the algorithm and the measurement.
203
198
  *
204
- * Deliberately NOT a crypto hash: `require('node:crypto')` costs 8 ms of
205
- * process start, which is most of what the precompiled manifest exists to
206
- * save. This runs in about 0.2 ms over 61 kB and only has to detect an edited
207
- * source, not resist an adversary — a wrong answer costs the slow path, never
208
- * correctness.
199
+ * road-to-blocking-severities 3.1: this comment used to argue against a crypto
200
+ * hash for a function that already used one. The retired argument and the
201
+ * numbers that retired it live there now.
209
202
  */
210
203
  export function _manifest_fingerprint(text: string): string {
211
204
  return tableFingerprint(text);
212
205
  }
213
206
 
214
207
  export function _load_yaml(p: string): JsonObject {
215
- // Precompiled fast path. The manifest is ~61 kB of YAML and is parsed on
216
- // EVERY dispatch, which measured 12 ms of a ~103 ms dispatch (plus 8 ms to
217
- // load the `yaml` module itself). The sibling `.json` is the same data with
218
- // comments stripped — 14.7 kB — and JSON.parse of it is sub-millisecond.
208
+ // Precompiled fast path. The manifest is ~61 kB of YAML parsed on EVERY
209
+ // dispatch — 12 ms of a ~103 ms dispatch, plus 8 ms to load the `yaml` module
210
+ // itself. The sibling `.json` is the same data with comments stripped,
211
+ // 14.7 kB, and JSON.parse of it is sub-millisecond.
219
212
  //
220
- // Freshness is decided by the SOURCE CONTENT, not by mtime. The first version
221
- // of this compared mtimes and that was a measured defect, not a theoretical
222
- // one: on a fresh `actions/checkout` both files get the checkout timestamp in
223
- // whatever order git wrote them, so whether the optimisation applied at all
224
- // was a coin flip. It won on the PR (p95 129 ms) and lost on the trunk
225
- // (p95 186 ms) for the same commit. A fingerprint is deterministic wherever
226
- // the tree came from.
213
+ // Freshness is SOURCE CONTENT, never mtime — the measured defect behind that
214
+ // is in `table_fingerprint.ts`, told once rather than in all three readers.
227
215
  //
228
216
  // Reading the YAML unconditionally costs ~0 ms (it is the PARSE that is
229
217
  // expensive), so the fast path still skips ~20 ms.
@@ -235,7 +223,14 @@ export function _load_yaml(p: string): JsonObject {
235
223
  if (
236
224
  _isObject(raw) &&
237
225
  raw["fingerprint"] === _manifest_fingerprint(text) &&
238
- _isObject(raw["manifest"])
226
+ _isObject(raw["manifest"]) &&
227
+ // road-to-blocking-severities 2.3, the manifest twin of the check in
228
+ // `host_lowering.resolveTable`, which carries the full reasoning. In
229
+ // one line: the label above binds this file to the YAML SOURCE and says
230
+ // nothing about the body underneath, so an edited `manifest` body with
231
+ // an untouched label was served — on the table that decides which
232
+ // concern runs on which slot at all. Absent field → fall through.
233
+ raw["body_fingerprint"] === _manifest_fingerprint(JSON.stringify(raw["manifest"]))
239
234
  ) {
240
235
  return raw["manifest"];
241
236
  }
@@ -565,10 +560,10 @@ function _resolve_tsx_invocation(
565
560
  * Escape hatch: AGENT_CONFIG_HOOKS_ISOLATED=1 forces the historical
566
561
  * spawn-per-concern path (also used by the bench harness for A/B numbers).
567
562
  *
568
- * Known trade-off vs the spawn path: the 30 s kill-timeout cannot preempt
569
- * in-process synchronous code. Concerns are repo-owned, budget-capped and
570
- * fail-open; the latency budget gate (hook-latency-budget.json) is the
571
- * standing regression net.
563
+ * Known trade-off vs the spawn path: a kill-timeout cannot preempt in-process
564
+ * synchronous code, so a slow concern is bounded on neither route — see
565
+ * `SPAWN_TIMEOUT_MS`. A CRASH is caught here, surfaces as rc 3, and the caller
566
+ * resolves it by declared severity.
572
567
  */
573
568
  /**
574
569
  * ## Why this still re-serialises the envelope per concern (step 1.1, MEASURED NULL)
@@ -678,6 +673,7 @@ function _run_concern_inproc(
678
673
  return { rc, stderr: err, stdout: out, duration_ms: performance.now() - started };
679
674
  }
680
675
 
676
+
681
677
  function _run_concern(concern: ConcernDef, envelope: JsonObject): RunResult {
682
678
  // In-process fast path — the default whenever the concern is in the
683
679
  // static registry (all manifest concerns; parity is CI-enforced).
@@ -728,31 +724,31 @@ function _run_concern(concern: ConcernDef, envelope: JsonObject): RunResult {
728
724
  encoding: "utf-8",
729
725
  cwd: workspace,
730
726
  env: concern_env,
731
- timeout: 30000,
727
+ timeout: SPAWN_TIMEOUT_MS, // historical bound; `SPAWN_TIMEOUT_MS` says why
732
728
  });
733
729
  const elapsed = performance.now() - started;
734
- if (proc.error) {
735
- // OSError / timeout equivalent — log execution-failed so the
736
- // never-block contract keeps a trace.
737
- const err = proc.error as NodeJS.ErrnoException;
738
- const typeName =
739
- err.code === "ETIMEDOUT" ? "TimeoutExpired" : err.name || "OSError";
730
+ // Three ways a spawn produces no verdict; `classifySpawnResult` owns the
731
+ // reading, this writes the record and carries the code.
732
+ const outcome = classifySpawnResult(proc.error, proc.status, proc.signal);
733
+ if (outcome.issueDetail !== null) {
740
734
  log_dispatch_issue(
741
735
  workspace,
742
736
  String(concern["name"] || "unknown"),
743
737
  "execution_failed",
744
- `${typeName}: ${err.message}`,
738
+ outcome.issueDetail,
745
739
  fix_hint(),
746
740
  );
741
+ }
742
+ if (outcome.noVerdict) {
747
743
  return {
748
- rc: 3,
749
- stderr: `${String(concern["name"])}: ${err.message}`,
744
+ rc: outcome.rc,
745
+ stderr: `${String(concern["name"])}: ${String(outcome.stderrNote)}\n${proc.stderr || ""}`,
750
746
  stdout: "",
751
747
  duration_ms: elapsed,
752
748
  };
753
749
  }
754
750
  return {
755
- rc: proc.status ?? 0,
751
+ rc: outcome.rc,
756
752
  stderr: proc.stderr || "",
757
753
  stdout: proc.stdout || "",
758
754
  duration_ms: elapsed,
@@ -1317,14 +1313,18 @@ export function main(argv?: string[]): number {
1317
1313
  let rc = rawRcResult;
1318
1314
  const raw_rc = rc;
1319
1315
  if (rc >= 3) {
1320
- if (!concern["fail_closed"]) {
1321
- rc = EXIT_ALLOW; // fail-open
1322
- } else {
1323
- rc = EXIT_BLOCK;
1324
- }
1316
+ // Resolved by DECLARED SEVERITY, no longer by `fail_closed:`. Every
1317
+ // crash path has already written an `execution_failed` row, so a refusal
1318
+ // here is traceable to a named failure. See `_resolve_execution_failure`.
1319
+ rc = _resolve_execution_failure(concern, refusal_retry);
1325
1320
  if (stderr_text) {
1326
1321
  process.stderr.write(stderr_text);
1327
1322
  }
1323
+ // The escape, printed only where a denial can repeat — `stop` is bounded
1324
+ // by the one-retry clause above. Reasoning: `noVerdictRefusalNotice`.
1325
+ if (rc === EXIT_BLOCK && !refusal_retry) {
1326
+ process.stderr.write(noVerdictRefusalNotice(String(concern["name"])));
1327
+ }
1328
1328
  }
1329
1329
  // P0.2 severity ceiling: an advisory concern can never block, on any host.
1330
1330
  // This covers BOTH the concern emitting EXIT_BLOCK directly and the
@@ -114,7 +114,8 @@ export const EXIT_CODES: readonly ExitCodeRow[] = Object.freeze([
114
114
  'resolves it by the concern`s declared severity, never by the number itself',
115
115
  owner: 'no concern emits this on purpose; the runtime does',
116
116
  authorizedBy:
117
- 'hook_manifest.yaml `fail_closed` and `severity` together — see ' +
117
+ 'hook_manifest.yaml `severity` alone — blocking refuses, anything else fails ' +
118
+ 'open; `fail_closed` is no longer read on this branch. See ' +
118
119
  'docs/contracts/hook-architecture-v1.md § Exit-code semantics',
119
120
  }),
120
121
  ] as const);
@@ -1 +1 @@
1
- {"fingerprint":"08575a71d81fd8a5ef9e4b400f4a38cb66c26e1dd701d98f3cdc0bdc9e4758b4:22235","table":{"schema_version":1,"hosts":{"claude":{"surfaces":{"any":{"entry_shape":"claude-settings-hooks","json_shape":"hook-specific-output","fail_policy":"propagate","timeout_unit":"seconds","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://code.claude.com/docs/en/hooks.md","docs_digest":"9dd68316c1111c118e3ddb82861ac6271a8cd164cee0d371b26165efff35fe88","probe_at":"2026-09-06","host_version":"2.1.263","expires":"2027-09-06"},"slots":{"session_start":{"native":"SessionStart","block_exit":null,"answered_at":"2026-09-29"},"session_end":{"native":"SessionEnd","block_exit":null,"answered_at":"2026-09-29"},"stop":{"native":"Stop","block_exit":2,"answered_at":"2026-09-29"},"user_prompt_submit":{"native":"UserPromptSubmit","block_exit":2,"answered_at":"2026-09-29"},"pre_tool_use":{"native":"PreToolUse","block_exit":2,"answered_at":"2026-09-29"},"post_tool_use":{"native":"PostToolUse","block_exit":null,"answered_at":"2026-09-29"},"pre_compact":{"native":"PreCompact","block_exit":null,"answered_at":"2026-09-29"},"subagent_start":{"native":"SubagentStart","block_exit":null,"answered_at":"2026-09-29"},"subagent_stop":{"native":"SubagentStop","block_exit":null,"answered_at":"2026-09-29"}}}}},"augment":{"surfaces":{"any":{"entry_shape":"augment-settings-hooks","json_shape":"none","fail_policy":"discard","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://docs.augmentcode.com/cli/hooks.md","docs_digest":"e500823be8303717020c1823f670e06d100108f6e3ac4b8d12a46d91fd1898fa","probe_at":null,"host_version":null,"expires":"2027-09-29"},"slots":{"session_start":{"native":"SessionStart","block_exit":null,"answered_at":"2026-09-29"},"session_end":{"native":"SessionEnd","block_exit":null,"answered_at":"2026-09-29"},"stop":{"native":"Stop","block_exit":null,"answered_at":"2026-09-29"},"pre_tool_use":{"native":"PreToolUse","block_exit":null,"answered_at":"2026-09-29"},"post_tool_use":{"native":"PostToolUse","block_exit":null,"answered_at":"2026-09-29"}}}}},"cursor":{"surfaces":{"any":{"entry_shape":"cursor-hooks-json","json_shape":"none","fail_policy":"propagate","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://cursor.com/docs/hooks.md","docs_digest":"7294a2d63b19d79a5283c948afe174cba8b06a7585563d34a2c41b90f5df0d39","probe_at":null,"host_version":null,"expires":"2027-09-29"},"slots":{"session_start":{"native":"sessionStart","block_exit":null,"answered_at":"2026-09-29"},"session_end":{"native":"sessionEnd","block_exit":null,"answered_at":"2026-09-29"},"stop":{"native":"stop","block_exit":null,"answered_at":"2026-09-29"},"user_prompt_submit":{"native":"beforeSubmitPrompt","block_exit":null,"answered_at":"2026-09-29"},"post_tool_use":{"native":"postToolUse","block_exit":null,"answered_at":"2026-09-29"}}}}},"cline":{"surfaces":{"any":{"entry_shape":"cline-per-event-script","json_shape":"none","fail_policy":"discard","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://docs.cline.bot/customization/hooks.md","docs_digest":"ef87561721f4cb80fe6d682b6866d638a4952dd416d36cd5ab398426ce64084b","probe_at":null,"host_version":null,"expires":"2027-09-29"},"slots":{"session_start":{"native":["TaskStart","TaskResume"],"block_exit":null,"answered_at":"2026-09-29"},"session_end":{"native":"TaskComplete","block_exit":null,"answered_at":"2026-09-29"},"stop":{"native":"TaskCancel","block_exit":null,"answered_at":"2026-09-29"},"user_prompt_submit":{"native":"UserPromptSubmit","block_exit":null,"answered_at":"2026-09-29"},"post_tool_use":{"native":"PostToolUse","block_exit":null,"answered_at":"2026-09-29"}}}}},"windsurf":{"surfaces":{"any":{"entry_shape":"windsurf-hooks-json","json_shape":"none","fail_policy":"propagate","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://docs.devin.ai/desktop/cascade/hooks.md","docs_digest":"460ffe7aa91998804920f6722c0153b9948eadd5edafa19582298475a19835a2","probe_at":null,"host_version":null,"expires":"2027-09-29"},"slots":{"session_start":{"native":"post_setup_worktree","block_exit":null,"answered_at":"2026-09-29"},"user_prompt_submit":{"native":"pre_user_prompt","block_exit":null,"answered_at":"2026-09-29"},"stop":{"native":"post_cascade_response","block_exit":null,"answered_at":"2026-09-29"}}}}},"gemini":{"surfaces":{"any":{"entry_shape":"gemini-settings-hooks","json_shape":"none","fail_policy":"propagate","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://geminicli.com/docs/hooks/reference.md","docs_digest":"06d37f2f66812ee78bb46c37a112203376f0bae4d3b8b44ac79d0bda4b86a118","probe_at":null,"host_version":null,"expires":"2027-09-29"},"slots":{"session_start":{"native":"SessionStart","block_exit":null,"answered_at":"2026-09-29","matcher":""},"session_end":{"native":"SessionEnd","block_exit":null,"answered_at":"2026-09-29","matcher":""},"stop":{"native":"AfterAgent","block_exit":null,"answered_at":"2026-09-29","matcher":""},"user_prompt_submit":{"native":"BeforeAgent","block_exit":null,"answered_at":"2026-09-29","matcher":""},"post_tool_use":{"native":"AfterTool","block_exit":null,"answered_at":"2026-09-29","matcher":".*"}}}}},"cowork":{"surfaces":{"any":{"entry_shape":"none","json_shape":"none","fail_policy":"discard","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":null,"docs_digest":null,"probe_at":null,"host_version":null,"expires":"2027-09-29"},"slots":{}}}},"copilot":{"surfaces":{"any":{"entry_shape":"none","json_shape":"none","fail_policy":"discard","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://docs.github.com/en/copilot/concepts/agents/hooks.md","docs_digest":"bfd40aa93ad62133efc17a50f189034f01594dc89a5d31a13a0c541d691acede","probe_at":null,"host_version":null,"expires":"2027-09-29"},"slots":{}}}},"codex":{"surfaces":{"any":{"entry_shape":"none","json_shape":"none","fail_policy":"discard","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://developers.openai.com/codex/hooks.md","docs_digest":"724881d81f1a9fdb35ea42d4851a11b1f6606bbc6e9d2d05c1e8c466fc25c630","probe_at":"2026-10-01","host_version":"0.148.0","expires":"2027-09-29"},"slots":{}}}}}}}
1
+ {"fingerprint":"08575a71d81fd8a5ef9e4b400f4a38cb66c26e1dd701d98f3cdc0bdc9e4758b4:22235","body_fingerprint":"26a02a8e0fb2b6be6eb1def6ae8792c021425598bd672c850e6582726d96a6a5:6376","table":{"schema_version":1,"hosts":{"claude":{"surfaces":{"any":{"entry_shape":"claude-settings-hooks","json_shape":"hook-specific-output","fail_policy":"propagate","timeout_unit":"seconds","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://code.claude.com/docs/en/hooks.md","docs_digest":"9dd68316c1111c118e3ddb82861ac6271a8cd164cee0d371b26165efff35fe88","probe_at":"2026-09-06","host_version":"2.1.263","expires":"2027-09-06"},"slots":{"session_start":{"native":"SessionStart","block_exit":null,"answered_at":"2026-09-29"},"session_end":{"native":"SessionEnd","block_exit":null,"answered_at":"2026-09-29"},"stop":{"native":"Stop","block_exit":2,"answered_at":"2026-09-29"},"user_prompt_submit":{"native":"UserPromptSubmit","block_exit":2,"answered_at":"2026-09-29"},"pre_tool_use":{"native":"PreToolUse","block_exit":2,"answered_at":"2026-09-29"},"post_tool_use":{"native":"PostToolUse","block_exit":null,"answered_at":"2026-09-29"},"pre_compact":{"native":"PreCompact","block_exit":null,"answered_at":"2026-09-29"},"subagent_start":{"native":"SubagentStart","block_exit":null,"answered_at":"2026-09-29"},"subagent_stop":{"native":"SubagentStop","block_exit":null,"answered_at":"2026-09-29"}}}}},"augment":{"surfaces":{"any":{"entry_shape":"augment-settings-hooks","json_shape":"none","fail_policy":"discard","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://docs.augmentcode.com/cli/hooks.md","docs_digest":"e500823be8303717020c1823f670e06d100108f6e3ac4b8d12a46d91fd1898fa","probe_at":null,"host_version":null,"expires":"2027-09-29"},"slots":{"session_start":{"native":"SessionStart","block_exit":null,"answered_at":"2026-09-29"},"session_end":{"native":"SessionEnd","block_exit":null,"answered_at":"2026-09-29"},"stop":{"native":"Stop","block_exit":null,"answered_at":"2026-09-29"},"pre_tool_use":{"native":"PreToolUse","block_exit":null,"answered_at":"2026-09-29"},"post_tool_use":{"native":"PostToolUse","block_exit":null,"answered_at":"2026-09-29"}}}}},"cursor":{"surfaces":{"any":{"entry_shape":"cursor-hooks-json","json_shape":"none","fail_policy":"propagate","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://cursor.com/docs/hooks.md","docs_digest":"7294a2d63b19d79a5283c948afe174cba8b06a7585563d34a2c41b90f5df0d39","probe_at":null,"host_version":null,"expires":"2027-09-29"},"slots":{"session_start":{"native":"sessionStart","block_exit":null,"answered_at":"2026-09-29"},"session_end":{"native":"sessionEnd","block_exit":null,"answered_at":"2026-09-29"},"stop":{"native":"stop","block_exit":null,"answered_at":"2026-09-29"},"user_prompt_submit":{"native":"beforeSubmitPrompt","block_exit":null,"answered_at":"2026-09-29"},"post_tool_use":{"native":"postToolUse","block_exit":null,"answered_at":"2026-09-29"}}}}},"cline":{"surfaces":{"any":{"entry_shape":"cline-per-event-script","json_shape":"none","fail_policy":"discard","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://docs.cline.bot/customization/hooks.md","docs_digest":"ef87561721f4cb80fe6d682b6866d638a4952dd416d36cd5ab398426ce64084b","probe_at":null,"host_version":null,"expires":"2027-09-29"},"slots":{"session_start":{"native":["TaskStart","TaskResume"],"block_exit":null,"answered_at":"2026-09-29"},"session_end":{"native":"TaskComplete","block_exit":null,"answered_at":"2026-09-29"},"stop":{"native":"TaskCancel","block_exit":null,"answered_at":"2026-09-29"},"user_prompt_submit":{"native":"UserPromptSubmit","block_exit":null,"answered_at":"2026-09-29"},"post_tool_use":{"native":"PostToolUse","block_exit":null,"answered_at":"2026-09-29"}}}}},"windsurf":{"surfaces":{"any":{"entry_shape":"windsurf-hooks-json","json_shape":"none","fail_policy":"propagate","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://docs.devin.ai/desktop/cascade/hooks.md","docs_digest":"460ffe7aa91998804920f6722c0153b9948eadd5edafa19582298475a19835a2","probe_at":null,"host_version":null,"expires":"2027-09-29"},"slots":{"session_start":{"native":"post_setup_worktree","block_exit":null,"answered_at":"2026-09-29"},"user_prompt_submit":{"native":"pre_user_prompt","block_exit":null,"answered_at":"2026-09-29"},"stop":{"native":"post_cascade_response","block_exit":null,"answered_at":"2026-09-29"}}}}},"gemini":{"surfaces":{"any":{"entry_shape":"gemini-settings-hooks","json_shape":"none","fail_policy":"propagate","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://geminicli.com/docs/hooks/reference.md","docs_digest":"06d37f2f66812ee78bb46c37a112203376f0bae4d3b8b44ac79d0bda4b86a118","probe_at":null,"host_version":null,"expires":"2027-09-29"},"slots":{"session_start":{"native":"SessionStart","block_exit":null,"answered_at":"2026-09-29","matcher":""},"session_end":{"native":"SessionEnd","block_exit":null,"answered_at":"2026-09-29","matcher":""},"stop":{"native":"AfterAgent","block_exit":null,"answered_at":"2026-09-29","matcher":""},"user_prompt_submit":{"native":"BeforeAgent","block_exit":null,"answered_at":"2026-09-29","matcher":""},"post_tool_use":{"native":"AfterTool","block_exit":null,"answered_at":"2026-09-29","matcher":".*"}}}}},"cowork":{"surfaces":{"any":{"entry_shape":"none","json_shape":"none","fail_policy":"discard","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":null,"docs_digest":null,"probe_at":null,"host_version":null,"expires":"2027-09-29"},"slots":{}}}},"copilot":{"surfaces":{"any":{"entry_shape":"none","json_shape":"none","fail_policy":"discard","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://docs.github.com/en/copilot/concepts/agents/hooks.md","docs_digest":"bfd40aa93ad62133efc17a50f189034f01594dc89a5d31a13a0c541d691acede","probe_at":null,"host_version":null,"expires":"2027-09-29"},"slots":{}}}},"codex":{"surfaces":{"any":{"entry_shape":"none","json_shape":"none","fail_policy":"discard","timeout_unit":"unknown","timeout_default":null,"verified":{"docs_at":"2026-09-29","docs_url":"https://developers.openai.com/codex/hooks.md","docs_digest":"724881d81f1a9fdb35ea42d4851a11b1f6606bbc6e9d2d05c1e8c466fc25c630","probe_at":"2026-10-01","host_version":"0.148.0","expires":"2027-09-29"},"slots":{}}}}}}}
@@ -251,7 +251,25 @@ export function resolveTable(yamlText: string, compiledText: string | null): Hos
251
251
  if (
252
252
  raw['fingerprint'] === tableFingerprint(yamlText) &&
253
253
  typeof table === 'object' &&
254
- table !== null
254
+ table !== null &&
255
+ // road-to-blocking-severities 2.3 — the BODY, not only its
256
+ // label. `fingerprint` above says the file was compiled from
257
+ // the YAML as it stands; it says nothing about whether the
258
+ // body underneath was edited afterwards, and an edited body
259
+ // with an untouched label was served. `body_fingerprint` is
260
+ // the fingerprint of the exact bytes the compiler emitted, so
261
+ // re-stringifying what we just parsed recovers them.
262
+ //
263
+ // A compiled file WITHOUT the field — one written before 2.3 —
264
+ // falls through here, which is slow and correct: accepting it
265
+ // would make the check optional and therefore absent.
266
+ //
267
+ // What it covers is a partial edit, never an author: anyone
268
+ // who edits the body can recompute this as easily as the
269
+ // compiler. The deny in `block_plumbing_writes` and the
270
+ // ratification record are the mechanisms aimed at an author;
271
+ // this one is aimed at a mistake.
272
+ raw['body_fingerprint'] === tableFingerprint(JSON.stringify(table))
255
273
  ) {
256
274
  return _structure(table as Record<string, unknown>);
257
275
  }
@@ -14,33 +14,38 @@
14
14
  * names the count so the caller can split the ask instead of guessing what
15
15
  * happened.
16
16
  *
17
- * WHAT IT SEES, honestly. `_lib/structured_ask.ts` carries no observed
18
- * per-host tool, so this guard matches on a name SHAPE and is filtered by the
19
- * manifest's per-concern `tools:` key to the same candidate names. No host's
20
- * delivered surface has been OBSERVED in this tree carrying a question-picker
21
- * tool, which is why `STRUCTURED_ASK_SHAPES` is empty and no per-host shape row
22
- * exists — the same sentence `_lib/structured_ask.ts` already uses about itself.
17
+ * WHAT IT SEES, honestly. `_lib/structured_ask.ts` carries ONE observed
18
+ * per-host tool — `claude`'s `AskUserQuestion`, 48 calls across versions
19
+ * 2.1.252 to 2.1.284 between 2026-09-01 and 2026-10-02.
23
20
  *
24
- * That is NARROWER than the claim this header used to make. The earlier
25
- * sentence asserted that on every host measured today the concern therefore
26
- * matches nothing at all, and the two are different claims with only the first
27
- * supported. The old wording is described here rather than quoted, because
28
- * 1.1's drift check is a grep and a grep cannot tell a refuted quotation from
29
- * a live assertion. The measurement base is ONE row:
30
- * `_lib/host_capability.ts`'s registry records `structured_ask: false` for
31
- * `claude` alone, observed-absent on Claude Code 2.1.263 on 2026-09-07, and the
32
- * other eight hosts in `docs/enforcement-by-host.md` have no row at all — never
33
- * looked, not measured. "Every host measured" read against that table therefore
34
- * describes one host, and a present-tense claim about what the concern matches
35
- * turns one dated single-version reading into a standing property, which the registry's own
36
- * comment refuses in the same breath: the observation "is not a claim that the
37
- * vendor ships no such tool anywhere, and a later session observing one writes
38
- * the `true` row over it".
21
+ * THIS GUARD DOES NOT USE THAT ROW, and the distinction matters enough to say
22
+ * rather than leave to a reader. `verdict()` calls `isStructuredAskTool(tool)`
23
+ * with no host id, so the shape branch is unreachable from here and the name
24
+ * SHAPE pattern decides on every host, `claude` included — filtered by the
25
+ * manifest's per-concern `tools:` key to the same candidate names. Threading a
26
+ * host id through would NARROW what this guard matches on the one host where a
27
+ * picker is known to exist (`ask_user_question` would stop matching), which is
28
+ * a behaviour change and not a header repair, so it is deliberately not made
29
+ * here.
39
30
  *
40
- * So the honest statement is about what has been looked at, not about what
41
- * fires. The concern exists so the first host whose delivered surface carries a
42
- * picker meets the rule already in force, not because a violation has been
43
- * observed.
31
+ * This header carried the opposite claim until 2026-10-05 — that no host's
32
+ * delivered surface had been observed carrying a picker — and it was wrong in
33
+ * a specific, instructive way rather than merely stale. Its measurement base
34
+ * was one `false` row taken from a SUBAGENT leg, whose surface carries no
35
+ * picker because a subagent has no user to ask. The step from "this leg has
36
+ * none" to "no host has been observed with one" is the substitution the
37
+ * observation protocol exists to refuse, and the registry comment now records
38
+ * which leg each reading came from for that reason. The old wording is
39
+ * described rather than quoted, because 1.1's drift check is a grep and a grep
40
+ * cannot tell a refuted quotation from a live assertion.
41
+ *
42
+ * The other eight hosts in `docs/enforcement-by-host.md` still have no row at
43
+ * all — never looked, not measured — so nothing here is a claim about them.
44
+ *
45
+ * WHAT THE OBSERVATION DOES NOT CHANGE: the deny threshold. One question per
46
+ * call is `ask-when-uncertain`'s Iron Law, not a host limit, and all 47
47
+ * parsed calls already carried exactly one. The guard has had nothing to deny
48
+ * and that is the expected reading, not evidence it is inert.
44
49
  *
45
50
  * ENFORCEMENT REACH, also honestly. `fail_closed: false` and the exit-1 deny
46
51
  * are only a deny where the host both binds `pre_tool_use` AND honours the
@@ -162,6 +167,17 @@ export function selfTest(): number {
162
167
  { tool_name: 'ask_user_question', tool_input: { question: 'which one?' } },
163
168
  false,
164
169
  ],
170
+ [
171
+ // PINS THE HEADER'S INVARIANT. `ask_user_question` is not the name
172
+ // claude's observed shape row carries, so this case denies only
173
+ // while `verdict()` passes no host id and the name-SHAPE pattern
174
+ // decides. Thread a host id through and the shape branch rejects
175
+ // the name, the call stops being an ask, and this flips to allow —
176
+ // which is the silent narrowing the header says is not being made.
177
+ 'a snake_case picker with two questions is still denied — this guard is host-blind',
178
+ { tool_name: 'ask_user_question', tool_input: { questions: [{}, {}] } },
179
+ true,
180
+ ],
165
181
  [
166
182
  'an empty ask passes — malformed, but not this guard\'s failure',
167
183
  { tool_name: 'AskUserQuestion', tool_input: { questions: [] } },