@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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +89 -237
- package/README.md +14 -12
- package/dist/agent-src/commands/analyze/inbox.md +1 -1
- package/dist/agent-src/commands/analyze/roadmap-repos.md +1 -1
- package/dist/agent-src/commands/bug/fix.md +17 -2
- package/dist/agent-src/commands/feature/plan.md +1 -1
- package/dist/agent-src/commands/feature/roadmap.md +1 -1
- package/dist/agent-src/commands/fix/ci.md +1 -1
- package/dist/agent-src/commands/implement-ticket.md +1 -1
- package/dist/agent-src/commands/jira-ticket.md +1 -1
- package/dist/agent-src/commands/roadmap/create.md +1 -1
- package/dist/agent-src/commands/roadmap/materialize.md +1 -1
- package/dist/agent-src/commands/roadmap/process-full.md +4 -4
- package/dist/agent-src/commands/roadmap/resolve-blockers.md +361 -0
- package/dist/agent-src/commands/roadmap/triage-parked.md +325 -0
- package/dist/agent-src/commands/roadmap.md +18 -4
- package/dist/agent-src/commands/tests/create.md +17 -3
- package/dist/agent-src/commands/tests/execute.md +22 -5
- package/dist/agent-src/contexts/authority/decision-revisit-gate-enforcement.md +43 -0
- package/dist/agent-src/contexts/communication/rules-auto/design-fidelity-enforcement.md +24 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +6 -6
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +32 -2
- package/dist/agent-src/contexts/execution/session-canary-enforcement-history.md +67 -0
- package/dist/agent-src/contexts/execution/toolchain-resolver.md +101 -7
- package/dist/agent-src/rules/active-remediation.md +1 -1
- package/dist/agent-src/rules/autonomous-execution.md +1 -0
- package/dist/agent-src/rules/broken-access-control.md +1 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +1 -0
- package/dist/agent-src/rules/context-hygiene.md +1 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +16 -33
- package/dist/agent-src/rules/design-fidelity.md +13 -16
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +1 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +1 -0
- package/dist/agent-src/rules/domain-safety-pii.md +1 -0
- package/dist/agent-src/rules/domain-safety-retention.md +1 -0
- package/dist/agent-src/rules/engineering-safety-floor.md +1 -0
- package/dist/agent-src/rules/evaluator-independence.md +3 -2
- package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
- package/dist/agent-src/rules/finance-safety-floor.md +1 -0
- package/dist/agent-src/rules/git-history-discipline.md +1 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
- package/dist/agent-src/rules/legal-safety-floor.md +3 -2
- package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/rules/media-governance-routing.md +14 -17
- package/dist/agent-src/rules/persona-governance.md +2 -2
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +1 -1
- package/dist/agent-src/rules/question-not-instruction.md +1 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
- package/dist/agent-src/rules/runtime-safety.md +1 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +1 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +3 -2
- package/dist/agent-src/rules/session-canary.md +8 -62
- package/dist/agent-src/rules/settings-ask-protocol.md +1 -0
- package/dist/agent-src/rules/source-confidentiality.md +1 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +1 -0
- package/dist/agent-src/rules/tool-safety.md +1 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +1 -0
- package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
- package/dist/agent-src/scripts/deferral_carry.ts +339 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +4 -1
- package/dist/agent-src/skills/api-design/data/manifest.json +2 -2
- package/dist/agent-src/skills/api-testing/SKILL.md +17 -0
- package/dist/agent-src/skills/code-intelligence/SKILL.md +14 -2
- package/dist/agent-src/skills/design-system-capture/references/import-procedure.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +2 -2
- package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
- package/dist/agent-src/skills/iconography/SKILL.md +1 -1
- package/dist/agent-src/skills/js-library-packaging/SKILL.md +2 -2
- package/dist/agent-src/skills/monorepo-workspace/SKILL.md +1 -1
- package/dist/agent-src/skills/quality-tools/SKILL.md +11 -1
- package/dist/agent-src/skills/quality-tools/references/go-tools.md +76 -0
- package/dist/agent-src/skills/quality-tools/references/python-tools.md +104 -0
- package/dist/agent-src/skills/review-routing/SKILL.md +2 -2
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +3 -3
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +7 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +17 -0
- package/dist/agent-src/skills/test-performance/SKILL.md +19 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +2 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
- package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
- package/dist/cli/registry.js +1 -1
- package/dist/cli-delegate/archive_completed_roadmaps.js +92 -12
- package/dist/cli-delegate/check_update_banner.js +12 -1
- package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
- package/dist/cli-delegate/{chunk-M2DPGFZ4.js → chunk-6V6TA25H.js} +22 -152
- package/dist/cli-delegate/chunk-7IQE3D5S.js +191 -0
- package/dist/cli-delegate/{chunk-6KFEC3G7.js → chunk-CQWX336L.js} +1 -1
- package/dist/cli-delegate/chunk-DEIV7XP7.js +108 -0
- package/dist/cli-delegate/chunk-EEHDXSYD.js +2040 -0
- package/dist/cli-delegate/{chunk-P6ZHP5W2.js → chunk-GKE7ZVFA.js} +4 -4
- package/dist/cli-delegate/chunk-JQOM7TTM.js +1201 -0
- package/dist/cli-delegate/{chunk-IGD6ERX6.js → chunk-JQVYKIZA.js} +10 -0
- package/dist/cli-delegate/{chunk-FC75PGJE.js → chunk-JY2I2DXV.js} +12 -1
- package/dist/cli-delegate/chunk-KB5FZ655.js +208 -0
- package/dist/cli-delegate/{chunk-7RQP4UDH.js → chunk-KR6EM23A.js} +235 -1455
- package/dist/cli-delegate/{chunk-LTYVIY76.js → chunk-LIYTLH6O.js} +1 -1
- package/dist/cli-delegate/{chunk-YJ72UIAK.js → chunk-NQ7WT43P.js} +1186 -244
- package/dist/cli-delegate/chunk-NXEILF2K.js +2160 -0
- package/dist/cli-delegate/{chunk-5R7HOLAF.js → chunk-NYX3L2PZ.js} +1 -1
- package/dist/cli-delegate/{chunk-4E2W63DA.js → chunk-OKJUIAPS.js} +62 -1
- package/dist/cli-delegate/{chunk-QAJS5YWY.js → chunk-PHD7OOLE.js} +2 -1
- package/dist/cli-delegate/{chunk-RGNIWTEA.js → chunk-QLDHIPWR.js} +1 -1
- package/dist/cli-delegate/{chunk-7XGBX6ZH.js → chunk-RQNNW7IF.js} +1314 -560
- package/dist/cli-delegate/{chunk-4XFP2Y3E.js → chunk-S4UMZNES.js} +1 -1
- package/dist/cli-delegate/{chunk-6KM6ORRY.js → chunk-S4YWNRKO.js} +24 -8
- package/dist/cli-delegate/{chunk-FP36SWJD.js → chunk-TRI6DQZ6.js} +2781 -340
- package/dist/cli-delegate/chunk-ULQRUR23.js +208 -0
- package/dist/cli-delegate/{chunk-NUHLI2RX.js → chunk-YCU3TIXH.js} +2 -0
- package/dist/cli-delegate/chunk-ZBZV2G5R.js +210 -0
- package/dist/cli-delegate/{chunk-WGXTVKCQ.js → chunk-ZUZKZYCN.js} +5 -3
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +1167 -234
- package/dist/cli-delegate/cmd_doctor.js +13 -6
- package/dist/cli-delegate/cmd_doctor_neighbours.js +23 -0
- package/dist/cli-delegate/cmd_explain.js +5 -5
- package/dist/cli-delegate/cmd_export.js +11 -8
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +10 -7
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_memory_get.js +3 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +3 -3
- package/dist/cli-delegate/cmd_preflight.js +6 -6
- package/dist/cli-delegate/cmd_prune.js +11 -8
- package/dist/cli-delegate/cmd_refresh.js +2 -1
- package/dist/cli-delegate/cmd_route_audit.js +8 -6
- package/dist/cli-delegate/cmd_route_explain.js +4 -4
- package/dist/cli-delegate/cmd_session_recycle.js +4 -3
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +11 -8
- package/dist/cli-delegate/cmd_settings_set.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +11 -8
- package/dist/cli-delegate/cmd_uninstall.js +11 -8
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +3 -2
- package/dist/cli-delegate/cmd_validate.js +11 -8
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/cli-delegate/deferral_carry.js +16 -0
- package/dist/cli-delegate/gate_execute.js +2 -2
- package/dist/cli-delegate/roadmap_gates.js +5 -5
- package/dist/cli-delegate/update_roadmap_progress.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +127 -75
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +5357 -6656
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/install/claudePathsPlan.js +47 -6
- package/dist/install/claudeRuleRewrite.js +7 -0
- package/dist/install/emit_host_rules_cli.js +39 -36
- package/dist/install/globalRuleScope.js +87 -0
- package/dist/install/host_rule_ownership.js +160 -0
- package/dist/install/install.mjs +2080 -1024
- package/dist/install/installThinLayer.js +261 -0
- package/dist/install/installedRuleLinks.js +176 -0
- package/dist/install/wizard-plan.js +24 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +31965 -27154
- package/dist/scripts/_lib/agent_settings.js +11 -0
- package/dist/scripts/_lib/claude_settings_hooks.js +10 -8
- package/dist/scripts/_lib/installed_layer.js +378 -0
- package/dist/scripts/_lib/lean_projection_mode.js +218 -15
- package/dist/scripts/_lib/rule_consequence_class.js +45 -0
- package/dist/scripts/_lib/rule_injection.js +67 -10
- package/dist/scripts/_lib/rule_law_section.js +122 -0
- package/dist/scripts/_lib/thin_rules.js +458 -0
- package/dist/scripts/_lib/windsurf_render.js +60 -0
- package/dist/scripts/condense.js +14 -26
- package/dist/scripts/project_thin_rules.js +14 -294
- package/dist/server/schemas/settings.js +6 -0
- package/dist/shared/skillRanking.js +74 -13
- package/docs/contracts/settings-classes.md +7 -5
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
- package/src/agent-src/scripts/deferral_carry.ts +339 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +4 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
- package/src/config/agent-settings.template.yml +62 -22
- package/src/config/continuity-surface.json +7 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +96 -3
- package/src/config/gate-violation-baselines.json +17 -9
- package/src/config/hook-bundle-budget.json +25 -0
- package/src/config/hook-token-budget.json +6 -1
- package/src/config/host-instruction-limits.json +33 -0
- package/src/config/routing-coverage-seed.json +24 -3
- package/src/config/rule-activation-census.json +7 -3
- package/src/config/rule-consequence-class.json +161 -0
- package/src/config/rule-law-ceilings.json +99 -0
- package/src/install/claudePathsPlan.ts +46 -4
- package/src/install/claudeRuleRewrite.ts +10 -1
- package/src/install/emit_host_rules_cli.ts +43 -34
- package/src/install/globalRuleScope.ts +109 -0
- package/src/install/host_rule_ownership.ts +170 -0
- package/src/install/installThinLayer.ts +312 -0
- package/src/install/installedRuleLinks.ts +220 -0
- package/src/install/wizard-plan.ts +24 -0
- package/src/scripts/_cli/cmd_doctor.ts +23 -71
- package/src/scripts/_cli/cmd_doctor_neighbours.ts +293 -0
- package/src/scripts/_dispatch.bash +16 -2
- package/src/scripts/_lib/agent_settings.ts +11 -0
- package/src/scripts/_lib/claude_settings_hooks.ts +10 -8
- package/src/scripts/_lib/estate_offsets.ts +241 -0
- package/src/scripts/_lib/exemption_shape.ts +143 -0
- package/src/scripts/_lib/fnv.ts +23 -0
- package/src/scripts/_lib/foreign_scan.ts +106 -0
- package/src/scripts/_lib/gitignore_block.ts +314 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +32 -0
- package/src/scripts/_lib/graph_feeder_record.ts +214 -0
- package/src/scripts/_lib/hook_settings.ts +73 -0
- package/src/scripts/_lib/host_capability.ts +40 -19
- package/src/scripts/_lib/host_hook_merge.ts +165 -0
- package/src/scripts/_lib/installed_layer.ts +538 -0
- package/src/scripts/_lib/json_merge.ts +82 -0
- package/src/scripts/_lib/json_pointers.ts +93 -11
- package/src/scripts/_lib/lean_projection_mode.ts +275 -15
- package/src/scripts/_lib/machine_wake.ts +16 -3
- package/src/scripts/_lib/neighbour_census.ts +760 -0
- package/src/scripts/_lib/neighbour_scan.ts +122 -0
- package/src/scripts/_lib/neighbour_tool_use.ts +189 -0
- package/src/scripts/_lib/prompt_shape.ts +46 -0
- package/src/scripts/_lib/release_migration_gate.ts +19 -3
- package/src/scripts/_lib/reserved_name_sweep.ts +73 -0
- package/src/scripts/_lib/review_coverage.ts +134 -0
- package/src/scripts/_lib/rule_consequence_class.ts +71 -0
- package/src/scripts/_lib/rule_injection.ts +130 -10
- package/src/scripts/_lib/rule_law_section.ts +147 -0
- package/src/scripts/_lib/rule_layer_overlap.ts +58 -0
- package/src/scripts/_lib/skill_origin.ts +254 -0
- package/src/scripts/_lib/structured_ask.ts +64 -21
- package/src/scripts/_lib/thin_rules.ts +540 -0
- package/src/scripts/_lib/touched_file_quality.ts +434 -0
- package/src/scripts/_lib/turn_end_refusals.ts +77 -2
- package/src/scripts/_lib/windsurf_render.ts +63 -0
- package/src/scripts/ai_council/cli_call_budget.ts +13 -5
- package/src/scripts/ai_council/clients.ts +26 -57
- package/src/scripts/ai_council/config.ts +1 -1
- package/src/scripts/ai_council/vendor_defaults.ts +45 -0
- package/src/scripts/audit_skill_overlap.ts +261 -0
- package/src/scripts/before_complete_hook.ts +243 -12
- package/src/scripts/bench_touched_file_quality.ts +238 -0
- package/src/scripts/check_enforcement_matrix.ts +377 -20
- package/src/scripts/check_estate_count.ts +130 -153
- package/src/scripts/check_finding_dispositions.ts +116 -4
- package/src/scripts/check_hook_bundle_composition.ts +370 -0
- package/src/scripts/check_kernel_edit_ratified.ts +49 -11
- package/src/scripts/check_memory.ts +7 -1
- package/src/scripts/check_release_highlights.ts +10 -0
- package/src/scripts/check_release_pr_shape.ts +38 -15
- package/src/scripts/check_standing_rule_delivery.ts +33 -13
- package/src/scripts/check_verify_expectation_delta.ts +521 -0
- package/src/scripts/code_graph/detect.ts +127 -7
- package/src/scripts/code_graph/foreign.ts +194 -0
- package/src/scripts/code_graph/query.ts +217 -4
- package/src/scripts/code_graph/types.ts +26 -2
- package/src/scripts/code_graph/validate.ts +3 -1
- package/src/scripts/code_graph/verbs.ts +103 -0
- package/src/scripts/compile_hook_manifest.ts +33 -1
- package/src/scripts/condense.ts +14 -27
- package/src/scripts/estate_exemption_shape.ts +202 -0
- package/src/scripts/generate_host_cost_table.ts +1 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +150 -9
- package/src/scripts/hooks/block_config_weakening.ts +67 -2
- package/src/scripts/hooks/block_plumbing_writes.ts +29 -1
- package/src/scripts/hooks/code_graph_context_hook.ts +252 -23
- package/src/scripts/hooks/concern_failure_policy.ts +280 -0
- package/src/scripts/hooks/dispatch_hook.ts +52 -52
- package/src/scripts/hooks/exit_codes.ts +2 -1
- package/src/scripts/hooks/host_lowering.json +1 -1
- package/src/scripts/hooks/host_lowering.ts +19 -1
- package/src/scripts/hooks/one_question_per_ask_hook.ts +41 -25
- package/src/scripts/hooks/payload_stub.ts +10 -1
- package/src/scripts/hooks/rule_inject_hook.ts +757 -63
- package/src/scripts/hooks/table_fingerprint.ts +21 -0
- package/src/scripts/hooks/telemetry_usage_hook.ts +64 -8
- package/src/scripts/hooks/turn_end_gate_hook.ts +84 -0
- package/src/scripts/hooks_status.ts +112 -0
- package/src/scripts/injection_scan_hook.ts +7 -0
- package/src/scripts/install.ts +72 -185
- package/src/scripts/installed_layer_report.ts +103 -0
- package/src/scripts/language_mirror_hook.ts +15 -6
- package/src/scripts/learning_sidecar.ts +119 -8
- package/src/scripts/lint_hook_manifest.ts +53 -0
- package/src/scripts/lint_major_migration_sections.ts +156 -26
- package/src/scripts/lint_pack_boundaries.ts +63 -5
- package/src/scripts/lint_rule_law_section.ts +517 -0
- package/src/scripts/lint_workflow_security.ts +161 -5
- package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -0
- package/src/scripts/mcp_server/graph_tools.ts +72 -3
- package/src/scripts/measure_routing_signal.ts +54 -16
- package/src/scripts/measure_skill_ranker_baseline.ts +216 -8
- package/src/scripts/memory_learn_hook.ts +91 -6
- package/src/scripts/model_rule_injection.ts +52 -5
- package/src/scripts/project_thin_rules.ts +34 -327
- package/src/scripts/report_evidence_temperature.ts +657 -0
- package/src/scripts/report_installed_rule_links.ts +157 -0
- package/src/scripts/report_label_agreement.ts +233 -0
- package/src/scripts/report_obligation_mechanism.ts +230 -0
- package/src/scripts/report_single_token_triggers.ts +277 -0
- package/src/scripts/report_skill_ranker_confusion.ts +442 -0
- package/src/scripts/report_standing_payload_by_host.ts +188 -15
- package/src/scripts/report_verification_record_coverage.ts +306 -0
- package/src/scripts/rule_activation_census.ts +5 -2
- package/src/scripts/rule_trigger_eval.ts +9 -9
- package/src/scripts/schemas/review-findings.schema.json +3 -2
- package/src/scripts/schemas/rule.schema.json +5 -0
- package/src/scripts/self_review_gate.ts +62 -7
- package/src/scripts/skill_tools/score_skill_relevance.ts +290 -25
- package/src/scripts/sweep_skill_ranker_signals.ts +129 -0
- package/src/scripts/sync_gitignore.ts +29 -235
- package/src/scripts/trigger_eval_grandfather.json +0 -5
- package/src/server/schemas/settings.ts +10 -0
- package/src/shared/skillRanking.ts +117 -11
- package/dist/cli-delegate/chunk-JNMVLWLR.js +0 -4457
- 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 →
|
|
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
|
-
*
|
|
202
|
-
*
|
|
196
|
+
* Content fingerprint of a manifest source — delegated to `tableFingerprint`,
|
|
197
|
+
* which owns the algorithm and the measurement.
|
|
203
198
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
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
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
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
|
|
221
|
-
//
|
|
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:
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
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:
|
|
727
|
+
timeout: SPAWN_TIMEOUT_MS, // historical bound; `SPAWN_TIMEOUT_MS` says why
|
|
732
728
|
});
|
|
733
729
|
const elapsed = performance.now() - started;
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
738
|
+
outcome.issueDetail,
|
|
745
739
|
fix_hint(),
|
|
746
740
|
);
|
|
741
|
+
}
|
|
742
|
+
if (outcome.noVerdict) {
|
|
747
743
|
return {
|
|
748
|
-
rc:
|
|
749
|
-
stderr: `${String(concern["name"])}: ${
|
|
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:
|
|
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
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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 `
|
|
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
|
|
18
|
-
* per-host tool
|
|
19
|
-
*
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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: [] } },
|