@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
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Fail when this branch is behind the base it will be merged into — asked of
4
+ * the REMOTE, not of the local tracking ref.
5
+ *
6
+ * WHY THIS EXISTS — measured, not hypothetical.
7
+ *
8
+ * `feedback-update-branch-before-push` says: behind target → merge the base in
9
+ * and re-run the gates, before the push. On 2026-08-08 that obligation was
10
+ * satisfied in form and missed in substance. The run opened with
11
+ * `git fetch origin --prune`, branched from `origin/main` at `c0a3630eb`,
12
+ * worked for roughly ninety minutes, and pushed without fetching again. Two PRs
13
+ * merged inside that window, one of them shipping the same roadmap phase the
14
+ * branch was implementing. The PR opened `CONFLICTING`, and a concurrent branch
15
+ * had already published a different number for the same census.
16
+ *
17
+ * The failure is not a forgotten rule. It is a live-state fact read once and
18
+ * then carried: `direct-answers` Iron Law 2 forbids exactly that for branch and
19
+ * sync state, and a ninety-minute-old reading is memory, not a check. Nothing
20
+ * caught it, because nothing looked — `/roadmap:next` screens the remote before
21
+ * SELECTION, `task preflight` reads the tree, and the session register is
22
+ * advisory. The obligation was model-carried at the one point where its input
23
+ * decays fastest.
24
+ *
25
+ * THIS GATE ASKS THE REMOTE. A version that compared HEAD against the local
26
+ * `origin/main` ref would reproduce the very defect it exists to catch: that
27
+ * ref is exactly the stale reading that caused the incident. So it runs
28
+ * `git ls-remote`, one network round trip, and compares against the SHA the
29
+ * server reports right now.
30
+ *
31
+ * It never fetches, merges, or rebases for you. Choosing how to catch up is a
32
+ * git-history decision (`git-history-discipline`), and a gate that rewrites
33
+ * history to make itself pass is a worse failure than the one it reports.
34
+ */
35
+ import { execFileSync } from "node:child_process";
36
+ import fs from "node:fs";
37
+ import os from "node:os";
38
+ import path from "node:path";
39
+ import { fileURLToPath } from "node:url";
40
+
41
+ import { runGateCli, runSelfTest, type SelfTestCase } from "./_lib/gate_self_test.js";
42
+ import { reportScanned } from "./_lib/scan_scope.js";
43
+
44
+ const DEFAULT_BASE = "main";
45
+ const SELF_TEST_MIN_CASES = 3;
46
+ const SELF_TEST_MIN_REJECT = 1;
47
+ const SCRIPT_REL = "src/scripts/check_branch_freshness.ts";
48
+
49
+ function git(args: string[]): string | null {
50
+ try {
51
+ return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /** The SHA the server reports for `<base>` right now — never a tracking ref. */
58
+ export function remoteHead(base: string, remote = "origin"): string | null {
59
+ const out = git(["ls-remote", "--heads", remote, base]);
60
+ if (out === null || out === "") {
61
+ return null;
62
+ }
63
+ const sha = out.split(/\s+/)[0];
64
+ return sha !== undefined && /^[0-9a-f]{40}$/.test(sha) ? sha : null;
65
+ }
66
+
67
+ /**
68
+ * Is `sha` already contained in HEAD?
69
+ *
70
+ * `merge-base --is-ancestor` needs the object locally. A SHA the server just
71
+ * reported may not be here yet, which is itself the answer — the branch cannot
72
+ * contain a commit it has never seen.
73
+ */
74
+ export function containsCommit(sha: string): boolean | null {
75
+ if (git(["cat-file", "-e", `${sha}^{commit}`]) === null) {
76
+ return false;
77
+ }
78
+ try {
79
+ execFileSync("git", ["merge-base", "--is-ancestor", sha, "HEAD"], { stdio: "ignore" });
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Build a throwaway origin + working clone, and optionally let a SECOND clone
88
+ * push so the working clone's tracking ref goes stale without it noticing.
89
+ *
90
+ * The stale-tracking-ref case is the whole point. A fixture that only advances
91
+ * the remote via the same clone would leave `origin/main` correct locally, and a
92
+ * gate reading the tracking ref would pass it — which is exactly the false green
93
+ * this gate exists to remove.
94
+ */
95
+ function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
96
+ const origin = path.join(root, "origin.git");
97
+ const work = path.join(root, "work");
98
+ fs.mkdirSync(origin, { recursive: true });
99
+ const g = (args: string[], at: string): void => {
100
+ execFileSync("git", args, { cwd: at, stdio: "ignore" });
101
+ };
102
+ const write = (at: string, name: string, body: string): void => {
103
+ fs.writeFileSync(path.join(at, name), body);
104
+ g(["add", "-A"], at);
105
+ g(["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", name], at);
106
+ };
107
+ g(["init", "--bare", "-b", DEFAULT_BASE], origin);
108
+ g(["init", "-b", DEFAULT_BASE, work], root);
109
+ g(["remote", "add", "origin", origin], work);
110
+ write(work, "a.txt", "one");
111
+ g(["push", "-u", "origin", DEFAULT_BASE], work);
112
+ g(["checkout", "-b", "feat/x"], work);
113
+ write(work, "b.txt", "two");
114
+ if (opts.advanceRemote) {
115
+ const other = path.join(root, "other");
116
+ g(["clone", origin, other], root);
117
+ write(other, "c.txt", "three");
118
+ g(["push"], other);
119
+ }
120
+ return work;
121
+ }
122
+
123
+ export function selfTest(repoRoot: string): number {
124
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "cbf-selftest-"));
125
+ const run = (advanceRemote: boolean, onBase: boolean): number => {
126
+ const dir = fs.mkdtempSync(path.join(root, "case-"));
127
+ const work = buildFixture(dir, { advanceRemote });
128
+ if (onBase) {
129
+ execFileSync("git", ["checkout", DEFAULT_BASE], { cwd: work, stdio: "ignore" });
130
+ }
131
+ // The child must not inherit a CI flag, or every case would no-op green.
132
+ delete process.env["CI"];
133
+ delete process.env["GITHUB_ACTIONS"];
134
+ return runGateCli(repoRoot, SCRIPT_REL, ["--quiet"], work);
135
+ };
136
+ const cases: SelfTestCase[] = [
137
+ {
138
+ name: "a branch whose tracking ref is stale while the remote has moved is REFUSED",
139
+ expect: "reject",
140
+ run: () => run(true, false),
141
+ },
142
+ {
143
+ name: "a branch containing the remote base passes",
144
+ expect: "accept",
145
+ run: () => run(false, false),
146
+ },
147
+ {
148
+ name: "standing on the base branch itself is a no-op, not a refusal",
149
+ expect: "accept",
150
+ run: () => run(false, true),
151
+ },
152
+ ];
153
+ try {
154
+ return runSelfTest({
155
+ gate: "check_branch_freshness",
156
+ cases,
157
+ minCases: SELF_TEST_MIN_CASES,
158
+ minRejectCases: SELF_TEST_MIN_REJECT,
159
+ });
160
+ } finally {
161
+ fs.rmSync(root, { recursive: true, force: true });
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Publish the scan scope: how many remote base refs this run actually resolved.
167
+ *
168
+ * One when the server answered, zero on every path that legitimately has
169
+ * nothing to ask — each with its reason, because a gate reporting success over
170
+ * an empty scope is the false green this repo keeps finding, and "there was
171
+ * nothing to check" and "I checked and it was fine" must not print the same.
172
+ */
173
+ function scanReport(scanned: number, allowEmpty?: string): void {
174
+ reportScanned({
175
+ gate: "check_branch_freshness",
176
+ scanned,
177
+ units: "remote base ref(s)",
178
+ roots: ["origin"],
179
+ ...(allowEmpty === undefined ? {} : { allowEmpty }),
180
+ });
181
+ }
182
+
183
+ export function main(argv: string[] = process.argv.slice(2)): number {
184
+ if (argv.includes("--self-test")) {
185
+ return selfTest(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."));
186
+ }
187
+ const quiet = argv.includes("--quiet");
188
+ const baseArg = argv.indexOf("--base");
189
+ const base = baseArg >= 0 ? (argv[baseArg + 1] ?? DEFAULT_BASE) : DEFAULT_BASE;
190
+
191
+ // CI merges or refuses on the server; a second opinion here would be noise.
192
+ if (process.env["CI"] === "true" || process.env["GITHUB_ACTIONS"] === "true") {
193
+ scanReport(0, "no-op in CI — the forge owns mergeability, and a second opinion here is noise");
194
+ return 0;
195
+ }
196
+
197
+ const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
198
+ if (branch === null || branch === "HEAD") {
199
+ scanReport(0, "detached HEAD — there is no branch whose freshness could be asked");
200
+ return 0;
201
+ }
202
+ if (branch === base) {
203
+ scanReport(0, `standing on ${base} itself — a branch cannot be behind itself`);
204
+ return 0;
205
+ }
206
+
207
+ const sha = remoteHead(base);
208
+ if (sha === null) {
209
+ // Loud, not silent. An offline run has not verified the invariant, and a
210
+ // gate that passes on an invariant it never evaluated is the false green
211
+ // this repo keeps finding.
212
+ scanReport(0, `origin/${base} unreachable — reported NOT VERIFIED rather than green`);
213
+ process.stdout.write(
214
+ `check_branch_freshness: could not reach origin/${base} — NOT VERIFIED (offline?). ` +
215
+ "Re-run before pushing.\n",
216
+ );
217
+ return 0;
218
+ }
219
+
220
+ scanReport(1);
221
+ if (containsCommit(sha) === true) {
222
+ if (!quiet) {
223
+ process.stdout.write(`✅ branch is current with origin/${base} (${sha.slice(0, 9)})` + "\n");
224
+ }
225
+ return 0;
226
+ }
227
+
228
+ console.error(`❌ branch is BEHIND origin/${base} — the remote is at ${sha.slice(0, 9)}.`);
229
+ console.error("");
230
+ console.error(" Pushing now opens a PR that may conflict, and worse: another branch");
231
+ console.error(" may already have shipped what this one is implementing.");
232
+ console.error("");
233
+ console.error(` git fetch origin && git merge origin/${base}`);
234
+ console.error(" then re-run the gates before pushing.");
235
+ console.error("");
236
+ console.error(
237
+ " This gate asks the REMOTE, not your tracking ref — a fetch from earlier in",
238
+ );
239
+ console.error(" the session is a memory, not a check (direct-answers Iron Law 2).");
240
+ return 1;
241
+ }
242
+
243
+ const invokedDirectly = ((): boolean => {
244
+ const entry = process.argv[1];
245
+ if (entry === undefined) return false;
246
+ try {
247
+ return fs.realpathSync(path.resolve(entry)) === fs.realpathSync(fileURLToPath(import.meta.url));
248
+ } catch {
249
+ return false;
250
+ }
251
+ })();
252
+
253
+ if (invokedDirectly) {
254
+ process.exit(main());
255
+ }
@@ -37,6 +37,14 @@
37
37
  * (ADR-104); any project-tree path is drift. Corrective mentions ("is
38
38
  * ignored", "never read", "no project-local", "superseded") pass on the
39
39
  * same line. (PATH_CHECK_GLOBS)
40
+ * 4. NO always-loaded carrier for the user-global fact. §1–§3 keep the tree
41
+ * honest but say nothing about REACH: before this check the fact lived
42
+ * only in `src/skills/ai-council/SKILL.md`, a skill body that reaches
43
+ * context only on activation, so consumer sessions repeatedly inferred
44
+ * "council not configured" from a missing `.agent-settings.yml` while the
45
+ * council was configured all along. §4 requires one rule under
46
+ * `src/rules/` to carry the fact and to project at least as widely as the
47
+ * kernel reference. (RULES_GLOB)
40
48
  *
41
49
  * Escape hatch: a line carrying `<!-- council-config-allowed -->` is exempt
42
50
  * (for a legitimate non-council `.agent-settings.yml` reference, e.g.
@@ -64,6 +72,7 @@ const SCAN_GLOBS = [
64
72
  'src/domains/meta/council/**/*.md',
65
73
  'src/domains/product-basic/roadmap/ai-council/**/*.md',
66
74
  'src/skills/ai-council/**/*.md',
75
+ 'src/rules/council-availability.md',
67
76
  'docs/contracts/ai-council-config.md',
68
77
  ] as const;
69
78
 
@@ -105,6 +114,35 @@ const PROJECT_PATH_COUNCIL_RE =
105
114
  const PATH_NEGATION_RE =
106
115
  /\b(not|never|removed|no\s+longer|neither|instead|ignored|superseded|no\s+project-local|not\s+read|does\s+not\s+read)\b/i;
107
116
 
117
+ // ── §4 always-loaded carrier check ─────────────────────────────────
118
+ // The user-global fact is useless where the agent cannot see it. Before this
119
+ // check the fact lived only in `src/skills/ai-council/SKILL.md` — a skill body
120
+ // that only reaches context on activation — so consumer sessions repeatedly
121
+ // inferred "council not configured" from a missing `.agent-settings.yml` and
122
+ // substituted a subagent fan-out while the council was configured all along.
123
+ // §4 pins the fix: at least one rule under `src/rules/` must carry the fact,
124
+ // declare a council trigger, and project at least as widely as the kernel.
125
+ const RULES_GLOB = 'src/rules/**/*.md';
126
+ // Reference for the reach requirement: a carrier scoped more narrowly than an
127
+ // always-loaded kernel rule would not reach the consumer repos that failed.
128
+ const REACH_REFERENCE_RULE = 'src/rules/direct-answers.md';
129
+ const WORKSPACES_RE = /^workspaces:\s*\[([^\]]*)\]/m;
130
+ // The three markers a carrier must show, all in the same file.
131
+ const CARRIER_MARKERS = [
132
+ {
133
+ re: /^\s*-\s*keyword:\s*"council"\s*$/m,
134
+ what: 'a `- keyword: "council"` trigger',
135
+ },
136
+ {
137
+ re: /~\/\.event4u\/agent-config\/settings\/\.ai-council\.yml/,
138
+ what: 'the one real user-global config path',
139
+ },
140
+ {
141
+ re: /NEVER\s+INFER\s+"NOT\s+CONFIGURED"/i,
142
+ what: 'the never-infer-from-project-files obligation',
143
+ },
144
+ ] as const;
145
+
108
146
  /**
109
147
  * Mirror Python `str.lstrip()` — strips all leading Unicode whitespace.
110
148
  * Python's `str.lstrip()` (no args) strips the Unicode whitespace class;
@@ -318,6 +356,84 @@ function find_path_violations(root: string): string[] {
318
356
  return findings;
319
357
  }
320
358
 
359
+ /** Parse a rule's `workspaces: [a, b, c]` frontmatter line into a set. */
360
+ function _workspaces(text: string): Set<string> {
361
+ const m = WORKSPACES_RE.exec(text);
362
+ if (m === null) {
363
+ return new Set<string>();
364
+ }
365
+ return new Set(
366
+ m[1]!
367
+ .split(',')
368
+ .map((s) => s.trim().replace(/^["']|["']$/g, ''))
369
+ .filter((s) => s.length > 0),
370
+ );
371
+ }
372
+
373
+ /**
374
+ * §4 — the user-global fact must have an always-loaded carrier (reach check).
375
+ *
376
+ * A correct resolver, a correct skill, and a green §1–§3 still left consumer
377
+ * sessions claiming "council not configured" — because the fact reached no
378
+ * always-loaded surface. This check fails when no rule under `src/rules/`
379
+ * carries it, and when every carrier that does is scoped more narrowly than
380
+ * the kernel reference (i.e. would not reach the consumer repos that failed).
381
+ */
382
+ function find_carrier_violations(root: string): string[] {
383
+ // Scope guard: only a tree that HAS a rule layer can carry the fact. A
384
+ // consumer checkout or a tmp fixture without `src/rules/` is out of scope,
385
+ // exactly as a missing `src/skills/ai-council/` is for §1. The real-repo
386
+ // test asserts this scope is live here, so the skip cannot go unnoticed.
387
+ if (!_isDir(path.join(root, 'src', 'rules'))) {
388
+ return [];
389
+ }
390
+ const rules = [...iter_files(root, [RULES_GLOB])];
391
+ if (rules.length === 0) {
392
+ return [
393
+ `${RULES_GLOB}: \`src/rules/\` exists but resolved to no files — ` +
394
+ 'the council user-global fact is unpinned.',
395
+ ];
396
+ }
397
+ const carriers = rules.filter((p) => {
398
+ const text = fs.readFileSync(p, 'utf-8');
399
+ return CARRIER_MARKERS.every((m) => m.re.test(text));
400
+ });
401
+ if (carriers.length === 0) {
402
+ return [
403
+ 'src/rules/: no always-loaded rule carries the council user-global ' +
404
+ 'fact. One rule must show all of — ' +
405
+ CARRIER_MARKERS.map((m) => m.what).join('; ') +
406
+ '. Without it the fact lives only in ' +
407
+ '`src/skills/ai-council/SKILL.md`, whose body reaches context ' +
408
+ 'only on skill activation.',
409
+ ];
410
+ }
411
+ const refPath = path.join(root, REACH_REFERENCE_RULE);
412
+ if (!_isFile(refPath)) {
413
+ return [];
414
+ }
415
+ const refWs = _workspaces(fs.readFileSync(refPath, 'utf-8'));
416
+ if (refWs.size === 0) {
417
+ return [];
418
+ }
419
+ const findings: string[] = [];
420
+ for (const p of carriers) {
421
+ const missing = [...refWs].filter(
422
+ (w) => !_workspaces(fs.readFileSync(p, 'utf-8')).has(w),
423
+ );
424
+ if (missing.length > 0) {
425
+ findings.push(
426
+ `${_relPosix(root, p)}: council carrier is scoped more narrowly ` +
427
+ `than \`${REACH_REFERENCE_RULE}\` — missing workspace(s): ` +
428
+ `${missing.join(', ')}. A carrier the consumer never receives ` +
429
+ 'is exactly the gap this check exists to close.',
430
+ );
431
+ }
432
+ }
433
+ // One carrier with full reach is enough; only report when none has it.
434
+ return findings.length === carriers.length ? findings : [];
435
+ }
436
+
321
437
  function main(): number {
322
438
  const root = process.cwd();
323
439
  // PATH_CHECK_GLOBS is the superset of both passes, so its resolution is the
@@ -338,7 +454,11 @@ function main(): number {
338
454
  }
339
455
  throw exc;
340
456
  }
341
- const findings = [...find_violations(root), ...find_path_violations(root)];
457
+ const findings = [
458
+ ...find_violations(root),
459
+ ...find_path_violations(root),
460
+ ...find_carrier_violations(root),
461
+ ];
342
462
  if (findings.length) {
343
463
  process.stdout.write('❌ Council config-location violations:\n\n');
344
464
  for (const f of findings) {
@@ -396,5 +516,9 @@ export {
396
516
  iter_files,
397
517
  find_violations,
398
518
  find_path_violations,
519
+ find_carrier_violations,
520
+ RULES_GLOB,
521
+ REACH_REFERENCE_RULE,
522
+ CARRIER_MARKERS,
399
523
  main,
400
524
  };