@event4u/agent-config 16.0.0 → 16.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +289 -140
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +74 -27
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/commands/work.md +4 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +47 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +19 -23
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/rules/ui-audit-gate.md +1 -2
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +80 -4
- package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +30 -5
- package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
- package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +218 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/dist/cli/python/knowledge_ingest.js +1 -1
- package/dist/cli/python/workspace_drive.js +1 -1
- package/dist/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-7XGBX6ZH.js} +973 -555
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-FP36SWJD.js} +587 -141
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-IGD6ERX6.js} +62 -7
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-JNMVLWLR.js} +99 -14
- package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
- package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +985 -831
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +7 -7
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +3 -3
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +90 -43
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +7682 -2772
- package/dist/hooks/dispatch.sha256 +1 -0
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2200 -1559
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3066 -1470
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +40 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
- package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +28 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/src/config/agent-settings.template.yml +168 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +36 -5
- package/src/config/continuity-surface.json +23 -2
- package/src/config/gate-coverage.yml +377 -13
- package/src/config/gate-violation-baselines.json +10 -6
- package/src/config/hook-latency-budget.json +21 -0
- package/src/config/hook-token-budget.json +4 -2
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/metric-registry.yml +114 -0
- package/src/config/placeholder-drift-budget.json +10 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +33 -5
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +7 -20
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +173 -0
- package/src/scripts/_cli/doctor_network_posture.ts +226 -0
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/body_portable.ts +102 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/cc_transcript.ts +132 -1
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/concern_sla_window.ts +200 -0
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/forge_protection.ts +381 -0
- package/src/scripts/_lib/forge_reader.ts +558 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/host_env_write.ts +111 -0
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +513 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/probe_inputs.ts +271 -0
- package/src/scripts/_lib/read_surface_scan.ts +322 -0
- package/src/scripts/_lib/release_highlights.ts +201 -11
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/shell_write_shapes.ts +108 -0
- package/src/scripts/_lib/structural_hiding.ts +622 -0
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/turn_end_refusals.ts +525 -4
- package/src/scripts/_lib/turn_end_transcript.ts +216 -0
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -0
- package/src/scripts/_lib/verification_command.ts +324 -0
- package/src/scripts/_lib/verification_evidence.ts +647 -0
- package/src/scripts/_lib/verify_clause.ts +280 -0
- package/src/scripts/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ai_team/team_dispatch.ts +4 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/before_complete_hook.ts +340 -23
- package/src/scripts/bench_hook_latency.ts +412 -5
- package/src/scripts/cache_realization_report.ts +61 -1
- package/src/scripts/check_argument_hint.ts +287 -0
- package/src/scripts/check_claims.ts +125 -1
- package/src/scripts/check_enforcement_coverage.ts +38 -9
- package/src/scripts/check_enforcement_matrix.ts +866 -0
- package/src/scripts/check_finding_dispositions.ts +22 -3
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +1025 -0
- package/src/scripts/check_host_docs_digest.ts +786 -0
- package/src/scripts/check_host_format_column.ts +559 -0
- package/src/scripts/check_input_references.ts +228 -0
- package/src/scripts/check_inputs_parity.ts +235 -0
- package/src/scripts/check_kernel_edit_ratified.ts +47 -5
- package/src/scripts/check_kill_switch_table.ts +294 -0
- package/src/scripts/check_placeholder_drift.ts +221 -0
- package/src/scripts/check_preamble_payload_budget.ts +75 -2
- package/src/scripts/check_read_surface_coverage.ts +515 -0
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +568 -0
- package/src/scripts/check_routing_coverage.ts +378 -11
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_static_parity.ts +17 -0
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/ci_settle.ts +145 -19
- package/src/scripts/closure_scan.ts +447 -0
- package/src/scripts/compile_hook_manifest.ts +102 -18
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/external_sources_denylist.json +29 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +127 -11
- package/src/scripts/hooks/block_config_weakening.ts +229 -4
- package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
- package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
- package/src/scripts/hooks/bundle_integrity.ts +291 -0
- package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
- package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
- package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/concern_timings.ts +94 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
- package/src/scripts/hooks/design_pass_hook.ts +221 -5
- package/src/scripts/hooks/design_slop_hook.ts +1 -2
- package/src/scripts/hooks/dispatch_hook.ts +25 -19
- package/src/scripts/hooks/edit_shape_hook.ts +1 -2
- package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
- package/src/scripts/hooks/evidence_independence.ts +97 -2
- package/src/scripts/hooks/exit_codes.ts +128 -0
- package/src/scripts/hooks/host_lowering.json +1 -0
- package/src/scripts/hooks/host_lowering.ts +128 -3
- package/src/scripts/hooks/host_lowering.yaml +262 -46
- package/src/scripts/hooks/host_semantics.ts +30 -2
- package/src/scripts/hooks/injection_budget.ts +9 -2
- package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
- package/src/scripts/hooks/journal_record_hook.ts +1 -2
- package/src/scripts/hooks/memory_recall_hook.ts +539 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +328 -0
- package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
- package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
- package/src/scripts/hooks/permission_gate.ts +1 -2
- package/src/scripts/hooks/push_settle_hook.ts +1 -3
- package/src/scripts/hooks/replay_hook.ts +2 -1
- package/src/scripts/hooks/reread_guard_hook.ts +1 -2
- package/src/scripts/hooks/review_baseline_hook.ts +1 -2
- package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
- package/src/scripts/hooks/rule_inject_hook.ts +55 -4
- package/src/scripts/hooks/run_continuation_hook.ts +30 -35
- package/src/scripts/hooks/skill_route_hook.ts +1 -2
- package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
- package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
- package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
- package/src/scripts/hooks/table_fingerprint.ts +49 -0
- package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
- package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
- package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
- package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
- package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
- package/src/scripts/hooks_doctor.ts +85 -0
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +109 -210
- package/src/scripts/janitor.ts +7 -0
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_deny_text.ts +419 -0
- package/src/scripts/lint_exit_codes.ts +262 -0
- package/src/scripts/lint_hook_manifest.ts +66 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +139 -12
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/lint_skill_descriptions.ts +1 -1
- package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
- package/src/scripts/mcp_server/prompts.ts +142 -1
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
- package/src/scripts/measure_turn_end_gate.ts +193 -0
- package/src/scripts/memory_lookup.ts +1 -1
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/prepack-check.mjs +53 -0
- package/src/scripts/release.ts +14 -98
- package/src/scripts/release_drill.ts +267 -1
- package/src/scripts/release_publication.ts +64 -1
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_invocation_surface.ts +383 -0
- package/src/scripts/report_obligation_carriers.ts +699 -0
- package/src/scripts/report_obligation_writer_split.ts +236 -0
- package/src/scripts/report_skill_menu_census.ts +208 -0
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/roadmap_verify_share.ts +213 -0
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +48 -0
- package/src/scripts/schemas/review-findings.schema.json +4 -0
- package/src/scripts/schemas/skill.schema.json +44 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/self_review_gate.ts +359 -10
- package/src/scripts/semantic_parity_census.ts +314 -0
- package/src/scripts/session_eol_report.ts +54 -1
- package/src/scripts/sweep_dead_scan_roots.ts +1 -1
- package/src/scripts/trigger_eval_grandfather.json +0 -3
- package/src/scripts/trigger_eval_rotation.ts +122 -27
- package/src/scripts/ui_conformance_probe.ts +774 -0
- package/src/scripts/update_prices.ts +85 -3
- package/src/scripts/write_bundle_digest.mjs +74 -0
- package/src/server/schemas/settings.ts +62 -7
- package/src/shared/missionExecution.ts +126 -0
- package/src/templates/consumer-settings/ONBOARDING.md +1 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
- package/dist/ui/assets/index-D0jaCRPM.js +0 -29
- package/dist/ui/assets/index-jSfMW8_t.css +0 -1
- /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* A deny never names its own kill switch.
|
|
4
|
+
*
|
|
5
|
+
* road-to-a-kernel-that-guards-its-plumbing 1.3.
|
|
6
|
+
*
|
|
7
|
+
* A blocking concern refuses a tool call and then prints a message to the one
|
|
8
|
+
* party that wanted the call to succeed. If that message names the environment
|
|
9
|
+
* variable, the `export` line or the settings key that turns the guard off, the
|
|
10
|
+
* refusal is a speed bump with instructions attached. The reader most motivated
|
|
11
|
+
* to follow them is the agent that was just refused.
|
|
12
|
+
*
|
|
13
|
+
* The rule is therefore about the OUTPUT, not about the mechanism: a kill
|
|
14
|
+
* switch may exist, may be documented in the concern's header, may be listed in
|
|
15
|
+
* the manifest and in the contract. It may not appear in the text the refusal
|
|
16
|
+
* prints. Two of the nine blocking concerns carry a kill switch today and both
|
|
17
|
+
* name it only in a comment — this gate is what keeps that true, rather than
|
|
18
|
+
* true-so-far.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IS SCANNED, stated precisely because the boundary is the whole design.
|
|
21
|
+
*
|
|
22
|
+
* The corpus is the `severity: blocking` concerns in `hook_manifest.yaml`, read
|
|
23
|
+
* from the manifest rather than listed here, so a guard added there joins the
|
|
24
|
+
* corpus without an edit. Within each, the unit is a MESSAGE-SHAPED string
|
|
25
|
+
* literal — comments stripped first, then any literal containing whitespace.
|
|
26
|
+
*
|
|
27
|
+
* The prose test is the discriminator, and it is what makes the settings-key
|
|
28
|
+
* rule usable at all: `'hooks.enabled'` as a lookup key is how a concern reads
|
|
29
|
+
* its own configuration and is not a message, while
|
|
30
|
+
* `'set hooks.enabled: false to skip'` is. A rule that fired on the bare key
|
|
31
|
+
* would fire on every concern that reads a setting, which is the false-positive
|
|
32
|
+
* shape that gets a young gate disabled.
|
|
33
|
+
*
|
|
34
|
+
* WHAT IT DOES NOT SEE, so nobody reads more into a green than is there:
|
|
35
|
+
* a message assembled at runtime from non-literal parts, a message in a module
|
|
36
|
+
* the concern imports, and a kill switch named in a way none of the three
|
|
37
|
+
* patterns matches. This is a pattern gate over source text, not a proof about
|
|
38
|
+
* emitted bytes.
|
|
39
|
+
*
|
|
40
|
+
* Exit codes: 0 clean · 1 a deny names a bypass · 2 the corpus could not be read.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import fs from 'node:fs';
|
|
44
|
+
import os from 'node:os';
|
|
45
|
+
import path from 'node:path';
|
|
46
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
47
|
+
|
|
48
|
+
import * as yaml from 'js-yaml';
|
|
49
|
+
|
|
50
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
51
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
52
|
+
import { assertScanned, reportScanned } from './_lib/scan_scope.js';
|
|
53
|
+
|
|
54
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
55
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
56
|
+
|
|
57
|
+
export const GATE = 'lint_deny_text';
|
|
58
|
+
|
|
59
|
+
const MANIFEST_REL = 'src/scripts/hook_manifest.yaml';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Top-level settings keys, so `hooks.enabled` reads as a settings path and
|
|
63
|
+
* `payload.tool_input` does not.
|
|
64
|
+
*
|
|
65
|
+
* A fixed list rather than a parse of the template: the template is a large
|
|
66
|
+
* file with placeholder substitutions, and the gate needs the discriminator to
|
|
67
|
+
* be stable and reviewable in a diff. Adding a top-level settings section is
|
|
68
|
+
* the moment to add it here; a missing entry costs a miss, never a false
|
|
69
|
+
* refusal.
|
|
70
|
+
*/
|
|
71
|
+
export const SETTINGS_ROOTS: readonly string[] = [
|
|
72
|
+
'ai_team',
|
|
73
|
+
'augment',
|
|
74
|
+
'chat_history',
|
|
75
|
+
'code_style',
|
|
76
|
+
'commands',
|
|
77
|
+
'consistency',
|
|
78
|
+
'continuity',
|
|
79
|
+
'cost',
|
|
80
|
+
'decision_engine',
|
|
81
|
+
'delivery',
|
|
82
|
+
'design',
|
|
83
|
+
'emergency',
|
|
84
|
+
'execution',
|
|
85
|
+
'github',
|
|
86
|
+
'hooks',
|
|
87
|
+
'knowledge',
|
|
88
|
+
'memory',
|
|
89
|
+
'model',
|
|
90
|
+
'onboarding',
|
|
91
|
+
'personal',
|
|
92
|
+
'pipelines',
|
|
93
|
+
'planning',
|
|
94
|
+
'profile',
|
|
95
|
+
'project',
|
|
96
|
+
'projection',
|
|
97
|
+
'quality',
|
|
98
|
+
'reasoning',
|
|
99
|
+
'roadmap',
|
|
100
|
+
'screenshots',
|
|
101
|
+
'subagents',
|
|
102
|
+
'telegraph',
|
|
103
|
+
'tokens',
|
|
104
|
+
'update_check',
|
|
105
|
+
'verbosity',
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
export interface DenyFinding {
|
|
109
|
+
readonly concern: string;
|
|
110
|
+
readonly file: string;
|
|
111
|
+
readonly kind: 'env-assignment' | 'export-line' | 'settings-key';
|
|
112
|
+
readonly text: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Characters after which a `/` begins a REGEX literal rather than a division.
|
|
117
|
+
*
|
|
118
|
+
* Regex handling is not a nicety here, it is the difference between a working
|
|
119
|
+
* gate and a nonsense one — measured on the first run against the real tree.
|
|
120
|
+
* `turn_end_gate_hook.ts` carries `/\bI (wi)ll\b/i`; without this the
|
|
121
|
+
* apostrophe inside a pattern like `/\bI'll\b/` opens a phantom string literal
|
|
122
|
+
* that swallows the next few hundred characters of real code and comments, and
|
|
123
|
+
* the gate then reports `export function …` as deny text. Three such
|
|
124
|
+
* false positives, all in one file, all from one unhandled token class.
|
|
125
|
+
*/
|
|
126
|
+
const REGEX_PRECEDERS = new Set('(,=:[!&|?{};+-*%~^<>'.split(''));
|
|
127
|
+
|
|
128
|
+
/** Strip `//` and block comments, preserving string literals and regexes. */
|
|
129
|
+
export function stripComments(src: string): string {
|
|
130
|
+
let out = '';
|
|
131
|
+
let i = 0;
|
|
132
|
+
let quote: string | null = null;
|
|
133
|
+
let lastSignificant = '';
|
|
134
|
+
while (i < src.length) {
|
|
135
|
+
const c = src[i] as string;
|
|
136
|
+
const next = src[i + 1];
|
|
137
|
+
if (quote !== null) {
|
|
138
|
+
out += c;
|
|
139
|
+
if (c === '\\') {
|
|
140
|
+
out += src[i + 1] ?? '';
|
|
141
|
+
i += 2;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (c === quote) {
|
|
145
|
+
quote = null;
|
|
146
|
+
}
|
|
147
|
+
i += 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (c === "'" || c === '"' || c === '`') {
|
|
151
|
+
quote = c;
|
|
152
|
+
out += c;
|
|
153
|
+
i += 1;
|
|
154
|
+
lastSignificant = c;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (c === '/' && next === '/') {
|
|
158
|
+
while (i < src.length && src[i] !== '\n') i += 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (c === '/' && next === '*') {
|
|
162
|
+
i += 2;
|
|
163
|
+
while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) i += 1;
|
|
164
|
+
i += 2;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (c === '/' && (lastSignificant === '' || REGEX_PRECEDERS.has(lastSignificant))) {
|
|
168
|
+
// Regex literal — consumed whole, emitted as a placeholder so the
|
|
169
|
+
// scanner below never sees its contents as a message.
|
|
170
|
+
i += 1;
|
|
171
|
+
let inClass = false;
|
|
172
|
+
while (i < src.length) {
|
|
173
|
+
const r = src[i] as string;
|
|
174
|
+
if (r === '\\') {
|
|
175
|
+
i += 2;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (r === '[') inClass = true;
|
|
179
|
+
else if (r === ']') inClass = false;
|
|
180
|
+
else if (r === '/' && !inClass) break;
|
|
181
|
+
else if (r === '\n') break;
|
|
182
|
+
i += 1;
|
|
183
|
+
}
|
|
184
|
+
i += 1;
|
|
185
|
+
out += '0';
|
|
186
|
+
lastSignificant = '0';
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
out += c;
|
|
190
|
+
i += 1;
|
|
191
|
+
if (!/\s/.test(c)) {
|
|
192
|
+
lastSignificant = c;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Message-shaped string literals — comments already stripped. */
|
|
199
|
+
export function messageLiterals(src: string): string[] {
|
|
200
|
+
const out: string[] = [];
|
|
201
|
+
const re = /'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)"|`((?:[^`\\]|\\.)*)`/g;
|
|
202
|
+
for (const m of src.matchAll(re)) {
|
|
203
|
+
const body = m[1] ?? m[2] ?? m[3] ?? '';
|
|
204
|
+
if (/\s/.test(body)) {
|
|
205
|
+
out.push(body);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const ENV_ASSIGNMENT_RE = /AGENT_CONFIG_[A-Z0-9_]+\s*=/;
|
|
212
|
+
const EXPORT_RE = /(^|[\s`'"(])export\s/;
|
|
213
|
+
|
|
214
|
+
function settingsKeyIn(text: string): boolean {
|
|
215
|
+
for (const root of SETTINGS_ROOTS) {
|
|
216
|
+
const re = new RegExp(`(^|[^A-Za-z0-9_.])${root}\\.[a-z][a-z0-9_]*`);
|
|
217
|
+
if (re.test(text)) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Findings for one concern's source text. */
|
|
225
|
+
export function scanSource(concern: string, file: string, src: string): DenyFinding[] {
|
|
226
|
+
const out: DenyFinding[] = [];
|
|
227
|
+
for (const text of messageLiterals(stripComments(src))) {
|
|
228
|
+
if (ENV_ASSIGNMENT_RE.test(text)) {
|
|
229
|
+
out.push({ concern, file, kind: 'env-assignment', text });
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (EXPORT_RE.test(text)) {
|
|
233
|
+
out.push({ concern, file, kind: 'export-line', text });
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (settingsKeyIn(text)) {
|
|
237
|
+
out.push({ concern, file, kind: 'settings-key', text });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** `severity: blocking` concerns, as `[name, script]`, read from the manifest. */
|
|
244
|
+
export function blockingConcerns(manifestText: string): Array<[string, string]> {
|
|
245
|
+
const doc = yaml.load(manifestText) as { concerns?: Record<string, Record<string, unknown>> };
|
|
246
|
+
const concerns = doc?.concerns ?? {};
|
|
247
|
+
const out: Array<[string, string]> = [];
|
|
248
|
+
for (const [name, body] of Object.entries(concerns)) {
|
|
249
|
+
if (String(body['severity'] ?? '') !== 'blocking') continue;
|
|
250
|
+
const script = body['script'];
|
|
251
|
+
if (typeof script === 'string') out.push([name, script]);
|
|
252
|
+
}
|
|
253
|
+
return out.sort((a, b) => a[0].localeCompare(b[0]));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function scan(root: string): { findings: DenyFinding[]; scanned: number } {
|
|
257
|
+
const manifest = path.join(root, MANIFEST_REL);
|
|
258
|
+
if (!fs.existsSync(manifest)) {
|
|
259
|
+
return { findings: [], scanned: 0 };
|
|
260
|
+
}
|
|
261
|
+
const findings: DenyFinding[] = [];
|
|
262
|
+
let scanned = 0;
|
|
263
|
+
for (const [name, script] of blockingConcerns(fs.readFileSync(manifest, 'utf8'))) {
|
|
264
|
+
const abs = path.join(root, script);
|
|
265
|
+
if (!fs.existsSync(abs)) continue;
|
|
266
|
+
scanned += 1;
|
|
267
|
+
findings.push(...scanSource(name, script, fs.readFileSync(abs, 'utf8')));
|
|
268
|
+
}
|
|
269
|
+
return { findings, scanned };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const REASON: Readonly<Record<DenyFinding['kind'], string>> = {
|
|
273
|
+
'env-assignment': 'names an environment kill switch, with its value',
|
|
274
|
+
'export-line': 'carries an `export` line the reader can paste',
|
|
275
|
+
'settings-key': 'names a settings key path',
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
export function main(argv: readonly string[]): number {
|
|
279
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
280
|
+
const rootIdx = argv.indexOf('--root');
|
|
281
|
+
const root = rootIdx === -1 ? ROOT : (argv[rootIdx + 1] ?? ROOT);
|
|
282
|
+
|
|
283
|
+
const { findings, scanned } = scan(root);
|
|
284
|
+
|
|
285
|
+
const ledger = new GateLedger(GATE);
|
|
286
|
+
ledger.plan(findings.map((f) => `${f.concern}:${f.kind}:${f.text.slice(0, 40)}`));
|
|
287
|
+
for (const f of findings) {
|
|
288
|
+
ledger.fail(`${f.concern}:${f.kind}:${f.text.slice(0, 40)}`, REASON[f.kind]);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// A gate that read nothing has not passed. BEFORE the verdict: a manifest
|
|
292
|
+
// that moved, or a `severity:` key that was renamed, would otherwise
|
|
293
|
+
// certify an empty corpus clean.
|
|
294
|
+
assertScanned({
|
|
295
|
+
gate: GATE,
|
|
296
|
+
scanned,
|
|
297
|
+
units: 'blocking concern(s)',
|
|
298
|
+
roots: [MANIFEST_REL],
|
|
299
|
+
});
|
|
300
|
+
reportScanned({
|
|
301
|
+
gate: GATE,
|
|
302
|
+
scanned,
|
|
303
|
+
units: 'blocking concern(s)',
|
|
304
|
+
roots: [MANIFEST_REL],
|
|
305
|
+
});
|
|
306
|
+
ledger.report();
|
|
307
|
+
|
|
308
|
+
if (findings.length > 0) {
|
|
309
|
+
for (const f of findings) {
|
|
310
|
+
process.stdout.write(
|
|
311
|
+
`❌ ${GATE}: ${f.file} (${f.concern}) ${REASON[f.kind]}:\n ${f.text.slice(0, 200)}\n`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
process.stdout.write(
|
|
315
|
+
'\nA blocking concern may HAVE a kill switch. Its refusal message may not name it.\n' +
|
|
316
|
+
"Move the sentence to the concern's header comment, the manifest entry, or\n" +
|
|
317
|
+
'docs/contracts/hook-architecture-v1.md — all three are read by people deciding\n' +
|
|
318
|
+
'whether the guard should exist, and none is read by the agent it just refused.\n',
|
|
319
|
+
);
|
|
320
|
+
return 1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
process.stdout.write(
|
|
324
|
+
`✅ ${GATE}: no blocking concern names its own bypass ` +
|
|
325
|
+
`(${String(scanned)} concern(s) scanned).\n`,
|
|
326
|
+
);
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function selfTest(): number {
|
|
331
|
+
const plant = (name: string, body: string): string => {
|
|
332
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `deny-text-${name}-`));
|
|
333
|
+
fs.mkdirSync(path.join(dir, 'src', 'scripts', 'hooks'), { recursive: true });
|
|
334
|
+
fs.writeFileSync(
|
|
335
|
+
path.join(dir, 'src', 'scripts', 'hook_manifest.yaml'),
|
|
336
|
+
[
|
|
337
|
+
'schema_version: 1',
|
|
338
|
+
'concerns:',
|
|
339
|
+
' probe-guard:',
|
|
340
|
+
' script: src/scripts/hooks/probe_guard.ts',
|
|
341
|
+
' severity: blocking',
|
|
342
|
+
' probe-advisory:',
|
|
343
|
+
' script: src/scripts/hooks/probe_advisory.ts',
|
|
344
|
+
' severity: advisory',
|
|
345
|
+
'',
|
|
346
|
+
].join('\n'),
|
|
347
|
+
'utf-8',
|
|
348
|
+
);
|
|
349
|
+
fs.writeFileSync(path.join(dir, 'src', 'scripts', 'hooks', 'probe_guard.ts'), body, 'utf-8');
|
|
350
|
+
fs.writeFileSync(
|
|
351
|
+
path.join(dir, 'src', 'scripts', 'hooks', 'probe_advisory.ts'),
|
|
352
|
+
"process.stderr.write('set AGENT_CONFIG_X=1 to skip this advice');\n",
|
|
353
|
+
'utf-8',
|
|
354
|
+
);
|
|
355
|
+
return dir;
|
|
356
|
+
};
|
|
357
|
+
const emit = (msg: string): string => `process.stderr.write(${JSON.stringify(msg)});\n`;
|
|
358
|
+
const run = (root: string): number =>
|
|
359
|
+
runGateCli(ROOT, path.join('src', 'scripts', 'lint_deny_text.ts'), ['--root', root], ROOT);
|
|
360
|
+
|
|
361
|
+
return runSelfTest({
|
|
362
|
+
gate: GATE,
|
|
363
|
+
minCases: 7,
|
|
364
|
+
minRejectCases: 3,
|
|
365
|
+
cases: [
|
|
366
|
+
{
|
|
367
|
+
name: 'an empty corpus is refused, not certified green',
|
|
368
|
+
expect: 'reject',
|
|
369
|
+
run: () => run(fs.mkdtempSync(path.join(os.tmpdir(), 'deny-text-empty-'))),
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: 'a deny naming an env kill switch with its value fails',
|
|
373
|
+
expect: 'reject',
|
|
374
|
+
run: () => run(plant('env', emit('BLOCKED — set AGENT_CONFIG_X=1 to skip'))),
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'a deny carrying an export line fails',
|
|
378
|
+
expect: 'reject',
|
|
379
|
+
run: () => run(plant('export', emit('BLOCKED — run export AC_OFF=1 first'))),
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: 'a deny naming a settings key path fails',
|
|
383
|
+
expect: 'reject',
|
|
384
|
+
run: () => run(plant('settings', emit('BLOCKED — flip hooks.enabled to false'))),
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: 'a deny that names no bypass passes',
|
|
388
|
+
expect: 'accept',
|
|
389
|
+
run: () => run(plant('clean', emit('BLOCKED — rebuild it from its source'))),
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: 'the kill switch named in a COMMENT passes — the rule is about output',
|
|
393
|
+
expect: 'accept',
|
|
394
|
+
run: () =>
|
|
395
|
+
run(
|
|
396
|
+
plant(
|
|
397
|
+
'comment',
|
|
398
|
+
'// Kill switch: AGENT_CONFIG_X=1.\n/* or AGENT_CONFIG_Y=1 */\n' +
|
|
399
|
+
emit('BLOCKED — rebuild it from its source'),
|
|
400
|
+
),
|
|
401
|
+
),
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: 'a bare settings key used as a lookup passes — it is not a message',
|
|
405
|
+
expect: 'accept',
|
|
406
|
+
run: () => run(plant('lookup', "const k = 'hooks.enabled';\nvoid k;\n")),
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: 'an ADVISORY concern naming its switch passes — only denials are scanned',
|
|
410
|
+
expect: 'accept',
|
|
411
|
+
run: () => run(plant('advisory-only', emit('BLOCKED — rebuild it'))),
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
418
|
+
process.exit(main(process.argv.slice(2)));
|
|
419
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* A hook's exit code comes from the table, never from a numeral.
|
|
4
|
+
*
|
|
5
|
+
* road-to-a-kernel-that-guards-its-plumbing 4.1, the enforcing half.
|
|
6
|
+
* `hooks/exit_codes.ts` is the table; this is what keeps it the only one.
|
|
7
|
+
*
|
|
8
|
+
* WHAT THE NUMERALS COST. Thirty-three files under `src/scripts/hooks/` each
|
|
9
|
+
* declared their own `EXIT_ALLOW = 0` / `EXIT_BLOCK = 1` / `EXIT_WARN = 2`, two
|
|
10
|
+
* of them under a third name. The danger is not a typo: 1 and 2 mean the
|
|
11
|
+
* OPPOSITE things on Claude Code from what they mean in this tree's internal
|
|
12
|
+
* language, which is why `host_semantics.ts` exists to lower one onto the
|
|
13
|
+
* other. A bare `process.exit(1)` in a concern is a claim about which of the
|
|
14
|
+
* two dialects the writer had in mind, and the source does not record which.
|
|
15
|
+
*
|
|
16
|
+
* WHAT IT SCANS. `.ts` under `src/scripts/hooks/`, refusing a call whose
|
|
17
|
+
* argument is a bare numeric literal. An identifier is accepted whatever it is
|
|
18
|
+
* called — this gate does not verify that the identifier came from the table,
|
|
19
|
+
* because a gate that resolved imports would be a type-checker, and the thing
|
|
20
|
+
* it would catch (a local `const EXIT_BLOCK = 1` shadowing the import) is
|
|
21
|
+
* already visible in review as a duplicate declaration. Stated rather than left
|
|
22
|
+
* for a reader to assume the green covers it.
|
|
23
|
+
*
|
|
24
|
+
* WHY THE DISPATCHERS ARE OUT OF SCOPE. The `*-dispatcher.sh` trampolines carry
|
|
25
|
+
* `process.exit(0)` inside a `node -e` one-liner that reads a workspace root
|
|
26
|
+
* off stdin. They are shell, they cannot import, and their exit code is the
|
|
27
|
+
* one-liner's, never a verdict. The table's own header says so; this gate scans
|
|
28
|
+
* `.ts` only, which is the same boundary stated once more where it is enforced.
|
|
29
|
+
*
|
|
30
|
+
* THE TABLE FILE ITSELF is excluded, and that is the one exclusion: it is where
|
|
31
|
+
* the numerals are DEFINED, so a gate that refused them there would forbid the
|
|
32
|
+
* fix it exists to require.
|
|
33
|
+
*
|
|
34
|
+
* Exit codes: 0 clean · 1 a bare numeral reached `process.exit` · 2 the corpus
|
|
35
|
+
* could not be read.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import fs from 'node:fs';
|
|
39
|
+
import os from 'node:os';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
42
|
+
|
|
43
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
44
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
45
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
46
|
+
|
|
47
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
48
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
49
|
+
|
|
50
|
+
export const GATE = 'lint_exit_codes';
|
|
51
|
+
|
|
52
|
+
const HOOKS_REL = path.join('src', 'scripts', 'hooks');
|
|
53
|
+
|
|
54
|
+
/** The file that DEFINES the numerals. Excluding it is not a carve-out. */
|
|
55
|
+
const TABLE_BASENAME = 'exit_codes.ts';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The fixture import line, ASSEMBLED rather than written out.
|
|
59
|
+
*
|
|
60
|
+
* `prepack-check.mjs` scans source text for relative imports and cannot tell a
|
|
61
|
+
* string literal from a real one, so a fixture containing the words
|
|
62
|
+
* `from './exit_codes.js'` reads as this file importing a sibling that does not
|
|
63
|
+
* exist beside it — the table lives under `hooks/`. It reported exactly that
|
|
64
|
+
* before this was assembled. Splitting the specifier is the smallest change
|
|
65
|
+
* that keeps the fixture honest about what a passing hook file looks like
|
|
66
|
+
* without asserting an import this file does not make.
|
|
67
|
+
*/
|
|
68
|
+
const FIXTURE_IMPORT = (name: string): string => `import { ${name} } from '.` + `/${TABLE_BASENAME.replace('.ts', '.js')}';\n`;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `process.exit(<digits>)`, with optional whitespace.
|
|
72
|
+
*
|
|
73
|
+
* Deliberately anchored on `process.exit` rather than on any `exit(`: a
|
|
74
|
+
* concern's own helper named `exit` is not the process boundary, and widening
|
|
75
|
+
* the pattern to catch it would red on ordinary code.
|
|
76
|
+
*/
|
|
77
|
+
const BARE_EXIT = /process\s*\.\s*exit\s*\(\s*(-?\d+)\s*\)/g;
|
|
78
|
+
|
|
79
|
+
export interface ExitFinding {
|
|
80
|
+
readonly file: string;
|
|
81
|
+
readonly line: number;
|
|
82
|
+
readonly code: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Findings in one file's text. Pure, so a test reads the decision. */
|
|
86
|
+
export function findBareExits(rel: string, text: string): ExitFinding[] {
|
|
87
|
+
const out: ExitFinding[] = [];
|
|
88
|
+
const lines = text.split('\n');
|
|
89
|
+
for (let i = 0; i < lines.length; i++) {
|
|
90
|
+
const line = lines[i] as string;
|
|
91
|
+
// A commented example is prose about the rule, not an exit.
|
|
92
|
+
if (/^\s*(\*|\/\/)/.test(line)) continue;
|
|
93
|
+
BARE_EXIT.lastIndex = 0;
|
|
94
|
+
let m: RegExpExecArray | null;
|
|
95
|
+
while ((m = BARE_EXIT.exec(line)) !== null) {
|
|
96
|
+
out.push({ file: rel, line: i + 1, code: m[1] as string });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hookFiles(root: string): string[] {
|
|
103
|
+
const dir = path.join(root, HOOKS_REL);
|
|
104
|
+
let entries: string[];
|
|
105
|
+
try {
|
|
106
|
+
entries = fs.readdirSync(dir);
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
return entries
|
|
111
|
+
.filter((f) => f.endsWith('.ts') && f !== TABLE_BASENAME)
|
|
112
|
+
.map((f) => path.join(HOOKS_REL, f))
|
|
113
|
+
.sort();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
117
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
118
|
+
const i = argv.indexOf('--root');
|
|
119
|
+
if (i !== -1 && (argv[i + 1] === undefined || argv[i + 1]?.startsWith('-') === true)) {
|
|
120
|
+
process.stderr.write(`${GATE}: \`--root\` needs a directory.\n`);
|
|
121
|
+
return 2;
|
|
122
|
+
}
|
|
123
|
+
const root = i === -1 ? ROOT : (argv[i + 1] as string);
|
|
124
|
+
|
|
125
|
+
const files = hookFiles(root);
|
|
126
|
+
const findings: ExitFinding[] = [];
|
|
127
|
+
// A file the gate cannot READ is not a file it cleared. The first version
|
|
128
|
+
// of this loop skipped it and then counted it complete, which is a false
|
|
129
|
+
// green with a number behind it — the exact shape `assertScanned` exists to
|
|
130
|
+
// refuse one level up, reintroduced inside the loop. Reported by an
|
|
131
|
+
// independent review before this landed.
|
|
132
|
+
const unreadable: string[] = [];
|
|
133
|
+
for (const rel of files) {
|
|
134
|
+
let text: string;
|
|
135
|
+
try {
|
|
136
|
+
text = fs.readFileSync(path.join(root, rel), 'utf-8');
|
|
137
|
+
} catch {
|
|
138
|
+
unreadable.push(rel);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
findings.push(...findBareExits(rel, text));
|
|
142
|
+
}
|
|
143
|
+
if (unreadable.length > 0) {
|
|
144
|
+
for (const rel of unreadable) {
|
|
145
|
+
process.stderr.write(`❌ ${GATE}: cannot read ${rel} — the corpus is incomplete.\n`);
|
|
146
|
+
}
|
|
147
|
+
return 2;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Before the verdict: a corpus of zero is a gate that read nothing, and a
|
|
151
|
+
// green over it says the tree is clean when nobody looked.
|
|
152
|
+
try {
|
|
153
|
+
reportScanned({ gate: GATE, scanned: files.length, units: 'hook file(s)', roots: [HOOKS_REL] });
|
|
154
|
+
} catch (exc) {
|
|
155
|
+
process.stderr.write(`❌ ${String((exc as Error).message)}\n`);
|
|
156
|
+
return 2;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const ledger = new GateLedger(GATE);
|
|
160
|
+
ledger.plan(files);
|
|
161
|
+
const failed = new Set(findings.map((f) => f.file));
|
|
162
|
+
for (const f of findings) {
|
|
163
|
+
ledger.fail(f.file, `bare exit numeral ${f.code} at line ${String(f.line)}`);
|
|
164
|
+
}
|
|
165
|
+
for (const rel of files) if (!failed.has(rel)) ledger.complete(rel);
|
|
166
|
+
ledger.report();
|
|
167
|
+
|
|
168
|
+
if (findings.length > 0) {
|
|
169
|
+
for (const f of findings) {
|
|
170
|
+
process.stdout.write(
|
|
171
|
+
`❌ ${GATE}: ${f.file}:${String(f.line)} exits with the bare numeral ` +
|
|
172
|
+
`\`${f.code}\`. Import the name from \`hooks/exit_codes.ts\` — the numbers ` +
|
|
173
|
+
'mean opposite things in this tree and on the host, and a numeral records ' +
|
|
174
|
+
'neither.\n',
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
`✅ ${GATE}: ${String(files.length)} hook file(s), no bare exit numeral.\n`,
|
|
181
|
+
);
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function selfTest(): number {
|
|
186
|
+
const plant = (name: string, body: string): string => {
|
|
187
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `exit-codes-${name}-`));
|
|
188
|
+
fs.mkdirSync(path.join(dir, HOOKS_REL), { recursive: true });
|
|
189
|
+
fs.writeFileSync(path.join(dir, HOOKS_REL, 'probe_hook.ts'), body, 'utf-8');
|
|
190
|
+
// The table itself, so the exclusion is exercised rather than assumed.
|
|
191
|
+
fs.writeFileSync(
|
|
192
|
+
path.join(dir, HOOKS_REL, TABLE_BASENAME),
|
|
193
|
+
'export const EXIT_ALLOW = 0;\nprocess.exit(0);\n',
|
|
194
|
+
'utf-8',
|
|
195
|
+
);
|
|
196
|
+
return dir;
|
|
197
|
+
};
|
|
198
|
+
const run = (root: string): number =>
|
|
199
|
+
runGateCli(ROOT, path.join('src', 'scripts', 'lint_exit_codes.ts'), ['--root', root], ROOT);
|
|
200
|
+
|
|
201
|
+
return runSelfTest({
|
|
202
|
+
gate: GATE,
|
|
203
|
+
minCases: 6,
|
|
204
|
+
minRejectCases: 4,
|
|
205
|
+
cases: [
|
|
206
|
+
{
|
|
207
|
+
name: 'an empty corpus is refused, not certified green',
|
|
208
|
+
expect: 'reject',
|
|
209
|
+
run: () => run(fs.mkdtempSync(path.join(os.tmpdir(), 'exit-codes-empty-'))),
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'a bare deny numeral fails',
|
|
213
|
+
expect: 'reject',
|
|
214
|
+
run: () => run(plant('block', 'process.exit(1);\n')),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'a bare allow numeral fails too — 0 is the one people think is harmless',
|
|
218
|
+
expect: 'reject',
|
|
219
|
+
run: () => run(plant('allow', 'process.exit(0);\n')),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'whitespace inside the call does not hide it',
|
|
223
|
+
expect: 'reject',
|
|
224
|
+
run: () => run(plant('spaced', 'process . exit ( 2 );\n')),
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'an imported name passes',
|
|
228
|
+
expect: 'accept',
|
|
229
|
+
run: () =>
|
|
230
|
+
run(
|
|
231
|
+
plant(
|
|
232
|
+
'named',
|
|
233
|
+
`${FIXTURE_IMPORT('EXIT_ALLOW')}process.exit(EXIT_ALLOW);\n`,
|
|
234
|
+
),
|
|
235
|
+
),
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
// The table defines the numerals; refusing them there would
|
|
239
|
+
// forbid the fix this gate exists to require. Exercised, not
|
|
240
|
+
// assumed — the plant writes a bare exit into it every time.
|
|
241
|
+
name: 'the table file is excluded even though it carries a numeral',
|
|
242
|
+
expect: 'accept',
|
|
243
|
+
run: () =>
|
|
244
|
+
run(
|
|
245
|
+
plant(
|
|
246
|
+
'table',
|
|
247
|
+
`${FIXTURE_IMPORT('EXIT_WARN')}process.exit(EXIT_WARN);\n`,
|
|
248
|
+
),
|
|
249
|
+
),
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function _isCliEntry(): boolean {
|
|
256
|
+
const entry = process.argv[1];
|
|
257
|
+
return entry !== undefined && import.meta.url === pathToFileURL(entry).href;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (_isCliEntry()) {
|
|
261
|
+
process.exit(main());
|
|
262
|
+
}
|