@event4u/agent-config 9.27.0 → 9.28.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.
Files changed (138) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +12 -11
  3. package/CHANGELOG.md +99 -0
  4. package/README.md +1 -1
  5. package/dist/agent-src/commands/analyze/conformance.md +24 -0
  6. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +10 -3
  7. package/dist/agent-src/rules/artifact-engagement-recording.md +4 -1
  8. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  9. package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
  10. package/dist/agent-src/rules/context-hygiene.md +3 -1
  11. package/dist/agent-src/rules/council-availability.md +93 -0
  12. package/dist/agent-src/rules/cross-source-consistency.md +3 -0
  13. package/dist/agent-src/rules/delegation-policy.md +29 -5
  14. package/dist/agent-src/rules/design-fidelity.md +3 -0
  15. package/dist/agent-src/rules/evaluator-independence.md +2 -1
  16. package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
  17. package/dist/agent-src/rules/finance-safety-floor.md +1 -1
  18. package/dist/agent-src/rules/git-history-discipline.md +1 -1
  19. package/dist/agent-src/rules/history-discipline.md +3 -1
  20. package/dist/agent-src/rules/legal-safety-floor.md +1 -1
  21. package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
  22. package/dist/agent-src/rules/onboarding-gate.md +3 -1
  23. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
  24. package/dist/agent-src/rules/role-mode-adherence.md +1 -1
  25. package/dist/agent-src/rules/scale-discipline.md +3 -1
  26. package/dist/agent-src/rules/self-repair-loop.md +103 -0
  27. package/dist/agent-src/rules/session-canary.md +2 -1
  28. package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
  29. package/dist/agent-src/rules/token-budget-discipline.md +3 -1
  30. package/dist/agent-src/rules/tool-safety.md +5 -1
  31. package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
  32. package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  33. package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
  34. package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
  35. package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
  36. package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
  37. package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
  38. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  39. package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
  40. package/dist/agent-src/templates/AGENTS.md +6 -10
  41. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  42. package/dist/cli/registry.js +7 -0
  43. package/dist/cli/registry.js.map +1 -1
  44. package/dist/cli-delegate/chunk-22GCIJ2O.js +270 -0
  45. package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-2BE5I2MN.js} +8 -8
  46. package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-7OMQCH4V.js} +1 -1
  47. package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-GB2FSUEE.js} +57 -7
  48. package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-JXARCGVN.js} +3 -3
  49. package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-PQSAQKHW.js} +466 -304
  50. package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
  51. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  52. package/dist/cli-delegate/cmd_brand_status.js +134 -0
  53. package/dist/cli-delegate/cmd_conformance.js +7 -7
  54. package/dist/cli-delegate/cmd_converge.js +3 -3
  55. package/dist/cli-delegate/cmd_doctor.js +4 -4
  56. package/dist/cli-delegate/cmd_explain.js +80 -330
  57. package/dist/cli-delegate/cmd_export.js +5 -5
  58. package/dist/cli-delegate/cmd_fleet.js +3 -3
  59. package/dist/cli-delegate/cmd_handoff.js +2 -2
  60. package/dist/cli-delegate/cmd_mcp_available.js +433 -0
  61. package/dist/cli-delegate/cmd_migrate.js +3 -3
  62. package/dist/cli-delegate/cmd_packs_active.js +153 -0
  63. package/dist/cli-delegate/cmd_preflight.js +1 -1
  64. package/dist/cli-delegate/cmd_prune.js +5 -5
  65. package/dist/cli-delegate/cmd_route_audit.js +8 -8
  66. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  67. package/dist/cli-delegate/cmd_settings_get.js +272 -0
  68. package/dist/cli-delegate/cmd_settings_migrate.js +5 -5
  69. package/dist/cli-delegate/cmd_settings_set.js +7 -2967
  70. package/dist/cli-delegate/cmd_sync.js +5 -5
  71. package/dist/cli-delegate/cmd_uninstall.js +5 -5
  72. package/dist/cli-delegate/cmd_update.js +4 -4
  73. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  74. package/dist/cli-delegate/cmd_validate.js +5 -5
  75. package/dist/cli-delegate/cmd_versions.js +4 -4
  76. package/dist/discovery/deprecation-report.md +1 -1
  77. package/dist/discovery/discovery-manifest.json +100 -40
  78. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  79. package/dist/discovery/discovery-manifest.summary.md +3 -3
  80. package/dist/discovery/orphan-report.md +1 -1
  81. package/dist/discovery/packs.json +7 -5
  82. package/dist/discovery/trust-report.md +11 -11
  83. package/dist/discovery/workspaces.json +6 -4
  84. package/dist/hooks/dispatch.js +9004 -6132
  85. package/dist/install/install.mjs +697 -507
  86. package/dist/mcp/registry-manifest.json +3 -3
  87. package/dist/mcp/server.json +2 -2
  88. package/dist/mcp/server.mjs +127 -20
  89. package/dist/router.json +1 -1
  90. package/dist/scripts/_lib/agent_settings.js +80 -15
  91. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  92. package/docs/contracts/settings-classes.md +29 -8
  93. package/docs/guidelines/agent-infra/memory-access.md +14 -3
  94. package/package.json +1 -1
  95. package/src/agent-src/templates/AGENTS.md +6 -10
  96. package/src/config/budgets.yml +29 -0
  97. package/src/config/evaluator-budgets.json +4 -4
  98. package/src/config/gate-coverage.yml +26 -0
  99. package/src/scripts/_cli/cmd_brand_status.ts +174 -0
  100. package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
  101. package/src/scripts/_cli/cmd_packs_active.ts +225 -0
  102. package/src/scripts/_cli/cmd_settings_get.ts +324 -0
  103. package/src/scripts/_dispatch.bash +57 -0
  104. package/src/scripts/_lib/agent_settings.ts +82 -16
  105. package/src/scripts/_lib/carrier_divergence.ts +135 -0
  106. package/src/scripts/_lib/host_capability.ts +84 -1
  107. package/src/scripts/_lib/prompt_shape.ts +88 -0
  108. package/src/scripts/_lib/review_skipped_record.ts +102 -0
  109. package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
  110. package/src/scripts/_lib/self_repair.ts +342 -0
  111. package/src/scripts/_lib/self_repair_store.ts +100 -0
  112. package/src/scripts/_lib/transcript_entry.ts +80 -0
  113. package/src/scripts/check_branch_freshness.ts +255 -0
  114. package/src/scripts/check_council_config_location.ts +125 -1
  115. package/src/scripts/check_standing_rule_delivery.ts +314 -0
  116. package/src/scripts/config/profiles.ts +13 -3
  117. package/src/scripts/conformance_scan.ts +272 -24
  118. package/src/scripts/council_availability_hook.ts +188 -0
  119. package/src/scripts/council_cli.ts +126 -7
  120. package/src/scripts/hook_manifest.yaml +107 -26
  121. package/src/scripts/hooks/block_no_verify.ts +39 -2
  122. package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
  123. package/src/scripts/hooks/concern_registry.ts +10 -0
  124. package/src/scripts/hooks/delegation_nudge_hook.ts +450 -0
  125. package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
  126. package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
  127. package/src/scripts/install.ts +151 -0
  128. package/src/scripts/language_mirror_hook.ts +30 -37
  129. package/src/scripts/measure_scope_dedup.ts +19 -13
  130. package/src/scripts/onboarding_gate_hook.ts +40 -1
  131. package/src/scripts/orchestration_record.ts +4 -1
  132. package/src/scripts/preamble_byte_census.ts +9 -20
  133. package/src/scripts/report_carrier_divergence.ts +364 -0
  134. package/src/scripts/report_skill_activation.ts +257 -0
  135. package/src/scripts/report_skill_obligation_violations.ts +579 -0
  136. package/src/scripts/routing_doctor.ts +77 -6
  137. package/src/scripts/self_repair_cli.ts +240 -0
  138. package/src/scripts/self_repair_hook.ts +241 -0
