@event4u/agent-config 9.23.0 → 9.25.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 (52) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +82 -0
  3. package/README.md +2 -2
  4. package/dist/agent-src/commands/agent-handoff.md +10 -3
  5. package/dist/agent-src/commands/analyze/conformance.md +182 -0
  6. package/dist/agent-src/commands/analyze/inbox.md +221 -0
  7. package/dist/agent-src/commands/analyze.md +4 -3
  8. package/dist/agent-src/commands/pr/create.md +6 -6
  9. package/dist/agent-src/rules/context-hygiene.md +3 -0
  10. package/dist/agent-src/rules/decision-revisit-gate.md +2 -0
  11. package/dist/agent-src/rules/evaluator-independence.md +105 -0
  12. package/dist/agent-src/rules/session-canary.md +13 -1
  13. package/dist/agent-src/rules/user-interaction.md +22 -0
  14. package/dist/agent-src/skills/finishing-a-development-branch/SKILL.md +1 -1
  15. package/dist/agent-src/skills/rule-writing/SKILL.md +45 -0
  16. package/dist/agent-src/skills/skill-writing/SKILL.md +114 -0
  17. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  18. package/dist/cli/registry.js +1 -0
  19. package/dist/cli/registry.js.map +1 -1
  20. package/dist/discovery/deprecation-report.md +1 -1
  21. package/dist/discovery/discovery-manifest.json +96 -22
  22. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  23. package/dist/discovery/discovery-manifest.summary.md +4 -4
  24. package/dist/discovery/orphan-report.md +1 -1
  25. package/dist/discovery/packs.json +11 -8
  26. package/dist/discovery/trust-report.md +3 -3
  27. package/dist/discovery/workspaces.json +9 -6
  28. package/dist/hooks/dispatch.js +1026 -328
  29. package/dist/mcp/registry-manifest.json +2 -2
  30. package/dist/mcp/server.json +2 -2
  31. package/dist/router.json +1 -1
  32. package/package.json +1 -1
  33. package/src/config/evaluator-budgets.json +68 -68
  34. package/src/config/gate-coverage.yml +103 -5
  35. package/src/config/gate-violation-baselines.json +11 -6
  36. package/src/scripts/_dispatch.bash +12 -0
  37. package/src/scripts/before_complete_hook.ts +127 -5
  38. package/src/scripts/check_depth_budget.ts +295 -0
  39. package/src/scripts/conformance_scan.ts +392 -0
  40. package/src/scripts/git_authorization_hook.ts +263 -0
  41. package/src/scripts/hook_manifest.yaml +50 -9
  42. package/src/scripts/hooks/block_unauthorized_git.ts +254 -0
  43. package/src/scripts/hooks/concern_registry.ts +8 -0
  44. package/src/scripts/hooks/evidence_independence.ts +334 -0
  45. package/src/scripts/language_mirror_hook.ts +267 -0
  46. package/src/scripts/lint_decision_impact.ts +246 -0
  47. package/src/scripts/lint_example_fences.ts +609 -0
  48. package/src/scripts/lint_rule_interactions.ts +90 -0
  49. package/src/scripts/lint_rule_migration_ledger.ts +480 -0
  50. package/src/scripts/lint_skill_descriptions.ts +86 -2
  51. package/src/scripts/measure_rule_absoluta.ts +231 -0
  52. package/src/scripts/schemas/rule.schema.json +10 -0
@@ -44,6 +44,17 @@
44
44
  * construction and wake up exactly when a new skill lands close to an existing
45
45
  * one. That is the point.
46
46
  *
47
+ * A seventh check runs on EVERY skill and is neither absence nor presence — it
48
+ * is a prohibition:
49
+ *
50
+ * (g) preemption-phrase — the description claims priority over a sibling, or
51
+ * instructs activation regardless of the request.
52
+ * Unlike (a)-(f), this defect harms OTHER skills: it
53
+ * wins turns that belonged to them, and the loss is
54
+ * invisible at the source. See PREEMPTION_PATTERNS
55
+ * for why every pattern is anchored on the directive
56
+ * rather than on a vocabulary word.
57
+ *
47
58
  * Deterministic, static-config only. Allowlist mirrors lint_skill_originality:
