@event4u/agent-config 14.1.0 → 14.4.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 (120) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +291 -181
  3. package/agents/templates/.ai-council.yml.example +35 -0
  4. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +83 -12
  5. package/dist/agent-src/rules/roadmap-progress-sync.md +48 -4
  6. package/dist/agent-src/scripts/resume_probe.ts +154 -9
  7. package/dist/agent-src/scripts/roadmap_gates.ts +210 -6
  8. package/dist/agent-src/skills/roadmap-management/SKILL.md +9 -4
  9. package/dist/agent-src/templates/agent-settings.md +70 -0
  10. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  11. package/dist/agent-src/templates/scripts/telemetry/remote.ts +571 -0
  12. package/dist/agent-src/templates/scripts/telemetry/settings.ts +222 -44
  13. package/dist/cli/python/workspace_drive.js +8 -3
  14. package/dist/cli/python/workspace_drive.js.map +1 -1
  15. package/dist/cli/python/workspace_hosts.js +16 -12
  16. package/dist/cli/python/workspace_hosts.js.map +1 -1
  17. package/dist/cli/registry.js +2 -0
  18. package/dist/cli/registry.js.map +1 -1
  19. package/dist/cli-delegate/{chunk-F3YUF7SA.js → chunk-3UCJWMMP.js} +1 -1
  20. package/dist/cli-delegate/chunk-F7DQWYR5.js +69 -0
  21. package/dist/cli-delegate/{chunk-7PZ4FSZB.js → chunk-FBKL6JWT.js} +61 -2
  22. package/dist/cli-delegate/{chunk-MNWRDMR5.js → chunk-PKOBQADP.js} +1 -1
  23. package/dist/cli-delegate/cmd_conformance.js +26 -11
  24. package/dist/cli-delegate/cmd_doctor.js +2 -2
  25. package/dist/cli-delegate/cmd_handoff.js +3 -3
  26. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  27. package/dist/cli-delegate/cmd_settings_set.js +17 -3
  28. package/dist/discovery/deprecation-report.md +1 -1
  29. package/dist/discovery/discovery-manifest.json +4 -4
  30. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  31. package/dist/discovery/discovery-manifest.summary.md +1 -1
  32. package/dist/discovery/dormancy-report.md +1 -1
  33. package/dist/discovery/orphan-report.md +1 -1
  34. package/dist/discovery/packs.json +2 -2
  35. package/dist/discovery/trust-report.md +1 -1
  36. package/dist/discovery/workspaces.json +2 -2
  37. package/dist/hooks/dispatch.js +3773 -1731
  38. package/dist/mcp/registry-manifest.json +2 -2
  39. package/dist/mcp/server.json +2 -2
  40. package/dist/mcp/server.mjs +1070 -408
  41. package/dist/scripts/_lib/host_launch.js +66 -0
  42. package/dist/scripts/_lib/host_launch.js.map +1 -0
  43. package/dist/server/io/yamlIO.js +175 -3
  44. package/dist/server/io/yamlIO.js.map +1 -1
  45. package/dist/server/routes/wizard.js +20 -1
  46. package/dist/server/routes/wizard.js.map +1 -1
  47. package/dist/shared/settingsConsent.js +25 -0
  48. package/dist/shared/settingsConsent.js.map +1 -1
  49. package/docs/contracts/settings-classes.md +11 -0
  50. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +2 -1
  51. package/package.json +1 -1
  52. package/src/agent-src/scripts/resume_probe.ts +154 -9
  53. package/src/agent-src/scripts/roadmap_gates.ts +210 -6
  54. package/src/agent-src/templates/scripts/telemetry/remote.ts +571 -0
  55. package/src/agent-src/templates/scripts/telemetry/settings.ts +222 -44
  56. package/src/config/estate-count-budget.json +120 -0
  57. package/src/config/evaluator-budgets.json +4 -4
  58. package/src/config/gate-coverage.yml +32 -4
  59. package/src/config/gate-violation-baselines.json +63 -63
  60. package/src/config/gitignore-block.txt +5 -0
  61. package/src/config/hook-latency-budget.json +40 -1
  62. package/src/config/hook-token-budget.json +22 -0
  63. package/src/config/rule-stub-ceilings.json +11 -3
  64. package/src/scripts/_dispatch.bash +42 -1
  65. package/src/scripts/_lib/council_fallback_posture.ts +81 -0
  66. package/src/scripts/_lib/council_fallback_wiring.ts +126 -0
  67. package/src/scripts/_lib/council_settings_block.ts +152 -0
  68. package/src/scripts/_lib/headless_invocation.ts +285 -0
  69. package/src/scripts/_lib/host_launch.ts +72 -0
  70. package/src/scripts/_lib/map_to_object.ts +16 -0
  71. package/src/scripts/_lib/roadmap_checkboxes.ts +88 -0
  72. package/src/scripts/_lib/run_checkpoint.ts +409 -0
  73. package/src/scripts/_lib/session_register.ts +32 -1
  74. package/src/scripts/_lib/skill_catalogue_series.ts +91 -0
  75. package/src/scripts/_lib/stdin.ts +35 -1
  76. package/src/scripts/_lib/turn_end_refusals.ts +12 -4
  77. package/src/scripts/_lib/unattended_guard.ts +371 -0
  78. package/src/scripts/ai_council/config.ts +27 -1
  79. package/src/scripts/ai_council/events_log.ts +12 -1
  80. package/src/scripts/ai_council/fallback_config.ts +114 -0
  81. package/src/scripts/ai_council/mid_flight_fallback.ts +295 -0
  82. package/src/scripts/ai_council/orchestrator.ts +251 -292
  83. package/src/scripts/ai_council/py_parity.ts +139 -0
  84. package/src/scripts/ai_council/response_render.ts +99 -0
  85. package/src/scripts/ai_council/spend_gate.ts +123 -0
  86. package/src/scripts/ai_council/transport_resolver.ts +81 -13
  87. package/src/scripts/bench_hook_injection.ts +100 -6
  88. package/src/scripts/bench_hook_latency.ts +442 -16
  89. package/src/scripts/check_estate_count.ts +924 -0
  90. package/src/scripts/compile_hook_manifest.ts +67 -0
  91. package/src/scripts/council_cli.ts +100 -125
  92. package/src/scripts/decision_memo.ts +325 -0
  93. package/src/scripts/dispatch_r2_reviewer.ts +169 -15
  94. package/src/scripts/hook_manifest.json +1 -0
  95. package/src/scripts/hook_manifest.yaml +172 -25
  96. package/src/scripts/hooks/concern_registry.ts +6 -0
  97. package/src/scripts/hooks/dispatch_hook.ts +183 -10
  98. package/src/scripts/hooks/dispatch_issues.ts +26 -0
  99. package/src/scripts/hooks/end_review_nudge_hook.ts +97 -24
  100. package/src/scripts/hooks/hook_stdin.ts +111 -5
  101. package/src/scripts/hooks/injection_budget.ts +595 -0
  102. package/src/scripts/hooks/payload_stub.ts +446 -0
  103. package/src/scripts/hooks/run_continuation_hook.ts +627 -0
  104. package/src/scripts/hooks/session_eol_hook.ts +52 -0
  105. package/src/scripts/hooks/skill_route_hook.ts +172 -11
  106. package/src/scripts/hooks/telemetry_usage_hook.ts +339 -0
  107. package/src/scripts/hooks/tool_result_bytes_hook.ts +27 -0
  108. package/src/scripts/hooks/turn_end_gate_hook.ts +45 -7
  109. package/src/scripts/hooks_doctor.ts +16 -2
  110. package/src/scripts/install-hooks.sh +26 -0
  111. package/src/scripts/interruption_report.ts +330 -13
  112. package/src/scripts/lint_hook_manifest.ts +167 -0
  113. package/src/scripts/roadmap_progress_hook.ts +254 -46
  114. package/src/scripts/run_supervise.ts +623 -0
  115. package/src/scripts/session_register_hook.ts +108 -8
  116. package/src/scripts/sessions_cli.ts +18 -6
  117. package/src/scripts/telemetry_disclosure_hook.ts +244 -0
  118. package/src/server/io/yamlIO.ts +167 -3
  119. package/src/shared/settingsConsent.ts +49 -2
  120. package/dist/cli-delegate/chunk-MMJL4TIX.js +0 -18