@@ -25,6 +25,7 @@ import * as fs from 'node:fs';
25
25
  import * as os from 'node:os';
26
26
  import * as path from 'node:path';
27
27
 
28
+ import { comparePair } from './_lib/carrier_divergence.js';
28
29
  import { censusRuleDir } from './preamble_byte_census.js';
29
30
 
30
31
  const REPO_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
@@ -91,17 +92,15 @@ function dedupableCount(userDir: string, sourceDir: string): { skipped: number;
91
92
  * perfect version alignment. Classifying the two apart is what turns "0/110"
92
93
  * from a puzzle into an answer: `provenanceOnly == total` means aligning
93
94
  * versions would buy exactly nothing.
95
+ *
96
+ * The three-way comparison now lives in `_lib/carrier_divergence.ts` because a
97
+ * second surface needs it (`report_carrier_divergence`, round-5 Phase 1.3). The
98
+ * DIRECTORY WALK stays here and stays anchored on `sourceDir`: this harness asks
99
+ * how many installed twins the dedup could skip, which is a question about the
100
+ * projection's own rules, not about the union of both carriers. Only the
101
+ * comparison is shared — that is the part that must not drift, and it is also
102
+ * the part `scope_dedup.test.ts` pins.
94
103
  */
95
- const OWNERSHIP_KEYS = ['package', 'source_path'] as const;
96
-
97
- function stripOwnershipKeys(buf: Buffer): string {
98
- return buf
99
- .toString('utf-8')
100
- .split('\n')
101
- .filter((line) => !OWNERSHIP_KEYS.some((k) => line.startsWith(`${k}:`)))
102
- .join('\n');
103
- }
104
-
105
104
  interface ReachabilitySplit {
106
105
  total: number;
107
106
  identical: number;
@@ -130,9 +129,16 @@ function classifyReachability(userDir: string, sourceDir: string): ReachabilityS
130
129
  continue;
131
130
  }
132
131
  const b = fs.readFileSync(path.join(sourceDir, entry));
133
- if (a.equals(b)) split.identical += 1;
134
- else if (stripOwnershipKeys(a) === stripOwnershipKeys(b)) split.provenanceOnly += 1;
135
- else split.bodyDiff += 1;
132
+ switch (comparePair(a, b)) {
133
+ case 'identical':
134
+ split.identical += 1;
135
+ break;
136
+ case 'provenance-only':
137
+ split.provenanceOnly += 1;
138
+ break;
139
+ default:
140
+ split.bodyDiff += 1;
141
+ }
136
142
  }
