@event4u/agent-config 16.1.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 +98 -257
- 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 +5 -5
- 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/commands/work.md +4 -0
- 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/autonomy-mechanics.md +4 -8
- 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/ui-audit-gate.md +1 -2
- 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/accessibility-auditor/data/aria-patterns.csv +1 -1
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
- 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-review/SKILL.md +30 -4
- package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
- package/dist/agent-src/skills/design-system-capture/references/import-procedure.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +18 -3
- package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
- 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/markitdown/SKILL.md +4 -0
- package/dist/agent-src/skills/monorepo-workspace/SKILL.md +1 -1
- package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
- 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 +25 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +30 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +302 -21
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
- package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/dist/cli/python/knowledge_ingest.js +1 -1
- package/dist/cli/python/workspace_drive.js +1 -1
- 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-UNCRJBT3.js → chunk-JQVYKIZA.js} +41 -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-YUM5KURG.js → chunk-KR6EM23A.js} +235 -1455
- package/dist/cli-delegate/{chunk-LTYVIY76.js → chunk-LIYTLH6O.js} +1 -1
- package/dist/cli-delegate/chunk-NQ7WT43P.js +2729 -0
- 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-HDVVGPQW.js → chunk-RQNNW7IF.js} +1405 -567
- package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-S4UMZNES.js} +1 -1
- package/dist/cli-delegate/{chunk-6KM6ORRY.js → chunk-S4YWNRKO.js} +24 -8
- package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-TRI6DQZ6.js} +3054 -325
- 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 +1862 -809
- 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 +132 -80
- 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 +7972 -5186
- package/dist/hooks/dispatch.sha256 +1 -0
- 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 +2226 -1055
- 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 +40913 -34865
- 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 +29 -0
- package/dist/shared/skillRanking.js +74 -13
- package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
- package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/docs/contracts/settings-classes.md +10 -6
- package/package.json +4 -4
- 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/app_spec.ts +10 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +302 -21
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
- package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/src/config/agent-settings.template.yml +91 -22
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/continuity-surface.json +9 -2
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +327 -3
- package/src/config/gate-violation-baselines.json +21 -9
- package/src/config/hook-bundle-budget.json +25 -0
- package/src/config/hook-latency-budget.json +21 -0
- package/src/config/hook-token-budget.json +8 -3
- package/src/config/host-instruction-limits.json +33 -0
- package/src/config/metric-registry.yml +114 -0
- package/src/config/placeholder-drift-budget.json +10 -0
- package/src/config/routing-coverage-seed.json +38 -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 +28 -90
- package/src/scripts/_cli/cmd_doctor_neighbours.ts +293 -0
- package/src/scripts/_cli/doctor_execution.ts +100 -17
- package/src/scripts/_cli/doctor_network_posture.ts +226 -0
- package/src/scripts/_dispatch.bash +16 -2
- package/src/scripts/_lib/agent_settings.ts +11 -0
- package/src/scripts/_lib/body_portable.ts +102 -0
- package/src/scripts/_lib/cc_transcript.ts +132 -1
- package/src/scripts/_lib/claude_settings_hooks.ts +10 -8
- package/src/scripts/_lib/concern_sla_window.ts +200 -0
- 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/forge_protection.ts +114 -0
- package/src/scripts/_lib/forge_reader.ts +558 -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_env_write.ts +111 -0
- 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/llm_proposer_transport.ts +14 -4
- 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/obligations.ts +119 -8
- package/src/scripts/_lib/probe_inputs.ts +271 -0
- package/src/scripts/_lib/prompt_shape.ts +46 -0
- package/src/scripts/_lib/read_surface_scan.ts +322 -0
- package/src/scripts/_lib/release_highlights.ts +2 -1
- package/src/scripts/_lib/release_migration_gate.ts +19 -3
- package/src/scripts/_lib/reserved_name_sweep.ts +73 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
- 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/shell_write_shapes.ts +108 -0
- package/src/scripts/_lib/skill_origin.ts +254 -0
- package/src/scripts/_lib/structural_hiding.ts +622 -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 +600 -4
- package/src/scripts/_lib/turn_end_transcript.ts +216 -0
- package/src/scripts/_lib/verification_command.ts +324 -0
- package/src/scripts/_lib/verification_evidence.ts +647 -0
- package/src/scripts/_lib/verify_clause.ts +280 -0
- 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/ai_team/team_dispatch.ts +4 -0
- package/src/scripts/audit_skill_overlap.ts +261 -0
- package/src/scripts/before_complete_hook.ts +573 -25
- package/src/scripts/bench_hook_latency.ts +412 -5
- package/src/scripts/bench_touched_file_quality.ts +238 -0
- package/src/scripts/cache_realization_report.ts +61 -1
- package/src/scripts/check_argument_hint.ts +287 -0
- package/src/scripts/check_claims.ts +125 -1
- package/src/scripts/check_enforcement_coverage.ts +38 -9
- package/src/scripts/check_enforcement_matrix.ts +621 -22
- package/src/scripts/check_estate_count.ts +130 -153
- package/src/scripts/check_finding_dispositions.ts +138 -7
- package/src/scripts/check_generator_sync.ts +71 -0
- package/src/scripts/check_hook_bundle_composition.ts +370 -0
- package/src/scripts/check_host_docs_digest.ts +786 -0
- package/src/scripts/check_host_format_column.ts +559 -0
- package/src/scripts/check_input_references.ts +228 -0
- package/src/scripts/check_inputs_parity.ts +235 -0
- package/src/scripts/check_kernel_edit_ratified.ts +89 -9
- package/src/scripts/check_kill_switch_table.ts +294 -0
- package/src/scripts/check_memory.ts +7 -1
- package/src/scripts/check_placeholder_drift.ts +221 -0
- package/src/scripts/check_read_surface_coverage.ts +515 -0
- package/src/scripts/check_release_highlights.ts +10 -0
- package/src/scripts/check_release_holds.ts +2 -1
- package/src/scripts/check_release_pr_shape.ts +38 -15
- package/src/scripts/check_routing_coverage.ts +378 -11
- package/src/scripts/check_standing_rule_delivery.ts +33 -13
- package/src/scripts/check_static_parity.ts +17 -0
- package/src/scripts/check_verify_expectation_delta.ts +521 -0
- package/src/scripts/ci_settle.ts +145 -19
- package/src/scripts/closure_scan.ts +115 -1
- 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 +90 -14
- package/src/scripts/condense.ts +14 -27
- package/src/scripts/estate_exemption_shape.ts +202 -0
- package/src/scripts/external_sources_denylist.json +29 -4
- package/src/scripts/generate_host_cost_table.ts +1 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +192 -14
- package/src/scripts/hooks/block_config_weakening.ts +294 -4
- package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
- package/src/scripts/hooks/block_plumbing_writes.ts +342 -0
- package/src/scripts/hooks/bundle_integrity.ts +291 -0
- package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
- package/src/scripts/hooks/code_graph_context_hook.ts +253 -25
- package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
- package/src/scripts/hooks/concern_failure_policy.ts +280 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/concern_timings.ts +94 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
- package/src/scripts/hooks/design_pass_hook.ts +100 -9
- package/src/scripts/hooks/design_slop_hook.ts +1 -2
- package/src/scripts/hooks/dispatch_hook.ts +77 -71
- package/src/scripts/hooks/edit_shape_hook.ts +1 -2
- package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
- package/src/scripts/hooks/evidence_independence.ts +1 -2
- package/src/scripts/hooks/exit_codes.ts +129 -0
- package/src/scripts/hooks/host_lowering.json +1 -0
- package/src/scripts/hooks/host_lowering.ts +146 -3
- package/src/scripts/hooks/host_lowering.yaml +262 -46
- package/src/scripts/hooks/host_semantics.ts +30 -2
- package/src/scripts/hooks/injection_budget.ts +9 -2
- package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
- package/src/scripts/hooks/journal_record_hook.ts +1 -2
- package/src/scripts/hooks/memory_recall_hook.ts +1 -2
- package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
- package/src/scripts/hooks/one_question_per_ask_hook.ts +43 -6
- package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
- package/src/scripts/hooks/payload_stub.ts +10 -1
- package/src/scripts/hooks/permission_gate.ts +1 -2
- package/src/scripts/hooks/push_settle_hook.ts +1 -3
- package/src/scripts/hooks/replay_hook.ts +2 -1
- package/src/scripts/hooks/reread_guard_hook.ts +1 -2
- package/src/scripts/hooks/review_baseline_hook.ts +1 -2
- package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
- package/src/scripts/hooks/rule_inject_hook.ts +765 -68
- package/src/scripts/hooks/run_continuation_hook.ts +13 -15
- package/src/scripts/hooks/skill_route_hook.ts +1 -2
- package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
- package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
- package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
- package/src/scripts/hooks/table_fingerprint.ts +70 -0
- package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
- package/src/scripts/hooks/telemetry_usage_hook.ts +65 -10
- package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
- package/src/scripts/hooks/turn_end_gate_hook.ts +421 -324
- package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
- package/src/scripts/hooks_status.ts +112 -0
- package/src/scripts/injection_scan_hook.ts +7 -0
- package/src/scripts/install.ts +75 -189
- 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_deny_text.ts +419 -0
- package/src/scripts/lint_exit_codes.ts +262 -0
- package/src/scripts/lint_hook_manifest.ts +108 -0
- package/src/scripts/lint_major_migration_sections.ts +156 -26
- package/src/scripts/lint_pack_boundaries.ts +63 -5
- package/src/scripts/lint_roadmap_blockers.ts +78 -12
- package/src/scripts/lint_rule_law_section.ts +517 -0
- package/src/scripts/lint_skill_descriptions.ts +1 -1
- package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
- 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/mcp_server/prompts.ts +142 -1
- package/src/scripts/measure_routing_signal.ts +54 -16
- package/src/scripts/measure_skill_ranker_baseline.ts +515 -53
- package/src/scripts/measure_turn_end_gate.ts +193 -0
- package/src/scripts/memory_learn_hook.ts +91 -6
- package/src/scripts/memory_lookup.ts +1 -1
- package/src/scripts/model_rule_injection.ts +52 -5
- package/src/scripts/prepack-check.mjs +53 -0
- package/src/scripts/project_thin_rules.ts +34 -327
- package/src/scripts/release_publication.ts +13 -1
- package/src/scripts/report_evidence_temperature.ts +657 -0
- package/src/scripts/report_installed_rule_links.ts +157 -0
- package/src/scripts/report_invocation_surface.ts +383 -0
- package/src/scripts/report_label_agreement.ts +233 -0
- package/src/scripts/report_obligation_carriers.ts +699 -0
- package/src/scripts/report_obligation_mechanism.ts +230 -0
- package/src/scripts/report_obligation_writer_split.ts +236 -0
- package/src/scripts/report_single_token_triggers.ts +277 -0
- package/src/scripts/report_skill_menu_census.ts +208 -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/roadmap_verify_share.ts +213 -0
- package/src/scripts/rule_activation_census.ts +5 -2
- package/src/scripts/rule_trigger_eval.ts +9 -9
- package/src/scripts/schemas/command.schema.json +44 -0
- package/src/scripts/schemas/review-findings.schema.json +7 -2
- package/src/scripts/schemas/rule.schema.json +5 -0
- package/src/scripts/schemas/skill.schema.json +44 -0
- package/src/scripts/self_review_gate.ts +421 -17
- package/src/scripts/semantic_parity_census.ts +314 -0
- package/src/scripts/session_eol_report.ts +54 -1
- package/src/scripts/skill_tools/score_skill_relevance.ts +290 -25
- package/src/scripts/sweep_dead_scan_roots.ts +1 -1
- 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 -7
- package/src/scripts/trigger_eval_rotation.ts +122 -27
- package/src/scripts/ui_conformance_probe.ts +156 -4
- package/src/scripts/update_prices.ts +85 -3
- package/src/scripts/write_bundle_digest.mjs +74 -0
- package/src/server/schemas/settings.ts +41 -0
- package/src/shared/skillRanking.ts +117 -11
- package/src/templates/consumer-settings/ONBOARDING.md +1 -0
- package/dist/cli-delegate/chunk-FXYUYU4N.js +0 -4416
- package/dist/cli-delegate/chunk-QJWAUMJW.js +0 -1072
- package/dist/cli-delegate/chunk-YDPILCOL.js +0 -152
- package/dist/ui/assets/index-DH1xc62v.js +0 -29
- package/dist/ui/assets/index-jSfMW8_t.css +0 -1
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
*
|
|
33
33
|
* TWO of the five are CONDITIONAL, and saying "unconditional" here would be the
|
|
34
34
|
* same stale-header defect this block corrects below. A and D are the
|
|
35
|
-
* completion-adjacent pair an open subagent dispatch excuses, so `
|
|
36
|
-
* them only when `dispatchOpen` is false; B, C and E run on every turn-end.
|
|
37
|
-
* narrowing is deliberate (Phase 3 Step 2, narrowed again by R2 round 2) and
|
|
38
|
-
* the third allow path, alongside the two re-entrancy layers — see the
|
|
39
|
-
* per-detector list in `
|
|
35
|
+
* completion-adjacent pair an open subagent dispatch excuses, so `runDetectors()`
|
|
36
|
+
* runs them only when `dispatchOpen` is false; B, C and E run on every turn-end.
|
|
37
|
+
* That narrowing is deliberate (Phase 3 Step 2, narrowed again by R2 round 2) and
|
|
38
|
+
* is the third allow path, alongside the two re-entrancy layers — see the
|
|
39
|
+
* per-detector list in `runDetectors()`.
|
|
40
40
|
*
|
|
41
41
|
* This block said "Three" and listed A/B/C until 2026-08-18, while `DetectorId`
|
|
42
42
|
* below has carried four since round 7. `DETECTOR_IDS` in
|
|
@@ -154,7 +154,6 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
154
154
|
import {
|
|
155
155
|
classify,
|
|
156
156
|
hasStableSessionId,
|
|
157
|
-
isSyntheticPrompt,
|
|
158
157
|
statePathFor as languageStatePathFor,
|
|
159
158
|
type Verdict,
|
|
160
159
|
} from '../language_mirror_hook.js';
|
|
@@ -163,7 +162,15 @@ import {
|
|
|
163
162
|
// write" is only true while the two agree, and importing a constant made that
|
|
164
163
|
// agreement invisible when the producer's layout moved. A builder makes the move
|
|
165
164
|
// a type error.
|
|
166
|
-
import {
|
|
165
|
+
import {
|
|
166
|
+
statePathFor as ciStatePathFor,
|
|
167
|
+
readTurnRunState,
|
|
168
|
+
type TurnRunState,
|
|
169
|
+
} from '../before_complete_hook.js';
|
|
170
|
+
// Re-exported: two test files address `readTurnRunState` at this path, and a
|
|
171
|
+
// reader arriving at detector C should find the reader where the detector uses
|
|
172
|
+
// it. The DEFINITION lives with the producer of the shape it reads.
|
|
173
|
+
export { readTurnRunState };
|
|
167
174
|
// The spec-backed options-block reading, imported rather than re-derived:
|
|
168
175
|
// `user-interaction` Iron Law 1's definition of an ask — a block PLUS its
|
|
169
176
|
// recommendation line — lives in exactly one place and detector E reads it from
|
|
@@ -179,9 +186,29 @@ import { statePathFor as ciStatePathFor } from '../before_complete_hook.js';
|
|
|
179
186
|
// Detector E, extracted to `_lib` where its prose costs no ratchet debt.
|
|
180
187
|
// Re-exported so every existing importer of this module is unchanged.
|
|
181
188
|
import { detectDroppedDecision } from '../_lib/dropped_decision.js';
|
|
189
|
+
// Step 3.2 of `road-to-a-graph-that-feeds-the-gate` — the shadow feeder. It
|
|
190
|
+
// lives in `_lib` for the reason every other extraction here does: this file
|
|
191
|
+
// sits against a 1,500-line shrink-only source budget, and the module it would
|
|
192
|
+
// otherwise grow into is one a recall reading has to be able to open on its own.
|
|
193
|
+
import {
|
|
194
|
+
appendFeederRow,
|
|
195
|
+
buildFeederRow,
|
|
196
|
+
type FeederLayer,
|
|
197
|
+
graphUntestedVerdict,
|
|
198
|
+
} from '../_lib/graph_feeder_record.js';
|
|
199
|
+
import { type GraphState, graphState } from '../code_graph/detect.js';
|
|
200
|
+
import { isVerificationCommand } from '../_lib/verification_command.js';
|
|
201
|
+
// Round 8 — the record path detector C prefers over its own regex. The regex
|
|
202
|
+
// reads a command's TEXT and `echo test` matches it; these read what the run
|
|
203
|
+
// exited with and what it printed.
|
|
204
|
+
import {
|
|
205
|
+
NO_RED_EVIDENCE_REASON,
|
|
206
|
+
describeRecordFinding as recordReason,
|
|
207
|
+
hasRedThenGreen,
|
|
208
|
+
readRunEvidence,
|
|
209
|
+
} from '../_lib/verification_evidence.js';
|
|
182
210
|
|
|
183
211
|
export { detectDroppedDecision };
|
|
184
|
-
import { isSafeTranscriptPath } from './end_review_nudge_hook.js';
|
|
185
212
|
import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
|
|
186
213
|
import { readHookStdin } from './hook_stdin.js';
|
|
187
214
|
import {
|
|
@@ -193,18 +220,24 @@ import { openRecordStats } from './subagent_ledger_hook.js';
|
|
|
193
220
|
import {
|
|
194
221
|
deriveSessionKey,
|
|
195
222
|
foldRefusal,
|
|
223
|
+
foldShadow,
|
|
196
224
|
parseRecord,
|
|
225
|
+
parseShadowRecord,
|
|
197
226
|
readInstallBoundary,
|
|
198
227
|
sessionRefusalFile,
|
|
228
|
+
sessionShadowFile,
|
|
229
|
+
shadowSetBy,
|
|
199
230
|
type RefusalRecord,
|
|
231
|
+
type ShadowLayer,
|
|
232
|
+
type ShadowRecord,
|
|
200
233
|
} from '../_lib/turn_end_refusals.js';
|
|
234
|
+
import { EXIT_ALLOW, EXIT_BLOCK } from './exit_codes.js';
|
|
201
235
|
|
|
202
236
|
/** Dispatcher-internal block code. Pinned to 1 by `concern_block_exit_parity`. */
|
|
203
|
-
const EXIT_BLOCK = 1;
|
|
204
|
-
const EXIT_ALLOW = 0;
|
|
205
237
|
|
|
206
238
|
/**
|
|
207
|
-
* Transcript-read ceiling for this hook, passed at
|
|
239
|
+
* Transcript-read ceiling for this hook, passed at its one production call
|
|
240
|
+
* site, `assembleDetectorInputs()`.
|
|
208
241
|
* Deliberately well under `isSafeTranscriptPath`'s own 50 MB refusal: the
|
|
209
242
|
* ordinal is a count over every entry, so the whole file is walked once per
|
|
210
243
|
* turn-end, and a session file grows for the life of the session. Past this the
|
|
@@ -227,6 +260,18 @@ export interface Finding {
|
|
|
227
260
|
/** The span that triggered it — quoted back so the refusal is actionable. */
|
|
228
261
|
evidence: string;
|
|
229
262
|
reason: string;
|
|
263
|
+
/**
|
|
264
|
+
* Which evidence path produced this finding, where a detector has two.
|
|
265
|
+
*
|
|
266
|
+
* Detectors C and F set it — the sentence here read "Only detector C" while
|
|
267
|
+
* F set it two functions below, which an independent review caught.
|
|
268
|
+
* `record` means a persisted run record was read;
|
|
269
|
+
* `transcript` means the recorder left nothing for this turn and the
|
|
270
|
+
* command-text scan answered instead — the mode a replay through
|
|
271
|
+
* `measure_turn_end_gate` / `check_detector_corpus` always runs in, so a
|
|
272
|
+
* corpus reading stays reproducible after the record path landed.
|
|
273
|
+
*/
|
|
274
|
+
mode?: 'record' | 'transcript';
|
|
230
275
|
}
|
|
231
276
|
|
|
232
277
|
// ---------------------------------------------------------------------------
|
|
@@ -487,6 +532,7 @@ export function readCiSettled(
|
|
|
487
532
|
return { seen: false, settled: false };
|
|
488
533
|
}
|
|
489
534
|
|
|
535
|
+
|
|
490
536
|
/**
|
|
491
537
|
* A completion claim in the delivered reply. Deliberately narrow: the German and
|
|
492
538
|
* English closings the corpus actually produced, anchored to a line so a mid-reply
|
|
@@ -656,62 +702,14 @@ export function detectLanguage(reply: string, pinned: Verdict): Finding | null {
|
|
|
656
702
|
const _EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
|
657
703
|
|
|
658
704
|
/**
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
* A positive list, and deliberately narrow. The alternative — treating any
|
|
662
|
-
* `Bash` call as verification — was rejected because `ls` would then clear an
|
|
663
|
-
* unverified edit, which is the failure mode `verify-before-complete` names
|
|
664
|
-
* ("relying on partial verification"). The cost of narrowness is a MISSED
|
|
665
|
-
* detection when a project verifies by some command not listed here, and that is
|
|
666
|
-
* the safe direction for a gate that can refuse a turn: a false negative costs
|
|
667
|
-
* one unguarded turn, a false positive teaches the user to switch the gate off.
|
|
668
|
-
*
|
|
669
|
-
* ## Audited 2026-08-17 — `road-to-stop-gate-honesty` step 2.2
|
|
670
|
-
*
|
|
671
|
-
* 52 commands this project's own surface actually produces (its `package.json`
|
|
672
|
-
* scripts, its `Taskfile` targets, `./scripts-run`, `./agent-config`, and the
|
|
673
|
-
* PHP / Python / Go / Rust toolchains) were run through
|
|
674
|
-
* `isVerificationCommand`. The table of every one, with its verdict, is
|
|
675
|
-
* `turn_end_verify_allowlist.test.ts`; that file is the fixture step 2.2
|
|
676
|
-
* requires, and every addition below has its own row in it.
|
|
677
|
-
*
|
|
678
|
-
* The draft that opened the roadmap expected to add `phpunit` and `pest`. Claim
|
|
679
|
-
* 4 had already overtaken that — both matched before this audit, as did
|
|
680
|
-
* `composer test` and `php artisan test` — so what the audit actually found was
|
|
681
|
-
* two different gaps:
|
|
682
|
-
*
|
|
683
|
-
* · `phpstan` — a static analyser of exactly the class already listed
|
|
684
|
-
* (`mypy`, `pyright`, `clippy`). Added.
|
|
685
|
-
* · `lint` followed by a WORD character — `lint_persistence`,
|
|
686
|
-
* `lint_provenance`, and every other `lint_*` script in `src/scripts/`
|
|
687
|
-
* missed, because `\blint\b` needs a boundary and `_` is a word character.
|
|
688
|
-
* `lint[-_:a-z]*` is the exact mirror of the `check[-_:a-z]*` this list
|
|
689
|
-
* already carried, which is why it is the narrow fix rather than a new idea.
|
|
705
|
+
* Re-exported, not re-derived: the selector moved to `_lib/verification_command.ts`
|
|
706
|
+
* so the evidence classifier can read it without a hook<->lib import cycle.
|
|
690
707
|
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
* · `npm run prepack` — a lifecycle hook whose content is per-project. Here it
|
|
696
|
-
* validates; elsewhere it copies files. Recognising it would clear an
|
|
697
|
-
* unverified edit in any repo whose prepack is a build step.
|
|
698
|
-
* · `task sync` / `task generate-tools` / `agent-config roadmap:progress` —
|
|
699
|
-
* GENERATORS. They rewrite the tree; they check nothing. A generator that
|
|
700
|
-
* cleared the gate would be the rubber stamp in its purest form.
|
|
701
|
-
* · `agent-config gates --all` — enumerates gates, runs none.
|
|
702
|
-
* · `vendor/bin/rector process --dry-run` — a refactoring tool. Its dry run
|
|
703
|
-
* prints a diff; it does not assert anything holds.
|
|
704
|
-
*
|
|
705
|
-
* `psalm` is the obvious sibling of `phpstan` and is also NOT added: nothing in
|
|
706
|
-
* the audited surface runs it, and "the team actually runs" is the step's own
|
|
707
|
-
* standard. It is named here so the next audit does not re-derive the question.
|
|
708
|
+
* The name stays exported HERE because two test files pin it at this path and
|
|
709
|
+
* because a reader arriving at detector C should find the selector where the
|
|
710
|
+
* detector uses it, not have to know it was relocated.
|
|
708
711
|
*/
|
|
709
|
-
|
|
710
|
-
/\b(test|tests|vitest|jest|pytest|phpunit|pest|tsc|eslint|ruff|mypy|pyright|clippy|phpstan|typecheck|lint[-_:a-z]*|build|ci|preflight|smoke[-_:a-z]*|check[-_:a-z]*|validate[-_:a-z]*)\b|\b(task|npm|pnpm|yarn|composer|cargo|go|make|php artisan|bun)\s+(run\s+)?\S*(test|check|lint|build|ci|typecheck)/i;
|
|
711
|
-
|
|
712
|
-
export function isVerificationCommand(command: string): boolean {
|
|
713
|
-
return _VERIFY_RE.test(command);
|
|
714
|
-
}
|
|
712
|
+
export { isVerificationCommand };
|
|
715
713
|
|
|
716
714
|
/**
|
|
717
715
|
* Fire when the turn changed a file and then ran nothing that could have
|
|
@@ -728,28 +726,55 @@ export function isVerificationCommand(command: string): boolean {
|
|
|
728
726
|
* no claim to verify, and refusing one would make the gate fire on the majority
|
|
729
727
|
* of turns, which is how a guard gets disabled.
|
|
730
728
|
*/
|
|
731
|
-
export function detectUnverifiedEdit(
|
|
729
|
+
export function detectUnverifiedEdit(
|
|
730
|
+
toolCalls: readonly ToolCall[],
|
|
731
|
+
runState: TurnRunState | null = null,
|
|
732
|
+
): Finding | null {
|
|
732
733
|
let lastEdit = -1;
|
|
733
734
|
for (let i = 0; i < toolCalls.length; i += 1) {
|
|
734
735
|
if (_EDIT_TOOLS.has(toolCalls[i]!.name)) lastEdit = i;
|
|
735
736
|
}
|
|
736
737
|
if (lastEdit === -1) return null;
|
|
738
|
+
const edited = toolCalls[lastEdit]!.path ?? toolCalls[lastEdit]!.name;
|
|
739
|
+
|
|
740
|
+
// THE RECORD PATH. Preferred whenever the recorder demonstrably witnessed
|
|
741
|
+
// this turn — see `readTurnRunState`, which returns null otherwise. The
|
|
742
|
+
// regex path below stays reachable and is not deprecated: it is the only
|
|
743
|
+
// path a transcript replay has, and it is what a host that binds no
|
|
744
|
+
// `post_tool_use` slot keeps.
|
|
745
|
+
if (runState !== null) {
|
|
746
|
+
const reading = readRunEvidence(runState);
|
|
747
|
+
if (reading.passed) return null;
|
|
748
|
+
// An instrument gap is never refused on (Risk 1 of the plan): the host
|
|
749
|
+
// surfaced no exit code, or wrote a record nothing can place, and
|
|
750
|
+
// neither is a fact about the operator's work. The transcript answer
|
|
751
|
+
// stands in, which is exactly the behavior before this path existed.
|
|
752
|
+
if (!reading.instrumentGap) {
|
|
753
|
+
return {
|
|
754
|
+
detector: 'verification',
|
|
755
|
+
evidence: edited,
|
|
756
|
+
reason: recordReason(reading.failed, reading.reasons),
|
|
757
|
+
mode: 'record',
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
737
762
|
for (let i = lastEdit + 1; i < toolCalls.length; i += 1) {
|
|
738
763
|
const c = toolCalls[i]!;
|
|
739
764
|
if (c.name === 'Bash' && c.command !== undefined && isVerificationCommand(c.command)) {
|
|
740
765
|
return null;
|
|
741
766
|
}
|
|
742
767
|
}
|
|
743
|
-
const edited = toolCalls[lastEdit]!.path;
|
|
744
768
|
return {
|
|
745
769
|
detector: 'verification',
|
|
746
770
|
// The path, never the diff: the evidence span is quoted into a refusal,
|
|
747
771
|
// and `ToolCall` is shaped so a file body cannot reach it.
|
|
748
|
-
evidence: edited
|
|
772
|
+
evidence: edited,
|
|
749
773
|
reason:
|
|
750
774
|
'this turn changed a file and then ran no verification command — ' +
|
|
751
775
|
'no test, type-check, lint or build call follows the last edit ' +
|
|
752
776
|
'(verify-before-complete: a claim without a fresh run is unverified)',
|
|
777
|
+
mode: 'transcript',
|
|
753
778
|
};
|
|
754
779
|
}
|
|
755
780
|
|
|
@@ -827,13 +852,55 @@ function _isProductionSource(p: string): boolean {
|
|
|
827
852
|
* turn; drop the second and it fires on a turn that did its job; drop the third
|
|
828
853
|
* and it fires mid-cycle on the red step of red-green-refactor.
|
|
829
854
|
*/
|
|
830
|
-
export function detectUntestedChange(
|
|
855
|
+
export function detectUntestedChange(
|
|
856
|
+
reply: string,
|
|
857
|
+
toolCalls: readonly ToolCall[],
|
|
858
|
+
runState: TurnRunState | null = null,
|
|
859
|
+
): Finding | null {
|
|
831
860
|
const edited = toolCalls
|
|
832
861
|
.filter((c) => _EDIT_TOOLS.has(c.name) && c.path !== undefined)
|
|
833
862
|
.map((c) => c.path as string);
|
|
834
863
|
const source = edited.filter(_isProductionSource);
|
|
835
864
|
if (source.length === 0) return null;
|
|
836
|
-
|
|
865
|
+
|
|
866
|
+
// THE TEST-FILE ESCAPE, and why step 5.1 narrowed it rather than removed it.
|
|
867
|
+
//
|
|
868
|
+
// "A test file was touched somewhere in the turn" is satisfied by a file
|
|
869
|
+
// containing `it('works', () => expect(true).toBe(true))`. A test never seen
|
|
870
|
+
// red has unknown sensitivity — it may assert nothing the change could
|
|
871
|
+
// break — so the escape as written accepted the shape of evidence instead of
|
|
872
|
+
// evidence.
|
|
873
|
+
//
|
|
874
|
+
// With records available, the escape asks for the cheapest observable proof
|
|
875
|
+
// that the assertion discriminates: the same target red, then green after
|
|
876
|
+
// the last edit. Without records it stays exactly as it was, because the
|
|
877
|
+
// transcript cannot see an exit code and a detector that refused on a
|
|
878
|
+
// transcript-only host would refuse every honest turn there.
|
|
879
|
+
// A NEW test file, not any test edit. Step 5.1 says "accepts a NEW test file
|
|
880
|
+
// only with that pair present", and `edited.some(_isTestPath)` is any
|
|
881
|
+
// test-path edit — so adjusting an assertion in an existing test alongside a
|
|
882
|
+
// production change and running it green once was refused. New-vs-existing
|
|
883
|
+
// is not decidable from a transcript; `Write` on a test path is the usable
|
|
884
|
+
// proxy, because `Edit` and `MultiEdit` presuppose a file that already
|
|
885
|
+
// existed. Narrowing here can only ALLOW turns the previous line refused.
|
|
886
|
+
const wroteNewTest = toolCalls.some(
|
|
887
|
+
(c) => c.name === 'Write' && c.path !== undefined && _isTestPath(c.path),
|
|
888
|
+
);
|
|
889
|
+
let noRedEvidence = false;
|
|
890
|
+
if (edited.some(_isTestPath)) {
|
|
891
|
+
// An EXISTING test adjusted alongside a production change keeps the old
|
|
892
|
+
// escape untouched — `Edit` and `MultiEdit` presuppose a file that was
|
|
893
|
+
// already there, so nothing new was claimed and there is no new
|
|
894
|
+
// assertion whose sensitivity is unknown.
|
|
895
|
+
if (!wroteNewTest) return null;
|
|
896
|
+
if (runState === null) return null;
|
|
897
|
+
if (hasRedThenGreen(runState)) return null;
|
|
898
|
+
// An instrument gap is not a missing red. Same rule as detector C: the
|
|
899
|
+
// host surfaced no exit code, so nothing about the operator's work is
|
|
900
|
+
// known, and the pre-record behavior stands in.
|
|
901
|
+
if (readRunEvidence(runState).instrumentGap) return null;
|
|
902
|
+
noRedEvidence = true;
|
|
903
|
+
}
|
|
837
904
|
|
|
838
905
|
// The claim gate, reusing detector D's pair rather than a second dialect of
|
|
839
906
|
// "done" — two lists of completion phrasings would drift, and the negation
|
|
@@ -844,8 +911,21 @@ export function detectUntestedChange(reply: string, toolCalls: readonly ToolCall
|
|
|
844
911
|
if (_NEGATED_CLAIM_RE.test(_lineAround(prose, m.index))) return null;
|
|
845
912
|
|
|
846
913
|
const shown = source.slice(0, 3).join(', ');
|
|
914
|
+
if (noRedEvidence) {
|
|
915
|
+
return {
|
|
916
|
+
detector: 'untested',
|
|
917
|
+
evidence: shown + (source.length > 3 ? ` (+${String(source.length - 3)} more)` : ''),
|
|
918
|
+
mode: 'record',
|
|
919
|
+
reason: NO_RED_EVIDENCE_REASON,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
// `transcript` unconditionally: no record contributed to THIS verdict — it
|
|
923
|
+
// is reached because the turn touched no test file at all, which is read off
|
|
924
|
+
// the tool calls. Labelling it `record` because a run state happened to be
|
|
925
|
+
// available would misattribute the evidence.
|
|
847
926
|
return {
|
|
848
927
|
detector: 'untested',
|
|
928
|
+
mode: 'transcript',
|
|
849
929
|
evidence: shown + (source.length > 3 ? ` (+${String(source.length - 3)} more)` : ''),
|
|
850
930
|
reason:
|
|
851
931
|
'a completion claim over production code this turn changed, with NO test file ' +
|
|
@@ -984,199 +1064,20 @@ function markRefusedTurn(
|
|
|
984
1064
|
// Transcript
|
|
985
1065
|
// ---------------------------------------------------------------------------
|
|
986
1066
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
/** Edit/Write only — the file the turn changed, used as the evidence span. */
|
|
1002
|
-
path?: string;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
export interface TranscriptTail {
|
|
1006
|
-
lastAssistant: string;
|
|
1007
|
-
/**
|
|
1008
|
-
* Every assistant text of the CURRENT turn, in order — reset at each genuine
|
|
1009
|
-
* user prompt exactly like `toolCalls`, and for the same reason: a question
|
|
1010
|
-
* asked three turns ago was answered, a question asked earlier in THIS turn
|
|
1011
|
-
* was not. Detector E reads it; the other four read `lastAssistant`, which is
|
|
1012
|
-
* always the last element when this is non-empty.
|
|
1013
|
-
*
|
|
1014
|
-
* A tool-only assistant entry contributes NOTHING here. `_messageText`
|
|
1015
|
-
* returns null for it and the collector skips it, so the ordinary shape of a
|
|
1016
|
-
* working turn — prose, tool call, prose — produces two elements and not
|
|
1017
|
-
* three. That is the whole false-positive surface of detector E, and it is
|
|
1018
|
-
* closed here rather than in the detector.
|
|
1019
|
-
*/
|
|
1020
|
-
assistantTurnTexts: string[];
|
|
1021
|
-
/**
|
|
1022
|
-
* How many GENUINE user prompts the transcript carries — harness-injected
|
|
1023
|
-
* user-role entries excluded via `isSyntheticPrompt`. This is the turn's
|
|
1024
|
-
* identity; see `alreadyRefusedTurn` for why the prompt's text is not.
|
|
1025
|
-
*/
|
|
1026
|
-
turnOrdinal: number;
|
|
1027
|
-
/**
|
|
1028
|
-
* The tool calls of the CURRENT turn, in order — reset at every genuine user
|
|
1029
|
-
* prompt, so a verification from three turns ago cannot vouch for an edit
|
|
1030
|
-
* made now. `_messageText` keeps only `type === 'text'` blocks, which is why
|
|
1031
|
-
* this needed its own extraction rather than a reading of `lastAssistant`:
|
|
1032
|
-
* tool activity is not in the prose at all.
|
|
1033
|
-
*/
|
|
1034
|
-
toolCalls: ToolCall[];
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
/**
|
|
1038
|
-
* The last assistant text, plus the turn ordinal, from a Claude JSONL
|
|
1039
|
-
* transcript. Mirrors `chat_history._extract_claude_transcript_response` for
|
|
1040
|
-
* the assistant half.
|
|
1041
|
-
*
|
|
1042
|
-
* `isSyntheticPrompt` is applied to every user-role entry — the same filter
|
|
1043
|
-
* `language_mirror_hook` uses, and for the same reason it was added there
|
|
1044
|
-
* (round-5 § 6.5): a background-task notification and a `<system-reminder>`
|
|
1045
|
-
* both occupy the user role without being chat messages. Counting them would
|
|
1046
|
-
* move the turn ordinal mid-turn, which is R2 finding 3.
|
|
1047
|
-
*/
|
|
1048
|
-
export function readTranscriptTail(
|
|
1049
|
-
transcriptPath: string,
|
|
1050
|
-
opts: { homeDir?: string; maxBytes?: number } = {},
|
|
1051
|
-
): TranscriptTail {
|
|
1052
|
-
const empty: TranscriptTail = {
|
|
1053
|
-
lastAssistant: '',
|
|
1054
|
-
turnOrdinal: 0,
|
|
1055
|
-
toolCalls: [],
|
|
1056
|
-
assistantTurnTexts: [],
|
|
1057
|
-
};
|
|
1058
|
-
if (!transcriptPath || !isSafeTranscriptPath(transcriptPath, opts)) return empty;
|
|
1059
|
-
let lines: string[];
|
|
1060
|
-
try {
|
|
1061
|
-
// R2 finding 12: `maxBytes` was accepted in the options type and never
|
|
1062
|
-
// used, so the declared cap was decoration. It is enforced here as well
|
|
1063
|
-
// as inside `isSafeTranscriptPath`, because this is the read it bounds.
|
|
1064
|
-
// The whole file still has to be walked — the turn ordinal is a count
|
|
1065
|
-
// over all entries, not something a tail can answer — so the cap is the
|
|
1066
|
-
// guard, not an optimisation.
|
|
1067
|
-
if (opts.maxBytes !== undefined && fs.statSync(transcriptPath).size > opts.maxBytes) {
|
|
1068
|
-
return empty;
|
|
1069
|
-
}
|
|
1070
|
-
lines = fs.readFileSync(transcriptPath, 'utf-8').split('\n');
|
|
1071
|
-
} catch {
|
|
1072
|
-
return empty;
|
|
1073
|
-
}
|
|
1074
|
-
let lastAssistant = '';
|
|
1075
|
-
let turnOrdinal = 0;
|
|
1076
|
-
let toolCalls: ToolCall[] = [];
|
|
1077
|
-
let assistantTurnTexts: string[] = [];
|
|
1078
|
-
for (const rawLine of lines) {
|
|
1079
|
-
const line = rawLine.trim();
|
|
1080
|
-
if (!line) continue;
|
|
1081
|
-
let obj: unknown;
|
|
1082
|
-
try {
|
|
1083
|
-
obj = JSON.parse(line);
|
|
1084
|
-
} catch {
|
|
1085
|
-
continue;
|
|
1086
|
-
}
|
|
1087
|
-
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) continue;
|
|
1088
|
-
const entry = obj as Record<string, unknown>;
|
|
1089
|
-
// Sidechain entries are a SUBAGENT's conversation recorded in the same
|
|
1090
|
-
// JSONL. A subagent prompt is a genuine-looking user-role text entry
|
|
1091
|
-
// appended mid-turn, so counting it moves the ordinal within the turn —
|
|
1092
|
-
// finding 3's failure class in a new shape (R2 round 2, finding 3).
|
|
1093
|
-
if (entry['isSidechain'] === true) continue;
|
|
1094
|
-
const role = entry['type'];
|
|
1095
|
-
if (role !== 'assistant' && role !== 'user') continue;
|
|
1096
|
-
const msg = entry['message'];
|
|
1097
|
-
if (typeof msg !== 'object' || msg === null || Array.isArray(msg)) continue;
|
|
1098
|
-
const content = (msg as Record<string, unknown>)['content'];
|
|
1099
|
-
// Tool calls are read BEFORE the text guard: an assistant entry that is
|
|
1100
|
-
// only a tool_use block has no text at all, so `continue`-ing on a null
|
|
1101
|
-
// text would drop exactly the entries this detector exists to see.
|
|
1102
|
-
if (role === 'assistant') {
|
|
1103
|
-
toolCalls.push(...extractToolCalls(content));
|
|
1104
|
-
}
|
|
1105
|
-
const text = _messageText(content);
|
|
1106
|
-
if (text === null) continue;
|
|
1107
|
-
if (role === 'assistant') {
|
|
1108
|
-
lastAssistant = text;
|
|
1109
|
-
assistantTurnTexts.push(text);
|
|
1110
|
-
} else if (!isSyntheticPrompt(text)) {
|
|
1111
|
-
turnOrdinal += 1;
|
|
1112
|
-
// A genuine user prompt starts a new turn, so the previous turn's
|
|
1113
|
-
// tool activity stops counting. Without this reset, a verification
|
|
1114
|
-
// run three turns ago would vouch for an edit made now — the
|
|
1115
|
-
// "fresh" in edit-without-FRESH-verification is this line.
|
|
1116
|
-
toolCalls = [];
|
|
1117
|
-
// Same boundary, same reason, for detector E: an options block the
|
|
1118
|
-
// user has since replied to is answered, not dropped. Resetting on
|
|
1119
|
-
// the identical line is what makes "in the SAME user turn" true of
|
|
1120
|
-
// the array rather than merely intended.
|
|
1121
|
-
assistantTurnTexts = [];
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
return {
|
|
1125
|
-
lastAssistant: lastAssistant.trim(),
|
|
1126
|
-
turnOrdinal,
|
|
1127
|
-
toolCalls,
|
|
1128
|
-
assistantTurnTexts,
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
/**
|
|
1133
|
-
* Extract this entry's tool calls, keeping only name, shell command and target
|
|
1134
|
-
* path. A tool input can hold a whole file body; nothing but those three fields
|
|
1135
|
-
* is carried forward.
|
|
1136
|
-
*
|
|
1137
|
-
* Exported because `measure_turn_end_gate` scores detectors C and E over a real
|
|
1138
|
-
* transcript corpus and therefore has to rebuild the same `ToolCall[]` the gate
|
|
1139
|
-
* sees. A second extractor there would be a second dialect of "what the turn
|
|
1140
|
-
* did", and the measurement would then be of that dialect rather than of the
|
|
1141
|
-
* shipped gate — the exact population-parity defect that script's own header
|
|
1142
|
-
* documents twice.
|
|
1143
|
-
*/
|
|
1144
|
-
export function extractToolCalls(content: unknown): ToolCall[] {
|
|
1145
|
-
if (!Array.isArray(content)) return [];
|
|
1146
|
-
const out: ToolCall[] = [];
|
|
1147
|
-
for (const blk of content) {
|
|
1148
|
-
if (typeof blk !== 'object' || blk === null || Array.isArray(blk)) continue;
|
|
1149
|
-
const b = blk as Record<string, unknown>;
|
|
1150
|
-
if (b['type'] !== 'tool_use') continue;
|
|
1151
|
-
const name = b['name'];
|
|
1152
|
-
if (typeof name !== 'string') continue;
|
|
1153
|
-
const input = b['input'];
|
|
1154
|
-
const call: ToolCall = { name };
|
|
1155
|
-
if (typeof input === 'object' && input !== null && !Array.isArray(input)) {
|
|
1156
|
-
const inp = input as Record<string, unknown>;
|
|
1157
|
-
const cmd = inp['command'];
|
|
1158
|
-
if (typeof cmd === 'string') call.command = cmd;
|
|
1159
|
-
const p = inp['file_path'] ?? inp['path'] ?? inp['notebook_path'];
|
|
1160
|
-
if (typeof p === 'string') call.path = p;
|
|
1161
|
-
}
|
|
1162
|
-
out.push(call);
|
|
1163
|
-
}
|
|
1164
|
-
return out;
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
function _messageText(content: unknown): string | null {
|
|
1168
|
-
if (typeof content === 'string') return content;
|
|
1169
|
-
if (!Array.isArray(content)) return null;
|
|
1170
|
-
const parts: string[] = [];
|
|
1171
|
-
for (const blk of content) {
|
|
1172
|
-
if (typeof blk !== 'object' || blk === null || Array.isArray(blk)) continue;
|
|
1173
|
-
const b = blk as Record<string, unknown>;
|
|
1174
|
-
if (b['type'] !== 'text') continue;
|
|
1175
|
-
const t = b['text'];
|
|
1176
|
-
if (typeof t === 'string') parts.push(t);
|
|
1177
|
-
}
|
|
1178
|
-
return parts.length > 0 ? parts.join('\n') : null;
|
|
1179
|
-
}
|
|
1067
|
+
// Extracted to `_lib/turn_end_transcript.ts` — same move, same reason, as
|
|
1068
|
+
// detector E above: `_lib` is where the prose costs no ratchet debt, and
|
|
1069
|
+
// this file hit its 1,500-line source budget when the shadow read landed.
|
|
1070
|
+
// Re-exported so every existing importer of this module is unchanged;
|
|
1071
|
+
// `tests/scripts/turn_end_gate_hook.test.ts` addresses all three names at
|
|
1072
|
+
// this path.
|
|
1073
|
+
import { readTranscriptTail, type ToolCall } from '../_lib/turn_end_transcript.js';
|
|
1074
|
+
|
|
1075
|
+
export {
|
|
1076
|
+
extractToolCalls,
|
|
1077
|
+
readTranscriptTail,
|
|
1078
|
+
type ToolCall,
|
|
1079
|
+
type TranscriptTail,
|
|
1080
|
+
} from '../_lib/turn_end_transcript.js';
|
|
1180
1081
|
|
|
1181
1082
|
// ---------------------------------------------------------------------------
|
|
1182
1083
|
// main
|
|
@@ -1186,18 +1087,41 @@ function str(v: JsonValue | undefined): string {
|
|
|
1186
1087
|
return typeof v === 'string' ? v : '';
|
|
1187
1088
|
}
|
|
1188
1089
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1090
|
+
/**
|
|
1091
|
+
* Everything the detectors read, assembled once per stop event.
|
|
1092
|
+
*
|
|
1093
|
+
* Extracted for step 2.1 of `road-to-a-stop-that-holds`, and the extraction —
|
|
1094
|
+
* not the shadow write — is the load-bearing half. The contract's Q1 asks how
|
|
1095
|
+
* often a turn the gate ALLOWS on a re-entrancy layer would have been refused
|
|
1096
|
+
* again, which needs the detectors to run on the allow path. There were two
|
|
1097
|
+
* ways to get them there: call them from a second assembly written beside the
|
|
1098
|
+
* first, or assemble once and call from both. `obligation_settle_hook.ts`'s
|
|
1099
|
+
* header already rules out the first — "a detector whose shadow measurement
|
|
1100
|
+
* and live behaviour come from different code measures nothing" — so this
|
|
1101
|
+
* function exists so that there is exactly one assembly and exactly one
|
|
1102
|
+
* detector list, read by the live verdict and by the shadow alike.
|
|
1103
|
+
*
|
|
1104
|
+
* `null` means the turn has no assistant text, which is `main()`'s existing
|
|
1105
|
+
* allow and is not a detector input the shadow could have measured either.
|
|
1106
|
+
*/
|
|
1107
|
+
interface DetectorInputs {
|
|
1108
|
+
workspaceRoot: string;
|
|
1109
|
+
rawSessionId: string;
|
|
1110
|
+
sessionKey: string;
|
|
1111
|
+
turnOrdinal: number;
|
|
1112
|
+
promptId: string;
|
|
1113
|
+
lastAssistant: string;
|
|
1114
|
+
toolCalls: ToolCall[];
|
|
1115
|
+
turnTexts: string[];
|
|
1116
|
+
closingFromPayload: boolean;
|
|
1117
|
+
dispatchOpen: boolean;
|
|
1118
|
+
runState: TurnRunState | null;
|
|
1119
|
+
}
|
|
1200
1120
|
|
|
1121
|
+
function assembleDetectorInputs(
|
|
1122
|
+
envelope: JsonObject,
|
|
1123
|
+
payload: JsonObject,
|
|
1124
|
+
): DetectorInputs | null {
|
|
1201
1125
|
const workspaceRoot = str(envelope['workspace_root'] as JsonValue | undefined) || process.cwd();
|
|
1202
1126
|
|
|
1203
1127
|
// Layer 1b — an open subagent dispatch is an EXPLICIT allow
|
|
@@ -1219,7 +1143,8 @@ export function main(): number {
|
|
|
1219
1143
|
// suppressed ALL FOUR detectors. A pending dispatch explains a promissory
|
|
1220
1144
|
// closing (A) and an unsettled completion claim (D); it explains nothing
|
|
1221
1145
|
// about a language mismatch (B) or an unverified edit (C), and silencing
|
|
1222
|
-
// those was scope Step 2 never asked for. Applied per detector
|
|
1146
|
+
// those was scope Step 2 never asked for. Applied per detector in
|
|
1147
|
+
// `runDetectors`.
|
|
1223
1148
|
//
|
|
1224
1149
|
// R2 round 2, finding 1: the count is TTL-filtered inside
|
|
1225
1150
|
// `openRecordStats`. A leaked record from a dispatch that never returned
|
|
@@ -1252,7 +1177,7 @@ export function main(): number {
|
|
|
1252
1177
|
transcriptPath,
|
|
1253
1178
|
{ maxBytes: TRANSCRIPT_READ_MAX_BYTES },
|
|
1254
1179
|
);
|
|
1255
|
-
if (!lastAssistant) return
|
|
1180
|
+
if (!lastAssistant) return null;
|
|
1256
1181
|
|
|
1257
1182
|
// The closing reply comes from the field the host hands us when it supplies
|
|
1258
1183
|
// one, not only from a file the host writes asynchronously. The transcript
|
|
@@ -1272,42 +1197,57 @@ export function main(): number {
|
|
|
1272
1197
|
? [...assistantTurnTexts, payloadClosing]
|
|
1273
1198
|
: assistantTurnTexts;
|
|
1274
1199
|
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1277
|
-
// ordinal
|
|
1278
|
-
//
|
|
1279
|
-
|
|
1280
|
-
// than colliding, and this one collides. It is named here rather than
|
|
1281
|
-
// papered over — the degradation is toward UNDER-refusing, which is the safe
|
|
1282
|
-
// direction, and layer 1 still covers the host that sends the flag.
|
|
1200
|
+
// The host's own id for the prompt being processed. Recorded beside the
|
|
1201
|
+
// ordinal in the refusal marker — never used as the guard key. The
|
|
1202
|
+
// ordinal's drift is what both re-entrancy layers exist for, so a later
|
|
1203
|
+
// reading needs the pair to tell a drifted ordinal from a second prompt.
|
|
1204
|
+
const promptId = str((payload['prompt_id'] ?? payload['promptId']) as JsonValue | undefined);
|
|
1283
1205
|
// RAW id, kept beside the derived key rather than replaced by it:
|
|
1284
1206
|
// `readLanguagePin` addresses the producer's own per-session file, and the
|
|
1285
|
-
// producer keys that on the raw `session_id`. Passing `sessionKey`
|
|
1286
|
-
// read a path nothing writes — the same shape as the STATE_FILE break
|
|
1287
|
-
//
|
|
1288
|
-
// The host's own id for the prompt being processed. Read here and recorded
|
|
1289
|
-
// beside the ordinal in the refusal marker — never used as the guard key.
|
|
1290
|
-
// The ordinal's drift is what both re-entrancy layers exist for, so a later
|
|
1291
|
-
// reading needs the pair to tell a drifted ordinal from a second prompt.
|
|
1292
|
-
const promptId = str(
|
|
1293
|
-
(payload['prompt_id'] ?? payload['promptId']) as JsonValue | undefined,
|
|
1294
|
-
);
|
|
1207
|
+
// producer keys that on the raw `session_id`. Passing `sessionKey` there
|
|
1208
|
+
// would read a path nothing writes — the same shape as the STATE_FILE break
|
|
1209
|
+
// this pairing exists to close.
|
|
1295
1210
|
const rawSessionId = str(envelope['session_id'] as JsonValue | undefined) || '';
|
|
1296
1211
|
const sessionKey = deriveSessionKey(rawSessionId || 'unknown-session');
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
//
|
|
1300
|
-
//
|
|
1301
|
-
// each one — no promise, no pin mismatch, no unverified edit, no unsettled
|
|
1302
|
-
// completion claim ⇒ no finding ⇒ the turn ends. That is the whole of "fires
|
|
1303
|
-
// when it is warranted"; there is no second, configurable notion of warranted
|
|
1304
|
-
// layered on top of it.
|
|
1212
|
+
|
|
1213
|
+
// ONE read of the recorder's state, shared by detectors C and F. Two reads
|
|
1214
|
+
// would be two file opens on the stop slot for one answer, and — worse —
|
|
1215
|
+
// could disagree if a post-tool event landed between them.
|
|
1305
1216
|
//
|
|
1306
|
-
//
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1310
|
-
//
|
|
1217
|
+
// It used to sit AFTER the Layer-2 allow, with a comment saying "a retry
|
|
1218
|
+
// pays no file read it immediately discards". Step 2.1 makes that false
|
|
1219
|
+
// rather than leaving the comment to rot: a retry now RUNS the detectors
|
|
1220
|
+
// to record whether it would have been refused again, so the read is
|
|
1221
|
+
// consumed on that path too. The cost is one `readFileSync` per retry,
|
|
1222
|
+
// which is the budget line 2.1 pre-registers.
|
|
1223
|
+
const runState = readTurnRunState(workspaceRoot, rawSessionId);
|
|
1224
|
+
|
|
1225
|
+
return {
|
|
1226
|
+
workspaceRoot,
|
|
1227
|
+
rawSessionId,
|
|
1228
|
+
sessionKey,
|
|
1229
|
+
turnOrdinal,
|
|
1230
|
+
promptId,
|
|
1231
|
+
lastAssistant,
|
|
1232
|
+
toolCalls,
|
|
1233
|
+
turnTexts,
|
|
1234
|
+
closingFromPayload,
|
|
1235
|
+
dispatchOpen,
|
|
1236
|
+
runState,
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Run every detector over one assembled input set.
|
|
1242
|
+
*
|
|
1243
|
+
* The one list, called by the live verdict and by the shadow read. Its
|
|
1244
|
+
* ordering and its `dispatchOpen` ternaries are the gate's behaviour; nothing
|
|
1245
|
+
* about the shadow may change them, which is why the shadow calls this rather
|
|
1246
|
+
* than carrying a copy.
|
|
1247
|
+
*/
|
|
1248
|
+
function runDetectors(inputs: DetectorInputs): Finding[] {
|
|
1249
|
+
const { dispatchOpen, lastAssistant, workspaceRoot, rawSessionId, toolCalls, runState } =
|
|
1250
|
+
inputs;
|
|
1311
1251
|
const findings: Finding[] = [];
|
|
1312
1252
|
for (const f of [
|
|
1313
1253
|
// A and D are the completion-adjacent pair a pending dispatch excuses
|
|
@@ -1315,17 +1255,14 @@ export function main(): number {
|
|
|
1315
1255
|
// excused by anything about a dispatch and run unchanged.
|
|
1316
1256
|
dispatchOpen ? null : detectPromissory(lastAssistant),
|
|
1317
1257
|
detectLanguage(lastAssistant, readLanguagePin(workspaceRoot, rawSessionId)),
|
|
1318
|
-
detectUnverifiedEdit(toolCalls),
|
|
1319
|
-
// Round 7 § Phase 1 — detector D. It is NOT unconditional
|
|
1320
|
-
//
|
|
1321
|
-
//
|
|
1322
|
-
//
|
|
1323
|
-
//
|
|
1324
|
-
//
|
|
1325
|
-
//
|
|
1326
|
-
// gating is where the comment above says gating belongs — inside the
|
|
1327
|
-
// detector: no CI observed, or a settled read, or no completion claim
|
|
1328
|
-
// ⇒ no finding.
|
|
1258
|
+
detectUnverifiedEdit(toolCalls, runState),
|
|
1259
|
+
// Round 7 § Phase 1 — detector D. It is NOT unconditional: A and D are
|
|
1260
|
+
// both excused by an open dispatch, per the note above. It shipped with
|
|
1261
|
+
// its own settings flag one commit earlier and that flag is gone:
|
|
1262
|
+
// `hooks.turn_end_gate.*` was deleted on 2026-08-12 because a
|
|
1263
|
+
// default-off safety gate is an absent one. Its gating is where the
|
|
1264
|
+
// note above says gating belongs — inside the detector: no CI observed,
|
|
1265
|
+
// or a settled read, or no completion claim ⇒ no finding.
|
|
1329
1266
|
dispatchOpen
|
|
1330
1267
|
? null
|
|
1331
1268
|
: detectCompletionClaim(lastAssistant, readCiSettled(workspaceRoot, rawSessionId)),
|
|
@@ -1337,25 +1274,185 @@ export function main(): number {
|
|
|
1337
1274
|
// that produced the measured failure — IS the continuation that drops
|
|
1338
1275
|
// the decision, so narrowing E the same way would silence it in exactly
|
|
1339
1276
|
// the case it exists for.
|
|
1340
|
-
detectDroppedDecision(turnTexts, closingFromPayload),
|
|
1277
|
+
detectDroppedDecision(inputs.turnTexts, inputs.closingFromPayload),
|
|
1341
1278
|
// Detector F takes the OTHER side of that same question, and the two
|
|
1342
1279
|
// sitting adjacent is the clearest place to say why. E fires on a
|
|
1343
1280
|
// question already asked, which an open dispatch cannot excuse. F fires
|
|
1344
1281
|
// on a completion CLAIM, and a turn waiting on a subagent has not
|
|
1345
1282
|
// finished — so its closing is not the claim F is about. Same slot,
|
|
1346
1283
|
// opposite trigger, opposite treatment of `dispatchOpen`.
|
|
1347
|
-
dispatchOpen ? null : detectUntestedChange(lastAssistant, toolCalls),
|
|
1284
|
+
dispatchOpen ? null : detectUntestedChange(lastAssistant, toolCalls, runState),
|
|
1348
1285
|
]) {
|
|
1349
1286
|
if (f) findings.push(f);
|
|
1350
1287
|
}
|
|
1288
|
+
return findings;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Write the graph feeder row for ONE stop (step 3.2).
|
|
1293
|
+
*
|
|
1294
|
+
* Called from both stop paths with the findings that path already computed, so
|
|
1295
|
+
* the F verdict in the row is the SAME verdict the gate acted on — never a
|
|
1296
|
+
* second evaluation written beside it, which is the shape
|
|
1297
|
+
* `obligation_settle_hook.ts` names as measuring nothing.
|
|
1298
|
+
*
|
|
1299
|
+
* It changes no exit code, and the whole body is wrapped: a stop that crashed on
|
|
1300
|
+
* its own instrument would be a new failure mode on a path that is supposed to
|
|
1301
|
+
* cost nothing. The graph half is skipped outright unless the state is `fresh`
|
|
1302
|
+
* or `edited` — an answer from an index that predates the change is not evidence
|
|
1303
|
+
* about the change.
|
|
1304
|
+
*/
|
|
1305
|
+
function recordGraphFeeder(
|
|
1306
|
+
inputs: DetectorInputs,
|
|
1307
|
+
findings: readonly Finding[],
|
|
1308
|
+
layer: FeederLayer,
|
|
1309
|
+
): void {
|
|
1310
|
+
if (is_replay_mode()) return;
|
|
1311
|
+
try {
|
|
1312
|
+
let state: GraphState;
|
|
1313
|
+
try {
|
|
1314
|
+
state = graphState(inputs.workspaceRoot);
|
|
1315
|
+
} catch {
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
// No graph at all is the silent case, exactly as it is for the context
|
|
1319
|
+
// hook: a consumer who never builds one never grows this file.
|
|
1320
|
+
if (state === 'absent') return;
|
|
1321
|
+
// The SAME path set detector F reads, filtered by the SAME predicate.
|
|
1322
|
+
// A completion review found the two arms running over different inputs:
|
|
1323
|
+
// the feeder passed every edit path while F filters to production
|
|
1324
|
+
// source, so the graph arm would have fired on precisely the turns F is
|
|
1325
|
+
// silent for — a recall comparison between two detectors answering
|
|
1326
|
+
// different questions, which is worse than no comparison.
|
|
1327
|
+
const paths = inputs.toolCalls
|
|
1328
|
+
.filter((c) => _EDIT_TOOLS.has(c.name) && c.path !== undefined)
|
|
1329
|
+
.map((c) => c.path as string)
|
|
1330
|
+
.filter(_isProductionSource);
|
|
1331
|
+
const f = findings.find((x) => x.detector === 'untested');
|
|
1332
|
+
appendFeederRow(
|
|
1333
|
+
inputs.workspaceRoot,
|
|
1334
|
+
inputs.sessionKey,
|
|
1335
|
+
buildFeederRow({
|
|
1336
|
+
turn: inputs.turnOrdinal,
|
|
1337
|
+
layer,
|
|
1338
|
+
state,
|
|
1339
|
+
fFired: f !== undefined,
|
|
1340
|
+
fMode: f?.mode ?? null,
|
|
1341
|
+
paths,
|
|
1342
|
+
graph: graphUntestedVerdict(inputs.workspaceRoot, state, paths),
|
|
1343
|
+
}),
|
|
1344
|
+
);
|
|
1345
|
+
} catch {
|
|
1346
|
+
// An instrument is never a reason to change what the gate does.
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* Record what this retry WOULD have been refused for, then let it end.
|
|
1352
|
+
*
|
|
1353
|
+
* Never changes a verdict: every caller returns `EXIT_ALLOW` whatever happens
|
|
1354
|
+
* here, and the whole body is wrapped because a retry that crashed on its own
|
|
1355
|
+
* instrument would be a new failure mode on a path that previously did no work
|
|
1356
|
+
* at all. A retry with no findings still lands, as `retries_observed` without a
|
|
1357
|
+
* row — that is the only way Q1 can read below 1.
|
|
1358
|
+
*/
|
|
1359
|
+
function recordShadow(inputs: DetectorInputs, layer: ShadowLayer): void {
|
|
1360
|
+
if (is_replay_mode()) return;
|
|
1361
|
+
try {
|
|
1362
|
+
const findings = runDetectors(inputs);
|
|
1363
|
+
// On `refused_turn` the answer is `ours` by construction — that marker
|
|
1364
|
+
// is this gate's own and nothing else writes it. On `stop_hook_active`
|
|
1365
|
+
// the host sets the flag for ANY stop hook's block, so it has to be
|
|
1366
|
+
// read from our own refusal record rather than assumed.
|
|
1367
|
+
const setBy = layer === 'refused_turn'
|
|
1368
|
+
? 'ours'
|
|
1369
|
+
: shadowSetBy(inputs.workspaceRoot, inputs.sessionKey);
|
|
1370
|
+
recordGraphFeeder(inputs, findings, layer);
|
|
1371
|
+
const file = sessionShadowFile(inputs.workspaceRoot, inputs.sessionKey);
|
|
1372
|
+
let prev: ShadowRecord | null = null;
|
|
1373
|
+
try {
|
|
1374
|
+
prev = parseShadowRecord(fs.readFileSync(file, 'utf-8'));
|
|
1375
|
+
} catch {
|
|
1376
|
+
// Absent or unreadable prior state starts a fresh record.
|
|
1377
|
+
}
|
|
1378
|
+
atomic_write_json(
|
|
1379
|
+
file,
|
|
1380
|
+
foldShadow(prev, {
|
|
1381
|
+
detectors: findings.map((f) => f.detector),
|
|
1382
|
+
turnOrdinal: inputs.turnOrdinal,
|
|
1383
|
+
at: new Date().toISOString(),
|
|
1384
|
+
layer,
|
|
1385
|
+
setBy,
|
|
1386
|
+
}) as unknown as Record<string, unknown>,
|
|
1387
|
+
);
|
|
1388
|
+
} catch {
|
|
1389
|
+
// An instrument is never a reason to change what the gate does.
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
export function main(): number {
|
|
1394
|
+
let envelope: JsonObject;
|
|
1395
|
+
let payload: JsonObject;
|
|
1396
|
+
try {
|
|
1397
|
+
[envelope, payload] = unwrap(readHookStdin(), 'claude');
|
|
1398
|
+
} catch {
|
|
1399
|
+
return EXIT_ALLOW;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Layer 1 — the host's own answer to "did I already refuse this turn?".
|
|
1403
|
+
//
|
|
1404
|
+
// Still an unconditional allow. What changed in step 2.1 is that the turn
|
|
1405
|
+
// is MEASURED on the way out: the detectors run once against the same
|
|
1406
|
+
// inputs the live path would have built, and a finding becomes a
|
|
1407
|
+
// `would_refuse_again` row. The verdict is byte-identical to the version
|
|
1408
|
+
// that returned here immediately.
|
|
1409
|
+
if (payload['stop_hook_active'] === true) {
|
|
1410
|
+
try {
|
|
1411
|
+
const retryInputs = assembleDetectorInputs(envelope, payload);
|
|
1412
|
+
if (retryInputs !== null) recordShadow(retryInputs, 'stop_hook_active');
|
|
1413
|
+
} catch {
|
|
1414
|
+
// Assembly is throw-safe by construction today; the guard is here
|
|
1415
|
+
// because this path did NO work before 2.1, so a regression in any
|
|
1416
|
+
// reader it now calls must not turn a clean retry into a crash.
|
|
1417
|
+
}
|
|
1418
|
+
return EXIT_ALLOW;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const inputs = assembleDetectorInputs(envelope, payload);
|
|
1422
|
+
if (inputs === null) return EXIT_ALLOW;
|
|
1423
|
+
|
|
1424
|
+
// Layer 2 — keyed on the turn's ORDINAL, never on the prompt's text.
|
|
1425
|
+
// A host that sends no `session_id` shares one bucket AND one small-integer
|
|
1426
|
+
// ordinal namespace, so an unrelated session whose ordinal matches a stored
|
|
1427
|
+
// `refused_turn` reads as already-refused and goes unguarded. R2 round 2,
|
|
1428
|
+
// finding 16: the sibling hook documents its own bucket as degrading rather
|
|
1429
|
+
// than colliding, and this one collides. It is named here rather than
|
|
1430
|
+
// papered over — the degradation is toward UNDER-refusing, which is the safe
|
|
1431
|
+
// direction, and layer 1 still covers the host that sends the flag.
|
|
1432
|
+
if (alreadyRefusedTurn(inputs.workspaceRoot, inputs.sessionKey, inputs.turnOrdinal)) {
|
|
1433
|
+
recordShadow(inputs, 'refused_turn');
|
|
1434
|
+
return EXIT_ALLOW;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// B and C run on every turn-end; A and D run only when no dispatch is open.
|
|
1438
|
+
// For the detectors that DO run, the gating is INSIDE each one — no promise,
|
|
1439
|
+
// no pin mismatch, no unverified edit, no unsettled completion claim ⇒ no
|
|
1440
|
+
// finding ⇒ the turn ends. That is the whole of "fires when it is
|
|
1441
|
+
// warranted"; there is no second, configurable notion of warranted layered
|
|
1442
|
+
// on top of it.
|
|
1443
|
+
const findings = runDetectors(inputs);
|
|
1444
|
+
// The live verdict's own row. It is written BEFORE the branch below, so a
|
|
1445
|
+
// turn the gate allows and a turn it refuses are both in the record — a
|
|
1446
|
+
// recall reading needs the allowed turns as its negative arm.
|
|
1447
|
+
recordGraphFeeder(inputs, findings, 'live');
|
|
1351
1448
|
if (findings.length === 0) return EXIT_ALLOW;
|
|
1352
1449
|
|
|
1353
1450
|
markRefusedTurn(
|
|
1354
|
-
workspaceRoot,
|
|
1355
|
-
sessionKey,
|
|
1356
|
-
turnOrdinal,
|
|
1451
|
+
inputs.workspaceRoot,
|
|
1452
|
+
inputs.sessionKey,
|
|
1453
|
+
inputs.turnOrdinal,
|
|
1357
1454
|
findings.map((f) => f.detector),
|
|
1358
|
-
promptId,
|
|
1455
|
+
inputs.promptId,
|
|
1359
1456
|
);
|
|
1360
1457
|
|
|
1361
1458
|
const lines = findings.map(
|