@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,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Do the links inside an installed rule file resolve from where the install
|
|
3
|
+
* puts it?
|
|
4
|
+
*
|
|
5
|
+
* A rule body is authored in `src/rules/` and projected to
|
|
6
|
+
* `dist/agent-src/rules/`, where `](../guidelines/x.md)` resolves because the
|
|
7
|
+
* sibling directory is right there. The installer then copies a SUBSET of
|
|
8
|
+
* those sibling directories into the host's rule directory, so the same link
|
|
9
|
+
* resolves or does not depending on a list in `install.ts` that the rule
|
|
10
|
+
* author never sees. Nothing checked the two against each other.
|
|
11
|
+
*
|
|
12
|
+
* This module answers one question per link โ resolved, or why not โ against a
|
|
13
|
+
* deploy plan rather than against a materialised tree, so the audit costs a
|
|
14
|
+
* `statSync` per distinct target instead of a copy of 299 skills.
|
|
15
|
+
*
|
|
16
|
+
* Side-effect-free, no CLI entry, no `process.exit`.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'node:fs';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
|
|
21
|
+
/** Why a link does not resolve from the installed file's own directory. */
|
|
22
|
+
export type LinkVerdict =
|
|
23
|
+
/** The target file is present in the installed tree. */
|
|
24
|
+
| 'resolved'
|
|
25
|
+
/** The link climbs above the install root โ a repo-tree path, never shipped. */
|
|
26
|
+
| 'outside-install-root'
|
|
27
|
+
/** The first segment names a directory the deploy plan does not write. */
|
|
28
|
+
| 'directory-not-deployed'
|
|
29
|
+
/** The directory IS deployed, but the file is not in it. */
|
|
30
|
+
| 'file-missing';
|
|
31
|
+
|
|
32
|
+
export interface LinkAudit {
|
|
33
|
+
/** Rule file the link was written in, as a bare id (no directory, no `.md`). */
|
|
34
|
+
rule: string;
|
|
35
|
+
/** The link target exactly as the markdown carries it, anchor included. */
|
|
36
|
+
target: string;
|
|
37
|
+
/** Install-root-relative path the target resolves to, or null when it escapes. */
|
|
38
|
+
resolved_to: string | null;
|
|
39
|
+
verdict: LinkVerdict;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LinkAuditReport {
|
|
43
|
+
audits: LinkAudit[];
|
|
44
|
+
/** Count per verdict, every verdict present even at zero. */
|
|
45
|
+
counts: Record<LinkVerdict, number>;
|
|
46
|
+
/** Unresolved links grouped by the first path segment, highest first. */
|
|
47
|
+
by_directory: { directory: string; count: number; verdict: LinkVerdict }[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A deploy plan: installed directory name -> the package directory copied into
|
|
52
|
+
* it. Exactly the shape of one `GLOBAL_DEPLOY_SOURCES` entry, inverted, so a
|
|
53
|
+
* caller can hand this module the real plan rather than a restatement of it.
|
|
54
|
+
*/
|
|
55
|
+
export type DeployPlan = Map<string, string>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build a plan from `GLOBAL_DEPLOY_SOURCES`-shaped pairs.
|
|
59
|
+
*
|
|
60
|
+
* The empty destination (`cline` maps rules to the root) is deliberately kept:
|
|
61
|
+
* a rule installed at the root resolves `../x` differently, and silently
|
|
62
|
+
* dropping the entry would make this module answer a question about a layout
|
|
63
|
+
* that is not the one being installed.
|
|
64
|
+
*/
|
|
65
|
+
export function deployPlanFrom(pairs: ReadonlyArray<readonly [string, string]>): DeployPlan {
|
|
66
|
+
const plan: DeployPlan = new Map();
|
|
67
|
+
for (const [source, dest] of pairs) plan.set(dest, source);
|
|
68
|
+
return plan;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Markdown inline links, minus the ones that are not a path into the tree. */
|
|
72
|
+
export function relativeLinkTargets(markdown: string): string[] {
|
|
73
|
+
const out: string[] = [];
|
|
74
|
+
for (const m of markdown.matchAll(/\]\(([^)\s]+)\)/g)) {
|
|
75
|
+
const target = m[1] as string;
|
|
76
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(target)) continue; // http:, mailto:, etc.
|
|
77
|
+
if (target.startsWith('#')) continue; // same-file anchor
|
|
78
|
+
if (target.startsWith('/')) continue; // absolute โ not ours to resolve
|
|
79
|
+
out.push(target);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Strip a trailing `#anchor`; a bare directory link keeps its trailing slash. */
|
|
85
|
+
function withoutAnchor(target: string): string {
|
|
86
|
+
const hash = target.indexOf('#');
|
|
87
|
+
return hash === -1 ? target : target.slice(0, hash);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Audit one link written in a rule that the install places at
|
|
92
|
+
* `<install-root>/<rulesDest>/<rule>.md`.
|
|
93
|
+
*/
|
|
94
|
+
export function auditLink(
|
|
95
|
+
rule: string,
|
|
96
|
+
target: string,
|
|
97
|
+
rulesDest: string,
|
|
98
|
+
plan: DeployPlan,
|
|
99
|
+
packageRoot: string,
|
|
100
|
+
): LinkAudit {
|
|
101
|
+
const bare = withoutAnchor(target);
|
|
102
|
+
// Resolve the link the way a reader does: from the installed file's own
|
|
103
|
+
// DIRECTORY โ `rulesDest` is that directory, not the file โ as a POSIX
|
|
104
|
+
// path relative to the install root. A sibling link (`[x](other.md)`, no
|
|
105
|
+
// `../`) therefore stays inside the rules directory, which is where the
|
|
106
|
+
// installer puts its sibling.
|
|
107
|
+
const fromDir = rulesDest === '' ? '.' : rulesDest;
|
|
108
|
+
const resolved = path.posix.normalize(path.posix.join(fromDir, bare));
|
|
109
|
+
|
|
110
|
+
if (resolved.startsWith('..')) {
|
|
111
|
+
return { rule, target, resolved_to: null, verdict: 'outside-install-root' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const segments = resolved.split('/');
|
|
115
|
+
// Which deployed directory does this land in? Normally the first segment
|
|
116
|
+
// names it. When rules install at the install ROOT (`cline` maps them to
|
|
117
|
+
// `''`), there is no such segment: a sibling link like `](scope-control.md)`
|
|
118
|
+
// resolves to `scope-control.md`, whose first segment is a FILE NAME. Read
|
|
119
|
+
// literally that is `directory-not-deployed` for a file the install writes
|
|
120
|
+
// right beside the rule โ 275 of cline's 523 links, every one of them fine.
|
|
121
|
+
// The `''` key exists in the plan precisely for this and could never be
|
|
122
|
+
// reached by a first-segment lookup.
|
|
123
|
+
const rootSource = plan.get('');
|
|
124
|
+
// The root entry answers for a BARE filename only. A link with a directory
|
|
125
|
+
// segment the plan does not carry is `directory-not-deployed`, the same as
|
|
126
|
+
// anywhere else โ resolving it against the rules source would probe a path
|
|
127
|
+
// the install never writes and report `file-missing`, whose documented
|
|
128
|
+
// meaning ("the directory IS deployed") would then be false.
|
|
129
|
+
const useRoot = rootSource !== undefined && segments.length === 1;
|
|
130
|
+
const source = useRoot ? rootSource : plan.get(segments[0] as string);
|
|
131
|
+
const rest = useRoot ? segments : segments.slice(1);
|
|
132
|
+
if (source === undefined) {
|
|
133
|
+
return { rule, target, resolved_to: resolved, verdict: 'directory-not-deployed' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// An empty `rest` means the link points at the deployed directory itself.
|
|
137
|
+
const onDisk = path.join(packageRoot, source, ...rest);
|
|
138
|
+
const exists = fs.existsSync(onDisk);
|
|
139
|
+
return {
|
|
140
|
+
rule,
|
|
141
|
+
target,
|
|
142
|
+
resolved_to: resolved,
|
|
143
|
+
verdict: exists ? 'resolved' : 'file-missing',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const EMPTY_COUNTS = (): Record<LinkVerdict, number> => ({
|
|
148
|
+
resolved: 0,
|
|
149
|
+
'outside-install-root': 0,
|
|
150
|
+
'directory-not-deployed': 0,
|
|
151
|
+
'file-missing': 0,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Audit every link in every rule the plan installs.
|
|
156
|
+
*
|
|
157
|
+
* `packageRoot` is the package checkout the installer copies FROM; the rule
|
|
158
|
+
* bodies are read from the plan's own rules source, so the audit cannot drift
|
|
159
|
+
* from what is shipped.
|
|
160
|
+
*/
|
|
161
|
+
export function auditInstalledRuleLinks(plan: DeployPlan, packageRoot: string): LinkAuditReport {
|
|
162
|
+
const audits: LinkAudit[] = [];
|
|
163
|
+
for (const [dest, source] of plan) {
|
|
164
|
+
if (!source.endsWith('/rules')) continue;
|
|
165
|
+
const dir = path.join(packageRoot, source);
|
|
166
|
+
if (!fs.existsSync(dir)) continue;
|
|
167
|
+
const files = fs
|
|
168
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
169
|
+
.filter((e) => !e.isDirectory() && e.name.endsWith('.md'))
|
|
170
|
+
.map((e) => e.name)
|
|
171
|
+
.sort();
|
|
172
|
+
for (const file of files) {
|
|
173
|
+
const rule = file.replace(/\.md$/, '');
|
|
174
|
+
const body = fs.readFileSync(path.join(dir, file), 'utf8');
|
|
175
|
+
for (const target of relativeLinkTargets(body)) {
|
|
176
|
+
audits.push(auditLink(rule, target, dest, plan, packageRoot));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const counts = EMPTY_COUNTS();
|
|
182
|
+
for (const a of audits) counts[a.verdict] += 1;
|
|
183
|
+
|
|
184
|
+
const grouped = new Map<string, { count: number; verdict: LinkVerdict }>();
|
|
185
|
+
for (const a of audits) {
|
|
186
|
+
if (a.verdict === 'resolved') continue;
|
|
187
|
+
const key = a.resolved_to === null ? a.target.replace(/[^/]*$/, '') : (a.resolved_to.split('/')[0] as string);
|
|
188
|
+
const row = grouped.get(key);
|
|
189
|
+
if (row) row.count += 1;
|
|
190
|
+
else grouped.set(key, { count: 1, verdict: a.verdict });
|
|
191
|
+
}
|
|
192
|
+
const by_directory = [...grouped.entries()]
|
|
193
|
+
.map(([directory, v]) => ({ directory, count: v.count, verdict: v.verdict }))
|
|
194
|
+
.sort((a, b) => b.count - a.count || a.directory.localeCompare(b.directory));
|
|
195
|
+
|
|
196
|
+
return { audits, counts, by_directory };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Standing-text cost of the rewrite option, in characters.
|
|
201
|
+
*
|
|
202
|
+
* Option A of this roadmap's step 1.2: leave the install plan alone and
|
|
203
|
+
* rewrite every link to an absolute package path. Each unresolved link then
|
|
204
|
+
* grows by the absolute prefix minus the `../` it replaces โ and it grows
|
|
205
|
+
* INSIDE a rule body, which is the text the host loads as instructions.
|
|
206
|
+
*
|
|
207
|
+
* `prefixChars` is the installed package root's own length, which is a
|
|
208
|
+
* property of the machine, so the caller supplies it and the report says which
|
|
209
|
+
* value it used.
|
|
210
|
+
*/
|
|
211
|
+
export function rewriteOptionCost(report: LinkAuditReport, prefixChars: number): number {
|
|
212
|
+
const unresolved = report.audits.filter((a) => a.verdict !== 'resolved');
|
|
213
|
+
let total = 0;
|
|
214
|
+
for (const a of unresolved) {
|
|
215
|
+
// `../x` and `../../x` both become `<abs>/x`: the climb is replaced.
|
|
216
|
+
const climb = (a.target.match(/\.\.\//g) ?? []).length * 3;
|
|
217
|
+
total += prefixChars + 1 - climb;
|
|
218
|
+
}
|
|
219
|
+
return total;
|
|
220
|
+
}
|
|
@@ -64,6 +64,30 @@ export const USER_SCOPE_PATHS: Readonly<Record<string, string>> = {
|
|
|
64
64
|
/**
|
|
65
65
|
* Universal Anthropic-shaped skill bundle deployed to markdown-skills
|
|
66
66
|
* tools. Mirrors `_CLAUDE_SKILL_BUNDLE` in the retired Python installer (ADR-200).
|
|
67
|
+
*
|
|
68
|
+
* NOT deploying `contexts/` and `guidelines/` here is a held decision, not an
|
|
69
|
+
* oversight โ see the `rule-link-targets-change-the-frozen-install-abi` blocker
|
|
70
|
+
* on `road-to-rule-triggers-and-links-that-hold`.
|
|
71
|
+
*
|
|
72
|
+
* A rule is authored against the package tree and projected verbatim, so
|
|
73
|
+
* `](../guidelines/x.md)` resolves in `dist/agent-src/rules/` and then does
|
|
74
|
+
* not resolve in the install, because this table does not carry those two
|
|
75
|
+
* directories. Measured by `report_installed_rule_links` on 2026-10-03: 136 of
|
|
76
|
+
* 523 links in an installed Claude rule directory point nowhere, 112 of them
|
|
77
|
+
* into exactly those two. Adding them is the cheaper of the two repairs on the
|
|
78
|
+
* budget that is scarce โ 0 standing characters against **5,152** for rewriting
|
|
79
|
+
* those same 112 links to absolute package paths, at a 48-character install
|
|
80
|
+
* prefix (the report prints the prefix it used; the figure scales with it).
|
|
81
|
+
*
|
|
82
|
+
* The totals were 160 of 550 when this was first measured and 158 of 545 on
|
|
83
|
+
* 2026-10-02; what fell since is the OTHER population โ repo-tree link forms no
|
|
84
|
+
* install ever held โ so the 112 this paragraph is about has not moved.
|
|
85
|
+
*
|
|
86
|
+
* It is held because this table is part of the FROZEN install ABI
|
|
87
|
+
* (`docs/contracts/install-layout.md`), and any change to it owes an
|
|
88
|
+
* `install_layout_version` bump plus a deprecation window: old and new shape
|
|
89
|
+
* side by side for a minor cycle, with in-place migration. That is a release
|
|
90
|
+
* commitment, not an implementation detail, so it is the owner's to make.
|
|
67
91
|
*/
|
|
68
92
|
const CLAUDE_SKILL_BUNDLE: ReadonlyArray<readonly [string, string]> = [
|
|
69
93
|
['dist/agent-src/rules', 'rules'],
|
|
@@ -134,6 +134,8 @@ import {
|
|
|
134
134
|
} from './detection_report.js';
|
|
135
135
|
import { review_gate_doctor_signal } from '../ai_team/review_gate.js';
|
|
136
136
|
import { resolvePackageRoot } from '../_lib/package_root.js';
|
|
137
|
+
import { rglob as _rglob_raw, scanForeign } from '../_lib/foreign_scan.js';
|
|
138
|
+
import { runCensus, summaryLine } from './cmd_doctor_neighbours.js';
|
|
137
139
|
import {
|
|
138
140
|
LEGACY_ALL,
|
|
139
141
|
excludedRuleBasenames,
|
|
@@ -617,77 +619,17 @@ function _collect_manifest_entries(
|
|
|
617
619
|
return [records, known];
|
|
618
620
|
}
|
|
619
621
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
Array.from(installed_tools.DEFAULT_DEPLOY_ROOTS);
|
|
632
|
-
const foreign: string[] = [];
|
|
633
|
-
const seen = new Set<string>();
|
|
634
|
-
for (const root_rel of roots) {
|
|
635
|
-
const root = _resolve_path(project_root, String(root_rel));
|
|
636
|
-
if (!pathExists(root) || !isDir(root)) {
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
for (const child of rglob(root)) {
|
|
640
|
-
if (!isFile(child)) {
|
|
641
|
-
continue;
|
|
642
|
-
}
|
|
643
|
-
let resolved: string;
|
|
644
|
-
try {
|
|
645
|
-
resolved = resolvePath(child);
|
|
646
|
-
} catch {
|
|
647
|
-
resolved = child;
|
|
648
|
-
}
|
|
649
|
-
if (known.has(resolved) || seen.has(resolved)) {
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
seen.add(resolved);
|
|
653
|
-
foreign.push(child);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
foreign.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
657
|
-
return foreign;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* `Path.rglob("*")` โ every descendant path (dirs + files), recursively.
|
|
662
|
-
* Order is not relied upon by callers (they sort the matched set), but the
|
|
663
|
-
* walk is deterministic (sorted per directory) to keep behaviour stable.
|
|
664
|
-
*/
|
|
665
|
-
function rglob(root: string): string[] {
|
|
666
|
-
const out: string[] = [];
|
|
667
|
-
const walk = (dir: string): void => {
|
|
668
|
-
let entries: fs.Dirent[];
|
|
669
|
-
try {
|
|
670
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
671
|
-
} catch {
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
675
|
-
for (const ent of entries) {
|
|
676
|
-
const full = path.join(dir, ent.name);
|
|
677
|
-
out.push(full);
|
|
678
|
-
let dirLike = ent.isDirectory();
|
|
679
|
-
if (ent.isSymbolicLink()) {
|
|
680
|
-
// pathlib rglob follows into symlinked dirs.
|
|
681
|
-
dirLike = isDir(full);
|
|
682
|
-
}
|
|
683
|
-
if (dirLike) {
|
|
684
|
-
walk(full);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
};
|
|
688
|
-
walk(root);
|
|
689
|
-
return out;
|
|
690
|
-
}
|
|
622
|
+
const rglob = (root: string): string[] => _rglob_raw(root, isDir, path.join);
|
|
623
|
+
const _scan_foreign = (project_root: string, manifest: Dict, known: Set<string>): string[] =>
|
|
624
|
+
scanForeign(project_root, manifest, known, {
|
|
625
|
+
pathExists,
|
|
626
|
+
isDir,
|
|
627
|
+
isFile,
|
|
628
|
+
resolvePath,
|
|
629
|
+
join: path.join,
|
|
630
|
+
resolveRoot: _resolve_path,
|
|
631
|
+
defaultRoots: installed_tools.DEFAULT_DEPLOY_ROOTS,
|
|
632
|
+
});
|
|
691
633
|
|
|
692
634
|
/**
|
|
693
635
|
* Split manifest records into missing / modified / tag-drift lists.
|
|
@@ -3028,6 +2970,11 @@ function _run_no_manifest(
|
|
|
3028
2970
|
_emit_checks_text(checks);
|
|
3029
2971
|
_emit_detection_text(project_root, checks);
|
|
3030
2972
|
}
|
|
2973
|
+
// Also on the no-manifest branch: a consumer with no project lockfile
|
|
2974
|
+
// still has neighbours, and a census that appeared only once a lockfile
|
|
2975
|
+
// existed would be missing on exactly the install shape most likely to
|
|
2976
|
+
// have other packages in it.
|
|
2977
|
+
if (opts.check === null && !opts.json) print(summaryLine(runCensus(project_root, _package_root())));
|
|
3031
2978
|
|
|
3032
2979
|
if (opts.check !== null) {
|
|
3033
2980
|
if (skipped_requested) {
|
|
@@ -3452,6 +3399,11 @@ function main(argv: string[] | null = null): number {
|
|
|
3452
3399
|
_emit_detection_text(project_root, checks);
|
|
3453
3400
|
if (opts.check === null) {
|
|
3454
3401
|
_emit_text(project_root, missing, modified, foreign, tag_drift);
|
|
3402
|
+
// One line, counts only โ the `foreign` listing above stays as it
|
|
3403
|
+
// is and nothing is removed from it. That listing walks THIS
|
|
3404
|
+
// package's deploy roots; the census walks where the hosts keep
|
|
3405
|
+
// things, which is a strictly larger question with its own verb.
|
|
3406
|
+
print(summaryLine(runCensus(project_root, _package_root())));
|
|
3455
3407
|
}
|
|
3456
3408
|
}
|
|
3457
3409
|
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent-config doctor neighbours` โ render the neighbour census.
|
|
3
|
+
*
|
|
4
|
+
* A separate entry point rather than another branch inside `cmd_doctor.ts`,
|
|
5
|
+
* which sits 2,000 lines above the source-size ceiling: a verb this size does
|
|
6
|
+
* not belong in a file that already cannot grow. The census itself is
|
|
7
|
+
* `_lib/neighbour_census.ts`; everything here is presentation and the one
|
|
8
|
+
* summary line the full `doctor` run links to.
|
|
9
|
+
*
|
|
10
|
+
* Refuses nothing. No label here removes, blocks, or rewrites a neighbour's
|
|
11
|
+
* artifact โ a label is a report, never a gate.
|
|
12
|
+
*
|
|
13
|
+
* It is no longer read-ONLY, and the two writes are named rather than implied.
|
|
14
|
+
* Both land under `agents/reports/` and both are caches of this run's own
|
|
15
|
+
* findings: `neighbour-scan.json`, the shape-scan verdicts the ranker reads on
|
|
16
|
+
* the hook path, and `neighbour-overlap.json`, the cross-root pairs keyed by the
|
|
17
|
+
* census digest. Nothing outside that directory is touched, and a failed write
|
|
18
|
+
* is not a failed census.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
|
+
|
|
25
|
+
import * as YAML from 'yaml';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
census,
|
|
29
|
+
fileDigest,
|
|
30
|
+
gatedEventsFrom,
|
|
31
|
+
scanCachePath,
|
|
32
|
+
type NeighbourCensus,
|
|
33
|
+
} from '../_lib/neighbour_census.js';
|
|
34
|
+
import { scanNeighbourBody, writeScanCache } from '../_lib/neighbour_scan.js';
|
|
35
|
+
import { resolvePackageRoot } from '../_lib/package_root.js';
|
|
36
|
+
import {
|
|
37
|
+
OVERLAP_THRESHOLD,
|
|
38
|
+
CROSS_CACHE_NAME,
|
|
39
|
+
cachedCrossRootPairs,
|
|
40
|
+
crossRootOverlapMap,
|
|
41
|
+
} from '../audit_skill_overlap.js';
|
|
42
|
+
|
|
43
|
+
interface ManifestShape {
|
|
44
|
+
concerns?: Record<string, { effect?: string }>;
|
|
45
|
+
platforms?: Record<string, Record<string, unknown>>;
|
|
46
|
+
native_event_aliases?: Record<string, Record<string, string>>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Read the hook manifest for the events a `permission` concern of ours sits on. */
|
|
50
|
+
function gatedEvents(packageRoot: string): Set<string> {
|
|
51
|
+
try {
|
|
52
|
+
// A missing or unreadable manifest yields an empty gate set, which
|
|
53
|
+
// downgrades `degraded` to `coordinated` rather than inventing a gate
|
|
54
|
+
// that is not there.
|
|
55
|
+
const file = path.join(packageRoot, 'src', 'scripts', 'hook_manifest.yaml');
|
|
56
|
+
const doc = YAML.parse(fs.readFileSync(file, 'utf-8')) as ManifestShape;
|
|
57
|
+
return gatedEventsFrom(doc, doc.native_event_aliases ?? {});
|
|
58
|
+
} catch {
|
|
59
|
+
return new Set<string>();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** This package's own skill names, for the `shadowed` warning. */
|
|
64
|
+
function ourSkillNames(packageRoot: string): Set<string> {
|
|
65
|
+
try {
|
|
66
|
+
return new Set(
|
|
67
|
+
fs
|
|
68
|
+
.readdirSync(path.join(packageRoot, 'src', 'skills'), { withFileTypes: true })
|
|
69
|
+
.filter((e) => e.isDirectory())
|
|
70
|
+
.map((e) => e.name),
|
|
71
|
+
);
|
|
72
|
+
} catch {
|
|
73
|
+
return new Set<string>();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The census, in two passes, and the second one is not an accident.
|
|
79
|
+
*
|
|
80
|
+
* The cross-root overlap can only be computed over the neighbour skills the
|
|
81
|
+
* census FOUND โ the unclaimed entries of two roots that also hold claimed ones,
|
|
82
|
+
* since `agent-config install` writes this package's own skills into
|
|
83
|
+
* `~/.claude/skills`. Pairing whole roots instead would compare our installed
|
|
84
|
+
* copies against our own source tree and publish the install as an overlap. So
|
|
85
|
+
* pass one finds them, the overlap runs over exactly those files, and pass two
|
|
86
|
+
* labels them.
|
|
87
|
+
*
|
|
88
|
+
* The shape scan is memoized across both passes, so the four linters run once
|
|
89
|
+
* per neighbour body rather than twice. The scan verdicts are then WRITTEN to
|
|
90
|
+
* the cache the ranker reads on the hook path โ this verb is the only producer,
|
|
91
|
+
* which is why its absence reads there as `no-scan-record` rather than as a pass.
|
|
92
|
+
*/
|
|
93
|
+
export function runCensus(
|
|
94
|
+
projectRoot: string,
|
|
95
|
+
packageRoot: string,
|
|
96
|
+
homeRoot?: string,
|
|
97
|
+
): NeighbourCensus {
|
|
98
|
+
const scanned = new Map<string, string | null>();
|
|
99
|
+
const scan = (p: string): string | null => {
|
|
100
|
+
const hit = scanned.get(p);
|
|
101
|
+
if (hit !== undefined) return hit;
|
|
102
|
+
const kind = scanNeighbourBody(p).kind;
|
|
103
|
+
scanned.set(p, kind);
|
|
104
|
+
return kind;
|
|
105
|
+
};
|
|
106
|
+
const base = {
|
|
107
|
+
gatedEvents: gatedEvents(packageRoot),
|
|
108
|
+
templatesRoot: path.join(packageRoot, 'src', 'agent-src', 'templates'),
|
|
109
|
+
ourSkillNames: ourSkillNames(packageRoot),
|
|
110
|
+
packageRoot,
|
|
111
|
+
scan,
|
|
112
|
+
...(homeRoot === undefined ? {} : { homeRoot }),
|
|
113
|
+
};
|
|
114
|
+
const first = census(projectRoot, base);
|
|
115
|
+
const ourRoot = path.join(packageRoot, 'src', 'skills');
|
|
116
|
+
let overlaps = new Map<string, { names: string[]; similarity: number }>();
|
|
117
|
+
if (first.skills.length > 0 && fs.existsSync(ourRoot)) {
|
|
118
|
+
try {
|
|
119
|
+
const { pairs } = cachedCrossRootPairs(
|
|
120
|
+
first.skills.map((s) => s.source),
|
|
121
|
+
ourRoot,
|
|
122
|
+
path.join(projectRoot, 'agents', 'reports', CROSS_CACHE_NAME),
|
|
123
|
+
OVERLAP_THRESHOLD,
|
|
124
|
+
);
|
|
125
|
+
overlaps = crossRootOverlapMap(pairs);
|
|
126
|
+
} catch {
|
|
127
|
+
// An overlap pass that cannot run leaves every entry unlabelled by
|
|
128
|
+
// it, which reads as `unclassified` โ the absence of a signal โ and
|
|
129
|
+
// never as a claim that the pair is disjoint.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const full = census(projectRoot, { ...base, overlaps });
|
|
133
|
+
writeScanCache(
|
|
134
|
+
scanCachePath(projectRoot),
|
|
135
|
+
// `fileDigest`, not the census's own `digest`: the ranker re-digests the
|
|
136
|
+
// file bytes it is about to index, and a cache written under a different
|
|
137
|
+
// digest function would read as `digest-changed` on every prompt.
|
|
138
|
+
full.skills.map((s) => ({
|
|
139
|
+
qualified: s.qualified,
|
|
140
|
+
digest: fileDigest(s.source) ?? '',
|
|
141
|
+
unscanned: s.unscanned,
|
|
142
|
+
})),
|
|
143
|
+
);
|
|
144
|
+
return full;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The one line the full `doctor` run prints, linking to this verb. */
|
|
148
|
+
export function summaryLine(c: NeighbourCensus): string {
|
|
149
|
+
const counts = [
|
|
150
|
+
`${c.hook_groups.length} hook group(s)`,
|
|
151
|
+
`${c.skills.length} skill(s)`,
|
|
152
|
+
`${c.commands.length} command(s)`,
|
|
153
|
+
`${c.agents.length} agent(s)`,
|
|
154
|
+
`${c.mcp_servers.length} mcp server(s)`,
|
|
155
|
+
`${c.rule_files.length} rule file(s)`,
|
|
156
|
+
`${c.instruction_sections.length} instruction section(s)`,
|
|
157
|
+
].join(', ');
|
|
158
|
+
const labels = c.hosts.map((h) => `${h.host}=${h.label}`).join(' ');
|
|
159
|
+
return ` ๐๏ธ neighbours: ${counts}${labels === '' ? '' : ` ยท ${labels}`} (run \`doctor neighbours\` for the census)`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function renderText(c: NeighbourCensus): string[] {
|
|
163
|
+
const out: string[] = [];
|
|
164
|
+
out.push(` ๐ project_root: ${c.project_root}`);
|
|
165
|
+
out.push('');
|
|
166
|
+
for (const h of c.hosts) {
|
|
167
|
+
out.push(` ${h.host}: ${h.label} โ ${h.label_reason}`);
|
|
168
|
+
for (const e of h.hook_groups) {
|
|
169
|
+
const matcher = e.matcher === null ? '' : ` matcher=${e.matcher}`;
|
|
170
|
+
const timeout = e.timeout === null ? '' : ` timeout=${e.timeout}`;
|
|
171
|
+
out.push(` ${e.event}: ${e.command_head}${matcher}${timeout} effect=${e.effect}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const section = (title: string, rows: ReadonlyArray<{ id: string }>): void => {
|
|
175
|
+
if (rows.length === 0) return;
|
|
176
|
+
out.push('');
|
|
177
|
+
out.push(` ${title} (${rows.length})`);
|
|
178
|
+
for (const r of rows) out.push(` ${r.id}`);
|
|
179
|
+
};
|
|
180
|
+
if (c.skills.length > 0) {
|
|
181
|
+
out.push('');
|
|
182
|
+
out.push(` skills (${c.skills.length})`);
|
|
183
|
+
for (const s of c.skills) {
|
|
184
|
+
const also = s.also.length === 0 ? '' : ` also: ${s.also.join(', ')}`;
|
|
185
|
+
const why = s.unscanned === null ? '' : ` unscanned: ${s.unscanned}`;
|
|
186
|
+
out.push(` ${s.qualified} compat: ${s.compat}${also}${why}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
section('commands', c.commands);
|
|
190
|
+
section('agents', c.agents);
|
|
191
|
+
if (c.mcp_servers.length > 0) {
|
|
192
|
+
out.push('');
|
|
193
|
+
out.push(` mcp servers (${c.mcp_servers.length})`);
|
|
194
|
+
for (const m of c.mcp_servers) {
|
|
195
|
+
// `used` and `advertised` are printed side by side so the reader
|
|
196
|
+
// never takes the first for a coverage ratio: the denominator is
|
|
197
|
+
// not missing by accident, it is not obtainable without launching
|
|
198
|
+
// the neighbour's process, which this census does not do.
|
|
199
|
+
out.push(
|
|
200
|
+
` ${m.id} tools used (${m.tools_window_days}d): ${m.tools_used_30d}` +
|
|
201
|
+
` advertised: ${m.tools_advertised}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
section('rule files', c.rule_files);
|
|
206
|
+
section('instruction sections', c.instruction_sections);
|
|
207
|
+
if (c.warnings.length > 0) {
|
|
208
|
+
out.push('');
|
|
209
|
+
for (const w of c.warnings) out.push(` โ ๏ธ ${w.kind}: ${w.detail}`);
|
|
210
|
+
}
|
|
211
|
+
out.push('');
|
|
212
|
+
out.push(
|
|
213
|
+
' The host owns load order. This census says what is there and what each entry would do;',
|
|
214
|
+
);
|
|
215
|
+
out.push(' it cannot say what ran first, and no label here refuses anything.');
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
220
|
+
let json = false;
|
|
221
|
+
let root = process.cwd();
|
|
222
|
+
let home: string | undefined;
|
|
223
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
224
|
+
if (argv[i] === '--json') json = true;
|
|
225
|
+
else if (argv[i] === '--project') {
|
|
226
|
+
root = argv[i + 1] ?? root;
|
|
227
|
+
i += 1;
|
|
228
|
+
} else if (argv[i] === '--home') {
|
|
229
|
+
// Fixture-only: the user-scope half of the census is real `$HOME`
|
|
230
|
+
// in production, and a test must be able to plant one.
|
|
231
|
+
home = argv[i + 1];
|
|
232
|
+
i += 1;
|
|
233
|
+
} else if (argv[i] === 'neighbours') {
|
|
234
|
+
continue;
|
|
235
|
+
} else {
|
|
236
|
+
process.stderr.write(`doctor neighbours: unknown argument: ${argv[i]}\n`);
|
|
237
|
+
return 2;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const c = runCensus(path.resolve(root), resolvePackageRoot(import.meta.url), home);
|
|
241
|
+
if (json) {
|
|
242
|
+
// Compact, not pretty-printed: this is the machine contract, and the
|
|
243
|
+
// gate that reads it matches on adjacency rather than on indentation.
|
|
244
|
+
process.stdout.write(`${JSON.stringify(c)}\n`);
|
|
245
|
+
} else {
|
|
246
|
+
for (const line of renderText(c)) process.stdout.write(`${line}\n`);
|
|
247
|
+
}
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Same entry guard the sibling CLIs use โ including the delegate branch, which
|
|
253
|
+
* an earlier version of this file left out and which the delegate smoke test
|
|
254
|
+
* caught: `no bundle is a silent no-op` reported `cmd_doctor_neighbours.js (no
|
|
255
|
+
* output, exit 0)`, the exact shape the paragraph below describes.
|
|
256
|
+
*
|
|
257
|
+
* Inside the cli-delegate bundle `--splitting` moves this module's body into a
|
|
258
|
+
* shared chunk, so the URL comparison weighs the CHUNK against `argv[1]` and
|
|
259
|
+
* never matches. `agent-config doctor` shipped that way once already โ a
|
|
260
|
+
* diagnostic reporting success while saying nothing. The invoked file name is
|
|
261
|
+
* the reliable signal there; a miss falls THROUGH to the realpath comparison
|
|
262
|
+
* rather than returning false, so a symlinked or renamed invocation still
|
|
263
|
+
* resolves. Inlined into the installer / hook / MCP bundle instead, it must
|
|
264
|
+
* never auto-run, which is why one "am I bundled" flag cannot answer "may I
|
|
265
|
+
* run" and both flags are read.
|
|
266
|
+
*/
|
|
267
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
268
|
+
declare const __AGENT_CONFIG_CLI_DELEGATE__: boolean | undefined;
|
|
269
|
+
|
|
270
|
+
function _isCliEntry(): boolean {
|
|
271
|
+
const bundled = typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
|
|
272
|
+
const cliDelegate =
|
|
273
|
+
typeof __AGENT_CONFIG_CLI_DELEGATE__ !== 'undefined' && __AGENT_CONFIG_CLI_DELEGATE__;
|
|
274
|
+
if (bundled && !cliDelegate) return false;
|
|
275
|
+
if (process.argv[1] === undefined) return false;
|
|
276
|
+
if (cliDelegate && path.basename(process.argv[1], '.js') === 'cmd_doctor_neighbours') {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
280
|
+
if (import.meta.url === argvUrl) return true;
|
|
281
|
+
try {
|
|
282
|
+
return (
|
|
283
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
284
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
285
|
+
);
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (_isCliEntry()) {
|
|
292
|
+
process.exit(main());
|
|
293
|
+
}
|