@event4u/agent-config 13.0.0 → 14.1.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 (123) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +195 -315
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/evals/roadmap.json +8 -0
  5. package/dist/agent-src/commands/fix/ci.md +39 -1
  6. package/dist/agent-src/commands/pr/create.md +65 -4
  7. package/dist/agent-src/commands/roadmap/next.md +28 -5
  8. package/dist/agent-src/commands/work.md +16 -0
  9. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
  10. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
  11. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
  12. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
  13. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
  14. package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
  15. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
  16. package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
  17. package/dist/agent-src/rules/design-review-after-ui-write.md +25 -8
  18. package/dist/agent-src/rules/ui-audit-gate.md +25 -8
  19. package/dist/agent-src/scripts/gate_execute.ts +395 -0
  20. package/dist/agent-src/scripts/resume_probe.ts +422 -0
  21. package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
  22. package/dist/agent-src/scripts/update_roadmap_progress.ts +53 -2
  23. package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
  24. package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
  25. package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
  26. package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
  27. package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
  28. package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
  29. package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
  30. package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
  31. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  32. package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
  33. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  34. package/dist/agent-src/templates/roadmaps.md +43 -3
  35. package/dist/cli/registry.js +1 -0
  36. package/dist/cli/registry.js.map +1 -1
  37. package/dist/cli-delegate/{chunk-N7FEST6D.js → chunk-7PZ4FSZB.js} +351 -174
  38. package/dist/cli-delegate/{chunk-3B72LSZL.js → chunk-F3YUF7SA.js} +1 -1
  39. package/dist/cli-delegate/cmd_conformance.js +336 -112
  40. package/dist/cli-delegate/cmd_doctor.js +2 -2
  41. package/dist/cli-delegate/cmd_handoff.js +1 -1
  42. package/dist/discovery/deprecation-report.md +1 -1
  43. package/dist/discovery/discovery-manifest.json +40 -18
  44. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  45. package/dist/discovery/discovery-manifest.summary.md +3 -3
  46. package/dist/discovery/dormancy-report.md +1 -1
  47. package/dist/discovery/orphan-report.md +1 -1
  48. package/dist/discovery/packs.json +6 -5
  49. package/dist/discovery/trust-report.md +2 -2
  50. package/dist/discovery/workspaces.json +5 -4
  51. package/dist/hooks/dispatch.js +2128 -1326
  52. package/dist/mcp/registry-manifest.json +3 -3
  53. package/dist/mcp/server.json +2 -2
  54. package/dist/mcp/server.mjs +1764 -899
  55. package/dist/router.json +1 -1
  56. package/dist/scripts/condense.js +1 -1
  57. package/dist/scripts/condense.js.map +1 -1
  58. package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
  59. package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
  60. package/dist/ui/index.html +1 -1
  61. package/package.json +1 -1
  62. package/src/agent-src/scripts/gate_execute.ts +395 -0
  63. package/src/agent-src/scripts/resume_probe.ts +422 -0
  64. package/src/agent-src/scripts/roadmap_gates.ts +138 -4
  65. package/src/agent-src/scripts/update_roadmap_progress.ts +53 -2
  66. package/src/config/ci-local-parity.yml +13 -0
  67. package/src/config/evaluator-budgets.json +4 -4
  68. package/src/config/gate-coverage.yml +111 -5
  69. package/src/config/gate-violation-baselines.json +5 -5
  70. package/src/config/rule-activation-census.json +41 -0
  71. package/src/config/rule-stub-ceilings.json +52 -0
  72. package/src/scripts/_dispatch.bash +4 -0
  73. package/src/scripts/_lib/evaluator_contract.ts +159 -0
  74. package/src/scripts/_lib/gate_ledger.ts +5 -1
  75. package/src/scripts/_lib/gh_transient.ts +80 -0
  76. package/src/scripts/_lib/release_material.ts +16 -5
  77. package/src/scripts/_lib/review_independence.ts +111 -0
  78. package/src/scripts/_lib/session_register.ts +14 -0
  79. package/src/scripts/_lib/skill_catalogue.ts +20 -0
  80. package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
  81. package/src/scripts/_lib/turn_end_refusals.ts +482 -0
  82. package/src/scripts/ai_council/_py_json.ts +96 -0
  83. package/src/scripts/ai_council/cli_call_budget.ts +357 -0
  84. package/src/scripts/ai_council/clients.ts +80 -143
  85. package/src/scripts/ai_council/config.ts +25 -11
  86. package/src/scripts/ai_council/probe_store.ts +196 -0
  87. package/src/scripts/ai_council/qualification.ts +425 -0
  88. package/src/scripts/ai_council/qualification_wiring.ts +243 -0
  89. package/src/scripts/ai_council/quorum_wiring.ts +211 -0
  90. package/src/scripts/ai_team/team_dispatch.ts +40 -6
  91. package/src/scripts/build_archive_index.ts +62 -3
  92. package/src/scripts/capture_skill_catalogue.ts +264 -4
  93. package/src/scripts/check_evaluator_schema.ts +249 -0
  94. package/src/scripts/check_gate_coverage.ts +93 -0
  95. package/src/scripts/check_pr_ci_current.ts +492 -0
  96. package/src/scripts/check_review_schema.ts +195 -0
  97. package/src/scripts/check_rule_activation_census.ts +451 -0
  98. package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
  99. package/src/scripts/condense.ts +1 -1
  100. package/src/scripts/council_cli.ts +141 -191
  101. package/src/scripts/dispatch_r2_reviewer.ts +8 -1
  102. package/src/scripts/git_authorization_hook.ts +186 -1
  103. package/src/scripts/hook_manifest.yaml +72 -5
  104. package/src/scripts/hooks/block_unauthorized_git.ts +62 -6
  105. package/src/scripts/hooks/concern_registry.ts +4 -0
  106. package/src/scripts/hooks/dispatch_hook.ts +43 -1
  107. package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
  108. package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
  109. package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
  110. package/src/scripts/hooks_doctor.ts +112 -0
  111. package/src/scripts/interruption_report.ts +443 -0
  112. package/src/scripts/lint_evidence_artifacts.ts +359 -0
  113. package/src/scripts/lint_roadmap_blockers.ts +65 -0
  114. package/src/scripts/lint_roadmap_complexity.ts +7 -1
  115. package/src/scripts/release.ts +38 -53
  116. package/src/scripts/rule_activation_census.ts +292 -0
  117. package/src/scripts/schemas/evaluator-output.schema.json +60 -0
  118. package/src/scripts/schemas/review-findings.schema.json +31 -0
  119. package/src/scripts/self_review_gate.ts +6 -0
  120. package/src/scripts/session_register_hook.ts +30 -1
  121. package/src/scripts/sync_pr_branch.ts +271 -0
  122. package/src/scripts/test_council_qualification.ts +213 -0
  123. package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
