@event4u/agent-config 14.6.0 → 14.8.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +255 -145
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +264 -10
- package/dist/agent-src/commands/analyze.md +1 -1
- package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
- package/dist/agent-src/commands/evals/roadmap.json +17 -1
- package/dist/agent-src/commands/pr/create.md +14 -2
- package/dist/agent-src/commands/pr/merge.md +379 -0
- package/dist/agent-src/commands/roadmap/process-full.md +272 -32
- package/dist/agent-src/commands/roadmap.md +1 -1
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +95 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +289 -106
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/context-hygiene.md +2 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +67 -32
- package/dist/agent-src/rules/fix-what-you-see.md +108 -0
- package/dist/agent-src/rules/no-roadmap-references.md +0 -5
- package/dist/agent-src/rules/recurring-criticism.md +110 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +20 -1
- package/dist/agent-src/rules/rule-type-governance.md +0 -2
- package/dist/agent-src/rules/self-repair-loop.md +17 -0
- package/dist/agent-src/rules/skill-quality.md +0 -2
- package/dist/agent-src/rules/source-confidentiality.md +37 -7
- package/dist/agent-src/scripts/archival_sweep.ts +117 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- package/dist/agent-src/scripts/gate_budget.ts +282 -0
- package/dist/agent-src/scripts/gate_execute.ts +72 -31
- package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
- package/dist/agent-src/scripts/update_roadmap_progress.ts +296 -53
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
- package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
- package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
- package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
- package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
- package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
- package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
- package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
- package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
- package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
- package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
- package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
- package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
- package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
- package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
- package/dist/agent-src/skills/security/SKILL.md +8 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
- package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
- package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
- package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +65 -4
- package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/dist/cli/registry.js +2 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
- package/dist/cli-delegate/{chunk-P6LQDIWK.js → chunk-7PRE6ZV2.js} +338 -0
- package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
- package/dist/cli-delegate/chunk-E4XIZDIB.js +402 -0
- package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-EEJUMCDZ.js} +16 -16
- package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
- package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-H4VIVD4C.js} +385 -284
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-LPPWO5CE.js} +12 -8
- package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
- package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-PMEKGDED.js} +74 -167
- package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
- package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
- package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +466 -716
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +10 -10
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +10 -9
- package/dist/cli-delegate/cmd_handoff.js +10 -9
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +1 -1
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +11 -10
- package/dist/cli-delegate/cmd_route_audit.js +6 -5
- package/dist/cli-delegate/cmd_route_explain.js +3 -3
- package/dist/cli-delegate/cmd_session_recycle.js +840 -30
- package/dist/cli-delegate/cmd_settings_get.js +3 -3
- package/dist/cli-delegate/cmd_settings_migrate.js +10 -9
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +11 -10
- package/dist/cli-delegate/cmd_uninstall.js +13 -11
- package/dist/cli-delegate/cmd_update.js +4 -4
- package/dist/cli-delegate/cmd_upgrade.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +11 -10
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +111 -37
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +5 -5
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +15 -12
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +11 -8
- package/dist/hooks/dispatch.js +3165 -2192
- package/dist/install/claudePathsPlan.js +187 -0
- package/dist/install/claudePathsPlan.js.map +1 -0
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.js.map +1 -0
- package/dist/install/hostLayerFingerprint.js +136 -0
- package/dist/install/hostLayerFingerprint.js.map +1 -0
- package/dist/install/install.mjs +553 -425
- package/dist/install/partitionEligibility.js +432 -0
- package/dist/install/partitionEligibility.js.map +1 -0
- package/dist/install/ruleLayerPartition.js +157 -0
- package/dist/install/ruleLayerPartition.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1863 -1234
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_src.js +41 -0
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/scripts/_lib/install_layout.js +83 -0
- package/dist/scripts/_lib/install_layout.js.map +1 -0
- package/dist/scripts/_lib/installed_lock.js +444 -0
- package/dist/scripts/_lib/installed_lock.js.map +1 -0
- package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
- package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
- package/dist/scripts/condense.js +63 -251
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +4 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +12 -6
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
- package/docs/guidelines/agent-infra/false-green.md +47 -1
- package/docs/guidelines/agent-infra/memory-access.md +17 -0
- package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archival_sweep.ts +117 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- package/src/agent-src/scripts/gate_budget.ts +282 -0
- package/src/agent-src/scripts/gate_execute.ts +72 -31
- package/src/agent-src/scripts/roadmap_gates.ts +74 -3
- package/src/agent-src/scripts/update_roadmap_progress.ts +296 -53
- package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/src/config/agent-settings.template.yml +27 -2
- package/src/config/ci-local-parity.yml +39 -0
- package/src/config/estate-count-budget.json +612 -3
- package/src/config/evaluator-budgets.json +3 -3
- package/src/config/gate-coverage.yml +220 -12
- package/src/config/gate-violation-baselines.json +73 -63
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/rule-activation-census.json +14 -6
- package/src/config/rule-stub-ceilings.json +34 -7
- package/src/install/claudePathsPlan.ts +196 -0
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/hostLayerFingerprint.ts +145 -0
- package/src/install/partitionEligibility.ts +502 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
- package/src/scripts/_dispatch.bash +17 -1
- package/src/scripts/_lib/ac_heading.ts +30 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -0
- package/src/scripts/_lib/agent_src.ts +42 -0
- package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
- package/src/scripts/_lib/envelope_grounding.ts +49 -9
- package/src/scripts/_lib/installed_lock.ts +50 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/md_section.ts +129 -0
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- package/src/scripts/_lib/scope_guard.sh +26 -3
- package/src/scripts/_lib/self_repair.ts +19 -0
- package/src/scripts/_lib/self_repair_class_b.ts +171 -0
- package/src/scripts/adr/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +12 -32
- package/src/scripts/adr_cite_check.ts +540 -39
- package/src/scripts/ai_council/cli_help.ts +55 -0
- package/src/scripts/audit_adr_coverage.ts +103 -17
- package/src/scripts/before_complete_hook.ts +153 -28
- package/src/scripts/bench_ab_v2_complexity.ts +27 -7
- package/src/scripts/bench_hook_latency.ts +204 -0
- package/src/scripts/build_archive_index.ts +34 -0
- package/src/scripts/check_adr_frontmatter.ts +574 -39
- package/src/scripts/check_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -0
- package/src/scripts/check_references.ts +78 -0
- package/src/scripts/check_release_published.ts +32 -11
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_single_delivery.ts +194 -17
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/ci_settle.ts +170 -0
- package/src/scripts/condense.ts +79 -259
- package/src/scripts/council_cli.ts +3 -3
- package/src/scripts/dispatch_r2_reviewer.ts +6 -14
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +77 -14
- package/src/scripts/hooks/block_no_verify.ts +61 -10
- package/src/scripts/hooks/block_unauthorized_git.ts +21 -1
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +154 -187
- package/src/scripts/hooks/dispatch_issues.ts +38 -19
- package/src/scripts/hooks/fallback_yaml.ts +66 -0
- package/src/scripts/hooks/host_semantics.ts +21 -0
- package/src/scripts/hooks/injection_budget.ts +13 -3
- package/src/scripts/hooks/py_json_dumps.ts +103 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
- package/src/scripts/hooks/run_continuation_hook.ts +8 -5
- package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
- package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
- package/src/scripts/hooks/state_io.ts +870 -21
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
- package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
- package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
- package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
- package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
- package/src/scripts/injection_scan_hook.ts +137 -5
- package/src/scripts/install.ts +16 -11
- package/src/scripts/language_mirror_hook.ts +561 -25
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_hook_manifest.ts +49 -0
- package/src/scripts/lint_plan_risk_register.ts +4 -3
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- package/src/scripts/lint_skill_router_head.ts +4 -4
- package/src/scripts/mcp_server/tools.ts +2 -2
- package/src/scripts/new_roadmap.ts +136 -0
- package/src/scripts/pack_mcp_content.ts +20 -5
- package/src/scripts/release.ts +37 -38
- package/src/scripts/report_layer_overlap.ts +6 -0
- package/src/scripts/routing_doctor.ts +5 -1
- package/src/scripts/self_repair_hook.ts +37 -5
- package/src/scripts/session_register_hook.ts +11 -0
- package/src/scripts/skill_usage_collect.ts +11 -1
- package/src/scripts/skill_usage_report.ts +278 -8
- package/src/scripts/sync_pr_branch.ts +170 -20
- package/src/server/schemas/settings.ts +8 -0
- package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
- package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
- package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* matching git tag, and npm's `latest` therefore lags behind main.
|
|
14
14
|
*
|
|
15
15
|
* Two independent invariants:
|
|
16
|
-
* 1. Tag invariant
|
|
17
|
-
*
|
|
16
|
+
* 1. Tag invariant: the version in `package.json` MUST have a matching git
|
|
17
|
+
* tag ON THE REMOTE. A local-only tag published nothing — `publish-npm.yml`
|
|
18
|
+
* triggers on `push: tags:` — so the probe asks `origin` (one `ls-remote`).
|
|
18
19
|
* 2. npm invariant (`--check-npm`, network): `npm view <pkg>
|
|
19
20
|
* dist-tags.latest` MUST equal the `package.json` version.
|
|
20
21
|
*
|
|
@@ -51,13 +52,32 @@ function _package_name(): string {
|
|
|
51
52
|
return String(data['name']);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
/**
|
|
56
|
+
* True when the tag is PUBLISHED — present on `origin`, never merely local.
|
|
57
|
+
*
|
|
58
|
+
* This probe used to answer local-OR-remote, and that made the gate reply
|
|
59
|
+
* "tagged, therefore published" to the exact state it exists to detect. A tag
|
|
60
|
+
* created by `release.ts` step 8 whose push then failed sits in the local
|
|
61
|
+
* repository while nothing has shipped: `publish-npm.yml` triggers on
|
|
62
|
+
* `push: tags:`, so a tag that never reached the remote published nothing.
|
|
63
|
+
* Measured 2026-08-20 on 14.6.0 — main carried `package.json` 14.6.0, the tag
|
|
64
|
+
* existed only in the maintainer's checkout, and npm still served 14.5.0.
|
|
65
|
+
*
|
|
66
|
+
* In CI the old form happened to be harmless: `actions/checkout` brings no
|
|
67
|
+
* unpushed local tag, so the remote arm decided. The hole was LOCAL — a
|
|
68
|
+
* maintainer running `task check-release-published` (or `task preflight`) on
|
|
69
|
+
* the machine that minted the tag got a green for an unpublished release, on
|
|
70
|
+
* the one machine where the stuck state is visible. The sibling defect in
|
|
71
|
+
* `release.ts::_detect_in_flight_target` is the same construct.
|
|
72
|
+
*
|
|
73
|
+
* @param git Seam for the probe. Production passes nothing and gets the real
|
|
74
|
+
* `_git`; tests inject a stub, because the alternative — reaching the branches
|
|
75
|
+
* only by creating and deleting tags in the checkout — is why neither arm of
|
|
76
|
+
* this function had a test while both were wrong.
|
|
77
|
+
*/
|
|
78
|
+
function _tag_exists(tag: string, git: (...args: string[]) => [number, string] = _git): boolean {
|
|
79
|
+
const [rc] = git('ls-remote', '--exit-code', '--tags', 'origin', tag);
|
|
80
|
+
return rc === 0;
|
|
61
81
|
}
|
|
62
82
|
|
|
63
83
|
function _on_main(): boolean {
|
|
@@ -179,8 +199,9 @@ function main(argv: readonly string[] = [], hooks: Hooks = _hooks): number {
|
|
|
179
199
|
if (!hooks._tag_exists(version)) {
|
|
180
200
|
problems.push(
|
|
181
201
|
`package.json is ${version} but no git tag ${version} exists ` +
|
|
182
|
-
`
|
|
183
|
-
`
|
|
202
|
+
`on origin — the release was bumped/merged but never tagged, ` +
|
|
203
|
+
`or the tag push never landed. Complete it: tag the ` +
|
|
204
|
+
`release-merge commit and push ` +
|
|
184
205
|
`(triggers publish-npm.yml), e.g. \`git tag ${version} && git ` +
|
|
185
206
|
`push origin ${version}\`.`,
|
|
186
207
|
);
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* check_rule_layer_partition.ts — one rule, one layer, across ALL FIVE host rule
|
|
4
|
+
* trees, not only `.claude/`.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS GATE EXISTS (measured 2026-08-22)
|
|
7
|
+
* -----------------------------------------
|
|
8
|
+
* ADR-236 decided that a rule is delivered from exactly one layer: package-only
|
|
9
|
+
* rules stay in the project tree, everything else arrives from the host's global
|
|
10
|
+
* directory and is withheld from the project tree. Measured in a freshly
|
|
11
|
+
* generated worktree with `partitionActive: true`, it holds for one host in five:
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* .claude/rules 13 files, 13 package-only, 0 global-only
|
|
15
|
+
* .clinerules 14 files, 13 package-only, 0 global-only
|
|
16
|
+
* .cursor/rules 126 files, 26 package-only, 100 global-only
|
|
17
|
+
* .windsurf/rules 113 files, 13 package-only, 100 global-only
|
|
18
|
+
* .augment/rules 118 files, 15 package-only, 103 global-only
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* The two symlink trees are right because `generate_rule_symlinks` filters on
|
|
22
|
+
* `isExclusivelyPackageOnly`. The cursor-`.mdc`, windsurf and augment emitters
|
|
23
|
+
* never ask, so they write the full set regardless — into directories whose
|
|
24
|
+
* global layers were verified to already hold all 103.
|
|
25
|
+
*
|
|
26
|
+
* WHY THE EXISTING GATES DO NOT CATCH IT
|
|
27
|
+
* --------------------------------------
|
|
28
|
+
* - `check_single_delivery` hardcodes `~/.claude` and `<repo>/.claude`
|
|
29
|
+
* (`:446-447`), so the four other host directories are outside its corpus.
|
|
30
|
+
* Its own docstring records that in CI it compares nothing at all, because
|
|
31
|
+
* `.claude/` is gitignored and no CI leg installs at user scope.
|
|
32
|
+
* - `check_rule_projection_integrity` audits the trees against
|
|
33
|
+
* `projected_rule_trees()` — the generator's own emit plan. It answers "is
|
|
34
|
+
* the tree what the generator meant to write", which is COMPLETENESS. If the
|
|
35
|
+
* generator means to write a duplicate, that gate is green by construction.
|
|
36
|
+
* This gate asks the other question: is what the generator meant to write
|
|
37
|
+
* already delivered from the global layer.
|
|
38
|
+
*
|
|
39
|
+
* WHAT IT REFUSES TO DO
|
|
40
|
+
* ---------------------
|
|
41
|
+
* It never reports a duplicate for a host whose global layer cannot be read. A
|
|
42
|
+
* project copy is the ONLY carrier when the global one is absent, so demanding
|
|
43
|
+
* its removal on unread evidence would delete the rule from that host entirely —
|
|
44
|
+
* the failure `partitionEligibility`'s fail-safe ordering exists to prevent. Such
|
|
45
|
+
* a host is SKIPPED, by name, with its reason printed. A gate that scans nothing
|
|
46
|
+
* and exits green is indistinguishable from a gate that passed, so the skip is
|
|
47
|
+
* always visible.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import * as fs from 'node:fs';
|
|
51
|
+
import * as os from 'node:os';
|
|
52
|
+
import * as path from 'node:path';
|
|
53
|
+
|
|
54
|
+
import { isExclusivelyPackageOnly, resolvePartitionVerdict } from '../install/partitionEligibility.js';
|
|
55
|
+
import { PROJECT_RULE_DIRS, globalRuleLayerNames, globalRuleLayerPath } from '../install/globalRuleLayers.js';
|
|
56
|
+
import { GateLedger, UnaccountedTargetsError } from './_lib/gate_ledger.js';
|
|
57
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
58
|
+
import { runSelfTest } from './_lib/gate_self_test.js';
|
|
59
|
+
|
|
60
|
+
/** Re-exported so `check_rule_layer_partition`'s own importers keep working. */
|
|
61
|
+
export { PROJECT_RULE_DIRS };
|
|
62
|
+
|
|
63
|
+
const RULES_SOURCE = 'src/rules';
|
|
64
|
+
|
|
65
|
+
/** One project directory's verdict. */
|
|
66
|
+
export interface DirAudit {
|
|
67
|
+
readonly dir: string;
|
|
68
|
+
readonly toolId: string;
|
|
69
|
+
/** Present in the project tree, normalised to `.md`, deduplicated. */
|
|
70
|
+
readonly projected: readonly string[];
|
|
71
|
+
/** Of those, the ones that are package-only and belong here. */
|
|
72
|
+
readonly packageOnly: readonly string[];
|
|
73
|
+
/**
|
|
74
|
+
* Of those, the ones that are global-scope AND verified present in the host's
|
|
75
|
+
* global layer — i.e. genuinely delivered twice.
|
|
76
|
+
*/
|
|
77
|
+
readonly duplicated: readonly string[];
|
|
78
|
+
/**
|
|
79
|
+
* Global-scope names in the project tree that the global layer does NOT hold.
|
|
80
|
+
* Not a finding: the project copy is the only carrier, which is the state the
|
|
81
|
+
* partition must not break.
|
|
82
|
+
*/
|
|
83
|
+
readonly soleCarrier: readonly string[];
|
|
84
|
+
/** Null when the layer could not be read — the skip case. */
|
|
85
|
+
readonly globalLayer: string | null;
|
|
86
|
+
readonly globalCount: number | null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface PartitionAudit {
|
|
90
|
+
readonly dirs: readonly DirAudit[];
|
|
91
|
+
/** dir → why it was skipped, when it was. */
|
|
92
|
+
readonly skipped: Readonly<Record<string, string>>;
|
|
93
|
+
readonly ledger: GateLedger;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** `foo.mdc` → `foo.md`; everything else unchanged. */
|
|
97
|
+
function normalise(name: string): string {
|
|
98
|
+
return name.endsWith('.mdc') ? `${name.slice(0, -4)}.md` : name;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Classify every rule in `src/rules` once. Reading the SOURCE rather than each
|
|
103
|
+
* projected copy matters: `.mdc` files carry a rewritten frontmatter dialect
|
|
104
|
+
* (`description` / `globs` / `alwaysApply` only), so `workspaces:` is not present
|
|
105
|
+
* to read there and a per-copy classification would call every cursor rule
|
|
106
|
+
* global-scope.
|
|
107
|
+
*/
|
|
108
|
+
function classifySource(root: string): Map<string, boolean> {
|
|
109
|
+
const out = new Map<string, boolean>();
|
|
110
|
+
const dir = path.join(root, RULES_SOURCE);
|
|
111
|
+
let entries: string[];
|
|
112
|
+
try {
|
|
113
|
+
entries = fs.readdirSync(dir);
|
|
114
|
+
} catch {
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
for (const f of entries) {
|
|
118
|
+
if (!f.endsWith('.md')) continue;
|
|
119
|
+
out.set(f, isExclusivelyPackageOnly(path.join(dir, f)));
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function auditRuleLayers(
|
|
125
|
+
root: string,
|
|
126
|
+
home: string = process.env['HOME'] ?? os.homedir(),
|
|
127
|
+
): PartitionAudit {
|
|
128
|
+
const ledger = new GateLedger('check_rule_layer_partition');
|
|
129
|
+
const cls = classifySource(root);
|
|
130
|
+
const dirs: DirAudit[] = [];
|
|
131
|
+
const skipped: Record<string, string> = {};
|
|
132
|
+
|
|
133
|
+
ledger.plan(Object.keys(PROJECT_RULE_DIRS));
|
|
134
|
+
|
|
135
|
+
for (const [dir, toolId] of Object.entries(PROJECT_RULE_DIRS)) {
|
|
136
|
+
const abs = path.join(root, dir);
|
|
137
|
+
let names: string[];
|
|
138
|
+
try {
|
|
139
|
+
names = fs.readdirSync(abs);
|
|
140
|
+
} catch {
|
|
141
|
+
// No project tree for this host — nothing can be duplicated from it.
|
|
142
|
+
ledger.skip(dir, 'no_applicable_files');
|
|
143
|
+
skipped[dir] = 'project directory absent';
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const projected = [
|
|
147
|
+
...new Set(
|
|
148
|
+
names
|
|
149
|
+
.filter((f) => f.endsWith('.md') || f.endsWith('.mdc'))
|
|
150
|
+
.map(normalise)
|
|
151
|
+
.filter((f) => cls.has(f)),
|
|
152
|
+
),
|
|
153
|
+
].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
154
|
+
|
|
155
|
+
const globalNames = globalRuleLayerNames(toolId, home);
|
|
156
|
+
if (globalNames === null) {
|
|
157
|
+
ledger.skip(dir, 'precondition_unmet');
|
|
158
|
+
skipped[dir] =
|
|
159
|
+
`no readable global layer at ${globalRuleLayerPath(toolId, home) ?? '(none for this host)'}` +
|
|
160
|
+
` — the project copies are the only carrier, so nothing is reported`;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const globalSet = new Set(globalNames);
|
|
164
|
+
|
|
165
|
+
const packageOnly = projected.filter((f) => cls.get(f) === true);
|
|
166
|
+
const globalScope = projected.filter((f) => cls.get(f) === false);
|
|
167
|
+
const duplicated = globalScope.filter((f) => globalSet.has(f));
|
|
168
|
+
const soleCarrier = globalScope.filter((f) => !globalSet.has(f));
|
|
169
|
+
|
|
170
|
+
dirs.push({
|
|
171
|
+
dir,
|
|
172
|
+
toolId,
|
|
173
|
+
projected,
|
|
174
|
+
packageOnly,
|
|
175
|
+
duplicated,
|
|
176
|
+
soleCarrier,
|
|
177
|
+
globalLayer: globalRuleLayerPath(toolId, home),
|
|
178
|
+
globalCount: globalNames.length,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (duplicated.length > 0) {
|
|
182
|
+
ledger.fail(dir, `${String(duplicated.length)} rules delivered from both layers`);
|
|
183
|
+
} else {
|
|
184
|
+
ledger.complete(dir);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { dirs, skipped, ledger };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The measurement table — printed in both modes, because it is the point. */
|
|
192
|
+
export function renderTable(audit: PartitionAudit): string[] {
|
|
193
|
+
const lines: string[] = [
|
|
194
|
+
'project directory tool files package-only DUPLICATED sole-carrier global',
|
|
195
|
+
];
|
|
196
|
+
for (const d of audit.dirs) {
|
|
197
|
+
lines.push(
|
|
198
|
+
d.dir.padEnd(22) +
|
|
199
|
+
d.toolId.padEnd(13) +
|
|
200
|
+
String(d.projected.length).padStart(6) +
|
|
201
|
+
String(d.packageOnly.length).padStart(14) +
|
|
202
|
+
String(d.duplicated.length).padStart(12) +
|
|
203
|
+
String(d.soleCarrier.length).padStart(14) +
|
|
204
|
+
String(d.globalCount ?? 0).padStart(8),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
for (const [dir, why] of Object.entries(audit.skipped)) {
|
|
208
|
+
lines.push(`${dir.padEnd(22)}skipped: ${why}`);
|
|
209
|
+
}
|
|
210
|
+
return lines;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* `--self-test`: does this gate DISCRIMINATE?
|
|
215
|
+
*
|
|
216
|
+
* An enforced `scanned:` floor proves the gate read something; only this proves the
|
|
217
|
+
* reading changes the verdict. Each case builds a synthetic project root and home,
|
|
218
|
+
* so the suite is hermetic and never touches the live tree — which matters here
|
|
219
|
+
* because the directories under audit are gitignored and a leaked fixture would sit
|
|
220
|
+
* in one unnoticed.
|
|
221
|
+
*
|
|
222
|
+
* Three rejecting cases, deliberately more than the floor of one: the three ways
|
|
223
|
+
* this gate can be wrong are a duplicate it fails to see, a sole-carrier it
|
|
224
|
+
* mistakes for a duplicate, and a missing global layer it reports on anyway. The
|
|
225
|
+
* middle and last are the ones that would DELETE a rule from a host.
|
|
226
|
+
*/
|
|
227
|
+
function selfTest(): number {
|
|
228
|
+
const seed = (opts: {
|
|
229
|
+
readonly projectHas: readonly string[];
|
|
230
|
+
readonly globalHas: readonly string[];
|
|
231
|
+
}): { root: string; home: string } => {
|
|
232
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'crlp-st-root-'));
|
|
233
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'crlp-st-home-'));
|
|
234
|
+
const src = path.join(root, RULES_SOURCE);
|
|
235
|
+
fs.mkdirSync(src, { recursive: true });
|
|
236
|
+
fs.writeFileSync(
|
|
237
|
+
path.join(src, 'pkg-only.md'),
|
|
238
|
+
'---\nworkspaces:\n - agent-config-maintainer\n---\n# pkg\n',
|
|
239
|
+
);
|
|
240
|
+
fs.writeFileSync(path.join(src, 'global-scope.md'), '---\ntier: 1\n---\n# global\n');
|
|
241
|
+
const proj = path.join(root, '.cursor/rules');
|
|
242
|
+
fs.mkdirSync(proj, { recursive: true });
|
|
243
|
+
for (const n of opts.projectHas) fs.writeFileSync(path.join(proj, n), '# x\n');
|
|
244
|
+
if (opts.globalHas.length > 0) {
|
|
245
|
+
const gdir = path.join(home, '.cursor/rules');
|
|
246
|
+
fs.mkdirSync(gdir, { recursive: true });
|
|
247
|
+
for (const n of opts.globalHas) fs.writeFileSync(path.join(gdir, n), '# x\n');
|
|
248
|
+
}
|
|
249
|
+
return { root, home };
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/** 1 when the audit would fail the build, 0 otherwise. */
|
|
253
|
+
const verdict = (root: string, home: string): number => {
|
|
254
|
+
const a = auditRuleLayers(root, home);
|
|
255
|
+
return a.dirs.some((d) => d.duplicated.length > 0) ? 1 : 0;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return runSelfTest({
|
|
259
|
+
gate: 'check_rule_layer_partition',
|
|
260
|
+
minCases: 4,
|
|
261
|
+
minRejectCases: 3,
|
|
262
|
+
cases: [
|
|
263
|
+
{
|
|
264
|
+
name: 'a global-scope rule in both layers is refused',
|
|
265
|
+
expect: 'reject',
|
|
266
|
+
run: () => {
|
|
267
|
+
const { root, home } = seed({
|
|
268
|
+
projectHas: ['pkg-only.md', 'global-scope.md'],
|
|
269
|
+
globalHas: ['global-scope.md'],
|
|
270
|
+
});
|
|
271
|
+
return verdict(root, home);
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: 'a .mdc duplicate is refused too (normalisation is not a bypass)',
|
|
276
|
+
expect: 'reject',
|
|
277
|
+
run: () => {
|
|
278
|
+
const { root, home } = seed({
|
|
279
|
+
projectHas: ['global-scope.mdc'],
|
|
280
|
+
globalHas: ['global-scope.md'],
|
|
281
|
+
});
|
|
282
|
+
return verdict(root, home);
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: 'a duplicate under a .mdc global layer is refused',
|
|
287
|
+
expect: 'reject',
|
|
288
|
+
run: () => {
|
|
289
|
+
const { root, home } = seed({
|
|
290
|
+
projectHas: ['global-scope.md'],
|
|
291
|
+
globalHas: ['global-scope.mdc'],
|
|
292
|
+
});
|
|
293
|
+
return verdict(root, home);
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: 'package-only in the project layer is accepted',
|
|
298
|
+
expect: 'accept',
|
|
299
|
+
run: () => {
|
|
300
|
+
const { root, home } = seed({
|
|
301
|
+
projectHas: ['pkg-only.md'],
|
|
302
|
+
globalHas: ['global-scope.md'],
|
|
303
|
+
});
|
|
304
|
+
return verdict(root, home);
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: 'a sole-carrier is accepted — removing it would delete the rule',
|
|
309
|
+
expect: 'accept',
|
|
310
|
+
run: () => {
|
|
311
|
+
const { root, home } = seed({
|
|
312
|
+
projectHas: ['global-scope.md'],
|
|
313
|
+
globalHas: ['something-else.md'],
|
|
314
|
+
});
|
|
315
|
+
return verdict(root, home);
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: 'no global layer at all is accepted, never reported on',
|
|
320
|
+
expect: 'accept',
|
|
321
|
+
run: () => {
|
|
322
|
+
const { root, home } = seed({
|
|
323
|
+
projectHas: ['global-scope.md'],
|
|
324
|
+
globalHas: [],
|
|
325
|
+
});
|
|
326
|
+
return verdict(root, home);
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* `--prune`: delete exactly the files this gate reports as duplicated.
|
|
335
|
+
*
|
|
336
|
+
* ## Why a gate ships a repair at all
|
|
337
|
+
*
|
|
338
|
+
* The documented repair is `task generate-tools`, and on this repository's own main
|
|
339
|
+
* checkout that command is **structurally inert**: `agents/.agent-tools.yml` is
|
|
340
|
+
* `skip-worktree`-masked to `tools: []`, so `_filter_tool_dirs` resolves to zero
|
|
341
|
+
* directories and the generator writes nothing AND sweeps nothing. Measured
|
|
342
|
+
* 2026-08-22: a full `task generate-tools` there emitted `rules=0` and left all 77
|
|
343
|
+
* stale symlinks in `.claude/rules` exactly where they were — dated 2026-07-30,
|
|
344
|
+
* i.e. from before the partition existed. That is why those 77 survived every
|
|
345
|
+
* regeneration for three weeks, and why "just regenerate" is not an answer for
|
|
346
|
+
* anyone whose tool selection is masked or narrow.
|
|
347
|
+
*
|
|
348
|
+
* So the sweep is keyed on the gate's own finding rather than on a generator run:
|
|
349
|
+
* a file is removed only when it is in a project rule directory, classifies as
|
|
350
|
+
* global-scope, and THAT host's global layer was read and verified to hold it. The
|
|
351
|
+
* `sole-carrier` column exists precisely to keep this honest — a rule the global
|
|
352
|
+
* layer does not have is never touched, in any mode.
|
|
353
|
+
*
|
|
354
|
+
* It is opt-in and prints every path. A gate that deletes by default is a gate
|
|
355
|
+
* nobody dares run.
|
|
356
|
+
*/
|
|
357
|
+
function prune(audit: PartitionAudit, root: string): number {
|
|
358
|
+
let removed = 0;
|
|
359
|
+
for (const d of audit.dirs) {
|
|
360
|
+
for (const name of d.duplicated) {
|
|
361
|
+
// Both spellings: the symlink trees write `.md`, the cursor emitter
|
|
362
|
+
// `.mdc`, and `.cursor/rules` holds both for the same rule.
|
|
363
|
+
for (const candidate of [name, `${name.slice(0, -3)}.mdc`]) {
|
|
364
|
+
const abs = path.join(root, d.dir, candidate);
|
|
365
|
+
try {
|
|
366
|
+
fs.lstatSync(abs);
|
|
367
|
+
} catch {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
fs.unlinkSync(abs);
|
|
371
|
+
process.stdout.write(` removed ${d.dir}/${candidate}\n`);
|
|
372
|
+
removed += 1;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
process.stdout.write(
|
|
377
|
+
`✅ pruned ${String(removed)} duplicated rule file(s) — each one verified present ` +
|
|
378
|
+
`in its host's global layer first\n`,
|
|
379
|
+
);
|
|
380
|
+
return removed;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Does a duplication in this projection mode mean an emitter FAILED, or that the
|
|
385
|
+
* partition deliberately did not run?
|
|
386
|
+
*
|
|
387
|
+
* Split out of `main` so both directions are testable without a lockfile on disk:
|
|
388
|
+
* the un-injected path can only assert whatever the machine happens to be, and this
|
|
389
|
+
* one bit differs between a maintainer checkout mid-release and a partitioned one —
|
|
390
|
+
* the same environment-dependence `ruleLayerPartition.ts` records for its own
|
|
391
|
+
* `active` override.
|
|
392
|
+
*/
|
|
393
|
+
export function partitionEnforces(mode: string): boolean {
|
|
394
|
+
return mode === 'dual-layer/partitioned';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
398
|
+
if (argv.includes('--self-test')) {
|
|
399
|
+
return selfTest();
|
|
400
|
+
}
|
|
401
|
+
const report = argv.includes('--report');
|
|
402
|
+
const doPrune = argv.includes('--prune');
|
|
403
|
+
const root = process.cwd();
|
|
404
|
+
const audit = auditRuleLayers(root);
|
|
405
|
+
|
|
406
|
+
for (const l of renderTable(audit)) process.stdout.write(`${l}\n`);
|
|
407
|
+
|
|
408
|
+
// Scope hardening. The count is the number of directory PAIRS actually
|
|
409
|
+
// compared, not the five planned — otherwise a run that skipped everything
|
|
410
|
+
// would publish `scanned: 5` and the number would describe an intention rather
|
|
411
|
+
// than a reading.
|
|
412
|
+
//
|
|
413
|
+
// `allowEmpty` is load-bearing rather than a formality: on a fresh CI checkout
|
|
414
|
+
// both sides are legitimately absent (the project trees are gitignored, no CI
|
|
415
|
+
// leg installs at user scope), so zero is the correct reading there and the
|
|
416
|
+
// skip lines above already name every host it applies to.
|
|
417
|
+
reportScanned({
|
|
418
|
+
gate: 'check_rule_layer_partition',
|
|
419
|
+
scanned: audit.dirs.length,
|
|
420
|
+
units: 'host rule directory pairs',
|
|
421
|
+
roots: Object.keys(PROJECT_RULE_DIRS),
|
|
422
|
+
allowEmpty:
|
|
423
|
+
'both layers are absent on a fresh checkout — the project rule trees are ' +
|
|
424
|
+
'gitignored and no CI leg installs at user scope, so there is no pair to compare. ' +
|
|
425
|
+
'The run prints "nothing compared … This is not a pass" instead of a success line.',
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
let tally;
|
|
429
|
+
try {
|
|
430
|
+
tally = audit.ledger.report();
|
|
431
|
+
} catch (exc) {
|
|
432
|
+
if (exc instanceof UnaccountedTargetsError) {
|
|
433
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
434
|
+
return 1;
|
|
435
|
+
}
|
|
436
|
+
throw exc;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const offenders = audit.dirs.filter((d) => d.duplicated.length > 0);
|
|
440
|
+
if (offenders.length === 0) {
|
|
441
|
+
const skips = Object.keys(audit.skipped).length;
|
|
442
|
+
if (tally.completed === 0) {
|
|
443
|
+
// Measured nothing. Saying "each rule is delivered from one layer" here
|
|
444
|
+
// would be the exact failure this gate's docstring names: a gate that
|
|
445
|
+
// scans nothing and exits green reads identically to one that passed.
|
|
446
|
+
// The skip lines above already say which hosts and why; this line has to
|
|
447
|
+
// agree with them rather than contradict them in the same output.
|
|
448
|
+
process.stdout.write(
|
|
449
|
+
`⚠️ check_rule_layer_partition: nothing compared — ${String(skips)} host ` +
|
|
450
|
+
`directory/directories skipped (see the reasons above). This is not a pass.\n`,
|
|
451
|
+
);
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
process.stdout.write(
|
|
455
|
+
`✅ check_rule_layer_partition: ${String(tally.completed)} host rule tree(s) deliver ` +
|
|
456
|
+
`each rule from one layer` +
|
|
457
|
+
(skips > 0 ? `; ${String(skips)} skipped (reasons above)` : '') +
|
|
458
|
+
`\n`,
|
|
459
|
+
);
|
|
460
|
+
return 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (doPrune) {
|
|
464
|
+
prune(audit, root);
|
|
465
|
+
// Re-audit rather than trusting the removal: the point of the mode is to
|
|
466
|
+
// reach zero, and asserting it from the same read that planned the deletion
|
|
467
|
+
// would prove only that the plan was executed.
|
|
468
|
+
const after = auditRuleLayers(root);
|
|
469
|
+
const left = after.dirs.reduce((n, d) => n + d.duplicated.length, 0);
|
|
470
|
+
if (left > 0) {
|
|
471
|
+
process.stderr.write(`❌ ${String(left)} duplicate(s) still present after prune\n`);
|
|
472
|
+
return 1;
|
|
473
|
+
}
|
|
474
|
+
process.stdout.write('✅ re-audit after prune: 0 duplicated\n');
|
|
475
|
+
return 0;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// A duplication the PARTITION ITSELF declined to remove is not a defect — it is
|
|
479
|
+
// the documented fail-safe, and hard-failing on it makes this gate contradict
|
|
480
|
+
// its own sibling. `check_single_delivery` reads the same fact and reports it:
|
|
481
|
+
// "a machine whose install predates the fingerprint keeps the full projection
|
|
482
|
+
// BY DESIGN". This gate refused instead, and the two verdicts on one condition
|
|
483
|
+
// are how a release push got blocked on 2026-08-22 with no reachable repair —
|
|
484
|
+
// `task generate-tools` re-emits exactly the files it demanded be gone, so the
|
|
485
|
+
// partition gate and `check_bridge_derivation` deadlock, each red in the state
|
|
486
|
+
// the other requires.
|
|
487
|
+
//
|
|
488
|
+
// The block is structural rather than incidental: during ANY release the
|
|
489
|
+
// building version is ahead of the installed one, so `resolvePartitionVerdict`
|
|
490
|
+
// returns `standalone/full` on every maintainer machine for the whole release
|
|
491
|
+
// window. A gate that cannot be green while a release is in progress is not
|
|
492
|
+
// measuring the release.
|
|
493
|
+
//
|
|
494
|
+
// Teeth are unchanged where the partition IS active: there a duplicate means an
|
|
495
|
+
// emitter failed to withhold what the partition selected, which is the defect
|
|
496
|
+
// this gate exists to catch. In CI both layers are absent, `offenders` is empty,
|
|
497
|
+
// and this branch is never reached — so nothing here relaxes the CI reading.
|
|
498
|
+
const verdict = resolvePartitionVerdict(root);
|
|
499
|
+
if (!partitionEnforces(verdict.mode)) {
|
|
500
|
+
for (const d of offenders) {
|
|
501
|
+
process.stdout.write(
|
|
502
|
+
`⚠️ ${d.dir}: ${String(d.duplicated.length)} rule(s) also present in ` +
|
|
503
|
+
`${d.globalLayer ?? '(unknown)'}\n`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
process.stdout.write(
|
|
507
|
+
`⚠️ check_rule_layer_partition: partition INACTIVE (${verdict.reason}), so the ` +
|
|
508
|
+
`generators emit the full projection by design and this overlap is the fail-safe ` +
|
|
509
|
+
`working. Reported, not enforced — run \`agent-config install\` to activate the ` +
|
|
510
|
+
`partition here, then this gate has teeth again.\n`,
|
|
511
|
+
);
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
for (const d of offenders) {
|
|
516
|
+
process.stderr.write(
|
|
517
|
+
`${report ? '⚠️ ' : '❌ '} ${d.dir}: ${String(d.duplicated.length)} rule(s) also present in ` +
|
|
518
|
+
`${d.globalLayer ?? '(unknown)'} — e.g. ${d.duplicated.slice(0, 5).join(', ')}\n`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
process.stderr.write(
|
|
522
|
+
`\nA rule in both layers is loaded twice: the host reads the global directory AND ` +
|
|
523
|
+
`the project one, with no dedup (ADR-236). Regenerate with \`task generate-tools\` ` +
|
|
524
|
+
`after the emitter for that directory consults the partition.\n`,
|
|
525
|
+
);
|
|
526
|
+
return report ? 0 : 1;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (process.argv[1] !== undefined && process.argv[1].endsWith('check_rule_layer_partition.ts')) {
|
|
530
|
+
process.exit(main());
|
|
531
|
+
}
|