@event4u/agent-config 9.28.0 → 9.30.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 (209) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +6 -6
  3. package/CHANGELOG.md +172 -5
  4. package/README.md +2 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/analyze/inbox.md +22 -1
  7. package/dist/agent-src/commands/cost/report.md +2 -2
  8. package/dist/agent-src/commands/explain-run.md +31 -1
  9. package/dist/agent-src/commands/team/adversarial.md +8 -7
  10. package/dist/agent-src/commands/team/delegate.md +14 -12
  11. package/dist/agent-src/commands/team/review.md +9 -8
  12. package/dist/agent-src/commands/team/status.md +9 -8
  13. package/dist/agent-src/commands/team.md +29 -19
  14. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  15. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  16. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  17. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +22 -1
  18. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +17 -0
  19. package/dist/agent-src/contexts/execution/subagent-response-contract.md +65 -1
  20. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  21. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  22. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  23. package/dist/agent-src/rules/delegation-policy.md +11 -7
  24. package/dist/agent-src/rules/preservation-guard.md +9 -0
  25. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  26. package/dist/agent-src/rules/token-budget-discipline.md +1 -1
  27. package/dist/agent-src/scripts/roadmap_gates.ts +312 -0
  28. package/dist/agent-src/scripts/update_roadmap_progress.ts +72 -4
  29. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  30. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  31. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  32. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  33. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +17 -5
  34. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  35. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  36. package/dist/agent-src/templates/AGENTS.md +4 -4
  37. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  38. package/dist/agent-src/templates/roadmaps.md +23 -0
  39. package/dist/cli/registry.js +2 -0
  40. package/dist/cli/registry.js.map +1 -1
  41. package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
  42. package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-BTJNRUX3.js} +22 -35
  43. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-DDUNNFYX.js} +24 -17
  44. package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-EQY4FLJB.js} +3 -3
  45. package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-IYTGIG2Y.js} +155 -91
  46. package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
  47. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  48. package/dist/cli-delegate/chunk-MMJL4TIX.js +18 -0
  49. package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
  50. package/dist/cli-delegate/chunk-O3CO5BFB.js +161 -0
  51. package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
  52. package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
  53. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  54. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  55. package/dist/cli-delegate/cmd_conformance.js +2803 -137
  56. package/dist/cli-delegate/cmd_doctor.js +10 -5
  57. package/dist/cli-delegate/cmd_explain.js +2 -2
  58. package/dist/cli-delegate/cmd_export.js +4 -4
  59. package/dist/cli-delegate/cmd_handoff.js +88 -8
  60. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  61. package/dist/cli-delegate/cmd_migrate.js +2 -2
  62. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  63. package/dist/cli-delegate/cmd_prune.js +4 -4
  64. package/dist/cli-delegate/cmd_route_audit.js +3 -2
  65. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  66. package/dist/cli-delegate/cmd_session_recycle.js +177 -0
  67. package/dist/cli-delegate/cmd_settings_get.js +2 -8
  68. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  69. package/dist/cli-delegate/cmd_settings_set.js +2 -2
  70. package/dist/cli-delegate/cmd_sync.js +4 -4
  71. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  72. package/dist/cli-delegate/cmd_update.js +1 -1
  73. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  74. package/dist/cli-delegate/cmd_validate.js +4 -4
  75. package/dist/cli-delegate/cmd_versions.js +1 -1
  76. package/dist/discovery/deprecation-report.md +1 -1
  77. package/dist/discovery/discovery-manifest.json +72 -23
  78. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  79. package/dist/discovery/discovery-manifest.summary.md +3 -2
  80. package/dist/discovery/orphan-report.md +1 -1
  81. package/dist/discovery/packs.json +39 -2
  82. package/dist/discovery/trust-report.md +2 -2
  83. package/dist/discovery/workspaces.json +12 -4
  84. package/dist/hooks/dispatch.js +2278 -611
  85. package/dist/install/install.mjs +56 -17
  86. package/dist/mcp/registry-manifest.json +3 -3
  87. package/dist/mcp/server.json +2 -2
  88. package/dist/mcp/server.mjs +4122 -699
  89. package/dist/router.json +1 -1
  90. package/dist/scripts/_lib/agent_settings.js +54 -0
  91. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  92. package/dist/scripts/condense.js +147 -2
  93. package/dist/scripts/condense.js.map +1 -1
  94. package/dist/server/schemas/settings.js +12 -7
  95. package/dist/server/schemas/settings.js.map +1 -1
  96. package/dist/shared/settingsCarveOut.js +0 -9
  97. package/dist/shared/settingsCarveOut.js.map +1 -1
  98. package/dist/shared/settingsClasses.js +11 -9
  99. package/dist/shared/settingsClasses.js.map +1 -1
  100. package/dist/ui/assets/index-B5qGYOJo.js +26 -0
  101. package/dist/ui/assets/index-B5qGYOJo.js.map +1 -0
  102. package/dist/ui/index.html +1 -1
  103. package/docs/contracts/settings-classes.md +58 -12
  104. package/package.json +1 -1
  105. package/src/agent-src/scripts/roadmap_gates.ts +312 -0
  106. package/src/agent-src/scripts/update_roadmap_progress.ts +72 -4
  107. package/src/agent-src/templates/AGENTS.md +4 -4
  108. package/src/config/agent-settings.template.yml +88 -66
  109. package/src/config/discovery/packs.yml +11 -0
  110. package/src/config/discovery/workspaces.yml +1 -1
  111. package/src/config/dispatch-economy-metrics.json +61 -0
  112. package/src/config/evaluator-budgets.json +4 -4
  113. package/src/config/gate-violation-baselines.json +3 -3
  114. package/src/config/hook-token-budget.json +91 -0
  115. package/src/config/recycle-threshold-budget.json +12 -0
  116. package/src/config/rule-enforcement-baseline.json +86 -0
  117. package/src/scripts/_cli/cmd_conformance.ts +31 -44
  118. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  119. package/src/scripts/_cli/cmd_session_recycle.ts +226 -0
  120. package/src/scripts/_dispatch.bash +23 -0
  121. package/src/scripts/_lib/agent_settings.ts +58 -0
  122. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  123. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  124. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  125. package/src/scripts/_lib/host_capability.ts +63 -6
  126. package/src/scripts/_lib/judgment_ladder.ts +634 -0
  127. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  128. package/src/scripts/_lib/orchestration_record.ts +78 -10
  129. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  130. package/src/scripts/_lib/recycle_envelope_paths.ts +35 -0
  131. package/src/scripts/_lib/self_repair.ts +354 -13
  132. package/src/scripts/_lib/self_repair_store.ts +28 -0
  133. package/src/scripts/_lib/session_eol.ts +230 -0
  134. package/src/scripts/_lib/session_role.ts +42 -0
  135. package/src/scripts/_lib/subagent_capsule.ts +341 -0
  136. package/src/scripts/_lib/subagent_response.ts +94 -0
  137. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  138. package/src/scripts/_lib/subagent_steering.ts +9 -4
  139. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  140. package/src/scripts/_lib/worker_budget.ts +37 -2
  141. package/src/scripts/ai_council/clients.ts +126 -55
  142. package/src/scripts/ai_council/config.ts +94 -6
  143. package/src/scripts/ai_council/handoff.ts +113 -0
  144. package/src/scripts/ai_council/modes.ts +19 -4
  145. package/src/scripts/ai_council/orchestrator.ts +105 -0
  146. package/src/scripts/ai_council/quorum.ts +84 -0
  147. package/src/scripts/ai_council/session.ts +44 -0
  148. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  149. package/src/scripts/ai_team/availability.ts +110 -0
  150. package/src/scripts/ai_team/config.ts +34 -14
  151. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  152. package/src/scripts/ask_transport.ts +221 -0
  153. package/src/scripts/bench_critic_protocol.ts +396 -0
  154. package/src/scripts/bench_hook_injection.ts +234 -0
  155. package/src/scripts/check_adr_frontmatter.ts +84 -40
  156. package/src/scripts/check_branch_freshness.ts +1 -0
  157. package/src/scripts/check_corpus_staleness.ts +18 -4
  158. package/src/scripts/check_detector_corpus.ts +219 -0
  159. package/src/scripts/check_gitignore_freshness.ts +42 -8
  160. package/src/scripts/check_hook_bundle_freshness.ts +24 -1
  161. package/src/scripts/check_kernel_prefix_stability.ts +1 -0
  162. package/src/scripts/check_standing_rule_delivery.ts +73 -32
  163. package/src/scripts/check_static_layer_stability.ts +154 -0
  164. package/src/scripts/check_suppression_hygiene.ts +48 -0
  165. package/src/scripts/condense.ts +162 -2
  166. package/src/scripts/council_cli.ts +401 -25
  167. package/src/scripts/dispatch_economy_report.ts +448 -0
  168. package/src/scripts/dispatch_r2_reviewer.ts +15 -2
  169. package/src/scripts/explain_run.ts +206 -4
  170. package/src/scripts/forensics_report.ts +333 -0
  171. package/src/scripts/generate_settings_reference.ts +1 -0
  172. package/src/scripts/handoff_context_hook.ts +112 -4
  173. package/src/scripts/hook_manifest.yaml +117 -10
  174. package/src/scripts/hooks/concern_registry.ts +8 -0
  175. package/src/scripts/hooks/delegation_nudge_hook.ts +106 -35
  176. package/src/scripts/hooks/dispatch_hook.ts +39 -2
  177. package/src/scripts/hooks/edit_shape_hook.ts +303 -0
  178. package/src/scripts/hooks/orchestration_record_hook.ts +19 -1
  179. package/src/scripts/hooks/reread_guard_hook.ts +314 -0
  180. package/src/scripts/hooks/rtk_wrap_hook.ts +133 -14
  181. package/src/scripts/hooks/session_eol_hook.ts +268 -0
  182. package/src/scripts/hooks/turn_end_gate_hook.ts +507 -0
  183. package/src/scripts/install-hooks.sh +1 -1
  184. package/src/scripts/language_mirror_hook.ts +89 -0
  185. package/src/scripts/lint_decision_impact.ts +14 -1
  186. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  187. package/src/scripts/lint_hook_manifest.ts +82 -0
  188. package/src/scripts/lint_never_silent.ts +276 -0
  189. package/src/scripts/lint_no_activation_gates.ts +271 -0
  190. package/src/scripts/lint_readme_serial_comma.ts +1 -0
  191. package/src/scripts/lint_rule_enforcement_declaration.ts +347 -0
  192. package/src/scripts/measure_turn_end_gate.ts +194 -0
  193. package/src/scripts/orchestration_record.ts +12 -1
  194. package/src/scripts/rebuild_hook_bundle.ts +178 -0
  195. package/src/scripts/release.ts +74 -2
  196. package/src/scripts/report_conformance_funnel.ts +284 -0
  197. package/src/scripts/report_imperative_density.ts +1 -0
  198. package/src/scripts/routing_doctor.ts +118 -36
  199. package/src/scripts/score_skill_selection.ts +19 -5
  200. package/src/scripts/self_repair_cli.ts +337 -59
  201. package/src/scripts/session_eol_report.ts +261 -0
  202. package/src/scripts/team_events_spike.ts +271 -0
  203. package/src/scripts/team_review_gate_hook.ts +31 -9
  204. package/src/server/schemas/settings.ts +24 -17
  205. package/src/shared/settingsCarveOut.ts +0 -10
  206. package/src/shared/settingsClasses.ts +11 -9
  207. package/src/templates/compact-instructions.md +40 -0
  208. package/dist/ui/assets/index-DmR0mbk4.js +0 -26
  209. package/dist/ui/assets/index-DmR0mbk4.js.map +0 -1
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Hook-injection byte bench + CI gate (road-to-token-economy-cache Phase 3.2)
4
+ * — the latency harness's twin for the INJECTION side.
5
+ *
6
+ * Runs every concern bound on the `claude` platform against the committed
7
+ * fixture envelope of each slot it is bound to (tests/fixtures/hooks/
8
+ * <event>.json), in-process via CONCERN_REGISTRY with the hook-stdin
9
+ * override, under AGENT_CONFIG_REPLAY=1 (state writes skipped). Measures the
10
+ * bytes of the stdout payload fields a concern injects — `reason` +
11
+ * `additional_context` + `context` — and gates them against
12
+ * `src/config/hook-token-budget.json`:
13
+ *
14
+ * - a concern whose emission exceeds its row (or the default cap) → RED
15
+ * - an EMITTING concern with no explicit row above the default is fine;
16
+ * the default cap IS its row — but a breach names the missing row so
17
+ * the fix is a registered decision, not a silent default bump
18
+ * - per-slot SUM over all bound concerns > the slot cap → RED
19
+ *
20
+ * HONEST SCOPE, stated up front: concerns are conditional-silence by design —
21
+ * a generic fixture triggers few of them, so most measure 0 bytes here. The
22
+ * gate therefore enforces the cap on whatever DOES fire under the committed
23
+ * fixtures (deterministic, CI-reproducible), while the census mode
24
+ * (`--record`) measures REAL sessions: it appends counts-only lines
25
+ * ({ts, slot, concern, bytes}) to agents/runtime/state/injection-census.jsonl
26
+ * — the repeat-injection-census blocker's instrument. Claiming the fixture
27
+ * numbers are the live distribution would be the coverage inflation this
28
+ * repo's gate discipline refuses; the two modes exist because neither
29
+ * substitutes for the other.
30
+ *
31
+ * Class A: in-process, no daemon, no network. Exit 0 green · 1 breach ·
32
+ * 2 unusable invocation. Dead-scope guarded: zero concerns run = RED.
33
+ *
34
+ * Usage:
35
+ * ./scripts-run src/scripts/bench_hook_injection [--format text|json]
36
+ * [--record] [--platform claude]
37
+ */
38
+ import * as fs from 'node:fs';
39
+ import * as path from 'node:path';
40
+ import { fileURLToPath, pathToFileURL } from 'node:url';
41
+
42
+ import { CONCERN_REGISTRY } from './hooks/concern_registry.js';
43
+ import { setHookStdinOverride, clearHookStdinOverride } from './hooks/hook_stdin.js';
44
+ import { _load_yaml, _resolve_concerns, EVENT_VOCABULARY, type JsonObject } from './hooks/dispatch_hook.js';
45
+
46
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
47
+ const _IN_BUNDLE = typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
48
+ const REPO_ROOT = path.resolve(
49
+ path.dirname(fileURLToPath(import.meta.url)),
50
+ ...(_IN_BUNDLE ? ['..'] : ['..', '..']),
51
+ );
52
+ const MANIFEST = path.join(REPO_ROOT, 'src', 'scripts', 'hook_manifest.yaml');
53
+ const BUDGET = path.join(REPO_ROOT, 'src', 'config', 'hook-token-budget.json');
54
+ const FIXTURES = path.join(REPO_ROOT, 'tests', 'fixtures', 'hooks');
55
+ const CENSUS_FILE = path.join('agents', 'runtime', 'state', 'injection-census.jsonl');
56
+
57
+ export interface Measurement {
58
+ slot: string;
59
+ concern: string;
60
+ bytes: number;
61
+ cap: number;
62
+ breach: boolean;
63
+ }
64
+
65
+ interface Budget {
66
+ default_cap_bytes: number;
67
+ per_concern_caps_bytes: Record<string, number | string>;
68
+ per_slot_sum_caps_bytes: Record<string, number | string>;
69
+ }
70
+
71
+ function capFor(budget: Budget, concern: string): number {
72
+ const v = budget.per_concern_caps_bytes[concern];
73
+ return typeof v === 'number' ? v : budget.default_cap_bytes;
74
+ }
75
+
76
+ /** Run one registered concern in-process with the fixture envelope; return payload bytes. */
77
+ export function runConcernOnce(
78
+ script: string,
79
+ args: readonly string[],
80
+ envelope: JsonObject,
81
+ ): number {
82
+ const main = CONCERN_REGISTRY[script];
83
+ if (main === undefined) return -1; // not in registry — parity tests own that failure
84
+ let out = '';
85
+ const prevWrite = process.stdout.write;
86
+ const prevErr = process.stderr.write;
87
+ const prevArgv = process.argv;
88
+ const prevReplay = process.env['AGENT_CONFIG_REPLAY'];
89
+ setHookStdinOverride(JSON.stringify(envelope));
90
+ process.env['AGENT_CONFIG_REPLAY'] = '1';
91
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
+ process.stdout.write = ((chunk: any, enc?: any, cb?: any): boolean => {
93
+ out += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8');
94
+ const callback = typeof enc === 'function' ? enc : cb;
95
+ if (typeof callback === 'function') callback();
96
+ return true;
97
+ }) as typeof process.stdout.write;
98
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
+ process.stderr.write = ((chunk: any, enc?: any, cb?: any): boolean => {
100
+ const callback = typeof enc === 'function' ? enc : cb;
101
+ if (typeof callback === 'function') callback();
102
+ return true;
103
+ }) as typeof process.stderr.write;
104
+ try {
105
+ process.argv = [prevArgv[0] as string, script, ...args, '--platform', 'claude'];
106
+ main([...args, '--platform', 'claude']);
107
+ } catch {
108
+ /* a crashing concern injects nothing — latency/parity suites own crashes */
109
+ } finally {
110
+ process.stdout.write = prevWrite;
111
+ process.stderr.write = prevErr;
112
+ process.argv = prevArgv;
113
+ if (prevReplay === undefined) delete process.env['AGENT_CONFIG_REPLAY'];
114
+ else process.env['AGENT_CONFIG_REPLAY'] = prevReplay;
115
+ clearHookStdinOverride();
116
+ }
117
+ const text = out.trim();
118
+ if (!text) return 0;
119
+ try {
120
+ const parsed = JSON.parse(text) as Record<string, unknown>;
121
+ let bytes = 0;
122
+ for (const key of ['reason', 'additional_context', 'context']) {
123
+ const v = parsed[key];
124
+ if (typeof v === 'string') bytes += Buffer.byteLength(v, 'utf-8');
125
+ }
126
+ return bytes;
127
+ } catch {
128
+ return Buffer.byteLength(text, 'utf-8'); // non-JSON stdout counts whole
129
+ }
130
+ }
131
+
132
+ export function bench(opts: { platform?: string; budgetPath?: string } = {}): { measurements: Measurement[]; slotSums: Record<string, { bytes: number; cap: number; breach: boolean }>; errors: string[] } {
133
+ const platform = opts.platform ?? 'claude';
134
+ const manifest = _load_yaml(MANIFEST);
135
+ const budget = JSON.parse(fs.readFileSync(opts.budgetPath ?? BUDGET, 'utf8')) as Budget;
136
+ const measurements: Measurement[] = [];
137
+ const slotSums: Record<string, { bytes: number; cap: number; breach: boolean }> = {};
138
+ const errors: string[] = [];
139
+
140
+ for (const event of EVENT_VOCABULARY) {
141
+ const fixtureFile = path.join(FIXTURES, `${event}.json`);
142
+ if (!fs.existsSync(fixtureFile)) continue;
143
+ const envelope = JSON.parse(fs.readFileSync(fixtureFile, 'utf8')) as JsonObject;
144
+ const concerns = _resolve_concerns(manifest, platform, event);
145
+ let sum = 0;
146
+ for (const concern of concerns) {
147
+ const bytes = runConcernOnce(String(concern['script']), Array.isArray(concern['args']) ? concern['args'].map(String) : [], envelope);
148
+ if (bytes < 0) {
149
+ errors.push(`${event}/${String(concern['name'])}: not in CONCERN_REGISTRY`);
150
+ continue;
151
+ }
152
+ const cap = capFor(budget, String(concern['name']));
153
+ const breach = bytes > cap;
154
+ measurements.push({ slot: event, concern: String(concern['name']), bytes, cap, breach });
155
+ sum += bytes;
156
+ }
157
+ const slotCapRaw = budget.per_slot_sum_caps_bytes[event];
158
+ const slotCap = typeof slotCapRaw === 'number' ? slotCapRaw : Number.MAX_SAFE_INTEGER;
159
+ slotSums[event] = { bytes: sum, cap: slotCap, breach: sum > slotCap };
160
+ }
161
+ return { measurements, slotSums, errors };
162
+ }
163
+
164
+ export function main(argv: readonly string[]): number {
165
+ let format: 'text' | 'json' = 'text';
166
+ let record = false;
167
+ let platform = 'claude';
168
+ for (let i = 0; i < argv.length; i++) {
169
+ const a = argv[i];
170
+ if (a === '--format') format = argv[++i] === 'json' ? 'json' : 'text';
171
+ else if (a === '--record') record = true;
172
+ else if (a === '--platform') platform = argv[++i] ?? platform;
173
+ }
174
+ let result: ReturnType<typeof bench>;
175
+ try {
176
+ result = bench({ platform });
177
+ } catch (err) {
178
+ process.stderr.write(`bench_hook_injection: ${err instanceof Error ? err.message : String(err)}\n`);
179
+ return 2;
180
+ }
181
+ // Dead-scope guard: a bench that ran zero concerns proves nothing (a
182
+ // gate that scans nothing exits green is the recorded failure class).
183
+ if (result.measurements.length === 0) {
184
+ process.stderr.write('bench_hook_injection: DEAD SCOPE — zero concerns ran; fixtures or manifest unreadable\n');
185
+ return 1;
186
+ }
187
+ if (record) {
188
+ try {
189
+ const file = path.join(process.cwd(), CENSUS_FILE);
190
+ fs.mkdirSync(path.dirname(file), { recursive: true });
191
+ const ts = new Date().toISOString();
192
+ for (const m of result.measurements) {
193
+ if (m.bytes > 0) {
194
+ fs.appendFileSync(file, JSON.stringify({ ts, slot: m.slot, concern: m.concern, bytes: m.bytes }) + '\n');
195
+ }
196
+ }
197
+ } catch {
198
+ /* census is observability — never fails the bench */
199
+ }
200
+ }
201
+ const breaches = result.measurements.filter((m) => m.breach);
202
+ const slotBreaches = Object.entries(result.slotSums).filter(([, s]) => s.breach);
203
+ if (format === 'json') {
204
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
205
+ } else {
206
+ const emitting = result.measurements.filter((m) => m.bytes > 0);
207
+ process.stdout.write(`bench_hook_injection: ${result.measurements.length} concern-slot pairs · ${emitting.length} emitted under the committed fixtures\n`);
208
+ for (const m of emitting) {
209
+ process.stdout.write(` ${m.slot.padEnd(20)} ${m.concern.padEnd(24)} ${String(m.bytes).padStart(6)} B (cap ${m.cap})${m.breach ? ' ❌ BREACH' : ''}\n`);
210
+ }
211
+ for (const [slot, s] of Object.entries(result.slotSums)) {
212
+ if (s.bytes > 0) process.stdout.write(` slot-sum ${slot.padEnd(20)} ${String(s.bytes).padStart(6)} B (cap ${s.cap === Number.MAX_SAFE_INTEGER ? '—' : s.cap})${s.breach ? ' ❌ BREACH' : ''}\n`);
213
+ }
214
+ for (const e of result.errors) process.stdout.write(` warn: ${e}\n`);
215
+ }
216
+ if (breaches.length > 0 || slotBreaches.length > 0) {
217
+ process.stderr.write(`bench_hook_injection: ${breaches.length} concern breach(es), ${slotBreaches.length} slot-sum breach(es) — the budget row is the decision surface (src/config/hook-token-budget.json)\n`);
218
+ return 1;
219
+ }
220
+ return 0;
221
+ }
222
+
223
+ function _isCliEntry(): boolean {
224
+ if (_IN_BUNDLE) return false;
225
+ if (process.argv[1] === undefined) return false;
226
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
227
+ if (import.meta.url === argvUrl) return true;
228
+ try {
229
+ return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
230
+ } catch {
231
+ return false;
232
+ }
233
+ }
234
+ if (_isCliEntry()) process.exit(main(process.argv.slice(2)));
@@ -35,6 +35,7 @@ import * as fs from 'node:fs';
35
35
  import * as path from 'node:path';
