@event4u/agent-config 9.15.0 → 9.17.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 (157) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +74 -0
  4. package/README.md +1 -1
  5. package/dist/agent-src/commands/agent-handoff.md +100 -48
  6. package/dist/agent-src/commands/commit.md +1 -1
  7. package/dist/agent-src/commands/condense.md +19 -64
  8. package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
  9. package/dist/agent-src/commands/memory/promote.md +14 -1
  10. package/dist/agent-src/commands/mode.md +9 -5
  11. package/dist/agent-src/commands/optimize/deep.md +72 -11
  12. package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
  13. package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
  14. package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
  15. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
  16. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
  17. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
  18. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
  19. package/dist/agent-src/profiles/agency.yml +3 -3
  20. package/dist/agent-src/profiles/content_creator.yml +2 -2
  21. package/dist/agent-src/profiles/developer.yml +2 -2
  22. package/dist/agent-src/profiles/finance.yml +1 -1
  23. package/dist/agent-src/profiles/founder.yml +2 -2
  24. package/dist/agent-src/profiles/ops.yml +2 -2
  25. package/dist/agent-src/rules/architecture.md +2 -2
  26. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  27. package/dist/agent-src/rules/context-hygiene.md +3 -2
  28. package/dist/agent-src/rules/domain-safety-pii.md +1 -1
  29. package/dist/agent-src/rules/output-discipline.md +9 -2
  30. package/dist/agent-src/rules/token-budget-discipline.md +2 -2
  31. package/dist/agent-src/rules/user-interaction.md +3 -4
  32. package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
  33. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
  34. package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
  35. package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
  36. package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
  37. package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
  38. package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
  39. package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
  40. package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
  41. package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
  42. package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
  43. package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
  44. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
  45. package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
  46. package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
  47. package/dist/agent-src/templates/agent-settings.md +13 -15
  48. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
  49. package/dist/agent-src/templates/persona.md +2 -2
  50. package/dist/agent-src/templates/roadmaps.md +1 -1
  51. package/dist/agent-src/templates/rule.md +1 -1
  52. package/dist/agent-src/templates/skill.md +1 -1
  53. package/dist/cli/mcp/content.js +1 -1
  54. package/dist/cli/mcp/content.js.map +1 -1
  55. package/dist/cli/registry.js +1 -0
  56. package/dist/cli/registry.js.map +1 -1
  57. package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
  58. package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-IMSXPRVH.js} +24 -1
  59. package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
  60. package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
  61. package/dist/cli-delegate/cmd_conformance.js +4 -3
  62. package/dist/cli-delegate/cmd_converge.js +3 -3
  63. package/dist/cli-delegate/cmd_doctor.js +3 -2
  64. package/dist/cli-delegate/cmd_export.js +4 -4
  65. package/dist/cli-delegate/cmd_handoff.js +3194 -0
  66. package/dist/cli-delegate/cmd_memory_get.js +35 -3754
  67. package/dist/cli-delegate/cmd_migrate.js +2 -2
  68. package/dist/cli-delegate/cmd_prune.js +4 -4
  69. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  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 +3 -3
  73. package/dist/cli-delegate/cmd_upgrade.js +3 -3
  74. package/dist/cli-delegate/cmd_validate.js +4 -4
  75. package/dist/cli-delegate/cmd_versions.js +3 -3
  76. package/dist/discovery/deprecation-report.md +1 -1
  77. package/dist/discovery/discovery-manifest.json +31 -31
  78. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  79. package/dist/discovery/discovery-manifest.summary.md +1 -1
  80. package/dist/discovery/orphan-report.md +1 -1
  81. package/dist/discovery/packs.json +2 -2
  82. package/dist/discovery/trust-report.md +1 -1
  83. package/dist/discovery/workspaces.json +2 -2
  84. package/dist/hooks/dispatch.js +602 -507
  85. package/dist/install/install.mjs +23 -0
  86. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  87. package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
  88. package/dist/mcp/registry-manifest.json +4 -4
  89. package/dist/mcp/server.json +2 -2
  90. package/dist/mcp/server.mjs +25 -3
  91. package/dist/scripts/_lib/agent_src.js +39 -6
  92. package/dist/scripts/_lib/agent_src.js.map +1 -1
  93. package/dist/server/routes/workspace.js +1 -1
  94. package/dist/server/routes/workspace.js.map +1 -1
  95. package/dist/server/schemas/settings.js +13 -0
  96. package/dist/server/schemas/settings.js.map +1 -1
  97. package/dist/shared/userMd/utils.js +1 -1
  98. package/dist/shared/userMd/utils.js.map +1 -1
  99. package/dist/ui/assets/index-DmR0mbk4.js +26 -0
  100. package/dist/ui/assets/index-DmR0mbk4.js.map +1 -0
  101. package/dist/ui/index.html +1 -1
  102. package/docs/guidelines/abstraction-thresholds.md +53 -0
  103. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
  104. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
  105. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
  106. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
  107. package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
  108. package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
  109. package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
  110. package/llms.txt +1 -0
  111. package/package.json +4 -4
  112. package/src/config/agent-settings.template.yml +25 -1
  113. package/src/config/budget-routing.json +7 -0
  114. package/src/config/evaluator-budgets.json +8 -11
  115. package/src/config/gate-coverage.yml +30 -0
  116. package/src/scripts/_cli/cmd_handoff.ts +405 -0
  117. package/src/scripts/_cli/handoff_generate.ts +453 -0
  118. package/src/scripts/_cli/handoff_sessions.ts +435 -0
  119. package/src/scripts/_dispatch.bash +9 -0
  120. package/src/scripts/_lib/agent_src.ts +36 -6
  121. package/src/scripts/_lib/release_material.ts +156 -0
  122. package/src/scripts/_lib/tier_budget_routing.ts +154 -28
  123. package/src/scripts/anchor_scoring_run.ts +1 -1
  124. package/src/scripts/bench_persona_placebo.ts +1 -1
  125. package/src/scripts/bench_quality_run.ts +1 -1
  126. package/src/scripts/check_cli_registry_budget_sync.ts +168 -0
  127. package/src/scripts/check_condensation.ts +2 -1
  128. package/src/scripts/check_finding_dispositions.ts +278 -0
  129. package/src/scripts/check_gate_coverage.ts +1 -1
  130. package/src/scripts/check_pack_size.ts +4 -3
  131. package/src/scripts/check_references.ts +1 -0
  132. package/src/scripts/check_release_highlights.ts +300 -0
  133. package/src/scripts/check_release_pr_shape.ts +4 -0
  134. package/src/scripts/check_release_surface_equality.ts +132 -0
  135. package/src/scripts/check_reply_consistency.ts +187 -138
  136. package/src/scripts/check_token_quality_golden.ts +1 -1
  137. package/src/scripts/cost/budget.mjs +18 -4
  138. package/src/scripts/handoff_context_hook.ts +148 -0
  139. package/src/scripts/hook_manifest.yaml +17 -7
  140. package/src/scripts/hooks/concern_registry.ts +2 -0
  141. package/src/scripts/lint_abstraction_thresholds.ts +325 -0
  142. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  143. package/src/scripts/lint_profile_personas.ts +153 -0
  144. package/src/scripts/lint_token_budget_discipline.ts +148 -0
  145. package/src/scripts/release.ts +264 -37
  146. package/src/scripts/release_drill.ts +491 -0
  147. package/src/scripts/routing_doctor.ts +76 -1
  148. package/src/scripts/schemas/command.schema.json +33 -0
  149. package/src/scripts/self_review_gate.ts +41 -3
  150. package/src/scripts/skill_linter.ts +3 -1
  151. package/src/scripts/smoke/router.sh +1 -1
  152. package/src/scripts/update_counts.ts +60 -12
  153. package/src/server/schemas/settings.ts +23 -0
  154. package/src/shared/userMd/utils.ts +1 -1
  155. package/dist/ui/assets/index-DJY_vQVn.js +0 -40
  156. package/dist/ui/assets/index-DJY_vQVn.js.map +0 -1
  157. package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import * as fs from 'node:fs';
