@event4u/agent-config 9.28.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.
Files changed (147) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +6 -6
  3. package/CHANGELOG.md +66 -0
  4. package/README.md +1 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/cost/report.md +2 -2
  7. package/dist/agent-src/commands/team/adversarial.md +8 -7
  8. package/dist/agent-src/commands/team/delegate.md +13 -12
  9. package/dist/agent-src/commands/team/review.md +9 -8
  10. package/dist/agent-src/commands/team/status.md +9 -8
  11. package/dist/agent-src/commands/team.md +29 -19
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  13. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  14. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  15. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +18 -0
  16. package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
  17. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  18. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  19. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  20. package/dist/agent-src/rules/delegation-policy.md +11 -7
  21. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  22. package/dist/agent-src/rules/token-budget-discipline.md +1 -1
  23. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  24. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  25. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  26. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  27. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
  28. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  29. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  30. package/dist/agent-src/templates/AGENTS.md +4 -4
  31. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  32. package/dist/agent-src/templates/roadmaps.md +23 -0
  33. package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
  34. package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
  35. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  36. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
  37. package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
  38. package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-NZ5N2XQL.js} +3 -3
  39. package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-OJM3MBJM.js} +1 -1
  40. package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-RCIA67XN.js} +111 -90
  41. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  42. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  43. package/dist/cli-delegate/cmd_conformance.js +6 -43
  44. package/dist/cli-delegate/cmd_doctor.js +9 -5
  45. package/dist/cli-delegate/cmd_explain.js +2 -2
  46. package/dist/cli-delegate/cmd_export.js +4 -4
  47. package/dist/cli-delegate/cmd_handoff.js +3 -3
  48. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  49. package/dist/cli-delegate/cmd_migrate.js +2 -2
  50. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  51. package/dist/cli-delegate/cmd_prune.js +4 -4
  52. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  53. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  54. package/dist/cli-delegate/cmd_settings_get.js +1 -7
  55. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  56. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  57. package/dist/cli-delegate/cmd_sync.js +4 -4
  58. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  59. package/dist/cli-delegate/cmd_update.js +1 -1
  60. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  61. package/dist/cli-delegate/cmd_validate.js +4 -4
  62. package/dist/cli-delegate/cmd_versions.js +1 -1
  63. package/dist/discovery/deprecation-report.md +1 -1
  64. package/dist/discovery/discovery-manifest.json +69 -20
  65. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  66. package/dist/discovery/discovery-manifest.summary.md +3 -2
  67. package/dist/discovery/orphan-report.md +1 -1
  68. package/dist/discovery/packs.json +39 -2
  69. package/dist/discovery/trust-report.md +2 -2
  70. package/dist/discovery/workspaces.json +12 -4
  71. package/dist/hooks/dispatch.js +373 -35
  72. package/dist/install/install.mjs +42 -17
  73. package/dist/mcp/registry-manifest.json +2 -2
  74. package/dist/mcp/server.json +2 -2
  75. package/dist/mcp/server.mjs +651 -262
  76. package/dist/router.json +1 -1
  77. package/dist/scripts/_lib/agent_settings.js +54 -0
  78. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  79. package/dist/scripts/condense.js +147 -2
  80. package/dist/scripts/condense.js.map +1 -1
  81. package/dist/server/schemas/settings.js +6 -7
  82. package/dist/server/schemas/settings.js.map +1 -1
  83. package/dist/shared/settingsCarveOut.js +0 -9
  84. package/dist/shared/settingsCarveOut.js.map +1 -1
  85. package/dist/shared/settingsClasses.js +11 -9
  86. package/dist/shared/settingsClasses.js.map +1 -1
  87. package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
  88. package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
  89. package/dist/ui/index.html +1 -1
  90. package/docs/contracts/settings-classes.md +39 -9
  91. package/package.json +1 -1
  92. package/src/agent-src/templates/AGENTS.md +4 -4
  93. package/src/config/agent-settings.template.yml +52 -66
  94. package/src/config/discovery/packs.yml +11 -0
  95. package/src/config/discovery/workspaces.yml +1 -1
  96. package/src/config/gate-violation-baselines.json +3 -3
  97. package/src/scripts/_cli/cmd_conformance.ts +14 -42
  98. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  99. package/src/scripts/_lib/agent_settings.ts +58 -0
  100. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  101. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  102. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  103. package/src/scripts/_lib/host_capability.ts +63 -6
  104. package/src/scripts/_lib/judgment_ladder.ts +382 -0
  105. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  106. package/src/scripts/_lib/orchestration_record.ts +37 -0
  107. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  108. package/src/scripts/_lib/self_repair.ts +160 -13
  109. package/src/scripts/_lib/self_repair_store.ts +28 -0
  110. package/src/scripts/_lib/subagent_capsule.ts +182 -0
  111. package/src/scripts/_lib/subagent_response.ts +19 -0
  112. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  113. package/src/scripts/_lib/subagent_steering.ts +9 -4
  114. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  115. package/src/scripts/_lib/worker_budget.ts +37 -2
  116. package/src/scripts/ai_council/clients.ts +76 -54
  117. package/src/scripts/ai_council/config.ts +94 -6
  118. package/src/scripts/ai_council/handoff.ts +113 -0
  119. package/src/scripts/ai_council/modes.ts +19 -4
  120. package/src/scripts/ai_council/orchestrator.ts +105 -0
  121. package/src/scripts/ai_council/quorum.ts +84 -0
  122. package/src/scripts/ai_council/session.ts +44 -0
  123. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  124. package/src/scripts/ai_team/availability.ts +110 -0
  125. package/src/scripts/ai_team/config.ts +34 -14
  126. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  127. package/src/scripts/bench_critic_protocol.ts +396 -0
  128. package/src/scripts/check_detector_corpus.ts +202 -0
  129. package/src/scripts/check_hook_bundle_freshness.ts +4 -1
  130. package/src/scripts/condense.ts +162 -2
  131. package/src/scripts/council_cli.ts +401 -25
  132. package/src/scripts/forensics_report.ts +333 -0
  133. package/src/scripts/hooks/delegation_nudge_hook.ts +69 -35
  134. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  135. package/src/scripts/lint_never_silent.ts +276 -0
  136. package/src/scripts/lint_no_activation_gates.ts +271 -0
  137. package/src/scripts/orchestration_record.ts +8 -0
  138. package/src/scripts/rebuild_hook_bundle.ts +177 -0
  139. package/src/scripts/release.ts +74 -2
  140. package/src/scripts/routing_doctor.ts +21 -36
  141. package/src/scripts/score_skill_selection.ts +19 -5
  142. package/src/scripts/self_repair_cli.ts +287 -57
  143. package/src/scripts/team_events_spike.ts +271 -0
  144. package/src/scripts/team_review_gate_hook.ts +31 -9
  145. package/src/server/schemas/settings.ts +10 -17
  146. package/src/shared/settingsCarveOut.ts +0 -10
  147. package/src/shared/settingsClasses.ts +11 -9
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Detector corpus gate (P4.3 of road-to-rule-delivery-integrity).
4
+ *
5
+ * Every self-repair detector must carry all THREE fixture classes, and every
6
+ * fixture must behave as its class says:
7
+ *
8
+ * fire a recorded real failure the detector must catch
9
+ * near-miss-fire a real failure phrased unlike the canonical one, which must
10
+ * still be caught — recall at the boundary
11
+ * must-not-fire text that superficially matches and must stay silent
12
+ *
13
+ * The three-class rule is the point of the gate rather than a stylistic one.
14
+ * Precision and recall fail in opposite directions and a single corpus hides
15
+ * that: a detector with `fire` cases only looks perfect while opening records on
16
+ * praise, which is exactly what `detectUserReport` did until 2026-08-09 (it
17
+ * fired on "das ist fine, du hast nichts falsch gemacht"). A missing class is
18
+ * therefore a gate failure, not a warning — the absent half is where the defects
19
+ * live.
20
+ *
21
+ * Registration is explicit: a detector added to `self_repair.ts` without a
22
+ * matching corpus entry fails here, which is the coupling that keeps the corpus
23
+ * from silently lagging the code.
24
+ */
25
+ import * as fs from 'node:fs';
26
+ import * as path from 'node:path';
27
+ import { fileURLToPath, pathToFileURL } from 'node:url';
28
+
29
+ import * as YAML from 'yaml';
30
+
31
+ import {
32
+ type DefectFinding,
33
+ type TurnSnapshot,
34
+ detectCouncilClaim,
35
+ detectLanguageMirror,
36
+ detectUserReport,
37
+ } from './_lib/self_repair.js';
38
+ import { assertScanned } from './_lib/scan_scope.js';
39
+
40
+ export const CORPUS_REL = path.join('tests', 'fixtures', 'self-repair-detector-corpus.yml');
41
+
42
+ const GATE = 'check_detector_corpus';
43
+ const CLASSES = ['fire', 'near-miss-fire', 'must-not-fire'] as const;
44
+ type FixtureClass = (typeof CLASSES)[number];
45
+
46
+ /** One corpus entry, in the union of shapes the three detectors need. */
47
+ export interface Fixture {
48
+ text?: string;
49
+ reply?: string;
50
+ pinned_language?: 'de' | 'en';
51
+ tool_commands?: string[];
52
+ note?: string;
53
+ }
54
+
55
+ export type Corpus = Record<string, Partial<Record<FixtureClass, Fixture[]>>>;
56
+
57
+ /**
58
+ * Every detector that must appear in the corpus, with how to run it.
59
+ *
60
+ * Keyed by the `defect_class` the detector produces, so the corpus reads as a
61
+ * list of defect classes rather than of function names.
62
+ */
63
+ export const DETECTORS: Record<string, (f: Fixture) => DefectFinding | null> = {
64
+ 'user-reported': (f) => detectUserReport(f.text ?? f.reply ?? ''),
65
+ 'council-availability-claim': (f) => detectCouncilClaim(asTurn(f)),
66
+ 'language-mirror': (f) => detectLanguageMirror(asTurn(f)),
67
+ };
68
+
69
+ function asTurn(f: Fixture): TurnSnapshot {
70
+ return {
71
+ prompt: f.text ?? '',
72
+ reply: f.reply ?? '',
73
+ toolCommands: f.tool_commands ?? [],
74
+ pinnedLanguage: f.pinned_language ?? null,
75
+ };
76
+ }
77
+
78
+ export interface Finding {
79
+ detector: string;
80
+ kind: 'missing-class' | 'empty-class' | 'wrong-outcome' | 'unregistered';
81
+ message: string;
82
+ }
83
+
84
+ /** Pure: audit a parsed corpus against the registered detectors. */
85
+ export function auditCorpus(corpus: Corpus): { findings: Finding[]; fixtures: number } {
86
+ const findings: Finding[] = [];
87
+ let fixtures = 0;
88
+
89
+ for (const name of Object.keys(corpus)) {
90
+ if (!(name in DETECTORS)) {
91
+ findings.push({
92
+ detector: name,
93
+ kind: 'unregistered',
94
+ message: `corpus names "${name}", which is not a registered detector`,
95
+ });
96
+ }
97
+ }
98
+
99
+ for (const [name, run] of Object.entries(DETECTORS)) {
100
+ const entry = corpus[name];
101
+ if (entry === undefined) {
102
+ findings.push({
103
+ detector: name,
104
+ kind: 'missing-class',
105
+ message: `no corpus entry at all — needs all of: ${CLASSES.join(', ')}`,
106
+ });
107
+ continue;
108
+ }
109
+ for (const cls of CLASSES) {
110
+ const cases = entry[cls];
111
+ if (cases === undefined) {
112
+ findings.push({
113
+ detector: name,
114
+ kind: 'missing-class',
115
+ message: `missing the \`${cls}\` class`,
116
+ });
117
+ continue;
118
+ }
119
+ if (cases.length === 0) {
120
+ // An empty class is the same hole as a missing one wearing a key.
121
+ findings.push({
122
+ detector: name,
123
+ kind: 'empty-class',
124
+ message: `\`${cls}\` is present but empty`,
125
+ });
126
+ continue;
127
+ }
128
+ for (const f of cases) {
129
+ fixtures += 1;
130
+ const fired = run(f) !== null;
131
+ const shouldFire = cls !== 'must-not-fire';
132
+ if (fired !== shouldFire) {
133
+ const what = f.text ?? f.reply ?? '(empty)';
134
+ findings.push({
135
+ detector: name,
136
+ kind: 'wrong-outcome',
137
+ message:
138
+ `\`${cls}\` fixture ${shouldFire ? 'did NOT fire' : 'FIRED'}: ` +
139
+ JSON.stringify(what.slice(0, 90)),
140
+ });
141
+ }
142
+ }
143
+ }
144
+ }
145
+ return { findings, fixtures };
146
+ }
147
+
148
+ export function loadCorpus(repoRoot: string): Corpus {
149
+ const p = path.join(repoRoot, CORPUS_REL);
150
+ const parsed = YAML.parse(fs.readFileSync(p, 'utf-8')) as { detectors?: Corpus };
151
+ return parsed.detectors ?? {};
152
+ }
153
+
154
+ export function main(repoRoot: string = process.cwd()): number {
155
+ const corpus = loadCorpus(repoRoot);
156
+ const { findings, fixtures } = auditCorpus(corpus);
157
+
158
+ // A corpus that parsed to nothing is the dead-scope case, not a pass.
159
+ assertScanned({
160
+ gate: GATE,
161
+ scanned: fixtures,
162
+ units: 'detector fixtures',
163
+ roots: [CORPUS_REL],
164
+ });
165
+
166
+ if (findings.length > 0) {
167
+ process.stderr.write(`${GATE}: ${String(findings.length)} finding(s)\n\n`);
168
+ for (const f of findings) {
169
+ process.stderr.write(` ${f.detector}: ${f.message}\n`);
170
+ }
171
+ process.stderr.write(
172
+ `\nEvery detector needs all three classes (${CLASSES.join(', ')}) and every\n` +
173
+ 'fixture must behave as its class says. Precision and recall fail in\n' +
174
+ 'opposite directions; a corpus missing one half hides the other.\n',
175
+ );
176
+ return 1;
177
+ }
178
+
179
+ process.stdout.write(
180
+ `✅ ${GATE}: ${String(Object.keys(DETECTORS).length)} detector(s) × ` +
181
+ `${String(CLASSES.length)} classes, ${String(fixtures)} fixtures, all behaving\n`,
182
+ );
183
+ return 0;
184
+ }
185
+
186
+ function isCliEntry(): boolean {
187
+ const argv1 = process.argv[1];
188
+ if (argv1 === undefined) {
189
+ return false;
190
+ }
191
+ try {
192
+ return (
193
+ fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(argv1))
194
+ );
195
+ } catch {
196
+ return pathToFileURL(argv1).href === import.meta.url;
197
+ }
198
+ }
199
+
200
+ if (isCliEntry()) {
201
+ process.exit(main());
202
+ }
@@ -31,7 +31,10 @@
31
31
  * WHAT IT DOES NOT DO: it never builds. Building into a live hook path is a