@@ -180,11 +180,11 @@ class RoadmapStats {
180
180
  }
181
181
 
182
182
  get open_blockers(): Blocker[] {
183
- return this.blockers.filter((b) => b.status !== 'resolved');
183
+ return this.blockers.filter((b) => !blocker_is_resolved(b));
184
184
  }
185
185
 
186
186
  get resolved_blockers(): Blocker[] {
187
- return this.blockers.filter((b) => b.status === 'resolved');
187
+ return this.blockers.filter((b) => blocker_is_resolved(b));
188
188
  }
189
189
 
190
190
  get done(): number {
@@ -365,6 +365,49 @@ interface Blocker {
365
365
  * costs one enum entry and turns a silent swallow into a rendered field.
366
366
  */
367
367
  question: string;
368
+ /**
369
+ * Gate taxonomy: `0` auto-run · `1` budget-preauthorized · `2`
370
+ * consent-once · `3` human-only. Empty string when the entry declares
371
+ * none, and an absent class READS AS 3 at every consumer.
372
+ *
373
+ * The default is the safe end on purpose. An agent that inferred the class
374
+ * at runtime could promote a Hard-Floor gate into something it executes;
375
+ * authoring it into the file means a misclassification is a reviewed edit,
376
+ * and `lint_roadmap_blockers` refuses a runnable class that names no
377
+ * command.
378
+ */
379
+ blockerClass: string;
380
+ /** The command a class-0/1 entry is resolved by running. */
381
+ run: string;
382
+ /** Spend estimate for class 1 — recorded, never enforced by the lint. */
383
+ budget: string;
384
+ }
385
+
386
+ /**
387
+ * Is this blocker settled?
388
+ *
389
+ * A PREFIX test, not equality, and the difference was measured: authors write
390
+ * the resolution into the status line — `**Status:** RESOLVED 2026-08-17 —
391
+ * **option (b)**, narrowed to two rules` — and an equality check matches
392
+ * neither `open` nor `resolved`, so the entry falls through to open. Found
393
+ * 2026-08-17 in `road-to-mixed-trigger-activation-cost`: a decision taken two
394
+ * days earlier was still being rendered by `agent-config gates` as a live one
395
+ * the reader owed an answer to, and it inflated every blocker count in the
396
+ * dashboard by one.
397
+ *
398
+ * `lint_roadmap_blockers` already read the same field as a prefix
399
+ * (`/^-\s*\*\*Status:\*\*\s*resolved/i`), so the two surfaces disagreed about
400
+ * what "resolved" means — the lint exempted the entry from its decidability
401
+ * ratchet while the dashboard counted it open.
402
+ */
403
+ function blocker_is_resolved(b: Blocker): boolean {
404
+ return /^resolved\b/.test(_strip(b.status).toLowerCase());
405
+ }
406
+
407
+ /** Class of a blocker, with the absent-field default applied. */
408
+ function blocker_class(b: Blocker): '0' | '1' | '2' | '3' {
409
+ const c = _strip(b.blockerClass).split(/[\s—–-]/)[0] as string;
410
+ return c === '0' || c === '1' || c === '2' ? c : '3';
368
411
  }
369
412
 
370
413
  // Strip fenced code blocks before blocker detection — a roadmap that shows the
@@ -508,6 +551,9 @@ function parse_blockers(raw_text: string): Blocker[] {
508
551
  recommendation: _blockerField(body, 'Recommendation') ?? '',
509
552
  ifNothing: _blockerField(body, 'If you do nothing') ?? '',
510
553
  question: _blockerField(body, 'Question') ?? '',
554
+ blockerClass: _blockerField(body, 'Class') ?? '',
555
+ run: _blockerField(body, 'Run') ?? '',
556
+ budget: _blockerField(body, 'Budget') ?? '',
511
557
  });
512
558
  }
513
559
  }
@@ -542,6 +588,9 @@ function parse_blockers(raw_text: string): Blocker[] {
542
588
  recommendation: '',
543
589
  ifNothing: '',
544
590
  question: '',
591
+ blockerClass: '',
592
+ run: '',
593
+ budget: '',
545
594
  });
546
595
  }
547
596
  return blockers;
@@ -1187,6 +1236,8 @@ export {
1187
1236
  is_roadmap_candidate,
1188
1237
  count_checkboxes,
1189
1238
  parse_blockers,
1239
+ blocker_class,
1240
+ blocker_is_resolved,
1190
1241
  blocker_needs_user,
1191
1242
  parse_roadmap,
1192
1243
  bar,
@@ -184,6 +184,19 @@ ci_only:
184
184
  # worth remote enforcement, OR it should simply be added to a workflow. Prefer
185
185
  # adding it: a local-only gate is the direction that let real defects merge.
186
186
  local_only:
187
+ - id: check_pr_ci_current
188
+ class: pre-push-scoped
189
+ reason: >
190
+ Compares the local branch tip against the open PR head and against the SHA
191
+ the PR's checks actually ran on. In CI that comparison is tautological by
192
+ construction — a workflow runs ON the PR head, so local, PR head and checks
193
+ head are the same commit and the gate can only ever pass. Its whole subject
194
+ is the window BEFORE a push (unpushed work, a green that ran against the
195
+ pre-fix commit, a PR head the checkout cannot reach because Update branch
196
+ was pressed), and that window does not exist inside a run. Declared rather
197
+ than wired: there is no remote equivalent to write, which is the distinction
198
+ this list exists to record. Sibling `check_branch_freshness` is a no-op in
199
+ CI for the same reason — the forge owns mergeability there.
187
200
  - id: roadmap_progress_hook
188
201
  class: git-hook-scoped
189
202
  reason: >
@@ -58,11 +58,11 @@
58
58
  "deterministic": false
59
59
  },