@@ -1,66 +1,66 @@
1
1
  {
2
- "$comment": "Violation ratchet for gates whose dead scan root was repaired. A repair REVEALS pre-existing debt; it does not create it. Each entry records the count revealed at repair time — the gate fails only when its count RISES. Lowering a count is a normal commit; raising one is a defect. Entries expire: a baseline that has not dropped within 56 days of `landed` (or of `reaffirmed.date`) fails its gate, because a number that never moves is suppression with extra steps. Mechanism + rationale: src/scripts/_lib/gate_baseline.ts. Decision: AI council 2026-08-02, roadmap road-to-gates-that-can-fail blocker `dead-gate-finding-triage`.",
3
- "gates": {
4
- "ci-parity:local-only": {
5
- "count": 166,
6
- "landed": "2026-08-13",
7
- "note": "Gates that `task ci` runs and no workflow does — the direction this manifest's own header calls the one that let real defects merge (16 stale index rows reached the trunk behind a local-only check-index). Revealed, not created, by repairing the checker's extraction: check_ci_local_parity built its CI-side set by regexing RAW workflow text for `task <name>` and expanding each closure, comments included. Several workflow comments contain the literal string `task ci` while stating that no workflow invokes it, so `ci` was read as an invocation and its full 247-gate closure entered the 'runs in CI' set. The count was therefore 0 BY CONSTRUCTION, and the prose documenting the gap was what suppressed it. Measured at the repair: CI-side 273 → 106, undeclared_local_only 0 → 167 of 247 local gates (68%). Baselined rather than hard-failed because 167 findings nobody can clear 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 gate registered in `task ci` with no workflow reds immediately. Lowering happens by wiring gates into workflows or declaring them under `local_only:` with a reason. Note the punchline, because it bounds the risk of this entry: check_ci_local_parity ITSELF runs in no workflow, so it is its own finding and this ratchet has no remote reach either — clearing the backlog is what gives it teeth. DRAINED BY ONE, 2026-08-13: check_ci_local_parity itself was wired into consistency.yml, so it left the set it exists to report. That is the whole shape of a legitimate drain here — the gate stops being local-only because a workflow now runs it, not because the declaration list grew. The floor in gate-coverage.yml moved in the same change and for the same reason: wiring two gates raised the parsed population 357 -> 360 (109 CI + 251 local). The intermediate 359 was the reading after the CI-side wiring alone and before check_gate_coverage joined the local `ci` chain; it was committed here by mistake and is corrected rather than left as a second number for the same measurement.",
8
- "falsifier": "./scripts-run src/scripts/check_ci_local_parity"
9
- },
10
- "check_no_new_legacy_path:hardcoded-scan-roots": {
11
- "count": 47,
12
- "landed": "2026-08-13",
13
- "note": "Lines under src/scripts/**/*.{ts,mts,mjs} that pass the retired `.agent-src.uncondensed` directory into `path.join(...)` — a filesystem path CONSTRUCTED against the tree ADR-051 abandoned, which is the exact shape of all 14 dead gates. Deliberately narrow so the number is signal, not noise: 236 raw mentions under src/scripts → 147 string-literal lines → 67 path-constructing lines when the pass was written, after exempting src/scripts/_lib/agent_src.ts (the shared resolver, whose job IS owning the constant — gates that route through it survived the move). Detectors holding the literal as data to match, strip, or print are not counted, and no prose, docs, tests or fixtures are read: that full-tree shape was measured at 44 files / 213 hits and rejected as the wrong lint (see the note in check_no_new_legacy_path.ts::find_offenders). Recorded at 58 rather than 67 because Phase 1's scan-root repairs were landing on this branch while the pass was being written and had already removed 9 — a baseline above the truth silently permits new violations. This count goes to ZERO as the remaining repairs land; every repair is a lowering commit. Lowered 56→51 by the road-to-renewal-foundation Phase 1 dead-root sweep (check_always_budget, check_portability, check_references, lint_framework_leakage, lint_agents_md). Lowered 51→49 by the gate-hardening adoption PR: skill_collision_clusters and skill_overlap were repointed at the live skills tree via the shared resolver — two gates that had been reading the retired container and scoring 0 of 288 skills. Lowered 49→48 by road-to-rule-delivery-integrity P2.2: score_skill_selection was repointed at the live skills tree via the shared resolver. It is the same shape as the two gates lowered before it — a scorer reading the retired container and scoring 0 of 288 skills — and here the consequence was sharper than a dead gate: the roadmap's pre-registered selection-accuracy criteria would have been compared against a baseline of silent zeros. Lowered 48→47 on 2026-08-13, and this one is a catch-up rather than a repair: the pass reported 47 against a baseline of 48, so a gain had already landed without the ratchet following it. Attributed to 455160160 (fix(evals): resolve the eval orchestrator against the live skills tree), the most recent repair in this branch's base that removes a path-constructing line — SKILLS_ROOT repointed through _lib/agent_src.ts::SRC_SKILLS(). A loose ratchet is not harmless: it silently permits one new violation to be added back without any gate noticing, which is the shape this entry exists to prevent."
14
- },
15
- "audit_user_type_axis": {
16
- "count": 1,
17
- "landed": "2026-08-02",
18
- "note": "One orphan user-type value: `legal`. Five legal-review-prep skills (contract-review, dpa-review, nda-triage, legal-intake-triage, legal-practice-profile) declare `recommended_for_user_types: [legal]`, but there is no `user-types/legal.yml` — the pack shipped an axis value with no user-type definition behind it, and the dead scan root hid it (the audit reported used=0 orphans=0 while reading zero skills; it now reads 288 and reports used=8). Clearing this entry is a product call, not a copy edit: either author `user-types/legal.yml` (tagline, recommended packs, install-path hint) or drop `legal` from those five skills. Not decided here because both options change shipped pack behaviour."
19
- },
20
- "lint_handoffs": {
21
- "count": 18,
22
- "landed": "2026-08-02",
23
- "note": "All 18 are `handoff_tier_mismatch`: a senior-tier skill links to a peer whose frontmatter carries no `tier` (reported as tier='unset') — e.g. build-buy-partner → adr-create, customer-research → po-discovery, gated-reach → threat-modeling. This is a tier-metadata backfill backlog on the LINKED-TO skills, not a broken link. The one genuinely dangling link revealed by the same repair (competitive-positioning → a non-existent ../analyze-reference-repo/SKILL.md; it is a command, not a skill) was FIXED in the repairing commit rather than baselined. Clearing this entry means backfilling `tier` on the linked-to skills, or deciding that `tier: unset` on a link target is not a violation and narrowing the rule."
24
- },
25
- "lint_roadmap_blockers:decidability": {
26
- "count": 26,
27
- "landed": "2026-08-16",
28
- "note": "Open blocker entries that are not yet DECIDABLE by the person who owns them — missing `Recommendation:` (which option and why), missing `If you do nothing:` (the cost of the non-decision), or a `What to do:` carrying no command, path or enumerated option set. Revealed by adding the decidability half to lint_roadmap_blockers, not created by it: the five original fields describe a situation and never made it actionable, so a blocker could pass the contract while handing the analysis back to the person least equipped to redo it. Measured at introduction: 36 open entries, ALL 36 undecidable, and 14 of the 46 total entries carry no command/path/option at all in `What to do`. Baselined rather than hard-failed because 36 findings nobody can clear in the change that reveals them is the gate-that-lands-as-N-instant-blockers failure this repository has refused before — the same reasoning as ci-parity:local-only above. First measured at 34 rather than 36 because the two blockers on the branch that introduced this (`ac3-false-positive-reading`, `publish-side-marker-invariant`) were rewritten to the new standard and then closed in the same change; a baseline above the truth silently permits new violations. Re-measured at 38 when the branch was re-pointed at current main, and the +4 is population growth rather than a loosening: main gained the four `road-to-inbox-harvest-2026-08-d-*` roadmaps (context-ledger, runtime-skill-routing, scheduled-deprecation, top-band-model-economy), each carrying one open blocker authored before this requirement existed. That is the honest number for the population this gate now scans — recording 34 against a 38-entry tree would have failed the gate on its own introducing commit and taught the next reader to distrust it. Lowering happens one blocker at a time, and every roadmap touched for other reasons is a chance to lower it. Lowered 38→37 by the scheduled-deprecation PR, which is exactly that shape: code-graph-removal-authorisation was rewritten to the new standard because the roadmap was being touched anyway. Its `If you do nothing:` was one of the few that was not the common no-cost answer — the release gate that PR ships would have refused the next major cut while the blocker stayed open. Recorded in the past tense because the maintainer then RESOLVED that blocker on the trunk by withdrawing the commitment, so nothing refuses a cut today. The lowering stands on the rewrite, not on the consequence, and the sentence is corrected rather than deleted because a baseline note arguing from a retracted state is how a ratchet's rationale rots. The 56-day expiry is the point, not a hazard: if this number has not moved in two months, the fields are not being written and the gate should fail until someone decides whether the requirement is real. Lowered 37→34 when the scheduled-deprecation branch merged current main: three blockers were resolved or rewritten on the trunk in the meantime, so the drop is a genuine drain plus a merge, not a loosening. Re-tightened here rather than left loose because a ratchet that lags its own tree permits violations back in silently. Lowered 34→29 on 2026-08-16 by rewriting six entries to the new standard (raw-capture-needs-host-env, telemetry-sample-size, repo-admin-and-usage, benchmark-spend, measurement-a-no-per-arm-builder-tier, measurement-b-no-renderable-lane-pair) in a PR whose only purpose was the drain — the first lowering not taken on the back of another change, which the note above says is the usual shape. Two findings from that pass are worth keeping. The tree measured 35 against a baseline of 34 BEFORE the drain, i.e. the ratchet was already red on main and nobody knew: this gate runs in `task ci`, no workflow invokes `task ci`, and the local run that would have shown it was read through a `tail`, which hid every row above the last six. And four of the six `If you do nothing:` answers are honestly that nothing degrades — that is the true answer for a held measurement, and writing it is what makes the entry decidable, so the field earns its place by being cheap to answer truthfully rather than by always carrying a cost. Lowered 29 → 27 on 2026-08-16 by the merge of this branch and the trunk drain above, and the arithmetic matters more than the delta: both sides rewrote entries from the same tree of 35 independently — the trunk six (recorded above), this branch two (deferred-finding-decision-reopen, spent-inbox-artifacts-await-deletion, rewritten because road-to-inbox-harvest-residuals was being touched anyway). The sets are disjoint, so the merged tree measures 27 and the gate itself reported the slack. Tightened rather than banked: 29 against a 27-entry tree would absorb the next two undecidable blockers without anything noticing, which is the lag this note has now corrected twice. Lowered 27 → 26 on 2026-08-16 by rewriting `picktier-wire-or-archive` (road-to-inbox-harvest-2026-08-d-top-band-model-economy), and this one is the usual shape the note describes rather than the dedicated-drain shape: the roadmap was being worked for its own Step 4.1 and the entry was rewritten because it was open on the desk. Worth recording for what writing the two fields SURFACED, which is the argument for the requirement: `Recommendation:` forced the question 'which option, and why' and the honest answer turned out not to be one of the three options at all — the entry defers to a converged council verdict, and the council cannot reach quorum (re-measured 0 of 2 the same day), so the actionable next step is a one-line fix to a user-global config file outside this repository. Five fields had described that situation accurately for two weeks without anyone noticing that the named blocker was infrastructure rather than judgement.",
29
- "falsifier": "./scripts-run src/scripts/lint_roadmap_blockers --quiet"
30
- },
31
- "lint_pack_boundaries": {
32
- "count": 337,
33
- "landed": "2026-08-02",
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
- },
36
- "check_gate_completeness": {
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
- "falsifier": "./scripts-run src/scripts/check_gate_completeness"
41
- },
42
- "check_depth_budget": {
43
- "count": 4,
44
- "landed": "2026-08-06",
45
- "note": "Four depth files exceed the 16,000-char per-file ceiling: rule-body-migration-inventory.md (23,782), design-antipatterns.md (20,707), roadmap-process-loop.md (20,360), orchestration-telemetry.md (16,155). Recorded rather than fixed because splitting four large reference documents is unrelated to the change that introduced the ceiling, and a gate that lands as four instant blockers is the shape this repository has already recorded as a failure. Shrink-only from here: a FIFTH over-ceiling file reds. roadmap-process-loop.md is the file that motivated the gate — it states 'Size budget: <= 4,000 chars' in its own header and is 5x over it, which is what a declared budget with nothing measuring it decays into. The ceiling is a GROWTH RATCHET, not a measured quality threshold: the 7,308-trajectory study behind the 3,500-token skill ceiling (ADR-217) was taken on procedural always-co-loaded skills, and this layer is declarative and loaded one or two files at a time. Replace the number when someone measures comprehension on THIS layer."
46
- },
47
- "gate-self-test:registered-non-adopters": {
48
- "count": 24,
49
- "landed": "2026-08-06",
50
- "note": "Self-test adoption ratchet, expressed as a shrink-only NON-adopter count. 24 of the 32 gates registered enforced in gate-coverage.yml carry neither a `--self-test` (`_lib/gate_self_test.ts`) nor a `// self-test-exempt: <reason>` marker, so nothing proves they DISCRIMINATE — an enforced `scanned:` floor proves a gate read something, only a self-test proves the reading changes the verdict. Deliberately NOT an adoption column ratcheted upward: check_gate_coverage.ts's own report_hardening_ratchet rejects that shape in this same file (\"grades the solution instead of the problem\"), and the sibling adoption ratchet (check_gate_completeness) is already a non-adopter count. Population is the manifest, not every script under src/scripts: padding the manifest to move this number would mean adding a floor per row. Shrink-only from here — a NEW registered gate must adopt or exempt. Migration mechanism, same as the ledger ratchet above: gates adopt as they are touched, not in a sweep — the eight current adopters were each added alongside a real change to that gate. The 56-day non-stagnation clause applies unchanged, so a count parked here fails on its own.",
51
- "falsifier": "./scripts-run src/scripts/check_gate_coverage"
52
- },
53
- "lint_settings_classes:derivable-surface": {
54
- "count": 83,
55
- "landed": "2026-08-12",
56
- "note": "Anti-regrowth ratchet for the settings disposition axis. 83 of 134 template leaves are classified `derivable` — the mechanism can decide from the situation, so the key should not exist — and each names the replacement that would decide instead (`lint_settings_classes` rejects the bare label, which is what keeps the class from becoming a synonym for \"inconvenient to keep\"). This is a DELETION QUEUE, not 83 deletions: a row stays until the mechanism it names actually exists, and deleting a key ahead of its mechanism is a silently-changed default rather than a simplification. Shrink-only from here, which is the anti-regrowth half — a NEW key classified `derivable` raises the number and reds, so a key whose answer the situation already carries cannot be added at all. The 56-day non-stagnation clause is the other half and is wanted here rather than tolerated: a queue that never drains is a stated direction nobody is walking. The queue opened at 88 of 140 on 2026-08-12 and drained to 83 of 134 the same day: six keys had no reader whatsoever (telegraph.speak_scope, chat_history.max_size_kb, chat_history.on_overflow, quality.wait_for_remote_ci, screenshots.data_bearing_gate, legal_review_prep.consented_at), five of them `derivable` and one a `consent` row reclassified because a published-egress confirmation is a Hard Floor no setting may lift. An unread key is the only deletion that cannot silently change a default, which is why that batch went first. The classification itself is docs/contracts/settings-classes.md.",
57
- "falsifier": "./scripts-run src/scripts/lint_settings_classes"
58
- },
59
- "check_source_size_budget": {
60
- "count": 19611,
61
- "landed": "2026-08-17",
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
- "falsifier": "./scripts-run src/scripts/check_source_size_budget --quiet"
2
+ "$comment": "Violation ratchet for gates whose dead scan root was repaired. A repair REVEALS pre-existing debt; it does not create it. Each entry records the count revealed at repair time — the gate fails only when its count RISES. Lowering a count is a normal commit; raising one is a defect. Entries expire: a baseline that has not dropped within 56 days of `landed` (or of `reaffirmed.date`) fails its gate, because a number that never moves is suppression with extra steps. Mechanism + rationale: src/scripts/_lib/gate_baseline.ts. Decision: AI council 2026-08-02, roadmap road-to-gates-that-can-fail blocker `dead-gate-finding-triage`.",
3
+ "gates": {
4
+ "ci-parity:local-only": {
5
+ "count": 166,
6
+ "landed": "2026-08-13",
7
+ "note": "Gates that `task ci` runs and no workflow does — the direction this manifest's own header calls the one that let real defects merge (16 stale index rows reached the trunk behind a local-only check-index). Revealed, not created, by repairing the checker's extraction: check_ci_local_parity built its CI-side set by regexing RAW workflow text for `task <name>` and expanding each closure, comments included. Several workflow comments contain the literal string `task ci` while stating that no workflow invokes it, so `ci` was read as an invocation and its full 247-gate closure entered the 'runs in CI' set. The count was therefore 0 BY CONSTRUCTION, and the prose documenting the gap was what suppressed it. Measured at the repair: CI-side 273 → 106, undeclared_local_only 0 → 167 of 247 local gates (68%). Baselined rather than hard-failed because 167 findings nobody can clear 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 gate registered in `task ci` with no workflow reds immediately. Lowering happens by wiring gates into workflows or declaring them under `local_only:` with a reason. Note the punchline, because it bounds the risk of this entry: check_ci_local_parity ITSELF runs in no workflow, so it is its own finding and this ratchet has no remote reach either — clearing the backlog is what gives it teeth. DRAINED BY ONE, 2026-08-13: check_ci_local_parity itself was wired into consistency.yml, so it left the set it exists to report. That is the whole shape of a legitimate drain here — the gate stops being local-only because a workflow now runs it, not because the declaration list grew. The floor in gate-coverage.yml moved in the same change and for the same reason: wiring two gates raised the parsed population 357 -> 360 (109 CI + 251 local). The intermediate 359 was the reading after the CI-side wiring alone and before check_gate_coverage joined the local `ci` chain; it was committed here by mistake and is corrected rather than left as a second number for the same measurement.",
8
+ "falsifier": "./scripts-run src/scripts/check_ci_local_parity"
9
+ },
10
+ "check_no_new_legacy_path:hardcoded-scan-roots": {
11
+ "count": 47,
12
+ "landed": "2026-08-13",
13
+ "note": "Lines under src/scripts/**/*.{ts,mts,mjs} that pass the retired `.agent-src.uncondensed` directory into `path.join(...)` — a filesystem path CONSTRUCTED against the tree ADR-051 abandoned, which is the exact shape of all 14 dead gates. Deliberately narrow so the number is signal, not noise: 236 raw mentions under src/scripts → 147 string-literal lines → 67 path-constructing lines when the pass was written, after exempting src/scripts/_lib/agent_src.ts (the shared resolver, whose job IS owning the constant — gates that route through it survived the move). Detectors holding the literal as data to match, strip, or print are not counted, and no prose, docs, tests or fixtures are read: that full-tree shape was measured at 44 files / 213 hits and rejected as the wrong lint (see the note in check_no_new_legacy_path.ts::find_offenders). Recorded at 58 rather than 67 because Phase 1's scan-root repairs were landing on this branch while the pass was being written and had already removed 9 — a baseline above the truth silently permits new violations. This count goes to ZERO as the remaining repairs land; every repair is a lowering commit. Lowered 56→51 by the road-to-renewal-foundation Phase 1 dead-root sweep (check_always_budget, check_portability, check_references, lint_framework_leakage, lint_agents_md). Lowered 51→49 by the gate-hardening adoption PR: skill_collision_clusters and skill_overlap were repointed at the live skills tree via the shared resolver — two gates that had been reading the retired container and scoring 0 of 288 skills. Lowered 49→48 by road-to-rule-delivery-integrity P2.2: score_skill_selection was repointed at the live skills tree via the shared resolver. It is the same shape as the two gates lowered before it — a scorer reading the retired container and scoring 0 of 288 skills — and here the consequence was sharper than a dead gate: the roadmap's pre-registered selection-accuracy criteria would have been compared against a baseline of silent zeros. Lowered 48→47 on 2026-08-13, and this one is a catch-up rather than a repair: the pass reported 47 against a baseline of 48, so a gain had already landed without the ratchet following it. Attributed to 455160160 (fix(evals): resolve the eval orchestrator against the live skills tree), the most recent repair in this branch's base that removes a path-constructing line — SKILLS_ROOT repointed through _lib/agent_src.ts::SRC_SKILLS(). A loose ratchet is not harmless: it silently permits one new violation to be added back without any gate noticing, which is the shape this entry exists to prevent."
14
+ },
15
+ "audit_user_type_axis": {
16
+ "count": 1,
17
+ "landed": "2026-08-02",
18
+ "note": "One orphan user-type value: `legal`. Five legal-review-prep skills (contract-review, dpa-review, nda-triage, legal-intake-triage, legal-practice-profile) declare `recommended_for_user_types: [legal]`, but there is no `user-types/legal.yml` — the pack shipped an axis value with no user-type definition behind it, and the dead scan root hid it (the audit reported used=0 orphans=0 while reading zero skills; it now reads 288 and reports used=8). Clearing this entry is a product call, not a copy edit: either author `user-types/legal.yml` (tagline, recommended packs, install-path hint) or drop `legal` from those five skills. Not decided here because both options change shipped pack behaviour."
19
+ },
20
+ "lint_handoffs": {
21
+ "count": 18,
22
+ "landed": "2026-08-02",
23
+ "note": "All 18 are `handoff_tier_mismatch`: a senior-tier skill links to a peer whose frontmatter carries no `tier` (reported as tier='unset') — e.g. build-buy-partner → adr-create, customer-research → po-discovery, gated-reach → threat-modeling. This is a tier-metadata backfill backlog on the LINKED-TO skills, not a broken link. The one genuinely dangling link revealed by the same repair (competitive-positioning → a non-existent ../analyze-reference-repo/SKILL.md; it is a command, not a skill) was FIXED in the repairing commit rather than baselined. Clearing this entry means backfilling `tier` on the linked-to skills, or deciding that `tier: unset` on a link target is not a violation and narrowing the rule."
24
+ },
25
+ "lint_roadmap_blockers:decidability": {
26
+ "count": 20,
27
+ "landed": "2026-08-19",
28
+ "note": "Open blocker entries that are not yet DECIDABLE by the person who owns them — missing `Recommendation:` (which option and why), missing `If you do nothing:` (the cost of the non-decision), or a `What to do:` carrying no command, path or enumerated option set. Revealed by adding the decidability half to lint_roadmap_blockers, not created by it: the five original fields describe a situation and never made it actionable, so a blocker could pass the contract while handing the analysis back to the person least equipped to redo it. Measured at introduction: 36 open entries, ALL 36 undecidable, and 14 of the 46 total entries carry no command/path/option at all in `What to do`. Baselined rather than hard-failed because 36 findings nobody can clear in the change that reveals them is the gate-that-lands-as-N-instant-blockers failure this repository has refused before — the same reasoning as ci-parity:local-only above. First measured at 34 rather than 36 because the two blockers on the branch that introduced this (`ac3-false-positive-reading`, `publish-side-marker-invariant`) were rewritten to the new standard and then closed in the same change; a baseline above the truth silently permits new violations. Re-measured at 38 when the branch was re-pointed at current main, and the +4 is population growth rather than a loosening: main gained the four `road-to-inbox-harvest-2026-08-d-*` roadmaps (context-ledger, runtime-skill-routing, scheduled-deprecation, top-band-model-economy), each carrying one open blocker authored before this requirement existed. That is the honest number for the population this gate now scans — recording 34 against a 38-entry tree would have failed the gate on its own introducing commit and taught the next reader to distrust it. Lowering happens one blocker at a time, and every roadmap touched for other reasons is a chance to lower it. Lowered 38→37 by the scheduled-deprecation PR, which is exactly that shape: code-graph-removal-authorisation was rewritten to the new standard because the roadmap was being touched anyway. Its `If you do nothing:` was one of the few that was not the common no-cost answer — the release gate that PR ships would have refused the next major cut while the blocker stayed open. Recorded in the past tense because the maintainer then RESOLVED that blocker on the trunk by withdrawing the commitment, so nothing refuses a cut today. The lowering stands on the rewrite, not on the consequence, and the sentence is corrected rather than deleted because a baseline note arguing from a retracted state is how a ratchet's rationale rots. The 56-day expiry is the point, not a hazard: if this number has not moved in two months, the fields are not being written and the gate should fail until someone decides whether the requirement is real. Lowered 37→34 when the scheduled-deprecation branch merged current main: three blockers were resolved or rewritten on the trunk in the meantime, so the drop is a genuine drain plus a merge, not a loosening. Re-tightened here rather than left loose because a ratchet that lags its own tree permits violations back in silently. Lowered 34→29 on 2026-08-16 by rewriting six entries to the new standard (raw-capture-needs-host-env, telemetry-sample-size, repo-admin-and-usage, benchmark-spend, measurement-a-no-per-arm-builder-tier, measurement-b-no-renderable-lane-pair) in a PR whose only purpose was the drain — the first lowering not taken on the back of another change, which the note above says is the usual shape. Two findings from that pass are worth keeping. The tree measured 35 against a baseline of 34 BEFORE the drain, i.e. the ratchet was already red on main and nobody knew: this gate runs in `task ci`, no workflow invokes `task ci`, and the local run that would have shown it was read through a `tail`, which hid every row above the last six. And four of the six `If you do nothing:` answers are honestly that nothing degrades — that is the true answer for a held measurement, and writing it is what makes the entry decidable, so the field earns its place by being cheap to answer truthfully rather than by always carrying a cost. Lowered 29 → 27 on 2026-08-16 by the merge of this branch and the trunk drain above, and the arithmetic matters more than the delta: both sides rewrote entries from the same tree of 35 independently — the trunk six (recorded above), this branch two (deferred-finding-decision-reopen, spent-inbox-artifacts-await-deletion, rewritten because road-to-inbox-harvest-residuals was being touched anyway). The sets are disjoint, so the merged tree measures 27 and the gate itself reported the slack. Tightened rather than banked: 29 against a 27-entry tree would absorb the next two undecidable blockers without anything noticing, which is the lag this note has now corrected twice. Lowered 27 → 26 on 2026-08-16 by rewriting `picktier-wire-or-archive` (road-to-inbox-harvest-2026-08-d-top-band-model-economy), and this one is the usual shape the note describes rather than the dedicated-drain shape: the roadmap was being worked for its own Step 4.1 and the entry was rewritten because it was open on the desk. Worth recording for what writing the two fields SURFACED, which is the argument for the requirement: `Recommendation:` forced the question 'which option, and why' and the honest answer turned out not to be one of the three options at all — the entry defers to a converged council verdict, and the council cannot reach quorum (re-measured 0 of 2 the same day), so the actionable next step is a one-line fix to a user-global config file outside this repository. Five fields had described that situation accurately for two weeks without anyone noticing that the named blocker was infrastructure rather than judgement. Lowered 26 → 22 on 2026-08-18 by road-to-estate-drawdown Step 0.1, which needed the two fields for a different reason and therefore drained six entries as a by-product: the consolidated decision sheet renders a DEFAULT plus that default's provenance per user-owned decision, and 7 of the 21 live entries recorded no `Recommendation:` to derive one from. Six were rewritten to the new standard (skill-activation-window, b-convergence-machine, maintainer-blind-ratings, real-orchestration-usage, human-gated-live-trigger-eval, manual-rubric-rater), each marked `agent-drafted` in the field itself so the sheet can label the provenance rather than presenting a drafted default beside a maintainer-recorded one as the same kind of statement. The seventh, `road-to-gated-reach-followup`'s legacy `> Blocked until …` note, is NOT counted here and was not rewritten: it is a blocked-until note rather than a `### blocker:` entry, so it has no field to carry, and converting it is a roadmap restructure this change did not take. **The tree was RED at 28 against this baseline of 26 before the drain, and again nobody knew** — the same failure the 34→29 paragraph above records, one gate cycle later: two entries had been added on the trunk without the fields, and this gate runs in `task ci`, which no workflow invokes. That is the second recorded instance of this ratchet being red on main undetected, which is an argument about where it runs rather than about its number. `human-gated-live-trigger-eval` also gained a command in `What to do:` (`rule_trigger_eval`, verified to exist at src/scripts/) because the field carried a path-less instruction; the other five needed only the two decidability fields. Lowered 22 -> 20 on 2026-08-19 by road-to-estate-drawdown Phase 2 triage batch 1, and RE-MEASURED after two merges of origin/main rather than carried over: the trunk gained roadmaps in between (the long-horizon pair and the council-fallback set), and the live count over the merged tree is still 20 across 39 roadmaps. Six roadmaps moved to later/, and two of them carried an undecidable open blocker each. The gain is a side effect of parking rather than of drafting a recommendation, so it is recorded as such — the two entries are unchanged and still undecidable where they sit; they are simply outside the active tree this ratchet scans. Walked down because the gate reported itself loose, and a baseline above the measurement is headroom a later change can spend with nothing objecting.",
29
+ "falsifier": "./scripts-run src/scripts/lint_roadmap_blockers --quiet"
30
+ },
31
+ "lint_pack_boundaries": {
32
+ "count": 337,
33
+ "landed": "2026-08-02",
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
+ },
36
+ "check_gate_completeness": {
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
+ "falsifier": "./scripts-run src/scripts/check_gate_completeness"
41
+ },
42
+ "check_depth_budget": {
43
+ "count": 4,
44
+ "landed": "2026-08-06",
45
+ "note": "Four depth files exceed the 16,000-char per-file ceiling: rule-body-migration-inventory.md (23,782), design-antipatterns.md (20,707), roadmap-process-loop.md (20,360), orchestration-telemetry.md (16,155). Recorded rather than fixed because splitting four large reference documents is unrelated to the change that introduced the ceiling, and a gate that lands as four instant blockers is the shape this repository has already recorded as a failure. Shrink-only from here: a FIFTH over-ceiling file reds. roadmap-process-loop.md is the file that motivated the gate — it states 'Size budget: <= 4,000 chars' in its own header and is 5x over it, which is what a declared budget with nothing measuring it decays into. The ceiling is a GROWTH RATCHET, not a measured quality threshold: the 7,308-trajectory study behind the 3,500-token skill ceiling (ADR-217) was taken on procedural always-co-loaded skills, and this layer is declarative and loaded one or two files at a time. Replace the number when someone measures comprehension on THIS layer."
46
+ },
47
+ "gate-self-test:registered-non-adopters": {
48
+ "count": 24,
49
+ "landed": "2026-08-06",
50
+ "note": "Self-test adoption ratchet, expressed as a shrink-only NON-adopter count. 24 of the 32 gates registered enforced in gate-coverage.yml carry neither a `--self-test` (`_lib/gate_self_test.ts`) nor a `// self-test-exempt: <reason>` marker, so nothing proves they DISCRIMINATE — an enforced `scanned:` floor proves a gate read something, only a self-test proves the reading changes the verdict. Deliberately NOT an adoption column ratcheted upward: check_gate_coverage.ts's own report_hardening_ratchet rejects that shape in this same file (\"grades the solution instead of the problem\"), and the sibling adoption ratchet (check_gate_completeness) is already a non-adopter count. Population is the manifest, not every script under src/scripts: padding the manifest to move this number would mean adding a floor per row. Shrink-only from here — a NEW registered gate must adopt or exempt. Migration mechanism, same as the ledger ratchet above: gates adopt as they are touched, not in a sweep — the eight current adopters were each added alongside a real change to that gate. The 56-day non-stagnation clause applies unchanged, so a count parked here fails on its own.",
51
+ "falsifier": "./scripts-run src/scripts/check_gate_coverage"
52
+ },
53
+ "lint_settings_classes:derivable-surface": {
54
+ "count": 83,
55
+ "landed": "2026-08-12",
56
+ "note": "Anti-regrowth ratchet for the settings disposition axis. 83 of 134 template leaves are classified `derivable` — the mechanism can decide from the situation, so the key should not exist — and each names the replacement that would decide instead (`lint_settings_classes` rejects the bare label, which is what keeps the class from becoming a synonym for \"inconvenient to keep\"). This is a DELETION QUEUE, not 83 deletions: a row stays until the mechanism it names actually exists, and deleting a key ahead of its mechanism is a silently-changed default rather than a simplification. Shrink-only from here, which is the anti-regrowth half — a NEW key classified `derivable` raises the number and reds, so a key whose answer the situation already carries cannot be added at all. The 56-day non-stagnation clause is the other half and is wanted here rather than tolerated: a queue that never drains is a stated direction nobody is walking. The queue opened at 88 of 140 on 2026-08-12 and drained to 83 of 134 the same day: six keys had no reader whatsoever (telegraph.speak_scope, chat_history.max_size_kb, chat_history.on_overflow, quality.wait_for_remote_ci, screenshots.data_bearing_gate, legal_review_prep.consented_at), five of them `derivable` and one a `consent` row reclassified because a published-egress confirmation is a Hard Floor no setting may lift. An unread key is the only deletion that cannot silently change a default, which is why that batch went first. The classification itself is docs/contracts/settings-classes.md.",
57
+ "falsifier": "./scripts-run src/scripts/lint_settings_classes"
58
+ },
59
+ "check_source_size_budget": {
60
+ "count": 19571,
61
+ "landed": "2026-08-19",
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. Lowered 19,611 -> 19,604 on 2026-08-19 by the long-horizon branch, and it is again a LOWERING through extraction rather than a raise. Worth recording because the branch is the first case the gate met that it had no answer for: two roadmaps of new behaviour whose footprint landed in three files that were ALREADY 3,962 lines over the ceiling before the work started, so the initial reading was +759 new. The council (2/2 seats, 2026-08-19) split between extracting and escalating and agreed on two negatives -- raising the baseline contradicts the gate's own words, and a preparatory refactor PR creates no headroom because lowering the baseline by N and then adding N back trips the gate at the same number. What closed it was seven extractions, each a cohesive unit moved UNCHANGED: mid_flight_fallback, fallback_config, response_render, py_parity, _lib/council_fallback_posture, _lib/council_fallback_wiring, _lib/council_settings_block (+ _lib/map_to_object, which had to move so the settings block could stop importing back into council_cli). Four duplicated call sites collapsed on the way -- the split/ask branch appeared three times and the projected-spend comparison twice, which is how a metered retry could have come to gate on different rules than the round it ran inside. The lesson the previous entry states holds and is now tested at scale: extract, then measure, and pin ONCE at the end -- an intermediate de-duplication briefly RAISED the total by 5, because in an over-ceiling file a shared helper's own docblock can cost more than the duplication it removes. Lowered 19,604 -> 19,571 on 2026-08-19, and this entry records a RED GATE before it records a lowering. The pin at 19,604 was taken at the end of the long-horizon extraction pass; the R2 completion review of that very branch then produced fixes that grew orchestrator.ts by 53 lines, and the gate went red at 19,656 -- 52 new, against a note in this file that claimed a lowering the tree no longer showed. The review caught it, not the author, which is the argument for reviewing a fix pass rather than only the work it fixes. Repaired the way this file's own text demands: by extracting, never by re-pinning. The spend gate came out whole into ai_council/spend_gate.ts -- CostBudget (the ceilings), _breach (the decision), OverrunEvent (what a crossing hands the operator) and the _total_usd arithmetic all three share, re-exported so no caller changed. Cohesive on its own terms: nothing in it calls a provider or knows what a round is. The refusal path of the unmetered escalation moved to mid_flight_fallback.refuseUnmeteredEscalation in the same pass, next to the runGatedRetry it mirrors. Second lesson, and it is the reason the first attempt only bought 5 lines: moving code INTO another over-ceiling file buys nothing, so check which files are actually over the line before choosing a destination -- the same shape as the note below where de-duplication briefly RAISED the total by 5.",
63
+ "falsifier": "./scripts-run src/scripts/check_source_size_budget --quiet"
64
+ }
64
65
  }
65
- }
66
66
  }
@@ -102,6 +102,11 @@
102
102
  .agent-engagement.jsonl
103
103
  .agent-engagement.jsonl.bak
104
104
 
105
+ # Agent config — org usage telemetry (Class-A records; default-off and
106
+ # inactive without an org pack, never commit)
107
+ .agent-telemetry.jsonl
108
+ .agent-telemetry.jsonl.bak
109
+
105
110
  # Agent config — volatile runtime artefacts (machine-generated,
106
111
  # regenerated on demand, never committed). Single catch-all per the
107
112
  # Volatile Runtime policy: everything under agents/runtime/ is
@@ -9,9 +9,48 @@
9
9
  "floor": "1 vCPU reference container (documented ceiling, not CI-enforced)"
10
10
  },
