@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
|
@@ -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 = [
|
|
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
|
};
|