@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +12 -11
- package/CHANGELOG.md +99 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/conformance.md +24 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +10 -3
- package/dist/agent-src/rules/artifact-engagement-recording.md +4 -1
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
- package/dist/agent-src/rules/context-hygiene.md +3 -1
- package/dist/agent-src/rules/council-availability.md +93 -0
- package/dist/agent-src/rules/cross-source-consistency.md +3 -0
- package/dist/agent-src/rules/delegation-policy.md +29 -5
- package/dist/agent-src/rules/design-fidelity.md +3 -0
- package/dist/agent-src/rules/evaluator-independence.md +2 -1
- package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
- package/dist/agent-src/rules/finance-safety-floor.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +1 -1
- package/dist/agent-src/rules/history-discipline.md +3 -1
- package/dist/agent-src/rules/legal-safety-floor.md +1 -1
- package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
- package/dist/agent-src/rules/onboarding-gate.md +3 -1
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
- package/dist/agent-src/rules/role-mode-adherence.md +1 -1
- package/dist/agent-src/rules/scale-discipline.md +3 -1
- package/dist/agent-src/rules/self-repair-loop.md +103 -0
- package/dist/agent-src/rules/session-canary.md +2 -1
- package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
- package/dist/agent-src/rules/token-budget-discipline.md +3 -1
- package/dist/agent-src/rules/tool-safety.md +5 -1
- package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
- package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
- package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +6 -10
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +7 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-22GCIJ2O.js +270 -0
- package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-2BE5I2MN.js} +8 -8
- package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-7OMQCH4V.js} +1 -1
- package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-GB2FSUEE.js} +57 -7
- package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-JXARCGVN.js} +3 -3
- package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-PQSAQKHW.js} +466 -304
- package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +134 -0
- package/dist/cli-delegate/cmd_conformance.js +7 -7
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +4 -4
- package/dist/cli-delegate/cmd_explain.js +80 -330
- package/dist/cli-delegate/cmd_export.js +5 -5
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_mcp_available.js +433 -0
- package/dist/cli-delegate/cmd_migrate.js +3 -3
- package/dist/cli-delegate/cmd_packs_active.js +153 -0
- package/dist/cli-delegate/cmd_preflight.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +5 -5
- package/dist/cli-delegate/cmd_route_audit.js +8 -8
- package/dist/cli-delegate/cmd_route_explain.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +272 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +5 -5
- package/dist/cli-delegate/cmd_settings_set.js +7 -2967
- package/dist/cli-delegate/cmd_sync.js +5 -5
- package/dist/cli-delegate/cmd_uninstall.js +5 -5
- 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 +5 -5
- package/dist/cli-delegate/cmd_versions.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +100 -40
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -5
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +9004 -6132
- package/dist/install/install.mjs +697 -507
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +127 -20
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +80 -15
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +29 -8
- package/docs/guidelines/agent-infra/memory-access.md +14 -3
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +6 -10
- package/src/config/budgets.yml +29 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +26 -0
- package/src/scripts/_cli/cmd_brand_status.ts +174 -0
- package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
- package/src/scripts/_cli/cmd_packs_active.ts +225 -0
- package/src/scripts/_cli/cmd_settings_get.ts +324 -0
- package/src/scripts/_dispatch.bash +57 -0
- package/src/scripts/_lib/agent_settings.ts +82 -16
- package/src/scripts/_lib/carrier_divergence.ts +135 -0
- package/src/scripts/_lib/host_capability.ts +84 -1
- package/src/scripts/_lib/prompt_shape.ts +88 -0
- package/src/scripts/_lib/review_skipped_record.ts +102 -0
- package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
- package/src/scripts/_lib/self_repair.ts +342 -0
- package/src/scripts/_lib/self_repair_store.ts +100 -0
- package/src/scripts/_lib/transcript_entry.ts +80 -0
- package/src/scripts/check_branch_freshness.ts +255 -0
- package/src/scripts/check_council_config_location.ts +125 -1
- package/src/scripts/check_standing_rule_delivery.ts +314 -0
- package/src/scripts/config/profiles.ts +13 -3
- package/src/scripts/conformance_scan.ts +272 -24
- package/src/scripts/council_availability_hook.ts +188 -0
- package/src/scripts/council_cli.ts +126 -7
- package/src/scripts/hook_manifest.yaml +107 -26
- package/src/scripts/hooks/block_no_verify.ts +39 -2
- package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
- package/src/scripts/hooks/concern_registry.ts +10 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +450 -0
- package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
- package/src/scripts/install.ts +151 -0
- package/src/scripts/language_mirror_hook.ts +30 -37
- package/src/scripts/measure_scope_dedup.ts +19 -13
- package/src/scripts/onboarding_gate_hook.ts +40 -1
- package/src/scripts/orchestration_record.ts +4 -1
- package/src/scripts/preamble_byte_census.ts +9 -20
- package/src/scripts/report_carrier_divergence.ts +364 -0
- package/src/scripts/report_skill_activation.ts +257 -0
- package/src/scripts/report_skill_obligation_violations.ts +579 -0
- package/src/scripts/routing_doctor.ts +77 -6
- package/src/scripts/self_repair_cli.ts +240 -0
- 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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
export
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
}
|