@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
|
@@ -62,8 +62,19 @@ import {
|
|
|
62
62
|
prune_stale_session_states,
|
|
63
63
|
session_state_file,
|
|
64
64
|
update_json_under_lock,
|
|
65
|
+
owns_session_state,
|
|
65
66
|
} from "./hooks/state_io.js";
|
|
66
67
|
import { readHookStdin } from "./hooks/hook_stdin.js";
|
|
68
|
+
import { hookSectionValue } from "./_lib/hook_settings.js";
|
|
69
|
+
import { isVerificationCommand, mightBeVerification } from "./_lib/verification_command.js";
|
|
70
|
+
import {
|
|
71
|
+
advisoryLine,
|
|
72
|
+
runTouchedFileQuality,
|
|
73
|
+
touchedFilesAtStop,
|
|
74
|
+
type QualityRun,
|
|
75
|
+
} from "./_lib/touched_file_quality.js";
|
|
76
|
+
import { runnerOf } from "./_lib/verification_evidence.js";
|
|
77
|
+
import { resolve_toolchain } from "../agent-src/templates/scripts/work_engine/stack/runner.js";
|
|
67
78
|
|
|
68
79
|
// NOTE: the Python docstring says `agents/runtime/state/`, but the code
|
|
69
80
|
// constant is `agents/state/`. Replicated verbatim — latent docstring/code
|
|
@@ -144,28 +155,6 @@ export const COMMAND_TOOLS: ReadonlySet<string> = new Set([
|
|
|
144
155
|
"RunShellCommand", // Cursor
|
|
145
156
|
]);
|
|
146
157
|
|
|
147
|
-
// Permissive verification-command pattern. Observability — false positives
|
|
148
|
-
// are cheaper than false negatives. Word-boundary anchored on common
|
|
149
|
-
// shell separators so chained commands (`task sync && task ci`) match.
|
|
150
|
-
//
|
|
151
|
-
// Python re flags: IGNORECASE. The pattern uses no Python-specific syntax;
|
|
152
|
-
// it ports verbatim. The leading `(?:^|[\s;&|`(])` class includes backtick.
|
|
153
|
-
const _VERIFICATION_RE = new RegExp(
|
|
154
|
-
"(?:^|[\\s;&|`(])(" +
|
|
155
|
-
"task\\s+(?:ci|test|tests|lint|check|qa|phpstan|rector|ecs|pest|pytest)" +
|
|
156
|
-
"|(?:\\./|\\.venv/bin/|vendor/bin/)?(?:pest|phpunit|phpstan|psalm|rector|ecs)\\b" +
|
|
157
|
-
"|(?:python3?|\\.venv/bin/python3?)\\s+-m\\s+pytest" +
|
|
158
|
-
"|pytest\\b" +
|
|
159
|
-
"|(?:npm|pnpm|yarn|bun)\\s+(?:run\\s+)?(?:test|check|lint|typecheck|tsc)" +
|
|
160
|
-
"|cargo\\s+(?:test|check|clippy)" +
|
|
161
|
-
"|go\\s+test" +
|
|
162
|
-
"|make\\s+(?:test|check|lint)" +
|
|
163
|
-
"|composer\\s+(?:test|check|lint|phpstan)" +
|
|
164
|
-
"|(?:php\\s+)?artisan\\s+test" +
|
|
165
|
-
")",
|
|
166
|
-
"i",
|
|
167
|
-
);
|
|
168
|
-
|
|
169
158
|
type StateDict = Record<string, unknown>;
|
|
170
159
|
|
|
171
160
|
/** Python datetime.now(timezone.utc).isoformat(timespec="seconds"). */
|
|
@@ -193,10 +182,92 @@ function _empty_state(): StateDict {
|
|
|
193
182
|
// has to survive it: the measured failure is a completion claim in a LATER
|
|
194
183
|
// turn than the poll it rests on.
|
|
195
184
|
ci_last: null,
|
|
185
|
+
// Round 8 — see `_reset_turn`. Present in the empty state so a freshly
|
|
186
|
+
// created file has the same shape as one that has seen a turn boundary;
|
|
187
|
+
// a reader must never have to distinguish "no runs yet" from "key absent".
|
|
188
|
+
verification_runs: [],
|
|
189
|
+
edits_this_turn: 0,
|
|
190
|
+
// NOTE — `quality_runs` and `quality_files_source` are deliberately ABSENT
|
|
191
|
+
// here, against the convention `verification_runs` above follows.
|
|
192
|
+
//
|
|
193
|
+
// That convention ("present in the empty state so a reader never has to tell
|
|
194
|
+
// 'no runs yet' from 'key absent'") is the right default and it loses to a
|
|
195
|
+
// stronger one: the roadmap's first acceptance criterion is that with
|
|
196
|
+
// `touched_file_quality: off` the record is BYTE-IDENTICAL to the base ref.
|
|
197
|
+
// Seeding two keys would break that for every install that never opted in,
|
|
198
|
+
// and "the default-off feature changed my state file" is exactly the kind of
|
|
199
|
+
// silent shape drift a consumer cannot audit. The convention's cost here is
|
|
200
|
+
// zero, because the only reader of these keys is 2.1's analysis, which counts
|
|
201
|
+
// stops — and a stop with no key is a stop where the pass did not run, which
|
|
202
|
+
// is a distinction it needs rather than one it must not make.
|
|
196
203
|
checked_at: _now(),
|
|
197
204
|
};
|
|
198
205
|
}
|
|
199
206
|
|
|
207
|
+
/** The three values `hooks.verify_before_complete.touched_file_quality` may take. */
|
|
208
|
+
export type TouchedFileQualityMode = "off" | "shadow" | "warn";
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The configured mode, defaulting to `off`.
|
|
212
|
+
*
|
|
213
|
+
* FAIL-CLOSED ON THE FLAG, FAIL-OPEN ON THE READ, exactly as
|
|
214
|
+
* `hookSectionEnabled` does: an unreadable settings file, a missing key, or a
|
|
215
|
+
* value outside the enum all resolve to `off`. A typo must never turn a pass
|
|
216
|
+
* ON — `warn` emits a line the operator did not ask for, and `shadow` spawns
|
|
217
|
+
* processes, so an unrecognised string is the one case where guessing is
|
|
218
|
+
* strictly worse than doing nothing.
|
|
219
|
+
*/
|
|
220
|
+
export function readTouchedFileQualityMode(consumer_root: string): TouchedFileQualityMode {
|
|
221
|
+
const raw = hookSectionValue(consumer_root, "verify_before_complete", "touched_file_quality");
|
|
222
|
+
return raw === "shadow" || raw === "warn" ? raw : "off";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Run the shadow quality pass for this stop, or return nothing.
|
|
227
|
+
*
|
|
228
|
+
* Kept OUTSIDE the state lock on purpose. `update_json_under_lock` holds a lock
|
|
229
|
+
* around its mutator, and a mutator that spawns `tsc` would hold it for the
|
|
230
|
+
* whole typecheck — every concurrent concern writing this session's state would
|
|
231
|
+
* block behind a linter. The pass runs first, the result is handed to `_update`
|
|
232
|
+
* as data, and the lock stays as short as it was.
|
|
233
|
+
*/
|
|
234
|
+
export function collectTouchedFileQuality(
|
|
235
|
+
consumer_root: string,
|
|
236
|
+
session_id: string,
|
|
237
|
+
): { runs: QualityRun[]; source: string } | null {
|
|
238
|
+
let touched: { files: readonly string[]; source: string };
|
|
239
|
+
try {
|
|
240
|
+
touched = touchedFilesAtStop(consumer_root, session_id);
|
|
241
|
+
} catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
if (touched.files.length === 0) {
|
|
245
|
+
// Nothing to measure. Recorded as an empty run list with its source, not as
|
|
246
|
+
// silence: 2.1 counts "stops with edits" against "stops with at least one
|
|
247
|
+
// entry", and a stop that produced no entry has to be distinguishable from
|
|
248
|
+
// a stop where the pass never ran.
|
|
249
|
+
return { runs: [], source: touched.source };
|
|
250
|
+
}
|
|
251
|
+
let commands: readonly string[] = [];
|
|
252
|
+
try {
|
|
253
|
+
commands = resolve_toolchain(consumer_root).quality;
|
|
254
|
+
} catch {
|
|
255
|
+
commands = [];
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
return {
|
|
259
|
+
runs: runTouchedFileQuality({
|
|
260
|
+
root: consumer_root,
|
|
261
|
+
commands,
|
|
262
|
+
files: touched.files,
|
|
263
|
+
}),
|
|
264
|
+
source: touched.source,
|
|
265
|
+
};
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
200
271
|
|
|
201
272
|
/** Return [tool_name, command_text] from a tool-event payload. */
|
|
202
273
|
function _extract_command(payload: StateDict): [string | null, string | null] {
|
|
@@ -230,7 +301,7 @@ function _is_verification(command: string): boolean {
|
|
|
230
301
|
// settle is genuine, which is what the FC-3b guard below decides. Before this
|
|
231
302
|
// change a CI poll counted for nothing at all, so nothing observed the
|
|
232
303
|
// difference between "settled" and "never started".
|
|
233
|
-
return
|
|
304
|
+
return isVerificationCommand(command) || isCiPoll(command);
|
|
234
305
|
}
|
|
235
306
|
|
|
236
307
|
/**
|
|
@@ -334,11 +405,303 @@ function _extract_output(payload: StateDict): string | null {
|
|
|
334
405
|
return null;
|
|
335
406
|
}
|
|
336
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Tool names that CHANGE the tree, across the platforms this concern sees.
|
|
410
|
+
*
|
|
411
|
+
* The first four mirror `_EDIT_TOOLS` in the turn-end gate exactly; the rest are
|
|
412
|
+
* the same three tools under the names Augment, Cline and Cursor give them. The
|
|
413
|
+
* gate reads a Claude transcript and can afford four names; this recorder runs
|
|
414
|
+
* on every platform the manifest binds `post_tool_use` on, so it needs all of
|
|
415
|
+
* them — a missed edit name means `edits_this_turn` undercounts and a
|
|
416
|
+
* verification run looks LATER in the sequence than it was, which is the one
|
|
417
|
+
* direction that could clear an unverified edit.
|
|
418
|
+
*/
|
|
419
|
+
const EDIT_TOOLS: ReadonlySet<string> = new Set([
|
|
420
|
+
"Edit",
|
|
421
|
+
"Write",
|
|
422
|
+
"MultiEdit",
|
|
423
|
+
"NotebookEdit",
|
|
424
|
+
"str-replace-editor",
|
|
425
|
+
"save-file",
|
|
426
|
+
"str_replace_editor",
|
|
427
|
+
"create_file",
|
|
428
|
+
"write_to_file",
|
|
429
|
+
"replace_in_file",
|
|
430
|
+
"apply_diff",
|
|
431
|
+
"edit_file",
|
|
432
|
+
]);
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* How many run records one turn may keep.
|
|
436
|
+
*
|
|
437
|
+
* The state file is read by a stop-slot hook on every turn, so it is on the
|
|
438
|
+
* latency path and cannot grow without a ceiling. The NEWEST are kept because
|
|
439
|
+
* the only question asked of them is whether a passing run follows the LAST
|
|
440
|
+
* edit, which is always answered by the tail.
|
|
441
|
+
*/
|
|
442
|
+
export const MAX_VERIFICATION_RUNS_PER_TURN = 24;
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Characters of tool output retained per stream per record.
|
|
446
|
+
*
|
|
447
|
+
* CHARACTERS, not bytes, and the name is kept for its existing importers: the
|
|
448
|
+
* slice is applied to a JS string. Multibyte output can therefore exceed this
|
|
449
|
+
* many bytes, which is a bound on the state file rather than a promise about
|
|
450
|
+
* it, and 4,096 characters is far more than any runner's summary needs.
|
|
451
|
+
*/
|
|
452
|
+
export const RUN_OUTPUT_TAIL_BYTES = 4096;
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Apply the per-turn cap, keeping the newest AND the earliest failing record.
|
|
456
|
+
*
|
|
457
|
+
* Two detectors read this array and they ask opposite-ended questions. Detector
|
|
458
|
+
* C asks whether a pass follows the last edit — always answered by the tail.
|
|
459
|
+
* Detector F asks whether a red preceded the green — answered by the head. A
|
|
460
|
+
* plain `slice(-N)` serves the first and can silently drop the evidence the
|
|
461
|
+
* second needs, turning a turn that did honest red-green work into a
|
|
462
|
+
* `no_red_evidence` refusal once the run count passes the cap.
|
|
463
|
+
*/
|
|
464
|
+
function _cap_runs(runs: unknown[]): unknown[] {
|
|
465
|
+
if (runs.length <= MAX_VERIFICATION_RUNS_PER_TURN) return runs;
|
|
466
|
+
const tail = runs.slice(-(MAX_VERIFICATION_RUNS_PER_TURN - 1));
|
|
467
|
+
const head = runs.find((r) => {
|
|
468
|
+
if (r === null || typeof r !== "object" || Array.isArray(r)) return false;
|
|
469
|
+
const rec = r as StateDict;
|
|
470
|
+
const code = rec["exit_code"];
|
|
471
|
+
return typeof code === "number" && code !== 0;
|
|
472
|
+
});
|
|
473
|
+
if (head === undefined || tail.includes(head)) {
|
|
474
|
+
return runs.slice(-MAX_VERIFICATION_RUNS_PER_TURN);
|
|
475
|
+
}
|
|
476
|
+
return [head, ...tail];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* The prefix a host puts on a FAILED shell result when it carries no exit field.
|
|
481
|
+
*
|
|
482
|
+
* Claude Code's Bash tool does exactly this: a failing call's whole tool
|
|
483
|
+
* response is the string `Error: Exit code 1\n<output>`. Measured over 1,077
|
|
484
|
+
* object-shaped and 11 string-shaped tool results in this machine's own
|
|
485
|
+
* transcripts (2026-09-29): every string-shaped result carried this prefix, and
|
|
486
|
+
* no shape of either kind carried any of the numeric field names below.
|
|
487
|
+
*/
|
|
488
|
+
const _ERROR_EXIT_PREFIX = /^Error:\s*Exit code\s*(\d+)/i;
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* The same statement without the `Error:` word, which is how the host words it
|
|
492
|
+
* on its FAILURE event.
|
|
493
|
+
*
|
|
494
|
+
* Measured 2026-10-01 on Claude Code 2.1.286: a failing `Bash` call fires
|
|
495
|
+
* `PostToolUseFailure`, whose envelope carries NO `tool_response` at all. The
|
|
496
|
+
* exit status is a top-level `error` string whose first line reads
|
|
497
|
+
* `Exit code N`, with the command's own diagnostics on the lines after it. The
|
|
498
|
+
* prefix above never matches that, so for as long as only the success event was
|
|
499
|
+
* bound this recorder could not have written a non-zero exit even once — which
|
|
500
|
+
* is exactly what 24 consecutive all-zero records on this host turned out to
|
|
501
|
+
* mean.
|
|
502
|
+
*/
|
|
503
|
+
const _BARE_EXIT_PREFIX = /^\s*Exit code\s*(\d+)/i;
|
|
504
|
+
|
|
505
|
+
/** The status a post-tool payload actually reports, and HOW it reported it. */
|
|
506
|
+
interface ExitReading {
|
|
507
|
+
readonly code: number | null;
|
|
508
|
+
/** Provenance, recorded so a reader can tell an observed code from a derived one. */
|
|
509
|
+
readonly source: "field" | "error_prefix" | "response_shape" | null;
|
|
510
|
+
/** The host said the call was interrupted — a kill, not a verdict on the work. */
|
|
511
|
+
readonly interrupted: boolean;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const _NO_EXIT: ExitReading = { code: null, source: null, interrupted: false };
|
|
515
|
+
|
|
516
|
+
function _numeric_exit_field(obj: StateDict): number | null {
|
|
517
|
+
for (const key of ["exit_code", "exitCode", "returncode", "returnCode", "status_code"]) {
|
|
518
|
+
const v = obj[key];
|
|
519
|
+
if (typeof v === "number" && Number.isFinite(v)) return Math.trunc(v);
|
|
520
|
+
if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) {
|
|
521
|
+
return Math.trunc(Number(v));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* What the host said about how the command ended.
|
|
529
|
+
*
|
|
530
|
+
* `null` is a REAL value and is never normalised to 0: several hosts surface no
|
|
531
|
+
* exit status at all, and writing 0 for "the host said nothing" would
|
|
532
|
+
* manufacture the strongest possible evidence out of silence. The classifier's
|
|
533
|
+
* `exit_code_unavailable` verdict exists so that gap stays visible.
|
|
534
|
+
*
|
|
535
|
+
* THREE SOURCES, in descending directness, and the third is the one that took a
|
|
536
|
+
* measurement rather than a guess.
|
|
537
|
+
*
|
|
538
|
+
* · `field` — a numeric exit field, wherever a host provides one.
|
|
539
|
+
* · `error_prefix` — the `Error: Exit code N` string form above. This is a
|
|
540
|
+
* real non-zero code, stated by the host in the only place it states it.
|
|
541
|
+
* · `response_shape` — an OBJECT response carrying `interrupted: false`
|
|
542
|
+
* alongside a `stdout` or `stderr` key. On Claude Code that is the success
|
|
543
|
+
* form, and the discriminator is deliberately the PRESENCE of
|
|
544
|
+
* `interrupted: false` rather than the ABSENCE of an error prefix: a
|
|
545
|
+
* positive signal from the host, not an inference from silence. A shape
|
|
546
|
+
* that carries neither reads as `null` and stays an instrument gap.
|
|
547
|
+
*
|
|
548
|
+
* Without the second and third readings this recorder wrote `exit_code: null`
|
|
549
|
+
* for every command on the one host that binds the turn-end gate, so the whole
|
|
550
|
+
* record path was inert — found by an independent review of the branch that
|
|
551
|
+
* introduced it, against 1,077 real tool results.
|
|
552
|
+
*/
|
|
553
|
+
/**
|
|
554
|
+
* Did the HOST say this call failed, independently of anything in its text?
|
|
555
|
+
*
|
|
556
|
+
* Read from the native event name, which the host writes in two places and the
|
|
557
|
+
* agent in neither: `hook_event_name` inside the payload, and `native_event` on
|
|
558
|
+
* the dispatcher envelope. Either is authoritative; both are checked because a
|
|
559
|
+
* raw-payload invocation carries only the first and a synthesised envelope only
|
|
560
|
+
* the second.
|
|
561
|
+
*
|
|
562
|
+
* An explicit set of OBSERVED names, with a suffix match behind it as a declared
|
|
563
|
+
* fallback rather than as the primary rule. A reviewer called the suffix-only
|
|
564
|
+
* form a maintenance trap and was right about the direction: the set is what a
|
|
565
|
+
* reader checks against the manifest, and `PostToolUseFailure` is the one
|
|
566
|
+
* spelling this package has actually seen a host send.
|
|
567
|
+
*
|
|
568
|
+
* The fallback is kept, and keeping it is the deliberate half. A host whose
|
|
569
|
+
* failure event this tree has never observed is exactly the case where guessing
|
|
570
|
+
* wrong is cheapest in one direction and expensive in the other: a false
|
|
571
|
+
* positive costs a refusal to record a zero, a false negative costs a
|
|
572
|
+
* manufactured pass on a red run. So the set is the rule and the suffix is the
|
|
573
|
+
* safe default for a name nobody here has met yet.
|
|
574
|
+
*/
|
|
575
|
+
export const FAILURE_EVENT_NAMES: ReadonlySet<string> = new Set(["PostToolUseFailure"]);
|
|
576
|
+
|
|
577
|
+
function _is_failure_event(payload: StateDict, envelope: StateDict): boolean {
|
|
578
|
+
for (const v of [payload["hook_event_name"], envelope["native_event"]]) {
|
|
579
|
+
if (typeof v !== "string" || v === "") continue;
|
|
580
|
+
if (FAILURE_EVENT_NAMES.has(v)) return true;
|
|
581
|
+
if (/failure$/i.test(v)) return true;
|
|
582
|
+
}
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function _extract_exit_reading(payload: StateDict, failure_event = false): ExitReading {
|
|
587
|
+
// On an event the host itself named a FAILURE, a zero is never recorded —
|
|
588
|
+
// from any of the three readings. The host has already stated the call did not
|
|
589
|
+
// succeed, so a `0` arrived at by parsing its text would contradict the only
|
|
590
|
+
// authoritative thing in the envelope, and it would do so in the single most
|
|
591
|
+
// dangerous direction: manufacturing the strongest possible pass evidence out
|
|
592
|
+
// of a red run. `null` instead, which the classifier already treats as an
|
|
593
|
+
// instrument gap and never as a pass.
|
|
594
|
+
//
|
|
595
|
+
// Raised by an independent reviewer who asked what happens when a command's
|
|
596
|
+
// own error text begins `Exit code 0`. Nothing observed produces that, which
|
|
597
|
+
// is exactly why it is worth closing before something does.
|
|
598
|
+
const seal = (r: ExitReading): ExitReading =>
|
|
599
|
+
failure_event && r.code === 0 ? { code: null, source: null, interrupted: r.interrupted } : r;
|
|
600
|
+
|
|
601
|
+
const direct = _numeric_exit_field(payload);
|
|
602
|
+
if (direct !== null) return seal({ code: direct, source: "field", interrupted: false });
|
|
603
|
+
|
|
604
|
+
// The FAILURE envelope, read before the success keys because it carries none
|
|
605
|
+
// of them: no `tool_response`, the status in a top-level `error` string, and
|
|
606
|
+
// the interrupt flag spelled `is_interrupt` rather than the nested
|
|
607
|
+
// `interrupted` below. Interruption is checked FIRST — a killed call is a
|
|
608
|
+
// statement about the kill and not a verdict on the work, so a host that
|
|
609
|
+
// sends both must not have its kill read as a failing exit.
|
|
610
|
+
const interrupted_top = payload["is_interrupt"] === true;
|
|
611
|
+
const err = payload["error"];
|
|
612
|
+
if (typeof err === "string") {
|
|
613
|
+
if (interrupted_top) return { code: null, source: null, interrupted: true };
|
|
614
|
+
const bare = _BARE_EXIT_PREFIX.exec(err) ?? _ERROR_EXIT_PREFIX.exec(err);
|
|
615
|
+
if (bare?.[1] !== undefined) {
|
|
616
|
+
return seal({ code: Number(bare[1]), source: "error_prefix", interrupted: false });
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (interrupted_top) return { code: null, source: null, interrupted: true };
|
|
620
|
+
|
|
621
|
+
for (const key of ["tool_response", "toolResponse", "result", "output"]) {
|
|
622
|
+
const v = payload[key];
|
|
623
|
+
if (typeof v === "string") {
|
|
624
|
+
const m = _ERROR_EXIT_PREFIX.exec(v);
|
|
625
|
+
if (m?.[1] !== undefined) {
|
|
626
|
+
return seal({ code: Number(m[1]), source: "error_prefix", interrupted: false });
|
|
627
|
+
}
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) continue;
|
|
631
|
+
const obj = v as StateDict;
|
|
632
|
+
const nested = _numeric_exit_field(obj);
|
|
633
|
+
if (nested !== null) return seal({ code: nested, source: "field", interrupted: false });
|
|
634
|
+
if (obj["interrupted"] === true) return { code: null, source: null, interrupted: true };
|
|
635
|
+
const hasStream = typeof obj["stdout"] === "string" || typeof obj["stderr"] === "string";
|
|
636
|
+
if (obj["interrupted"] === false && hasStream) {
|
|
637
|
+
return seal({ code: 0, source: "response_shape", interrupted: false });
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return _NO_EXIT;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* The command's stdout and stderr as REAL strings, never as a stringified blob.
|
|
645
|
+
*
|
|
646
|
+
* Separate from `_extract_output` above, which feeds the pre-existing `vacuous`
|
|
647
|
+
* counters and whose behavior must not move: that one returns
|
|
648
|
+
* `JSON.stringify(v)` for an object response, and every parser in
|
|
649
|
+
* `verification_evidence.ts` is line-anchored (`/^[ \t]*Tests:?.../m` and
|
|
650
|
+
* friends). JSON escapes a newline as the two characters `\` and `n`, so a
|
|
651
|
+
* stringified response can never match any of them — the classifier was blind
|
|
652
|
+
* to every summary on the host's success shape. Found by the same review.
|
|
653
|
+
*/
|
|
654
|
+
function _extract_run_streams(payload: StateDict): { stdout: string; stderr: string } {
|
|
655
|
+
for (const key of ["tool_response", "toolResponse", "result", "output"]) {
|
|
656
|
+
const v = payload[key];
|
|
657
|
+
if (typeof v === "string") return { stdout: v, stderr: "" };
|
|
658
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) continue;
|
|
659
|
+
const obj = v as StateDict;
|
|
660
|
+
const out = obj["stdout"] ?? obj["output"];
|
|
661
|
+
const err = obj["stderr"];
|
|
662
|
+
if (typeof out === "string" || typeof err === "string") {
|
|
663
|
+
return {
|
|
664
|
+
stdout: typeof out === "string" ? out : "",
|
|
665
|
+
stderr: typeof err === "string" ? err : "",
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const topOut = payload["stdout"] ?? payload["output"];
|
|
670
|
+
const topErr = payload["stderr"];
|
|
671
|
+
if (typeof topOut === "string" || typeof topErr === "string") {
|
|
672
|
+
return {
|
|
673
|
+
stdout: typeof topOut === "string" ? topOut : "",
|
|
674
|
+
stderr: typeof topErr === "string" ? topErr : "",
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
// The failure envelope's one text field. Recorded as `stdout` because the
|
|
678
|
+
// host collapses both streams into it and says nothing about which was
|
|
679
|
+
// which — a recorder that split them would be inventing a fact — and because
|
|
680
|
+
// every parser in `verification_evidence` is line-anchored over stdout. The
|
|
681
|
+
// alternative, dropping it, would leave a red run recorded with its exit code
|
|
682
|
+
// and no reason, which is the half the reader actually needs.
|
|
683
|
+
const errText = payload["error"];
|
|
684
|
+
if (typeof errText === "string") return { stdout: errText, stderr: "" };
|
|
685
|
+
return { stdout: "", stderr: "" };
|
|
686
|
+
}
|
|
687
|
+
|
|
337
688
|
function _reset_turn(state: StateDict, session_id: string): StateDict {
|
|
338
689
|
state["session_id"] = session_id || state["session_id"] || "";
|
|
339
690
|
state["turn_started_at"] = _now();
|
|
340
691
|
state["verifications_this_turn"] = 0;
|
|
341
692
|
state["verified_this_turn"] = false;
|
|
693
|
+
// Round 8 — the run records the stop gate reads instead of the command text,
|
|
694
|
+
// and the edit counter that places them in the turn's sequence. Both are
|
|
695
|
+
// TURN-scoped: a run from the previous turn demonstrably did not exercise this
|
|
696
|
+
// turn's edits, which is the freshness argument `verify-before-complete` makes.
|
|
697
|
+
state["verification_runs"] = [];
|
|
698
|
+
state["edits_this_turn"] = 0;
|
|
699
|
+
// TURN-scoped for the same reason as its neighbour: a quality verdict from
|
|
700
|
+
// the previous turn describes files this turn may already have changed, and a
|
|
701
|
+
// stale PASS is the direction that misleads. DELETED rather than emptied, so
|
|
702
|
+
// that a turn boundary restores the base-ref shape — see `_empty_state`.
|
|
703
|
+
delete state["quality_runs"];
|
|
704
|
+
delete state["quality_files_source"];
|
|
342
705
|
// FC-3b turn-scoped counters: a CI settle must be witnessed within the same
|
|
343
706
|
// turn that claims it, so the in-flight observation does not survive a turn.
|
|
344
707
|
state["ci_saw_pending"] = false;
|
|
@@ -357,7 +720,19 @@ function _asInt(v: unknown): number {
|
|
|
357
720
|
return Number.isFinite(n) ? Math.trunc(n) : 0;
|
|
358
721
|
}
|
|
359
722
|
|
|
360
|
-
|
|
723
|
+
/**
|
|
724
|
+
* Apply one event to the state.
|
|
725
|
+
*
|
|
726
|
+
* `quality` is the shadow pass's already-computed result, handed in rather than
|
|
727
|
+
* fetched: this function runs INSIDE the state lock, and a spawn in here would
|
|
728
|
+
* hold that lock for the duration of a typecheck.
|
|
729
|
+
*/
|
|
730
|
+
function _update(
|
|
731
|
+
state: StateDict,
|
|
732
|
+
event: string,
|
|
733
|
+
envelope: StateDict,
|
|
734
|
+
quality: { runs: QualityRun[]; source: string } | null = null,
|
|
735
|
+
): StateDict {
|
|
361
736
|
const session_id = (envelope["session_id"] || state["session_id"] || "") as string;
|
|
362
737
|
if (session_id && session_id !== state["session_id"]) {
|
|
363
738
|
// Session boundary — reset session-scoped counters.
|
|
@@ -376,6 +751,16 @@ function _update(state: StateDict, event: string, envelope: StateDict): StateDic
|
|
|
376
751
|
state = _reset_turn(state, session_id);
|
|
377
752
|
}
|
|
378
753
|
|
|
754
|
+
// The host that wrote this file. Recorded because the instrument-gap reading
|
|
755
|
+
// is a PER-HOST question and the witness carried no answer to it: every
|
|
756
|
+
// coverage number had to be attributed by hand, and a file from an unknown
|
|
757
|
+
// host is indistinguishable from a host with nothing to report. Written on
|
|
758
|
+
// every update rather than once, so a file predating this field acquires it
|
|
759
|
+
// the first time the host touches it; until then a reader sees it absent,
|
|
760
|
+
// which is the honest reading and not a default.
|
|
761
|
+
const platform = envelope["platform"];
|
|
762
|
+
if (typeof platform === "string" && platform) state["platform"] = platform;
|
|
763
|
+
|
|
379
764
|
let payload = envelope["payload"];
|
|
380
765
|
if (!(typeof payload === "object" && payload !== null && !Array.isArray(payload))) {
|
|
381
766
|
payload = {};
|
|
@@ -386,6 +771,11 @@ function _update(state: StateDict, event: string, envelope: StateDict): StateDic
|
|
|
386
771
|
state = _reset_turn(state, session_id);
|
|
387
772
|
} else if (event === "post_tool_use") {
|
|
388
773
|
const [tool, cmd] = _extract_command(pl);
|
|
774
|
+
// Counted BEFORE the verification branch, so a run recorded in this same
|
|
775
|
+
// event is placed after the edits that preceded it and never after itself.
|
|
776
|
+
if (tool !== null && EDIT_TOOLS.has(tool)) {
|
|
777
|
+
state["edits_this_turn"] = _asInt(state["edits_this_turn"]) + 1;
|
|
778
|
+
}
|
|
389
779
|
if (cmd && _is_verification(cmd)) {
|
|
390
780
|
const output = _extract_output(pl);
|
|
391
781
|
// No readable output → the guard has nothing to judge, so behaviour is
|
|
@@ -439,6 +829,7 @@ function _update(state: StateDict, event: string, envelope: StateDict): StateDic
|
|
|
439
829
|
vacuous,
|
|
440
830
|
counted: counts,
|
|
441
831
|
};
|
|
832
|
+
|
|
442
833
|
if (counts) {
|
|
443
834
|
state["verifications_this_turn"] = _asInt(state["verifications_this_turn"]) + 1;
|
|
444
835
|
state["verifications_this_session"] =
|
|
@@ -448,8 +839,73 @@ function _update(state: StateDict, event: string, envelope: StateDict): StateDic
|
|
|
448
839
|
state["nonevidence_this_turn"] = _asInt(state["nonevidence_this_turn"]) + 1;
|
|
449
840
|
}
|
|
450
841
|
}
|
|
842
|
+
|
|
843
|
+
// The RUN record, written beside `last_verification` and gated on a
|
|
844
|
+
// DIFFERENT, wider selector — the one the turn-end gate's detector C uses.
|
|
845
|
+
//
|
|
846
|
+
// Two selectors is a smell, so here is why it is not one — and the reason
|
|
847
|
+
// was rewritten 2026-09-30, because the version standing here was half
|
|
848
|
+
// wrong. It claimed the narrow selector protected `verifications_this_turn`
|
|
849
|
+
// AND the FC-3b `ci_last` discrimination. `ci_last` is written inside the
|
|
850
|
+
// nested `isCiPoll` branch above, reached through the second disjunct of
|
|
851
|
+
// `_is_verification` whatever the first one says, so no widening of the
|
|
852
|
+
// first could ever have moved it; and nothing branches on the counter.
|
|
853
|
+
// Both are now pinned by tests rather than asserted in a comment.
|
|
854
|
+
//
|
|
855
|
+
// What survives is a real distinction, and it is about ERROR COST, not
|
|
856
|
+
// about two spellings of one idea. `isVerificationCommand` answers *did
|
|
857
|
+
// this verify* — a false positive there clears an unverified edit, so it is
|
|
858
|
+
// head-anchored. `mightBeVerification` answers *is this worth writing down*
|
|
859
|
+
// — and the run record decides nothing by itself, since `classifyRun` is
|
|
860
|
+
// what judges it. So the record path wants every command the gate might
|
|
861
|
+
// read, including the ones the classifier will then reject —
|
|
862
|
+
// `not_a_verification_command` is a verdict the gate needs to SEE, and a
|
|
863
|
+
// recorder that filtered those out would leave the gate unable to tell "the
|
|
864
|
+
// turn ran `echo test`" from "the turn ran nothing at all".
|
|
865
|
+
//
|
|
866
|
+
// `after_edits` is the edit counter as it stood when the command ran. The
|
|
867
|
+
// reader compares it against the number of edits the turn made in total:
|
|
868
|
+
// equal or greater means nothing was edited afterwards. That is an ORDINAL
|
|
869
|
+
// comparison needing no clock, so it survives a host whose timestamps are
|
|
870
|
+
// coarse or absent, and it degrades toward under-refusing when the reader's
|
|
871
|
+
// own count is short.
|
|
872
|
+
if (cmd && mightBeVerification(cmd)) {
|
|
873
|
+
const streams = _extract_run_streams(pl);
|
|
874
|
+
const exit = _extract_exit_reading(pl, _is_failure_event(pl, envelope));
|
|
875
|
+
const runs = Array.isArray(state["verification_runs"])
|
|
876
|
+
? [...(state["verification_runs"] as unknown[])]
|
|
877
|
+
: [];
|
|
878
|
+
runs.push({
|
|
879
|
+
command: cmd.slice(0, 512),
|
|
880
|
+
tool,
|
|
881
|
+
exit_code: exit.code,
|
|
882
|
+
// Provenance, not decoration: `response_shape` is a code this recorder
|
|
883
|
+
// DERIVED from the host's success form, and a reader disputing a verdict
|
|
884
|
+
// needs to know which of the three readings produced it.
|
|
885
|
+
exit_source: exit.source,
|
|
886
|
+
interrupted: exit.interrupted,
|
|
887
|
+
stdout_tail: streams.stdout.slice(-RUN_OUTPUT_TAIL_BYTES),
|
|
888
|
+
// Written, and previously not: `RunRecord.stderr_tail` was declared and
|
|
889
|
+
// read by the classifier's `output()` while nothing populated it, so
|
|
890
|
+
// stderr reached a parser only by accident inside a stringified blob.
|
|
891
|
+
stderr_tail: streams.stderr.slice(-RUN_OUTPUT_TAIL_BYTES),
|
|
892
|
+
runner: runnerOf(cmd),
|
|
893
|
+
after_edits: _asInt(state["edits_this_turn"]),
|
|
894
|
+
at: _now(),
|
|
895
|
+
});
|
|
896
|
+
// The NEWEST are kept for detector C, which asks whether a pass follows
|
|
897
|
+
// the last edit. Detector F asks a question the head answers — was there a
|
|
898
|
+
// red before the green — so the cap keeps the FIRST failing record too,
|
|
899
|
+
// ahead of the tail, rather than letting a chatty turn drop the red half
|
|
900
|
+
// of its own red-then-green pair.
|
|
901
|
+
state["verification_runs"] = _cap_runs(runs);
|
|
902
|
+
}
|
|
451
903
|
} else if (event === "stop") {
|
|
452
904
|
state["last_stop_at"] = _now();
|
|
905
|
+
if (quality !== null) {
|
|
906
|
+
state["quality_runs"] = quality.runs;
|
|
907
|
+
state["quality_files_source"] = quality.source;
|
|
908
|
+
}
|
|
453
909
|
}
|
|
454
910
|
|
|
455
911
|
state["checked_at"] = _now();
|
|
@@ -511,6 +967,15 @@ export function run(
|
|
|
511
967
|
|
|
512
968
|
const target = path.join(consumer_root, statePathFor(session_id));
|
|
513
969
|
|
|
970
|
+
// The shadow quality pass, computed BEFORE the lock (see `_update`'s header)
|
|
971
|
+
// and only at `stop`. `off` is the template default and the resolved value of
|
|
972
|
+
// every unreadable or unrecognised setting, so an install that never opted in
|
|
973
|
+
// spawns nothing, reads no manifest and writes no new field — the acceptance
|
|
974
|
+
// criterion is that its record is byte-identical to the base ref.
|
|
975
|
+
const quality_mode = event === "stop" ? readTouchedFileQualityMode(consumer_root) : "off";
|
|
976
|
+
const quality =
|
|
977
|
+
quality_mode === "off" ? null : collectTouchedFileQuality(consumer_root, session_id);
|
|
978
|
+
|
|
514
979
|
// LOAD → UPDATE → PUBLISH under ONE lock, not three separate steps.
|
|
515
980
|
//
|
|
516
981
|
// This was `_load_state` / `_update` / `atomic_write_json`, which makes the
|
|
@@ -541,7 +1006,7 @@ export function run(
|
|
|
541
1006
|
// treats a missing counter as 0 and the `=== true` guards treat a missing
|
|
542
1007
|
// flag as false, so every assertion would still pass while `schema_version`
|
|
543
1008
|
// and an explicit `ci_last: null` vanished from a freshly created file.
|
|
544
|
-
state = _update({ ..._empty_state(), ...loaded } as StateDict, event, envelope);
|
|
1009
|
+
state = _update({ ..._empty_state(), ...loaded } as StateDict, event, envelope, quality);
|
|
545
1010
|
return state;
|
|
546
1011
|
});
|
|
547
1012
|
if (outcome === "failed") {
|
|
@@ -562,6 +1027,21 @@ export function run(
|
|
|
562
1027
|
);
|
|
563
1028
|
}
|
|
564
1029
|
|
|
1030
|
+
// `warn` — ONE advisory line, and the exit code does not move.
|
|
1031
|
+
//
|
|
1032
|
+
// In this tree a warn is conventionally an exit of 2, and on the one host that
|
|
1033
|
+
// honours a deny that reads as a BLOCK. D3 of the roadmap settles it the other
|
|
1034
|
+
// way for this field: shadow → warn only, never a block, on the strength of
|
|
1035
|
+
// this hook's own contract ("the hook itself never blocks — it is
|
|
1036
|
+
// observability infra, not control flow"). So the line goes to stderr and the
|
|
1037
|
+
// function returns 0 on this path exactly as on every other.
|
|
1038
|
+
if (quality_mode === "warn" && quality !== null) {
|
|
1039
|
+
const line = advisoryLine(quality.runs);
|
|
1040
|
+
if (line !== null) {
|
|
1041
|
+
process.stderr.write(`${line}\n`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
565
1045
|
if (verbose) {
|
|
566
1046
|
process.stderr.write(
|
|
567
1047
|
`verify-before-complete-hook: event=${event} ` +
|
|
@@ -613,6 +1093,74 @@ export function main(argv?: string[]): number {
|
|
|
613
1093
|
return run(_readStdin(), { consumer_root: process.cwd(), verbose: args.verbose });
|
|
614
1094
|
}
|
|
615
1095
|
|
|
1096
|
+
// ---------------------------------------------------------------------------
|
|
1097
|
+
// The consumer side of `verification_runs`, living with its producer
|
|
1098
|
+
// ---------------------------------------------------------------------------
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Why the turn-end gate's reader sits HERE and not in the gate.
|
|
1102
|
+
*
|
|
1103
|
+
* The shape of `verification_runs` and `edits_this_turn` is this file's; the
|
|
1104
|
+
* reader is the only consumer of that shape, and the gate already imported this
|
|
1105
|
+
* module's path BUILDER rather than a path constant for exactly the reason that
|
|
1106
|
+
* applies here too — "the consumer cannot read a path the producer does not
|
|
1107
|
+
* write" is only true while the two agree, and separating them made the
|
|
1108
|
+
* agreement invisible when the layout moved. Keeping the reader beside the
|
|
1109
|
+
* writer makes a shape change a type error in one file instead of a silent
|
|
1110
|
+
* disagreement across two.
|
|
1111
|
+
*
|
|
1112
|
+
* It also keeps the gate under its source-size ceiling without deleting
|
|
1113
|
+
* anything, which is the move the ratchet asks for rather than a trim.
|
|
1114
|
+
*/
|
|
1115
|
+
export interface TurnRunState {
|
|
1116
|
+
readonly runs: readonly unknown[];
|
|
1117
|
+
readonly edits_this_turn: number;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* The turn's recorded verification runs, or `null` when the recorder is not live.
|
|
1122
|
+
*
|
|
1123
|
+
* WHY THE LIVENESS TEST IS `edits_this_turn >= 1` AND NOT "the file exists".
|
|
1124
|
+
* The obvious predicate — a state file with a `verification_runs` key — is
|
|
1125
|
+
* wrong in a way that would refuse honest work on entire platforms. That key is
|
|
1126
|
+
* present in the recorder's EMPTY state, so a host whose `post_tool_use` slot
|
|
1127
|
+
* the manifest does not bind still has a file carrying `[]`, written by the
|
|
1128
|
+
* prompt and stop events alone. Reading that as "this turn ran nothing" would
|
|
1129
|
+
* refuse every editing turn on such a host, whatever the operator actually ran.
|
|
1130
|
+
*
|
|
1131
|
+
* `edits_this_turn` is the one field only a `post_tool_use` event can raise. The
|
|
1132
|
+
* detector reaches here having already found an edit in the transcript, so a
|
|
1133
|
+
* recorder that saw none of this turn's tool events is exactly the case this
|
|
1134
|
+
* returns `null` for — and `null` means the transcript path answers, which is
|
|
1135
|
+
* the behavior that predates the record path.
|
|
1136
|
+
*
|
|
1137
|
+
* The ownership check is the same one detector D applies to `ci_last`, for the
|
|
1138
|
+
* same reason: a FOREIGN file's passing record would vouch for a run this
|
|
1139
|
+
* session never made.
|
|
1140
|
+
*/
|
|
1141
|
+
export function readTurnRunState(
|
|
1142
|
+
workspaceRoot: string,
|
|
1143
|
+
session_id: string,
|
|
1144
|
+
): TurnRunState | null {
|
|
1145
|
+
if (!has_stable_session_id(session_id)) return null;
|
|
1146
|
+
try {
|
|
1147
|
+
const raw = fs.readFileSync(path.join(workspaceRoot, statePathFor(session_id)), 'utf-8');
|
|
1148
|
+
const decoded: unknown = JSON.parse(raw);
|
|
1149
|
+
if (!owns_session_state(decoded, session_id)) return null;
|
|
1150
|
+
if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) return null;
|
|
1151
|
+
const state = decoded as Record<string, unknown>;
|
|
1152
|
+
const runs = state['verification_runs'];
|
|
1153
|
+
const edits = state['edits_this_turn'];
|
|
1154
|
+
if (!Array.isArray(runs)) return null;
|
|
1155
|
+
if (typeof edits !== 'number' || !Number.isFinite(edits) || edits < 1) return null;
|
|
1156
|
+
return { runs: runs as readonly unknown[], edits_this_turn: edits };
|
|
1157
|
+
} catch {
|
|
1158
|
+
// Absent, unreadable or malformed — the recorder said nothing, so the
|
|
1159
|
+
// transcript answers. Never a refusal of its own.
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
616
1164
|
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
617
1165
|
// every module shares the bundle's `import.meta.url` (see cmd_migrate.ts).
|
|
618
1166
|
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|