32
32
  * documented hazard in this repo (an esbuild run that overwrites the
33
33
  * dispatcher a running hook is executing can wedge the tool loop), so the
34
- * remedy is printed for a human to run deliberately.
34
+ * remedy is printed for a human to run deliberately. The SAFE rebuild
35
+ * (build to .new → probe → atomic rename) lives in `rebuild_hook_bundle.ts`,
36
+ * which preflight runs right before this gate — so a red here means the heal
37
+ * itself failed or was bypassed, not merely that a merge landed.
35
38
  *
36
39
  * Exit codes: 0 = fresh, or no self-hosted bundle to check · 1 = stale.
37
40
  */
@@ -1174,11 +1174,27 @@ export function generate_rule_symlinks(): number {
1174
1174
  const skip = new Set(rules.filter((r) => !emit_here.has(r)));
1175
1175
  skipped_per_tool[tool_dir] = skip;
1176
1176
  _mkdirp(target_dir);
1177
- // Clean stale symlinks — a rule that became de-duplicable this run is
1177
+ // Clean stale entries — a rule that became de-duplicable this run is
1178
1178
  // stale at project scope too, so it has to go with the rest.
1179
+ //
1180
+ // Symlink-only was correct while every tool dir was a symlink tree. Two
1181
+ // paths now write REAL files (`thin` mode, and the Claude emitter
1182
+ // below), and a stale real file is invisible to an `_isSymlink` test —
1183
+ // so a renamed or newly de-duplicable rule would leave its body behind,
1184
+ // loaded unconditionally, forever.
1185
+ //
1186
+ // Ownership test for a real file: its basename is an agent-config rule
1187
+ // in the projection source. A consumer's own hand-written rule is only
1188
+ // touched if it collides with one of ours by name, in which case it was
1189
+ // already being overwritten.
1179
1190
  for (const item of _iterdirSorted(target_dir)) {
1180
1191
  const name = path.basename(item);
1181
- if (_isSymlink(item) && (!rules.includes(name) || skip.has(name)) && name !== 'README.md') {
1192
+ if (name === 'README.md') {
1193
+ continue;
1194
+ }
1195
+ const is_ours =
1196
+ _isSymlink(item) || _isFile(path.join(MODULE_STATE.RULES_SOURCE, name));
1197
+ if (is_ours && (!rules.includes(name) || skip.has(name))) {
1182
1198
  fs.unlinkSync(item);
1183
1199
  }
1184
1200
  }
@@ -1192,6 +1208,11 @@ export function generate_rule_symlinks(): number {
1192
1208
  }
1193
1209
  if (thin_files !== null) {
1194
1210
  _writeText(link, thin_files[rule] as string);
1211
+ } else if (tool_dir === '.claude/rules') {
1212
+ // Host-native activation (P3.1): emit the rule with the host's
1213
+ // own `paths:` key instead of symlinking agent-config
1214
+ // frontmatter this host does not read.
1215
+ _emit_claude_rule(path.join(MODULE_STATE.RULES_SOURCE, rule), link);
1195
1216
  } else {
1196
1217
  fs.symlinkSync(path.join(rel_prefix, rule), link);
1197
1218
  }
@@ -1355,6 +1376,145 @@ export function _emit_windsurf_rule(source: string, target: string): void {
1355
1376
  _writeText(target, lines.join('\n'));
1356
1377
  }
1357
1378
 
1379
+ /**
1380
+ * A brace group the host expands, versus an agent-config placeholder that only
1381
+ * LOOKS like one.
1382
+ *
1383
+ * `path_prefix: "{module_root}/"` in `roadmap-ci-steps-policy` is resolved by
1384
+ * this suite from `modules.root_paths`. Emitted verbatim into a host `paths:`
1385
+ * list it is a brace group with no comma, which the host expands to the literal
1386
+ * string `{module_root}` — a pattern that matches no file. The rule would then
1387
+ * reach the model **never** instead of on-demand, and nothing would say so: the
1388
+ * probed contract records this exact degradation as silent
1389
+ * (`claude-code-rules-dir-contract.md` § constraints).
1390
+ *
1391
+ * Dropping the pattern is fail-safe in the one direction that matters. A rule
1392
+ * left with no pattern at all carries no `paths:` key and loads
1393
+ * unconditionally — the status quo, not a regression. A rule carrying a
1394
+ * no-op pattern is invisible.
1395
+ *
1396
+ * The discriminator is the comma, not a placeholder allowlist: `{ts,tsx}` is a
1397
+ * real alternation, `{module_root}` cannot be one.
1398
+ */
1399
+ export function _is_unresolved_placeholder(pattern: string): boolean {
1400
+ for (const m of pattern.matchAll(/\{([^}]*)\}/g)) {
1401
+ if (!(m[1] as string).includes(',')) {
1402
+ return true;
1403
+ }
1404
+ }
1405
+ return false;
1406
+ }
1407
+
1408
+ /**
1409
+ * `[` opens a bracket expression for the host. An unparseable one makes that
1410
+ * pattern match nothing while the rule's other patterns keep working — so a
1411
+ * literal `[` in a trigger is escaped rather than trusted.
1412
+ */
1413
+ export function _escape_claude_bracket(pattern: string): string {
1414
+ return pattern.replace(/\[/g, '\\[');
1415
+ }
1416
+
1417
+ /**
1418
+ * Expanded-pattern cost of one glob: the product of its brace alternations.
1419
+ * The host budgets a rule's whole `paths:` list at 1,000 expanded patterns and
1420
+ * 4 MiB, and a list over budget is used UNEXPANDED — braces then match
1421
+ * literally, i.e. nothing. Counting keeps the emitter under the cliff instead
1422
+ * of discovering it as a silent no-op.
1423
+ */
1424
+ export function _expanded_pattern_count(pattern: string): number {
1425
+ let n = 1;
1426
+ for (const m of pattern.matchAll(/\{([^}]*)\}/g)) {
1427
+ n *= (m[1] as string).split(',').length;
1428
+ }
1429
+ return n;
1430
+ }
1431
+
1432
+ /** Host-documented budget for one rule's whole `paths:` list. */
1433
+ export const CLAUDE_PATHS_PATTERN_BUDGET = 1000;
1434
+
1435
+ export interface ClaudePathsPlan {
1436
+ /** Patterns to emit under `paths:`, in declaration order. */
1437
+ globs: string[];
1438
+ /** Patterns deliberately not emitted, with the reason a reader can act on. */
1439
+ dropped: { pattern: string; reason: 'unresolved-placeholder' | 'over-budget' }[];
1440
+ }
1441
+
1442
+ /**
1443
+ * Derive the host's `paths:` list for one rule, with every drop recorded.
1444
+ *
1445
+ * Kernel / always-apply rules get an EMPTY list on purpose: absent `paths:` is
1446
+ * how this host says "load unconditionally", which is exactly what an Iron-Law
1447
+ * rule needs. Scoping one would be a correctness regression dressed as a byte
1448
+ * saving — and the same contract notes path-scoped rules are not re-injected
1449
+ * after `/compact`, so an obligation that must survive compaction cannot be
1450
+ * path-scoped at all.
1451
+ */
1452
+ export function _claude_paths_plan(meta: Record<string, unknown>): ClaudePathsPlan {
1453
+ const always_apply = Boolean(meta['alwaysApply'] || meta['type'] === 'always');
1454
+ if (always_apply) {
1455
+ return { globs: [], dropped: [] };
1456
+ }
1457
+ const globs: string[] = [];
1458
+ const dropped: ClaudePathsPlan['dropped'] = [];
1459
+ let spent = 0;
1460
+ for (const raw of derive_trigger_globs(meta)) {
1461
+ if (_is_unresolved_placeholder(raw)) {
1462
+ dropped.push({ pattern: raw, reason: 'unresolved-placeholder' });
1463
+ continue;
1464
+ }
1465
+ const cost = _expanded_pattern_count(raw);
1466
+ if (spent + cost > CLAUDE_PATHS_PATTERN_BUDGET) {
1467
+ dropped.push({ pattern: raw, reason: 'over-budget' });
1468
+ continue;
1469
+ }
1470
+ spent += cost;
1471
+ globs.push(_escape_claude_bracket(raw));
1472
+ }
1473
+ return { globs, dropped };
1474
+ }
1475
+
1476
+ /**
1477
+ * Emit a Claude Code rule carrying the host's OWN activation key.
1478
+ *
1479
+ * Sibling of {@link _emit_cursor_mdc} and {@link _emit_windsurf_rule}, and the
1480
+ * third member of a set that had two. `.claude/rules` was a symlink projection
1481
+ * of `dist/agent-src/rules`, which carried agent-config's own frontmatter
1482
+ * vocabulary (`type`, `tier`, `triggers`, `alwaysApply`, …) into a host that
1483
+ * reads **none** of it — so every rule loaded unconditionally and the corpus
1484
+ * arrived in full every session.
1485
+ *
1486
+ * Emitted frontmatter is therefore `paths:` and nothing else: the one key the
1487
+ * probed contract records as read by this host. A rule with no path-shaped
1488
+ * trigger gets no frontmatter at all rather than a block the host ignores —
1489
+ * keys that do nothing are bytes in every session's standing context.
1490
+ *
1491
+ * The body is copied byte-for-byte from the projection source (ADR-201's
1492
+ * copy-plus-rewrite discipline); only the frontmatter is host-shaped.
1493
+ */
1494
+ export function _emit_claude_rule(source: string, target: string): void {
1495
+ const [meta, body] = _parse_frontmatter(_readText(source));
1496
+ const plan = _claude_paths_plan(meta);
1497
+ for (const d of plan.dropped) {
1498
+ _print(
1499
+ ` ⚠️ ${path.basename(target)}: dropped \`paths:\` pattern ${JSON.stringify(d.pattern)} (${d.reason}) — ` +
1500
+ (plan.globs.length > 0
1501
+ ? 'the rule keeps its other patterns'
1502
+ : 'the rule now loads unconditionally'),
1503
+ );
1504
+ }
1505
+ const lines: string[] = [];
1506
+ if (plan.globs.length > 0) {
1507
+ lines.push('---', 'paths:');
1508
+ for (const g of plan.globs) {
1509
+ lines.push(` - ${_yaml_scalar(g)}`);
1510
+ }
1511
+ lines.push('---', '');
1512
+ }
1513
+ lines.push(_pyRstrip(body) + '\n');
1514
+ _mkdirp(path.dirname(target));
1515
+ _writeText(target, lines.join('\n'));
1516
+ }
1517
+
1358
1518
  /** Python str.rstrip() with no args. */
1359
1519
  function _pyRstrip(s: string): string {
1360
1520
  return s.replace(/\s+$/u, '');