@event4u/agent-config 16.0.0 → 16.2.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 +289 -140
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +74 -27
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/commands/work.md +4 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +47 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +19 -23
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/rules/ui-audit-gate.md +1 -2
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +80 -4
- package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +30 -5
- package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
- package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +218 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -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 +260 -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/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-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-7XGBX6ZH.js} +973 -555
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-FP36SWJD.js} +587 -141
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-IGD6ERX6.js} +62 -7
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-JNMVLWLR.js} +99 -14
- package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
- package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
- 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 +985 -831
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +7 -7
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +3 -3
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +90 -43
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +7682 -2772
- package/dist/hooks/dispatch.sha256 +1 -0
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2200 -1559
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3066 -1470
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +40 -5
- package/dist/shared/missionExecution.js +97 -0
- 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/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +28 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- 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 +260 -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/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 +168 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +36 -5
- package/src/config/continuity-surface.json +23 -2
- package/src/config/gate-coverage.yml +377 -13
- package/src/config/gate-violation-baselines.json +10 -6
- package/src/config/hook-latency-budget.json +21 -0
- package/src/config/hook-token-budget.json +4 -2
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/metric-registry.yml +114 -0
- package/src/config/placeholder-drift-budget.json +10 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +33 -5
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +7 -20
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +173 -0
- package/src/scripts/_cli/doctor_network_posture.ts +226 -0
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/body_portable.ts +102 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/cc_transcript.ts +132 -1
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/concern_sla_window.ts +200 -0
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/forge_protection.ts +381 -0
- package/src/scripts/_lib/forge_reader.ts +558 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/host_env_write.ts +111 -0
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +513 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/probe_inputs.ts +271 -0
- package/src/scripts/_lib/read_surface_scan.ts +322 -0
- package/src/scripts/_lib/release_highlights.ts +201 -11
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/shell_write_shapes.ts +108 -0
- package/src/scripts/_lib/structural_hiding.ts +622 -0
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/turn_end_refusals.ts +525 -4
- package/src/scripts/_lib/turn_end_transcript.ts +216 -0
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -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/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ai_team/team_dispatch.ts +4 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/before_complete_hook.ts +340 -23
- package/src/scripts/bench_hook_latency.ts +412 -5
- 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 +866 -0
- package/src/scripts/check_finding_dispositions.ts +22 -3
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +1025 -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 +47 -5
- package/src/scripts/check_kill_switch_table.ts +294 -0
- package/src/scripts/check_placeholder_drift.ts +221 -0
- package/src/scripts/check_preamble_payload_budget.ts +75 -2
- package/src/scripts/check_read_surface_coverage.ts +515 -0
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +568 -0
- package/src/scripts/check_routing_coverage.ts +378 -11
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_static_parity.ts +17 -0
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/ci_settle.ts +145 -19
- package/src/scripts/closure_scan.ts +447 -0
- package/src/scripts/compile_hook_manifest.ts +102 -18
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/external_sources_denylist.json +29 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +127 -11
- package/src/scripts/hooks/block_config_weakening.ts +229 -4
- package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
- package/src/scripts/hooks/block_plumbing_writes.ts +314 -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 +1 -2
- package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
- package/src/scripts/hooks/concern_registry.ts +6 -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 +221 -5
- package/src/scripts/hooks/design_slop_hook.ts +1 -2
- package/src/scripts/hooks/dispatch_hook.ts +25 -19
- 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 +97 -2
- package/src/scripts/hooks/exit_codes.ts +128 -0
- package/src/scripts/hooks/host_lowering.json +1 -0
- package/src/scripts/hooks/host_lowering.ts +128 -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 +539 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +328 -0
- package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
- package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
- 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 +55 -4
- package/src/scripts/hooks/run_continuation_hook.ts +30 -35
- 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 +49 -0
- package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
- package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
- package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
- package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
- package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
- package/src/scripts/hooks_doctor.ts +85 -0
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +109 -210
- package/src/scripts/janitor.ts +7 -0
- package/src/scripts/lint_decision_classes.ts +464 -0
- 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 +66 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +139 -12
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/lint_skill_descriptions.ts +1 -1
- package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
- package/src/scripts/mcp_server/prompts.ts +142 -1
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
- package/src/scripts/measure_turn_end_gate.ts +193 -0
- package/src/scripts/memory_lookup.ts +1 -1
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/prepack-check.mjs +53 -0
- package/src/scripts/release.ts +14 -98
- package/src/scripts/release_drill.ts +267 -1
- package/src/scripts/release_publication.ts +64 -1
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_invocation_surface.ts +383 -0
- package/src/scripts/report_obligation_carriers.ts +699 -0
- package/src/scripts/report_obligation_writer_split.ts +236 -0
- package/src/scripts/report_skill_menu_census.ts +208 -0
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/roadmap_verify_share.ts +213 -0
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +48 -0
- package/src/scripts/schemas/review-findings.schema.json +4 -0
- package/src/scripts/schemas/skill.schema.json +44 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/self_review_gate.ts +359 -10
- package/src/scripts/semantic_parity_census.ts +314 -0
- package/src/scripts/session_eol_report.ts +54 -1
- package/src/scripts/sweep_dead_scan_roots.ts +1 -1
- package/src/scripts/trigger_eval_grandfather.json +0 -3
- package/src/scripts/trigger_eval_rotation.ts +122 -27
- package/src/scripts/ui_conformance_probe.ts +774 -0
- package/src/scripts/update_prices.ts +85 -3
- package/src/scripts/write_bundle_digest.mjs +74 -0
- package/src/server/schemas/settings.ts +62 -7
- package/src/shared/missionExecution.ts +126 -0
- package/src/templates/consumer-settings/ONBOARDING.md +1 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
- package/dist/ui/assets/index-D0jaCRPM.js +0 -29
- package/dist/ui/assets/index-jSfMW8_t.css +0 -1
- /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
|
@@ -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,18 @@ 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
|
+
import { isVerificationCommand } from '../_lib/verification_command.js';
|
|
190
|
+
// Round 8 — the record path detector C prefers over its own regex. The regex
|
|
191
|
+
// reads a command's TEXT and `echo test` matches it; these read what the run
|
|
192
|
+
// exited with and what it printed.
|
|
193
|
+
import {
|
|
194
|
+
NO_RED_EVIDENCE_REASON,
|
|
195
|
+
describeRecordFinding as recordReason,
|
|
196
|
+
hasRedThenGreen,
|
|
197
|
+
readRunEvidence,
|
|
198
|
+
} from '../_lib/verification_evidence.js';
|
|
182
199
|
|
|
183
200
|
export { detectDroppedDecision };
|
|
184
|
-
import { isSafeTranscriptPath } from './end_review_nudge_hook.js';
|
|
185
201
|
import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
|
|
186
202
|
import { readHookStdin } from './hook_stdin.js';
|
|
187
203
|
import {
|
|
@@ -193,18 +209,23 @@ import { openRecordStats } from './subagent_ledger_hook.js';
|
|
|
193
209
|
import {
|
|
194
210
|
deriveSessionKey,
|
|
195
211
|
foldRefusal,
|
|
212
|
+
foldShadow,
|
|
196
213
|
parseRecord,
|
|
214
|
+
parseShadowRecord,
|
|
197
215
|
readInstallBoundary,
|
|
198
216
|
sessionRefusalFile,
|
|
217
|
+
sessionShadowFile,
|
|
199
218
|
type RefusalRecord,
|
|
219
|
+
type ShadowLayer,
|
|
220
|
+
type ShadowRecord,
|
|
200
221
|
} from '../_lib/turn_end_refusals.js';
|
|
222
|
+
import { EXIT_ALLOW, EXIT_BLOCK } from './exit_codes.js';
|
|
201
223
|
|
|
202
224
|
/** Dispatcher-internal block code. Pinned to 1 by `concern_block_exit_parity`. */
|
|
203
|
-
const EXIT_BLOCK = 1;
|
|
204
|
-
const EXIT_ALLOW = 0;
|
|
205
225
|
|
|
206
226
|
/**
|
|
207
|
-
* Transcript-read ceiling for this hook, passed at
|
|
227
|
+
* Transcript-read ceiling for this hook, passed at its one production call
|
|
228
|
+
* site, `assembleDetectorInputs()`.
|
|
208
229
|
* Deliberately well under `isSafeTranscriptPath`'s own 50 MB refusal: the
|
|
209
230
|
* ordinal is a count over every entry, so the whole file is walked once per
|
|
210
231
|
* turn-end, and a session file grows for the life of the session. Past this the
|
|
@@ -227,6 +248,18 @@ export interface Finding {
|
|
|
227
248
|
/** The span that triggered it — quoted back so the refusal is actionable. */
|
|
228
249
|
evidence: string;
|
|
229
250
|
reason: string;
|
|
251
|
+
/**
|
|
252
|
+
* Which evidence path produced this finding, where a detector has two.
|
|
253
|
+
*
|
|
254
|
+
* Detectors C and F set it — the sentence here read "Only detector C" while
|
|
255
|
+
* F set it two functions below, which an independent review caught.
|
|
256
|
+
* `record` means a persisted run record was read;
|
|
257
|
+
* `transcript` means the recorder left nothing for this turn and the
|
|
258
|
+
* command-text scan answered instead — the mode a replay through
|
|
259
|
+
* `measure_turn_end_gate` / `check_detector_corpus` always runs in, so a
|
|
260
|
+
* corpus reading stays reproducible after the record path landed.
|
|
261
|
+
*/
|
|
262
|
+
mode?: 'record' | 'transcript';
|
|
230
263
|
}
|
|
231
264
|
|
|
232
265
|
// ---------------------------------------------------------------------------
|
|
@@ -487,6 +520,7 @@ export function readCiSettled(
|
|
|
487
520
|
return { seen: false, settled: false };
|
|
488
521
|
}
|
|
489
522
|
|
|
523
|
+
|
|
490
524
|
/**
|
|
491
525
|
* A completion claim in the delivered reply. Deliberately narrow: the German and
|
|
492
526
|
* English closings the corpus actually produced, anchored to a line so a mid-reply
|
|
@@ -656,62 +690,14 @@ export function detectLanguage(reply: string, pinned: Verdict): Finding | null {
|
|
|
656
690
|
const _EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
|
657
691
|
|
|
658
692
|
/**
|
|
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.
|
|
693
|
+
* Re-exported, not re-derived: the selector moved to `_lib/verification_command.ts`
|
|
694
|
+
* so the evidence classifier can read it without a hook<->lib import cycle.
|
|
690
695
|
*
|
|
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.
|
|
696
|
+
* The name stays exported HERE because two test files pin it at this path and
|
|
697
|
+
* because a reader arriving at detector C should find the selector where the
|
|
698
|
+
* detector uses it, not have to know it was relocated.
|
|
708
699
|
*/
|
|
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
|
-
}
|
|
700
|
+
export { isVerificationCommand };
|
|
715
701
|
|
|
716
702
|
/**
|
|
717
703
|
* Fire when the turn changed a file and then ran nothing that could have
|
|
@@ -728,28 +714,55 @@ export function isVerificationCommand(command: string): boolean {
|
|
|
728
714
|
* no claim to verify, and refusing one would make the gate fire on the majority
|
|
729
715
|
* of turns, which is how a guard gets disabled.
|
|
730
716
|
*/
|
|
731
|
-
export function detectUnverifiedEdit(
|
|
717
|
+
export function detectUnverifiedEdit(
|
|
718
|
+
toolCalls: readonly ToolCall[],
|
|
719
|
+
runState: TurnRunState | null = null,
|
|
720
|
+
): Finding | null {
|
|
732
721
|
let lastEdit = -1;
|
|
733
722
|
for (let i = 0; i < toolCalls.length; i += 1) {
|
|
734
723
|
if (_EDIT_TOOLS.has(toolCalls[i]!.name)) lastEdit = i;
|
|
735
724
|
}
|
|
736
725
|
if (lastEdit === -1) return null;
|
|
726
|
+
const edited = toolCalls[lastEdit]!.path ?? toolCalls[lastEdit]!.name;
|
|
727
|
+
|
|
728
|
+
// THE RECORD PATH. Preferred whenever the recorder demonstrably witnessed
|
|
729
|
+
// this turn — see `readTurnRunState`, which returns null otherwise. The
|
|
730
|
+
// regex path below stays reachable and is not deprecated: it is the only
|
|
731
|
+
// path a transcript replay has, and it is what a host that binds no
|
|
732
|
+
// `post_tool_use` slot keeps.
|
|
733
|
+
if (runState !== null) {
|
|
734
|
+
const reading = readRunEvidence(runState);
|
|
735
|
+
if (reading.passed) return null;
|
|
736
|
+
// An instrument gap is never refused on (Risk 1 of the plan): the host
|
|
737
|
+
// surfaced no exit code, or wrote a record nothing can place, and
|
|
738
|
+
// neither is a fact about the operator's work. The transcript answer
|
|
739
|
+
// stands in, which is exactly the behavior before this path existed.
|
|
740
|
+
if (!reading.instrumentGap) {
|
|
741
|
+
return {
|
|
742
|
+
detector: 'verification',
|
|
743
|
+
evidence: edited,
|
|
744
|
+
reason: recordReason(reading.failed, reading.reasons),
|
|
745
|
+
mode: 'record',
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
737
750
|
for (let i = lastEdit + 1; i < toolCalls.length; i += 1) {
|
|
738
751
|
const c = toolCalls[i]!;
|
|
739
752
|
if (c.name === 'Bash' && c.command !== undefined && isVerificationCommand(c.command)) {
|
|
740
753
|
return null;
|
|
741
754
|
}
|
|
742
755
|
}
|
|
743
|
-
const edited = toolCalls[lastEdit]!.path;
|
|
744
756
|
return {
|
|
745
757
|
detector: 'verification',
|
|
746
758
|
// The path, never the diff: the evidence span is quoted into a refusal,
|
|
747
759
|
// and `ToolCall` is shaped so a file body cannot reach it.
|
|
748
|
-
evidence: edited
|
|
760
|
+
evidence: edited,
|
|
749
761
|
reason:
|
|
750
762
|
'this turn changed a file and then ran no verification command — ' +
|
|
751
763
|
'no test, type-check, lint or build call follows the last edit ' +
|
|
752
764
|
'(verify-before-complete: a claim without a fresh run is unverified)',
|
|
765
|
+
mode: 'transcript',
|
|
753
766
|
};
|
|
754
767
|
}
|
|
755
768
|
|
|
@@ -827,13 +840,55 @@ function _isProductionSource(p: string): boolean {
|
|
|
827
840
|
* turn; drop the second and it fires on a turn that did its job; drop the third
|
|
828
841
|
* and it fires mid-cycle on the red step of red-green-refactor.
|
|
829
842
|
*/
|
|
830
|
-
export function detectUntestedChange(
|
|
843
|
+
export function detectUntestedChange(
|
|
844
|
+
reply: string,
|
|
845
|
+
toolCalls: readonly ToolCall[],
|
|
846
|
+
runState: TurnRunState | null = null,
|
|
847
|
+
): Finding | null {
|
|
831
848
|
const edited = toolCalls
|
|
832
849
|
.filter((c) => _EDIT_TOOLS.has(c.name) && c.path !== undefined)
|
|
833
850
|
.map((c) => c.path as string);
|
|
834
851
|
const source = edited.filter(_isProductionSource);
|
|
835
852
|
if (source.length === 0) return null;
|
|
836
|
-
|
|
853
|
+
|
|
854
|
+
// THE TEST-FILE ESCAPE, and why step 5.1 narrowed it rather than removed it.
|
|
855
|
+
//
|
|
856
|
+
// "A test file was touched somewhere in the turn" is satisfied by a file
|
|
857
|
+
// containing `it('works', () => expect(true).toBe(true))`. A test never seen
|
|
858
|
+
// red has unknown sensitivity — it may assert nothing the change could
|
|
859
|
+
// break — so the escape as written accepted the shape of evidence instead of
|
|
860
|
+
// evidence.
|
|
861
|
+
//
|
|
862
|
+
// With records available, the escape asks for the cheapest observable proof
|
|
863
|
+
// that the assertion discriminates: the same target red, then green after
|
|
864
|
+
// the last edit. Without records it stays exactly as it was, because the
|
|
865
|
+
// transcript cannot see an exit code and a detector that refused on a
|
|
866
|
+
// transcript-only host would refuse every honest turn there.
|
|
867
|
+
// A NEW test file, not any test edit. Step 5.1 says "accepts a NEW test file
|
|
868
|
+
// only with that pair present", and `edited.some(_isTestPath)` is any
|
|
869
|
+
// test-path edit — so adjusting an assertion in an existing test alongside a
|
|
870
|
+
// production change and running it green once was refused. New-vs-existing
|
|
871
|
+
// is not decidable from a transcript; `Write` on a test path is the usable
|
|
872
|
+
// proxy, because `Edit` and `MultiEdit` presuppose a file that already
|
|
873
|
+
// existed. Narrowing here can only ALLOW turns the previous line refused.
|
|
874
|
+
const wroteNewTest = toolCalls.some(
|
|
875
|
+
(c) => c.name === 'Write' && c.path !== undefined && _isTestPath(c.path),
|
|
876
|
+
);
|
|
877
|
+
let noRedEvidence = false;
|
|
878
|
+
if (edited.some(_isTestPath)) {
|
|
879
|
+
// An EXISTING test adjusted alongside a production change keeps the old
|
|
880
|
+
// escape untouched — `Edit` and `MultiEdit` presuppose a file that was
|
|
881
|
+
// already there, so nothing new was claimed and there is no new
|
|
882
|
+
// assertion whose sensitivity is unknown.
|
|
883
|
+
if (!wroteNewTest) return null;
|
|
884
|
+
if (runState === null) return null;
|
|
885
|
+
if (hasRedThenGreen(runState)) return null;
|
|
886
|
+
// An instrument gap is not a missing red. Same rule as detector C: the
|
|
887
|
+
// host surfaced no exit code, so nothing about the operator's work is
|
|
888
|
+
// known, and the pre-record behavior stands in.
|
|
889
|
+
if (readRunEvidence(runState).instrumentGap) return null;
|
|
890
|
+
noRedEvidence = true;
|
|
891
|
+
}
|
|
837
892
|
|
|
838
893
|
// The claim gate, reusing detector D's pair rather than a second dialect of
|
|
839
894
|
// "done" — two lists of completion phrasings would drift, and the negation
|
|
@@ -844,8 +899,21 @@ export function detectUntestedChange(reply: string, toolCalls: readonly ToolCall
|
|
|
844
899
|
if (_NEGATED_CLAIM_RE.test(_lineAround(prose, m.index))) return null;
|
|
845
900
|
|
|
846
901
|
const shown = source.slice(0, 3).join(', ');
|
|
902
|
+
if (noRedEvidence) {
|
|
903
|
+
return {
|
|
904
|
+
detector: 'untested',
|
|
905
|
+
evidence: shown + (source.length > 3 ? ` (+${String(source.length - 3)} more)` : ''),
|
|
906
|
+
mode: 'record',
|
|
907
|
+
reason: NO_RED_EVIDENCE_REASON,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
// `transcript` unconditionally: no record contributed to THIS verdict — it
|
|
911
|
+
// is reached because the turn touched no test file at all, which is read off
|
|
912
|
+
// the tool calls. Labelling it `record` because a run state happened to be
|
|
913
|
+
// available would misattribute the evidence.
|
|
847
914
|
return {
|
|
848
915
|
detector: 'untested',
|
|
916
|
+
mode: 'transcript',
|
|
849
917
|
evidence: shown + (source.length > 3 ? ` (+${String(source.length - 3)} more)` : ''),
|
|
850
918
|
reason:
|
|
851
919
|
'a completion claim over production code this turn changed, with NO test file ' +
|
|
@@ -984,199 +1052,20 @@ function markRefusedTurn(
|
|
|
984
1052
|
// Transcript
|
|
985
1053
|
// ---------------------------------------------------------------------------
|
|
986
1054
|
|
|
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
|
-
}
|
|
1055
|
+
// Extracted to `_lib/turn_end_transcript.ts` — same move, same reason, as
|
|
1056
|
+
// detector E above: `_lib` is where the prose costs no ratchet debt, and
|
|
1057
|
+
// this file hit its 1,500-line source budget when the shadow read landed.
|
|
1058
|
+
// Re-exported so every existing importer of this module is unchanged;
|
|
1059
|
+
// `tests/scripts/turn_end_gate_hook.test.ts` addresses all three names at
|
|
1060
|
+
// this path.
|
|
1061
|
+
import { readTranscriptTail, type ToolCall } from '../_lib/turn_end_transcript.js';
|
|
1062
|
+
|
|
1063
|
+
export {
|
|
1064
|
+
extractToolCalls,
|
|
1065
|
+
readTranscriptTail,
|
|
1066
|
+
type ToolCall,
|
|
1067
|
+
type TranscriptTail,
|
|
1068
|
+
} from '../_lib/turn_end_transcript.js';
|
|
1180
1069
|
|
|
1181
1070
|
// ---------------------------------------------------------------------------
|
|
1182
1071
|
// main
|
|
@@ -1186,18 +1075,41 @@ function str(v: JsonValue | undefined): string {
|
|
|
1186
1075
|
return typeof v === 'string' ? v : '';
|
|
1187
1076
|
}
|
|
1188
1077
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1078
|
+
/**
|
|
1079
|
+
* Everything the detectors read, assembled once per stop event.
|
|
1080
|
+
*
|
|
1081
|
+
* Extracted for step 2.1 of `road-to-a-stop-that-holds`, and the extraction —
|
|
1082
|
+
* not the shadow write — is the load-bearing half. The contract's Q1 asks how
|
|
1083
|
+
* often a turn the gate ALLOWS on a re-entrancy layer would have been refused
|
|
1084
|
+
* again, which needs the detectors to run on the allow path. There were two
|
|
1085
|
+
* ways to get them there: call them from a second assembly written beside the
|
|
1086
|
+
* first, or assemble once and call from both. `obligation_settle_hook.ts`'s
|
|
1087
|
+
* header already rules out the first — "a detector whose shadow measurement
|
|
1088
|
+
* and live behaviour come from different code measures nothing" — so this
|
|
1089
|
+
* function exists so that there is exactly one assembly and exactly one
|
|
1090
|
+
* detector list, read by the live verdict and by the shadow alike.
|
|
1091
|
+
*
|
|
1092
|
+
* `null` means the turn has no assistant text, which is `main()`'s existing
|
|
1093
|
+
* allow and is not a detector input the shadow could have measured either.
|
|
1094
|
+
*/
|
|
1095
|
+
interface DetectorInputs {
|
|
1096
|
+
workspaceRoot: string;
|
|
1097
|
+
rawSessionId: string;
|
|
1098
|
+
sessionKey: string;
|
|
1099
|
+
turnOrdinal: number;
|
|
1100
|
+
promptId: string;
|
|
1101
|
+
lastAssistant: string;
|
|
1102
|
+
toolCalls: ToolCall[];
|
|
1103
|
+
turnTexts: string[];
|
|
1104
|
+
closingFromPayload: boolean;
|
|
1105
|
+
dispatchOpen: boolean;
|
|
1106
|
+
runState: TurnRunState | null;
|
|
1107
|
+
}
|
|
1200
1108
|
|
|
1109
|
+
function assembleDetectorInputs(
|
|
1110
|
+
envelope: JsonObject,
|
|
1111
|
+
payload: JsonObject,
|
|
1112
|
+
): DetectorInputs | null {
|
|
1201
1113
|
const workspaceRoot = str(envelope['workspace_root'] as JsonValue | undefined) || process.cwd();
|
|
1202
1114
|
|
|
1203
1115
|
// Layer 1b — an open subagent dispatch is an EXPLICIT allow
|
|
@@ -1219,7 +1131,8 @@ export function main(): number {
|
|
|
1219
1131
|
// suppressed ALL FOUR detectors. A pending dispatch explains a promissory
|
|
1220
1132
|
// closing (A) and an unsettled completion claim (D); it explains nothing
|
|
1221
1133
|
// about a language mismatch (B) or an unverified edit (C), and silencing
|
|
1222
|
-
// those was scope Step 2 never asked for. Applied per detector
|
|
1134
|
+
// those was scope Step 2 never asked for. Applied per detector in
|
|
1135
|
+
// `runDetectors`.
|
|
1223
1136
|
//
|
|
1224
1137
|
// R2 round 2, finding 1: the count is TTL-filtered inside
|
|
1225
1138
|
// `openRecordStats`. A leaked record from a dispatch that never returned
|
|
@@ -1252,7 +1165,7 @@ export function main(): number {
|
|
|
1252
1165
|
transcriptPath,
|
|
1253
1166
|
{ maxBytes: TRANSCRIPT_READ_MAX_BYTES },
|
|
1254
1167
|
);
|
|
1255
|
-
if (!lastAssistant) return
|
|
1168
|
+
if (!lastAssistant) return null;
|
|
1256
1169
|
|
|
1257
1170
|
// The closing reply comes from the field the host hands us when it supplies
|
|
1258
1171
|
// one, not only from a file the host writes asynchronously. The transcript
|
|
@@ -1272,42 +1185,57 @@ export function main(): number {
|
|
|
1272
1185
|
? [...assistantTurnTexts, payloadClosing]
|
|
1273
1186
|
: assistantTurnTexts;
|
|
1274
1187
|
|
|
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.
|
|
1188
|
+
// The host's own id for the prompt being processed. Recorded beside the
|
|
1189
|
+
// ordinal in the refusal marker — never used as the guard key. The
|
|
1190
|
+
// ordinal's drift is what both re-entrancy layers exist for, so a later
|
|
1191
|
+
// reading needs the pair to tell a drifted ordinal from a second prompt.
|
|
1192
|
+
const promptId = str((payload['prompt_id'] ?? payload['promptId']) as JsonValue | undefined);
|
|
1283
1193
|
// RAW id, kept beside the derived key rather than replaced by it:
|
|
1284
1194
|
// `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
|
-
);
|
|
1195
|
+
// producer keys that on the raw `session_id`. Passing `sessionKey` there
|
|
1196
|
+
// would read a path nothing writes — the same shape as the STATE_FILE break
|
|
1197
|
+
// this pairing exists to close.
|
|
1295
1198
|
const rawSessionId = str(envelope['session_id'] as JsonValue | undefined) || '';
|
|
1296
1199
|
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.
|
|
1200
|
+
|
|
1201
|
+
// ONE read of the recorder's state, shared by detectors C and F. Two reads
|
|
1202
|
+
// would be two file opens on the stop slot for one answer, and — worse —
|
|
1203
|
+
// could disagree if a post-tool event landed between them.
|
|
1305
1204
|
//
|
|
1306
|
-
//
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1310
|
-
//
|
|
1205
|
+
// It used to sit AFTER the Layer-2 allow, with a comment saying "a retry
|
|
1206
|
+
// pays no file read it immediately discards". Step 2.1 makes that false
|
|
1207
|
+
// rather than leaving the comment to rot: a retry now RUNS the detectors
|
|
1208
|
+
// to record whether it would have been refused again, so the read is
|
|
1209
|
+
// consumed on that path too. The cost is one `readFileSync` per retry,
|
|
1210
|
+
// which is the budget line 2.1 pre-registers.
|
|
1211
|
+
const runState = readTurnRunState(workspaceRoot, rawSessionId);
|
|
1212
|
+
|
|
1213
|
+
return {
|
|
1214
|
+
workspaceRoot,
|
|
1215
|
+
rawSessionId,
|
|
1216
|
+
sessionKey,
|
|
1217
|
+
turnOrdinal,
|
|
1218
|
+
promptId,
|
|
1219
|
+
lastAssistant,
|
|
1220
|
+
toolCalls,
|
|
1221
|
+
turnTexts,
|
|
1222
|
+
closingFromPayload,
|
|
1223
|
+
dispatchOpen,
|
|
1224
|
+
runState,
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Run every detector over one assembled input set.
|
|
1230
|
+
*
|
|
1231
|
+
* The one list, called by the live verdict and by the shadow read. Its
|
|
1232
|
+
* ordering and its `dispatchOpen` ternaries are the gate's behaviour; nothing
|
|
1233
|
+
* about the shadow may change them, which is why the shadow calls this rather
|
|
1234
|
+
* than carrying a copy.
|
|
1235
|
+
*/
|
|
1236
|
+
function runDetectors(inputs: DetectorInputs): Finding[] {
|
|
1237
|
+
const { dispatchOpen, lastAssistant, workspaceRoot, rawSessionId, toolCalls, runState } =
|
|
1238
|
+
inputs;
|
|
1311
1239
|
const findings: Finding[] = [];
|
|
1312
1240
|
for (const f of [
|
|
1313
1241
|
// A and D are the completion-adjacent pair a pending dispatch excuses
|
|
@@ -1315,17 +1243,14 @@ export function main(): number {
|
|
|
1315
1243
|
// excused by anything about a dispatch and run unchanged.
|
|
1316
1244
|
dispatchOpen ? null : detectPromissory(lastAssistant),
|
|
1317
1245
|
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.
|
|
1246
|
+
detectUnverifiedEdit(toolCalls, runState),
|
|
1247
|
+
// Round 7 § Phase 1 — detector D. It is NOT unconditional: A and D are
|
|
1248
|
+
// both excused by an open dispatch, per the note above. It shipped with
|
|
1249
|
+
// its own settings flag one commit earlier and that flag is gone:
|
|
1250
|
+
// `hooks.turn_end_gate.*` was deleted on 2026-08-12 because a
|
|
1251
|
+
// default-off safety gate is an absent one. Its gating is where the
|
|
1252
|
+
// note above says gating belongs — inside the detector: no CI observed,
|
|
1253
|
+
// or a settled read, or no completion claim ⇒ no finding.
|
|
1329
1254
|
dispatchOpen
|
|
1330
1255
|
? null
|
|
1331
1256
|
: detectCompletionClaim(lastAssistant, readCiSettled(workspaceRoot, rawSessionId)),
|
|
@@ -1337,25 +1262,113 @@ export function main(): number {
|
|
|
1337
1262
|
// that produced the measured failure — IS the continuation that drops
|
|
1338
1263
|
// the decision, so narrowing E the same way would silence it in exactly
|
|
1339
1264
|
// the case it exists for.
|
|
1340
|
-
detectDroppedDecision(turnTexts, closingFromPayload),
|
|
1265
|
+
detectDroppedDecision(inputs.turnTexts, inputs.closingFromPayload),
|
|
1341
1266
|
// Detector F takes the OTHER side of that same question, and the two
|
|
1342
1267
|
// sitting adjacent is the clearest place to say why. E fires on a
|
|
1343
1268
|
// question already asked, which an open dispatch cannot excuse. F fires
|
|
1344
1269
|
// on a completion CLAIM, and a turn waiting on a subagent has not
|
|
1345
1270
|
// finished — so its closing is not the claim F is about. Same slot,
|
|
1346
1271
|
// opposite trigger, opposite treatment of `dispatchOpen`.
|
|
1347
|
-
dispatchOpen ? null : detectUntestedChange(lastAssistant, toolCalls),
|
|
1272
|
+
dispatchOpen ? null : detectUntestedChange(lastAssistant, toolCalls, runState),
|
|
1348
1273
|
]) {
|
|
1349
1274
|
if (f) findings.push(f);
|
|
1350
1275
|
}
|
|
1276
|
+
return findings;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Record what this retry WOULD have been refused for, then let it end.
|
|
1281
|
+
*
|
|
1282
|
+
* Never changes a verdict: every caller returns `EXIT_ALLOW` whatever happens
|
|
1283
|
+
* here, and the whole body is wrapped because a retry that crashed on its own
|
|
1284
|
+
* instrument would be a new failure mode on a path that previously did no work
|
|
1285
|
+
* at all. A retry with no findings still lands, as `retries_observed` without a
|
|
1286
|
+
* row — that is the only way Q1 can read below 1.
|
|
1287
|
+
*/
|
|
1288
|
+
function recordShadow(inputs: DetectorInputs, layer: ShadowLayer): void {
|
|
1289
|
+
if (is_replay_mode()) return;
|
|
1290
|
+
try {
|
|
1291
|
+
const findings = runDetectors(inputs);
|
|
1292
|
+
const file = sessionShadowFile(inputs.workspaceRoot, inputs.sessionKey);
|
|
1293
|
+
let prev: ShadowRecord | null = null;
|
|
1294
|
+
try {
|
|
1295
|
+
prev = parseShadowRecord(fs.readFileSync(file, 'utf-8'));
|
|
1296
|
+
} catch {
|
|
1297
|
+
// Absent or unreadable prior state starts a fresh record.
|
|
1298
|
+
}
|
|
1299
|
+
atomic_write_json(
|
|
1300
|
+
file,
|
|
1301
|
+
foldShadow(prev, {
|
|
1302
|
+
detectors: findings.map((f) => f.detector),
|
|
1303
|
+
turnOrdinal: inputs.turnOrdinal,
|
|
1304
|
+
at: new Date().toISOString(),
|
|
1305
|
+
layer,
|
|
1306
|
+
}) as unknown as Record<string, unknown>,
|
|
1307
|
+
);
|
|
1308
|
+
} catch {
|
|
1309
|
+
// An instrument is never a reason to change what the gate does.
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
export function main(): number {
|
|
1314
|
+
let envelope: JsonObject;
|
|
1315
|
+
let payload: JsonObject;
|
|
1316
|
+
try {
|
|
1317
|
+
[envelope, payload] = unwrap(readHookStdin(), 'claude');
|
|
1318
|
+
} catch {
|
|
1319
|
+
return EXIT_ALLOW;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Layer 1 — the host's own answer to "did I already refuse this turn?".
|
|
1323
|
+
//
|
|
1324
|
+
// Still an unconditional allow. What changed in step 2.1 is that the turn
|
|
1325
|
+
// is MEASURED on the way out: the detectors run once against the same
|
|
1326
|
+
// inputs the live path would have built, and a finding becomes a
|
|
1327
|
+
// `would_refuse_again` row. The verdict is byte-identical to the version
|
|
1328
|
+
// that returned here immediately.
|
|
1329
|
+
if (payload['stop_hook_active'] === true) {
|
|
1330
|
+
try {
|
|
1331
|
+
const retryInputs = assembleDetectorInputs(envelope, payload);
|
|
1332
|
+
if (retryInputs !== null) recordShadow(retryInputs, 'stop_hook_active');
|
|
1333
|
+
} catch {
|
|
1334
|
+
// Assembly is throw-safe by construction today; the guard is here
|
|
1335
|
+
// because this path did NO work before 2.1, so a regression in any
|
|
1336
|
+
// reader it now calls must not turn a clean retry into a crash.
|
|
1337
|
+
}
|
|
1338
|
+
return EXIT_ALLOW;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const inputs = assembleDetectorInputs(envelope, payload);
|
|
1342
|
+
if (inputs === null) return EXIT_ALLOW;
|
|
1343
|
+
|
|
1344
|
+
// Layer 2 — keyed on the turn's ORDINAL, never on the prompt's text.
|
|
1345
|
+
// A host that sends no `session_id` shares one bucket AND one small-integer
|
|
1346
|
+
// ordinal namespace, so an unrelated session whose ordinal matches a stored
|
|
1347
|
+
// `refused_turn` reads as already-refused and goes unguarded. R2 round 2,
|
|
1348
|
+
// finding 16: the sibling hook documents its own bucket as degrading rather
|
|
1349
|
+
// than colliding, and this one collides. It is named here rather than
|
|
1350
|
+
// papered over — the degradation is toward UNDER-refusing, which is the safe
|
|
1351
|
+
// direction, and layer 1 still covers the host that sends the flag.
|
|
1352
|
+
if (alreadyRefusedTurn(inputs.workspaceRoot, inputs.sessionKey, inputs.turnOrdinal)) {
|
|
1353
|
+
recordShadow(inputs, 'refused_turn');
|
|
1354
|
+
return EXIT_ALLOW;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// B and C run on every turn-end; A and D run only when no dispatch is open.
|
|
1358
|
+
// For the detectors that DO run, the gating is INSIDE each one — no promise,
|
|
1359
|
+
// no pin mismatch, no unverified edit, no unsettled completion claim ⇒ no
|
|
1360
|
+
// finding ⇒ the turn ends. That is the whole of "fires when it is
|
|
1361
|
+
// warranted"; there is no second, configurable notion of warranted layered
|
|
1362
|
+
// on top of it.
|
|
1363
|
+
const findings = runDetectors(inputs);
|
|
1351
1364
|
if (findings.length === 0) return EXIT_ALLOW;
|
|
1352
1365
|
|
|
1353
1366
|
markRefusedTurn(
|
|
1354
|
-
workspaceRoot,
|
|
1355
|
-
sessionKey,
|
|
1356
|
-
turnOrdinal,
|
|
1367
|
+
inputs.workspaceRoot,
|
|
1368
|
+
inputs.sessionKey,
|
|
1369
|
+
inputs.turnOrdinal,
|
|
1357
1370
|
findings.map((f) => f.detector),
|
|
1358
|
-
promptId,
|
|
1371
|
+
inputs.promptId,
|
|
1359
1372
|
);
|
|
1360
1373
|
|
|
1361
1374
|
const lines = findings.map(
|