11
11
  "budgets_ms": {
12
- "pre_tool_use": { "p95_ci": 150, "p95_1vcpu": 400 },
12
+ "pre_tool_use": { "p95_ci": 175, "p95_1vcpu": 400 },
13
13
  "any_hook_event": { "p95_ci": 250 }
14
14
  },
15
+ "gate_remeasure": {
16
+ "attempts": 1,
17
+ "_comment": "OFF (attempts 1 = measure once, no re-measure). The mechanism exists in bench_hook_latency.ts: under --gate a slot over its cap is re-measured and the MINIMUM p95 kept, because a load spike inside one 50-run sample moves p95 up and never down. It ships DISABLED because pre_tool_use_cap_derivation.why_not_a_retry_or_best_of_n rejected it as the first move on measured grounds — the spread is between runner CLASSES (p50 111-148 ms), not noise within one machine, so a slow runner measures slow twice. Two CI runs of the mechanism agree: it recovered 151->150 and 160->153, i.e. 1-7 ms against a 42 ms spread. Set attempts to 3 ONLY as the escalation that same block names, if the re-derived 175 cap starts flapping."
18
+ },
19
+ "normalized": {
20
+ "observe_only": true,
21
+ "p95_excess_ci": null,
22
+ "control": "node -e 0, measured through the identical spawnSync harness in the same run",
23
+ "_comment": "Observe-only measurement row, printed on every bench run and gating nothing. Every slot reading is spawn + interpreter start + bundle load + concern work, and only the last term is ours; the control isolates the process-spawn term so excess_ms = slot - control is closer to this tree's own cost than the raw reading. It exists to collect the data a future normalized gate would need, and to sharpen pre_tool_use_cap_derivation.revisit_if — which currently reads runner speed off the p50, a cheaper signal that needs no extra measurement. Arming this row would be a config change (p95_excess_ci + observe_only false), and the limitation below says why that is not justified today.",
24
+ "_measured_limitation_2026_08_19": "THE CONTROL UNDER-CORRECTS, measured, and this is recorded so nobody arms the row believing a property that was not shown. Same commit, two environments: darwin read slot 101 / control 45 / excess 56 ms; GitHub ubuntu read slot 151 / control 26 / excess 124 ms. The control went DOWN while the slot went UP, because node -e 0 covers interpreter startup only while a dispatch also loads a ~1 MB bundle and runs the concern chain — the dominant and varying term. So excess_ms is NOT portable across hardware classes. Whether it is nonetheless steadier than the absolute WITHIN one runner class is UNMEASURED: two CI runs read control 26/28 ms and excess 124/125 ms against raw first samples of 151/160, which is suggestive but n=2 and computed on post-re-measure values. Collect from the nightly before setting a bar, and keep the row observe-only if it does not hold rather than arming a second flapping gate."
25
+ },
26
+ "pre_tool_use_cap_derivation": {
27
+ "_comment": "Why p95_ci is 175 and not the 150 registered on 2026-07-27. 150 was NOT bent around a red check — it was measured to sit INSIDE the legitimate distribution, which concern-activation-policy.md § Deriving a threshold names as a false-positive generator by construction: `a threshold inside the legitimate distribution`. The gate was flapping on IDENTICAL code, and the flap was already diagnosed in this tree by road-to-per-turn-hook-economy step 0.5 — it names three main runs at 151, 152 and 117 ms and concludes cross-runner variance after step 0.4 excluded the CLI wrapper (2 ms). What was missing was never the diagnosis; it was moving the cap.",
28
+ "re_derived_at": "2026-08-19",
29
+ "rule_cited": "concern-activation-policy.md § Deriving a threshold — 'the observed legitimate distribution's 99th percentile, plus one'. The load-bearing half is 'outside the distribution'; a threshold inside it is a false-positive generator by construction.",
30
+ "rule_applied": "max + ~11%, i.e. 175 — NOT p99+1, and the substitution is deliberate rather than a slip. Over this sample p99+1 is 158 ms, which sits ONE millisecond above the observed max: that satisfies the rule's letter while leaving no room for a tail measured on 10 of 33 runs. The '+1' in the cited rule is calibrated to a shadow window that emits `would_fire` at several candidate thresholds and therefore knows its own distribution densely; this sample is 10 single points from CI logs and does not. Where the rule's precision assumption does not hold, the honest move is more headroom and a stated departure, not a number that borrows an authority it does not have.",
31
+ "measured_failure_rate": "5 of 33 tests.yml runs on main (15%) failed this step, 2026-08-16..18. Several of those runs carry a failure followed by a success for the SAME step, i.e. the standing workaround was re-running the job by hand.",
32
+ "observed_p95_ms_gated_cli_leg": {
33
+ "_comment": "One value per run, pre_tool_use, the `via cli` leg the gate actually compares. Every one is legitimate code — the reds differ from the greens only in which runner drew the job. SELECTION RULE, stated because it bounds the derivation: these 10 are the runs whose Static Checks log was read, not a sample designed for coverage — 6 greens and 4 reds picked from the head of the 33-run window, plus this branch's own PR run. The 5th red main run's value was never captured, so the true max over all 33 runs is UNRECORDED and may exceed 157 ms. That is the main reason the applied formula carries more headroom than p99+1.",
34
+ "green_main_runs": [115, 120, 146, 150, 150],
35
+ "red_main_runs_on_the_old_cap": [151, 152, 152, 154],
36
+ "red_main_runs_not_sampled": 1,
37
+ "pr_run_comment_only_diff": [157],
38
+ "spread": "115-157 ms, i.e. 42 ms of run-to-run variance on unchanged code — 24% of the new 175 ms cap and 28% of the old 150 ms one"
39
+ },
40
+ "why_not_a_retry_or_best_of_n": "A second bench on the same job would not help: the spread is between runner classes (p50 111-148 ms), not transient noise within one machine, so a slow runner measures slow twice. Retry logic is the escalation if 175 flaps, not the first move.",
41
+ "revisit_if": "The p50 — not the p95 — of a green run rises above 160 ms. Read the MEDIAN because that is the runner-speed indicator: the observed p50 range is 111-148 ms, so a p50 past 160 is outside every machine this window saw and is the cost itself growing. It then belongs to the owning roadmap (road-to-per-turn-hook-economy, D-2: the dispatcher's own payload read+parse, once per event) rather than to a third cap raise. Raising this number again without a measured distribution beside it is the config-bending this block exists to make visible.",
42
+ "_revisit_if_correction": "First written as 'a green run measures p95 above 160 ms', which round 2 finding 9 refuted from inside this same block: 160 sits 15 ms BELOW the new cap and 3 ms above the observed max, so the very first green 161 ms run would pass the gate and simultaneously trip a trigger declaring the variance to be over. A trigger has to live outside the spread its own cap absorbs, and on a p95 with 42 ms of runner spread no such point exists below 175. The p50 has a spread of 37 ms and a ceiling of 148, so it does."
43
+ },
44
+ "per_turn_composite": {
45
+ "_comment": "D-1 of road-to-per-turn-hook-economy: the slot budgets above gate per EVENT, and a single tool call fires both pre_tool_use and post_tool_use with their full concern chains. A ten-tool-call turn therefore pays twenty dispatcher cold starts plus the prompt and stop slots while every individual slot stays green — the number the user experiences has no row. This row is that number. Registered by step 4.1; the CEILING is step 4.2 and is the maintainer's, so the row ships observe-only and CANNOT fail a build. Definition before bar, deliberately: two people computing two different composites would both call the row green.",
46
+ "definition": "(pre_tool_use + post_tool_use) * tool_calls + user_prompt_submit + stop",
47
+ "aggregation": "p50",
48
+ "_aggregation_note": "Documentation of the code, not an input to it: the bench reads tool_calls from this file but hardcodes the p50 aggregation and re-implements the formula, so editing `aggregation` or `definition` changes nothing. Kept because a row whose meaning lives only in a TS file is unreadable here; corrected from silence to a stated non-binding after the R2 review flagged both keys as unread.",
49
+ "tool_calls": 10,
50
+ "observe_only": true,
51
+ "p50_ci": null,
52
+ "_observe_only_note": "While observe_only is true the bench PRINTS the composite on every run, including the CI gate run, and never fails on it. Setting p50_ci to a number and observe_only to false is the whole of step 4.2 — no code change is needed to arm it. Do not arm it from a single run: the composite sums four slot measurements and multiplies two of them by tool_calls, so it accumulates runner variance faster than any single slot (risk 5), which is why the same absolute-cap-plus-pathology-net posture the slots use applies here too."
53
+ },
15
54
  "regression_gate": {
16
55
  "max_regression_pct": 200,
17
56
  "_comment": "Pathology net only — the ABSOLUTE budgets above are the binding gate for wall-clock. The original 20% window was falsified by measured GitHub-runner variance: two CI runs of IDENTICAL code measured p95 77-89 ms and 92-103 ms (+23% run-to-run, 2026-07-27, runs 30303438480 / round-2 tests.yml). Same rationale as the regression_pct 200 override on the evaluator timing budgets: a tight creep rule on shared-runner wall-clock flaps; sizes/counts keep tight windows, timings get absolute caps."
@@ -42,6 +42,22 @@
42
42
  "pre_compact": 2048,
43
43
  "agent_error": 1024
44
44
  },