60
60
  "cli_help_command_count": {
61
- "max": 98,
62
- "last_measured": 98,
63
- "last_measured_at": "2026-08-15",
61
+ "max": 99,
62
+ "last_measured": 99,
63
+ "last_measured_at": "2026-08-17",
64
64
  "method": "registry enumeration — count of `{ name: '...', disposition` entries in src/cli/registry.ts (NOT --help prose parsing; methods historically disagreed 74 vs 76 vs 79)",
65
- "baseline_note": "80 → 81 on 2026-08-03 (release 9.15.0 gate). This RECORDS growth that already shipped through its own reviewed PR; it does not authorize new surface. The +1 is f02db54c2 (fix(cli): register routing:doctor in the CLI registry) — the command existed, only its registry entry was missing, and the registering PR did not move this budget. The gate did its job again: breach WARNS on main and BLOCKS on release, which is the conscious-decision point rather than a silently raised cap. Re-measured 2026-08-03 via the pinned method: 81. Headroom stays zero by design — the next registry addition must move this number in its own PR. 81 → 82 on 2026-08-04: the `handoff` subcommand (road-to-agent-handoff-resume Phase 4) registers in the same PR that adds the surface, per this record's own contract. Re-measured via the pinned method: 82. 82 → 84 on 2026-08-04: the `route:explain` + `route:audit` verbs (routing-correctness validation surface) register in the same PR that adds the surface. Re-measured via the pinned method: 84. 84 → 85 on 2026-08-05: the `settings:set` verb (road-to-zero-ceremony-settings Phase 2 — the one agent-reachable settings writer) registers in the same PR that adds the surface, per this record's own contract. Re-measured via the pinned method: 85. 85 → 86 on 2026-08-06: the `conformance:behavior` verb (road-to-agent-behavior-conformance Phase 5 — replays the local transcript store through the four mechanised conformance checks) registers in the same PR that adds the surface, per this record's own contract. Re-measured via the pinned method: 86. 86 → 88 on 2026-08-07: the `sessions:list` + `sessions:claim` verbs (road-to-parallel-session-coordination — the shell surface of the shared session register, needed because /roadmap:next is command markdown and can only reach the register through a shell call) register in the same PR that adds the surface, per this record's own contract. Re-measured via the pinned method: 88. 88 → 89 on 2026-08-08: the `council:status` verb registers in the same PR that adds the surface, per this record's own contract. It exists because an agent in a consumer project announced \"kein Council konfiguriert (keine .agent-settings.yml)\" and substituted a weaker path while the council was configured user-globally the whole time — there was no verb that answered availability, so the only route was an inference from a filename, and the intuitive filename is the one location ADR-104 guarantees is never consulted. Re-measured via the pinned method: 89. 89 → 91 on 2026-08-08: the `self-repair:status` + `self-repair:release` verbs (the queued-defect reader and the single Hard-Floor-gated outward step of the self-repair loop — the loop is deliberately unable to publish unless a human runs the release command) register in the same PR that adds the surface, per this record's own contract. Landed alongside the +1 council:status move in the same branch. Re-measured via the pinned method: 91. 91 → 94 on 2026-08-08: the `packs:active`, `settings:get` and `mcp:available` verbs (road-to-capability-answerability Phase 2 — the three capability probes for the gaps that had none) register in the same PR that adds the surface, per this record's own contract. Each answers a question that was previously only inferable from a filename: which packs are active and from which file, what a setting resolves to and which file set it, and what is declared in mcp.json versus what can actually launch. 94 -> 95 in the same PR: `brand:status` (Phase 2.4 — brand-layer presence, which the rule depended on and no surface could answer). Re-measured twice via the pinned method on the unchanged tree: 95/95. 95 → 96 on 2026-08-10: the `session:recycle` verb (road-to-token-economy-recycling Phase 2.2 — the deterministic producer of the main-session recycle envelope; the recycle advisory names this exact verb, so it must exist as a shell surface) registers in the same PR that adds the surface, per this record's own contract. Re-measured via the pinned method: 96. 96 -> 97 on 2026-08-10: the `gates` verb registers in the same PR that adds the surface, per this record's own contract. It exists because the blocker fields the dashboard already parses (Owner, What to do, Resolved when — CI-enforced by lint_roadmap_blockers) had no owner-filtered reader: the dashboard counts the open blockers across a 600-line file and links each to a per-roadmap anchor, so \"which of these are mine\" was a manual read of every anchor. The same inference-from-a-filename shape as the council:status entry above, one layer up. This branch and the session:recycle branch each measured 96 independently and landed on the same number for different verbs; the merge makes it 97, re-measured twice via the pinned method on the merged tree: 97/97. 97 -> 98 on 2026-08-15: the `workspace:doctor` verb (road-to-inbox-harvest-2026-08-c-workspace-identity Phase 3 — the read-only report over the new `workspaceIdentity()` resolver) registers in the same PR that adds the surface, per this record's own contract. It exists for the same inference-from-a-filename shape as the council:status and gates entries above, one layer down: the tree answered \"where am I\" in eight ad-hoc places across six files, and the 12.0.0 span shipped the same worktree misclassification twice, so no verb could be asked which checkout is the main one. Read-only by construction — the roadmap's Non-goals put worktree disposal out of scope and its Risk 3 names \"the pressure read becomes a disposal path by accident\" as the failure to design against, so the command carries no --fix and no --prune. Re-measured twice via the pinned method on the unchanged tree: 98/98."
65
+ "baseline_note": "80 → 81 on 2026-08-03 (release 9.15.0 gate). This RECORDS growth that already shipped through its own reviewed PR; it does not authorize new surface. The +1 is f02db54c2 (fix(cli): register routing:doctor in the CLI registry) — the command existed, only its registry entry was missing, and the registering PR did not move this budget. The gate did its job again: breach WARNS on main and BLOCKS on release, which is the conscious-decision point rather than a silently raised cap. Re-measured 2026-08-03 via the pinned method: 81. Headroom stays zero by design — the next registry addition must move this number in its own PR. 81 → 82 on 2026-08-04: the `handoff` subcommand (road-to-agent-handoff-resume Phase 4) registers in the same PR that adds the surface, per this record's own contract. Re-measured via the pinned method: 82. 82 → 84 on 2026-08-04: the `route:explain` + `route:audit` verbs (routing-correctness validation surface) register in the same PR that adds the surface. Re-measured via the pinned method: 84. 84 → 85 on 2026-08-05: the `settings:set` verb (road-to-zero-ceremony-settings Phase 2 — the one agent-reachable settings writer) registers in the same PR that adds the surface, per this record's own contract. Re-measured via the pinned method: 85. 85 → 86 on 2026-08-06: the `conformance:behavior` verb (road-to-agent-behavior-conformance Phase 5 — replays the local transcript store through the four mechanised conformance checks) registers in the same PR that adds the surface, per this record's own contract. Re-measured via the pinned method: 86. 86 → 88 on 2026-08-07: the `sessions:list` + `sessions:claim` verbs (road-to-parallel-session-coordination — the shell surface of the shared session register, needed because /roadmap:next is command markdown and can only reach the register through a shell call) register in the same PR that adds the surface, per this record's own contract. Re-measured via the pinned method: 88. 88 → 89 on 2026-08-08: the `council:status` verb registers in the same PR that adds the surface, per this record's own contract. It exists because an agent in a consumer project announced \"kein Council konfiguriert (keine .agent-settings.yml)\" and substituted a weaker path while the council was configured user-globally the whole time — there was no verb that answered availability, so the only route was an inference from a filename, and the intuitive filename is the one location ADR-104 guarantees is never consulted. Re-measured via the pinned method: 89. 89 → 91 on 2026-08-08: the `self-repair:status` + `self-repair:release` verbs (the queued-defect reader and the single Hard-Floor-gated outward step of the self-repair loop — the loop is deliberately unable to publish unless a human runs the release command) register in the same PR that adds the surface, per this record's own contract. Landed alongside the +1 council:status move in the same branch. Re-measured via the pinned method: 91. 91 → 94 on 2026-08-08: the `packs:active`, `settings:get` and `mcp:available` verbs (road-to-capability-answerability Phase 2 — the three capability probes for the gaps that had none) register in the same PR that adds the surface, per this record's own contract. Each answers a question that was previously only inferable from a filename: which packs are active and from which file, what a setting resolves to and which file set it, and what is declared in mcp.json versus what can actually launch. 94 -> 95 in the same PR: `brand:status` (Phase 2.4 — brand-layer presence, which the rule depended on and no surface could answer). Re-measured twice via the pinned method on the unchanged tree: 95/95. 95 → 96 on 2026-08-10: the `session:recycle` verb (road-to-token-economy-recycling Phase 2.2 — the deterministic producer of the main-session recycle envelope; the recycle advisory names this exact verb, so it must exist as a shell surface) registers in the same PR that adds the surface, per this record's own contract. Re-measured via the pinned method: 96. 96 -> 97 on 2026-08-10: the `gates` verb registers in the same PR that adds the surface, per this record's own contract. It exists because the blocker fields the dashboard already parses (Owner, What to do, Resolved when — CI-enforced by lint_roadmap_blockers) had no owner-filtered reader: the dashboard counts the open blockers across a 600-line file and links each to a per-roadmap anchor, so \"which of these are mine\" was a manual read of every anchor. The same inference-from-a-filename shape as the council:status entry above, one layer up. This branch and the session:recycle branch each measured 96 independently and landed on the same number for different verbs; the merge makes it 97, re-measured twice via the pinned method on the merged tree: 97/97. 97 -> 98 on 2026-08-15: the `workspace:doctor` verb (road-to-inbox-harvest-2026-08-c-workspace-identity Phase 3 — the read-only report over the new `workspaceIdentity()` resolver) registers in the same PR that adds the surface, per this record's own contract. It exists for the same inference-from-a-filename shape as the council:status and gates entries above, one layer down: the tree answered \"where am I\" in eight ad-hoc places across six files, and the 12.0.0 span shipped the same worktree misclassification twice, so no verb could be asked which checkout is the main one. Read-only by construction — the roadmap's Non-goals put worktree disposal out of scope and its Risk 3 names \"the pressure read becomes a disposal path by accident\" as the failure to design against, so the command carries no --fix and no --prune. Re-measured twice via the pinned method on the unchanged tree: 98/98. 98 -> 99 on 2026-08-17: the `council:quota` verb (road-to-council-quota-accounting-truth Phase 2) registers in the same PR that adds the surface, per this record's own contract. Exactly the inference-from-a-filename shape as the council:status entry above, one layer down: `cmd_quota` had existed in council_cli.ts and been reachable only via `scripts-run`, so the documented recovery path for an exhausted shared counter -- read the usage, reset one provider -- did not resolve through `agent-config`, and an operator debugging a quota failure met `unknown command`. The verb is read-only apart from the pre-existing `--reset <provider>`, which already required `--confirm`. Re-measured twice via the pinned method on the unchanged tree: 99/99."
66
66
  },
