@event4u/agent-config 9.27.0 → 9.29.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 +13 -12
- package/CHANGELOG.md +165 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/analyze/conformance.md +24 -0
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +13 -12
- package/dist/agent-src/commands/team/review.md +9 -8
- package/dist/agent-src/commands/team/status.md +9 -8
- package/dist/agent-src/commands/team.md +29 -19
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +28 -3
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +6 -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 +38 -10
- 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/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
- package/dist/agent-src/rules/token-budget-discipline.md +4 -2
- 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/forensics-report/SKILL.md +114 -0
- package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
- 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/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -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/subagent-orchestration/SKILL.md +7 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +8 -12
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +23 -0
- package/dist/cli/registry.js +7 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-66OVWGB5.js +270 -0
- package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-LCFJIQ4J.js} +89 -7
- package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
- package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-MP3JT3IU.js} +3 -3
- package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-NZ5N2XQL.js} +467 -305
- package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-OJM3MBJM.js} +1 -1
- package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-RCIA67XN.js} +117 -96
- 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 +10 -47
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +10 -6
- package/dist/cli-delegate/cmd_explain.js +80 -330
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- 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 +6 -6
- 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 +266 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +8 -2968
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- 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 +6 -6
- package/dist/cli-delegate/cmd_versions.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +164 -55
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +44 -5
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +16 -6
- package/dist/hooks/dispatch.js +9341 -6131
- package/dist/install/install.mjs +739 -524
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +777 -281
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +134 -15
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +147 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +6 -7
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsCarveOut.js +0 -9
- package/dist/shared/settingsCarveOut.js.map +1 -1
- package/dist/shared/settingsClasses.js +11 -9
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
- package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +68 -17
- package/docs/guidelines/agent-infra/memory-access.md +14 -3
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +8 -12
- package/src/config/agent-settings.template.yml +52 -66
- package/src/config/budgets.yml +29 -0
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +26 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/scripts/_cli/cmd_brand_status.ts +174 -0
- package/src/scripts/_cli/cmd_conformance.ts +14 -42
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- 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 +140 -16
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
- package/src/scripts/_lib/auto_dispatch.ts +23 -18
- package/src/scripts/_lib/capsule_trigger.ts +124 -0
- package/src/scripts/_lib/carrier_divergence.ts +135 -0
- package/src/scripts/_lib/host_capability.ts +144 -4
- package/src/scripts/_lib/judgment_ladder.ts +382 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +37 -0
- package/src/scripts/_lib/orchestration_savings.ts +1 -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 +489 -0
- package/src/scripts/_lib/self_repair_store.ts +128 -0
- package/src/scripts/_lib/subagent_capsule.ts +182 -0
- package/src/scripts/_lib/subagent_response.ts +19 -0
- package/src/scripts/_lib/subagent_spawn.ts +13 -0
- package/src/scripts/_lib/subagent_steering.ts +9 -4
- package/src/scripts/_lib/tier_budget_routing.ts +6 -1
- package/src/scripts/_lib/transcript_entry.ts +80 -0
- package/src/scripts/_lib/worker_budget.ts +37 -2
- package/src/scripts/ai_council/clients.ts +76 -54
- package/src/scripts/ai_council/config.ts +94 -6
- package/src/scripts/ai_council/handoff.ts +113 -0
- package/src/scripts/ai_council/modes.ts +19 -4
- package/src/scripts/ai_council/orchestrator.ts +105 -0
- package/src/scripts/ai_council/quorum.ts +84 -0
- package/src/scripts/ai_council/session.ts +44 -0
- package/src/scripts/ai_council/transport_resolver.ts +127 -1
- package/src/scripts/ai_team/availability.ts +110 -0
- package/src/scripts/ai_team/config.ts +34 -14
- package/src/scripts/ai_team/team_dispatch.ts +71 -29
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/check_branch_freshness.ts +255 -0
- package/src/scripts/check_council_config_location.ts +125 -1
- package/src/scripts/check_detector_corpus.ts +202 -0
- package/src/scripts/check_hook_bundle_freshness.ts +4 -1
- package/src/scripts/check_standing_rule_delivery.ts +314 -0
- package/src/scripts/condense.ts +162 -2
- 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 +527 -32
- package/src/scripts/forensics_report.ts +333 -0
- 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 +484 -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/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/measure_scope_dedup.ts +19 -13
- package/src/scripts/onboarding_gate_hook.ts +40 -1
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/preamble_byte_census.ts +9 -20
- package/src/scripts/rebuild_hook_bundle.ts +177 -0
- package/src/scripts/release.ts +74 -2
- 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 +95 -39
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +470 -0
- package/src/scripts/self_repair_hook.ts +241 -0
- package/src/scripts/team_events_spike.ts +271 -0
- package/src/scripts/team_review_gate_hook.ts +31 -9
- package/src/server/schemas/settings.ts +10 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- package/src/shared/settingsClasses.ts +11 -9
|
@@ -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,177 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Safe self-heal for the self-hosted hook bundle — the builder that
|
|
4
|
+
* `check_hook_bundle_freshness` deliberately is not.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS — measured, not hypothetical.
|
|
7
|
+
*
|
|
8
|
+
* The freshness gate reds after ANY hook-source merge from main, because
|
|
9
|
+
* `dist/hooks/dispatch.js` is untracked and nothing in the local chain
|
|
10
|
+
* rebuilds it. On 2026-08-09 that turned `task release` (9.28.0) into a
|
|
11
|
+
* blocked push with a remedy the operator had to run by hand — a routine
|
|
12
|
+
* pre-push chore promoted to a release incident. The gate is right to never
|
|
13
|
+
* build (its documented hazard: esbuild overwriting the dispatcher a running
|
|
14
|
+
* hook is executing can wedge the tool loop). The hazard is in HOW a naive
|
|
15
|
+
* rebuild writes, not in rebuilding: this script builds to a sibling `.new`
|
|
16
|
+
* file, proves the result actually dispatches, and only then renames it into
|
|
17
|
+
* place — an atomic same-directory rename that a running hook holding an open
|
|
18
|
+
* file descriptor never observes mid-write.
|
|
19
|
+
*
|
|
20
|
+
* The `.new` file MUST live in `dist/hooks/` (not a temp dir): the bundle
|
|
21
|
+
* resolves `src/scripts/hook_manifest.yaml` relative to its OWN location via
|
|
22
|
+
* the `__AGENT_CONFIG_BUNDLE__` path-depth switch. Built anywhere else, every
|
|
23
|
+
* probe answers "manifest missing" with exit 0 and the smoke test proves
|
|
24
|
+
* nothing (measured 2026-08-08).
|
|
25
|
+
*
|
|
26
|
+
* The esbuild invocation is NOT duplicated here — it is read from
|
|
27
|
+
* `package.json`'s `build:hooks` script and only the `--outfile=` is
|
|
28
|
+
* rewritten, so the flag set (banner, defines, target) has exactly one home.
|
|
29
|
+
*
|
|
30
|
+
* Behaviour:
|
|
31
|
+
* no self-hosted bundle → no-op (fresh checkout / CI — nothing is running
|
|
32
|
+
* stale code, and materialising a bundle here would
|
|
33
|
+
* change what other gates see)
|
|
34
|
+
* bundle fresh → no-op
|
|
35
|
+
* bundle stale → rebuild to `.new`, probe, rename into place
|
|
36
|
+
* build or probe fails → the `.new` file is removed and the OLD bundle
|
|
37
|
+
* keeps running (old-but-working beats broken);
|
|
38
|
+
* exit 1 so the freshness gate behind this still
|
|
39
|
+
* reds and the failure is loud.
|
|
40
|
+
*
|
|
41
|
+
* Exit codes: 0 = fresh / healed / nothing to heal · 1 = heal failed.
|
|
42
|
+
*/
|
|
43
|
+
import { spawnSync } from "node:child_process";
|
|
44
|
+
import * as fs from "node:fs";
|
|
45
|
+
import * as path from "node:path";
|
|
46
|
+
import { fileURLToPath } from "node:url";
|
|
47
|
+
|
|
48
|
+
import { check } from "./check_hook_bundle_freshness.js";
|
|
49
|
+
|
|
50
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
51
|
+
const REPO_ROOT = path.resolve(HERE, "..", "..");
|
|
52
|
+
const LIVE_REL = path.join("dist", "hooks", "dispatch.js");
|
|
53
|
+
const NEW_REL = path.join("dist", "hooks", "dispatch.new.js");
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Rewrite the `--outfile=` of the canonical `build:hooks` script to the
|
|
57
|
+
* `.new` sibling. Returns null when the expected outfile token is absent —
|
|
58
|
+
* a changed `package.json` must fail loudly, never build to a guessed path.
|
|
59
|
+
*/
|
|
60
|
+
export function rewriteOutfile(script: string, newOutfile: string): string | null {
|
|
61
|
+
const token = `--outfile=${LIVE_REL}`;
|
|
62
|
+
if (!script.includes(token)) return null;
|
|
63
|
+
return script.replace(token, `--outfile=${newOutfile}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Verdict on a dispatcher probe. The probe contract is pinned against the
|
|
68
|
+
* real dry-run output: exit 0 AND parseable JSON AND a non-empty `concerns`
|
|
69
|
+
* array. Exit 0 alone is NOT enough — a bundle that cannot find its manifest
|
|
70
|
+
* answers "manifest missing" with exit 0 (measured 2026-08-08), which is
|
|
71
|
+
* exactly the broken dispatcher this probe exists to keep out of the live
|
|
72
|
+
* path. Returns null on pass, a human-readable reason on failure.
|
|
73
|
+
*/
|
|
74
|
+
export function probeVerdict(exitCode: number, stdout: string): string | null {
|
|
75
|
+
if (exitCode !== 0) return `probe exited ${String(exitCode)}`;
|
|
76
|
+
let parsed: unknown;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(stdout);
|
|
79
|
+
} catch {
|
|
80
|
+
return `probe output is not JSON: ${stdout.slice(0, 120)}`;
|
|
81
|
+
}
|
|
82
|
+
const concerns = (parsed as { concerns?: unknown }).concerns;
|
|
83
|
+
if (!Array.isArray(concerns) || concerns.length === 0) {
|
|
84
|
+
return "probe JSON carries no concerns — the bundle dispatched nothing";
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function cleanup(newAbs: string): void {
|
|
90
|
+
try {
|
|
91
|
+
fs.rmSync(newAbs, { force: true });
|
|
92
|
+
} catch {
|
|
93
|
+
// Best-effort: a leftover .new file is inert (nothing executes it).
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function main(): number {
|
|
98
|
+
const r = check(REPO_ROOT);
|
|
99
|
+
|
|
100
|
+
if (r.skipped) {
|
|
101
|
+
process.stdout.write(
|
|
102
|
+
`✅ OK rebuild_hook_bundle: no self-hosted ${LIVE_REL} — nothing runs stale code, nothing to heal\n`,
|
|
103
|
+
);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
if (r.stale.length === 0) {
|
|
107
|
+
process.stdout.write("✅ OK rebuild_hook_bundle: bundle already fresh — no rebuild needed\n");
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
process.stdout.write(
|
|
112
|
+
`🔧 hook bundle stale (${String(r.stale.length)} newer source(s)) — rebuilding safely (build .new → probe → atomic rename)\n`,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "package.json"), "utf-8")) as {
|
|
116
|
+
scripts?: Record<string, string>;
|
|
117
|
+
};
|
|
118
|
+
const buildScript = pkg.scripts?.["build:hooks"];
|
|
119
|
+
if (buildScript === undefined) {
|
|
120
|
+
console.error("❌ rebuild_hook_bundle: package.json has no build:hooks script");
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
const cmd = rewriteOutfile(buildScript, NEW_REL);
|
|
124
|
+
if (cmd === null) {
|
|
125
|
+
console.error(
|
|
126
|
+
`❌ rebuild_hook_bundle: build:hooks no longer targets --outfile=${LIVE_REL} — ` +
|
|
127
|
+
"refusing to build to a guessed path; update this script alongside package.json",
|
|
128
|
+
);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const newAbs = path.join(REPO_ROOT, NEW_REL);
|
|
133
|
+
const build = spawnSync("sh", ["-c", cmd], {
|
|
134
|
+
cwd: REPO_ROOT,
|
|
135
|
+
encoding: "utf-8",
|
|
136
|
+
env: { ...process.env, PATH: `${path.join(REPO_ROOT, "node_modules", ".bin")}:${process.env["PATH"] ?? ""}` },
|
|
137
|
+
});
|
|
138
|
+
if (build.status !== 0) {
|
|
139
|
+
cleanup(newAbs);
|
|
140
|
+
console.error(`❌ rebuild_hook_bundle: esbuild failed (${String(build.status)}):`);
|
|
141
|
+
console.error((build.stderr || build.stdout || "").trim());
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const probe = spawnSync(
|
|
146
|
+
"node",
|
|
147
|
+
[newAbs, "--platform", "claude", "--event", "pre_tool_use", "--dry-run"],
|
|
148
|
+
{ cwd: REPO_ROOT, encoding: "utf-8", input: "{}" },
|
|
149
|
+
);
|
|
150
|
+
const verdict = probeVerdict(probe.status ?? 1, probe.stdout ?? "");
|
|
151
|
+
if (verdict !== null) {
|
|
152
|
+
cleanup(newAbs);
|
|
153
|
+
console.error(`❌ rebuild_hook_bundle: new bundle failed its probe — ${verdict}`);
|
|
154
|
+
console.error(" The old bundle stays in place (old-but-working beats broken).");
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fs.renameSync(newAbs, path.join(REPO_ROOT, LIVE_REL));
|
|
159
|
+
process.stdout.write(
|
|
160
|
+
`✅ OK rebuild_hook_bundle: rebuilt + probed + renamed into place (${String(r.checked)} bundled source(s))\n`,
|
|
161
|
+
);
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const invokedDirectly = ((): boolean => {
|
|
166
|
+
const entry = process.argv[1];
|
|
167
|
+
if (entry === undefined) return false;
|
|
168
|
+
try {
|
|
169
|
+
return fs.realpathSync(path.resolve(entry)) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
})();
|
|
174
|
+
|
|
175
|
+
if (invokedDirectly) {
|
|
176
|
+
process.exit(main());
|
|
177
|
+
}
|
package/src/scripts/release.ts
CHANGED
|
@@ -1073,6 +1073,78 @@ function _tag_exists_remote(tag: string): boolean {
|
|
|
1073
1073
|
return r.returncode === 0;
|
|
1074
1074
|
}
|
|
1075
1075
|
|
|
1076
|
+
/**
|
|
1077
|
+
* True when git rejected a tag push because the ref already exists on the
|
|
1078
|
+
* remote. Wording pinned from the real failure (2026-08-09, 9.28.0 — two
|
|
1079
|
+
* concurrent resume runs, the loser crashed):
|
|
1080
|
+
*
|
|
1081
|
+
* ! [remote rejected] 9.28.0 -> 9.28.0 (cannot lock ref 'refs/tags/9.28.0': reference already exists)
|
|
1082
|
+
*
|
|
1083
|
+
* plus the `[rejected] … (already exists)` variant git emits when the local
|
|
1084
|
+
* and remote tag objects differ.
|
|
1085
|
+
*/
|
|
1086
|
+
export function _is_tag_already_exists(stderr: string, stdout: string): boolean {
|
|
1087
|
+
const text = `${stderr}\n${stdout}`;
|
|
1088
|
+
return /\[(?:remote )?rejected\]/i.test(text) && /already exists/i.test(text);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/** Peeled commit the remote tag points at, or null when the tag is absent. */
|
|
1092
|
+
function _remote_tag_commit(tag: string): string | null {
|
|
1093
|
+
const r = run(['git', 'ls-remote', REMOTE, `refs/tags/${tag}`, `refs/tags/${tag}^{}`], {
|
|
1094
|
+
check: false,
|
|
1095
|
+
capture: true,
|
|
1096
|
+
});
|
|
1097
|
+
if (r.returncode !== 0) return null;
|
|
1098
|
+
const lines = r.stdout.trim().split('\n').filter(Boolean);
|
|
1099
|
+
if (lines.length === 0) return null;
|
|
1100
|
+
// An annotated tag lists the tag object AND the peeled `^{}` commit — the
|
|
1101
|
+
// peeled line wins so the comparison is commit-vs-commit regardless of
|
|
1102
|
+
// whether the two runs minted distinct tag objects for the same commit.
|
|
1103
|
+
const peeled = lines.find((l) => l.trimEnd().endsWith('^{}'));
|
|
1104
|
+
return (peeled ?? (lines[0] as string)).split('\t')[0] ?? null;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Push a tag, tolerating exactly ONE failure: the concurrent-release race.
|
|
1109
|
+
* `_tag_exists_remote` in step 8 is a live check, but between it and this
|
|
1110
|
+
* push a parallel `task release` run can land the same tag (measured
|
|
1111
|
+
* 2026-08-09, 9.28.0). When the remote tag already points at the same commit
|
|
1112
|
+
* as the local one, the repository IS in the desired state — continue. Any
|
|
1113
|
+
* other rejection, and a same-name tag on a DIFFERENT commit, stays fatal
|
|
1114
|
+
* with the push output as the error.
|
|
1115
|
+
*/
|
|
1116
|
+
function _push_tag(tag: string): void {
|
|
1117
|
+
const first = run(['git', 'push', REMOTE, tag], { check: false, capture: true });
|
|
1118
|
+
if (first.returncode === 0) {
|
|
1119
|
+
process.stdout.write(first.stdout);
|
|
1120
|
+
process.stderr.write(first.stderr);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (_is_tag_already_exists(first.stderr, first.stdout)) {
|
|
1124
|
+
const remote = _remote_tag_commit(tag);
|
|
1125
|
+
const local = git(['rev-list', '-n', '1', tag], { capture: true });
|
|
1126
|
+
if (remote !== null && remote === local) {
|
|
1127
|
+
process.stdout.write(
|
|
1128
|
+
`↻ tag ${tag} already on ${REMOTE} at the same commit — a parallel run pushed it; continuing\n`,
|
|
1129
|
+
);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
process.stdout.write(first.stdout);
|
|
1133
|
+
process.stderr.write(first.stderr);
|
|
1134
|
+
die(
|
|
1135
|
+
`tag ${tag} exists on ${REMOTE} pointing at ${remote ?? '<unreadable>'} while the local tag ` +
|
|
1136
|
+
`points at ${local} — refusing to overwrite a published tag; ` +
|
|
1137
|
+
'delete the wrong one deliberately and re-run',
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
process.stdout.write(first.stdout);
|
|
1141
|
+
process.stderr.write(first.stderr);
|
|
1142
|
+
die(
|
|
1143
|
+
`push of tag ${tag} failed (exit ${first.returncode}) — ` +
|
|
1144
|
+
'the push output above is the real error (a pre-push gate, credentials, or protection)',
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1076
1148
|
/** Most recent PR (any state) with `release/X.Y.Z` as head, or null. */
|
|
1077
1149
|
function _pr_for_branch(branch: string): Record<string, unknown> | null {
|
|
1078
1150
|
const r = run(
|
|
@@ -2029,7 +2101,7 @@ function execute(
|
|
|
2029
2101
|
_step(8, total, `Tag ${plan.target} already on ${REMOTE} — skip`);
|
|
2030
2102
|
} else {
|
|
2031
2103
|
_step(8, total, `Tag ${plan.target} exists locally — push only`);
|
|
2032
|
-
|
|
2104
|
+
_push_tag(plan.target);
|
|
2033
2105
|
}
|
|
2034
2106
|
} else {
|
|
2035
2107
|
// Sequencing is load-bearing (release-truth Phase 1, council
|
|
@@ -2047,7 +2119,7 @@ function execute(
|
|
|
2047
2119
|
);
|
|
2048
2120
|
}
|
|
2049
2121
|
run(['git', 'tag', '-a', plan.target, '-m', tag_message_from_section(merged!.body, plan.target)]);
|
|
2050
|
-
|
|
2122
|
+
_push_tag(plan.target);
|
|
2051
2123
|
}
|
|
2052
2124
|
|
|
2053
2125
|
// ─── 9. GitHub Release ──────────────────────────────────────────────────
|
|
@@ -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
|
+
}
|