45
+ "per_turn_aggregate_bytes": {
46
+ "_comment": "road-to-standing-context-40k Phase 4 step 4.1 — the sum the USER feels, which no row above could see. The per-slot caps bound ONE fire; a turn fires user_prompt_submit once, pre_tool_use and post_tool_use once per tool call, and stop once, so the number a turn actually injects is a multiple of them and was unregistered. This row is the injection twin of `per_turn_composite` in hook-latency-budget.json: same definition shape, same tool_calls, different unit. bench_hook_injection derives and prints it on every run; the DISPATCHER enforces `ceiling_bytes` at runtime by dropping lowest-severity advisories first (src/scripts/hooks/injection_budget.ts), exempting severity:blocking and fail_closed concerns by construction so the policy can never hide a safety-relevant warning.",
47
+ "owner": "maintainer",
48
+ "review_by": "2026-11-10",
49
+ "definition": "(pre_tool_use + post_tool_use) x tool_calls + user_prompt_submit + stop",
50
+ "tool_calls": 10,
51
+ "tool_calls_rationale": "Taken verbatim from hook-latency-budget.json's per_turn_composite rather than re-derived. Two definitions of 'a representative turn' in one repo is how two people compute two different composites and both call their row green.",
52
+ "slots_counted": ["user_prompt_submit", "pre_tool_use", "post_tool_use", "stop"],
53
+ "excluded_slots": ["session_start", "session_end", "pre_compact", "agent_error"],
54
+ "excluded_slots_rationale": "session_start is excluded BY THE STEP: it is the one-shot restore slot that legitimately carries hot-context, handoff-context and the canary contract, and capping it against a per-turn number would evict exactly the payloads it exists to deliver. The other three are not per-turn events at all.",
55
+ "ceiling_bytes": 47104,
56
+ "ceiling_derivation": "NOT a fresh number, and deliberately so: it is the arithmetic consequence of the per_slot_sum_caps_bytes rows already registered above — 4096 + (2048 + 2048) x 10 + 2048. A tighter per-turn ceiling would contradict per-slot rows that are already the committed decision, and a looser one would let the composition exceed what every part was capped at. Tightening it is a decision about those rows, taken there, in a PR.",
57
+ "gate_on_ceiling": false,
58
+ "gate_on_ceiling_rationale": "The bench REPORTS the aggregate but does not red the build on it yet — the first-reading discipline the latency twin's observe_only flag encodes. Measured 2026-08-19 under the committed fixtures: ~1,142 B against a 47,104 B ceiling, i.e. the row is registered roughly forty times above any reading that exists. The reading MOVES between runs — 922, 1,140 and 1,142 B observed within one hour — and the mechanism is worth naming precisely rather than as 'flaky': the stop-slot end-review-nudge sizes its line from the branch's own mutation volume, and replay skips its writes but not its reads, so the number grows as the working branch grows. That is a pre-existing bench non-determinism this row surfaces rather than introduces, and it is the strongest single reason not to arm the ceiling against it. Arming a ceiling nobody has ever approached would gate on an untested inequality. Runtime enforcement is NOT gated by this flag and is always on: this flag governs CI, the ceiling governs the live session.",
59
+ "honest_null_consequence": "If the census shows real turns sitting near the ceiling, the per-slot rows move first and this row follows from them. If real turns never approach it, the row is a bound that costs nothing and the finding is published at review date either way."
60
+ },
45
61
  "injection_load_metric": {
46
62
  "definition": "Roadmap 3.4: total injected bytes per session and per turn, hook-carried. The census mode of bench_hook_injection (--record) appends counts-only lines to agents/runtime/state/injection-census.jsonl; the caps above are the thresholds; breach trend at review date decides tighten vs relax — by evidence, in a PR. The census is also the repeat-injection-census blocker's instrument (one instrumented week ranks the repeaters for the 3.3 idempotency gate).",
47
63
  "privacy": "census lines carry {ts, slot, concern, bytes} — counts and ids only, never content"
@@ -56,6 +72,12 @@
56
72
  "instrument": "rule-trips.json warn counters for the skill-route concern (fires) + conformance audits for whether the pointed-at skill was opened (adoption); HONEST GAP: no deterministic counter exists for 'the agent acted on the pointer' - the numerator is audit-carried and registered as such rather than implied to be automatic",
57
73
  "threshold": "none committed before data - baseline first, threshold at review date (2026-11-10)"
58
74
  },