67
67
  "mcp_public_tool_count": {
68
68
  "max": 20,
@@ -40,13 +40,26 @@
40
40
  # visible, or it repeats the sin it exists to catch.
41
41
  # canary optional mutation recipe (Phase 7) — see CANARY below
42
42
  #
43
- # COVERAGE — THE HONEST DENOMINATOR (re-measured 2026-08-16)
43
+ # COVERAGE — THE HONEST DENOMINATOR (re-measured 2026-08-17)
44
44
  # ----------------------------------------------------------
45
- # 260 `lint_*` / `check_*` / `audit_*` scripts live under `src/scripts/`
45
+ # 266 `lint_*` / `check_*` / `audit_*` scripts live under `src/scripts/`
46
46
  # (the count is computed, not asserted — `check_gate_coverage.count_gate_scripts`).
47
- # **37 of them emit the machine-readable `scanned: <N>` line under their
48
- # CI-identical argv, and all 37 are listed below.** The manifest is therefore
49
- # complete with respect to its own contract, and covers ~14 % of the population.
47
+ # **41 of them emit the machine-readable `scanned: <N>` line under their
48
+ # CI-identical argv, and 40 of those 41 are listed below.** The manifest is
49
+ # therefore complete with respect to its own contract for every gate that can
50
+ # carry a floor, and covers ~15 % of the population.
51
+ #
52
+ # The one emitter deliberately NOT listed is `lint_evidence_artifacts`, for the
53
+ # same reason `check_rule_projection_integrity` is not (see the paragraph
54
+ # below): it is diff-scoped, so it legitimately emits `scanned: 0` on any
55
+ # branch that adds no evidence artifact, and `min_scanned: 0` is the
56
+ # false-count shape this file rejects. Its ledger half IS discharged — it
57
+ # adopts `_lib/gate_ledger.ts` and `_lib/scan_scope.ts`, so
58
+ # `check_gate_completeness` counts it as an adopter.
59
+ #
60
+ # Counting emitters rather than rows is the point of stating both numbers: the
61
+ # 2026-08-05 and 2026-08-10 readings below were miscounts precisely because one
62
+ # number was updated and the other was not.
50
63
  # (The 2026-08-05 reading said "32 … and all 31 are listed" and the 2026-08-10
51
64
  # reading said "33 … and all 33" while 34 entries were listed — those two are
52
65
  # the MISCOUNTS this paragraph exists to warn about, and they are listed here
@@ -169,6 +182,99 @@ gates:
169
182
  the Taskfile parse degrades and the population silently collapses —
170
183
  cannot pass.
171
184
 
185
+ - id: check_rule_activation_census
186
+ argv: []
187
+ min_scanned: 100
188
+ corpus: "rule files under src/rules/ (117 at baseline)"
189
+ status: enforced
190
+ note: >
191
+ Floor set below the live count so a legitimate rule retirement does not red
192
+ the gate that watches activation. It is still far above a collapse: if the
193
+ rule root moved, this gate's own assertScanned fires first and the manifest
194
+ row is the second net. The gate exists because commit 33c7c20 moved nineteen
195
+ rules from path-scoped to always-on with nothing watching, so a version of
196
+ it that silently scanned zero would be the same failure twice.
197
+
198
+ - id: check_rule_stub_ceiling
199
+ argv: []
200
+ min_scanned: 100
201
+ corpus: "rule files under src/rules/ (117 at baseline; 44 carry a migration pointer)"
202
+ status: enforced
203
+ note: >
204
+ Scans the whole rule root and reports that count, not the 44 migrated rules
205
+ it ratchets — the denominator is what proves the root still exists, and a
206
+ floor on the migrated subset would fall as rules legitimately finish
207
+ migrating. Floor set below the live 117 so a rule retirement does not red
208
+ the gate. The gate's own assertScanned fires first if the root moves; this
209
+ row is the second net. It exists because the sibling census pins the corpus
210
+ total with a 2,000-token drift allowance, which is green while one rule
211
+ grows and another shrinks — measured at adoption, the corpus sat 411 tokens
212
+ above its pinned baseline with that gate green.
213
+ canary:
214
+ class: contract-violation
215
+ path: src/rules/zz-canary-stub-probe.md
216
+ content: |
217
+ ---
218
+ type: "auto"
219
+ ---
220
+
221
+ # ZZ canary probe
222
+
223
+ Body migrated to `guideline:zz-canary-target-that-does-not-exist`.
224
+ # A created rule is the create-only shape this gate is built for: it carries
225
+ # a migration pointer whose target resolves nowhere AND has no ceiling in the
226
+ # baseline, so it trips two independent axes. Without a recipe the gate would
227
+ # ship permanently UNPROVEN — a green run over a real corpus is not evidence
228
+ # that the reading changes the verdict, which is the whole point of the
229
+ # canary. The gate also carries a `--self-test` with seven rejecting cases;
230
+ # the two prove different things and neither replaces the other.
231
+
232
+ - id: check_review_schema
233
+ argv: []
234
+ min_scanned: 1
235
+ corpus: "committed review-findings ledgers under agents/evidence/release-findings/ (1 at baseline)"
236
+ status: enforced
237
+ canary:
238
+ class: contract-violation
239
+ path: agents/evidence/release-findings/zz-canary-probe.json
240
+ content: |
241
+ {
242
+ "schema_version": 1,
243
+ "release": "0.0.0-canary",
244
+ "review_independence": "same-family",
245
+ "acceptance_status": "accepted",
246
+ "assurance": "independent",
247
+ "reviewers": ["anthropic", "anthropic"],
248
+ "findings": []
249
+ }
250
+ note: >
251
+ Floor is 1 because the corpus genuinely holds one ledger today, and a
252
+ floor of 0 would let the directory empty out silently — the shape this
253
+ manifest exists to refuse. The canary plants the exact defect the gate was
254
+ built for: a same-family reviewer set claiming `accepted`. It must red.
255
+
256
+ - id: check_evaluator_schema
257
+ argv: []
258
+ min_scanned: 5
259
+ corpus: "registered evaluator x recorded verifier fixture pairs (6 at baseline)"
260
+ status: enforced
261
+ canary:
262
+ class: contract-violation
263
+ path: tests/fixtures/evaluator-output/lint_output_slop.degraded.stdout
264
+ content: |
265
+ planted canary: output with no clean sentinel and no file:line rows
266
+ note: >
267
+ The corpus is (evaluator x fixture) pairs, not evaluators, because the
268
+ mapping is what can be wrong and each flavour exercises a different branch
269
+ of it. The floor sits below the live count so retiring one fixture flavour
270
+ does not red the change that retires it, but above 3 so a corpus that
271
+ silently collapsed to green-only cannot pass — a green-only corpus cannot
272
+ distinguish a working adapter from one that hardcodes success, which is
273
+ the whole reason red and degraded fixtures are committed alongside.
274
+ The canary plants a `degraded` stdout with no matching `.exit`, so the
275
+ adapter reads exit 0 with an unparseable body: expected `unreadable`,
276
+ asserted `present` by the green expectation table -> the gate must fail.
277
+
172
278
  - id: check_suppression_hygiene
173
279
  argv: []
174
280
  min_scanned: 8
@@ -34,9 +34,9 @@
34
34
  "note": "Markdown links between source artefacts that cross a pack boundary the source pack's `requires` graph does not reach — revealed when the scan root moved off the deleted packages/ tree to the live corpus (0 -> 753 artefacts scanned). 82 distinct pack pairs across 131 source files; largest single class is engineering-base -> meta (73), skills in a consumer-installable pack citing this package's own maintenance skills. Each is a link that dangles for a consumer who installs the source pack without the target's. Judged real drift rather than an over-strict gate because it is long-tailed — top bucket 22%, no single systematic cause — unlike the lint_artefact_frontmatter case, where one required shape had 0-of-288 adoption. Fix per finding: add the edge to packs.yml requires, retarget the link, or move the artefact."
35
35
  },