22
22
  import * as path from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
23
24
 
24
25
  export type BudgetTier = 'cheap' | 'medium' | 'strong';
25
26
 
@@ -123,19 +124,91 @@ export const RESERVE_FILE = 'tier-reserves.jsonl';
123
124
  export const COOLDOWN_FILE = 'tier-cooldowns.json';
124
125
  export const DEFAULT_COOLDOWN_MS = 60 * 60 * 1000;
125
126
 
127
+ // Reserve lifecycle constants — single source of truth shared with
128
+ // budget.mjs (the second reserve reader) via src/config/budget-routing.json.
129
+ // Duplicating the literal here was the exact two-truths defect the external
130
+ // review flagged; both readers now load the same file.
131
+ const _LIFECYCLE_CONFIG = path.join(
132
+ path.dirname(fileURLToPath(import.meta.url)),
133
+ '..',
134
+ '..',
135
+ 'config',
136
+ 'budget-routing.json',
137
+ );
138
+
139
+ function _lifecycle(): { reserve_ttl_ms: number; lock_break_ms: number } {
140
+ try {
141
+ const raw = JSON.parse(fs.readFileSync(_LIFECYCLE_CONFIG, 'utf-8')) as Record<string, unknown>;
142
+ const ttl = typeof raw['reserve_ttl_ms'] === 'number' ? raw['reserve_ttl_ms'] : 600_000;
143
+ const brk = typeof raw['lock_break_ms'] === 'number' ? raw['lock_break_ms'] : 30_000;
144
+ return { reserve_ttl_ms: ttl, lock_break_ms: brk };
145
+ } catch {
146
+ return { reserve_ttl_ms: 600_000, lock_break_ms: 30_000 };
147
+ }
148
+ }
149
+
150
+ export function reserveTtlMs(): number {
151
+ return _lifecycle().reserve_ttl_ms;
152
+ }
153
+
126
154
  interface ReserveEntry {
127
155
  ts_ms: number;
128
156
  tier: BudgetTier;
129
157
  est_usd: number;
130
- status: 'pending';
158
+ status: 'pending' | 'settled';
159
+ }
160
+
161
+ function _readReserves(reservePath: string): ReserveEntry[] {
162
+ if (!fs.existsSync(reservePath)) {
163
+ return [];
164
+ }
165
+ const out: ReserveEntry[] = [];
166
+ for (const line of fs.readFileSync(reservePath, 'utf-8').split('\n')) {
167
+ if (!line.trim()) continue;
168
+ try {
169
+ out.push(JSON.parse(line) as ReserveEntry);
170
+ } catch {
171
+ // tolerate a torn line — reserve file is advisory spend state
172
+ }
173
+ }
174
+ return out;
175
+ }
176
+
177
+ /** Take the reserve lock; break a stale one (crash leftover) once. */
178
+ function _takeLock(lock: string, now_ms: number, lock_break_ms: number): number | null {
179
+ for (let attempt = 0; attempt < 2; attempt += 1) {
180
+ try {
181
+ return fs.openSync(lock, 'wx');
182
+ } catch {
183
+ try {
184
+ const age = now_ms - fs.statSync(lock).mtimeMs;
185
+ if (age > lock_break_ms) {
186
+ fs.rmSync(lock, { force: true });
187
+ continue; // one retry after breaking the stale lock
188
+ }
189
+ } catch {
190
+ continue; // lock vanished between open and stat — retry once
191
+ }
192
+ return null;
193
+ }
194
+ }
195
+ return null;
131
196
  }