48
59
  * `lint_skill_descriptions_allowlist.json`, capped at 20 (autonomous-execution
49
60
  * allowlist-growth antipattern — over-cap means the linter is wrong).
@@ -78,6 +89,62 @@ const CONDITION_RE =
78
89
  const IMPERATIVE_LEAD_RE =
79
90
  /^["'`]*\s*(write|author|convert|review|run|add|build|generate|create|fix|wire|detect|analy[sz]e|optimi[sz]e|draft|extract|route|turn|produce|score|consolidate|rate|model|estimate|plan|check|audit|ship|emit|render|guide|poll|capture|explain|map|prepare|handle|manage|track|compare|validate|verify|enforce|choose|pick|enumerate|investigate|format|scaffold|design|define|describe|gate|block|refactor|migrate|transcribe|summari[sz]e|orchestrate|surface|propose|walk|stress-test|red-team|challenge|bootstrap|seed|install|deploy|package|split|merge|condense|harvest|evaluate|assess|diagnose|reproduce|trace|inspect|lint|test)/i;
80
91
 
92
+ /**
93
+ * (g) Preemption phrases — a description that argues for itself against the
94
+ * rest of the catalogue.
95
+ *
96
+ * Every other check here asks whether a description carries ENOUGH signal.
97
+ * This one is the opposite: a description claiming priority over a sibling, or
98
+ * instructing activation regardless of the request, degrades routing **globally
99
+ * and invisibly**. It does not misroute its own skill — it wins turns that
100
+ * belonged to other skills, and the loss surfaces as those skills mysteriously
101
+ * under-firing. The sweep behind this check found the class live in a shipped
102
+ * third-party description and in a second suite's own catalogue.
103
+ *
104
+ * Every pattern is anchored on the **directive**, never on a vocabulary word,
105
+ * and that is the whole design. A first pass keyed on the words alone
106
+ * (`overrides`, `supersedes`, `for every …`) returned three hits on this corpus
107
+ * and **all three were false positives**: `override-management` manages
108
+ * overrides, `decision-review` asks whether an ADR should be superseded, and
109
+ * `prediction-pool-optimizer` answers "every question" in a prediction pool.
110
+ * Those are subjects, not claims. The patterns below therefore require the
111
+ * shape of an instruction to the router (`always use`, `use this instead of`,
112
+ * `this skill takes precedence`), which no domain noun produces.
113
+ */
114
+ const PREEMPTION_PATTERNS: ReadonlyArray<readonly [string, RegExp]> = [
115
+ [
116
+ 'unconditional activation',
117
+ /\b(?:always|invariably|unconditionally)\s+(?:use|load|apply|run|activate|invoke|prefer)\b/i,
118
+ ],
119
+ [
120
+ 'activation regardless of the request',
121
+ /\b(?:regardless of|no matter)\s+(?:the|what|which)\b|\b(?:for|on)\s+(?:every|all|any)\s+(?:request|turn|prompt|conversation|session)s?\b|\beven if not (?:asked|requested)\b/i,
122
+ ],
123
+ [
124
+ 'priority over a sibling',
125
+ /\b(?:use|load|apply|invoke|prefer)\s+(?:this|these|it)\b[^.;]{0,60}?\b(?:instead of|rather than|over|in preference to)\b/i,
126
+ ],
127
+ [
128
+ 'authority claim over the catalogue',
129
+ /\bthis (?:skill|one|artifact)\s+(?:takes precedence|outranks|supersedes|overrides|wins)\b|\b(?:takes precedence over|outranks)\s+(?:all|any|every)\b/i,
130
+ ],
131
+ [
132
+ 'load-order claim',
133
+ /\b(?:before|prior to)\s+(?:any|all)\s+other\s+(?:skill|tool|rule)s?\b|\bload (?:this )?first\b/i,
134
+ ],
135
+ ];
136
+
137
+ /** The first preemption claim in a description, or null. */
138
+ function preemptionPhrase(desc: string): { kind: string; match: string } | null {
139
+ for (const [kind, rx] of PREEMPTION_PATTERNS) {
140
+ const m = rx.exec(desc);
141
+ if (m) {
142
+ return { kind: kind as string, match: m[0] };
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+
81
148
  interface Violation {
82
149
  slug: string;
83
150
  code:
@@ -86,7 +153,8 @@ interface Violation {
86
153
  | 'triggers-are-name'
87
154
  | 'no-routing-signal'
88
155
  | 'clustered-no-sibling-routing'
89
- | 'clustered-no-quoted-phrase';
156
+ | 'clustered-no-quoted-phrase'
157
+ | 'preemption-phrase';
90
158
  detail: string;
91
159
  }
92
160
 
@@ -204,6 +272,22 @@ function analyseSkill(slug: string, fm: Record<string, string>, siblings: readon
204
272
  });
205
273
  }
206
274
 
275
+ // (g) a claim of priority over the rest of the catalogue. Unlike (a)-(d),
276
+ // this defect harms OTHER skills, so it is checked on every skill and is
277
+ // never scoped to a cluster — a preemption phrase costs turns regardless of
278
+ // whether the router already had a near-neighbour to confuse it with.
279
+ const preemption = preemptionPhrase(desc);
280
+ if (preemption) {
281
+ out.push({
282
+ slug,
283
+ code: 'preemption-phrase',
284
+ detail:
285
+ `description claims ${preemption.kind} ("${preemption.match}") — a description ` +
286
+ 'argues for its own routing conditions, never against its siblings; this wins ' +
287
+ 'turns that belonged to other skills and the loss is invisible at the source',
288
+ });
289
+ }
290
+
207
291
  // (e) + (f) fire ONLY for skills the canonical tool put in a cluster.
208
292
  if (siblings.length > 0) {
209
293
  const unrouted = siblings.filter((s) => !desc.includes(s));
@@ -332,4 +416,4 @@ if (path.resolve(process.argv[1] ?? '') === path.resolve(_HERE)) {
332
416
  process.exit(main(process.argv.slice(2)));
333
417
  }
334
418
 
335
- export { analyseSkill, normalize, triggerPhrases, main };
419
+ export { analyseSkill, normalize, triggerPhrases, preemptionPhrase, main };
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Measure how many rules carry an imperative ABSOLUTE, and which terms.
4
+ *
5
+ * WHY THIS EXISTS. `road-to-rule-coherence` was designed around the figure
6
+ * "17 rules carry competing absolutes". A scratch census during that roadmap
7
+ * refuted it and reported 97 of 111 — and that refutation is what killed the
8
+ * precedence lattice and the coverage ratchet: a governance mechanism over 17
9
+ * outliers is proportionate, the same mechanism over ~90% of the corpus is
10
+ * declaration debt across almost all of it.
11
+ *
12
+ * But the 97 came from a throwaway script in a scratch directory, so it was an
13
+ * unverified number replacing an unverified number — the exact failure class it
14
+ * had just exposed. Re-deriving it here shows BOTH prior figures were lexicon
15
+ * artifacts:
16
+ *
17
+ * strict (ALL-CAPS imperatives only) 79 / 111 (71.2%)
18
+ * same lexicon, case-insensitive 97 / 111 (87.4%) <- the prior 97
19
+ * carries an Iron Law block (structural) 94 / 111 (84.7%)
20
+ *
21
+ * The 97 is reproducible — it is the case-insensitive reading, which counts
22
+ * lowercase "must"/"never" in explanatory prose as mandates. The strict reading
23
+ * is the better proxy for an imperative, but it has false negatives of its own:
24
+ * `downstream-changes` carries a full Iron Law block ("EVERY EDIT IS INCOMPLETE
25
+ * ... IS A CRITICAL FAILURE") using none of the lexicon terms, and scores 0.
26
+ *
27
+ * So no single number is defensible, and the third row is why that does not
28
+ * matter: the structural count needs no lexicon at all, and all three readings
29
+ * land at 71-87%. The conclusion the roadmap actually rests on — absolutes are
30
+ * the house style, not 17 outliers — is invariant to the method. Cite the range
31
+ * and the structural figure; do not cite a point estimate.
32
+ *
33
+ * NOT A GATE, deliberately. The `measure_` prefix keeps it out of
34
+ * `_lib/gate_population`'s gate set, and it emits no `scanned:` contract line,
35
+ * so it carries no `gate-coverage.yml` registration duty. There is nothing here
36
+ * to enforce: "how many rules speak in absolutes" is a property of the house
37
+ * writing style, not a defect with a threshold. Adding a threshold would
38
+ * recreate the ratchet the measurement itself argued against.
39
+ *
40
+ * Frontmatter is excluded — `description:` routinely contains trigger words
41
+ * that are not imperatives addressed to the agent.
42
+ *
43
+ * Output: default table, `--json` for the deterministic record.
44
+ *
45
+ * SCOPE: this is a CLOSED decision input, not ongoing instrumentation — see
46
+ * docs/decisions/ADR-218-absoluta-census-is-a-closed-decision-input.md. Nothing
47
+ * consumes the number; it exists so the figures in that record can be
48
+ * re-derived instead of cited from a commit message. A future "did this PR
49
+ * introduce a new absolute?" check is a DIFFERENT tool that watches diffs.
50
+ */
51
+
52
+ import * as fs from 'node:fs';
53
+ import * as path from 'node:path';
54
+ import { pathToFileURL } from 'node:url';
55
+
56
+ import { SRC_RULES } from './_lib/agent_src.js';
57
+
58
+ /**
59
+ * The lexicon IS the measurement — enumerated here rather than described, so a
60
+ * re-run reproduces the number exactly and a disagreement about the number is a
61
+ * disagreement about this list.
62
+ *
63
+ * Case-sensitive where the term is an ALL-CAPS imperative (`NEVER`, `ALWAYS`,
64
+ * `STOP`, `MUST`, `FORBIDDEN`): lowercase "never" appears in ordinary prose
65
+ * ("never mind that") and is not a mandate. Case-insensitive where the phrase
66
+ * is a mandate in any casing (`non-bypassable`, `Hard Floor`, `no exceptions`)
67
+ * or a verb stem (`refuse`, `abort`).
68
+ */
69
+ export const ABSOLUTA_LEXICON: ReadonlyArray<{
70
+ term: string;
71
+ pattern: RegExp;
72
+ note: string;
73
+ }> = [
74
+ { term: 'NEVER', pattern: /\bNEVER\b/g, note: 'ALL-CAPS only — lowercase "never" is ordinary prose' },
75
+ { term: 'ALWAYS', pattern: /\bALWAYS\b/g, note: 'ALL-CAPS only' },
76
+ { term: 'STOP', pattern: /\bSTOP\b/g, note: 'ALL-CAPS only' },
77
+ { term: 'MUST', pattern: /\bMUST\b/g, note: 'ALL-CAPS only — lowercase "must" is common in explanatory prose' },
78
+ { term: 'FORBIDDEN', pattern: /\bFORBIDDEN\b/g, note: 'ALL-CAPS only' },
79
+ { term: 'non-bypassable', pattern: /non-bypassable/gi, note: 'any casing — always a mandate' },
80
+ { term: 'Hard Floor', pattern: /hard floor/gi, note: 'any casing — names the supremacy tier' },
81
+ { term: 'no exceptions', pattern: /no exceptions/gi, note: 'any casing' },
82
+ { term: 'refuse', pattern: /\brefuses?\b|\brefusal\b/gi, note: 'verb stem, any casing' },
83
+ { term: 'abort', pattern: /\baborts?\b/gi, note: 'verb stem, any casing' },
84
+ ];
85
+
86
+ export interface RuleAbsoluta {
87
+ rule: string;
88
+ total: number;
89
+ perTerm: Record<string, number>;
90
+ /** True when the file has a body but no absolute — the pointer-stub class. */
91
+ stub: boolean;
92
+ }
93
+
94
+ export interface AbsolutaCensus {
95
+ rules_scanned: number;
96
+ rules_with_absolute: number;
97
+ rules_without_absolute: number;
98
+ share_with_absolute: number;
99
+ occurrences_total: number;
100
+ per_term: Record<string, { occurrences: number; rules: number }>;
101
+ without_absolute: string[];
102
+ per_rule: RuleAbsoluta[];
103
+ /** Case-insensitive reading of the same lexicon — the upper bound. */
104
+ rules_with_absolute_loose: number;
105
+ /**
106
+ * Structural, lexicon-independent signal: rules carrying an `Iron Law`
107
+ * heading or a `**Iron Law` marker. This is the number that does NOT move
108
+ * when the lexicon is argued about, which is why it is reported alongside.
109
+ */
110
+ rules_with_iron_law: number;
111
+ }
112
+
113
+ /** `## Iron Law`, `### Iron Laws`, `**Iron Law.**`, `Iron Law 1`, … */
114
+ const IRON_LAW_MARKER = /^#{1,6}\s+.*Iron Law|^\*\*Iron Law/im;
115
+
116
+ /** Strip YAML frontmatter; return the body only. */
117
+ export function stripFrontmatter(text: string): string {
118
+ if (!text.startsWith('---\n')) return text;
119
+ const end = text.indexOf('\n---\n', 4);
120
+ if (end === -1) return text;
121
+ return text.slice(end + '\n---\n'.length);
122
+ }
123
+
124
+ export function censusAbsoluta(rulesDir: string): AbsolutaCensus {
125
+ const files = fs
126
+ .readdirSync(rulesDir)
127
+ .filter((f) => f.endsWith('.md'))
128
+ .sort();
129
+
130
+ const perRule: RuleAbsoluta[] = [];
131
+ const perTerm: Record<string, { occurrences: number; rules: number }> = {};
132
+ for (const { term } of ABSOLUTA_LEXICON) perTerm[term] = { occurrences: 0, rules: 0 };
133
+
134
+ let occurrences = 0;
135
+ let looseHits = 0;
136
+ let ironLawHits = 0;
137
+ for (const file of files) {
138
+ const body = stripFrontmatter(fs.readFileSync(path.join(rulesDir, file), 'utf-8'));
139
+ const counts: Record<string, number> = {};
140
+ let total = 0;
141
+ let looseTotal = 0;
142
+ for (const { term, pattern } of ABSOLUTA_LEXICON) {
143
+ // Fresh regex per file: /g patterns carry lastIndex across calls.
144
+ const n = (body.match(new RegExp(pattern.source, pattern.flags)) ?? []).length;
145
+ counts[term] = n;
146
+ total += n;
147
+ if (n > 0) {
148
+ perTerm[term]!.occurrences += n;
149
+ perTerm[term]!.rules += 1;
150
+ }
151
+ // Loose reading: same lexicon, case-insensitive throughout. This is
152
+ // the upper bound, and it is the reading a prior scratch census
153
+ // used — reproducing it here is what identifies that method.
154
+ looseTotal += (body.match(new RegExp(pattern.source, 'gi')) ?? []).length;
155
+ }
156
+ occurrences += total;
157
+ if (looseTotal > 0) looseHits += 1;
158
+ if (IRON_LAW_MARKER.test(body)) ironLawHits += 1;
159
+ perRule.push({ rule: path.basename(file, '.md'), total, perTerm: counts, stub: total === 0 });
160
+ }
161
+
162
+ const withAbs = perRule.filter((r) => r.total > 0).length;
163
+ return {
164
+ rules_scanned: perRule.length,
165
+ rules_with_absolute: withAbs,
166
+ rules_without_absolute: perRule.length - withAbs,
167
+ share_with_absolute: perRule.length === 0 ? 0 : Number((withAbs / perRule.length).toFixed(4)),
168
+ occurrences_total: occurrences,
169
+ per_term: perTerm,
170
+ without_absolute: perRule.filter((r) => r.stub).map((r) => r.rule),
171
+ per_rule: perRule,
172
+ rules_with_absolute_loose: looseHits,
173
+ rules_with_iron_law: ironLawHits,
174
+ };
175
+ }
176
+
177
+ function render(c: AbsolutaCensus): string {
178
+ const lines: string[] = [];
179
+ lines.push('Rule absoluta census');
180
+ lines.push('');
181
+ lines.push(
182
+ ` rules scanned ${c.rules_scanned}`,
183
+ );
184
+ lines.push(
185
+ ` carry >=1 absolute ${c.rules_with_absolute} (${(c.share_with_absolute * 100).toFixed(1)}%)`,
186
+ );
187
+ lines.push(` carry none ${c.rules_without_absolute}`);
188
+ lines.push(
189
+ ` same lexicon, loose ${c.rules_with_absolute_loose} (${((c.rules_with_absolute_loose / c.rules_scanned) * 100).toFixed(1)}%) <- case-insensitive upper bound`,
190
+ );
191
+ lines.push(
192
+ ` carry an Iron Law ${c.rules_with_iron_law} (${((c.rules_with_iron_law / c.rules_scanned) * 100).toFixed(1)}%) <- structural, lexicon-free`,
193
+ );
194
+ lines.push(` total occurrences ${c.occurrences_total} (strict)`);
195
+ lines.push('');
196
+ lines.push(' per term (occurrences / rules):');
197
+ for (const { term } of ABSOLUTA_LEXICON) {
198
+ const e = c.per_term[term]!;
199
+ lines.push(` ${term.padEnd(16)} ${String(e.occurrences).padStart(4)} / ${e.rules}`);
200
+ }
201
+ if (c.without_absolute.length > 0) {
202
+ lines.push('');
203
+ lines.push(' rules with NO absolute (expected: migrated pointer stubs):');
204
+ for (const r of c.without_absolute) lines.push(` ${r}`);
205
+ }
206
+ return lines.join('\n');
207
+ }
208
+
209
+ export function main(argv: string[]): number {
210
+ const json = argv.includes('--json');
211
+ const dir = SRC_RULES();
212
+ if (!fs.existsSync(dir)) {
213
+ process.stderr.write(`measure_rule_absoluta: rules dir missing: ${dir}\n`);
214
+ return 1;
215
+ }
216
+ const c = censusAbsoluta(dir);
217
+ if (c.rules_scanned === 0) {
218
+ // A census over zero files would print a confident 0% — the
219
+ // scan-nothing-and-look-green shape this repo has been burned by.
220
+ process.stderr.write(`measure_rule_absoluta: scanned 0 rules under ${dir}\n`);
221
+ return 1;
222
+ }
223
+ process.stdout.write((json ? JSON.stringify(c, null, 2) : render(c)) + '\n');
224
+ return 0;
225
+ }
226
+
227
+ /* c8 ignore start */
228
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
229
+ process.exitCode = main(process.argv.slice(2));
230
+ }
231
+ /* c8 ignore stop */
@@ -18,6 +18,16 @@
18
18
  "enum": ["package", "project"],
19
19
  "default": "package"
20
20
  },
21
+ "decision_impact": {
22
+ "type": "string",
23
+ "enum": ["overrides-model-default", "encodes-house-choice", "already-complied-with"],
24
+ "description": "What is at stake when this rule fires — distinct from `type`, which is a DELIVERY class (how the rule reaches the model). `overrides-model-default`: the model's base behaviour is wrong here, so the rule is load-bearing. `encodes-house-choice`: the model would make *a* defensible choice but not *our* choice. `already-complied-with`: the model does this unprompted — the one value that permits removal, and therefore the one carrying an evidence bar (`decision_impact_evidence`, enforced by lint_decision_impact). OPTIONAL by design: classifying all 111 rules in one batch is exactly the shape that trips check_kernel_prefix_stability, so it lands on new rules and on rules touched for another reason."
25
+ },
26
+ "decision_impact_evidence": {
27
+ "type": "string",
28
+ "minLength": 20,
29
+ "description": "Required when `decision_impact: already-complied-with`, forbidden otherwise. A pointer a reviewer can follow to the observation that the model complies without this rule — an eval report, a PR discussion, a trajectory sample. The bar exists because that classification is what permits deleting a rule, and 'the model probably does this anyway' is the cheapest possible way to remove a floor that was working precisely because nothing had crossed it."
30
+ },
21
31
  "description": {
22
32
  "type": "string",
23
33
  "minLength": 1,