36
36
  import { fileURLToPath, pathToFileURL } from 'node:url';
37
37
 
38
+ import { GateLedger } from './_lib/gate_ledger.js';
38
39
  import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
39
40
 
40
41
  const _HERE = path.dirname(fileURLToPath(import.meta.url));
@@ -101,48 +102,81 @@ export function trigger_is_meaningful(value: string): boolean {
101
102
  return true;
102
103
  }
103
104
 
104
- export function check(dir: string = ADR_DIR): AdrFinding[] {
105
+ /** Every frontmatter finding for one ADR file. Pure — no I/O, no enumeration. */
106
+ export function check_one(rel: string, text: string): AdrFinding[] {
107
+ const findings: AdrFinding[] = [];
108
+ const fm = parse_frontmatter(text);
109
+
110
+ if (fm === null) {
111
+ findings.push({ file: rel, level: 'error', message: 'no YAML frontmatter block' });
112
+ return findings;
113
+ }
114
+ for (const req of REQUIRED) {
115
+ if (!fm[req]) findings.push({ file: rel, level: 'error', message: `missing \`${req}\`` });
116
+ }
117
+ if (fm['status'] && !ALLOWED_STATUS.has(fm['status'])) {
118
+ findings.push({
119
+ file: rel,
120
+ level: 'error',
121
+ message: `status \`${fm['status']}\` is not one of: ${[...ALLOWED_STATUS].sort().join(', ')}`,
122
+ });
123
+ }
124
+
125
+ const date = fm['date'] ?? '';
126
+ const grandfathered = date !== '' && date < REVIEW_TRIGGER_SINCE;
127
+ const trigger = fm['review_trigger'];
128
+
129
+ if (!trigger) {
130
+ findings.push({
131
+ file: rel,
132
+ level: grandfathered ? 'warn' : 'error',
133
+ message: grandfathered
134
+ ? `no \`review_trigger\` (grandfathered — dated ${date}, before ${REVIEW_TRIGGER_SINCE})`
135
+ : `missing \`review_trigger\` — name the CONDITION that would reopen this decision`,
136
+ });
137
+ } else if (!trigger_is_meaningful(trigger)) {
138
+ findings.push({
139
+ file: rel,
140
+ level: grandfathered ? 'warn' : 'error',
141
+ message: `\`review_trigger\` is a cadence, not a condition: "${trigger}". A calendar review is ignored; an event fires.`,
142
+ });
143
+ }
144
+ return findings;
145
+ }
146
+
147
+ /**
148
+ * Check every ADR in `dir`, optionally recording per-file completeness.
149
+ *
150
+ * The candidate set is every `*.md` in the directory, not the `ADR-*.md` subset:
151
+ * the name filter used to be a bare `continue`, so a decision record saved as
152
+ * `adr-131-foo.md` (lower case) or `131-foo.md` left the corpus without a trace
153
+ * and the gate still printed a green line over the files it did read. It is now
154
+ * an out-of-scope outcome with a reason, and it prints.
155
+ *
156
+ * A `warn`-level finding resolves as `complete`: warnings are allowed by this
157
+ * gate's exit-code contract, so `fail` is reserved for what actually reds it.
158
+ */
159
+ export function check(dir: string = ADR_DIR, ledger?: GateLedger): AdrFinding[] {
105
160
  const findings: AdrFinding[] = [];
106
161
  if (!fs.existsSync(dir)) return findings;
107
162
 
108
- for (const name of fs.readdirSync(dir).sort()) {
109
- if (!/^ADR-.*\.md$/.test(name)) continue;
110
- const rel = path.relative(REPO_ROOT, path.join(dir, name));
111
- const fm = parse_frontmatter(fs.readFileSync(path.join(dir, name), 'utf-8'));
163
+ const candidates = fs.readdirSync(dir).filter((n) => n.endsWith('.md')).sort();
164
+ ledger?.plan(candidates);
112
165
 
113
- if (fm === null) {
114
- findings.push({ file: rel, level: 'error', message: 'no YAML frontmatter block' });
166
+ for (const name of candidates) {
167
+ if (!/^ADR-.*\.md$/.test(name)) {
168
+ ledger?.outOfScope(name, 'not_applicable_kind');
115
169
  continue;
116
170
  }
117
- for (const req of REQUIRED) {
118
- if (!fm[req]) findings.push({ file: rel, level: 'error', message: `missing \`${req}\`` });
119
- }
120
- if (fm['status'] && !ALLOWED_STATUS.has(fm['status'])) {
121
- findings.push({
122
- file: rel,
123
- level: 'error',
124
- message: `status \`${fm['status']}\` is not one of: ${[...ALLOWED_STATUS].sort().join(', ')}`,
125
- });
126
- }
171
+ const rel = path.relative(REPO_ROOT, path.join(dir, name));
172
+ const own = check_one(rel, fs.readFileSync(path.join(dir, name), 'utf-8'));
173
+ findings.push(...own);
127
174
 
128
- const date = fm['date'] ?? '';
129
- const grandfathered = date !== '' && date < REVIEW_TRIGGER_SINCE;
130
- const trigger = fm['review_trigger'];
131
-
132
- if (!trigger) {
133
- findings.push({
134
- file: rel,
135
- level: grandfathered ? 'warn' : 'error',
136
- message: grandfathered
137
- ? `no \`review_trigger\` (grandfathered — dated ${date}, before ${REVIEW_TRIGGER_SINCE})`
138
- : `missing \`review_trigger\` — name the CONDITION that would reopen this decision`,
139
- });
140
- } else if (!trigger_is_meaningful(trigger)) {
141
- findings.push({
142
- file: rel,
143
- level: grandfathered ? 'warn' : 'error',
144
- message: `\`review_trigger\` is a cadence, not a condition: "${trigger}". A calendar review is ignored; an event fires.`,
145
- });
175
+ const errors = own.filter((f) => f.level === 'error').length;
176
+ if (errors > 0) {
177
+ ledger?.fail(name, `${String(errors)} frontmatter error(s)`);
178
+ } else {
179
+ ledger?.complete(name);
146
180
  }
147
181
  }
148
182
  return findings;
@@ -150,10 +184,17 @@ export function check(dir: string = ADR_DIR): AdrFinding[] {
150
184
 
151
185
  function main(argv: string[]): number {
152
186
  const as_json = argv.includes('--json');
153
- const findings = check();
154
- const total = fs.existsSync(ADR_DIR)
155
- ? fs.readdirSync(ADR_DIR).filter((f) => /^ADR-.*\.md$/.test(f)).length
156
- : 0;
187
+ const ledger = new GateLedger('check_adr_frontmatter');
188
+ const findings = check(ADR_DIR, ledger);
189
+ // Finalized here, printed later by `report()` (which re-finalizes — the call
190
+ // is pure): the denominator is needed for the scope assertion below, which
191
+ // runs before any output. It is the ledger's own per-file accounting — the
192
+ // ADRs that actually reached a checked outcome — where it used to be a SECOND
193
+ // `readdirSync` of the same directory, independent of the loop that did the
194
+ // work, so the printed count could not disagree with the scan even when the
195
+ // scan had skipped files.
196
+ const tally = ledger.finalize();
197
+ const total = tally.completed + tally.failed;
157
198
 
158
199
  // `check()` returns findings, never a count, so a moved `docs/decisions/`
159
200
  // yields an empty list — reported as "0 ADR(s) · 0 error(s)" and green.
@@ -174,7 +215,9 @@ function main(argv: string[]): number {
174
215
  }
175
216
 
176
217
  if (as_json) {
177
- process.stdout.write(JSON.stringify({ findings }, null, 2) + '\n');
218
+ // The tally rides in the payload rather than on stdout: `ledger.report()`
219
+ // would print beside the JSON and break every parser of this mode.
220
+ process.stdout.write(JSON.stringify({ findings, ledger: tally }, null, 2) + '\n');
178
221
  return 0;
179
222
  }
180
223
 
@@ -185,6 +228,7 @@ function main(argv: string[]): number {
185
228
  `ADR frontmatter: ${total} ADR(s) · ${errors.length} error(s) · ` +
186
229
  `${warns.length} grandfathered without a revisit condition\n`,
187
230
  );
231
+ ledger.report();
188
232
  for (const e of errors) process.stderr.write(` ❌ ${e.file}: ${e.message}\n`);
189
233
 
190
234
  if (errors.length > 0) {
@@ -41,6 +41,7 @@ import { fileURLToPath } from "node:url";
41
41
  import { runGateCli, runSelfTest, type SelfTestCase } from "./_lib/gate_self_test.js";
42
42
  import { reportScanned } from "./_lib/scan_scope.js";
43
43
 
44
+ // ledger-exempt: single remote-ref probe — the entire scope is ONE ls-remote answer (0 or 1 refs) resolved to one aggregate ancestor verdict, and every empty path already publishes its reason via reportScanned allowEmpty; there is no per-target collection to account.
44
45
  const DEFAULT_BASE = "main";
45
46
  const SELF_TEST_MIN_CASES = 3;
46
47
  const SELF_TEST_MIN_REJECT = 1;
@@ -49,6 +49,7 @@ import * as os from 'node:os';
49
49
  import * as path from 'node:path';
50
50
  import { fileURLToPath, pathToFileURL } from 'node:url';
51
51
 
52
+ import { GateLedger } from './_lib/gate_ledger.js';
52
53
  import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
53
54
  import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
54
55
 
@@ -423,20 +424,32 @@ export interface RunResult {
423
424
  readonly corpora: number;
424
425
  /** CSVs actually opened, reported so "checked nothing" cannot read as clean. */
425
426
  readonly csvs: number;
427
+ /** Per-corpus completeness accounting — every discovered corpus reaches one outcome. */
428
+ readonly ledger: GateLedger;
426
429
  }
427
430
 
428
431
  export function run_checks(todayStamp: number, skillsDir: string = SKILLS_DIR): RunResult {
429
432
  const corpora = discover_corpora(skillsDir);
433
+ const ledger = new GateLedger('check_corpus_staleness');
434
+ ledger.plan(corpora.map((c) => c.slug));
430
435
  const findings: CorpusFinding[] = [];
431
436
  let csvs = 0;
432
437
  for (const corpus of corpora) {
433
- findings.push(...check_staleness(corpus, todayStamp));
434
- findings.push(...check_attribution_pin(corpus));
438
+ const found: CorpusFinding[] = [
439
+ ...check_staleness(corpus, todayStamp),
440
+ ...check_attribution_pin(corpus),
441
+ ];
435
442
  const csv = check_csv_integrity(corpus);
436
- findings.push(...csv.findings);
443
+ found.push(...csv.findings);
437
444
  csvs += csv.opened;
445
+ findings.push(...found);
446
+ if (found.length > 0) {
447
+ ledger.fail(corpus.slug, `${String(found.length)} corpus finding(s)`);
448
+ } else {
449
+ ledger.complete(corpus.slug);
450
+ }
438
451
  }
439
- return { findings, corpora: corpora.length, csvs };
452
+ return { findings, corpora: corpora.length, csvs, ledger };
440
453
  }
441
454
 
442
455
  const HELP = `check_corpus_staleness — offline staleness + CSV-integrity gate for the grounding corpora
@@ -651,6 +664,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
651
664
 
652
665
  // gate-coverage contract (src/config/gate-coverage.yml): corpora inspected.
653
666
  process.stdout.write(`scanned: ${String(result.corpora)}\n`);
667
+ result.ledger.report();
654
668
 
655
669
  if (result.findings.length > 0) {
656
670
  process.stdout.write(`❌ ${String(result.findings.length)} corpus finding(s):\n`);
@@ -0,0 +1,219 @@
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 { GateLedger } from './_lib/gate_ledger.js';
32
+ import {
33
+ type DefectFinding,
34
+ type TurnSnapshot,
35
+ detectCouncilClaim,
36
+ detectLanguageMirror,
37
+ detectUserReport,
38
+ } from './_lib/self_repair.js';
39
+ import { assertScanned } from './_lib/scan_scope.js';
40
+
41
+ export const CORPUS_REL = path.join('tests', 'fixtures', 'self-repair-detector-corpus.yml');
42
+
43
+ const GATE = 'check_detector_corpus';
44
+ const CLASSES = ['fire', 'near-miss-fire', 'must-not-fire'] as const;
45
+ type FixtureClass = (typeof CLASSES)[number];
46
+
47
+ /** One corpus entry, in the union of shapes the three detectors need. */
48
+ export interface Fixture {
49
+ text?: string;
50
+ reply?: string;
51
+ pinned_language?: 'de' | 'en';
52
+ tool_commands?: string[];
53
+ note?: string;
54
+ }
55
+
56
+ export type Corpus = Record<string, Partial<Record<FixtureClass, Fixture[]>>>;
57
+
58
+ /**
59
+ * Every detector that must appear in the corpus, with how to run it.
60
+ *
61
+ * Keyed by the `defect_class` the detector produces, so the corpus reads as a
62
+ * list of defect classes rather than of function names.
63
+ */
64
+ export const DETECTORS: Record<string, (f: Fixture) => DefectFinding | null> = {
65
+ 'user-reported': (f) => detectUserReport(f.text ?? f.reply ?? ''),
66
+ 'council-availability-claim': (f) => detectCouncilClaim(asTurn(f)),
67
+ 'language-mirror': (f) => detectLanguageMirror(asTurn(f)),
68
+ };
69
+
70
+ function asTurn(f: Fixture): TurnSnapshot {
71
+ return {
72
+ prompt: f.text ?? '',
73
+ reply: f.reply ?? '',
74
+ toolCommands: f.tool_commands ?? [],
75
+ pinnedLanguage: f.pinned_language ?? null,
76
+ };
77
+ }
78
+
79
+ export interface Finding {
80
+ detector: string;
81
+ kind: 'missing-class' | 'empty-class' | 'wrong-outcome' | 'unregistered';
82
+ message: string;
83
+ }
84
+
85
+ /** Pure: audit a parsed corpus against the registered detectors. */
86
+ export function auditCorpus(corpus: Corpus): { findings: Finding[]; fixtures: number } {
87
+ const findings: Finding[] = [];
88
+ let fixtures = 0;
89
+
90
+ for (const name of Object.keys(corpus)) {
91
+ if (!(name in DETECTORS)) {
92
+ findings.push({
93
+ detector: name,
94
+ kind: 'unregistered',
95
+ message: `corpus names "${name}", which is not a registered detector`,
96
+ });
97
+ }
98
+ }
99
+
100
+ for (const [name, run] of Object.entries(DETECTORS)) {
101
+ const entry = corpus[name];
102
+ if (entry === undefined) {
103
+ findings.push({
104
+ detector: name,
105
+ kind: 'missing-class',
106
+ message: `no corpus entry at all — needs all of: ${CLASSES.join(', ')}`,
107
+ });
108
+ continue;
109
+ }
110
+ for (const cls of CLASSES) {
111
+ const cases = entry[cls];
112
+ if (cases === undefined) {
113
+ findings.push({
114
+ detector: name,
115
+ kind: 'missing-class',
116
+ message: `missing the \`${cls}\` class`,
117
+ });
118
+ continue;
119
+ }
120
+ if (cases.length === 0) {
121
+ // An empty class is the same hole as a missing one wearing a key.
122
+ findings.push({
123
+ detector: name,
124
+ kind: 'empty-class',
125
+ message: `\`${cls}\` is present but empty`,
126
+ });
127
+ continue;
128
+ }
129
+ for (const f of cases) {
130
+ fixtures += 1;
131
+ const fired = run(f) !== null;
132
+ const shouldFire = cls !== 'must-not-fire';
133
+ if (fired !== shouldFire) {
134
+ const what = f.text ?? f.reply ?? '(empty)';
135
+ findings.push({
136
+ detector: name,
137
+ kind: 'wrong-outcome',
138
+ message:
139
+ `\`${cls}\` fixture ${shouldFire ? 'did NOT fire' : 'FIRED'}: ` +
140
+ JSON.stringify(what.slice(0, 90)),
141
+ });
142
+ }
143
+ }
144
+ }
145
+ }
146
+ return { findings, fixtures };
147
+ }
148
+
149
+ export function loadCorpus(repoRoot: string): Corpus {
150
+ const p = path.join(repoRoot, CORPUS_REL);
151
+ const parsed = YAML.parse(fs.readFileSync(p, 'utf-8')) as { detectors?: Corpus };
152
+ return parsed.detectors ?? {};
153
+ }
154
+
155
+ export function main(repoRoot: string = process.cwd()): number {
156
+ const corpus = loadCorpus(repoRoot);
157
+ const { findings, fixtures } = auditCorpus(corpus);
158
+
159
+ // A corpus that parsed to nothing is the dead-scope case, not a pass.
160
+ assertScanned({
161
+ gate: GATE,
162
+ scanned: fixtures,
163
+ units: 'detector fixtures',
164
+ roots: [CORPUS_REL],
165
+ });
166
+
167
+ // Per-detector completeness: every registered detector AND every corpus
168
+ // entry reaches exactly one outcome, so a detector silently absent from the
169
+ // loop above could not read as a pass.
170
+ const ledger = new GateLedger(GATE);
171
+ const names = [...new Set([...Object.keys(DETECTORS), ...Object.keys(corpus)])].sort();
172
+ ledger.plan(names);
173
+ const failing = new Set(findings.map((f) => f.detector));
174
+ for (const name of names) {
175
+ if (failing.has(name)) {
176
+ ledger.fail(name, 'corpus finding(s) against this detector');
177
+ } else {
178
+ ledger.complete(name);
179
+ }
180
+ }
181
+ ledger.report();
182
+
183
+ if (findings.length > 0) {
184
+ process.stderr.write(`${GATE}: ${String(findings.length)} finding(s)\n\n`);
185
+ for (const f of findings) {
186
+ process.stderr.write(` ${f.detector}: ${f.message}\n`);
187
+ }
188
+ process.stderr.write(
189
+ `\nEvery detector needs all three classes (${CLASSES.join(', ')}) and every\n` +
190
+ 'fixture must behave as its class says. Precision and recall fail in\n' +
191
+ 'opposite directions; a corpus missing one half hides the other.\n',
192
+ );
193
+ return 1;
194
+ }
195
+
196
+ process.stdout.write(
197
+ `✅ ${GATE}: ${String(Object.keys(DETECTORS).length)} detector(s) × ` +
198
+ `${String(CLASSES.length)} classes, ${String(fixtures)} fixtures, all behaving\n`,
199
+ );
200
+ return 0;
201
+ }
202
+
203
+ function isCliEntry(): boolean {
204
+ const argv1 = process.argv[1];
205
+ if (argv1 === undefined) {
206
+ return false;
207
+ }
208
+ try {
209
+ return (
210
+ fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(argv1))
211
+ );
212
+ } catch {
213
+ return pathToFileURL(argv1).href === import.meta.url;
214
+ }
215
+ }
216
+
217
+ if (isCliEntry()) {
218
+ process.exit(main());
219
+ }