75
+ "skill_route_bare_suppression_rate": {
76
+ "definition": "road-to-catalogue-host-fit Phase 3 step 3.2: of the prompts where skill-route FIRES, the share whose warn reason carries `N suppressed as host-bare` - i.e. how often a skill the ranker wanted to POINT AT is one the current host is KNOWN to have delivered without its description. `N` counts the pointer window (TOP_K of the unfiltered ranking), never the whole ranked list: `rank` returns every non-zero-scoring skill, so counting the tail would let a bare name at rank 40 - never pointable, never a loss - bump the count on nearly every fire (R2 finding 1). Registered beside skill_route_pointer_rate because it measures the same concern's behaviour under the same owner and review date. NO THRESHOLD IS COMMITTED, and the refusal is inherited from the sibling rather than restated: the FILTER is derived from a measurement (all 16 bare entries in the 2026-08-12 claude observation are in the ranker's catalogue, `capture_skill_catalogue --pointable-bare`), the OUTCOME - whether suppressing them changes what the agent does - is not.",
77
+ "instrument": "the dispatcher's warn reason string for the skill-route concern (`, N suppressed as host-bare`, appended only when non-zero) + agents/evidence/metrics/skill-catalogue.jsonl for the population; TWO HONEST GAPS, both registered as gaps rather than implied to be automatic. (1) rule-trips.json counts fires per concern and does NOT retain the reason, so the numerator is log-carried, not counter-carried - the same posture the sibling metric takes for its own audit-carried numerator. (2) TOTAL suppression is INVISIBLE here by construction (R2 finding 4): when the filter empties the pointer set the concern returns before writing stdout, so no reason line exists for the one case the roadmap calls load-bearing. `RouteDecision.suppressed` carries it at the API boundary and nothing reads that in production.",
78
+ "threshold": "none committed before data - baseline first, threshold at the shared review date (2026-11-10)",
79
+ "falsifiers": "For the FILTER half only, never for the outcome. UPPER: a live rate far above the share of the pointer window the host degrades means the ranker preferentially ranks exactly what the host drops - a finding about the projection, which belongs to Phase 2. Name the populations rather than a bare ratio, because three different denominators are in play and an earlier draft of this row conflated them (R2 finding 11): the 2026-08-12 claude observation carries `entries_total` 336, of which only 35 were enumerated at all (16 bare + 19 described), against a ranker catalogue of 338 entries at probe time. The comparable share is 16 of the 35 ENUMERATED entries; 16/336 and 16/338 describe different things and neither is this metric's denominator. LOWER: a rate of 0 does NOT establish that the ranker and the host's bare set fail to intersect - gap (2) above makes complete suppression indistinguishable from no suppression in this instrument. `capture_skill_catalogue --pointable-bare` is the instrument that separates them, and it must be read before any zero here is interpreted."
80
+ },
59
81
  "full_rewrite_small_diff_rate": {
60
82
  "definition": "edit-shape fires / Write calls per session (the full-rewrite-with-small-diff rate). Kill criterion per the shipped nudge standard: a nudge whose verdicts are measurably ignored (adoption near zero over the review window, from conformance audits) gets its trigger tightened or the injected line removed.",
61
83
  "instrument": "rule-trips.json warn counters (the dispatcher already counts every warn per concern) + conformance audits for adoption"
@@ -1,6 +1,6 @@
1
1
  {
2
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",
3
+ "measured_at_commit": "4fae6e5b7",
4
4
  "tokens_exact": true,
5
5
  "ceilings": {
6
6
  "active-remediation": 1601,
@@ -38,7 +38,7 @@
38
38
  "provider-lifecycle-discipline": 472,
39
39
  "reviewer-awareness": 131,
40
40
  "roadmap-ci-steps-policy": 564,
41
- "roadmap-progress-sync": 1424,
41
+ "roadmap-progress-sync": 2117,
42
42
  "rule-type-governance": 87,
43
43
  "skill-improvement-trigger": 81,
44
44
  "skill-quality": 78,
@@ -48,5 +48,13 @@
48
48
  "untrusted-input-defense": 845,
49
49
  "upstream-proposal": 76
50
50
  },
51
- "history": []
51
+ "history": [
52
+ {
53
+ "date": "2026-08-19",
54
+ "rule": "roadmap-progress-sync",
55
+ "from": 1424,
56
+ "to": 2117,
57
+ "reason": "Iron Law 3 gained the preservation test that decides WHO resolves a deferred item — a routing table plus the two council refinements that make it binding (a follow-up naming a roadmap that does not exist yet fails closed to the user; a council resolution is recorded at the item or it did not happen) and the residual limit both authoring seats named. This is obligation surface, not explanation: the table decides a route and the two refinements are the conditions under which the route is legal, so a reader who reaches only the pointer target has already made the decision. It could not move to the pointer target either — roadmap-progress-mechanics.md sits at the 16,000-char check_depth_budget ceiling in this same change, and pushing prose there reds a count ratchet whose whole job is refusing a fifth over-ceiling file. The section was condensed once before this raise (2201 -> 2117) by turning four prose paragraphs into bullets; further cutting would delete substance, which preservation-guard forbids."
58
+ }
59
+ ]
52
60
  }
@@ -170,6 +170,18 @@ Tier 1 — power-user (release shape, audit, migration):
170
170
  pushed, else an issue. Running it is your Hard-Floor
171
171
  confirmation.
172
172
  Usage: self-repair:release <fingerprint> [--dry-run]
173
+ decision:memo Record or list a question the run resolved without
174
+ asking you, so it stays reviewable afterwards.
175
+ Local-only (gitignored); the locked decision
176
+ classes are refused at the config schema, not here.
177
+ Usage: decision:memo write --run <id> --question <q>
178
+ --chosen <c> --reasoning <r> --resolver <who>
179
+ --confidence <high|medium|low>
180
+ decision:memo list --run <id>
181
+ run:supervise Report runs whose session died with open steps
182
+ left. Report-only; --relaunch is the acting path.
183
+ NEVER merges, pushes, or closes anything.
184
+ Usage: run:supervise --once [--root PATH]
173
185
  EOF
174
186
  fi
175
187
 
@@ -217,7 +229,11 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
217
229
  --json, --reply (reply-close form: the one blocking
218
230
  decision in full, the rest as a count; prints
219
231
  nothing when none are yours), --pending (staged
220
- requires_confirmation actions awaiting you)
232
+ requires_confirmation actions awaiting you),
233
+ --sheet (ONE consolidated decision sheet: every
234
+ user-owned decision, sorted by unblock count, each
235
+ with a default and that default's provenance —
236
+ accept-all-defaults is a valid answer)
221
237
  capabilities:index Regenerate CAPABILITIES.yaml — the package coverage index