36
36
  "check_gate_completeness": {
37
- "count": 216,
38
- "landed": "2026-08-12",
39
- "note": "Ledger adoption ratchet. 216 of 244 registered gates do not yet carry per-target completeness accounting (`_lib/gate_ledger.ts`) or a `// ledger-exempt:` marker. This is deliberately NOT a sweep: road-to-skill-ecosystem-gate-integrity Phase 1 adopts exactly three gates (lint_framework_leakage, check_references, lint_roadmap_complexity) plus the checker itself to prove the interface, and the remainder migrates as gates are touched. Shrink-only from here — the number may not grow, so a NEW gate must adopt or exempt.",
37
+ "count": 215,
38
+ "landed": "2026-08-17",
39
+ "note": "Ledger adoption ratchet. Gates registered under the CI task roots that carry neither per-target completeness accounting (`_lib/gate_ledger.ts`) nor a `// ledger-exempt:` marker. This is deliberately NOT a sweep: road-to-skill-ecosystem-gate-integrity Phase 1 adopts exactly three gates (lint_framework_leakage, check_references, lint_roadmap_complexity) plus the checker itself to prove the interface, and the remainder migrates as gates are touched. Shrink-only from here — the number may not grow, so a NEW gate must adopt or exempt. Lowered 216 -> 215 on 2026-08-17 by discharging the obligation three NEW arrivals had incurred, which is the standing rule being enforced rather than a drain: the registered population grew 244 -> 258 and three of the fourteen arrivals adopted nothing — `build_archive_index`, `check_gate_coverage`, `check_pr_ci_current`. The reading was 218 against this baseline of 216, and the missing arithmetic is that one PRE-existing gate (`lint_rule_skill_pack_reach`) had adopted in the meantime: 216 + 3 - 1 = 218. All three adopted a real ledger. `check_pr_ci_current` first took a reasoned exemption claiming no target population; the completion review refuted it from the file itself — the check rows ARE a population, and rows whose `state` is not a string were being dropped before any verdict ran, so a dropped row could be the failing one. The exemption was withdrawn and the ledger adopted, counting kept rows as read, dropped rows as skips, and rows a pre-row verdict never reached as out of scope. The denominator in this note was 14 stale (it said 244) and is now expressed without one, because a count pinned to a population that grows weekly rots faster than the count itself. Note what this lowering does NOT buy: headroom is zero either way, so the next un-adopted arrival reds this gate immediately — which is the intended behaviour and the reason the number is not padded. The refreshed `landed` is load-bearing rather than bookkeeping: `checkRatchet` returns `improved` BEFORE it evaluates staleness, so an entry left at 216 while the tree measures 215 would never expire at any age (pinned by tests/lib/gate_baseline.test.ts, which asserts `improved` at landed + STALE_AFTER_DAYS + 400 days) — the greenest available state and the worst, since it switches off the anti-fossilization clause exactly when someone has done real work.",
40
40
  "falsifier": "./scripts-run src/scripts/check_gate_completeness"
41
41
  },