137
143
  return split;
138
144
  }
@@ -39,6 +39,45 @@ import { atomic_write_json } from "./hooks/state_io.js";
39
39
  import { readHookStdin } from "./hooks/hook_stdin.js";
40
40
 
41
41
  export const SETTINGS_FILE = ".agent-settings.yml";
42
+
43
+ /**
44
+ * Project-relative settings locations, canonical first.
45
+ *
46
+ * The canonical project settings file is `agents/settings/.agent-settings.yml`;
47
+ * the repo-root file is a documented legacy fallback
48
+ * (`agent_settings.ts::project_settings_path`). This hook read ONLY the legacy
49
+ * path, so on a canonically installed consumer the file was always "missing" and
50
+ * the gate resolved to `settings_file_missing` → do not block. An Iron-Law gate
51
+ * that cannot fire on a correct install is not a gate.
52
+ *
53
+ * Inlined rather than imported: this module is deliberately dependency-free so
54
+ * it can run as a hook on every platform. The list is two entries and its source
55
+ * of truth is named above.
56
+ */
57
+ export const SETTINGS_CANDIDATES: readonly string[] = [
58
+ path.join("agents", "settings", SETTINGS_FILE),
59
+ SETTINGS_FILE,
60
+ ];
61
+
62
+ /**
63
+ * First existing settings file under `consumer_root`, canonical before legacy.
64
+ *
65
+ * Falls back to the CANONICAL path when neither exists, so the "missing" branch
66
+ * reports the file a consumer should create rather than the one they should not.
67
+ */
68
+ export function resolve_settings_path(consumer_root: string): string {
69
+ for (const rel of SETTINGS_CANDIDATES) {
70
+ const candidate = path.join(consumer_root, rel);
71
+ try {
72
+ if (fs.statSync(candidate).isFile()) {
73
+ return candidate;
74
+ }
75
+ } catch {
76
+ continue;
77
+ }
78
+ }
79
+ return path.join(consumer_root, SETTINGS_CANDIDATES[0] as string);
80
+ }
42
81
  // NOTE: the Python docstring says `agents/runtime/state/`, but the code
43
82
  // constant is `agents/state/`. Replicated verbatim — this is a latent
44
83
  // docstring/code divergence in the retired Python implementation (ADR-200 § replicate
