@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
|
@@ -36,41 +36,90 @@
|
|
|
36
36
|
* does. On any other host, and on a `lean_projection.mode: eager-all` rollback,
|
|
37
37
|
* it emits nothing.
|
|
38
38
|
*
|
|
39
|
+
* THE INSTALL DECLARES THE SCOPE, THE PACKAGE SUPPLIES THE TEXT (step 1.3).
|
|
40
|
+
* A rule is delivered only when the host's own rule directories carry a file
|
|
41
|
+
* for it — `~/.claude/rules/` and `<project>/.claude/rules/`, as a union,
|
|
42
|
+
* because the host loads both. The router is the PACKAGE's list of what could
|
|
43
|
+
* route and is a larger set: 13 router tier rules are maintainer-only by
|
|
44
|
+
* `workspaces`, and before this filter a consumer could receive a body for a
|
|
45
|
+
* rule their install never carried. The two halves cannot be one read — under
|
|
46
|
+
* `delivery` the installed files are thin stubs by construction, so they can say
|
|
47
|
+
* WHICH rules are in scope and never WHAT they say. Where no host rule directory
|
|
48
|
+
* exists at all, nothing declares a scope and no filter applies; see
|
|
49
|
+
* `rule_layer_overlap.hostRuleLayerIds` for why that is not the empty set.
|
|
50
|
+
*
|
|
51
|
+
* THE HOST FORM, BY THE PROJECTOR'S OWN PARSER (step 1.4). Delivered text is
|
|
52
|
+
* `rule_law_section.ruleBody` of the file: frontmatter and HTML comments
|
|
53
|
+
* removed. Both are bytes the model cannot act on — the frontmatter is the
|
|
54
|
+
* routing surface the router has already read, and the comments are authoring
|
|
55
|
+
* scaffolding no host renders. `project_thin_rules.ts` calls the same function
|
|
56
|
+
* for the same reason, so "what a stub carries" and "what a delivery carries"
|
|
57
|
+
* cannot drift into two spellings. A file with nothing left after the strip is
|
|
58
|
+
* not delivered: an empty `<rule>` element is framing with no content in it.
|
|
59
|
+
*
|
|
60
|
+
* ONE STRING THE HOST DOES NOT REPLACE, AND A MANIFEST (step 1.5). Claude Code
|
|
61
|
+
* substitutes an `additionalContext` over 10,000 CHARACTERS with a path and a
|
|
62
|
+
* 2,000-character preview, so an oversized delivery does not arrive truncated —
|
|
63
|
+
* it does not arrive. {@link COMPOSED_CHARS} bounds the whole rule-produced
|
|
64
|
+
* string at 8,000, and `compose` fills it in the order D1 fixes: the law of
|
|
65
|
+
* every matched high-consequence rule, then the highest-priority full body that
|
|
66
|
+
* fits, then further bodies, then the law of everything still unsent, then the
|
|
67
|
+
* manifest. Nothing is ever cut mid-obligation — a rule whose law alone does not
|
|
68
|
+
* fit is REPORTED, because half a law reads like a whole one.
|
|
69
|
+
*
|
|
70
|
+
* AND EVERY MATCH IS IN THE MANIFEST, labelled `full`, `law`, `omitted_budget`
|
|
71
|
+
* or `source_unavailable`. A pointer is never labelled delivered. This is where
|
|
72
|
+
* `selectForInjection`'s `dropped` list is finally read: before it, a body the
|
|
73
|
+
* byte cap discarded left no trace, so a consumer could not tell a rule that did
|
|
74
|
+
* not match from one that matched and was thrown away.
|
|
75
|
+
*
|
|
39
76
|
* ONE MATCHER, SHARED WITH THE OFFLINE MODEL. Everything about selection,
|
|
40
77
|
* ordering, capping and body loading comes from `_lib/rule_injection.ts`, which
|
|
41
78
|
* `model_rule_injection.ts` also imports. Step 0.5 states the reason in as many
|
|
42
79
|
* words: an experiment whose offline pricing and runtime delivery use different
|
|
43
80
|
* matchers measures nothing.
|
|
44
81
|
*
|
|
45
|
-
* ONCE PER SESSION PER RULE, RE-ARMED ON COMPACTION. A rule's body
|
|
46
|
-
* the first time one of its triggers fires and not again, because
|
|
47
|
-
* already has it. Compaction is exactly the event that makes that
|
|
48
|
-
* `pre_compact`
|
|
49
|
-
* uses.
|
|
50
|
-
*
|
|
82
|
+
* ONCE PER SESSION PER RULE, RE-ARMED AND RESTORED ON COMPACTION. A rule's body
|
|
83
|
+
* is injected the first time one of its triggers fires and not again, because
|
|
84
|
+
* the model already has it. Compaction is exactly the event that makes that
|
|
85
|
+
* false, so `pre_compact` empties the seen-set — the same pin-lost shape
|
|
86
|
+
* `language-mirror` uses.
|
|
87
|
+
*
|
|
88
|
+
* STATE LIVES UNDER THE USER-GLOBAL ROOT, KEYED BY PROJECT (step 1.7). It used
|
|
89
|
+
* to be `<workspace>/agents/runtime/state/rule-inject/`, so every consumer
|
|
90
|
+
* session left dispatcher bookkeeping as untracked files inside someone else's
|
|
91
|
+
* repository. The seen-set is state about a SESSION, not about the project.
|
|
92
|
+
* The delivered-row ledger beside it is deliberately NOT moved — see
|
|
93
|
+
* {@link statePath} for the join that depends on where it is.
|
|
51
94
|
*
|
|
52
95
|
* WHAT IS RE-DELIVERED AFTER A COMPACTION, EXACTLY
|
|
53
|
-
* (road-to-delivery-for-every-host 2.4
|
|
54
|
-
* compaction boundary
|
|
55
|
-
* does not say what a reader may rely on):
|
|
96
|
+
* (road-to-delivery-for-every-host 2.4, amended by step 1.6 — stated because a
|
|
97
|
+
* rule lost at a compaction boundary used to be lost for the rest of the
|
|
98
|
+
* session, and "re-armed" alone does not say what a reader may rely on):
|
|
56
99
|
*
|
|
57
|
-
* · `pre_compact`
|
|
100
|
+
* · `pre_compact` empties the WHOLE seen-set for that session, not the rules
|
|
58
101
|
* matched on the compacted turn. There is no per-rule bookkeeping to be
|
|
59
|
-
* partially wrong about.
|
|
102
|
+
* partially wrong about. Since 1.6 it also hands those ids to `pending`
|
|
103
|
+
* rather than deleting them — see {@link armRestore}.
|
|
60
104
|
* · Nothing is delivered BY the compaction itself. The slot emits zero bytes
|
|
61
|
-
* and exits allow; re-arming is silent.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
105
|
+
* and exits allow; re-arming is silent. It cannot deliver: the host is about
|
|
106
|
+
* to discard the context it would emit into.
|
|
107
|
+
* · `session_start` with `source: compact` sends the LAW section of every
|
|
108
|
+
* rule in that pending set, under the same 8,000-character budget, once.
|
|
109
|
+
* That is the half this paragraph used to end at: a rule whose trigger does
|
|
110
|
+
* not recur is no longer unrestored. A rule with no law section is reported
|
|
111
|
+
* in the manifest rather than having its whole body sent in its place.
|
|
112
|
+
* · A rule's BODY still returns on the next turn whose trigger matches it.
|
|
113
|
+
* The restore does not re-arm the de-duplication and does not replace
|
|
114
|
+
* trigger-driven delivery; it covers the gap between the two.
|
|
115
|
+
* · The seen-set is per session, so a compaction in one session re-arms and
|
|
116
|
+
* restores only that session.
|
|
117
|
+
*
|
|
118
|
+
* Held by fixtures in `tests/scripts/rule_inject_hook.test.ts` under "once per
|
|
119
|
+
* session per rule" and "re-deliver after a compact", and — for the BINDING
|
|
120
|
+
* rather than the function — by the dispatcher column in
|
|
121
|
+
* `rule_inject_foreign_matrix.test.ts`, because a suite that only calls `main()`
|
|
122
|
+
* stays green over a slot the dispatcher never routes to this concern.
|
|
74
123
|
*
|
|
75
124
|
* NEVER BLOCKS. Every failure path returns 0: unreadable stdin, malformed JSON,
|
|
76
125
|
* missing router, unreadable body, unwritable state. The one non-zero exit is
|
|
@@ -90,29 +139,38 @@
|
|
|
90
139
|
* claude 2.1.241, three verdicts). Delivery is therefore orchestrator-only, and
|
|
91
140
|
* no third binding is added.
|
|
92
141
|
*/
|
|
142
|
+
import { createHash } from 'node:crypto';
|
|
93
143
|
import * as fs from 'node:fs';
|
|
94
144
|
import * as path from 'node:path';
|
|
95
145
|
import process from 'node:process';
|
|
96
146
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
97
147
|
|
|
98
|
-
import { hookSectionEnabled
|
|
99
|
-
import
|
|
100
|
-
|
|
101
|
-
normalizeLeanProjectionMode,
|
|
102
|
-
resolveLeanProjectionHosts,
|
|
103
|
-
} from '../_lib/lean_projection_mode.js';
|
|
148
|
+
import { hookSectionEnabled } from '../_lib/hook_settings.js';
|
|
149
|
+
import * as user_global_paths from '../_lib/user_global_paths.js';
|
|
150
|
+
import { deliversBodies, resolveLeanProjection } from '../_lib/lean_projection_mode.js';
|
|
104
151
|
import { enforcement_class_from_frontmatter } from '../_lib/obligation_frequency.js';
|
|
105
|
-
import { appendDelivered, stamp, type DeliveredRow } from '../_lib/obligations.js';
|
|
106
152
|
import {
|
|
153
|
+
appendDelivered,
|
|
154
|
+
stamp,
|
|
155
|
+
type DeliveredRow,
|
|
156
|
+
type WriterInput,
|
|
157
|
+
} from '../_lib/obligations.js';
|
|
158
|
+
import {
|
|
159
|
+
allTierRules,
|
|
160
|
+
bytesOf,
|
|
107
161
|
loadRuleBody,
|
|
108
162
|
loadRouter,
|
|
109
163
|
matchTierRules,
|
|
164
|
+
ruleSources,
|
|
110
165
|
selectForInjection,
|
|
166
|
+
type SelectionResult,
|
|
167
|
+
type TierRuleMatch,
|
|
111
168
|
} from '../_lib/rule_injection.js';
|
|
169
|
+
import { readConsequenceClass, stubLawIds } from '../_lib/rule_consequence_class.js';
|
|
170
|
+
import { lawText, ruleBody } from '../_lib/rule_law_section.js';
|
|
171
|
+
import { hostRuleLayerIds } from '../_lib/rule_layer_overlap.js';
|
|
112
172
|
import { readHookStdin } from './hook_stdin.js';
|
|
113
|
-
|
|
114
|
-
const EXIT_ALLOW = 0;
|
|
115
|
-
const EXIT_WARN = 2;
|
|
173
|
+
import { EXIT_ALLOW, EXIT_WARN } from './exit_codes.js';
|
|
116
174
|
|
|
117
175
|
/**
|
|
118
176
|
* Per-prompt injection ceiling, in UTF-8 BYTES.
|
|
@@ -186,45 +244,142 @@ export function workspaceRoot(env: JsonObject): string {
|
|
|
186
244
|
// ── seen-set state ───────────────────────────────────────────────────────
|
|
187
245
|
|
|
188
246
|
export interface SeenState {
|
|
247
|
+
/** Ids already delivered in this session. */
|
|
189
248
|
rules: string[];
|
|
249
|
+
/**
|
|
250
|
+
* Ids the compaction boundary took away, waiting for the restore slot.
|
|
251
|
+
*
|
|
252
|
+
* IDS, NEVER TEXT. The restore re-reads each rule's law from the corpus; a
|
|
253
|
+
* seen-set that cached bodies would be a second copy of the rule layer
|
|
254
|
+
* living in a consumer's state directory, going stale on every upgrade.
|
|
255
|
+
*/
|
|
256
|
+
pending?: string[];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* A stable, readable, collision-free directory name for one project root.
|
|
261
|
+
*
|
|
262
|
+
* The basename alone collides — a developer with `~/work/api` and
|
|
263
|
+
* `~/clients/api` would share a seen-set and silently lose deliveries in both.
|
|
264
|
+
* The digest alone is unreadable, and this directory is one a human opens when
|
|
265
|
+
* a delivery looks wrong. Both, so neither problem is the one you get.
|
|
266
|
+
*/
|
|
267
|
+
function projectKey(root: string): string {
|
|
268
|
+
const abs = path.resolve(root);
|
|
269
|
+
const name = (path.basename(abs) || 'root').replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 40);
|
|
270
|
+
return `${name}-${createHash('sha256').update(abs, 'utf-8').digest('hex').slice(0, 12)}`;
|
|
190
271
|
}
|
|
191
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Where this session's seen-set lives (step 1.7).
|
|
275
|
+
*
|
|
276
|
+
* UNDER THE USER-GLOBAL ROOT, KEYED BY PROJECT. It used to be written to
|
|
277
|
+
* `<workspace>/agents/runtime/state/rule-inject/`, so every consumer session
|
|
278
|
+
* left dispatcher bookkeeping inside someone else's repository — untracked
|
|
279
|
+
* files in their `git status`, in a directory their `.gitignore` says nothing
|
|
280
|
+
* about unless this package's managed block was installed. The seen-set is
|
|
281
|
+
* state ABOUT a session, not about the project, and the project is the one
|
|
282
|
+
* place it has no business being.
|
|
283
|
+
*
|
|
284
|
+
* THE DELIVERED-ROW LEDGER IS DELIBERATELY NOT MOVED WITH IT.
|
|
285
|
+
* `recordDelivered` writes through `_lib/obligations.ts`, which resolves under
|
|
286
|
+
* the project root, and `road-to-a-stop-that-holds` Phase 3 reads those rows
|
|
287
|
+
* and joins them on the session id. Moving the ledger would be a second,
|
|
288
|
+
* larger change with a live consumer; moving only the seen-set leaves that join
|
|
289
|
+
* untouched by construction, because the two were never keyed on each other.
|
|
290
|
+
*
|
|
291
|
+
* `EVENT4U_CONFIG_HOME` and `$HOME` resolve the root, so a test points it
|
|
292
|
+
* somewhere disposable instead of writing into the developer's own state.
|
|
293
|
+
*
|
|
294
|
+
* ONE COST, STATED: an upgrade mid-session orphans the old file and the seen-set
|
|
295
|
+
* reads empty, which re-injects each matched rule once more. Session state is
|
|
296
|
+
* per session and cheap to rebuild; a migration step for it would be more code
|
|
297
|
+
* than the duplicate it prevents.
|
|
298
|
+
*/
|
|
192
299
|
export function statePath(root: string, session: string): string {
|
|
193
300
|
const safe = session.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 80) || 'unknown';
|
|
194
|
-
return path.join(
|
|
301
|
+
return path.join(
|
|
302
|
+
user_global_paths.write_target(path.join('state', 'rule-inject', projectKey(root))),
|
|
303
|
+
`${safe}.json`,
|
|
304
|
+
);
|
|
195
305
|
}
|
|
196
306
|
|
|
197
|
-
|
|
307
|
+
function readState(root: string, session: string): SeenState {
|
|
198
308
|
try {
|
|
199
309
|
const raw = fs.readFileSync(statePath(root, session), 'utf-8');
|
|
200
310
|
const parsed = JSON.parse(raw) as SeenState;
|
|
201
|
-
return
|
|
311
|
+
return {
|
|
312
|
+
rules: Array.isArray(parsed.rules) ? parsed.rules.map(String) : [],
|
|
313
|
+
pending: Array.isArray(parsed.pending) ? parsed.pending.map(String) : [],
|
|
314
|
+
};
|
|
202
315
|
} catch {
|
|
203
|
-
return
|
|
316
|
+
return { rules: [], pending: [] }; // fresh session, or a file nothing can parse
|
|
204
317
|
}
|
|
205
318
|
}
|
|
206
319
|
|
|
207
|
-
export function
|
|
320
|
+
export function readSeen(root: string, session: string): Set<string> {
|
|
321
|
+
return new Set(readState(root, session).rules);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function writeState(root: string, session: string, state: SeenState): void {
|
|
208
325
|
const p = statePath(root, session);
|
|
209
326
|
try {
|
|
210
327
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
211
|
-
|
|
212
|
-
fs.writeFileSync(`${p}.tmp`, `${JSON.stringify(payload)}\n`, 'utf-8');
|
|
328
|
+
fs.writeFileSync(`${p}.tmp`, `${JSON.stringify(state)}\n`, 'utf-8');
|
|
213
329
|
fs.renameSync(`${p}.tmp`, p);
|
|
214
330
|
} catch {
|
|
215
331
|
/* unwritable state must never fail a turn — worst case a re-injection */
|
|
216
332
|
}
|
|
217
333
|
}
|
|
218
334
|
|
|
219
|
-
export function
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
335
|
+
export function writeSeen(root: string, session: string, seen: Set<string>): void {
|
|
336
|
+
const prev = readState(root, session);
|
|
337
|
+
writeState(root, session, { rules: [...seen].sort(), pending: prev.pending ?? [] });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Hand the seen-set to the restore slot instead of deleting it (step 1.6).
|
|
342
|
+
*
|
|
343
|
+
* `pre_compact` used to `rm` this file, which re-armed delivery correctly and
|
|
344
|
+
* threw away the one thing the restore needs. The de-duplication is still
|
|
345
|
+
* cleared — `rules` goes empty, so a rule whose trigger fires again gets its
|
|
346
|
+
* BODY back exactly as before — and the ids move to `pending`, where
|
|
347
|
+
* `session_start` with `source: compact` reads them and sends each rule's LAW.
|
|
348
|
+
*
|
|
349
|
+
* That is the half the header used to end at: a rule whose trigger does not
|
|
350
|
+
* recur was not restored at all, and a rule lost at a compaction boundary is
|
|
351
|
+
* lost for the rest of the session.
|
|
352
|
+
*/
|
|
353
|
+
export function armRestore(root: string, session: string): void {
|
|
354
|
+
const prev = readState(root, session);
|
|
355
|
+
const pending = [...new Set([...prev.rules, ...(prev.pending ?? [])])].sort();
|
|
356
|
+
if (pending.length === 0) {
|
|
357
|
+
try {
|
|
358
|
+
fs.rmSync(statePath(root, session), { force: true });
|
|
359
|
+
} catch {
|
|
360
|
+
/* ignore */
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
224
363
|
}
|
|
364
|
+
writeState(root, session, { rules: [], pending });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** The ids waiting for the restore slot. */
|
|
368
|
+
export function readPending(root: string, session: string): Set<string> {
|
|
369
|
+
return new Set(readState(root, session).pending ?? []);
|
|
225
370
|
}
|
|
226
371
|
|
|
227
|
-
|
|
372
|
+
/**
|
|
373
|
+
* Drop the pending set, once the restore it fed has actually been built.
|
|
374
|
+
*
|
|
375
|
+
* SEPARATE FROM THE READ on purpose. A single take-and-clear loses the set when
|
|
376
|
+
* the build fails, on a slot that does not fire again for that boundary — so
|
|
377
|
+
* the clear is the caller's last step rather than its first.
|
|
378
|
+
*/
|
|
379
|
+
export function clearPending(root: string, session: string): void {
|
|
380
|
+
const prev = readState(root, session);
|
|
381
|
+
if ((prev.pending ?? []).length > 0) writeState(root, session, { rules: prev.rules, pending: [] });
|
|
382
|
+
}
|
|
228
383
|
|
|
229
384
|
/** The user's prompt, across the shapes the hosts use. */
|
|
230
385
|
export function extractPrompt(payload: JsonObject): string {
|
|
@@ -238,12 +393,162 @@ export function extractFilePath(payload: JsonObject): string | null {
|
|
|
238
393
|
return str(ti, 'file_path', 'path', 'filePath', 'notebook_path');
|
|
239
394
|
}
|
|
240
395
|
|
|
241
|
-
|
|
396
|
+
/**
|
|
397
|
+
* Per-fire ceiling on the whole rule-produced string, in CHARACTERS (D2).
|
|
398
|
+
*
|
|
399
|
+
* CHARACTERS, because that is the unit the host's own threshold is in. Claude
|
|
400
|
+
* Code replaces an `additionalContext` longer than 10,000 CHARACTERS with a
|
|
401
|
+
* path and a 2,000-character preview, so a delivery over that line is not
|
|
402
|
+
* truncated — it is substituted, and the obligation text never arrives at all.
|
|
403
|
+
*
|
|
404
|
+
* WHICH STRING IT BOUNDS, stated because the two readings give different
|
|
405
|
+
* margins and D2's own wording did not separate them. This number bounds the
|
|
406
|
+
* RULE-PRODUCED string: the `<rule>` elements and the manifest, which is what
|
|
407
|
+
* this concern emits. The host's 10,000 applies to the `additionalContext` it
|
|
408
|
+
* assembles, and the other concerns on this slot measured 1,272 characters of
|
|
409
|
+
* non-rule text. So the margin is 2,000 if the host measures this concern's
|
|
410
|
+
* payload alone, and **728** if it measures the assembled string — the
|
|
411
|
+
* conservative reading, and still a margin. 8,000 is safe under both; which
|
|
412
|
+
* reading is right has not been established here and the smaller number is the
|
|
413
|
+
* one to plan against.
|
|
414
|
+
*
|
|
415
|
+
* IT BOUNDS THE WHOLE STRING, framing and manifest included, because that is
|
|
416
|
+
* what the host measures. {@link CAP_BYTES} above bounds something else — the
|
|
417
|
+
* SELECTION, shared byte-for-byte with the offline model — and both still
|
|
418
|
+
* apply: selection drops whole bodies to stay under the byte cap, then
|
|
419
|
+
* composition drops or demotes to stay under this one.
|
|
420
|
+
*
|
|
421
|
+
* THE TWO CANNOT BREACH EACH OTHER AT ANY RATIO THIS CORPUS PRODUCES, and the
|
|
422
|
+
* ratio is measured rather than assumed: over the 121 projected rule files the
|
|
423
|
+
* mean is 1.0088 bytes per character and the per-file maximum is 1.0346
|
|
424
|
+
* (`no-decorative-emojis-in-git-surfaces.md`), so 8,000 characters is at most
|
|
425
|
+
* 8,277 bytes against a registered 16,384. A composed string would have to
|
|
426
|
+
* average 2.05 bytes per character to reach the byte row, which no Markdown
|
|
427
|
+
* rule corpus produces. `per_concern_caps_chars` in
|
|
428
|
+
* `src/config/hook-token-budget.json` registers this number in its own unit
|
|
429
|
+
* rather than converting it into the bytes map, and a fixture pins both.
|
|
430
|
+
*/
|
|
431
|
+
export const COMPOSED_CHARS = 8000;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* What a matched rule actually got, as the manifest labels it.
|
|
435
|
+
*
|
|
436
|
+
* `full` and `law` mean text arrived. `omitted_budget` and `source_unavailable`
|
|
437
|
+
* mean it did not, and both are a POINTER — an id the model can ask for. A
|
|
438
|
+
* pointer is never labelled delivered; that distinction is the whole reason the
|
|
439
|
+
* manifest has four values instead of a present/absent flag.
|
|
440
|
+
*/
|
|
441
|
+
export type DeliveryForm = 'full' | 'law' | 'omitted_budget' | 'source_unavailable';
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The host's own threshold, in the host's own unit (step 0.3).
|
|
445
|
+
*
|
|
446
|
+
* Claude Code REPLACES an `additionalContext` longer than this with a path and
|
|
447
|
+
* a 2,000-character preview. A composed string over this line is therefore not
|
|
448
|
+
* truncated — it is substituted, and the obligation text never arrives. That is
|
|
449
|
+
* why this number is the one the arrival record checks against rather than
|
|
450
|
+
* {@link COMPOSED_CHARS}: 8,000 is the budget this concern holds itself to, and
|
|
451
|
+
* 10,000 is the line past which the host stops delivering what it was handed.
|
|
452
|
+
*
|
|
453
|
+
* WHICH STRING IS MEASURED, stated because risk 5 of this roadmap's register
|
|
454
|
+
* says the answer is not established. This records the RULE-PRODUCED string,
|
|
455
|
+
* which is the only one this concern can see. If the host measures the
|
|
456
|
+
* ASSEMBLED `additionalContext` instead, the real margin is 728 rather than
|
|
457
|
+
* 2,000 and a reading of 0 here is necessary but not sufficient. The record is
|
|
458
|
+
* honest about which of the two it is; it does not settle the question.
|
|
459
|
+
*/
|
|
460
|
+
export const HOST_REPLACE_CHARS = 10000;
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* The three forms step 0.3 names, which is not {@link DeliveryForm}'s four.
|
|
464
|
+
*
|
|
465
|
+
* `omitted_budget` and `source_unavailable` are different REASONS for the same
|
|
466
|
+
* outcome — an id the model can ask for and no text. The arrival record asks
|
|
467
|
+
* what the model got, so the two collapse to `pointer`; the manifest keeps the
|
|
468
|
+
* distinction, because a consumer diagnosing a broken install needs the reason
|
|
469
|
+
* and a reader counting arrivals does not.
|
|
470
|
+
*/
|
|
471
|
+
export type ArrivalForm = 'body' | 'law' | 'pointer';
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* How far a matched rule got, in the levels D4 fixed (step 0.3).
|
|
475
|
+
*
|
|
476
|
+
* - `A0` discoverable — a file or pointer exists.
|
|
477
|
+
* - `A1` routed — a trigger matched.
|
|
478
|
+
* - `A2` arrived — the obligation text is in context.
|
|
479
|
+
* - `A3` enforced — a gate blocks the forbidden outcome.
|
|
480
|
+
*
|
|
481
|
+
* THIS CONCERN NEVER WRITES A0 OR A3, and both omissions are deliberate rather
|
|
482
|
+
* than unimplemented. A0 is the level of a rule that never routed, and this
|
|
483
|
+
* concern only ever sees rules that did — writing an A0 row per fire for every
|
|
484
|
+
* rule in the corpus would be a per-turn sweep of the router to record the
|
|
485
|
+
* absence of an event. A3 belongs to whatever gate refuses the outcome; a
|
|
486
|
+
* carrier that recorded its own delivery as `enforced` would be the
|
|
487
|
+
* emitter-record-read-as-compliance mistake the delivered ledger's own header
|
|
488
|
+
* carries a council lock against.
|
|
489
|
+
*/
|
|
490
|
+
export type ArrivalLevel = 'A0' | 'A1' | 'A2' | 'A3';
|
|
491
|
+
|
|
492
|
+
/** One matched rule, with what it got and how far that got it. */
|
|
493
|
+
export interface ArrivalRow {
|
|
494
|
+
readonly rule: string;
|
|
495
|
+
readonly form: ArrivalForm;
|
|
496
|
+
readonly arrival: ArrivalLevel;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** One fire, measured on the composed string in the host's unit. */
|
|
500
|
+
export interface ArrivalRecord {
|
|
501
|
+
/** The composed string's length in CHARACTERS — what the host counts. */
|
|
502
|
+
readonly chars: number;
|
|
503
|
+
/** 1 when this fire was over {@link HOST_REPLACE_CHARS}, else 0. Summing the column gives the over-budget count. */
|
|
504
|
+
readonly over_budget: number;
|
|
505
|
+
/** Which slot the fire reached the host on. */
|
|
506
|
+
readonly slot: string;
|
|
507
|
+
readonly at: string;
|
|
508
|
+
readonly rows: readonly ArrivalRow[];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** The manifest's four reasons, as the three forms a reader of arrivals wants. */
|
|
512
|
+
export function arrivalForm(f: DeliveryForm): ArrivalForm {
|
|
513
|
+
if (f === 'full') return 'body';
|
|
514
|
+
if (f === 'law') return 'law';
|
|
515
|
+
return 'pointer';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* What this fire actually delivered, in the host's unit.
|
|
520
|
+
*
|
|
521
|
+
* TWO WAYS TO STAY AT A1, and they are different failures with the same
|
|
522
|
+
* reading. A pointer never carried text in the first place — step 0.3's "a
|
|
523
|
+
* pointer is never recorded above A1", taken literally. And a composed string
|
|
524
|
+
* the host REPLACES carried text that the host then did not deliver, so every
|
|
525
|
+
* row in that fire is A1 however good its form looked: the manifest says `body`
|
|
526
|
+
* and the model received a path. Recording those as A2 would be the instrument
|
|
527
|
+
* reporting its own intent instead of the outcome, which is the one thing a
|
|
528
|
+
* measurement of arrival must not do.
|
|
529
|
+
*/
|
|
530
|
+
export function arrivalRecord(injection: Injection, slot: string, now = stamp()): ArrivalRecord {
|
|
531
|
+
const chars = injection.body.length;
|
|
532
|
+
const replaced = chars > HOST_REPLACE_CHARS;
|
|
533
|
+
return {
|
|
534
|
+
chars,
|
|
535
|
+
over_budget: replaced ? 1 : 0,
|
|
536
|
+
slot,
|
|
537
|
+
at: now,
|
|
538
|
+
rows: injection.manifest.map(([rule, f]) => {
|
|
539
|
+
const form = arrivalForm(f);
|
|
540
|
+
return { rule, form, arrival: form === 'pointer' || replaced ? 'A1' : 'A2' };
|
|
541
|
+
}),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
242
544
|
|
|
243
545
|
export interface Injection {
|
|
546
|
+
/** Ids whose TEXT was emitted — what the ledger records. */
|
|
244
547
|
rules: string[];
|
|
245
548
|
bytes: number;
|
|
246
549
|
body: string;
|
|
550
|
+
/** Every matched rule with the form it got, in router order. */
|
|
551
|
+
manifest: Array<[string, DeliveryForm]>;
|
|
247
552
|
}
|
|
248
553
|
|
|
249
554
|
/**
|
|
@@ -268,7 +573,7 @@ export interface Injection {
|
|
|
268
573
|
export function recordDelivered(root: string, session: string, ruleIds: string[]): number {
|
|
269
574
|
try {
|
|
270
575
|
const now = stamp();
|
|
271
|
-
const rows: DeliveredRow[] = ruleIds.map((id) => {
|
|
576
|
+
const rows: WriterInput<DeliveredRow>[] = ruleIds.map((id) => {
|
|
272
577
|
const body = loadRuleBody(root, id);
|
|
273
578
|
return {
|
|
274
579
|
rule: id,
|
|
@@ -284,6 +589,256 @@ export function recordDelivered(root: string, session: string, ruleIds: string[]
|
|
|
284
589
|
}
|
|
285
590
|
}
|
|
286
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Ids whose LAW may stand on its own, empty when the config is unreachable.
|
|
594
|
+
*
|
|
595
|
+
* `stubLawIds` and not `Object.keys(members)`: the `no_stub` subset is exactly
|
|
596
|
+
* the members whose law is missing or too long to carry the obligation alone,
|
|
597
|
+
* which is why the projector ships those full-bodied. Serving one of them its
|
|
598
|
+
* law here would be the thin-stub defect `rule_consequence_class.ts` exists to
|
|
599
|
+
* prevent, reproduced at the delivery end — and `stubLawIds` is that module's
|
|
600
|
+
* own answer, so re-deriving the distinction here would be a second reader of a
|
|
601
|
+
* class that is supposed to have one.
|
|
602
|
+
*
|
|
603
|
+
* MEMOISED PER ROOT. The config is 12 kB of JSON and this runs on the
|
|
604
|
+
* `user_prompt_submit` hot path; a dispatch is one process and an install does
|
|
605
|
+
* not change under it.
|
|
606
|
+
*/
|
|
607
|
+
const HI_CACHE = new Map<string, Set<string>>();
|
|
608
|
+
|
|
609
|
+
function highConsequenceIds(root: string): Set<string> {
|
|
610
|
+
const hit = HI_CACHE.get(root);
|
|
611
|
+
if (hit !== undefined) return hit;
|
|
612
|
+
let out = new Set<string>();
|
|
613
|
+
for (const r of [root, ruleSources(root).pkg]) {
|
|
614
|
+
if (r === null) continue;
|
|
615
|
+
try {
|
|
616
|
+
out = stubLawIds(readConsequenceClass(r));
|
|
617
|
+
break;
|
|
618
|
+
} catch {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
HI_CACHE.set(root, out);
|
|
623
|
+
return out;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** The manifest block, which is part of the budgeted string rather than beside it. */
|
|
627
|
+
function manifestText(rows: Array<[string, DeliveryForm]>): string {
|
|
628
|
+
return `<rule-manifest>\n${rows.map(([id, f]) => `${id}=${f}`).join('\n')}\n</rule-manifest>`;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Compose one fire under {@link COMPOSED_CHARS}, in the order D1 fixes.
|
|
633
|
+
*
|
|
634
|
+
* THE ORDER IS A DECISION, not an implementation detail. D1:
|
|
635
|
+
*
|
|
636
|
+
* 1. the law of every matched HIGH-CONSEQUENCE rule,
|
|
637
|
+
* 2. then the highest-priority full body that fits,
|
|
638
|
+
* 3. then further full bodies,
|
|
639
|
+
* 4. then the law of everything still unsent,
|
|
640
|
+
* 5. then the manifest.
|
|
641
|
+
*
|
|
642
|
+
* Phase 1 first because a high-consequence rule's law arriving is worth more
|
|
643
|
+
* than a lower-consequence rule's whole body, and a budget that filled in
|
|
644
|
+
* priority order alone would spend itself before reaching it.
|
|
645
|
+
*
|
|
646
|
+
* PHASE 1 IS A FLOOR AND NEVER A CEILING, which is the half the first
|
|
647
|
+
* implementation got backwards. A rule served its law in phase 1 can be
|
|
648
|
+
* UPGRADED to its whole body in phase 2 when the budget still has room for the
|
|
649
|
+
* difference — `put` replaces the part in place and charges only the delta.
|
|
650
|
+
* Without that, the 23 class members with a law section could never receive
|
|
651
|
+
* their body however empty the budget was: `security-sensitive-stop` would have
|
|
652
|
+
* dropped 5,687 characters to 429 on every fire, and its id still enters the
|
|
653
|
+
* seen-set, so the body would not have come back later either. D1's "then the
|
|
654
|
+
* highest-priority full body that fits" reads as a floor and has to behave like
|
|
655
|
+
* one.
|
|
656
|
+
*
|
|
657
|
+
* EVERY MATCHED CLASS MEMBER REACHES PHASE 1, including one the byte cap
|
|
658
|
+
* dropped. `selectForInjection` ranks on score alone and knows nothing about
|
|
659
|
+
* the class, so a high-consequence rule can lose that race; D1 says "every
|
|
660
|
+
* matched", so the text for a dropped class member is loaded here rather than
|
|
661
|
+
* left unreachable.
|
|
662
|
+
*
|
|
663
|
+
* NOTHING IS EVER TRUNCATED. A rule whose law alone does not fit is REPORTED —
|
|
664
|
+
* `omitted_budget` in the manifest — because half an obligation reads like a
|
|
665
|
+
* whole one and is the more dangerous output. The manifest's space is reserved
|
|
666
|
+
* BEFORE any text is admitted, using the longest label for every matched id, so
|
|
667
|
+
* the report can never be the thing that pushes the string over.
|
|
668
|
+
*
|
|
669
|
+
* `sel.dropped` is read here, and that is the point of the step as much as the
|
|
670
|
+
* budget is: before this, a body dropped by the byte cap left no trace at all,
|
|
671
|
+
* so a consumer could not tell a rule that did not match from one that matched
|
|
672
|
+
* and was silently discarded.
|
|
673
|
+
*/
|
|
674
|
+
function compose(
|
|
675
|
+
root: string,
|
|
676
|
+
sel: SelectionResult,
|
|
677
|
+
hi: Set<string>,
|
|
678
|
+
lawOnly = false,
|
|
679
|
+
): Injection | null {
|
|
680
|
+
const form = new Map<string, DeliveryForm>();
|
|
681
|
+
const full = new Map<string, string>();
|
|
682
|
+
const read = (id: string): void => {
|
|
683
|
+
const raw = loadRuleBody(root, id);
|
|
684
|
+
// STEP 1.4 — the host form, by the parser the thin projector uses.
|
|
685
|
+
// `ruleBody` strips frontmatter and HTML comments: the first is the
|
|
686
|
+
// routing surface the router already read, the second is authoring
|
|
687
|
+
// scaffolding no host renders to a model. A file with nothing left after
|
|
688
|
+
// the strip carries no obligation — an empty `<rule>` element is framing
|
|
689
|
+
// with no content inside it.
|
|
690
|
+
const body = raw === null ? '' : ruleBody(raw);
|
|
691
|
+
if (body === '') {
|
|
692
|
+
form.set(id, 'source_unavailable');
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
form.set(id, 'omitted_budget');
|
|
696
|
+
full.set(id, body);
|
|
697
|
+
};
|
|
698
|
+
for (const m of sel.dropped) {
|
|
699
|
+
if (hi.has(m.id)) {
|
|
700
|
+
read(m.id);
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
form.set(m.id, (sel.bodyBytes.get(m.id) ?? 0) === 0 ? 'source_unavailable' : 'omitted_budget');
|
|
704
|
+
}
|
|
705
|
+
for (const m of sel.selected) read(m.id);
|
|
706
|
+
|
|
707
|
+
const all = [...sel.selected, ...sel.dropped].sort((a, b) => a.order - b.order);
|
|
708
|
+
const tiers = new Map(all.map((m) => [m.id, m.tier]));
|
|
709
|
+
const reserve = manifestText(all.map((m) => [m.id, 'source_unavailable']));
|
|
710
|
+
let left = COMPOSED_CHARS - reserve.length - 2;
|
|
711
|
+
// BOTH UNITS RESERVE THE MANIFEST. The byte row is the registered one, so a
|
|
712
|
+
// manifest charged against the character budget and not against this one
|
|
713
|
+
// would let the emission exceed the row it is registered at.
|
|
714
|
+
let bytesLeft = CAP_BYTES - bytesOf(reserve) - 2;
|
|
715
|
+
const parts: string[] = [];
|
|
716
|
+
const partOf = new Map<string, number>();
|
|
717
|
+
const put = (id: string, kind: 'full' | 'law', text: string): boolean => {
|
|
718
|
+
const part = `<rule id="${id}" tier="${tiers.get(id) ?? ''}" form="${kind}">\n${text}\n</rule>`;
|
|
719
|
+
const at = partOf.get(id);
|
|
720
|
+
const prev = at === undefined ? '' : (parts[at] as string);
|
|
721
|
+
const sep = at === undefined && parts.length > 0 ? 2 : 0;
|
|
722
|
+
// BOTH UNITS, INDEPENDENTLY. The measured 1.0346 bytes-per-character
|
|
723
|
+
// maximum over today's corpus says the character budget binds first, and
|
|
724
|
+
// a measurement over one corpus is evidence rather than an invariant: a
|
|
725
|
+
// 6,000-character payload of three-byte code points is 18,000 bytes.
|
|
726
|
+
// Safety that rests on a ratio stops being safety the day the corpus
|
|
727
|
+
// changes, so the registered byte row is enforced here too and the ratio
|
|
728
|
+
// is what makes the second check almost never bind rather than what
|
|
729
|
+
// makes it unnecessary.
|
|
730
|
+
const cost = part.length - prev.length + sep;
|
|
731
|
+
const costBytes = bytesOf(part) - bytesOf(prev) + sep;
|
|
732
|
+
if (cost > left || costBytes > bytesLeft) return false;
|
|
733
|
+
left -= cost;
|
|
734
|
+
bytesLeft -= costBytes;
|
|
735
|
+
if (at === undefined) {
|
|
736
|
+
partOf.set(id, parts.length);
|
|
737
|
+
parts.push(part);
|
|
738
|
+
} else {
|
|
739
|
+
parts[at] = part;
|
|
740
|
+
}
|
|
741
|
+
form.set(id, kind);
|
|
742
|
+
return true;
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const ranked = [...all].sort((a, b) => b.score - a.score || a.order - b.order);
|
|
746
|
+
for (const m of ranked) {
|
|
747
|
+
const body = full.get(m.id);
|
|
748
|
+
if (body === undefined || !hi.has(m.id)) continue;
|
|
749
|
+
const law = lawText(body);
|
|
750
|
+
// A class member with no law section cannot be served by phase 1. It is
|
|
751
|
+
// left to compete for a full body below rather than reported, which is
|
|
752
|
+
// `no_stub`'s own answer to the same state.
|
|
753
|
+
if (law !== null) put(m.id, 'law', law);
|
|
754
|
+
}
|
|
755
|
+
for (const m of ranked) {
|
|
756
|
+
const body = full.get(m.id);
|
|
757
|
+
if (body === undefined || form.get(m.id) === 'full') continue;
|
|
758
|
+
// A RESTORE SENDS LAWS, with one exception: a rule with NO law section
|
|
759
|
+
// has no law to send, and reporting it `omitted_budget` would blame a
|
|
760
|
+
// budget decision nobody made. Those compete for their body instead, so
|
|
761
|
+
// every label on the restore is true.
|
|
762
|
+
if (lawOnly && lawText(body) !== null) continue;
|
|
763
|
+
put(m.id, 'full', body);
|
|
764
|
+
}
|
|
765
|
+
for (const m of ranked) {
|
|
766
|
+
const body = full.get(m.id);
|
|
767
|
+
if (body === undefined || form.get(m.id) !== 'omitted_budget') continue;
|
|
768
|
+
const law = lawText(body);
|
|
769
|
+
if (law !== null) put(m.id, 'law', law);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const rows: Array<[string, DeliveryForm]> = all.map((m) => [
|
|
773
|
+
m.id,
|
|
774
|
+
form.get(m.id) ?? 'source_unavailable',
|
|
775
|
+
]);
|
|
776
|
+
const ids = rows.filter(([, f]) => f === 'full' || f === 'law').map(([id]) => id);
|
|
777
|
+
// THE MANIFEST IS BOUNDED TOO, and by construction rather than by how many
|
|
778
|
+
// rules this router happens to carry. Its space is reserved above at the
|
|
779
|
+
// longest label, so it always fits what was reserved — but if the RESERVE
|
|
780
|
+
// itself exceeded the budget (enough matches that the report alone is over
|
|
781
|
+
// 8,000 characters) the fill loop would admit nothing and the manifest
|
|
782
|
+
// would be the whole emission, over the cap. No router today comes close;
|
|
783
|
+
// "no corpus today comes close" is the shape of claim the byte check above
|
|
784
|
+
// exists because of, so this is a property instead.
|
|
785
|
+
//
|
|
786
|
+
// UNDELIVERED ROWS GO FIRST. A row for a rule whose text IS in this string
|
|
787
|
+
// must never be the one dropped — that would report a delivered rule as
|
|
788
|
+
// omitted, which is the one thing AC-2 forbids outright.
|
|
789
|
+
const order = [...rows.keys()].sort(
|
|
790
|
+
(a, b) =>
|
|
791
|
+
Number(['full', 'law'].includes((rows[a] as [string, DeliveryForm])[1])) -
|
|
792
|
+
Number(['full', 'law'].includes((rows[b] as [string, DeliveryForm])[1])),
|
|
793
|
+
);
|
|
794
|
+
const head = parts.join('\n\n') + (parts.length > 0 ? '\n\n' : '');
|
|
795
|
+
let drop = 0;
|
|
796
|
+
let body = head + manifestText(rows);
|
|
797
|
+
while (body.length > COMPOSED_CHARS && drop < rows.length) {
|
|
798
|
+
drop += 1;
|
|
799
|
+
const cut = new Set(order.slice(0, drop));
|
|
800
|
+
const kept = rows.filter((_, i) => !cut.has(i));
|
|
801
|
+
body = head + manifestText([...kept, [`+${drop}`, 'omitted_budget']]);
|
|
802
|
+
}
|
|
803
|
+
return { rules: ids, bytes: bytesOf(body), body, manifest: rows };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Build the post-compaction restore, or `null` for silence (step 1.6).
|
|
808
|
+
*
|
|
809
|
+
* LAWS, NOT BODIES, and the asymmetry with a normal fire is the point. A
|
|
810
|
+
* compaction removed text the model was already under; re-sending every body
|
|
811
|
+
* would re-spend the whole budget on rules whose obligations the session may
|
|
812
|
+
* never touch again. The law is the part that cannot be inferred from the rest,
|
|
813
|
+
* so it is what the budget buys back. Everything that does not fit is reported
|
|
814
|
+
* in the same manifest as any other fire.
|
|
815
|
+
*
|
|
816
|
+
* The router supplies each id's tier and declaration order, because the
|
|
817
|
+
* seen-set deliberately carries neither — see {@link SeenState.pending}.
|
|
818
|
+
*/
|
|
819
|
+
function restore(root: string, ids: Set<string>): Injection | null {
|
|
820
|
+
if (ids.size === 0) return null;
|
|
821
|
+
let router;
|
|
822
|
+
try {
|
|
823
|
+
router = loadRouter(root);
|
|
824
|
+
} catch {
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
const selected: TierRuleMatch[] = [];
|
|
828
|
+
let order = 0;
|
|
829
|
+
for (const r of allTierRules(router)) {
|
|
830
|
+
if (ids.has(r.id)) selected.push({ id: r.id, tier: r.tier, score: 0, order });
|
|
831
|
+
order += 1;
|
|
832
|
+
}
|
|
833
|
+
if (selected.length === 0) return null;
|
|
834
|
+
return compose(
|
|
835
|
+
root,
|
|
836
|
+
{ selected, dropped: [], bytes: 0, bodyBytes: new Map() },
|
|
837
|
+
new Set(selected.map((m) => m.id)),
|
|
838
|
+
true,
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
|
|
287
842
|
/**
|
|
288
843
|
* Build the injection for one event, or `null` for silence.
|
|
289
844
|
*
|
|
@@ -305,21 +860,30 @@ export function buildInjection(
|
|
|
305
860
|
} catch {
|
|
306
861
|
return null; // no router — nothing to deliver, and never a failure
|
|
307
862
|
}
|
|
863
|
+
// STEP 1.3 — the install declares the scope, the package supplies the text.
|
|
864
|
+
//
|
|
865
|
+
// The router is the PACKAGE's list of every rule that could route. What a
|
|
866
|
+
// given consumer is actually under is what their install wrote into the
|
|
867
|
+
// host's rule directories, and the two are not the same set: 13 router tier
|
|
868
|
+
// rules are maintainer-only by `workspaces`, and before this filter a
|
|
869
|
+
// consumer could receive a body for a rule their install never carried.
|
|
870
|
+
//
|
|
871
|
+
// The two halves cannot be one read, and that is the reason this is a
|
|
872
|
+
// filter rather than a different body source. In `delivery` mode the
|
|
873
|
+
// installed files are thin stubs by construction, so the installed layer can
|
|
874
|
+
// say WHICH rules are in scope and never WHAT they say; the body still comes
|
|
875
|
+
// from the package corpus `ruleSources` resolves.
|
|
876
|
+
//
|
|
877
|
+
// `null` means no host rule layer exists to declare a scope — see
|
|
878
|
+
// `hostRuleLayerIds`. No filter then, because scoping to an empty set would
|
|
879
|
+
// re-create the silence step 1.1 repaired.
|
|
880
|
+
const scope = hostRuleLayerIds(root);
|
|
308
881
|
const matches = matchTierRules(router, prompt, openFiles, command).filter(
|
|
309
|
-
(m) => !seen.has(m.id),
|
|
882
|
+
(m) => !seen.has(m.id) && (scope === null || scope.has(m.id)),
|
|
310
883
|
);
|
|
311
884
|
if (matches.length === 0) return null;
|
|
312
885
|
const sel = selectForInjection(root, matches, CAP_BYTES);
|
|
313
|
-
|
|
314
|
-
const ids: string[] = [];
|
|
315
|
-
for (const m of sel.selected) {
|
|
316
|
-
const body = loadRuleBody(root, m.id);
|
|
317
|
-
if (body === null) continue;
|
|
318
|
-
ids.push(m.id);
|
|
319
|
-
parts.push(`<rule id="${m.id}" tier="${m.tier}">\n${body.trim()}\n</rule>`);
|
|
320
|
-
}
|
|
321
|
-
if (ids.length === 0) return null;
|
|
322
|
-
return { rules: ids, bytes: sel.bytes, body: parts.join('\n\n') };
|
|
886
|
+
return compose(root, sel, highConsequenceIds(root));
|
|
323
887
|
}
|
|
324
888
|
|
|
325
889
|
// ── main ─────────────────────────────────────────────────────────────────
|
|
@@ -358,10 +922,16 @@ export const DELIVERY_HOST = 'claude-code';
|
|
|
358
922
|
* to open. `AGENT_CONFIG_REPLAY` re-imposes it, which is what keeps
|
|
359
923
|
* `bench_hook_injection` measuring the configured tree rather than the probe.
|
|
360
924
|
*/
|
|
361
|
-
export function gateOpen(root: string, cliEntry: boolean): boolean {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
925
|
+
export function gateOpen(root: string, cliEntry: boolean, packageRoot?: string | null): boolean {
|
|
926
|
+
// ONE resolver since step 1.2 — the full cascade (template base, canonical
|
|
927
|
+
// `agents/settings/`, project root, user-global), not the legacy root file
|
|
928
|
+
// this concern used to read alone. `packageRoot` is where the template is
|
|
929
|
+
// found; inside the bundle nothing else can locate it.
|
|
930
|
+
const { mode, hosts } = resolveLeanProjection({
|
|
931
|
+
projectRoot: root,
|
|
932
|
+
packageRoot: packageRoot ?? null,
|
|
933
|
+
});
|
|
934
|
+
if (deliversBodies(mode) && hosts.hosts.includes(DELIVERY_HOST)) return true;
|
|
365
935
|
if (hookSectionEnabled(root, 'rule_inject')) return true;
|
|
366
936
|
return cliEntry && process.env['AGENT_CONFIG_REPLAY'] !== '1';
|
|
367
937
|
}
|
|
@@ -390,11 +960,38 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
390
960
|
const session = str(env, 'session_id', 'sessionId') ?? str(payload, 'session_id', 'sessionId') ?? 'unknown';
|
|
391
961
|
|
|
392
962
|
if (slot === 'pre_compact') {
|
|
393
|
-
|
|
394
|
-
return EXIT_ALLOW; // re-arm is silent; the next
|
|
963
|
+
armRestore(root, session);
|
|
964
|
+
return EXIT_ALLOW; // re-arm is silent; the restore happens on the next slot
|
|
965
|
+
}
|
|
966
|
+
if (slot === 'session_start') {
|
|
967
|
+
// ONLY on `source: compact`. `startup`, `resume`, `clear` and `fork`
|
|
968
|
+
// either begin a session that never had a seen-set or hand back a
|
|
969
|
+
// transcript the host already restored; emitting there would be a
|
|
970
|
+
// duplicate, and on `fork` a duplicate belonging to a session that is
|
|
971
|
+
// still alive. An unknown source emits nothing rather than guessing —
|
|
972
|
+
// the same stance `handoff-context`'s `sourceGate` takes.
|
|
973
|
+
if ((str(payload, 'source') ?? str(env, 'source')) !== 'compact') return EXIT_ALLOW;
|
|
974
|
+
if (!gateOpen(root, _isCliEntry(), ruleSources(root).pkg)) return EXIT_ALLOW;
|
|
975
|
+
// READ, then BUILD, then clear — never clear first. A restore that
|
|
976
|
+
// fires twice on one boundary is a duplicate delivery, so the set has to
|
|
977
|
+
// be cleared; clearing it BEFORE the build means a build that fails
|
|
978
|
+
// (the package moved mid-session, or an upgrade renamed every pending
|
|
979
|
+
// rule out of the router) loses the set permanently and silently, on the
|
|
980
|
+
// one slot that does not come round again.
|
|
981
|
+
const restored = restore(root, readPending(root, session));
|
|
982
|
+
if (restored === null) return EXIT_ALLOW;
|
|
983
|
+
clearPending(root, session);
|
|
984
|
+
recordDelivered(root, session, restored.rules);
|
|
985
|
+
return emit(restored, slot, root, session);
|
|
395
986
|
}
|
|
396
987
|
if (slot !== 'user_prompt_submit' && slot !== 'pre_tool_use') return EXIT_ALLOW;
|
|
397
|
-
|
|
988
|
+
|
|
989
|
+
// Resolved ONCE, before the gate. The gate needs the package root to find
|
|
990
|
+
// the settings template and the delivery needs it to find the corpus;
|
|
991
|
+
// reading the environment and probing the filesystem twice for the same
|
|
992
|
+
// answer is a cost every hook dispatch on every slot would pay.
|
|
993
|
+
const src = ruleSources(root);
|
|
994
|
+
if (!gateOpen(root, _isCliEntry(), src.pkg)) return EXIT_ALLOW;
|
|
398
995
|
|
|
399
996
|
let prompt = '';
|
|
400
997
|
let openFiles: string[] | null = null;
|
|
@@ -412,6 +1009,39 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
412
1009
|
openFiles = [fp];
|
|
413
1010
|
}
|
|
414
1011
|
|
|
1012
|
+
// Fail CLOSED and SAY SO. An empty delivery is indistinguishable from "no
|
|
1013
|
+
// rule matched", which is the exact silence this concern shipped with; one
|
|
1014
|
+
// line on stderr — which the dispatcher captures — is what makes a broken
|
|
1015
|
+
// install diagnosable from outside this file. Still allow: a carrier that
|
|
1016
|
+
// cannot find its corpus must not fail a turn.
|
|
1017
|
+
if (src.gap !== null) {
|
|
1018
|
+
// Fail CLOSED and SAY SO, through the channel the dispatcher actually
|
|
1019
|
+
// surfaces. Writing to stderr looks right and is not: `_run_concern_inproc`
|
|
1020
|
+
// captures a concern's stderr and `dispatch_hook` re-emits it only at
|
|
1021
|
+
// rc >= 3 (a crash), so a concern exiting allow is silent by
|
|
1022
|
+
// construction — which is the exact failure this diagnostic exists to
|
|
1023
|
+
// end, reproduced one layer up. Found by the 1.8 matrix against the
|
|
1024
|
+
// built bundle; no in-process fixture could have seen it.
|
|
1025
|
+
//
|
|
1026
|
+
// WHERE IT ACTUALLY LANDS, measured against the built bundle rather
|
|
1027
|
+
// than assumed: on Claude Code `emitFor` translates an advisory warn on
|
|
1028
|
+
// this slot into `hookSpecificOutput.additionalContext` at exit 0, so
|
|
1029
|
+
// this line reaches the MODEL, not the operator's terminal. That is the
|
|
1030
|
+
// host's translation and not a choice available here — `reason` alone
|
|
1031
|
+
// has no terminal-facing path on this slot.
|
|
1032
|
+
//
|
|
1033
|
+
// It is the right place anyway: the agent is what the user is talking
|
|
1034
|
+
// to, so an agent told its rule corpus is missing can say so. The cost
|
|
1035
|
+
// is one short line per turn for as long as the install stays broken,
|
|
1036
|
+
// which is bounded and deliberate. Bounding it further — once per
|
|
1037
|
+
// session, via the seen-set — needs a state write this concern cannot
|
|
1038
|
+
// currently afford (`src/config/hook-bundle-budget.json` left 30 bytes
|
|
1039
|
+
// of headroom at this commit), and is recorded in the roadmap rather
|
|
1040
|
+
// than silently skipped.
|
|
1041
|
+
process.stdout.write(`${JSON.stringify({ decision: 'warn', reason: src.gap })}\n`);
|
|
1042
|
+
return EXIT_WARN;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
415
1045
|
const seen = readSeen(root, session);
|
|
416
1046
|
const injection = buildInjection(root, prompt, openFiles, command, seen);
|
|
417
1047
|
if (injection === null) return EXIT_ALLOW; // silence is the default
|
|
@@ -420,10 +1050,77 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
420
1050
|
writeSeen(root, session, seen);
|
|
421
1051
|
recordDelivered(root, session, injection.rules);
|
|
422
1052
|
|
|
1053
|
+
return emit(injection, slot, root, session);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Where this session's arrival record lives (step 0.3).
|
|
1058
|
+
*
|
|
1059
|
+
* BESIDE THE SEEN-SET, under the user-global root and keyed by project, for the
|
|
1060
|
+
* reason step 1.7 moved that one: a measurement about a session is not state
|
|
1061
|
+
* about the project, and a consumer's `git status` is the wrong place to learn
|
|
1062
|
+
* what this dispatcher counted. The delivered-row ledger stays where it is —
|
|
1063
|
+
* `road-to-a-stop-that-holds` Phase 3 joins on it — and this is a different
|
|
1064
|
+
* record with a different reader, so the two do not have to live together.
|
|
1065
|
+
*
|
|
1066
|
+
* JSONL rather than one rewritten object: a fire appends, and an append cannot
|
|
1067
|
+
* lose the rows already written by partially serialising the whole file. The
|
|
1068
|
+
* over-budget COUNT the step asks for is the sum of the `over_budget` column,
|
|
1069
|
+
* which a reader computes; storing a running total here would be a second
|
|
1070
|
+
* number that can disagree with the rows it summarises.
|
|
1071
|
+
*/
|
|
1072
|
+
export function arrivalPath(root: string, session: string): string {
|
|
1073
|
+
const safe = session.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 80) || 'unknown';
|
|
1074
|
+
return path.join(
|
|
1075
|
+
user_global_paths.write_target(path.join('state', 'rule-inject', projectKey(root))),
|
|
1076
|
+
`${safe}.arrival.jsonl`,
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Append one fire's arrival record, best-effort.
|
|
1082
|
+
*
|
|
1083
|
+
* NEVER THROWS. This is an instrument, and a turn refused because an instrument
|
|
1084
|
+
* missed a reading is worse than the missing reading — the same stance
|
|
1085
|
+
* `appendDelivered` takes, for the same reason.
|
|
1086
|
+
*/
|
|
1087
|
+
export function recordArrival(root: string, session: string, record: ArrivalRecord): void {
|
|
1088
|
+
try {
|
|
1089
|
+
const p = arrivalPath(root, session);
|
|
1090
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
1091
|
+
fs.appendFileSync(p, `${JSON.stringify(record)}\n`, 'utf-8');
|
|
1092
|
+
} catch {
|
|
1093
|
+
/* an unwritable instrument costs a reading, never a turn */
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/** Read back what this session recorded, dropping any line nothing can parse. */
|
|
1098
|
+
export function readArrival(root: string, session: string): ArrivalRecord[] {
|
|
1099
|
+
let raw: string;
|
|
1100
|
+
try {
|
|
1101
|
+
raw = fs.readFileSync(arrivalPath(root, session), 'utf-8');
|
|
1102
|
+
} catch {
|
|
1103
|
+
return [];
|
|
1104
|
+
}
|
|
1105
|
+
const out: ArrivalRecord[] = [];
|
|
1106
|
+
for (const line of raw.split('\n')) {
|
|
1107
|
+
if (line.trim() === '') continue;
|
|
1108
|
+
try {
|
|
1109
|
+
out.push(JSON.parse(line) as ArrivalRecord);
|
|
1110
|
+
} catch {
|
|
1111
|
+
continue; // a torn last line must not cost the rows before it
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return out;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/** The one place a delivery reaches the host, shared by the fire and the restore. */
|
|
1118
|
+
function emit(injection: Injection, slot: string, root: string, session: string): number {
|
|
1119
|
+
recordArrival(root, session, arrivalRecord(injection, slot));
|
|
423
1120
|
process.stdout.write(
|
|
424
1121
|
`${JSON.stringify({
|
|
425
1122
|
decision: 'warn',
|
|
426
|
-
reason: `rule-inject: ${injection.rules.length} rule
|
|
1123
|
+
reason: `rule-inject: ${injection.rules.length}/${injection.manifest.length} matched rule(s) sent on ${slot} (${injection.body.length} chars)`,
|
|
427
1124
|
additional_context: injection.body,
|
|
428
1125
|
})}\n`,
|
|
429
1126
|
);
|