@event4u/agent-config 16.1.0 → 16.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +98 -257
- package/README.md +14 -12
- package/dist/agent-src/commands/analyze/inbox.md +1 -1
- package/dist/agent-src/commands/analyze/roadmap-repos.md +1 -1
- package/dist/agent-src/commands/bug/fix.md +17 -2
- package/dist/agent-src/commands/feature/plan.md +1 -1
- package/dist/agent-src/commands/feature/roadmap.md +1 -1
- package/dist/agent-src/commands/fix/ci.md +1 -1
- package/dist/agent-src/commands/implement-ticket.md +1 -1
- package/dist/agent-src/commands/jira-ticket.md +1 -1
- package/dist/agent-src/commands/roadmap/create.md +1 -1
- package/dist/agent-src/commands/roadmap/materialize.md +1 -1
- package/dist/agent-src/commands/roadmap/process-full.md +5 -5
- package/dist/agent-src/commands/roadmap/resolve-blockers.md +361 -0
- package/dist/agent-src/commands/roadmap/triage-parked.md +325 -0
- package/dist/agent-src/commands/roadmap.md +18 -4
- package/dist/agent-src/commands/tests/create.md +17 -3
- package/dist/agent-src/commands/tests/execute.md +22 -5
- package/dist/agent-src/commands/work.md +4 -0
- package/dist/agent-src/contexts/authority/decision-revisit-gate-enforcement.md +43 -0
- package/dist/agent-src/contexts/communication/rules-auto/design-fidelity-enforcement.md +24 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +6 -6
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +32 -2
- package/dist/agent-src/contexts/execution/session-canary-enforcement-history.md +67 -0
- package/dist/agent-src/contexts/execution/toolchain-resolver.md +101 -7
- package/dist/agent-src/rules/active-remediation.md +1 -1
- package/dist/agent-src/rules/autonomous-execution.md +1 -0
- package/dist/agent-src/rules/broken-access-control.md +1 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +1 -0
- package/dist/agent-src/rules/context-hygiene.md +1 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +16 -33
- package/dist/agent-src/rules/design-fidelity.md +13 -16
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +1 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +1 -0
- package/dist/agent-src/rules/domain-safety-pii.md +1 -0
- package/dist/agent-src/rules/domain-safety-retention.md +1 -0
- package/dist/agent-src/rules/engineering-safety-floor.md +1 -0
- package/dist/agent-src/rules/evaluator-independence.md +3 -2
- package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
- package/dist/agent-src/rules/finance-safety-floor.md +1 -0
- package/dist/agent-src/rules/git-history-discipline.md +1 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
- package/dist/agent-src/rules/legal-safety-floor.md +3 -2
- package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/rules/media-governance-routing.md +14 -17
- package/dist/agent-src/rules/persona-governance.md +2 -2
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +1 -1
- package/dist/agent-src/rules/question-not-instruction.md +1 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
- package/dist/agent-src/rules/runtime-safety.md +1 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +1 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +3 -2
- package/dist/agent-src/rules/session-canary.md +8 -62
- package/dist/agent-src/rules/settings-ask-protocol.md +1 -0
- package/dist/agent-src/rules/source-confidentiality.md +1 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +1 -0
- package/dist/agent-src/rules/tool-safety.md +1 -0
- package/dist/agent-src/rules/ui-audit-gate.md +1 -2
- package/dist/agent-src/rules/untrusted-input-defense.md +1 -0
- package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
- package/dist/agent-src/scripts/deferral_carry.ts +339 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +4 -1
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
- package/dist/agent-src/skills/api-design/data/manifest.json +2 -2
- package/dist/agent-src/skills/api-testing/SKILL.md +17 -0
- package/dist/agent-src/skills/code-intelligence/SKILL.md +14 -2
- package/dist/agent-src/skills/design-review/SKILL.md +30 -4
- package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
- package/dist/agent-src/skills/design-system-capture/references/import-procedure.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +18 -3
- package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
- package/dist/agent-src/skills/iconography/SKILL.md +1 -1
- package/dist/agent-src/skills/js-library-packaging/SKILL.md +2 -2
- package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
- package/dist/agent-src/skills/monorepo-workspace/SKILL.md +1 -1
- package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
- package/dist/agent-src/skills/quality-tools/SKILL.md +11 -1
- package/dist/agent-src/skills/quality-tools/references/go-tools.md +76 -0
- package/dist/agent-src/skills/quality-tools/references/python-tools.md +104 -0
- package/dist/agent-src/skills/review-routing/SKILL.md +2 -2
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +3 -3
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +7 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +17 -0
- package/dist/agent-src/skills/test-performance/SKILL.md +19 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +25 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +30 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +302 -21
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
- package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/dist/cli/python/knowledge_ingest.js +1 -1
- package/dist/cli/python/workspace_drive.js +1 -1
- package/dist/cli/registry.js +1 -1
- package/dist/cli-delegate/archive_completed_roadmaps.js +92 -12
- package/dist/cli-delegate/check_update_banner.js +12 -1
- package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
- package/dist/cli-delegate/{chunk-M2DPGFZ4.js → chunk-6V6TA25H.js} +22 -152
- package/dist/cli-delegate/chunk-7IQE3D5S.js +191 -0
- package/dist/cli-delegate/{chunk-6KFEC3G7.js → chunk-CQWX336L.js} +1 -1
- package/dist/cli-delegate/chunk-DEIV7XP7.js +108 -0
- package/dist/cli-delegate/chunk-EEHDXSYD.js +2040 -0
- package/dist/cli-delegate/{chunk-P6ZHP5W2.js → chunk-GKE7ZVFA.js} +4 -4
- package/dist/cli-delegate/chunk-JQOM7TTM.js +1201 -0
- package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-JQVYKIZA.js} +41 -0
- package/dist/cli-delegate/{chunk-FC75PGJE.js → chunk-JY2I2DXV.js} +12 -1
- package/dist/cli-delegate/chunk-KB5FZ655.js +208 -0
- package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-KR6EM23A.js} +235 -1455
- package/dist/cli-delegate/{chunk-LTYVIY76.js → chunk-LIYTLH6O.js} +1 -1
- package/dist/cli-delegate/chunk-NQ7WT43P.js +2729 -0
- package/dist/cli-delegate/chunk-NXEILF2K.js +2160 -0
- package/dist/cli-delegate/{chunk-5R7HOLAF.js → chunk-NYX3L2PZ.js} +1 -1
- package/dist/cli-delegate/{chunk-4E2W63DA.js → chunk-OKJUIAPS.js} +62 -1
- package/dist/cli-delegate/{chunk-QAJS5YWY.js → chunk-PHD7OOLE.js} +2 -1
- package/dist/cli-delegate/{chunk-RGNIWTEA.js → chunk-QLDHIPWR.js} +1 -1
- package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-RQNNW7IF.js} +1405 -567
- package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-S4UMZNES.js} +1 -1
- package/dist/cli-delegate/{chunk-6KM6ORRY.js → chunk-S4YWNRKO.js} +24 -8
- package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-TRI6DQZ6.js} +3054 -325
- package/dist/cli-delegate/chunk-ULQRUR23.js +208 -0
- package/dist/cli-delegate/{chunk-NUHLI2RX.js → chunk-YCU3TIXH.js} +2 -0
- package/dist/cli-delegate/chunk-ZBZV2G5R.js +210 -0
- package/dist/cli-delegate/{chunk-WGXTVKCQ.js → chunk-ZUZKZYCN.js} +5 -3
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +1862 -809
- package/dist/cli-delegate/cmd_doctor.js +13 -6
- package/dist/cli-delegate/cmd_doctor_neighbours.js +23 -0
- package/dist/cli-delegate/cmd_explain.js +5 -5
- package/dist/cli-delegate/cmd_export.js +11 -8
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +10 -7
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_memory_get.js +3 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +3 -3
- package/dist/cli-delegate/cmd_preflight.js +6 -6
- package/dist/cli-delegate/cmd_prune.js +11 -8
- package/dist/cli-delegate/cmd_refresh.js +2 -1
- package/dist/cli-delegate/cmd_route_audit.js +8 -6
- package/dist/cli-delegate/cmd_route_explain.js +4 -4
- package/dist/cli-delegate/cmd_session_recycle.js +4 -3
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +11 -8
- package/dist/cli-delegate/cmd_settings_set.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +11 -8
- package/dist/cli-delegate/cmd_uninstall.js +11 -8
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +3 -2
- package/dist/cli-delegate/cmd_validate.js +11 -8
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/cli-delegate/deferral_carry.js +16 -0
- package/dist/cli-delegate/gate_execute.js +2 -2
- package/dist/cli-delegate/roadmap_gates.js +5 -5
- package/dist/cli-delegate/update_roadmap_progress.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +132 -80
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +7972 -5186
- package/dist/hooks/dispatch.sha256 +1 -0
- package/dist/install/claudePathsPlan.js +47 -6
- package/dist/install/claudeRuleRewrite.js +7 -0
- package/dist/install/emit_host_rules_cli.js +39 -36
- package/dist/install/globalRuleScope.js +87 -0
- package/dist/install/host_rule_ownership.js +160 -0
- package/dist/install/install.mjs +2226 -1055
- package/dist/install/installThinLayer.js +261 -0
- package/dist/install/installedRuleLinks.js +176 -0
- package/dist/install/wizard-plan.js +24 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +40913 -34865
- package/dist/scripts/_lib/agent_settings.js +11 -0
- package/dist/scripts/_lib/claude_settings_hooks.js +10 -8
- package/dist/scripts/_lib/installed_layer.js +378 -0
- package/dist/scripts/_lib/lean_projection_mode.js +218 -15
- package/dist/scripts/_lib/rule_consequence_class.js +45 -0
- package/dist/scripts/_lib/rule_injection.js +67 -10
- package/dist/scripts/_lib/rule_law_section.js +122 -0
- package/dist/scripts/_lib/thin_rules.js +458 -0
- package/dist/scripts/_lib/windsurf_render.js +60 -0
- package/dist/scripts/condense.js +14 -26
- package/dist/scripts/project_thin_rules.js +14 -294
- package/dist/server/schemas/settings.js +29 -0
- package/dist/shared/skillRanking.js +74 -13
- package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
- package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/docs/contracts/settings-classes.md +10 -6
- package/package.json +4 -4
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
- package/src/agent-src/scripts/deferral_carry.ts +339 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +4 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +302 -21
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
- package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/src/config/agent-settings.template.yml +91 -22
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/continuity-surface.json +9 -2
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +327 -3
- package/src/config/gate-violation-baselines.json +21 -9
- package/src/config/hook-bundle-budget.json +25 -0
- package/src/config/hook-latency-budget.json +21 -0
- package/src/config/hook-token-budget.json +8 -3
- package/src/config/host-instruction-limits.json +33 -0
- package/src/config/metric-registry.yml +114 -0
- package/src/config/placeholder-drift-budget.json +10 -0
- package/src/config/routing-coverage-seed.json +38 -3
- package/src/config/rule-activation-census.json +7 -3
- package/src/config/rule-consequence-class.json +161 -0
- package/src/config/rule-law-ceilings.json +99 -0
- package/src/install/claudePathsPlan.ts +46 -4
- package/src/install/claudeRuleRewrite.ts +10 -1
- package/src/install/emit_host_rules_cli.ts +43 -34
- package/src/install/globalRuleScope.ts +109 -0
- package/src/install/host_rule_ownership.ts +170 -0
- package/src/install/installThinLayer.ts +312 -0
- package/src/install/installedRuleLinks.ts +220 -0
- package/src/install/wizard-plan.ts +24 -0
- package/src/scripts/_cli/cmd_doctor.ts +28 -90
- package/src/scripts/_cli/cmd_doctor_neighbours.ts +293 -0
- package/src/scripts/_cli/doctor_execution.ts +100 -17
- package/src/scripts/_cli/doctor_network_posture.ts +226 -0
- package/src/scripts/_dispatch.bash +16 -2
- package/src/scripts/_lib/agent_settings.ts +11 -0
- package/src/scripts/_lib/body_portable.ts +102 -0
- package/src/scripts/_lib/cc_transcript.ts +132 -1
- package/src/scripts/_lib/claude_settings_hooks.ts +10 -8
- package/src/scripts/_lib/concern_sla_window.ts +200 -0
- package/src/scripts/_lib/estate_offsets.ts +241 -0
- package/src/scripts/_lib/exemption_shape.ts +143 -0
- package/src/scripts/_lib/fnv.ts +23 -0
- package/src/scripts/_lib/foreign_scan.ts +106 -0
- package/src/scripts/_lib/forge_protection.ts +114 -0
- package/src/scripts/_lib/forge_reader.ts +558 -0
- package/src/scripts/_lib/gitignore_block.ts +314 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +32 -0
- package/src/scripts/_lib/graph_feeder_record.ts +214 -0
- package/src/scripts/_lib/hook_settings.ts +73 -0
- package/src/scripts/_lib/host_capability.ts +40 -19
- package/src/scripts/_lib/host_env_write.ts +111 -0
- package/src/scripts/_lib/host_hook_merge.ts +165 -0
- package/src/scripts/_lib/installed_layer.ts +538 -0
- package/src/scripts/_lib/json_merge.ts +82 -0
- package/src/scripts/_lib/json_pointers.ts +93 -11
- package/src/scripts/_lib/lean_projection_mode.ts +275 -15
- package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
- package/src/scripts/_lib/machine_wake.ts +16 -3
- package/src/scripts/_lib/neighbour_census.ts +760 -0
- package/src/scripts/_lib/neighbour_scan.ts +122 -0
- package/src/scripts/_lib/neighbour_tool_use.ts +189 -0
- package/src/scripts/_lib/obligations.ts +119 -8
- package/src/scripts/_lib/probe_inputs.ts +271 -0
- package/src/scripts/_lib/prompt_shape.ts +46 -0
- package/src/scripts/_lib/read_surface_scan.ts +322 -0
- package/src/scripts/_lib/release_highlights.ts +2 -1
- package/src/scripts/_lib/release_migration_gate.ts +19 -3
- package/src/scripts/_lib/reserved_name_sweep.ts +73 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
- package/src/scripts/_lib/review_coverage.ts +134 -0
- package/src/scripts/_lib/rule_consequence_class.ts +71 -0
- package/src/scripts/_lib/rule_injection.ts +130 -10
- package/src/scripts/_lib/rule_law_section.ts +147 -0
- package/src/scripts/_lib/rule_layer_overlap.ts +58 -0
- package/src/scripts/_lib/shell_write_shapes.ts +108 -0
- package/src/scripts/_lib/skill_origin.ts +254 -0
- package/src/scripts/_lib/structural_hiding.ts +622 -0
- package/src/scripts/_lib/structured_ask.ts +64 -21
- package/src/scripts/_lib/thin_rules.ts +540 -0
- package/src/scripts/_lib/touched_file_quality.ts +434 -0
- package/src/scripts/_lib/turn_end_refusals.ts +600 -4
- package/src/scripts/_lib/turn_end_transcript.ts +216 -0
- package/src/scripts/_lib/verification_command.ts +324 -0
- package/src/scripts/_lib/verification_evidence.ts +647 -0
- package/src/scripts/_lib/verify_clause.ts +280 -0
- package/src/scripts/_lib/windsurf_render.ts +63 -0
- package/src/scripts/ai_council/cli_call_budget.ts +13 -5
- package/src/scripts/ai_council/clients.ts +26 -57
- package/src/scripts/ai_council/config.ts +1 -1
- package/src/scripts/ai_council/vendor_defaults.ts +45 -0
- package/src/scripts/ai_team/team_dispatch.ts +4 -0
- package/src/scripts/audit_skill_overlap.ts +261 -0
- package/src/scripts/before_complete_hook.ts +573 -25
- package/src/scripts/bench_hook_latency.ts +412 -5
- package/src/scripts/bench_touched_file_quality.ts +238 -0
- package/src/scripts/cache_realization_report.ts +61 -1
- package/src/scripts/check_argument_hint.ts +287 -0
- package/src/scripts/check_claims.ts +125 -1
- package/src/scripts/check_enforcement_coverage.ts +38 -9
- package/src/scripts/check_enforcement_matrix.ts +621 -22
- package/src/scripts/check_estate_count.ts +130 -153
- package/src/scripts/check_finding_dispositions.ts +138 -7
- package/src/scripts/check_generator_sync.ts +71 -0
- package/src/scripts/check_hook_bundle_composition.ts +370 -0
- package/src/scripts/check_host_docs_digest.ts +786 -0
- package/src/scripts/check_host_format_column.ts +559 -0
- package/src/scripts/check_input_references.ts +228 -0
- package/src/scripts/check_inputs_parity.ts +235 -0
- package/src/scripts/check_kernel_edit_ratified.ts +89 -9
- package/src/scripts/check_kill_switch_table.ts +294 -0
- package/src/scripts/check_memory.ts +7 -1
- package/src/scripts/check_placeholder_drift.ts +221 -0
- package/src/scripts/check_read_surface_coverage.ts +515 -0
- package/src/scripts/check_release_highlights.ts +10 -0
- package/src/scripts/check_release_holds.ts +2 -1
- package/src/scripts/check_release_pr_shape.ts +38 -15
- package/src/scripts/check_routing_coverage.ts +378 -11
- package/src/scripts/check_standing_rule_delivery.ts +33 -13
- package/src/scripts/check_static_parity.ts +17 -0
- package/src/scripts/check_verify_expectation_delta.ts +521 -0
- package/src/scripts/ci_settle.ts +145 -19
- package/src/scripts/closure_scan.ts +115 -1
- package/src/scripts/code_graph/detect.ts +127 -7
- package/src/scripts/code_graph/foreign.ts +194 -0
- package/src/scripts/code_graph/query.ts +217 -4
- package/src/scripts/code_graph/types.ts +26 -2
- package/src/scripts/code_graph/validate.ts +3 -1
- package/src/scripts/code_graph/verbs.ts +103 -0
- package/src/scripts/compile_hook_manifest.ts +90 -14
- package/src/scripts/condense.ts +14 -27
- package/src/scripts/estate_exemption_shape.ts +202 -0
- package/src/scripts/external_sources_denylist.json +29 -4
- package/src/scripts/generate_host_cost_table.ts +1 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +192 -14
- package/src/scripts/hooks/block_config_weakening.ts +294 -4
- package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
- package/src/scripts/hooks/block_plumbing_writes.ts +342 -0
- package/src/scripts/hooks/bundle_integrity.ts +291 -0
- package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
- package/src/scripts/hooks/code_graph_context_hook.ts +253 -25
- package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
- package/src/scripts/hooks/concern_failure_policy.ts +280 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/concern_timings.ts +94 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
- package/src/scripts/hooks/design_pass_hook.ts +100 -9
- package/src/scripts/hooks/design_slop_hook.ts +1 -2
- package/src/scripts/hooks/dispatch_hook.ts +77 -71
- package/src/scripts/hooks/edit_shape_hook.ts +1 -2
- package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
- package/src/scripts/hooks/evidence_independence.ts +1 -2
- package/src/scripts/hooks/exit_codes.ts +129 -0
- package/src/scripts/hooks/host_lowering.json +1 -0
- package/src/scripts/hooks/host_lowering.ts +146 -3
- package/src/scripts/hooks/host_lowering.yaml +262 -46
- package/src/scripts/hooks/host_semantics.ts +30 -2
- package/src/scripts/hooks/injection_budget.ts +9 -2
- package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
- package/src/scripts/hooks/journal_record_hook.ts +1 -2
- package/src/scripts/hooks/memory_recall_hook.ts +1 -2
- package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
- package/src/scripts/hooks/one_question_per_ask_hook.ts +43 -6
- package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
- package/src/scripts/hooks/payload_stub.ts +10 -1
- package/src/scripts/hooks/permission_gate.ts +1 -2
- package/src/scripts/hooks/push_settle_hook.ts +1 -3
- package/src/scripts/hooks/replay_hook.ts +2 -1
- package/src/scripts/hooks/reread_guard_hook.ts +1 -2
- package/src/scripts/hooks/review_baseline_hook.ts +1 -2
- package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
- package/src/scripts/hooks/rule_inject_hook.ts +765 -68
- package/src/scripts/hooks/run_continuation_hook.ts +13 -15
- package/src/scripts/hooks/skill_route_hook.ts +1 -2
- package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
- package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
- package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
- package/src/scripts/hooks/table_fingerprint.ts +70 -0
- package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
- package/src/scripts/hooks/telemetry_usage_hook.ts +65 -10
- package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
- package/src/scripts/hooks/turn_end_gate_hook.ts +421 -324
- package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
- package/src/scripts/hooks_status.ts +112 -0
- package/src/scripts/injection_scan_hook.ts +7 -0
- package/src/scripts/install.ts +75 -189
- package/src/scripts/installed_layer_report.ts +103 -0
- package/src/scripts/language_mirror_hook.ts +15 -6
- package/src/scripts/learning_sidecar.ts +119 -8
- package/src/scripts/lint_deny_text.ts +419 -0
- package/src/scripts/lint_exit_codes.ts +262 -0
- package/src/scripts/lint_hook_manifest.ts +108 -0
- package/src/scripts/lint_major_migration_sections.ts +156 -26
- package/src/scripts/lint_pack_boundaries.ts +63 -5
- package/src/scripts/lint_roadmap_blockers.ts +78 -12
- package/src/scripts/lint_rule_law_section.ts +517 -0
- package/src/scripts/lint_skill_descriptions.ts +1 -1
- package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
- package/src/scripts/lint_workflow_security.ts +161 -5
- package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -0
- package/src/scripts/mcp_server/graph_tools.ts +72 -3
- package/src/scripts/mcp_server/prompts.ts +142 -1
- package/src/scripts/measure_routing_signal.ts +54 -16
- package/src/scripts/measure_skill_ranker_baseline.ts +515 -53
- package/src/scripts/measure_turn_end_gate.ts +193 -0
- package/src/scripts/memory_learn_hook.ts +91 -6
- package/src/scripts/memory_lookup.ts +1 -1
- package/src/scripts/model_rule_injection.ts +52 -5
- package/src/scripts/prepack-check.mjs +53 -0
- package/src/scripts/project_thin_rules.ts +34 -327
- package/src/scripts/release_publication.ts +13 -1
- package/src/scripts/report_evidence_temperature.ts +657 -0
- package/src/scripts/report_installed_rule_links.ts +157 -0
- package/src/scripts/report_invocation_surface.ts +383 -0
- package/src/scripts/report_label_agreement.ts +233 -0
- package/src/scripts/report_obligation_carriers.ts +699 -0
- package/src/scripts/report_obligation_mechanism.ts +230 -0
- package/src/scripts/report_obligation_writer_split.ts +236 -0
- package/src/scripts/report_single_token_triggers.ts +277 -0
- package/src/scripts/report_skill_menu_census.ts +208 -0
- package/src/scripts/report_skill_ranker_confusion.ts +442 -0
- package/src/scripts/report_standing_payload_by_host.ts +188 -15
- package/src/scripts/report_verification_record_coverage.ts +306 -0
- package/src/scripts/roadmap_verify_share.ts +213 -0
- package/src/scripts/rule_activation_census.ts +5 -2
- package/src/scripts/rule_trigger_eval.ts +9 -9
- package/src/scripts/schemas/command.schema.json +44 -0
- package/src/scripts/schemas/review-findings.schema.json +7 -2
- package/src/scripts/schemas/rule.schema.json +5 -0
- package/src/scripts/schemas/skill.schema.json +44 -0
- package/src/scripts/self_review_gate.ts +421 -17
- package/src/scripts/semantic_parity_census.ts +314 -0
- package/src/scripts/session_eol_report.ts +54 -1
- package/src/scripts/skill_tools/score_skill_relevance.ts +290 -25
- package/src/scripts/sweep_dead_scan_roots.ts +1 -1
- package/src/scripts/sweep_skill_ranker_signals.ts +129 -0
- package/src/scripts/sync_gitignore.ts +29 -235
- package/src/scripts/trigger_eval_grandfather.json +0 -7
- package/src/scripts/trigger_eval_rotation.ts +122 -27
- package/src/scripts/ui_conformance_probe.ts +156 -4
- package/src/scripts/update_prices.ts +85 -3
- package/src/scripts/write_bundle_digest.mjs +74 -0
- package/src/server/schemas/settings.ts +41 -0
- package/src/shared/skillRanking.ts +117 -11
- package/src/templates/consumer-settings/ONBOARDING.md +1 -0
- package/dist/cli-delegate/chunk-FXYUYU4N.js +0 -4416
- package/dist/cli-delegate/chunk-QJWAUMJW.js +0 -1072
- package/dist/cli-delegate/chunk-YDPILCOL.js +0 -152
- package/dist/ui/assets/index-DH1xc62v.js +0 -29
- package/dist/ui/assets/index-jSfMW8_t.css +0 -1
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The neighbour census — what else is installed beside this package, by shape.
|
|
3
|
+
*
|
|
4
|
+
* A consumer installs several agent packages. Until now this suite could not
|
|
5
|
+
* say so: `doctor` listed unclaimed FILES under its own deploy roots and
|
|
6
|
+
* nothing else, so a neighbour's hook entry, skill, command, MCP server or
|
|
7
|
+
* instruction section was invisible, and the only way the suite ever
|
|
8
|
+
* acknowledged one was by deleting it.
|
|
9
|
+
*
|
|
10
|
+
* Read-only, and by SHAPE rather than by name. No vendor name is a constant
|
|
11
|
+
* here: an artefact is a neighbour's because nothing of ours claims it — no
|
|
12
|
+
* signature in the command, no entry in the lockfile, no heading in our own
|
|
13
|
+
* template — never because it matched a list of other packages. A name list
|
|
14
|
+
* would be wrong the day a package renames and wrong forever about one nobody
|
|
15
|
+
* thought of.
|
|
16
|
+
*
|
|
17
|
+
* Secrets discipline: a hook command line can carry a token, and this census
|
|
18
|
+
* can end up in a committed report. Only the command HEAD is ever printed —
|
|
19
|
+
* the first whitespace-separated token, which is the program — never its
|
|
20
|
+
* arguments. The fixture asserts that an argument string planted in a
|
|
21
|
+
* neighbour's command never reaches the output.
|
|
22
|
+
*
|
|
23
|
+
* The honest limit this whole surface sits inside: the host owns load order
|
|
24
|
+
* and context order. The census can say a neighbour is there and what its
|
|
25
|
+
* entry would do; it cannot say what ran first, and nothing here implies it.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { createHash } from 'node:crypto';
|
|
29
|
+
import * as fs from 'node:fs';
|
|
30
|
+
import * as os from 'node:os';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { HOOK_SIGNATURES, entryCommands, isManagedEntry } from './host_hook_merge.js';
|
|
34
|
+
import { TOOL_USE_WINDOW_DAYS, serverSegment, toolsUsedByServer } from './neighbour_tool_use.js';
|
|
35
|
+
import { scanText } from './secret_detector.js';
|
|
36
|
+
import { readRecordedHashes } from '../../install/recordedOwnership.js';
|
|
37
|
+
import { manifest_path } from './installed_tools.js';
|
|
38
|
+
import {
|
|
39
|
+
classifyCompat,
|
|
40
|
+
fileDigest,
|
|
41
|
+
makeSkillOriginResolver,
|
|
42
|
+
qualifiedSkillName,
|
|
43
|
+
scanVerdict,
|
|
44
|
+
type Compat,
|
|
45
|
+
type ScanCache,
|
|
46
|
+
} from './skill_origin.js';
|
|
47
|
+
|
|
48
|
+
// The authorship + scan primitives live in `skill_origin.ts` so the ranker can
|
|
49
|
+
// reach them without this file's module graph; re-exported here because every
|
|
50
|
+
// existing caller of the census expects one import site.
|
|
51
|
+
export {
|
|
52
|
+
AUTHORED_SKILL_ROOT,
|
|
53
|
+
SCAN_CACHE_RELATIVE,
|
|
54
|
+
classifyCompat,
|
|
55
|
+
fileDigest,
|
|
56
|
+
makeSkillOriginResolver,
|
|
57
|
+
packageClaimedPaths,
|
|
58
|
+
qualifiedSkillName,
|
|
59
|
+
readScanCache,
|
|
60
|
+
scanCachePath,
|
|
61
|
+
scanVerdict,
|
|
62
|
+
splitQualifiedName,
|
|
63
|
+
type Compat,
|
|
64
|
+
type ScanCache,
|
|
65
|
+
type ScanRecord,
|
|
66
|
+
type SkillOrigin,
|
|
67
|
+
} from './skill_origin.js';
|
|
68
|
+
|
|
69
|
+
/** The seven shape classes a neighbour artefact can take. */
|
|
70
|
+
export type ShapeClass =
|
|
71
|
+
| 'hook_group'
|
|
72
|
+
| 'skill'
|
|
73
|
+
| 'command'
|
|
74
|
+
| 'agent'
|
|
75
|
+
| 'mcp_server'
|
|
76
|
+
| 'rule_file'
|
|
77
|
+
| 'instruction_section';
|
|
78
|
+
|
|
79
|
+
/** What a hook entry does to a turn, as far as this tree can tell. */
|
|
80
|
+
export type Effect =
|
|
81
|
+
| 'permission'
|
|
82
|
+
| 'context'
|
|
83
|
+
| 'verification'
|
|
84
|
+
| 'telemetry'
|
|
85
|
+
| 'memory'
|
|
86
|
+
| 'notification'
|
|
87
|
+
| 'formatting'
|
|
88
|
+
| 'unknown';
|
|
89
|
+
|
|
90
|
+
export interface NeighbourEntry {
|
|
91
|
+
/** `<origin>:<name>` — origin is WHERE it was found, never who wrote it. */
|
|
92
|
+
id: string;
|
|
93
|
+
shape: ShapeClass;
|
|
94
|
+
digest: string;
|
|
95
|
+
/** The file it was found in, relative to the root it was found under. */
|
|
96
|
+
source: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A foreign MCP server, with what this package has OBSERVED of it.
|
|
101
|
+
*
|
|
102
|
+
* Two numbers, and the asymmetry between them is the point. `tools_used_30d`
|
|
103
|
+
* is counted from names that actually passed through the post-tool hook, so it
|
|
104
|
+
* is a floor on use and never an estimate. `tools_advertised` is the literal
|
|
105
|
+
* `'unknown'`: finding out what a server offers means launching it and running
|
|
106
|
+
* a handshake, and nothing in this read-only census starts a neighbour's
|
|
107
|
+
* process. A `0` here therefore means "none observed", never "none exist" —
|
|
108
|
+
* a store that was never written and a server that was never called are the
|
|
109
|
+
* same reading, and the field name says `used`, not `available`, for that reason.
|
|
110
|
+
*/
|
|
111
|
+
export interface NeighbourMcpEntry extends NeighbourEntry {
|
|
112
|
+
shape: 'mcp_server';
|
|
113
|
+
tools_used_30d: number;
|
|
114
|
+
tools_advertised: 'unknown';
|
|
115
|
+
/** The window `tools_used_30d` was counted over, in days. */
|
|
116
|
+
tools_window_days: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ForeignHookEntry extends NeighbourEntry {
|
|
120
|
+
shape: 'hook_group';
|
|
121
|
+
event: string;
|
|
122
|
+
matcher: string | null;
|
|
123
|
+
/** The program only. Arguments are never read into the report. */
|
|
124
|
+
command_head: string;
|
|
125
|
+
timeout: number | null;
|
|
126
|
+
effect: Effect;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A neighbour skill, with the three things a reader needs beyond its id.
|
|
131
|
+
*
|
|
132
|
+
* `compat` is computed WITHOUT a model: a name collision is a string compare, an
|
|
133
|
+
* overlap is the cosine `audit_skill_overlap` already uses at the threshold it
|
|
134
|
+
* already calibrates, and `unscanned` is a linter verdict. Nothing here is a
|
|
135
|
+
* judgement about whether the neighbour is good, and nothing is suppressed on
|
|
136
|
+
* any label — `unclassified` in particular is the ABSENCE of a signal, never a
|
|
137
|
+
* clearance.
|
|
138
|
+
*/
|
|
139
|
+
export interface NeighbourSkillEntry extends NeighbourEntry {
|
|
140
|
+
shape: 'skill';
|
|
141
|
+
/** What a route line prints for it: `home:design-system`, `project:…`. */
|
|
142
|
+
qualified: string;
|
|
143
|
+
compat: Compat;
|
|
144
|
+
/** Our skills it shadows by name or overlaps by content, sorted. */
|
|
145
|
+
also: string[];
|
|
146
|
+
/** Finding kind when the shape scan refused the body, else `null`. */
|
|
147
|
+
unscanned: string | null;
|
|
148
|
+
/** Highest cross-root similarity against one of ours, or `null`. */
|
|
149
|
+
similarity: number | null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type EnvironmentLabel = 'controlled' | 'coordinated' | 'degraded' | 'uncontrolled';
|
|
153
|
+
|
|
154
|
+
export interface HostCensus {
|
|
155
|
+
host: string;
|
|
156
|
+
file: string;
|
|
157
|
+
present: boolean;
|
|
158
|
+
hook_groups: ForeignHookEntry[];
|
|
159
|
+
/** Our own group is registered in this file. */
|
|
160
|
+
ours_bound: boolean;
|
|
161
|
+
label: EnvironmentLabel;
|
|
162
|
+
/** Why the label is what it is — one clause, always present. */
|
|
163
|
+
label_reason: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface CensusWarning {
|
|
167
|
+
kind: 'double-gate' | 'shadowed' | 'removed-after-install' | 'settings-takeover';
|
|
168
|
+
detail: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface NeighbourCensus {
|
|
172
|
+
project_root: string;
|
|
173
|
+
hosts: HostCensus[];
|
|
174
|
+
hook_groups: ForeignHookEntry[];
|
|
175
|
+
skills: NeighbourSkillEntry[];
|
|
176
|
+
commands: NeighbourEntry[];
|
|
177
|
+
agents: NeighbourEntry[];
|
|
178
|
+
mcp_servers: NeighbourMcpEntry[];
|
|
179
|
+
rule_files: NeighbourEntry[];
|
|
180
|
+
instruction_sections: NeighbourEntry[];
|
|
181
|
+
warnings: CensusWarning[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Our MCP server's key — ours, so naming it here is not a vendor table. */
|
|
185
|
+
const OUR_MCP_KEY = 'agent-config';
|
|
186
|
+
|
|
187
|
+
function sha256(text: string): string {
|
|
188
|
+
return createHash('sha256').update(text, 'utf-8').digest('hex');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function readJson(p: string): Record<string, unknown> | null {
|
|
192
|
+
try {
|
|
193
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
194
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
195
|
+
return parsed as Record<string, unknown>;
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The program a command line runs — a CLASSIFIER, not a disclosure.
|
|
203
|
+
*
|
|
204
|
+
* The census needs enough to tell one neighbour's hook from another's; it does
|
|
205
|
+
* not need to reproduce anything runnable. A hook command line can carry a
|
|
206
|
+
* token, and this output can end up in a committed report, so the rule is that
|
|
207
|
+
* only a plain program name survives and everything else collapses to
|
|
208
|
+
* `(redacted)`.
|
|
209
|
+
*
|
|
210
|
+
* Taking "the first token" was not enough, and an independent review supplied
|
|
211
|
+
* the three shapes that defeat it:
|
|
212
|
+
*
|
|
213
|
+
* - `TOKEN=sk-live-… ./hook` — the first token IS the secret, because a
|
|
214
|
+
* leading environment assignment is not an argument.
|
|
215
|
+
* - an `https://` program carrying userinfo before the `@` — the credential is
|
|
216
|
+
* inside the program rather than beside it. (Described rather than written
|
|
217
|
+
* out: this repository's own `check_secret_leak` classifies that URI shape
|
|
218
|
+
* `db-connection-uri` and fails the build on it, which is the gate behaving
|
|
219
|
+
* correctly — a doc example is not a reason to add an allow-marker.)
|
|
220
|
+
* - `curl -H 'Auth: a;b' …` — splitting on `;` ignores quoting, so an
|
|
221
|
+
* argument fragment gets promoted into the output.
|
|
222
|
+
*
|
|
223
|
+
* So: leading `VAR=…` assignments are dropped rather than reported, a program
|
|
224
|
+
* carrying userinfo or a query string is redacted whole, the result must look
|
|
225
|
+
* like a bare path or program name or it is redacted, and whatever survives all
|
|
226
|
+
* of that is passed through this repository's own `secret_detector` as the last
|
|
227
|
+
* gate. The shape clause makes the quoting case safe without a shell parser — a
|
|
228
|
+
* fragment promoted out of a quoted argument does not look like a program name;
|
|
229
|
+
* the detector catches the one case shape cannot, a credential whose characters
|
|
230
|
+
* are themselves a legal program name.
|
|
231
|
+
*
|
|
232
|
+
* The residual, stated rather than implied: a secret that is neither
|
|
233
|
+
* credential-shaped nor high-entropy and is the whole command is
|
|
234
|
+
* indistinguishable from a program of that name, by this or any classifier that
|
|
235
|
+
* does not know what the program is. The census does not claim otherwise.
|
|
236
|
+
*/
|
|
237
|
+
export function commandHead(command: string): string {
|
|
238
|
+
const trimmed = command.trim();
|
|
239
|
+
if (trimmed === '') return '';
|
|
240
|
+
// A shell guard prefix (`[ -x ./x ] || exit 0; real-command …`) is this
|
|
241
|
+
// package's own shape and a neighbour may use one too; take the program
|
|
242
|
+
// after the last `;` so the head names what actually runs.
|
|
243
|
+
const lastStatement = trimmed.split(';').pop() ?? trimmed;
|
|
244
|
+
const tokens = lastStatement.trim().split(/\s+/).filter((t) => t !== '');
|
|
245
|
+
// Drop leading environment assignments — the value is the exact thing
|
|
246
|
+
// that must not be printed, and the program is whatever follows.
|
|
247
|
+
let i = 0;
|
|
248
|
+
while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i] ?? '')) i += 1;
|
|
249
|
+
const head = tokens[i] ?? '';
|
|
250
|
+
if (head === '') return '(redacted)';
|
|
251
|
+
// A bare program name or path, and nothing else: letters, digits and the
|
|
252
|
+
// punctuation a path uses. No `@`, no `?`, no `&`, no quote, so neither a
|
|
253
|
+
// credential-bearing URL nor a promoted argument fragment can pass.
|
|
254
|
+
if (!/^[A-Za-z0-9_./\\:-]+$/.test(head) || head.includes('@')) return '(redacted)';
|
|
255
|
+
// Last gate: a credential can be spelled like a program name, and shape
|
|
256
|
+
// alone cannot see that. The repo's own detector can.
|
|
257
|
+
return scanText(head).length === 0 ? head : '(redacted)';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The census's name for one host hook file.
|
|
262
|
+
*
|
|
263
|
+
* Scope-prefixed because the same host has a project file and a user file and
|
|
264
|
+
* they are different environments: a neighbour registered in `~/.cursor` is
|
|
265
|
+
* present in every project the consumer opens, one in `.cursor` only here.
|
|
266
|
+
* A label that collapsed the two would report the wrong blast radius.
|
|
267
|
+
*/
|
|
268
|
+
export function hostNameFor(label: string): string {
|
|
269
|
+
const user = label.startsWith('~/');
|
|
270
|
+
const dir = label.replace(/^~\//, '').replace(/\/[^/]+$/, '');
|
|
271
|
+
const name = dir
|
|
272
|
+
.split('/')
|
|
273
|
+
.map((seg) => seg.replace(/^\./, ''))
|
|
274
|
+
.join('/');
|
|
275
|
+
return user ? `user:${name}` : `project:${name}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Every host hook file this package knows how to read, with its signature.
|
|
280
|
+
*
|
|
281
|
+
* `homeRoot` is injectable because the user-scope half of this list is real
|
|
282
|
+
* `$HOME` in production and must be a planted tree in a fixture — a census
|
|
283
|
+
* that could only read the developer's own machine would be untestable, and
|
|
284
|
+
* its output would carry that machine into any report.
|
|
285
|
+
*/
|
|
286
|
+
export function hookFiles(
|
|
287
|
+
projectRoot: string,
|
|
288
|
+
homeRoot?: string,
|
|
289
|
+
): Array<{ host: string; file: string; signature: string }> {
|
|
290
|
+
const home = homeRoot ?? os.homedir();
|
|
291
|
+
const expand = (label: string): string =>
|
|
292
|
+
label.startsWith('~/') ? path.join(home, label.slice(2)) : path.join(projectRoot, label);
|
|
293
|
+
const out = Object.entries(HOOK_SIGNATURES).map(([label, signature]) => ({
|
|
294
|
+
host: hostNameFor(label),
|
|
295
|
+
file: expand(label),
|
|
296
|
+
signature,
|
|
297
|
+
}));
|
|
298
|
+
// Claude is the one host whose managed block predates this table; its
|
|
299
|
+
// signature lives with its own writer and is not a hook-array label.
|
|
300
|
+
out.push({
|
|
301
|
+
host: 'project:claude',
|
|
302
|
+
file: path.join(projectRoot, '.claude', 'settings.json'),
|
|
303
|
+
signature: 'dispatch:hook --platform claude',
|
|
304
|
+
});
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Foreign hook entries in one host file, plus whether ours is registered. */
|
|
309
|
+
export function censusHookFile(
|
|
310
|
+
host: string,
|
|
311
|
+
file: string,
|
|
312
|
+
signature: string,
|
|
313
|
+
): { entries: ForeignHookEntry[]; oursBound: boolean; present: boolean } {
|
|
314
|
+
const doc = readJson(file);
|
|
315
|
+
if (doc === null) return { entries: [], oursBound: false, present: false };
|
|
316
|
+
const hooks = doc['hooks'];
|
|
317
|
+
if (hooks === null || typeof hooks !== 'object' || Array.isArray(hooks)) {
|
|
318
|
+
return { entries: [], oursBound: false, present: true };
|
|
319
|
+
}
|
|
320
|
+
const entries: ForeignHookEntry[] = [];
|
|
321
|
+
let oursBound = false;
|
|
322
|
+
for (const [event, list] of Object.entries(hooks as Record<string, unknown>)) {
|
|
323
|
+
if (!Array.isArray(list)) continue;
|
|
324
|
+
for (const raw of list) {
|
|
325
|
+
if (isManagedEntry(raw, signature)) {
|
|
326
|
+
oursBound = true;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const commands = entryCommands(raw);
|
|
330
|
+
const head = commandHead(commands[0] ?? '');
|
|
331
|
+
const obj = (raw ?? {}) as Record<string, unknown>;
|
|
332
|
+
const matcher = typeof obj['matcher'] === 'string' ? obj['matcher'] : null;
|
|
333
|
+
const timeout = typeof obj['timeout'] === 'number' ? obj['timeout'] : null;
|
|
334
|
+
entries.push({
|
|
335
|
+
id: `${host}:${event}/${head === '' ? 'unreadable' : head}`,
|
|
336
|
+
shape: 'hook_group',
|
|
337
|
+
digest: sha256(JSON.stringify(raw)),
|
|
338
|
+
source: file,
|
|
339
|
+
event,
|
|
340
|
+
matcher,
|
|
341
|
+
command_head: head,
|
|
342
|
+
timeout,
|
|
343
|
+
// No host exposes another package's hook execution to a hook,
|
|
344
|
+
// so a neighbour's effect is not readable from its
|
|
345
|
+
// registration. `unknown` is the honest answer, and 2.3 treats
|
|
346
|
+
// it as capable of denying rather than assuming it is not.
|
|
347
|
+
effect: 'unknown',
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
entries.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
352
|
+
return { entries, oursBound, present: true };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Files directly under `dir` the manifest does not claim. */
|
|
356
|
+
function unclaimedUnder(
|
|
357
|
+
dir: string,
|
|
358
|
+
origin: string,
|
|
359
|
+
shape: ShapeClass,
|
|
360
|
+
owned: ReadonlySet<string>,
|
|
361
|
+
opts: { nested?: boolean } = {},
|
|
362
|
+
): NeighbourEntry[] {
|
|
363
|
+
let names: string[];
|
|
364
|
+
try {
|
|
365
|
+
names = fs.readdirSync(dir).sort();
|
|
366
|
+
} catch {
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
const out: NeighbourEntry[] = [];
|
|
370
|
+
for (const name of names) {
|
|
371
|
+
if (name.startsWith('.')) continue;
|
|
372
|
+
const full = path.join(dir, name);
|
|
373
|
+
// A skill is a DIRECTORY with a SKILL.md; a command or rule is a file.
|
|
374
|
+
const probe = opts.nested === true ? path.join(full, 'SKILL.md') : full;
|
|
375
|
+
let body: string;
|
|
376
|
+
try {
|
|
377
|
+
const st = fs.statSync(probe);
|
|
378
|
+
if (!st.isFile()) continue;
|
|
379
|
+
body = fs.readFileSync(probe, 'utf-8');
|
|
380
|
+
} catch {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (owned.has(path.resolve(probe))) continue;
|
|
384
|
+
out.push({
|
|
385
|
+
id: `${origin}:${opts.nested === true ? name : name.replace(/\.[^.]+$/, '')}`,
|
|
386
|
+
shape,
|
|
387
|
+
digest: sha256(body),
|
|
388
|
+
source: probe,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Top-level headings in `file` that this package's own template does not carry. */
|
|
395
|
+
export function foreignSections(file: string, templateFile: string, origin: string): NeighbourEntry[] {
|
|
396
|
+
let body: string;
|
|
397
|
+
try {
|
|
398
|
+
body = fs.readFileSync(file, 'utf-8');
|
|
399
|
+
} catch {
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
let ours = new Set<string>();
|
|
403
|
+
try {
|
|
404
|
+
ours = new Set(
|
|
405
|
+
fs
|
|
406
|
+
.readFileSync(templateFile, 'utf-8')
|
|
407
|
+
.split('\n')
|
|
408
|
+
.filter((l) => /^#{1,2} /.test(l))
|
|
409
|
+
.map((l) => l.replace(/^#{1,2} /, '').trim()),
|
|
410
|
+
);
|
|
411
|
+
} catch {
|
|
412
|
+
// No template to compare against: every section reads as foreign,
|
|
413
|
+
// which is the conservative answer and is visible as such.
|
|
414
|
+
}
|
|
415
|
+
const out: NeighbourEntry[] = [];
|
|
416
|
+
const lines = body.split('\n');
|
|
417
|
+
const isHeading = (n: number): boolean => /^#{1,2} /.test(lines[n] ?? '');
|
|
418
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
419
|
+
if (!isHeading(i)) continue;
|
|
420
|
+
const heading = (lines[i] ?? '').replace(/^#{1,2} /, '').trim();
|
|
421
|
+
if (ours.has(heading)) continue;
|
|
422
|
+
let j = i + 1;
|
|
423
|
+
while (j < lines.length && !isHeading(j)) j += 1;
|
|
424
|
+
out.push({
|
|
425
|
+
id: `${origin}:${heading}`,
|
|
426
|
+
shape: 'instruction_section',
|
|
427
|
+
digest: sha256(lines.slice(i, j).join('\n')),
|
|
428
|
+
source: file,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return out;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** MCP servers in `.mcp.json` other than ours, each with its observed use. */
|
|
435
|
+
function foreignMcpServers(projectRoot: string, now: Date): NeighbourMcpEntry[] {
|
|
436
|
+
const file = path.join(projectRoot, '.mcp.json');
|
|
437
|
+
const doc = readJson(file);
|
|
438
|
+
const servers = doc?.['mcpServers'];
|
|
439
|
+
if (servers === null || servers === undefined || typeof servers !== 'object' || Array.isArray(servers)) {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
const foreign = Object.entries(servers as Record<string, unknown>).filter(
|
|
443
|
+
([name]) => name !== OUR_MCP_KEY,
|
|
444
|
+
);
|
|
445
|
+
// The candidate set is the keys `.mcp.json` lists, passed IN rather than
|
|
446
|
+
// parsed out of the tool names — see `toolBelongsTo` for why parsing is
|
|
447
|
+
// ambiguous exactly where the sanitiser is lossy.
|
|
448
|
+
const used = toolsUsedByServer(projectRoot, now, foreign.map(([name]) => serverSegment(name)));
|
|
449
|
+
return foreign
|
|
450
|
+
.map(([name, spec]) => ({
|
|
451
|
+
id: `project:${name}`,
|
|
452
|
+
shape: 'mcp_server' as const,
|
|
453
|
+
digest: sha256(JSON.stringify(spec)),
|
|
454
|
+
source: file,
|
|
455
|
+
tools_used_30d: used.get(serverSegment(name)) ?? 0,
|
|
456
|
+
tools_advertised: 'unknown' as const,
|
|
457
|
+
tools_window_days: TOOL_USE_WINDOW_DAYS,
|
|
458
|
+
}))
|
|
459
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Label one host's environment.
|
|
464
|
+
*
|
|
465
|
+
* `degraded` is deliberately easy to reach — a foreign entry whose effect
|
|
466
|
+
* cannot be read sits on a slot we gate, and that IS the degraded case. The
|
|
467
|
+
* label names the entries responsible so the consumer can judge; it is a
|
|
468
|
+
* report and never a refusal, and nothing downstream reads it as one.
|
|
469
|
+
*/
|
|
470
|
+
export function labelFor(
|
|
471
|
+
entries: readonly ForeignHookEntry[],
|
|
472
|
+
oursBound: boolean,
|
|
473
|
+
gatedEvents: ReadonlySet<string>,
|
|
474
|
+
): { label: EnvironmentLabel; reason: string } {
|
|
475
|
+
if (!oursBound) {
|
|
476
|
+
return {
|
|
477
|
+
label: 'uncontrolled',
|
|
478
|
+
reason: 'this package has no hook group registered in this file',
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (entries.length === 0) {
|
|
482
|
+
return { label: 'controlled', reason: 'no foreign hook entry in this file' };
|
|
483
|
+
}
|
|
484
|
+
const onGated = entries.filter((e) => gatedEvents.has(e.event));
|
|
485
|
+
const stopEntries = entries.filter((e) => /stop/i.test(e.event));
|
|
486
|
+
if (onGated.length > 0 || stopEntries.length > 0) {
|
|
487
|
+
const named = [...new Set([...onGated, ...stopEntries].map((e) => e.id))].sort();
|
|
488
|
+
return {
|
|
489
|
+
label: 'degraded',
|
|
490
|
+
reason: `foreign entr(ies) of unread effect on a slot this package gates: ${named.join(', ')}`,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
label: 'coordinated',
|
|
495
|
+
reason: `${entries.length} foreign entr(ies), none on a slot this package gates`,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export interface CensusOptions {
|
|
500
|
+
/** Events this package registers a `permission` concern on. */
|
|
501
|
+
gatedEvents?: ReadonlySet<string>;
|
|
502
|
+
/** Where this package's instruction templates live. */
|
|
503
|
+
templatesRoot?: string;
|
|
504
|
+
/** Our own skill names, for the `shadowed` warning. */
|
|
505
|
+
ourSkillNames?: ReadonlySet<string>;
|
|
506
|
+
/** The user-scope root; real `$HOME` in production, planted in a fixture. */
|
|
507
|
+
homeRoot?: string;
|
|
508
|
+
/** Maintainer checkout root, for the authored-tree exception in the resolver. */
|
|
509
|
+
packageRoot?: string;
|
|
510
|
+
/**
|
|
511
|
+
* Cross-root overlap, keyed by the neighbour `SKILL.md`'s absolute path.
|
|
512
|
+
*
|
|
513
|
+
* Passed IN rather than computed here: the pair finder lives in
|
|
514
|
+
* `audit_skill_overlap` and the shape scan in `neighbour_scan`, and both of
|
|
515
|
+
* those pull module graphs this file must stay out of — it is reachable from
|
|
516
|
+
* the ranker, which runs on the `user_prompt_submit` hook path. Absent means
|
|
517
|
+
* the overlap pass did not run, which reads as no overlap and is visible as
|
|
518
|
+
* `unclassified` rather than claimed as disjoint.
|
|
519
|
+
*/
|
|
520
|
+
overlaps?: ReadonlyMap<string, { names: readonly string[]; similarity: number }>;
|
|
521
|
+
/**
|
|
522
|
+
* The shape scan, injected — returns the finding kind, or `null` on a pass.
|
|
523
|
+
*
|
|
524
|
+
* Injected rather than imported for the reason the `overlaps` field gives:
|
|
525
|
+
* `neighbour_scan` pulls four linters and, through one of them,
|
|
526
|
+
* `node:child_process`, and this file is reachable from the ranker on the
|
|
527
|
+
* hook path. Absent, verdicts fall back to the recorded cache, and a missing
|
|
528
|
+
* record there is a REFUSAL rather than a pass — see `scanVerdict`.
|
|
529
|
+
*/
|
|
530
|
+
scan?: (skillMdPath: string) => string | null;
|
|
531
|
+
/** Recorded shape-scan verdicts, for the no-scanner path. */
|
|
532
|
+
scanCache?: ScanCache | null;
|
|
533
|
+
/** Clock, for the MCP use window. Injected so a fixture can pin the day. */
|
|
534
|
+
now?: Date;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Run the whole census for one consumer root. */
|
|
538
|
+
export function census(projectRoot: string, opts: CensusOptions = {}): NeighbourCensus {
|
|
539
|
+
const gated = opts.gatedEvents ?? new Set<string>();
|
|
540
|
+
const owned = new Set(readRecordedHashes(manifest_path(projectRoot), projectRoot).keys());
|
|
541
|
+
const home = opts.homeRoot ?? os.homedir();
|
|
542
|
+
|
|
543
|
+
const hosts: HostCensus[] = [];
|
|
544
|
+
const hookGroups: ForeignHookEntry[] = [];
|
|
545
|
+
for (const { host, file, signature } of hookFiles(projectRoot, home)) {
|
|
546
|
+
const { entries, oursBound, present } = censusHookFile(host, file, signature);
|
|
547
|
+
if (!present) continue;
|
|
548
|
+
const { label, reason } = labelFor(entries, oursBound, gated);
|
|
549
|
+
hosts.push({
|
|
550
|
+
host,
|
|
551
|
+
file,
|
|
552
|
+
present,
|
|
553
|
+
hook_groups: entries,
|
|
554
|
+
ours_bound: oursBound,
|
|
555
|
+
label,
|
|
556
|
+
label_reason: reason,
|
|
557
|
+
});
|
|
558
|
+
hookGroups.push(...entries);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const ourSkillsForCompat = opts.ourSkillNames ?? new Set<string>();
|
|
562
|
+
const originOf = makeSkillOriginResolver(projectRoot, {
|
|
563
|
+
...(opts.packageRoot === undefined ? {} : { packageRoot: opts.packageRoot }),
|
|
564
|
+
});
|
|
565
|
+
const scanCache = opts.scanCache !== undefined ? opts.scanCache : null;
|
|
566
|
+
const overlaps = opts.overlaps ?? new Map();
|
|
567
|
+
const skills: NeighbourSkillEntry[] = [
|
|
568
|
+
...unclaimedUnder(path.join(projectRoot, '.claude', 'skills'), 'project', 'skill', owned, {
|
|
569
|
+
nested: true,
|
|
570
|
+
}),
|
|
571
|
+
...unclaimedUnder(path.join(home, '.claude', 'skills'), 'user', 'skill', owned, {
|
|
572
|
+
nested: true,
|
|
573
|
+
}),
|
|
574
|
+
].map((entry) => {
|
|
575
|
+
const name = entry.id.split(':').slice(1).join(':');
|
|
576
|
+
const origin = originOf(entry.source);
|
|
577
|
+
const qualified = qualifiedSkillName(name, origin);
|
|
578
|
+
const unscanned =
|
|
579
|
+
opts.scan !== undefined
|
|
580
|
+
? opts.scan(entry.source)
|
|
581
|
+
: (() => {
|
|
582
|
+
const v = scanVerdict(scanCache, qualified, fileDigest(entry.source));
|
|
583
|
+
return v.ok ? null : v.kind;
|
|
584
|
+
})();
|
|
585
|
+
const shadows = ourSkillsForCompat.has(name) ? [name] : [];
|
|
586
|
+
const hit = overlaps.get(path.resolve(entry.source));
|
|
587
|
+
// An unscanned body's overlap is derived from text this census refused
|
|
588
|
+
// to read, so it is not reported at all — see `classifyCompat`.
|
|
589
|
+
const overlapNames = unscanned === null && hit !== undefined ? [...hit.names] : [];
|
|
590
|
+
return {
|
|
591
|
+
...entry,
|
|
592
|
+
shape: 'skill' as const,
|
|
593
|
+
qualified,
|
|
594
|
+
compat: classifyCompat({ unscanned, shadows, overlaps: overlapNames }),
|
|
595
|
+
also: [...new Set([...shadows, ...overlapNames])].sort(),
|
|
596
|
+
unscanned,
|
|
597
|
+
similarity: overlapNames.length > 0 && hit !== undefined ? hit.similarity : null,
|
|
598
|
+
};
|
|
599
|
+
});
|
|
600
|
+
const commands = unclaimedUnder(
|
|
601
|
+
path.join(projectRoot, '.claude', 'commands'),
|
|
602
|
+
'project',
|
|
603
|
+
'command',
|
|
604
|
+
owned,
|
|
605
|
+
);
|
|
606
|
+
const agents = unclaimedUnder(
|
|
607
|
+
path.join(projectRoot, '.claude', 'agents'),
|
|
608
|
+
'project',
|
|
609
|
+
'agent',
|
|
610
|
+
owned,
|
|
611
|
+
);
|
|
612
|
+
const ruleFiles = [
|
|
613
|
+
...unclaimedUnder(path.join(projectRoot, '.cursor', 'rules'), 'cursor', 'rule_file', owned),
|
|
614
|
+
...unclaimedUnder(path.join(projectRoot, '.windsurf', 'rules'), 'windsurf', 'rule_file', owned),
|
|
615
|
+
...unclaimedUnder(path.join(projectRoot, '.claude', 'rules'), 'claude', 'rule_file', owned),
|
|
616
|
+
];
|
|
617
|
+
|
|
618
|
+
const templates = opts.templatesRoot ?? '';
|
|
619
|
+
const instructionSections = [
|
|
620
|
+
...foreignSections(
|
|
621
|
+
path.join(projectRoot, 'AGENTS.md'),
|
|
622
|
+
path.join(templates, 'AGENTS.md'),
|
|
623
|
+
'AGENTS.md',
|
|
624
|
+
),
|
|
625
|
+
...foreignSections(
|
|
626
|
+
path.join(projectRoot, '.github', 'copilot-instructions.md'),
|
|
627
|
+
path.join(templates, 'copilot-instructions.md'),
|
|
628
|
+
'copilot-instructions.md',
|
|
629
|
+
),
|
|
630
|
+
];
|
|
631
|
+
|
|
632
|
+
const warnings: CensusWarning[] = [];
|
|
633
|
+
const ourSkills = opts.ourSkillNames ?? new Set<string>();
|
|
634
|
+
for (const s of skills) {
|
|
635
|
+
const name = s.id.split(':').slice(1).join(':');
|
|
636
|
+
if (ourSkills.has(name)) {
|
|
637
|
+
warnings.push({
|
|
638
|
+
kind: 'shadowed',
|
|
639
|
+
detail: `a skill named \`${name}\` sits at ${s.source} and this package ships one under the same name; which one the host resolves is the host's choice, not ours`,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
warnings.push(...doubleGateWarnings(hookGroups, gated));
|
|
644
|
+
warnings.push(...settingsTakeover(path.join(projectRoot, '.claude', 'settings.json')));
|
|
645
|
+
warnings.push(...settingsTakeover(path.join(home, '.claude', 'settings.json')));
|
|
646
|
+
for (const h of hosts) {
|
|
647
|
+
if (!h.ours_bound) {
|
|
648
|
+
warnings.push({
|
|
649
|
+
kind: 'removed-after-install',
|
|
650
|
+
detail: `${h.file} carries hook entries but none of ours — this package's group is absent after its own install`,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return {
|
|
656
|
+
project_root: projectRoot,
|
|
657
|
+
hosts,
|
|
658
|
+
hook_groups: hookGroups,
|
|
659
|
+
skills,
|
|
660
|
+
commands,
|
|
661
|
+
agents,
|
|
662
|
+
mcp_servers: foreignMcpServers(projectRoot, opts.now ?? new Date()),
|
|
663
|
+
rule_files: ruleFiles,
|
|
664
|
+
instruction_sections: instructionSections,
|
|
665
|
+
warnings,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* One warning per event where a foreign entry shares a slot this package gates.
|
|
671
|
+
*
|
|
672
|
+
* Worded as the host behaves and no further: every registered hook on an event
|
|
673
|
+
* runs, and a deny from any of them applies. There is no order to report
|
|
674
|
+
* because the host does not give one, so the warning never implies that ours
|
|
675
|
+
* runs first, last, or instead.
|
|
676
|
+
*/
|
|
677
|
+
export function doubleGateWarnings(
|
|
678
|
+
entries: readonly ForeignHookEntry[],
|
|
679
|
+
gatedEvents: ReadonlySet<string>,
|
|
680
|
+
): CensusWarning[] {
|
|
681
|
+
const byEvent = new Map<string, ForeignHookEntry[]>();
|
|
682
|
+
for (const e of entries) {
|
|
683
|
+
if (!gatedEvents.has(e.event)) continue;
|
|
684
|
+
const arr = byEvent.get(e.event);
|
|
685
|
+
if (arr) arr.push(e);
|
|
686
|
+
else byEvent.set(e.event, [e]);
|
|
687
|
+
}
|
|
688
|
+
return [...byEvent.entries()]
|
|
689
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
690
|
+
.map(([event, list]) => ({
|
|
691
|
+
kind: 'double-gate' as const,
|
|
692
|
+
detail:
|
|
693
|
+
`${event}: ${list.length} foreign entr(ies) (${list.map((e) => e.id).join(', ')}) ` +
|
|
694
|
+
`share this event with a gate of ours. The host runs both; either deny applies.`,
|
|
695
|
+
}));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Settings keys a neighbour can set that change what the host does to US.
|
|
700
|
+
*
|
|
701
|
+
* Liveness, not trust: this is not an accusation that whoever wrote the key
|
|
702
|
+
* meant harm, and the census never removes one. It is the second half of
|
|
703
|
+
* "is this package still doing what it was installed to do" — a `model`
|
|
704
|
+
* override decides which model reads our rules, and a `CLAUDE_CODE_*`
|
|
705
|
+
* environment key can change the harness underneath every hook we bound.
|
|
706
|
+
* Both are legitimate things for a consumer to set. Neither is visible
|
|
707
|
+
* anywhere else, which is the whole reason the census reports them.
|
|
708
|
+
*/
|
|
709
|
+
export function settingsTakeover(settingsFile: string): CensusWarning[] {
|
|
710
|
+
const doc = readJson(settingsFile);
|
|
711
|
+
if (doc === null) return [];
|
|
712
|
+
const out: CensusWarning[] = [];
|
|
713
|
+
if (doc['model'] !== undefined) {
|
|
714
|
+
out.push({
|
|
715
|
+
kind: 'settings-takeover',
|
|
716
|
+
detail: `${settingsFile} sets \`model\` — the host resolves every turn against it, including the ones this package's rules are loaded into`,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
const env = doc['env'];
|
|
720
|
+
if (env !== null && typeof env === 'object' && !Array.isArray(env)) {
|
|
721
|
+
const keys = Object.keys(env as Record<string, unknown>)
|
|
722
|
+
.filter((k) => k.startsWith('CLAUDE_CODE_'))
|
|
723
|
+
.sort();
|
|
724
|
+
if (keys.length > 0) {
|
|
725
|
+
out.push({
|
|
726
|
+
kind: 'settings-takeover',
|
|
727
|
+
detail: `${settingsFile} sets host environment key(s) ${keys.join(', ')} — these change the harness the hooks this package bound run inside`,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return out;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** Events this package registers a `permission` concern on, from the manifest. */
|
|
735
|
+
export function gatedEventsFrom(
|
|
736
|
+
manifest: { concerns?: Record<string, { effect?: string }>; platforms?: Record<string, Record<string, unknown>> },
|
|
737
|
+
nativeAliases: Record<string, Record<string, string>> = {},
|
|
738
|
+
): Set<string> {
|
|
739
|
+
const concerns = manifest.concerns ?? {};
|
|
740
|
+
const permission = new Set(
|
|
741
|
+
Object.keys(concerns).filter((n) => concerns[n]?.effect === 'permission'),
|
|
742
|
+
);
|
|
743
|
+
const out = new Set<string>();
|
|
744
|
+
for (const [host, events] of Object.entries(manifest.platforms ?? {})) {
|
|
745
|
+
if (events === null || typeof events !== 'object') continue;
|
|
746
|
+
for (const [event, chain] of Object.entries(events)) {
|
|
747
|
+
if (!Array.isArray(chain)) continue;
|
|
748
|
+
if (!chain.some((c) => permission.has(String(c)))) continue;
|
|
749
|
+
out.add(event);
|
|
750
|
+
const native = nativeAliases[host]?.[event];
|
|
751
|
+
if (typeof native === 'string') out.add(native);
|
|
752
|
+
// The host's own event name is what a neighbour's file uses, and
|
|
753
|
+
// the alias table maps one way; add the reverse hits too.
|
|
754
|
+
for (const [nativeName, acEvent] of Object.entries(nativeAliases[host] ?? {})) {
|
|
755
|
+
if (acEvent === event) out.add(nativeName);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return out;
|
|
760
|
+
}
|