@event4u/agent-config 16.2.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 +89 -237
- 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 +4 -4
- 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/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/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/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/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-system-capture/references/import-procedure.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +2 -2
- 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/monorepo-workspace/SKILL.md +1 -1
- 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 +2 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
- package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
- 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-IGD6ERX6.js → chunk-JQVYKIZA.js} +10 -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-7RQP4UDH.js → chunk-KR6EM23A.js} +235 -1455
- package/dist/cli-delegate/{chunk-LTYVIY76.js → chunk-LIYTLH6O.js} +1 -1
- package/dist/cli-delegate/{chunk-YJ72UIAK.js → chunk-NQ7WT43P.js} +1186 -244
- 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-7XGBX6ZH.js → chunk-RQNNW7IF.js} +1314 -560
- package/dist/cli-delegate/{chunk-4XFP2Y3E.js → chunk-S4UMZNES.js} +1 -1
- package/dist/cli-delegate/{chunk-6KM6ORRY.js → chunk-S4YWNRKO.js} +24 -8
- package/dist/cli-delegate/{chunk-FP36SWJD.js → chunk-TRI6DQZ6.js} +2781 -340
- 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 +1167 -234
- 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 +127 -75
- 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 +5357 -6656
- package/dist/hooks/dispatch.sha256 +1 -1
- 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 +2080 -1024
- 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 +31965 -27154
- 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 +6 -0
- package/dist/shared/skillRanking.js +74 -13
- package/docs/contracts/settings-classes.md +7 -5
- package/package.json +1 -1
- 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/apply.ts +48 -6
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
- package/src/config/agent-settings.template.yml +62 -22
- package/src/config/continuity-surface.json +7 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +96 -3
- package/src/config/gate-violation-baselines.json +17 -9
- package/src/config/hook-bundle-budget.json +25 -0
- package/src/config/hook-token-budget.json +6 -1
- package/src/config/host-instruction-limits.json +33 -0
- package/src/config/routing-coverage-seed.json +24 -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 +23 -71
- package/src/scripts/_cli/cmd_doctor_neighbours.ts +293 -0
- package/src/scripts/_dispatch.bash +16 -2
- package/src/scripts/_lib/agent_settings.ts +11 -0
- package/src/scripts/_lib/claude_settings_hooks.ts +10 -8
- 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/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_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/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/prompt_shape.ts +46 -0
- package/src/scripts/_lib/release_migration_gate.ts +19 -3
- package/src/scripts/_lib/reserved_name_sweep.ts +73 -0
- 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/skill_origin.ts +254 -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 +77 -2
- 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/audit_skill_overlap.ts +261 -0
- package/src/scripts/before_complete_hook.ts +243 -12
- package/src/scripts/bench_touched_file_quality.ts +238 -0
- package/src/scripts/check_enforcement_matrix.ts +377 -20
- package/src/scripts/check_estate_count.ts +130 -153
- package/src/scripts/check_finding_dispositions.ts +116 -4
- package/src/scripts/check_hook_bundle_composition.ts +370 -0
- package/src/scripts/check_kernel_edit_ratified.ts +49 -11
- package/src/scripts/check_memory.ts +7 -1
- package/src/scripts/check_release_highlights.ts +10 -0
- package/src/scripts/check_release_pr_shape.ts +38 -15
- package/src/scripts/check_standing_rule_delivery.ts +33 -13
- package/src/scripts/check_verify_expectation_delta.ts +521 -0
- 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 +33 -1
- package/src/scripts/condense.ts +14 -27
- package/src/scripts/estate_exemption_shape.ts +202 -0
- package/src/scripts/generate_host_cost_table.ts +1 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +150 -9
- package/src/scripts/hooks/block_config_weakening.ts +67 -2
- package/src/scripts/hooks/block_plumbing_writes.ts +29 -1
- package/src/scripts/hooks/code_graph_context_hook.ts +252 -23
- package/src/scripts/hooks/concern_failure_policy.ts +280 -0
- package/src/scripts/hooks/dispatch_hook.ts +52 -52
- package/src/scripts/hooks/exit_codes.ts +2 -1
- package/src/scripts/hooks/host_lowering.json +1 -1
- package/src/scripts/hooks/host_lowering.ts +19 -1
- package/src/scripts/hooks/one_question_per_ask_hook.ts +41 -25
- package/src/scripts/hooks/payload_stub.ts +10 -1
- package/src/scripts/hooks/rule_inject_hook.ts +757 -63
- package/src/scripts/hooks/table_fingerprint.ts +21 -0
- package/src/scripts/hooks/telemetry_usage_hook.ts +64 -8
- package/src/scripts/hooks/turn_end_gate_hook.ts +84 -0
- package/src/scripts/hooks_status.ts +112 -0
- package/src/scripts/injection_scan_hook.ts +7 -0
- package/src/scripts/install.ts +72 -185
- 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_hook_manifest.ts +53 -0
- package/src/scripts/lint_major_migration_sections.ts +156 -26
- package/src/scripts/lint_pack_boundaries.ts +63 -5
- package/src/scripts/lint_rule_law_section.ts +517 -0
- 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/measure_routing_signal.ts +54 -16
- package/src/scripts/measure_skill_ranker_baseline.ts +216 -8
- package/src/scripts/memory_learn_hook.ts +91 -6
- package/src/scripts/model_rule_injection.ts +52 -5
- package/src/scripts/project_thin_rules.ts +34 -327
- package/src/scripts/report_evidence_temperature.ts +657 -0
- package/src/scripts/report_installed_rule_links.ts +157 -0
- package/src/scripts/report_label_agreement.ts +233 -0
- package/src/scripts/report_obligation_mechanism.ts +230 -0
- package/src/scripts/report_single_token_triggers.ts +277 -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/rule_activation_census.ts +5 -2
- package/src/scripts/rule_trigger_eval.ts +9 -9
- package/src/scripts/schemas/review-findings.schema.json +3 -2
- package/src/scripts/schemas/rule.schema.json +5 -0
- package/src/scripts/self_review_gate.ts +62 -7
- package/src/scripts/skill_tools/score_skill_relevance.ts +290 -25
- 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 -5
- package/src/server/schemas/settings.ts +10 -0
- package/src/shared/skillRanking.ts +117 -11
- package/dist/cli-delegate/chunk-JNMVLWLR.js +0 -4457
- package/dist/cli-delegate/chunk-YDPILCOL.js +0 -152
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape scan a neighbour skill body passes before any of it may be indexed.
|
|
3
|
+
*
|
|
4
|
+
* SCAN BEFORE INJECT. A foreign `SKILL.md` is a file another package put on
|
|
5
|
+
* disk, and the ranker reaches it through the catalogue union. Nothing about
|
|
6
|
+
* being in `~/.claude/skills` makes its prose safe to fold into a term index
|
|
7
|
+
* whose output names skills to a model — the body is untrusted content in the
|
|
8
|
+
* sense `untrusted-input-defense` means it, and the floor here is the same one
|
|
9
|
+
* that rule states: it is data until something has looked at it.
|
|
10
|
+
*
|
|
11
|
+
* THE SHAPES ARE THE ONES THIS SUITE ALREADY SCANS ITS OWN CORPUS WITH. Four
|
|
12
|
+
* `security_lint` checks, reused as functions rather than re-implemented
|
|
13
|
+
* (decision D3 of the lane): instruction smuggling, hidden Unicode, mixed-script
|
|
14
|
+
* confusables and dangerous frontmatter. Re-declaring their patterns here would
|
|
15
|
+
* create a second copy to drift against the first, and the first is the one CI
|
|
16
|
+
* keeps honest.
|
|
17
|
+
*
|
|
18
|
+
* ANY FINDING REFUSES THE BODY — severity is NOT the discriminator. In the
|
|
19
|
+
* corpus linters, `HIGH` fails a build and `MED` warns, because the corpus is
|
|
20
|
+
* this package's own prose and a warning there is a maintainer's to weigh. A
|
|
21
|
+
* neighbour body is nobody's to weigh at rank time, and the cost of refusing is
|
|
22
|
+
* known and small: the skill still RANKS, by its name, with the finding kind
|
|
23
|
+
* printed. Risk-register row 4 is exactly this trade and resolves the same way —
|
|
24
|
+
* a shell snippet can be an installer skill's whole point, so the skill is never
|
|
25
|
+
* hidden, only un-indexed.
|
|
26
|
+
*
|
|
27
|
+
* WHY THIS IS NOT IMPORTED BY THE RANKER. It pulls four linter modules and,
|
|
28
|
+
* through one of them, `node:child_process`. The ranker runs inside the shared
|
|
29
|
+
* hook bundle on `user_prompt_submit`, where every byte is paid by every concern
|
|
30
|
+
* on every dispatch and `check_hook_bundle_composition` holds the ceiling. So
|
|
31
|
+
* the scan runs in the census CLI and its verdict reaches the ranker as a cached
|
|
32
|
+
* record — see `neighbour_census.scanVerdict`, where a missing record is a
|
|
33
|
+
* refusal rather than a pass.
|
|
34
|
+
*/
|
|
35
|
+
import * as fs from 'node:fs';
|
|
36
|
+
import * as path from 'node:path';
|
|
37
|
+
|
|
38
|
+
import * as sl from './security_lint.js';
|
|
39
|
+
import { fileDigest, type ScanCache, type ScanRecord } from './skill_origin.js';
|
|
40
|
+
import { _scan as scanSmuggling } from '../lint_instruction_smuggling.js';
|
|
41
|
+
import { _scan as scanHiddenUnicode } from '../lint_hidden_unicode.js';
|
|
42
|
+
import { _scan as scanConfusables } from '../lint_confusables.js';
|
|
43
|
+
import { _scan as scanFrontmatter } from '../lint_skill_frontmatter_safety.js';
|
|
44
|
+
|
|
45
|
+
/** The shape checks, in the order their findings are reported. */
|
|
46
|
+
const SHAPES: ReadonlyArray<readonly [string, (sf: sl.ScannedFile) => sl.Finding[]]> = [
|
|
47
|
+
['instruction-smuggling', scanSmuggling],
|
|
48
|
+
['dangerous-frontmatter', scanFrontmatter],
|
|
49
|
+
['hidden-unicode', scanHiddenUnicode],
|
|
50
|
+
['mixed-script-confusable', scanConfusables],
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export interface ScanResult {
|
|
54
|
+
/** `null` when every shape passed; otherwise the first finding's check id. */
|
|
55
|
+
kind: string | null;
|
|
56
|
+
/** Every finding, for a report that wants more than the first kind. */
|
|
57
|
+
findings: sl.Finding[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Scan one `SKILL.md`.
|
|
62
|
+
*
|
|
63
|
+
* An unreadable file is a refusal, not a pass (`unreadable`): a body nobody
|
|
64
|
+
* could open is in exactly the state this gate exists to keep out of the index,
|
|
65
|
+
* and returning `kind: null` for it would make an IO error look like a clean
|
|
66
|
+
* scan. A THROWING check is reported under its own id rather than swallowed, for
|
|
67
|
+
* the same reason.
|
|
68
|
+
*/
|
|
69
|
+
export function scanNeighbourBody(skillMdPath: string): ScanResult {
|
|
70
|
+
let sf: sl.ScannedFile;
|
|
71
|
+
try {
|
|
72
|
+
sf = sl.scan_file(skillMdPath);
|
|
73
|
+
} catch {
|
|
74
|
+
return { kind: 'unreadable', findings: [] };
|
|
75
|
+
}
|
|
76
|
+
const findings: sl.Finding[] = [];
|
|
77
|
+
for (const [id, scan] of SHAPES) {
|
|
78
|
+
try {
|
|
79
|
+
findings.push(...scan(sf));
|
|
80
|
+
} catch {
|
|
81
|
+
findings.push(
|
|
82
|
+
new sl.Finding(sf.rel, 0, id, 'HIGH', 'shape check threw on this body', 1.0),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { kind: findings.length === 0 ? null : (findings[0] as sl.Finding).check, findings };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Record what the scan found, for the ranker to read on the hook path.
|
|
91
|
+
*
|
|
92
|
+
* The digest is the FILE's, not the finding's: the ranker re-digests the body it
|
|
93
|
+
* is about to index and compares, so a body edited after its scan reads as
|
|
94
|
+
* `digest-changed` and goes back to name-only ranking until the census runs
|
|
95
|
+
* again. That compare is the whole mechanism behind "a changed digest rescans
|
|
96
|
+
* first" — without it a one-time pass would keep clearing a file forever.
|
|
97
|
+
*
|
|
98
|
+
* A write failure is reported to the caller rather than thrown: the scan is
|
|
99
|
+
* correct either way, and a read-only report directory must not fail a census.
|
|
100
|
+
*/
|
|
101
|
+
export function writeScanCache(file: string, entries: readonly ScanRecord[]): boolean {
|
|
102
|
+
const payload: ScanCache = {
|
|
103
|
+
schema_version: 1,
|
|
104
|
+
entries: [...entries].sort((a, b) => (a.qualified < b.qualified ? -1 : 1)),
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
108
|
+
fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A `ScanRecord` for one file, digest included. */
|
|
116
|
+
export function scanRecordFor(qualified: string, skillMdPath: string): ScanRecord {
|
|
117
|
+
return {
|
|
118
|
+
qualified,
|
|
119
|
+
digest: fileDigest(skillMdPath) ?? '',
|
|
120
|
+
unscanned: scanNeighbourBody(skillMdPath).kind,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The foreign-MCP-tool-name store: one writer on the post-tool hook path, one
|
|
3
|
+
* reader in the neighbour census.
|
|
4
|
+
*
|
|
5
|
+
* WHY A MODULE OF ITS OWN, AND NOT A CONSTANT IN EITHER SIDE. The writer lives
|
|
6
|
+
* in `hooks/telemetry_usage_hook.ts`, which is inlined into the shared hook
|
|
7
|
+
* bundle `check_hook_bundle_composition` caps; the reader lives in
|
|
8
|
+
* `_lib/neighbour_census.ts`, which is not. Putting the pair here lets esbuild
|
|
9
|
+
* tree-shake {@link toolsUsedByServer} out of the hook bundle while both sides
|
|
10
|
+
* still name one path — a constant copied into two files is a store that
|
|
11
|
+
* silently splits in two the first time one copy is edited.
|
|
12
|
+
*
|
|
13
|
+
* WHAT IS RECORDED, AND WHAT IS NOT. One map of `mcp__<server>__<tool>` to the
|
|
14
|
+
* ISO date it was last seen. No arguments, no responses, no session id, no
|
|
15
|
+
* project path beyond the file's own location. The names recorded are names the
|
|
16
|
+
* consumer's own `.mcp.json` already lists, and the file is written under
|
|
17
|
+
* `agents/runtime/`, which is gitignored and local-only — nothing here is sent
|
|
18
|
+
* anywhere, and no transport reads it.
|
|
19
|
+
*
|
|
20
|
+
* NOT GATED ON THE TELEMETRY OPT-IN, DELIBERATELY. `telemetry_usage_hook`'s
|
|
21
|
+
* Class-A capture is default-off because it feeds an outbound spool. This store
|
|
22
|
+
* feeds `doctor neighbours`, a local report the consumer runs on their own tree.
|
|
23
|
+
* Gating it on an org telemetry switch would make the census print `0` on every
|
|
24
|
+
* install that never enabled one — a number that looks like a measurement and is
|
|
25
|
+
* an instrumentation artifact, which is the exact failure the usage hook's own
|
|
26
|
+
* header records from the collector it replaced.
|
|
27
|
+
*
|
|
28
|
+
* GROWTH, DECLARED (scale-discipline R-A7). The store is a map keyed by tool
|
|
29
|
+
* NAME, not an append log: one entry per distinct `mcp__…` name the consumer
|
|
30
|
+
* has ever called, each rewritten in place at most once per day. The bound is
|
|
31
|
+
* therefore the size of the neighbour's tool surface — tens of entries, not a
|
|
32
|
+
* series — so it needs no TTL job. The reader applies the window; entries older
|
|
33
|
+
* than it stay on disk and stop counting, which is why a stale entry is inert
|
|
34
|
+
* rather than wrong.
|
|
35
|
+
*/
|
|
36
|
+
import * as fs from 'node:fs';
|
|
37
|
+
import * as path from 'node:path';
|
|
38
|
+
|
|
39
|
+
/** Host-side prefix of every MCP tool name: `mcp__<server>__<tool>`. */
|
|
40
|
+
export const MCP_TOOL_PREFIX = 'mcp__';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The store, relative to the project root. Gitignored; see the header.
|
|
44
|
+
*
|
|
45
|
+
* A literal with forward slashes rather than `path.join`, and the reason is
|
|
46
|
+
* measured rather than stylistic: `path.join` here is a top-level call, so the
|
|
47
|
+
* `node:path` namespace import survives into the hook bundle even though every
|
|
48
|
+
* function in this module that needs it is tree-shaken out of that bundle.
|
|
49
|
+
* `path.join(root, REL)` at each call site normalises the separator on Windows,
|
|
50
|
+
* so the only thing lost is the authoring convention.
|
|
51
|
+
*/
|
|
52
|
+
export const FOREIGN_TOOL_USE_REL = 'agents/runtime/neighbour-tool-use.json';
|
|
53
|
+
|
|
54
|
+
/** The census window. 30 days, matching the step that asked for the number. */
|
|
55
|
+
export const TOOL_USE_WINDOW_DAYS = 30;
|
|
56
|
+
|
|
57
|
+
/** The filename the writer's root is the directory of. */
|
|
58
|
+
const SETTINGS_FILENAME = '.agent-settings.yml';
|
|
59
|
+
|
|
60
|
+
/** `YYYY-MM-DD` in UTC — the granularity the store keys on. */
|
|
61
|
+
export function usageDay(now: Date): string {
|
|
62
|
+
return now.toISOString().slice(0, 10);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const DAY_RE = /^\d{4}-\d{2}-\d{2}$/u;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Is this store value a day the window comparison can be trusted on?
|
|
69
|
+
*
|
|
70
|
+
* The comparison is lexicographic, which is exactly right for `YYYY-MM-DD` and
|
|
71
|
+
* quietly wrong for anything else: `'TODO'`, `'unknown'` and `'hand edited'`
|
|
72
|
+
* all sort ABOVE a real cutoff, so without this check a hand-edited or
|
|
73
|
+
* half-written store inflates the count instead of being ignored. The reader's
|
|
74
|
+
* promise is that a malformed store yields nothing, and a malformed VALUE is
|
|
75
|
+
* the likelier half of that.
|
|
76
|
+
*/
|
|
77
|
+
export function isUsageDay(value: unknown): value is string {
|
|
78
|
+
return typeof value === 'string' && DAY_RE.test(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The `.mcp.json` key as it appears inside a tool name.
|
|
83
|
+
*
|
|
84
|
+
* Hosts sanitise the key before embedding it — a server registered as
|
|
85
|
+
* `claude.ai Claude Docs` arrives as `mcp__claude_ai_Claude_Docs__…`. Normalising
|
|
86
|
+
* BOTH sides the same way is what keeps a matched count from quietly being a
|
|
87
|
+
* zero: an exact-key match would report `0` for every server whose name carries
|
|
88
|
+
* a dot or a space, which reads as "never used" rather than as "never matched".
|
|
89
|
+
*/
|
|
90
|
+
export function serverSegment(key: string): string {
|
|
91
|
+
return key.replace(/[^A-Za-z0-9_-]/gu, '_');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Does `tool` belong to the server whose sanitised key is `segment`?
|
|
96
|
+
*
|
|
97
|
+
* Driven by the KNOWN segment rather than by parsing the tool name, and that
|
|
98
|
+
* direction is the whole correctness argument. Parsing — split at the first
|
|
99
|
+
* `__` after the prefix — is ambiguous exactly where {@link serverSegment} is
|
|
100
|
+
* lossy: `Acme Inc. Tools` sanitises to `Acme_Inc__Tools`, whose own name
|
|
101
|
+
* contains the separator, so a parser reads the server as `Acme_Inc` and the
|
|
102
|
+
* census reports `0` for a server in daily use. That is the same never-matched
|
|
103
|
+
* zero `serverSegment` exists to prevent, reintroduced one layer down. Matching
|
|
104
|
+
* against the keys `.mcp.json` actually lists cannot be ambiguous, because the
|
|
105
|
+
* candidate set is finite and known.
|
|
106
|
+
*/
|
|
107
|
+
export function toolBelongsTo(tool: string, segment: string): boolean {
|
|
108
|
+
if (!tool.startsWith(MCP_TOOL_PREFIX)) return false;
|
|
109
|
+
const rest = tool.slice(MCP_TOOL_PREFIX.length);
|
|
110
|
+
return rest === segment || rest.startsWith(`${segment}__`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The directory the store lives under, from any directory inside the project.
|
|
115
|
+
*
|
|
116
|
+
* The WRITER roots the store at the directory holding `.agent-settings.yml`
|
|
117
|
+
* (`telemetry_usage_hook.readSettingsFor`), because a session started in a
|
|
118
|
+
* subdirectory would otherwise scatter one store per directory it happened to
|
|
119
|
+
* start in. The reader has to resolve the same way or it looks in the wrong
|
|
120
|
+
* place: a monorepo with settings at the repo root and `.mcp.json` under
|
|
121
|
+
* `packages/web` would report `0` for a server in daily use. The two walks are
|
|
122
|
+
* pinned equal by a fixture rather than by assertion.
|
|
123
|
+
*
|
|
124
|
+
* No settings file on any ancestor → `start`, matching the writer's own
|
|
125
|
+
* fallback.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveStoreRoot(start: string): string {
|
|
128
|
+
let dir = path.resolve(start);
|
|
129
|
+
for (;;) {
|
|
130
|
+
try {
|
|
131
|
+
if (fs.statSync(path.join(dir, SETTINGS_FILENAME)).isFile()) return dir;
|
|
132
|
+
} catch {
|
|
133
|
+
// Not here — keep walking.
|
|
134
|
+
}
|
|
135
|
+
const parent = path.dirname(dir);
|
|
136
|
+
if (parent === dir) return path.resolve(start);
|
|
137
|
+
dir = parent;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Distinct tools seen per server inside the window, keyed by server segment.
|
|
143
|
+
*
|
|
144
|
+
* `segments` is the closed candidate set — the sanitised `.mcp.json` keys. Every
|
|
145
|
+
* one of them appears in the result, so a server with no sightings reads `0`
|
|
146
|
+
* rather than being absent. Longest segment wins where one is a prefix of
|
|
147
|
+
* another, so a tool is counted once.
|
|
148
|
+
*
|
|
149
|
+
* A missing, unreadable or malformed store yields all-zero — true of what was
|
|
150
|
+
* observed. It does not mean the server was unused, and the census says so next
|
|
151
|
+
* to the number.
|
|
152
|
+
*/
|
|
153
|
+
export function toolsUsedByServer(
|
|
154
|
+
projectRoot: string,
|
|
155
|
+
now: Date,
|
|
156
|
+
segments: Iterable<string>,
|
|
157
|
+
): Map<string, number> {
|
|
158
|
+
const known = [...new Set(segments)].sort((a, b) => b.length - a.length);
|
|
159
|
+
const counts = new Map<string, number>(known.map((s) => [s, 0]));
|
|
160
|
+
if (known.length === 0) return counts;
|
|
161
|
+
|
|
162
|
+
let decoded: unknown;
|
|
163
|
+
try {
|
|
164
|
+
const root = resolveStoreRoot(projectRoot);
|
|
165
|
+
decoded = JSON.parse(fs.readFileSync(path.join(root, FOREIGN_TOOL_USE_REL), 'utf-8'));
|
|
166
|
+
} catch {
|
|
167
|
+
return counts;
|
|
168
|
+
}
|
|
169
|
+
if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) return counts;
|
|
170
|
+
|
|
171
|
+
const today = usageDay(now);
|
|
172
|
+
// Inclusive of both ends, so the span is WINDOW days and not WINDOW+1: a
|
|
173
|
+
// floor computed at `now - 30d` would count 31 distinct days under a field
|
|
174
|
+
// named `tools_used_30d`.
|
|
175
|
+
const floor = usageDay(new Date(now.getTime() - (TOOL_USE_WINDOW_DAYS - 1) * 86_400_000));
|
|
176
|
+
|
|
177
|
+
for (const [tool, seen] of Object.entries(decoded as Record<string, unknown>)) {
|
|
178
|
+
// A future date is as much a sign of a bad write as a malformed one,
|
|
179
|
+
// and without the upper bound a single `9999-01-01` counts forever.
|
|
180
|
+
if (!isUsageDay(seen) || seen < floor || seen > today) continue;
|
|
181
|
+
for (const segment of known) {
|
|
182
|
+
if (toolBelongsTo(tool, segment)) {
|
|
183
|
+
counts.set(segment, (counts.get(segment) ?? 0) + 1);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return counts;
|
|
189
|
+
}
|
|
@@ -41,6 +41,50 @@
|
|
|
41
41
|
* over-filtering would silently stop pinning real prompts, which is worse.
|
|
42
42
|
* Measured across 1 540 user-role entries: 509 open at character zero with a
|
|
43
43
|
* marker, and in 0 of them does human text survive wrapper removal.
|
|
44
|
+
*
|
|
45
|
+
* `<agent-message` added 2026-10-03, by the same under-filtering it warns
|
|
46
|
+
* about: a subagent hand-back reaches this slot as `<agent-message from="…">`,
|
|
47
|
+
* and 12 of the 153 hook-sourced user records in this project's capture open
|
|
48
|
+
* with it. It pinned a German session to `en`, after which the turn-end gate
|
|
49
|
+
* refused each German reply — a reply language derived from a frame nobody
|
|
50
|
+
* typed. Its literal stops before the `>` because the element carries an
|
|
51
|
+
* attribute; the anchor at character zero is unchanged.
|
|
52
|
+
*
|
|
53
|
+
* **The fourth shape has NO envelope tag, and it is the exception this file's
|
|
54
|
+
* own "structural, not linguistic" rule has to name rather than quietly break.**
|
|
55
|
+
* The host resumes a rate-limited session with a plain sentence —
|
|
56
|
+
* `Your claude.ai usage limit has reset. Continue the task you were working
|
|
57
|
+
* on…` — carrying no element, no bracket and no stamp. 5 of the 153
|
|
58
|
+
* hook-sourced user records in this project's capture open with it. Measured
|
|
59
|
+
* 2026-10-03: it pinned a German session to `en` with `prompt_chars: 148`,
|
|
60
|
+
* which is that sentence's exact length, and every German reply for the rest
|
|
61
|
+
* of the run was then refused by the turn-end gate as wrong-language.
|
|
62
|
+
*
|
|
63
|
+
* Matching prose is what the paragraph above warns against, so what makes this
|
|
64
|
+
* one admissible is stated rather than assumed: it is a FIXED host string, not
|
|
65
|
+
* a linguistic pattern, and the character-zero anchor does the same work a tag
|
|
66
|
+
* would — a human asking *why did I get "Your claude.ai usage limit has
|
|
67
|
+
* reset"* does not open with it. A test pins that direction. If the host ever
|
|
68
|
+
* reworded the sentence this guard would silently stop matching, which is the
|
|
69
|
+
* honest failure mode: it under-filters back to today's behaviour rather than
|
|
70
|
+
* misfiring on a real turn.
|
|
71
|
+
*
|
|
72
|
+
* It is deliberately NOT added to `machine_wake.ts`. There the same
|
|
73
|
+
* misclassification retains an authorization instead of mis-pinning a reply,
|
|
74
|
+
* and a prose match is too weak a warrant for that consequence — see the
|
|
75
|
+
* asymmetry note below, which this is the first real test of.
|
|
76
|
+
*
|
|
77
|
+
* **A SECOND VOCABULARY ANSWERS THIS SAME QUESTION AND THE TWO HAVE DRIFTED.**
|
|
78
|
+
* `_lib/machine_wake.ts`'s `humanTypedThisTurn` guards per-turn STATE on this
|
|
79
|
+
* slot (the git-authorization ledger, the suggestion latch) while this one
|
|
80
|
+
* guards the language pin. As of this change both know `<task-notification>`,
|
|
81
|
+
* `<system-reminder>` and `<agent-message`; only this one knows
|
|
82
|
+
* `[SYSTEM NOTIFICATION` and `<local-command-caveat>`. That asymmetry is NOT
|
|
83
|
+
* obviously a defect and was deliberately left: mistaking a real turn for a
|
|
84
|
+
* machine one costs a mis-pinned reply here and a RETAINED AUTHORIZATION
|
|
85
|
+
* there, so the two sides do not want the same error budget — and
|
|
86
|
+
* `<local-command-caveat>` in particular precedes a real typed prompt. Unify
|
|
87
|
+
* them only with that asymmetry answered, never as tidying.
|
|
44
88
|
*/
|
|
45
89
|
export function isSyntheticPrompt(prompt: string): boolean {
|
|
46
90
|
const head = prompt.trimStart().slice(0, 400);
|
|
@@ -49,6 +93,8 @@ export function isSyntheticPrompt(prompt: string): boolean {
|
|
|
49
93
|
head.startsWith("<task-notification>") ||
|
|
50
94
|
head.startsWith("<system-reminder>") ||
|
|
51
95
|
head.startsWith("<local-command-caveat>") ||
|
|
96
|
+
head.startsWith("<agent-message") ||
|
|
97
|
+
head.startsWith("Your claude.ai usage limit has reset") ||
|
|
52
98
|
/^\[SYSTEM NOTIFICATION\b/.test(head)
|
|
53
99
|
);
|
|
54
100
|
}
|
|
@@ -24,6 +24,7 @@ import * as path from 'node:path';
|
|
|
24
24
|
import { REPO_ROOT } from '../release_env.js';
|
|
25
25
|
import { die, run, type RunResult } from '../release_publication.js';
|
|
26
26
|
import {
|
|
27
|
+
BREAKING_INDEX_PATH,
|
|
27
28
|
MIGRATION_PATH as MAJOR_MIGRATION_PATH,
|
|
28
29
|
pendingMajorFinding,
|
|
29
30
|
} from '../lint_major_migration_sections.js';
|
|
@@ -148,8 +149,15 @@ export function assert_scheduled_deprecations_clear(
|
|
|
148
149
|
* real run will refuse and returns, because `--dry-run exits 0` is a contract
|
|
149
150
|
* this file's own tests assert.
|
|
150
151
|
*
|
|
152
|
+
* A major owes TWO records and this seam reads both: the migration section and
|
|
153
|
+
* the by-major row in `BREAKING_CHANGES.md`. Checking only the first at cut
|
|
154
|
+
* time while the lint checks both would let a cut create the exact state the
|
|
155
|
+
* lint then reds `main` over — the gate arriving one merge too late, which is
|
|
156
|
+
* the shape `pendingMajorFinding` exists to avoid in the first place.
|
|
157
|
+
*
|
|
151
158
|
* @param migrationReader Seam for the file read, so a test can drive the
|
|
152
159
|
* refusal without mutating a tracked file.
|
|
160
|
+
* @param indexReader The same seam for the breaking-changes index.
|
|
153
161
|
*/
|
|
154
162
|
export function assert_major_migration_section(
|
|
155
163
|
target: string,
|
|
@@ -157,18 +165,23 @@ export function assert_major_migration_section(
|
|
|
157
165
|
opts: { previewOnly?: boolean } = {},
|
|
158
166
|
migrationReader: () => string = () =>
|
|
159
167
|
fs.readFileSync(path.join(REPO_ROOT, MAJOR_MIGRATION_PATH), 'utf-8'),
|
|
168
|
+
indexReader: () => string = () =>
|
|
169
|
+
fs.readFileSync(path.join(REPO_ROOT, BREAKING_INDEX_PATH), 'utf-8'),
|
|
160
170
|
): void {
|
|
161
171
|
let migration: string;
|
|
172
|
+
let index: string;
|
|
162
173
|
try {
|
|
163
174
|
migration = migrationReader();
|
|
175
|
+
index = indexReader();
|
|
164
176
|
} catch (exc) {
|
|
165
177
|
die(
|
|
166
|
-
`refusing the ${target} cut: cannot read ${MAJOR_MIGRATION_PATH} ` +
|
|
178
|
+
`refusing the ${target} cut: cannot read ${MAJOR_MIGRATION_PATH} or ` +
|
|
179
|
+
`${BREAKING_INDEX_PATH} ` +
|
|
167
180
|
`(${exc instanceof Error ? exc.message : String(exc)}). That is an environment ` +
|
|
168
181
|
'failure, not a finding — fix the checkout, then re-run.',
|
|
169
182
|
);
|
|
170
183
|
}
|
|
171
|
-
const finding = pendingMajorFinding(target, changelog_entry, migration);
|
|
184
|
+
const finding = pendingMajorFinding(target, changelog_entry, migration, index);
|
|
172
185
|
if (finding === null) {
|
|
173
186
|
return;
|
|
174
187
|
}
|
|
@@ -180,5 +193,8 @@ export function assert_major_migration_section(
|
|
|
180
193
|
);
|
|
181
194
|
return;
|
|
182
195
|
}
|
|
183
|
-
die(
|
|
196
|
+
die(
|
|
197
|
+
`refusing the ${target} cut: ${MAJOR_MIGRATION_PATH} or ${BREAKING_INDEX_PATH} ` +
|
|
198
|
+
`does not name ${target}.`,
|
|
199
|
+
);
|
|
184
200
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reserved-name sweep over flat Claude Code command files.
|
|
3
|
+
*
|
|
4
|
+
* A flat command whose slug shadows a Claude Code built-in never ships on the
|
|
5
|
+
* claude-code anchor — the suite complements the host, it does not overlay its
|
|
6
|
+
* built-ins. The sweep that enforces this used to delete EVERY builtin-named
|
|
7
|
+
* `.md` in the anchor's `commands/` directory, including one another agent
|
|
8
|
+
* package or the consumer had put there. The consumer's own `/review` command
|
|
9
|
+
* disappeared at install time with no message, because the sweep's only
|
|
10
|
+
* question was the filename.
|
|
11
|
+
*
|
|
12
|
+
* The question it asks now is ownership: a file is removed when this package
|
|
13
|
+
* wrote it — the current deploy's own written set, or a path a previous
|
|
14
|
+
* install recorded in the installed-tools manifest. Anything else is FOREIGN:
|
|
15
|
+
* it stays on disk and is reported, so the consumer learns that a built-in
|
|
16
|
+
* name is shadowed by someone and can decide, rather than finding their file
|
|
17
|
+
* gone.
|
|
18
|
+
*
|
|
19
|
+
* Both halves of the sweep's original purpose survive: ours never ships under
|
|
20
|
+
* a reserved name, and the suite still never overlays a built-in with its own
|
|
21
|
+
* file. What it no longer does is enforce that on a file that is not its own.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
|
|
27
|
+
export interface ReservedSweepResult {
|
|
28
|
+
/** Slugs of OUR files removed because the name is a host built-in. */
|
|
29
|
+
reserved: string[];
|
|
30
|
+
/** Slugs left in place because nothing proves this package wrote them. */
|
|
31
|
+
foreign: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Remove our builtin-named flat commands from `commandsDir`; keep foreign ones.
|
|
36
|
+
*
|
|
37
|
+
* Both ownership sets are anchor-relative, exactly as the deploy inventory
|
|
38
|
+
* records them. `currentFiles` is this deploy's written set — the live half,
|
|
39
|
+
* and the only one that exists on a first install. `recordedFiles` is what a
|
|
40
|
+
* previous deploy recorded under the same anchor, so a builtin-named file of
|
|
41
|
+
* ours left behind by an older version is still reaped rather than becoming
|
|
42
|
+
* permanently unprovable. A removed entry is dropped from `currentFiles` so
|
|
43
|
+
* the inventory record stays consistent with the tree, exactly as before.
|
|
44
|
+
*/
|
|
45
|
+
export function sweepReservedNames(
|
|
46
|
+
commandsDir: string,
|
|
47
|
+
currentFiles: Set<string>,
|
|
48
|
+
isBuiltinName: (slug: string) => boolean,
|
|
49
|
+
recordedFiles: ReadonlySet<string>,
|
|
50
|
+
): ReservedSweepResult {
|
|
51
|
+
const reserved: string[] = [];
|
|
52
|
+
const foreign: string[] = [];
|
|
53
|
+
let entries: string[] = [];
|
|
54
|
+
try {
|
|
55
|
+
entries = fs.readdirSync(commandsDir).filter((f) => f.endsWith('.md'));
|
|
56
|
+
} catch {
|
|
57
|
+
return { reserved, foreign };
|
|
58
|
+
}
|
|
59
|
+
for (const fname of entries.sort()) {
|
|
60
|
+
const slug = fname.slice(0, -'.md'.length);
|
|
61
|
+
if (!isBuiltinName(slug)) continue;
|
|
62
|
+
const rel = `commands/${fname}`;
|
|
63
|
+
const ours = currentFiles.has(rel) || recordedFiles.has(rel);
|
|
64
|
+
if (!ours) {
|
|
65
|
+
foreign.push(slug);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
fs.rmSync(path.join(commandsDir, fname), { force: true });
|
|
69
|
+
currentFiles.delete(rel);
|
|
70
|
+
reserved.push(slug);
|
|
71
|
+
}
|
|
72
|
+
return { reserved, foreign };
|
|
73
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much of a release the self-review actually read, stated where the record
|
|
3
|
+
* is read.
|
|
4
|
+
*
|
|
5
|
+
* `self_review_gate` writes a `coverage` block — `{chunks, filesReviewed,
|
|
6
|
+
* filesTotal, unreviewed[]}` — into the findings artifact, `--ingest` carries it
|
|
7
|
+
* into the per-release ledger named by {@link LEDGER_DIR}, and from there NOBODY
|
|
8
|
+
* reads `filesReviewed` back. The 16.2.0 ledger records `filesReviewed: 65,
|
|
9
|
+
* filesTotal: 678`: the review saw under a tenth of the change. Every consumer
|
|
10
|
+
* of that record — the disposition gate's success line, the release-highlights
|
|
11
|
+
* gate, a human opening the file — saw output that said nothing about it.
|
|
12
|
+
*
|
|
13
|
+
* A finding count over 9.6 % of a diff and the same count over all of it are
|
|
14
|
+
* different claims, and the record spelled them identically.
|
|
15
|
+
*
|
|
16
|
+
* A LABEL, NEVER A FLOOR, and the distinction is the decision.
|
|
17
|
+
*
|
|
18
|
+
* Nothing here refuses. `coverageLabel` returns a string or `null`; no caller
|
|
19
|
+
* turns it into an exit code, and none may. Raising what a review reads costs
|
|
20
|
+
* one more paid model request per chunk per cut, which is a spend decision
|
|
21
|
+
* belonging to the owner. A gate that failed a cut on partial coverage would
|
|
22
|
+
* force that decision through the back door — the operator would either pay or
|
|
23
|
+
* be unable to release — which is exactly the move the roadmap's decision D1
|
|
24
|
+
* records as rejected.
|
|
25
|
+
*
|
|
26
|
+
* The honest cost of that choice, stated rather than left to be discovered: a
|
|
27
|
+
* release can ship with 9.6 % coverage and this module will only have said so.
|
|
28
|
+
* Saying so is the whole claim.
|
|
29
|
+
*
|
|
30
|
+
* It lives in its own module because two consumers read it — `check_finding_dispositions` on the ledger and
|
|
31
|
+
* `check_release_highlights` beside the Known-limitations field — and a label
|
|
32
|
+
* the two spell differently is a label a reader cannot compare across the two
|
|
33
|
+
* places they meet it. One definition is also the seam the unit tests use, so
|
|
34
|
+
* the arithmetic is checked without either CLI in the loop.
|
|
35
|
+
*/
|
|
36
|
+
import * as fs from 'node:fs';
|
|
37
|
+
import * as path from 'node:path';
|
|
38
|
+
|
|
39
|
+
/** Where the per-release ledgers live, relative to the repository root. */
|
|
40
|
+
export const LEDGER_DIR = path.join('agents', 'evidence', 'release-findings');
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The coverage block as the gate writes it. Every field optional: this type
|
|
44
|
+
* describes an artifact read back off disk, which may be from an older writer
|
|
45
|
+
* or hand-edited, so the reader's job is to decide whether it can answer rather
|
|
46
|
+
* than to assume it can.
|
|
47
|
+
*/
|
|
48
|
+
export interface ReviewCoverage {
|
|
49
|
+
readonly chunks?: unknown;
|
|
50
|
+
readonly filesReviewed?: unknown;
|
|
51
|
+
readonly filesTotal?: unknown;
|
|
52
|
+
readonly unreviewed?: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _finite(v: unknown): number | null {
|
|
56
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The one sentence both gates print, or `null` when there is nothing to say.
|
|
61
|
+
*
|
|
62
|
+
* `null` covers three cases deliberately collapsed into one, because they lead
|
|
63
|
+
* to the same output: full coverage (nothing to warn about), an absent block (an
|
|
64
|
+
* older ledger, written before the gate recorded coverage), and a malformed one
|
|
65
|
+
* (a string where a number belongs, a zero total). A reader who sees no label
|
|
66
|
+
* learns "this record makes no partial-coverage claim" — which is true of all
|
|
67
|
+
* three — and never a fabricated percentage derived from a field that was not
|
|
68
|
+
* a number.
|
|
69
|
+
*
|
|
70
|
+
* `filesTotal === 0` is `null` rather than `0 of 0 (partial)`: a change set with
|
|
71
|
+
* no files was fully read by any definition, and dividing by it says nothing.
|
|
72
|
+
*/
|
|
73
|
+
export function coverageLabel(coverage: unknown): string | null {
|
|
74
|
+
if (coverage === null || typeof coverage !== 'object') {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const c = coverage as ReviewCoverage;
|
|
78
|
+
const reviewed = _finite(c.filesReviewed);
|
|
79
|
+
const total = _finite(c.filesTotal);
|
|
80
|
+
if (reviewed === null || total === null || total <= 0 || reviewed >= total) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return `self-review read ${String(reviewed)} of ${String(total)} changed files (partial)`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The `coverage` block of a committed ledger, or `undefined`.
|
|
88
|
+
*
|
|
89
|
+
* Never throws. A missing ledger is the normal state of an in-flight release and
|
|
90
|
+
* a corrupt one is already the business of the gate that parses it properly —
|
|
91
|
+
* a label is not the place to discover either, and a reader that threw here
|
|
92
|
+
* would turn an informational line into a crash on a release branch.
|
|
93
|
+
*/
|
|
94
|
+
export function readLedgerCoverage(root: string, release: string): unknown {
|
|
95
|
+
const file = path.join(root, LEDGER_DIR, `${release}.json`);
|
|
96
|
+
try {
|
|
97
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
98
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return (parsed as Record<string, unknown>)['coverage'];
|
|
102
|
+
} catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `coverageLabel` over the committed ledger for `release`. */
|
|
108
|
+
export function ledgerCoverageLabel(root: string, release: string): string | null {
|
|
109
|
+
return coverageLabel(readLedgerCoverage(root, release));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The same label as a Known-limitations line, for the release-highlights gate.
|
|
114
|
+
*
|
|
115
|
+
* It carries the label plus the sentence that keeps a reader from mistaking it
|
|
116
|
+
* for a refusal, because the field it prints beside is one the gate CAN refuse
|
|
117
|
+
* on. Without that sentence the next person to read the output reasonably
|
|
118
|
+
* concludes the gate is about to start failing cuts on coverage, and the cheapest
|
|
119
|
+
* way to make the message go away becomes raising the ceiling — which is the
|
|
120
|
+
* owner's spend decision, taken by whoever happened to read the line.
|
|
121
|
+
*
|
|
122
|
+
* Returns `null` when there is nothing to say, so the caller prints nothing
|
|
123
|
+
* rather than a reassuring sentence about a number it does not have.
|
|
124
|
+
*/
|
|
125
|
+
export function coverageLimitationNote(label: string | null): string | null {
|
|
126
|
+
if (label === null) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return (
|
|
130
|
+
`ℹ️ ${label} — a limitation of the RECORD, not a verdict on the release. ` +
|
|
131
|
+
'Stated, never enforced:\n what the review reads is a spend decision and this ' +
|
|
132
|
+
'gate does not take it.'
|
|
133
|
+
);
|
|
134
|
+
}
|