132
197
 
133
198
  /**
134
- * Atomic budget reserve: O_EXCL lock → sum ledger+reserves → append a
135
- * pending entry → release. Returns granted=false (never throws) when the
136
- * ceiling would be crossed or the lock cannot be taken quickly — the
137
- * caller falls back per the relation (fail-open at the DECISION layer,
138
- * conservative at the SPEND layer).
199
+ * Atomic budget reserve — full lifecycle (external review 2026-08-03):
200
+ *
201
+ * acquire → expire (TTL) / settle → compact-on-write
202
+ *
203
+ * Under the lock: live reserves = pending entries younger than the shared
204
+ * TTL (reserves are RACE PROTECTION, not spend accounting — real spend is
205
+ * the ledger's job); the file is COMPACTED to exactly those live entries
206
+ * on every acquire, so it stays bounded; a lock file older than
207
+ * `lock_break_ms` is a crash leftover and is broken with one retry.
208
+ * Returns granted=false (never throws) when the ceiling would be crossed
209
+ * or a fresh lock is genuinely held — the caller falls back per the
210
+ * relation (fail-open at the DECISION layer, conservative at the SPEND
211
+ * layer).
139
212
  */
140
213
  export function acquireBudgetPermit(options: {
141
214
  tracking_dir: string;
@@ -148,44 +221,45 @@ export function acquireBudgetPermit(options: {
148
221
  const dir = options.tracking_dir;
149
222
  fs.mkdirSync(dir, { recursive: true });
150
223
  const lock = path.join(dir, `${RESERVE_FILE}.lock`);
151
- let fd: number;
152
- try {
153
- fd = fs.openSync(lock, 'wx');
154
- } catch {
224
+ const { reserve_ttl_ms, lock_break_ms } = _lifecycle();
225
+ const fd = _takeLock(lock, options.now_ms, lock_break_ms);
226
+ if (fd === null) {
155
227
  return { granted: false, reason: 'reserve lock busy — treat tier as unavailable this instant' };
156
228
  }
157
229
  try {
158
230
  const reservePath = path.join(dir, RESERVE_FILE);
159
- let reserved = 0;
160
- if (fs.existsSync(reservePath)) {
161
- for (const line of fs.readFileSync(reservePath, 'utf-8').split('\n')) {
162
- if (!line.trim()) continue;
163
- try {
164
- const e = JSON.parse(line) as ReserveEntry;
165
- if (e.tier === options.tier && e.status === 'pending') {
166
- reserved += e.est_usd;
167
- }
168
- } catch {
169
- // tolerate a torn line — reserve file is advisory spend state
170
- }
171
- }
172
- }
231
+ const live = _readReserves(reservePath).filter(
232
+ (e) => e.status === 'pending' && options.now_ms - e.ts_ms < reserve_ttl_ms,
233
+ );
234
+ const reserved = live
235
+ .filter((e) => e.tier === options.tier)
236
+ .reduce((acc, e) => acc + e.est_usd, 0);
173
237
  if (
174
238
  options.ceiling_usd !== null &&
175
239
  options.spent_usd + reserved + options.estimated_cost_usd > options.ceiling_usd
176
240
  ) {
241
+ // Compact even on deny — expiry must not depend on a grant.
242
+ fs.writeFileSync(
243
+ reservePath,
244
+ live.map((e) => JSON.stringify(e)).join('\n') + (live.length ? '\n' : ''),
245
+ 'utf-8',
246
+ );
177
247
  return {
178
248
  granted: false,
179
249
  reason: `ceiling ${options.ceiling_usd} would be crossed (spent ${options.spent_usd} + reserved ${reserved})`,
180
250
  };
181
251
  }
182
- const entry: ReserveEntry = {
252
+ live.push({
183
253
  ts_ms: options.now_ms,
184
254
  tier: options.tier,
185
255
  est_usd: options.estimated_cost_usd,
186
256
  status: 'pending',
187
- };
188
- fs.appendFileSync(reservePath, `${JSON.stringify(entry)}\n`, 'utf-8');
257
+ });
258
+ fs.writeFileSync(
259
+ reservePath,
260
+ live.map((e) => JSON.stringify(e)).join('\n') + '\n',
261
+ 'utf-8',
262
+ );
189
263
  return { granted: true, reason: 'reserved' };
190
264
  } finally {
191
265
  fs.closeSync(fd);
@@ -193,7 +267,54 @@ export function acquireBudgetPermit(options: {
193
267
  }
194
268
  }
195
269
 
196
- /** Record a quota/429 error: the tier cools down; pickTier skips it. */
270
+ /**
271
+ * Settle the oldest live pending reserve of a tier (call when the real
272
+ * cost lands in the ledger, so the estimate stops counting immediately
273
+ * instead of waiting out the TTL). Settled entries are dropped by the
274
+ * next compaction. Best-effort: lock busy → no-op (the TTL is the
275
+ * backstop that makes settling optional, never load-bearing).
276
+ */
277
+ export function settlePermit(options: {
278
+ tracking_dir: string;
279
+ tier: BudgetTier;
280
+ now_ms: number;
281
+ }): boolean {
282
+ const lock = path.join(options.tracking_dir, `${RESERVE_FILE}.lock`);
283
+ const { reserve_ttl_ms, lock_break_ms } = _lifecycle();
284
+ const fd = _takeLock(lock, options.now_ms, lock_break_ms);
285
+ if (fd === null) {
286
+ return false;
287
+ }
288
+ try {
289
+ const reservePath = path.join(options.tracking_dir, RESERVE_FILE);
290
+ const live = _readReserves(reservePath).filter(
291
+ (e) => e.status === 'pending' && options.now_ms - e.ts_ms < reserve_ttl_ms,
292
+ );
293
+ const idx = live.findIndex((e) => e.tier === options.tier);
294
+ if (idx === -1) {
295
+ return false;
296
+ }
297
+ live.splice(idx, 1);
298
+ fs.writeFileSync(
299
+ reservePath,
300
+ live.map((e) => JSON.stringify(e)).join('\n') + (live.length ? '\n' : ''),
301
+ 'utf-8',
302
+ );
303
+ return true;
304
+ } finally {
305
+ fs.closeSync(fd);
306
+ fs.rmSync(lock, { force: true });
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Record a quota/429 error: the tier cools down; pickTier skips it.
312
+ *
313
+ * Deliberately lock-free: last-writer-wins on a 3-key timestamp map is
314
+ * benign (two concurrent trips write near-identical "until" values), so
315
+ * the reserve lock is not reused here (accepted in the 2026-08-03 review).
316
+ * Expired entries are purged on every write so the map never grows stale.
317
+ */
197
318
  export function tripCooldown(options: {
198
319
  tracking_dir: string;
199
320
  tier: BudgetTier;
@@ -208,6 +329,11 @@ export function tripCooldown(options: {
208
329
  } catch {
209
330
  state = {};
210
331
  }
332
+ for (const [k, v] of Object.entries(state)) {
333
+ if (typeof v !== 'number' || v <= options.now_ms) {
334
+ delete state[k];
335
+ }
336
+ }
211
337
  state[options.tier] = until;
212
338
  fs.mkdirSync(options.tracking_dir, { recursive: true });
213
339
  fs.writeFileSync(p, `${JSON.stringify(state, null, 2)}\n`, 'utf-8');
@@ -22,7 +22,7 @@ import * as path from 'node:path';
22
22
  import process from 'node:process';
23
23
  import { fileURLToPath } from 'node:url';
24
24
 
25
- import yaml from 'js-yaml';
25
+ import * as yaml from 'js-yaml';
26
26
 
27
27
  import { build_thin, RULES_SOURCE } from './project_thin_rules.js';
28
28
  import { cohensKappa } from './check_quality_regression.js';
@@ -29,7 +29,7 @@ import * as path from 'node:path';
29
29
  import process from 'node:process';
30
30
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
31
 
32
- import yaml from 'js-yaml';
32
+ import * as yaml from 'js-yaml';
33
33
 
34
34
  import { signTestP } from './second_brain_retrieval.js';
35
35
 
@@ -30,7 +30,7 @@ import * as path from 'node:path';
30
30
  import process from 'node:process';
31
31
  import { fileURLToPath, pathToFileURL } from 'node:url';
32
32
 
33
- import yaml from 'js-yaml';
33
+ import * as yaml from 'js-yaml';
34
34
 
35
35
  import { build_thin, RULES_SOURCE } from './project_thin_rules.js';
36
36
  import { evaluatePair, type JudgeFn, type JudgeVerdict, type PairResult } from './check_quality_regression.js';
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * CLI-registry ↔ evaluator-budget sync gate.
4
+ *
5
+ * The evaluator budget `cli_help_command_count` (src/config/evaluator-budgets.json)
6
+ * and the committed measurement record (agents/evidence/metrics/
7
+ * evaluator-measurements.json) are derived from `src/cli/registry.ts`, but until
8
+ * this gate nothing forced the PR that changes the registry to move them: the
9
+ * breach only WARNED on main and BLOCKED the next release. That is how 9.15.0
10
+ * went red — f02db54c2 registered `routing:doctor` (80 → 81 entries) and the
11
+ * drift surfaced days later, at release time, on an unrelated branch.
12
+ *
13
+ * This gate runs on every PR (Static Checks + `task preflight`) and fails the
14
+ * moment the three numbers disagree, so the registering PR carries the budget
15
+ * move itself.
16
+ *
17
+ * The count uses the SAME pinned method as evaluator_umbrella.sh — the number
18
+ * of `{ name: ` entry openers in src/cli/registry.ts (NOT --help prose
19
+ * parsing; methods historically disagreed 74 vs 76 vs 79).
20
+ *
21
+ * Exit codes: 0 in sync · 1 out of sync · 2 a required file is missing or
22
+ * unparseable (fail-closed: a gate that cannot read its inputs must not
23
+ * certify them).
24
+ */
25
+
26
+ import * as fs from 'node:fs';
27
+ import * as path from 'node:path';
28
+ import { fileURLToPath, pathToFileURL } from 'node:url';
29
+
30
+ const _HERE = fileURLToPath(import.meta.url);
31
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
32
+
33
+ const REGISTRY_PATH = path.join(REPO_ROOT, 'src/cli/registry.ts');
34
+ const BUDGETS_PATH = path.join(REPO_ROOT, 'src/config/evaluator-budgets.json');
35
+ const RECORD_PATH = path.join(REPO_ROOT, 'agents/evidence/metrics/evaluator-measurements.json');
36
+
37
+ const METRIC = 'cli_help_command_count';
38
+
39
+ /** Pinned method — byte-for-byte the umbrella's CLI_COUNT probe. */
40
+ function count_registry_entries(source: string): number {
41
+ return (source.match(/\{ name: /g) || []).length;
42
+ }
43
+
44
+ interface BudgetEntry {
45
+ max?: unknown;
46
+ last_measured?: unknown;
47
+ }
48
+
49
+ interface SyncInputs {
50
+ count: number;
51
+ budget: BudgetEntry | undefined;
52
+ recorded: unknown;
53
+ }
54
+
55
+ const REMEDY =
56
+ 'Fix, in THIS PR (the one that moves src/cli/registry.ts):\n' +
57
+ ` 1. src/config/evaluator-budgets.json → budgets.${METRIC}: set max +\n` +
58
+ ' last_measured to the new count, bump last_measured_at, and extend\n' +
59
+ ' baseline_note naming the commit that moves the surface.\n' +
60
+ ' 2. Regenerate the committed record (never hand-edit):\n' +
61
+ ' node src/scripts/record_evaluator_measurements.mjs \\\n' +
62
+ ' --measurements <file with the re-measured metrics> \\\n' +
63
+ ' --out agents/evidence/metrics/evaluator-measurements.json \\\n' +
64
+ ' --sha "$(git rev-parse HEAD)"\n' +
65
+ 'Otherwise the drift stays invisible until the next release, where the\n' +
66
+ 'evaluator gate blocks it (the 9.15.0 failure mode this gate exists to stop).\n';
67
+
68
+ /** Pure comparison — the CLI owns the filesystem. */
69
+ function collect_findings(inputs: SyncInputs): string[] {
70
+ const findings: string[] = [];
71
+ const { count, budget, recorded } = inputs;
72
+
73
+ if (budget === undefined) {
74
+ findings.push(`budgets.${METRIC} is missing from evaluator-budgets.json`);
75
+ return findings;
76
+ }
77
+ if (budget.last_measured !== count) {
78
+ findings.push(
79
+ `registry has ${count} entries but budgets.${METRIC}.last_measured says ` +
80
+ `${String(budget.last_measured)}`,
81
+ );
82
+ }
83
+ if (typeof budget.max !== 'number' || count > budget.max) {
84
+ findings.push(
85
+ `registry has ${count} entries but budgets.${METRIC}.max is ` +
86
+ `${String(budget.max)}`,
87
+ );
88
+ }
89
+ if (recorded === undefined) {
90
+ findings.push(
91
+ `measurements.${METRIC} is missing from the committed record ` +
92
+ '(agents/evidence/metrics/evaluator-measurements.json)',
93
+ );
94
+ } else if (recorded !== count) {
95
+ findings.push(
96
+ `registry has ${count} entries but the committed record says ` +
97
+ `${String(recorded)}`,
98
+ );
99
+ }
100
+ return findings;
101
+ }
102
+
103
+ function _read_json(p: string): Record<string, unknown> {
104
+ return JSON.parse(fs.readFileSync(p, 'utf-8')) as Record<string, unknown>;
105
+ }
106
+
107
+ function main(): number {
108
+ let registrySource: string;
109
+ let budgetsDoc: Record<string, unknown>;
110
+ let recordDoc: Record<string, unknown>;
111
+ try {
112
+ registrySource = fs.readFileSync(REGISTRY_PATH, 'utf-8');
113
+ budgetsDoc = _read_json(BUDGETS_PATH);
114
+ recordDoc = _read_json(RECORD_PATH);
115
+ } catch (err) {
116
+ process.stderr.write(
117
+ `ERROR: cannot read a gate input: ${err instanceof Error ? err.message : String(err)}\n`,
118
+ );
119
+ return 2;
120
+ }
121
+
122
+ const count = count_registry_entries(registrySource);
123
+ const budgets = budgetsDoc['budgets'] as Record<string, BudgetEntry> | undefined;
124
+ const measurements = recordDoc['measurements'] as Record<string, unknown> | undefined;
125
+ const findings = collect_findings({
126
+ count,
127
+ budget: budgets?.[METRIC],
128
+ recorded: measurements?.[METRIC],
129
+ });
130
+
131
+ process.stdout.write('scanned: 3\n');
132
+ if (findings.length > 0) {
133
+ for (const f of findings) {
134
+ process.stdout.write(`❌ ${METRIC} out of sync: ${f}\n`);
135
+ }
136
+ process.stdout.write('\n' + REMEDY);
137
+ return 1;
138
+ }
139
+ process.stdout.write(
140
+ `✅ ${METRIC} in sync — registry (${count}) == budget == committed record.\n`,
141
+ );
142
+ return 0;
143
+ }
144
+
145
+ function _isCliEntry(): boolean {
146
+ if (process.argv[1] === undefined) {
147
+ return false;
148
+ }
149
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
150
+ if (import.meta.url === argvUrl) {
151
+ return true;
152
+ }
153
+ // Symlinked invocations (installed projections, macOS /var → /private/var)
154
+ // make the raw URLs differ — compare realpaths so the entry guard fires.
155
+ try {
156
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
157
+ const argvPath = fs.realpathSync(path.resolve(process.argv[1]));
158
+ return here === argvPath;
159
+ } catch {
160
+ return false;
161
+ }
162
+ }
163
+
164
+ if (_isCliEntry() || process.argv[1] === _HERE) {
165
+ process.exit(main());
166
+ }
167
+
168
+ export { collect_findings, count_registry_entries, main };
@@ -14,7 +14,8 @@
14
14
  * - All headings from source present in condensed
15
15
  * - All code blocks preserved exactly
16
16
  * - YAML frontmatter identical
17
- * - Word count reduction within healthy range (10-60%)
17
+ * - Word count: excessive-reduction alarm only (> 60% lost warns; expected
18
+ * reduction is 0% post-ADR-201 verbatim copy — see the note near the check)
18
19
  * - Iron Law sections (## Iron Law / ### Iron Law / ## The Iron Law /
19
20
  * Iron Laws / numbered) preserved per `preservation-guard`: heading
20
21
  * verbatim at original level, structural-unit survival.