42
42
  "check_depth_budget": {
@@ -57,9 +57,9 @@
57
57
  "falsifier": "./scripts-run src/scripts/lint_settings_classes"
58
58
  },
59
59
  "check_source_size_budget": {
60
- "count": 19725,
60
+ "count": 19611,
61
61
  "landed": "2026-08-17",
62
- "note": "Lowered 19,763 → 19,725 on 2026-08-17 by the T4/T5 endpoint branch, and it is a lowering rather than a re-anchor: that branch grew two already-over-ceiling files (bench_ab_v2_run.ts, bench_ab_v2_stats.ts) by 89 lines and paid it back by EXTRACTION — the workspace/transcript lifecycle and the T5 report adapters moved into `_lib/`, which is exactly what this entry means by lowering happening through splitting a god-file. Trimming comments was tried first and reached 5 of the 89; the extraction went 38 past the baseline, which is the difference between the two moves. Total lines above the 1,500-line source ceiling, summed across the 1,063 hand-written `.ts` files under `src/` — `*.d.ts` and `*.test.ts` are excluded, the same two kinds `_lib/gate_population` excludes and for the same stated reasons (a declaration has no runtime behaviour; a test file is not the reviewable code this ceiling bounds). Latent rather than cosmetic: a GENERATED `.d.ts` over the ceiling would otherwise red a ratchet aimed at hand-written code. Revealed by introducing check_source_size_budget, not created by it: every other size budget in this suite guards prose (rules by char cap, skills by token class, the depth layer by check_depth_budget), and the TypeScript the package actually ships carried none — which is where the largest artefacts in the tree sit. Fifteen files are over the ceiling; the worst three are install.ts (5,461), skill_linter.ts (4,742) and council_cli.ts (4,058). THE METRIC IS EXCESS LINES, NOT A FILE COUNT, and that is the load-bearing choice: a file count would read 15 whether install.ts sits at 5,461 or at 9,000, so the gate would stay green while the exact defect it exists for got worse. Summing the excess costs the same one integer and fails on growth inside an already-listed file — pinned by a self-test case that holds the file count at 1 and moves only the size. Baselined rather than hard-failed because 15 god-files nobody can split in the change that reveals them is the gate-that-lands-as-N-instant-blockers failure this repository has refused before; shrink-only keeps the property that matters, namely that a NEW file over 1,500 lines reds immediately. The ceiling is a growth ratchet, never a quality threshold: 1,500 was chosen by measuring this tree (15 files over 1,500, versus 44 over 1,000 and 75 over 800) as the tightest ceiling whose ledger stays signal rather than becoming the 20-plus-entry allowlist this repository's own rules call evidence that the linter is wrong. Lowering happens by splitting a god-file, and the ordering is deliberate: ratchet BEFORE split, so every later split is a lowering commit whose gain is visible in one integer, instead of a large diff with nothing asserting it improved anything. The 56-day expiry is the point, not a hazard — a ceiling with no forcing function would leave these fifteen files at these fifteen sizes indefinitely with the gate green the whole time. Re-anchored 19,676 → 19,763 on 2026-08-16 when this branch merged current main, and the +87 is population growth rather than a loosening — the same shape the blocker ratchet above records for its own +4. Every one of the 87 lines is `src/scripts/mcp_server/tools.ts` growing on the trunk between this branch's base (8edeae39) and the merge; no file in this branch's own diff is over the ceiling at all, which is checkable in one command: `git diff --stat <base> origin/main -- <the fifteen listed files>`. Recording 19,676 against a 19,763-line tree is the failure this entry's own test asserts against — a baseline below the truth fails the gate on arrival, and one above it silently sells a free regression.",
62
+ "note": "Lowered 19,626 → 19,611 on 2026-08-17 by the release-gh-transient branch, and it is a LOWERING through extraction, never a raise — the −15 is against the trunk value, reproducible with `git stash && npx tsx src/scripts/check_source_size_budget.ts`. That branch hardened `release.ts` against GitHub's 5xx responses, which is exactly the kind of change that grows an already over-ceiling file: release.ts sits at 2,819 and carries 1,319 of this total. The retry loop, the transient classifier and the argv elider therefore went into a NEW module (`_lib/gh_transient.ts`, 74 lines, under the ceiling and contributing nothing here), leaving release.ts a 20-line wrapper. The payback came from the five call sites the wrapper replaced: each was a `run(['gh', …], {check, capture})` block spread over up to 17 lines, and each collapses to one `gh([…])` call. The ordering is the lesson and it matches the two entries below: extract, then measure — a wrapper written inline would have added ~20 lines and turned a hardening commit into a ratchet regression. Second lesson, learned the hard way on this same branch: pin the baseline ONCE, at the end. Two intermediate pins (19,606 then 19,612) made subsequent legitimate edits read as `N new` against numbers that had never been on the trunk.",
63
63
  "falsifier": "./scripts-run src/scripts/check_source_size_budget --quiet"
64
64
  }
65
65
  }