222
238
  (capability area → coverage → backing skills/commands → gaps).
223
239
  Pass --check to fail if stale (for CI). Reads src/ (package repo).
@@ -1174,6 +1190,29 @@ cmd_self_repair() {
1174
1190
  exec_ts "$script" "$sub" "$@"
1175
1191
  }
1176
1192
 
1193
+ # `decision:memo {write,list}` — the decision-memo channel (UOTL Phase 4.3).
1194
+ # A question resolved without contacting the user is only legitimate if the
1195
+ # resolution is reviewable afterwards; this is where that record goes. Writes
1196
+ # under agents/runtime/state/decisions/<run>/, which is gitignored, so a memo
1197
+ # never reaches a commit unless a human copies it out.
1198
+ cmd_decision_memo() {
1199
+ local sub="$1"; shift || true
1200
+ local script
1201
+ script="$(resolve_script "src/scripts/decision_memo.ts")" || return 1
1202
+ exec_ts "$script" "$sub" "$@"
1203
+ }
1204
+
1205
+ # `run:supervise` — the out-of-process watcher for runs whose session died
1206
+ # with open steps left. Report-only by default; the acting path is behind
1207
+ # --relaunch because starting a session spends tokens with nobody watching.
1208
+ # It NEVER merges, pushes, or closes anything — the auto-merge the reference
1209
+ # design carries is a named rejection, not an unbuilt feature.
1210
+ cmd_run_supervise() {
1211
+ local script
1212
+ script="$(resolve_script "src/scripts/run_supervise.ts")" || return 1
1213
+ exec_ts "$script" "$@"
1214
+ }
1215
+
1177
1216
  # `use --profile=<id>` — switch the active experience/profile. Writes