@@ -141,7 +180,7 @@ export function run(options: {
141
180
  }): number {
142
181
  const { consumer_root } = options;
143
182
  const verbose = options.verbose ?? false;
144
- const settings_path = path.join(consumer_root, SETTINGS_FILE);
183
+ const settings_path = resolve_settings_path(consumer_root);
145
184
  let settings_present = false;
146
185
  try {
147
186
  settings_present = fs.statSync(settings_path).isFile();
@@ -41,7 +41,10 @@ import {
41
41
  type RouteTaken,
42
42
  } from './_lib/orchestration_record.js';
43
43
 
44
- const DEFAULT_DIR = 'agents/runtime/state/audit';
44
+ // Exported so other writers of the same audit-log-v1 orchestration line
45
+ // (e.g. orchestration_record_hook.ts, the deterministic PostToolUse capture)
46
+ // target the exact same directory instead of duplicating the literal.
47
+ export const DEFAULT_DIR = 'agents/runtime/state/audit';
45
48
 
46
49
  type Flags = Record<string, string | boolean>;
47
50
 
@@ -44,6 +44,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
44
44
  import * as YAML from 'yaml';
45
45
 
46
46
  import { resolveGlobalProfilePath } from './_lib/agent_user_profile.js';
47
+ import { censusRuleDir, type RuleDirCensus } from './_lib/carrier_divergence.js';
47
48
  import { DEFAULT_PROJECTS_ROOT, scanTranscripts } from './_lib/cc_transcript.js';
48
49
  import { censusDuplicateScope } from './_lib/duplicate_scope_census.js';
49
50
  import { computeColdStarts } from './cache_realization_report.js';
@@ -71,26 +72,14 @@ function readCharsIfExists(p: string): number {
71
72
 
72
73
  // ── source 1/2: rule directories ────────────────────────────────────────
73
74
 
74
- export interface RuleDirCensus {
75
- files: number;
76
- chars: number;
77
- }
78
-
79
- export function censusRuleDir(dir: string): RuleDirCensus {
80
- if (!fs.existsSync(dir)) return { files: 0, chars: 0 };
81
- let files = 0;
82
- let chars = 0;
83
- for (const name of fs.readdirSync(dir)) {
84
- if (!name.endsWith('.md')) continue;
85
- try {
86
- chars += fs.statSync(path.join(dir, name)).size;
87
- files += 1;
88
- } catch {
89
- // Unreadable entry — skip rather than fail the whole census.
90
- }
91
- }
92
- return { files, chars };
93
- }
75
+ // Moved to `_lib/carrier_divergence.ts` when `conformance_scan` needed the same
76
+ // count (round-6 Phase 4.3): that module is on a bundled CLI path and this one
77
+ // pulls `yaml` plus the cold-start report, so importing this file to reach fifteen
78
+ // lines of `statSync` was the wrong direction. Re-exported so every existing
79
+ // caller keeps its import and there is still one definition. Imported as well as
80
+ // re-exported: a bare `export … from` does not bind the name in this module's own
81
+ // scope, and this file uses both below.
82
+ export { censusRuleDir, type RuleDirCensus };
94
83
 
95
84
  export interface PerRuleCost {
96
85
  file: string;
@@ -0,0 +1,364 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * report_carrier_divergence.ts — which rules the two carriers deliver
4
+ * differently, BY NAME.
5
+ *
6
+ * Executes round-5 Phase 1.3, landed under round-6 Phase 4.1. Advisory: **it
7
+ * gates on nothing** and must not acquire a threshold, for the reason
8
+ * `report_skill_activation` and `report_imperative_density` must not — plus one
9
+ * specific to this surface, below.
10
+ *
11
+ * WHY THIS EXISTS
12
+ * ---------------
13
+ * The suite installs machine-globally (`~/.claude/rules/`) and the package's own
14
+ * checkout also projects per-project (`dist/agent-src/rules/`, which every
15
+ * project-scope tree symlinks). A machine that has both loads the shared rules
16
+ * twice, and nothing said whether the two copies agree. Round 5 measured the
17
+ * condition once and named the dangerous case: its global copy of
18
+ * `git-history-discipline` asserted an unqualified "deterministically blocked by
19
+ * the `block-no-verify` PreToolUse guard" while the shipped copy carried the
20
+ * host-scoped retraction of exactly that claim. The agent held both, with no
21
+ * precedence marker.
22
+ *
23
+ * A one-off measurement cannot own that condition, because the condition is
24
+ * **transient and maintainer-dependent**: it appears when the checkout moves
25
+ * ahead of the installed release and disappears the next time the maintainer
26
+ * reinstalls. Verified both directions — see § THE 2026-08-08 RE-MEASUREMENT.
27
+ * So the deliverable is a report anyone can re-run, not a number in a roadmap.
28
+ *
29
+ * WHY IT MUST NOT BECOME A GATE
30
+ * -----------------------------
31
+ * A maintainer developing the package is *legitimately* ahead of their own
32
+ * global install — `condense.ts` says so where it explains why the byte-keyed
33
+ * dedup is inert in that state. A gate here would fail on the normal
34
+ * development condition, and a gate that fires on sanctioned behaviour gets
35
+ * ignored, which is the failure `check-rule-invariants` already paid for once.
36
+ * The point is that the condition becomes VISIBLE, not that it becomes illegal.
37
+ *
38
+ * THE THREE CLASSES, AND WHICH ONE A READER MUST ACT ON
39
+ * ----------------------------------------------------
40
+ * body-diff the two carriers deliver different TEXT. This is the class
41
+ * round 5's contradiction lived in, and the only one that
42
+ * needs a decision. Precedence when it appears: the project
43
+ * projection is generated from `src/` at the current commit,
44
+ * so it is the newer copy and it wins; the global install is
45
+ * a release snapshot. Printed with the report so the
46
+ * precedence is read at the moment it matters rather than
47
+ * looked up.
48
+ * provenance-only identical after removing the two installer ownership keys.
49
+ * Structural, expected, and decided against closing —
50
+ * `agents/settings/contexts/dedup-reachability-refusal.md`.
51
+ * Reported as a count with that pointer, never as a finding,
52
+ * or the real class drowns in it.
53
+ * one-carrier-only a rule present at exactly one scope. Not a contradiction:
54
+ * project-only is a rule newer than the installed release,
55
+ * global-only is a rule the release still carries. Named
56
+ * because asymmetric reach is what makes a project-scope
57
+ * measurement non-transferable to another machine.
58
+ *
59
+ * NOT IN SCOPE. Skills, commands and contexts have the same two-carrier shape
60
+ * and are not compared here; this executes the rule-scoped step that was
61
+ * specified. Nor does it compare `~/.claude/CLAUDE.md` against the project's
62
+ * `CLAUDE.md` — a different mechanism with a different precedence story.
63
+ *
64
+ * Exit: 0 always, except a usage error (1). Deliberate — see above.
65
+ */
66
+ import * as fs from 'node:fs';
67
+ import * as os from 'node:os';
68
+ import * as path from 'node:path';
69
+ import process from 'node:process';
70
+ import { fileURLToPath, pathToFileURL } from 'node:url';
71
+
72
+ import { comparePair, type PairVerdict } from './_lib/carrier_divergence.js';
73
+
74
+ const _HERE = fileURLToPath(import.meta.url);
75
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
76
+
77
+ /**
78
+ * The project carrier — the tree the host actually reads at project scope, NOT
79
+ * `dist/agent-src/rules/`.
80
+ *
81
+ * The distinction is load-bearing and was found by measuring: the global
82
+ * installer delivers the five ADR-004 `type: manual` rules while the project
83
+ * projection deliberately omits them, so anchoring on `dist` (which holds all
84
+ * 115) reports those five as *shared* when in fact only one carrier delivers
85
+ * them. Two writers, different filters — exactly the asymmetry this report
86
+ * exists to name. `dist` is still read, as the reference for what COULD be
87
+ * projected, which is what makes the manual class attributable rather than a
88
+ * bare absence.
89
+ */
90
+ export const PROJECT_RULES = path.join('.claude', 'rules');
91
+ /** What the project projection is generated from — the reference set. */
92
+ export const PROJECTION_SOURCE = path.join('dist', 'agent-src', 'rules');
93
+ /** The machine-global carrier, relative to the user's home. */
94
+ export const GLOBAL_RULES = path.join('.claude', 'rules');
95
+
96
+ export interface CarrierDivergence {
97
+ projectDir: string;
98
+ globalDir: string;
99
+ /** `false` when the global carrier is absent — every count is then 0. */
100
+ globalPresent: boolean;
101
+ /** `false` when the project tree has not been generated in this checkout. */
102
+ projectPresent: boolean;
103
+ shared: number;
104
+ identical: string[];
105
+ provenanceOnly: string[];
106
+ bodyDiff: string[];
107
+ projectOnly: string[];
108
+ globalOnly: string[];
109
+ /**
110
+ * Shared names whose copies could not both be read (a dangling symlink from a
111
+ * half-finished install). Counted rather than dropped: a pair that silently
112
+ * leaves `shared` makes every other number in the report unaccounted for.
113
+ */
114
+ unreadable: string[];
115
+ /**
116
+ * Rules the projection source carries, the global carrier delivers, and the
117
+ * project projection omits by design (`type: manual`). A subset of
118
+ * `globalOnly`, separated so the expected half does not read as drift.
119
+ */
120
+ manualOnlyGlobal: string[];
121
+ }
122
+
123
+ function _listRules(dir: string): string[] {
124
+ try {
125
+ return fs
126
+ .readdirSync(dir)
127
+ .filter((f) => f.endsWith('.md'))
128
+ .sort();
129
+ } catch {
130
+ return [];
131
+ }
132
+ }
133
+
134
+ /** `type:` from a rule's frontmatter, or `''` when unreadable / absent. */
135
+ function _ruleType(file: string): string {
136
+ let text: string;
137
+ try {
138
+ text = fs.readFileSync(file, 'utf-8');
139
+ } catch {
140
+ return '';
141
+ }
142
+ if (!text.startsWith('---')) return '';
143
+ const end = text.indexOf('\n---', 3);
144
+ const fm = end === -1 ? text : text.slice(0, end);
145
+ const m = /^type:\s*"?([a-z]+)"?/m.exec(fm);
146
+ return m === null ? '' : (m[1] as string);
147
+ }
148
+
149
+ /**
150
+ * Compare the two carriers over the UNION of their rule names.
151
+ *
152
+ * The union is the point: anchoring on either side alone hides one direction of
153
+ * asymmetric reach, and it was the hidden direction — rules the global install
154
+ * carries and the project does not — that made round 5's cross-project
155
+ * comparison hard to interpret.
156
+ *
157
+ * `sourceDir` is the projection source, read only to attribute a global-only
158
+ * rule to the ADR-004 manual filter rather than to drift. Absent → the manual
159
+ * split is simply empty, never a guess.
160
+ */
161
+ export function compareCarriers(
162
+ projectDir: string,
163
+ globalDir: string,
164
+ sourceDir?: string,
165
+ ): CarrierDivergence {
166
+ const out: CarrierDivergence = {
167
+ projectDir,
168
+ globalDir,
169
+ globalPresent: fs.existsSync(globalDir),
170
+ projectPresent: fs.existsSync(projectDir),
171
+ shared: 0,
172
+ identical: [],
173
+ provenanceOnly: [],
174
+ bodyDiff: [],
175
+ projectOnly: [],
176
+ globalOnly: [],
177
+ unreadable: [],
178
+ manualOnlyGlobal: [],
179
+ };
180
+ const project = new Set(_listRules(projectDir));
181
+ const global = new Set(_listRules(globalDir));
182
+
183
+ for (const rule of [...new Set([...project, ...global])].sort()) {
184
+ const inP = project.has(rule);
185
+ const inG = global.has(rule);
186
+ if (inP && !inG) {
187
+ out.projectOnly.push(rule);
188
+ continue;
189
+ }
190
+ if (!inP && inG) {
191
+ out.globalOnly.push(rule);
192
+ if (sourceDir !== undefined && _ruleType(path.join(sourceDir, rule)) === 'manual') {
193
+ out.manualOnlyGlobal.push(rule);
194
+ }
195
+ continue;
196
+ }
197
+ out.shared += 1;
198
+ let verdict: PairVerdict;
199
+ try {
200
+ verdict = comparePair(
201
+ fs.readFileSync(path.join(projectDir, rule)),
202
+ fs.readFileSync(path.join(globalDir, rule)),
203
+ );
204
+ } catch {
205
+ // An unreadable copy is not a body difference and must not be
206
+ // reported as one — a permission error would otherwise manufacture
207
+ // the only class this report asks a reader to act on. It is still
208
+ // NAMED: a pair that just disappears from `shared` leaves the ledger
209
+ // short with nothing saying why.
210
+ out.shared -= 1;
211
+ out.unreadable.push(rule);
212
+ continue;
213
+ }
214
+ if (verdict === 'identical') out.identical.push(rule);
215
+ else if (verdict === 'provenance-only') out.provenanceOnly.push(rule);
216
+ else out.bodyDiff.push(rule);
217
+ }
218
+ return out;
219
+ }
220
+
221
+ export function render(d: CarrierDivergence): string {
222
+ const lines: string[] = [];
223
+ lines.push('cross-carrier rule divergence — advisory, gates on nothing');
224
+ lines.push(` project carrier: ${d.projectDir}`);
225
+ lines.push(` global carrier: ${d.globalDir}`);
226
+ lines.push('');
227
+
228
+ if (!d.globalPresent) {
229
+ lines.push(' The global carrier is absent — this machine loads the project projection');
230
+ lines.push(' only, so there is no second copy to diverge from. Nothing to report, and');
231
+ lines.push(' that is a real answer rather than a skipped check.');
232
+ return lines.join('\n');
233
+ }
234
+ if (!d.projectPresent) {
235
+ lines.push(' The project rule tree does not exist in this checkout — it is generated and');
236
+ lines.push(' gitignored, so a fresh clone has none, and `agents/.agent-tools.yml` may');
237
+ lines.push(' select zero tools deliberately. Run `task generate-tools` and re-run this');
238
+ lines.push(' report. Reported rather than silently substituted with the projection');
239
+ lines.push(' source: dist/ holds rules the project tree omits by design, so reading it');
240
+ lines.push(' as the project carrier would answer a different question than the one asked.');
241
+ return lines.join('\n');
242
+ }
243
+
244
+ lines.push(` shared rule names ${d.shared}`);
245
+ lines.push(` byte-identical ${d.identical.length}`);
246
+ lines.push(` differ ONLY in the install stamp ${d.provenanceOnly.length}`);
247
+ lines.push(` differ in BODY ${d.bodyDiff.length}`);
248
+ if (d.unreadable.length > 0) {
249
+ lines.push(` unreadable on one side ${d.unreadable.length} (${d.unreadable.join(', ')})`);
250
+ lines.push(' Not counted as shared and NOT a body difference — a dangling entry is');
251
+ lines.push(' a broken install, not a disagreement about content.');
252
+ }
253
+ lines.push(` present at project scope only ${d.projectOnly.length}`);
254
+ lines.push(` present at global scope only ${d.globalOnly.length}`);
255
+ lines.push('');
256
+
257
+ if (d.bodyDiff.length > 0) {
258
+ lines.push(' BODY DIVERGENCE — the two carriers deliver different text for these rules.');
259
+ lines.push(' Both copies reach the model, with no precedence marker between them, so a');
260
+ lines.push(' claim one copy retracts can be re-asserted by the other. PRECEDENCE: the');
261
+ lines.push(' project projection is generated from src/ at this commit and wins; the');
262
+ lines.push(' global install is a release snapshot. Close it by reinstalling the global');
263
+ lines.push(' copy, or by reading the project copy as authoritative.');
264
+ for (const r of d.bodyDiff) lines.push(` - ${r}`);
265
+ lines.push('');
266
+ }
267
+
268
+ if (d.provenanceOnly.length > 0) {
269
+ lines.push(` ${String(d.provenanceOnly.length)} pair(s) differ only by the installer's package:/source_path: stamp.`);
270
+ lines.push(' Not a content difference and NOT a defect: install.ts stamps every installed');
271
+ lines.push(' rule, the in-repo projection stamps nothing. Closing it was decided against');
272
+ lines.push(' on 2026-07-31 — agents/settings/contexts/dedup-reachability-refusal.md holds');
273
+ lines.push(' the analysis and the five reopen conditions. Reported as a count on purpose:');
274
+ lines.push(' naming every expected pair would bury the class above.');
275
+ lines.push('');
276
+ }
277
+
278
+ if (d.projectOnly.length > 0) {
279
+ lines.push(` project-scope only (${String(d.projectOnly.length)}) — newer than the installed release:`);
280
+ lines.push(` ${d.projectOnly.join(', ')}`);
281
+ }
282
+ const driftGlobalOnly = d.globalOnly.filter((r) => !d.manualOnlyGlobal.includes(r));
283
+ if (driftGlobalOnly.length > 0) {
284
+ lines.push(` global-scope only (${String(driftGlobalOnly.length)}) — the release carries these, this checkout does not project them:`);
285
+ lines.push(` ${driftGlobalOnly.join(', ')}`);
286
+ }
287
+ if (d.manualOnlyGlobal.length > 0) {
288
+ lines.push(
289
+ ` delivered by the global carrier ONLY, by design (${String(d.manualOnlyGlobal.length)}, ADR-004 \`type: manual\`):`,
290
+ );
291
+ lines.push(` ${d.manualOnlyGlobal.join(', ')}`);
292
+ lines.push(' The project projection filters these out and the global installer does not.');
293
+ lines.push(' Two writers, different filters — so on a machine with both carriers these');
294
+ lines.push(' rules reach the model, and on a project-only machine they do not. Expected,');
295
+ lines.push(' and worth knowing before citing any project-scope reach figure.');
296
+ }
297
+ if (d.projectOnly.length > 0 || d.globalOnly.length > 0) {
298
+ lines.push('');
299
+ lines.push(' Asymmetric reach is why a project-scope conformance figure does not transfer');
300
+ lines.push(' to another machine: the rule set the model was told differs by carrier.');
301
+ }
302
+
303
+ if (d.bodyDiff.length === 0) {
304
+ lines.push('');
305
+ lines.push(' No body divergence. The condition is transient — it returns whenever the');
306
+ lines.push(' checkout moves ahead of the installed release — so this is a reading of');
307
+ lines.push(' right now, not a property of the repo. Re-run it, do not cite it.');
308
+ }
309
+ return lines.join('\n');
310
+ }
311
+
312
+ export function main(argv?: readonly string[]): number {
313
+ const args = argv ?? process.argv.slice(2);
314
+ let projectDir = path.join(REPO_ROOT, PROJECT_RULES);
315
+ let globalDir = path.join(os.homedir(), GLOBAL_RULES);
316
+ let sourceDir = path.join(REPO_ROOT, PROJECTION_SOURCE);
317
+ // A value must not itself be a flag. `--project --global /x` otherwise
318
+ // resolves a directory literally named `--global` and silently drops `/x`,
319
+ // reporting "tree does not exist" instead of a usage error — the flag-VALUE
320
+ // half of the hole this file's own test closes on the flag-NAME half.
321
+ const value = (i: number): string | null => {
322
+ const v = args[i + 1];
323
+ return v === undefined || v.startsWith('-') ? null : v;
324
+ };
325
+ for (let i = 0; i < args.length; i += 1) {
326
+ const a = args[i];
327
+ if (a === '--project' || a === '--global' || a === '--source') {
328
+ const v = value(i);
329
+ if (v === null) {
330
+ process.stderr.write(`report_carrier_divergence: ${a} needs a directory\n`);
331
+ return 1;
332
+ }
333
+ const resolved = path.resolve(v);
334
+ if (a === '--project') projectDir = resolved;
335
+ else if (a === '--global') globalDir = resolved;
336
+ else sourceDir = resolved;
337
+ i += 1;
338
+ } else if (a === '--help' || a === '-h') {
339
+ process.stdout.write(
340
+ 'usage: report_carrier_divergence [--project DIR] [--global DIR] [--source DIR]\n',
341
+ );
342
+ return 0;
343
+ } else if (a !== undefined && a.startsWith('--') && a !== '--quiet') {
344
+ process.stderr.write(`report_carrier_divergence: unknown flag ${a}\n`);
345
+ return 1;
346
+ }
347
+ }
348
+ process.stdout.write(`${render(compareCarriers(projectDir, globalDir, sourceDir))}\n`);
349
+ return 0;
350
+ }
351
+
352
+ function _isCliEntry(): boolean {
353
+ if (process.argv[1] === undefined) return false;
354
+ if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
355
+ try {
356
+ return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
357
+ } catch {
358
+ return false;
359
+ }
360
+ }
361
+
362
+ if (_isCliEntry()) {
363
+ process.exit(main());
364
+ }