@@ -0,0 +1,41 @@
1
+ {
2
+ "_comment": "Rule-activation ratchet baseline (road-to-mixed-trigger-activation-cost step 4.1). Two axes: the scoped/mixed ID SETS are pinned by identity, and the unconditional token total may only walk DOWN. Raising a number here to clear a failing check is the config-weakening move this repo blocks by construction \u2014 every raise needs a baseline_history entry whose reason is a real sentence. Regenerate with `check_rule_activation_census --write-baseline`; never hand-edit the counts.",
3
+ "measured_at_commit": "097ab6549",
4
+ "tokens_exact": true,
5
+ "unconditional_tokens": 103265,
6
+ "scoped_ids": [
7
+ "design-review-after-ui-write",
8
+ "no-roadmap-references",
9
+ "roadmap-progress-sync",
10
+ "rule-type-governance",
11
+ "skill-quality",
12
+ "source-confidentiality",
13
+ "source-of-truth",
14
+ "ui-audit-gate"
15
+ ],
16
+ "mixed_ids": [
17
+ "augment-edit-discipline",
18
+ "design-fidelity",
19
+ "doc-screenshot-hygiene",
20
+ "domain-adoption-policy",
21
+ "framework-neutrality-in-generic-skills",
22
+ "image-likeness-and-rights",
23
+ "laravel-translations",
24
+ "lethal-trifecta-guard",
25
+ "linked-projects-onboarding-gate",
26
+ "low-impact-corpus-privacy-floor",
27
+ "markdown-safe-codeblocks",
28
+ "onboarding-gate",
29
+ "persona-governance",
30
+ "php-coding",
31
+ "provider-lifecycle-discipline",
32
+ "roadmap-ci-steps-policy",
33
+ "settings-ask-protocol"
34
+ ],
35
+ "baseline_history": [
36
+ {
37
+ "date": "2026-08-17",
38
+ "reason": "Re-anchored WITH the authorized change, not to clear a red. road-to-mixed-trigger-activation-cost Phase 2, amendment option (b) narrowed to the two path-dominant per-edit rules: `design-review-after-ui-write` and `ui-audit-gate` had their `component` and `design token` keyword triggers removed as an authoring decision, so the emitter restores `paths:` for both. Scoped 6 -> 8, mixed 19 -> 17, unconditional corpus 107194 -> the value above. The direction is DOWN on the weight axis and the identity move is the authorized one; the maintainer asked for this entry in the same commit so the ratchet documents its own sanctioned change instead of reporting it as drift."
39
+ }
40
+ ]
41
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ "_comment": "Per-rule stub ceilings (road-to-rule-stub-projection Phase 2). One exact-BPE token ceiling per rule carrying a migration pointer. SHRINK-ONLY: a rule may fall below its ceiling freely, and may not rise above it. Raising a ceiling to clear a failing check is the config-weakening move this repo blocks by construction — every raise needs a `history` entry whose `reason` is a real sentence. The ceilings are a floor-plus-residue SNAPSHOT taken at adoption, not a target: a rule at its ceiling is not thereby correct, only not worse. Regenerate with `check_rule_stub_ceiling --write-baseline`; never hand-edit the counts.",
3
+ "measured_at_commit": "d8e2ee655",
4
+ "tokens_exact": true,
5
+ "ceilings": {
6
+ "active-remediation": 1601,
7
+ "analysis-skill-routing": 76,
8
+ "architecture": 955,
9
+ "artifact-drafting-protocol": 611,
10
+ "augment-edit-discipline": 137,
11
+ "autonomous-execution": 1633,
12
+ "brand-consistency": 133,
13
+ "broken-access-control": 743,
14
+ "cli-output-handling": 83,
15
+ "code-comment-discipline": 692,
16
+ "commit-conventions": 136,
17
+ "context-hygiene": 2447,
18
+ "copilot-routing": 128,
19
+ "decision-revisit-gate": 545,
20
+ "design-fidelity": 2245,
21
+ "devcontainer-routing": 128,
22
+ "docker-commands": 79,
23
+ "domain-adoption-policy": 497,
24
+ "framework-neutrality-in-generic-skills": 621,
25
+ "git-history-discipline": 1205,
26
+ "improve-before-implement": 845,
27
+ "laravel-routing": 126,
28
+ "laravel-translations": 82,
29
+ "legal-safety-floor": 1401,
30
+ "linked-projects-onboarding-gate": 172,
31
+ "minimal-safe-diff": 893,
32
+ "missing-tool-handling": 103,
33
+ "model-recommendation": 156,
34
+ "no-roadmap-references": 644,
35
+ "package-ci-checks": 74,
36
+ "persona-governance": 330,
37
+ "php-coding": 80,
38
+ "provider-lifecycle-discipline": 472,
39
+ "reviewer-awareness": 131,
40
+ "roadmap-ci-steps-policy": 564,
41
+ "roadmap-progress-sync": 1424,
42
+ "rule-type-governance": 87,
43
+ "skill-improvement-trigger": 81,
44
+ "skill-quality": 78,
45
+ "slash-command-routing-policy": 77,
46
+ "symfony-routing": 129,
47
+ "ui-audit-gate": 1280,
48
+ "untrusted-input-defense": 845,
49
+ "upstream-proposal": 76
50
+ },
51
+ "history": []
52
+ }
@@ -162,6 +162,9 @@ Tier 1 — power-user (release shape, audit, migration):
162
162
  Usage: council:render <responses.json>
163
163
  council:status Report whether an AI council is configured, and from where
164
164
  (no API call, no spend)
165
+ council:quota Show today's per-provider CLI-call usage against the
166
+ enforced cap; --reset <provider> --confirm clears one
167
+ (no API call, no spend)
165
168
  self-repair:status List queued agent-config defect records (read-only)
166
169
  self-repair:release Publish one defect record — a PR when the fix can be
167
170
  pushed, else an issue. Running it is your Hard-Floor
@@ -1443,6 +1446,7 @@ main() {
1443
1446
  council:run) cmd_council run "$@" ;;
1444
1447
  council:render) cmd_council render "$@" ;;
1445
1448
  council:status) cmd_council status "$@" ;;
1449
+ council:quota) cmd_council quota "$@" ;;
1446
1450
  self-repair:status) cmd_self_repair status "$@" ;;
1447
1451
  self-repair:release) cmd_self_repair release "$@" ;;
1448
1452
  update) cmd_update "$@" ;;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The evaluator-output contract: types, the registry of wrapped verifiers, and