1178
1217
  # profile.id into the canonical .agent-settings.yml; the explicit
1179
1218
  # profile-switch seam named by ADR-040 (road-to-6.0.0-a Step 8).
@@ -1449,6 +1488,8 @@ main() {
1449
1488
  council:quota) cmd_council quota "$@" ;;
1450
1489
  self-repair:status) cmd_self_repair status "$@" ;;
1451
1490
  self-repair:release) cmd_self_repair release "$@" ;;
1491
+ decision:memo) cmd_decision_memo "$@" ;;
1492
+ run:supervise) cmd_run_supervise "$@" ;;
1452
1493
  update) cmd_update "$@" ;;
1453
1494
  upgrade) cmd_upgrade "$@" ;;
1454
1495
  refresh) cmd_refresh "$@" ;;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * council_fallback_posture — "would this seat have an api rung to fall back
3
+ * to?", for `council:status`.
4
+ *
5
+ * Lives here rather than in `council_cli.ts` because that file is 2,700 lines
6
+ * over the source ceiling and the documented fix is extraction
7
+ * (`gate-violation-baselines.json` § check_source_size_budget).
8
+ *
9
+ * The posture is derived from the SAME two facts the twin factory in
10
+ * `build_members` uses — the provider has an api constructor, and its key
11
+ * resolves — so the line `council:status` prints and the retry that actually
12
+ * happens cannot disagree. It deliberately does not CONSTRUCT the client:
13
+ * `council:status` is the one command that must stay free of side effects.
14
+ */
15
+
16
+ export type FallbackPosture = 'api' | 'none' | 'n/a';
17
+
18
+ export interface PostureInput {
19
+ /** Resolved transport for this member; `null` when it did not resolve. */
20
+ readonly transport: string | null;
21
+ /** Does the provider have an api constructor at all? */
22
+ readonly hasApiRung: boolean;
23
+ /** The member's own `api_key_ref`, or `null` when it sets none. */
24
+ readonly apiKeyRef: string | null;
25
+ /** Does that explicit ref resolve? `undefined` when there is no ref. */
26
+ readonly refResolves: boolean | undefined;
27
+ /**
28
+ * The keyless loader for this provider, when one exists. Only anthropic
29
+ * and openai have one; the strict constructors refuse without an explicit
30
+ * ref. Probed rather than assumed — the generic loader can come up empty,
31
+ * and reporting `'api'` on the strength of the provider name would print
32
+ * a rung that is not there.
33
+ */
34
+ readonly keylessResolves: (() => boolean) | undefined;
35
+ }
36
+
37
+ /**
38
+ * `'n/a'` for a member that is not on the cli rung: there is nothing to fall
39
+ * back FROM, and reporting `'none'` there would read as a missing capability
40
+ * rather than an inapplicable question.
41
+ */
42
+ export function fallbackPostureFor(input: PostureInput): FallbackPosture {
43
+ if (input.transport !== 'cli') return 'n/a';
44
+ if (!input.hasApiRung) return 'none';
45
+ if (input.apiKeyRef !== null) return input.refResolves === true ? 'api' : 'none';
46
+ if (input.keylessResolves === undefined) return 'none';
47
+ try {
48
+ return input.keylessResolves() ? 'api' : 'none';
49
+ } catch {
50
+ return 'none';
51
+ }
52
+ }
53
+
54
+ /**
55
+ * The `council:status` lines for the fallback posture.
56
+ *
57
+ * Returns `[]` when no seat is on the cli rung: there is nothing to fall back
58
+ * FROM, and a heading over an empty list reads as a missing capability.
59
+ */
60
+ export function renderPostureLines(
61
+ postures: ReadonlyArray<readonly [string, FallbackPosture]>,
62
+ apiOnQuota: boolean,
63
+ ): string[] {
64
+ const live = postures.filter(([, p]) => p !== 'n/a');
65
+ if (live.length === 0) return [];
66
+ const lines = live.map(
67
+ ([name, p]) =>
68
+ ` fallback ${name}: ` +
69
+ (p === 'api'
70
+ ? 'would fall back to api'
71
+ : 'no api rung — a lost cli transport loses the seat'),
72
+ );
73
+ // Separate from the per-seat lines: not a per-seat property but the one
74
+ // billing-class decision, governing every seat at once.
75
+ lines.push(
76
+ ` fallback quota api_on_quota: ${apiOnQuota ? 'on' : 'off'}` +
77
+ ` — an exhausted cli quota ${apiOnQuota ? 'MAY' : 'may not'}` +
78
+ ' retry on the metered rung',
79
+ );
80
+ return lines;
81
+ }