3
+ * the validator both the gate and any future loop share.
4
+ *
5
+ * Contract: `docs/contracts/evaluator-output.md`.
6
+ * Schema: `src/scripts/schemas/evaluator-output.schema.json`.
7
+ *
8
+ * The adapters below are PURE — each maps a verifier's already-captured
9
+ * `{stdout, stderr, exitCode}` onto the contract shape. That is deliberate and
10
+ * it is what makes the gate cheap: the mapping is the part that can be wrong,
11
+ * and it can be tested against recorded fixtures without paying to re-run three
12
+ * verifiers CI already runs once. Spike s02 established that none of the three
13
+ * needs a change on its own side for this to work.
14
+ */
15
+ import { load_schema, validate, type YamlValue } from '../validate_frontmatter.js';
16
+
17
+ export type MetricState = 'present' | 'absent' | 'unreadable';
18
+ export type Direction = 'maximize' | 'minimize';
19
+
20
+ export interface EvaluatorOutput {
21
+ readonly schema_version: 1;
22
+ readonly name: string;
23
+ readonly pass: boolean;
24
+ readonly score: number;
25
+ readonly metric?: number;
26
+ readonly metric_state?: MetricState;
27
+ readonly direction?: Direction;
28
+ readonly error?: string;
29
+ }
30
+
31
+ /** What a verifier invocation produced, before any interpretation. */
32
+ export interface RawRun {
33
+ readonly stdout: string;
34
+ readonly stderr: string;
35
+ readonly exitCode: number;
36
+ }
37
+
38
+ export interface EvaluatorAdapter {
39
+ readonly name: string;
40
+ /** The verifier this adapter wraps, as its CI-identical script name. */
41
+ readonly wraps: string;
42
+ readonly direction: Direction;
43
+ readonly parse: (raw: RawRun) => EvaluatorOutput;
44
+ }
45
+
46
+ /**
47
+ * Read a count off either stream.
48
+ *
49
+ * Both streams on purpose: `check_references` writes its count to stderr
50
+ * (measured, spike s02), and a stdout-only reader returned a null metric beside
51
+ * `pass: true` — a degraded reading that looked clean. A wrapper reads both; it
52
+ * still emits its own verdict on stdout alone, per the contract.
53
+ */
54
+ function readCount(raw: RawRun, re: RegExp): number | null {
55
+ const m = `${raw.stdout}\n${raw.stderr}`.match(re);
56
+ if (m === null || m[1] === undefined) return null;
57
+ const n = Number(m[1]);
58
+ return Number.isFinite(n) ? n : null;
59
+ }
60
+
61
+ /**
62
+ * Fold a possibly-missing count into the three contract fields.
63
+ *
64
+ * `expected` says whether this verifier is supposed to have a number at all.
65
+ * When it is and the number is missing, the result is `unreadable` — never
66
+ * `absent`, and never a silent zero. That collapse is the exact defect the
67
+ * `metric_state` field exists to prevent.
68
+ */
69
+ function withMetric(
70
+ base: Omit<EvaluatorOutput, 'metric' | 'metric_state' | 'score'>,
71
+ count: number | null,
72
+ expected: boolean,
73
+ ): EvaluatorOutput {
74
+ if (count !== null) {
75
+ return { ...base, metric: count, metric_state: 'present', score: -count };
76
+ }
77
+ // `metric` is OMITTED rather than set to null: the subset validator this
78
+ // estate ships rejects an array-typed `type`, so the schema spells the field
79
+ // as an optional number. Absence is the null.
80
+ if (!expected) {
81
+ return { ...base, metric_state: 'absent', score: base.pass ? 0 : -1 };
82
+ }
83
+ return {
84
+ ...base,
85
+ metric_state: 'unreadable',
86
+ score: -1,
87
+ error: 'expected a count in the verifier output and found none',
88
+ };
89
+ }
90
+
91
+ export const EVALUATORS: readonly EvaluatorAdapter[] = [
92
+ {
93
+ name: 'validate_frontmatter',
94
+ wraps: 'validate_frontmatter',
95
+ direction: 'minimize',
96
+ parse: (raw) =>
97
+ withMetric(
98
+ { schema_version: 1, name: 'validate_frontmatter', pass: raw.exitCode === 0, direction: 'minimize' },
99
+ // Group 2 is the failing count; group 1 is the corpus size, which
100
+ // is the denominator and not the thing being minimized.
101
+ readCount(raw, /\d+\s+artefacts,\s+(\d+)\s+failing/),
102
+ true,
103
+ ),
104
+ },
105
+ {
106
+ name: 'lint_output_slop',
107
+ wraps: 'lint_output_slop',
108
+ direction: 'minimize',
109
+ parse: (raw) => {
110
+ const text = `${raw.stdout}\n${raw.stderr}`;
111
+ const clean = /clean\s+—\s+no placeholder-prose patterns found/.test(text);
112
+ const count = clean ? 0 : (text.match(/^\s*\S+:\d+/gm) ?? []).length;
113
+ return withMetric(
114
+ { schema_version: 1, name: 'lint_output_slop', pass: raw.exitCode === 0, direction: 'minimize' },
115
+ count,
116
+ true,
117
+ );
118
+ },
119
+ },
120
+ {
121
+ name: 'check_references',
122
+ wraps: 'check_references',
123
+ direction: 'minimize',
124
+ parse: (raw) => {
125
+ const text = `${raw.stdout}\n${raw.stderr}`;
126
+ const scanned = readCount(raw, /scanned:\s*(\d+)/);
127
+ // A gate that scanned nothing has not measured a clean tree — it has
128
+ // not measured. Report that as unreadable rather than as zero broken
129
+ // references, which is the manufactured-green shape this estate names.
130
+ if (scanned === null || scanned === 0) {
131
+ return withMetric(
132
+ { schema_version: 1, name: 'check_references', pass: raw.exitCode === 0, direction: 'minimize' },
133
+ null,
134
+ true,
135
+ );
136
+ }
137
+ const clean = /No broken references found/.test(text);
138
+ return withMetric(
139
+ { schema_version: 1, name: 'check_references', pass: raw.exitCode === 0, direction: 'minimize' },
140
+ clean ? 0 : (text.match(/^\s*broken:/gim) ?? []).length || 1,
141
+ true,
142
+ );
143
+ },
144
+ },
145
+ ];
146
+
147
+ /**
148
+ * Schema violations for one evaluator output. Empty array = conformant.
149
+ *
150
+ * `SchemaError.format()` rather than `String(e)`: the class carries no
151
+ * `toString`, so stringifying it yields `[object Object]` — a violation list
152
+ * that names nothing, which is indistinguishable from a broken validator.
153
+ */
154
+ export function validateEvaluatorOutput(value: unknown): string[] {
155
+ const schema = load_schema('evaluator-output');
156
+ return validate(value as YamlValue, schema)
157
+ .filter((e) => e.severity !== 'warning')
158
+ .map((e) => e.format());
159
+ }
@@ -57,7 +57,9 @@ export type GateSkipReason =
57
57
  | 'generated_artifact'
58
58
  | 'dead_scan_root'
59
59
  | 'declared_exemption'
60
- | 'not_applicable_kind';
60
+ | 'not_applicable_kind'
61
+ | 'precondition_unmet'
62
+ | 'check_did_not_run';
61
63
 
62
64
  /**
63
65
  * One sentence per skip code, printed verbatim in the ledger's own output.
@@ -79,6 +81,8 @@ export const SKIP_REASON_MESSAGE: Record<GateSkipReason, string> = {
79
81
  dead_scan_root: 'the root this target was enumerated from no longer exists',
80
82
  declared_exemption: 'the target carries an explicit, reviewable exemption from this check',
81
83
  not_applicable_kind: 'the target is a kind of artifact this check does not apply to',
84
+ precondition_unmet: 'a precondition of this run settled the verdict before the target could be inspected',
85
+ check_did_not_run: 'the check itself could not be executed, so the target was never read',
82
86
  };
83
87
 
84
88
  /** Terminal outcomes a planned target can reach. Exactly one, exactly once. */