@event4u/agent-config 12.0.0 → 13.0.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 (148) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +273 -0
  3. package/README.md +1 -1
  4. package/agents/templates/.ai-council.yml.example +46 -3
  5. package/dist/agent-src/commands/cost/report.md +23 -0
  6. package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
  7. package/dist/agent-src/contexts/model-recommendations.md +6 -4
  8. package/dist/agent-src/contexts/subagent-configuration.md +69 -7
  9. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  10. package/dist/agent-src/rules/design-fidelity.md +1 -1
  11. package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
  12. package/dist/agent-src/rules/evaluator-independence.md +27 -5
  13. package/dist/agent-src/rules/git-history-discipline.md +5 -1
  14. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  15. package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
  16. package/dist/agent-src/rules/ui-audit-gate.md +9 -4
  17. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  18. package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
  19. package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -1
  20. package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
  21. package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
  22. package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
  23. package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
  24. package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
  25. package/dist/agent-src/skills/roadmap-writing/SKILL.md +11 -11
  26. package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
  27. package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/agent-src/templates/roadmaps.md +41 -3
  30. package/dist/cli/registry.js +1 -0
  31. package/dist/cli/registry.js.map +1 -1
  32. package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-3B72LSZL.js} +123 -2338
  33. package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
  34. package/dist/cli-delegate/chunk-N7FEST6D.js +3895 -0
  35. package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-XURJ4AQC.js} +566 -298
  36. package/dist/cli-delegate/cmd_conformance.js +4 -4
  37. package/dist/cli-delegate/cmd_doctor.js +2 -2
  38. package/dist/cli-delegate/cmd_export.js +2 -2
  39. package/dist/cli-delegate/cmd_handoff.js +1 -1
  40. package/dist/cli-delegate/cmd_prune.js +2 -2
  41. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  42. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  43. package/dist/cli-delegate/cmd_sync.js +2 -2
  44. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  45. package/dist/cli-delegate/cmd_validate.js +2 -2
  46. package/dist/discovery/deprecation-report.md +1 -1
  47. package/dist/discovery/discovery-manifest.json +44 -23
  48. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  49. package/dist/discovery/discovery-manifest.summary.md +3 -3
  50. package/dist/discovery/dormancy-report.md +1 -1
  51. package/dist/discovery/orphan-report.md +1 -1
  52. package/dist/discovery/packs.json +6 -5
  53. package/dist/discovery/trust-report.md +2 -2
  54. package/dist/discovery/workspaces.json +5 -4
  55. package/dist/hooks/dispatch.js +10625 -8541
  56. package/dist/install/install.mjs +754 -484
  57. package/dist/mcp/registry-manifest.json +3 -3
  58. package/dist/mcp/server.json +2 -2
  59. package/dist/mcp/server.mjs +1302 -167
  60. package/dist/router.json +1 -1
  61. package/dist/scripts/_lib/model_tier.js +9 -0
  62. package/dist/scripts/_lib/model_tier.js.map +1 -1
  63. package/dist/scripts/condense.js +50 -0
  64. package/dist/scripts/condense.js.map +1 -1
  65. package/dist/server/schemas/settings.js +2 -2
  66. package/dist/server/schemas/settings.js.map +1 -1
  67. package/docs/MIGRATION.md +20 -1
  68. package/docs/guidelines/design-fidelity-mechanics.md +21 -0
  69. package/docs/guidelines/design-handover-extraction.md +87 -0
  70. package/docs/guidelines/prompt-templates.md +24 -1
  71. package/package.json +1 -1
  72. package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  73. package/src/agent-src/scripts/roadmap_gates.ts +30 -2
  74. package/src/agent-src/scripts/update_roadmap_progress.ts +47 -1
  75. package/src/config/evaluator-budgets.json +9 -8
  76. package/src/config/gate-coverage.yml +63 -11
  77. package/src/config/gate-violation-baselines.json +12 -0
  78. package/src/config/hook-token-budget.json +15 -2
  79. package/src/config/preamble-payload-budget.json +28 -0
  80. package/src/scripts/_dispatch.bash +10 -0
  81. package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
  82. package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
  83. package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
  84. package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
  85. package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
  86. package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
  87. package/src/scripts/_lib/git_common_dir.ts +173 -0
  88. package/src/scripts/_lib/model_tier.ts +9 -0
  89. package/src/scripts/_lib/release_highlights.ts +108 -7
  90. package/src/scripts/_lib/skill_catalogue.ts +1115 -0
  91. package/src/scripts/_lib/tier_budget_routing.ts +31 -331
  92. package/src/scripts/ai_council/clients.ts +197 -18
  93. package/src/scripts/ai_council/config.ts +16 -1
  94. package/src/scripts/ai_council/low_impact.ts +14 -1
  95. package/src/scripts/ai_council/orchestrator.ts +14 -1
  96. package/src/scripts/ai_council/transport_resolver.ts +32 -2
  97. package/src/scripts/bench_ab_v2_complexity.ts +252 -0
  98. package/src/scripts/bench_ab_v2_run.ts +3 -42
  99. package/src/scripts/bench_ab_v2_safety.ts +296 -0
  100. package/src/scripts/bench_ab_v2_search.ts +343 -0
  101. package/src/scripts/bench_ab_v2_stats.ts +191 -0
  102. package/src/scripts/build_archive_index.ts +383 -0
  103. package/src/scripts/capture_skill_catalogue.ts +279 -317
  104. package/src/scripts/check_branch_freshness.ts +7 -1
  105. package/src/scripts/check_release_published.ts +6 -2
  106. package/src/scripts/check_release_trunk_sync.ts +18 -2
  107. package/src/scripts/check_source_size_budget.ts +428 -0
  108. package/src/scripts/check_suppression_hygiene.ts +5 -1
  109. package/src/scripts/condense.ts +50 -1
  110. package/src/scripts/cost/backfill_rates.mjs +298 -0
  111. package/src/scripts/cost/budget.mjs +12 -32
  112. package/src/scripts/cost_summary.ts +25 -0
  113. package/src/scripts/dispatch_r2_reviewer.ts +15 -5
  114. package/src/scripts/evidence_report.ts +15 -4
  115. package/src/scripts/external_sources_denylist.json +1 -0
  116. package/src/scripts/hook_manifest.yaml +75 -15
  117. package/src/scripts/hooks/concern_registry.ts +4 -0
  118. package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
  119. package/src/scripts/hooks/skill_route_hook.ts +226 -0
  120. package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
  121. package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
  122. package/src/scripts/hot_context_hook.ts +13 -1
  123. package/src/scripts/install.ts +129 -0
  124. package/src/scripts/janitor.ts +77 -2
  125. package/src/scripts/lint_design_antipattern_parity.ts +40 -6
  126. package/src/scripts/lint_harvest_provenance.ts +35 -1
  127. package/src/scripts/lint_plan_risk_register.ts +22 -9
  128. package/src/scripts/lint_roadmap_blockers.ts +109 -6
  129. package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
  130. package/src/scripts/lint_scheduled_deprecations.ts +738 -0
  131. package/src/scripts/lint_skill_router_head.ts +21 -0
  132. package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
  133. package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
  134. package/src/scripts/mcp_server/tools.ts +87 -0
  135. package/src/scripts/migration_status.ts +17 -1
  136. package/src/scripts/probe_review_binding_drift.ts +731 -0
  137. package/src/scripts/release.ts +100 -6
  138. package/src/scripts/routing_doctor.ts +50 -13
  139. package/src/scripts/schemas/command.schema.json +3 -3
  140. package/src/scripts/schemas/skill.schema.json +20 -2
  141. package/src/scripts/schemas/subagent.schema.json +2 -2
  142. package/src/scripts/session_eol_report.ts +81 -0
  143. package/src/scripts/trigger_coverage.ts +224 -3
  144. package/src/scripts/workspace_doctor.ts +464 -0
  145. package/src/scripts/worktree_cleanup_check.ts +2 -1
  146. package/src/server/schemas/settings.ts +2 -2
  147. package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
  148. package/src/config/budget-routing.json +0 -7
@@ -0,0 +1,87 @@
1
+ # Design Handover — URL / live-page extraction
2
+
3
+ > Where a URL handover gets extracted to, what consumes it, and in what order it is read back
4
+
5
+ _Split out of [`design-fidelity-mechanics`](design-fidelity-mechanics.md) § URL / live-page handover, which carries the obligation surface and routes here. The split is the per-file depth ceiling doing its job: the mechanics file sat at 14.2k of its 16k budget, and compressing this material to fit would have cost the parts that make it actionable._
6
+
7
+ ```
8
+ A HANDOVER THAT ARRIVES AS A URL IS STILL RUNG 2, NOT RUNG 4.
9
+ EXTRACT THROUGH THE USER'S CONNECTED BROWSER TOOLS INTO FILES
10
+ BEFORE ANY UI WRITE. A SCREENSHOT TAKEN DURING EXTRACTION
11
+ CARRIES QA DUTY ONLY — IT IS NEVER THE THING YOU BUILD FROM.
12
+ ```
13
+
14
+ When the artifact is handed over as a **URL** rather than a file — a published
15
+ artifact link, a builder's share link, a staging or `localhost` page — rung 2 of
16
+ the [data-basis ladder](design-fidelity-mechanics.md#data-basis-ladder) is
17
+ reachable and rung 4 is not the fallback it looks like. Pull the DOM,
18
+ stylesheets, scripts and assets through the browser tools the user has
19
+ connected, and land them as files first.
20
+
21
+ ## Where the extraction lands
22
+
23
+ Into `design-system.json` under `.claude/design-system/` — the `path_prefix` the
24
+ `design-fidelity` rule already routes on. That file is an **existing contract**,
25
+ specified in
26
+ [`design-system-json.md`](../../src/skills/design-system-capture/references/design-system-json.md);
27
+ this guideline defines no format of its own. Raw source files land beside it,
28
+ and screenshots taken while extracting land in a references directory next to
29
+ them.
30
+
31
+ ## What consumes it
32
+
33
+ [`/design-system:import`](../../src/domains/engineering-base/design-system/import/command.md)
34
+ runs the extraction output through the three-lane adapter
35
+ (`native` / `dtcg` / `dembrandt`) into the contract shape, then hands it to
36
+ `design-system-capture`'s per-field import. The adapter is **offline** — it
37
+ reads a file and writes the contract shape; it never fetches, crawls, or
38
+ launches a browser.
39
+
40
+ ## The lock boundary — stated here because this is where an agent would cross it
41
+
42
+ The package ships the contract, the adapter, and the instructions. It does
43
+ **not** ship the crawler, the Playwright runtime, or a font-bundler. Extraction
44
+ runs through the user's own connected tooling, and its output is *observed, not
45
+ authoritative*: every field is confirmed by the human, and a value conflicting
46
+ with a registered brand token is flagged rather than applied
47
+ (`brand-source-of-truth`).
48
+
49
+ ## Retrieval order — so the source survives the session
50
+
51
+ Check in this order and stop at the first hit. The point is that the next
52
+ session reads a file instead of re-screenshotting the same page.
53
+
54
+ | # | Source | Why it ranks here |
55
+ |---|---|---|
56
+ | 1 | The project's own `design-system.json` | Already confirmed by a human in an earlier session — the only rung whose values someone signed off on |
57
+ | 2 | A previous extraction artifact under `.claude/design-system/` | Observed and file-backed; re-reading it costs nothing and re-extracting may drift |
58
+ | 3 | The live page | Authoritative for *today's* page, but only reachable while it is up and the tooling is connected |
59
+
60
+ **Persistence is not restated here.** Skip-if-exists unless explicitly forced,
61
+ and never silently discarding a prior confirmed decision, are owned by
62
+ [`/design-system:generate`](../../src/domains/engineering-base/design-system/generate/command.md)
63
+ — *"never overwrite a confirmed `DESIGN.md`"*, *"Never persist silently."* This
64
+ guideline states the order; that command states what happens on a collision, and
65
+ stating it twice is how the two drift apart.
66
+
67
+ ## The producer sentence
68
+
69
+ The documented easy path is a connected extractor MCP, attached once and reused.
70
+ Where none is available, the manual Chrome-DevTools-MCP channel is the fallback,
71
+ and where neither is reachable the handover degrades to rung 3 or 4 — which the
72
+ ladder requires you to **name**, not to pass off as a source-based build.
73
+
74
+ ## Coverage — stated honestly
75
+
76
+ No fixture scores this class yet. `daf-source-over-screenshot` scores the rung
77
+ choice on an *attached* artifact, not a URL handover, and it is itself recorded
78
+ as SKIPPED on 2026-08-13 for want of a page-reaching capture primitive. Closing
79
+ the gap needs a fixture whose host actually has one; claiming a regression
80
+ witness before then would be the fabrication this package's evidence discipline
81
+ exists to prevent.
82
+
83
+ ## See also
84
+
85
+ - [`design-fidelity-mechanics`](design-fidelity-mechanics.md) — the data-basis ladder this operationalises, and the adopt-the-code duty that governs what you do with the extracted source.
86
+ - [`design-fidelity`](../../src/rules/design-fidelity.md) — the rule; a provided artifact is the spec.
87
+ - [`design-system-capture`](../../src/skills/design-system-capture/SKILL.md) — the per-field import the adapter hands off to.
@@ -138,9 +138,32 @@ Given an existing good output, reconstruct the prompt that would
138
138
  produce it. Used to mine prompts from public LLM artifacts. Not a
139
139
  shaping template — a forensic one.
140
140
 
141
+ ## Viral-prompt anti-patterns — what this catalogue deliberately omits
142
+
143
+ Prompt collections that circulate socially optimise for looking impressive
144
+ on the way in, not for what the target model can act on. None of the
145
+ templates above encodes the three patterns below, and that is a decision
146
+ rather than an oversight — a template is a slot structure, and these three
147
+ are slots the model cannot fill honestly.
148
+
149
+ | Anti-pattern | Why no template carries it |
150
+ |---|---|
151
+ | **Grandiose persona** — "world's foremost", "legendary", "10× expert" | Every template's role slot takes a *capability* (`RTF` Role, `CRISPE` Capacity, `CO-STAR` Style). A superlative is not a capability: it spends tokens and changes nothing the model does. |
152
+ | **Presupposed canon** — "the 7 laws of X", "the standard X framework", for a body of knowledge that may not exist | A presupposition in the context slot is answered by confabulation, and the invented canon inherits the authority of the frame. `Few-Shot` exists precisely so that a real pattern is *shown* rather than asserted. |
153
+ | **Scope stuffing** — analysis *and* strategy *and* copy *and* a checklist in one prompt | Each template has one Expectation / Objective / Format slot by construction. Four deliverables in one prompt produce four shallow ones; split them, or name the primary. |
154
+
155
+ The catalogue's own position: a template earns its place by making a
156
+ request *checkable*, not by making it sound authoritative. Where a rough
157
+ prompt carries one of these three, the
158
+ [`prompt-optimizer`](../../.agent-src.uncondensed/skills/prompt-optimizer/SKILL.md)
159
+ strips it in its de-inflation sub-step before any template is picked —
160
+ and strips nothing else, because everything outside these three is a
161
+ requirement the author meant. This section is own analysis; it records a
162
+ decision about this catalogue rather than adopting an outside claim.
163
+
141
164
  ## Rejection note
142
165
 
143
- Upstream `nidhinjs/prompt-master` claims that only five techniques
166
+ An external prompt-collection claims that only five techniques
144
167
  are "safe" for production prompting:
145
168
 
146
169
  - few-shot
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
3
  "mcpName": "io.github.event4u-app/agent-config",
4
- "version": "12.0.0",
4
+ "version": "13.0.0",
5
5
  "description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including \"zero runtime daemon\".",
6
6
  "license": "MIT",
7
7
  "private": false,
@@ -403,6 +403,38 @@ function _regen_dashboard(root: string, dry_run: boolean): void {
403
403
  }
404
404
  }
405
405
 
406
+ /**
407
+ * Rebuild the archive index after a move, when the builder is present.
408
+ *
409
+ * The sweep is the one operation that changes what `agents/roadmaps/archive/`
410
+ * contains, so it is the one operation that can leave the generated index
411
+ * stale — and the drift gate on that index runs in the same PR the sweep runs
412
+ * in. Regenerating here keeps the two from disagreeing by construction rather
413
+ * than by remembering.
414
+ *
415
+ * `build_archive_index` is package-internal (`src/scripts/`), while this sweep
416
+ * is projected into consumer installs that carry no such script. Absent
417
+ * builder → nothing to regenerate, and that is a normal consumer state, not a
418
+ * failure. The spawn is best-effort for the same reason `_regen_dashboard` is:
419
+ * a sweep that archived correctly must not report failure because a
420
+ * regeneration could not run.
421
+ */
422
+ function _regen_archive_index(root: string, dry_run: boolean): void {
423
+ if (dry_run) {
424
+ return;
425
+ }
426
+ const script = path.join(root, 'src', 'scripts', 'build_archive_index.ts');
427
+ if (!_isFile(script)) {
428
+ return;
429
+ }
430
+ _runTwin(root, script);
431
+ for (const rel of ['agents/roadmaps/archive/INDEX.md', 'agents/roadmaps/archive/index.json']) {
432
+ if (_isFile(path.join(root, rel))) {
433
+ _run(['git', 'add', '--', rel], root);
434
+ }
435
+ }
436
+ }
437
+
406
438
  /** Spawn the dashboard generator twin (tsx) with cwd=root, mirroring `[sys.executable, script]`. */
407
439
  function _runTwin(root: string, script: string): void {
408
440
  // Python uses `sys.executable` — the interpreter already running this
@@ -491,6 +523,7 @@ function main(argv?: readonly string[]): number {
491
523
  return 0;
492
524
  }
493
525
  _regen_dashboard(root, ns.dry_run);
526
+ _regen_archive_index(root, ns.dry_run);
494
527
  const verb = ns.dry_run ? 'Would archive' : 'Archived';
495
528
  for (const rec of archived) {
496
529
  process.stdout.write(
@@ -126,10 +126,10 @@ function wrap(text: string, width: number, indent: string): string[] {
126
126
 
127
127
  /**
128
128
  * Column width for the label gutter — sized to the longest label in use
129
- * (`Blocked until:`, 14 chars) plus one separating space, so no value ever
129
+ * (`If you do nothing:`, 18 chars) plus one separating space, so no value ever
130
130
  * runs flush against its own label.
131
131
  */
132
- const LABEL_W = 15;
132
+ const LABEL_W = 19;
133
133
 
134
134
  /** A labelled block: ` Label: value`, wrapped and hanging-indented. */
135
135
  function field(label: string, value: string): string[] {
@@ -183,6 +183,23 @@ function renderEntry(e: Entry, index: number): string[] {
183
183
  ...field('Blocks:', e.blocker.blocks),
184
184
  ];
185
185
 
186
+ // What is actually being decided, when the entry says so.
187
+ if (e.blocker.question) {
188
+ out.push(...field('The question:', e.blocker.question));
189
+ }
190
+
191
+ // The recommendation leads, because it is the answer — everything below it
192
+ // is how to carry the answer out. An entry that predates the field says so
193
+ // rather than rendering a silent gap: the reader needs to know the analysis
194
+ // is missing, not merely that a line is absent.
195
+ out.push(
196
+ ...field(
197
+ 'Recommendation:',
198
+ e.blocker.recommendation ||
199
+ '(none recorded — this entry predates the field; ask for one before deciding)',
200
+ ),
201
+ );
202
+
186
203
  const doLabel = isLegacy(e.blocker) ? 'Blocked until:' : 'Do this:';
187
204
  const steps = regroupTodo(e.blocker.todo);
188
205
  if (steps.length === 0) {
@@ -203,6 +220,9 @@ function renderEntry(e: Entry, index: number): string[] {
203
220
  out.push(...lines.map((l, i) => (i === 0 ? indent + l : l)));
204
221
  }
205
222
  }
223
+ if (e.blocker.ifNothing) {
224
+ out.push(...field('If you do nothing:', e.blocker.ifNothing));
225
+ }
206
226
  out.push(...field('Done when:', e.blocker.resolvedWhen));
207
227
  return out;
208
228
  }
@@ -238,6 +258,14 @@ function render(entries: readonly Entry[], all: boolean): string {
238
258
  }
239
259
  if (shown.length > 0) {
240
260
  lines.push('');
261
+ // Deciding is not the same as executing, and a list that only decides
262
+ // leaves the second half with the person who has the least context.
263
+ // Naming the guided path here is the cheapest place to close that gap:
264
+ // it is on screen at exactly the moment the reader is looking at a
265
+ // decision they did not write.
266
+ lines.push('Not sure about one of these? Ask your agent:');
267
+ lines.push(' "guide me through <id>" — one decision, step by step');
268
+ lines.push('');
241
269
  }
242
270
  return lines.join('\n') + '\n';
243
271
  }
@@ -347,6 +347,24 @@ interface Blocker {
347
347
  blocks: string;
348
348
  todo: string[];
349
349
  resolvedWhen: string;
350
+ /**
351
+ * The two fields that make a blocker *decidable* rather than merely
352
+ * described. Empty string when the entry predates them — the ratchet in
353
+ * `lint_roadmap_blockers` keeps the backlog legal, so the renderer has to
354
+ * cope with absence rather than assume it away.
355
+ */
356
+ recommendation: string;
357
+ ifNothing: string;
358
+ /**
359
+ * Optional one-line statement of what is actually being decided.
360
+ *
361
+ * Not required, but recognised: three entries in the tree already used
362
+ * `- **Question:**` and, because the field list did not know it, the
363
+ * continuation logic silently appended it to `Blocks` — the decision's
364
+ * subject arrived glued to the end of an unrelated sentence. Naming it
365
+ * costs one enum entry and turns a silent swallow into a rendered field.
366
+ */
367
+ question: string;
350
368
  }
351
369
 
352
370
  // Strip fenced code blocks before blocker detection — a roadmap that shows the
@@ -362,7 +380,23 @@ function _stripFencedCode(text: string): string {
362
380
  return text.replace(FENCED_CODE_RE, (m) => '\n'.repeat(_splitlines(m).length));
363
381
  }
364
382
 
365
- const BLOCKER_FIELD_RE = /^-[ \t]*\*\*(Status|Owner|Blocks|Resolved when|What to do):\*\*/i;
383
+ /**
384
+ * Terminator for a blocker field's continuation lines — ANY `- **Label:**`
385
+ * bullet, not a closed list of known labels.
386
+ *
387
+ * It used to enumerate eight names, which silently diverged from the contract
388
+ * the two call sites below already document ("up to the next `- **Field:**`
389
+ * marker"): a bullet whose label was not on the list did not terminate, so the
390
+ * PREVIOUS field kept absorbing it and the dashboard rendered the two as one
391
+ * run-on sentence. Measured when `- **Options:**` and `- **Side finding:**`
392
+ * first appeared in a blocker — a reader saw the side finding as part of
393
+ * `Resolved when`, i.e. the generator changed what a field meant.
394
+ *
395
+ * A closed list is the wrong shape for a terminator: every future field name
396
+ * would have to be added here to avoid corrupting the field above it, and the
397
+ * failure is silent in generated output nobody edits by hand.
398
+ */
399
+ const BLOCKER_FIELD_RE = /^-[ \t]*\*\*[^*]+:\*\*/;
366
400
 
367
401
  /** Strip an inline `<!-- comment -->` and trim. */
368
402
  function _stripComment(s: string): string {
@@ -471,6 +505,9 @@ function parse_blockers(raw_text: string): Blocker[] {
471
505
  blocks: _blockerField(body, 'Blocks') ?? '(unspecified)',
472
506
  todo: _blockerTodo(body),
473
507
  resolvedWhen: _blockerField(body, 'Resolved when') ?? '(unspecified)',
508
+ recommendation: _blockerField(body, 'Recommendation') ?? '',
509
+ ifNothing: _blockerField(body, 'If you do nothing') ?? '',
510
+ question: _blockerField(body, 'Question') ?? '',
474
511
  });
475
512
  }
476
513
  }
@@ -502,6 +539,9 @@ function parse_blockers(raw_text: string): Blocker[] {
502
539
  blocks: 'entire roadmap',
503
540
  todo: [parts.join(' ')],
504
541
  resolvedWhen: 'condition described above clears',
542
+ recommendation: '',
543
+ ifNothing: '',
544
+ question: '',
505
545
  });
506
546
  }
507
547
  return blockers;
@@ -869,6 +909,12 @@ function render(roadmaps: RoadmapStats[], bundles: Bundle[] | null = null): stri
869
909
  lines.push('**Blockers**\n');
870
910
  for (const b of openBlockers) {
871
911
  lines.push(`- **${b.id}** (owner: ${b.owner}) — blocks ${b.blocks}`);
912
+ if (b.recommendation) {
913
+ lines.push(` - **Recommendation:** ${b.recommendation}`);
914
+ }
915
+ if (b.ifNothing) {
916
+ lines.push(` - **If you do nothing:** ${b.ifNothing}`);
917
+ }
872
918
  lines.push(' - **What to do:**');
873
919
  for (const step of b.todo) {
874
920
  lines.push(` ${step}`);
@@ -58,17 +58,18 @@
58
58
  "deterministic": false
59
59
  },
60
60
  "cli_help_command_count": {
61
- "max": 97,
62
- "last_measured": 97,
63
- "last_measured_at": "2026-08-10",
61
+ "max": 98,
62
+ "last_measured": 98,
63
+ "last_measured_at": "2026-08-15",
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."
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."
66
66
  },
67
67
  "mcp_public_tool_count": {
68
- "max": 19,
69
- "last_measured": 19,
70
- "last_measured_at": "2026-08-02",
71
- "method": "tools/list length on the stdio server (post-ADR-132: implemented tools only)"
68
+ "max": 20,
69
+ "last_measured": 20,
70
+ "last_measured_at": "2026-08-17",
71
+ "method": "tools/list length on the stdio server (post-ADR-132: implemented tools only)",
72
+ "baseline_note": "19 → 20 on 2026-08-17 (release 13.0.0 gate). The +1 is 866bd9861 (feat(mcp): recover what the host dropped — real ranker tool + a route to it, road-to-inbox-harvest-2026-08-d-runtime-skill-routing Phase 4): `suggest_skill_for_task` was promoted from a discovery STUB to a real ALLOWLIST entry and removed from STUB_TOOLS. Worth stating precisely, because the consumer-visible NAME did not grow — the stub already published that name and its `task` + `limit` input schema, which the promotion carried over verbatim. What grew is the count of tools tools/list actually returns, which is exactly what this metric measures post-ADR-132: implemented tools only. Same shape as the cli_help_command_count history above — this RECORDS growth that already shipped through its own reviewed PR (866bd9861 is on main), it does not authorize new surface. The registering PR did not move this budget, so the gate did its job: the breach warned on main and blocked on the release ref, which is the conscious-decision point rather than a silently raised cap. Re-measured twice via the pinned method on a freshly built dist/mcp/server.mjs: 20/20. Headroom stays zero by design — the next tool that leaves STUB_TOOLS must move this number in its own PR."
72
73
  }
73
74
  }
74
75
  }
@@ -40,20 +40,24 @@
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-13)
43
+ # COVERAGE — THE HONEST DENOMINATOR (re-measured 2026-08-16)
44
44
  # ----------------------------------------------------------
45
- # 257 `lint_*` / `check_*` / `audit_*` scripts live under `src/scripts/`
45
+ # 260 `lint_*` / `check_*` / `audit_*` scripts live under `src/scripts/`
46
46
  # (the count is computed, not asserted — `check_gate_coverage.count_gate_scripts`).
47
- # **35 of them emit the machine-readable `scanned: <N>` line under their
48
- # CI-identical argv, and all 35 are listed below.** The manifest is therefore
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
49
  # complete with respect to its own contract, and covers ~14 % of the population.
50
- # (The 2026-08-05 reading said "32 … and all 31 are listed"; the 2026-08-10
51
- # reading said "33 … and all 33" while 34 entries were listed. Both halves are
52
- # RECOUNTED here: 257 is what `count_gate_scripts()` returns on the tree that
53
- # carries this change, and 35 is the entries counted directly. It read 255 while
54
- # this change was being written and 257 after `main` merged in two further gate
55
- # scripts — re-measured at the merge rather than left at the earlier reading,
56
- # which is the same discipline the note below records failing.)
50
+ # (The 2026-08-05 reading said "32 … and all 31 are listed" and the 2026-08-10
51
+ # reading said "33 … and all 33" while 34 entries were listed — those two are
52
+ # the MISCOUNTS this paragraph exists to warn about, and they are listed here
53
+ # precisely because they were wrong, not correct-at-the-time. The 2026-08-13
54
+ # reading ("257 … 35 of them") and the 2026-08-15 reading ("259 … 36 of them")
55
+ # were both correct when written.
56
+ # Both halves are RECOUNTED here on 2026-08-16: 260 is what
57
+ # `count_gate_scripts()` returns on the tree that carries this change, and 37 is
58
+ # `grep -c '^ - id:'` on this file. Neither number was derived by adding one to
59
+ # the previous prose, which is the move the note below forbids — and the
60
+ # temptation was live, because this change does add exactly one of each.)
57
61
  #
58
62
  # A 2026-08-13 revision of this paragraph first wrote 256, derived as "255 before
59
63
  # this change plus the one it adds" — inside the sentence claiming to have
@@ -769,3 +773,51 @@ gates:
769
773
  The canary plants a manifest whose declared CSV does not exist — the exact
770
774
  gap `corpus-grounding/scripts/schema_validator.ts` leaves open, since it
771
775
  computes the path and never opens the file.
776
+
777
+ - id: lint_scheduled_deprecations
778
+ argv: ["--quiet"]
779
+ min_scanned: 1
780
+ corpus: "rows of the `docs/MIGRATION.md` scheduled-deprecations table (3 at baseline)"
781
+ status: enforced
782
+ note: >
783
+ `argv` carries `--quiet` because that is what the task passes: the
784
+ Taskfile resolves `{{.QUIET_FLAG}}` to `--quiet` unless
785
+ AGENT_SCRIPT_VERBOSITY=verbose, and the consistency workflow goes through
786
+ the same task. It passed as a bare call only because `reportScanned` is
787
+ not quiet-gated while this gate's success line and ledger report are — a
788
+ coincidence, not the contract rule 2 states.
789
+ The floor is 1, not "a floor comfortably below the corpus", and the reason
790
+ is worth stating rather than hiding: the corpus is three rows, so no floor
791
+ exists that is both below it and far above a collapse. What the floor
792
+ actually buys here is the contract that the count is PUBLISHED; the
793
+ collapse case — a drifted heading or a reformatted table parsing to zero
794
+ rows — is caught one layer up by the gate's own `reportScanned`
795
+ assertion, which reds instead of printing a green line over nothing.
796
+ That case is the gate's own subject, so it is pinned by a test fixture
797
+ too (`a table that parses to zero rows reds rather than exiting green`).
798
+ No `canary:` recipe: this gate's only violation shape is a MODIFICATION of
799
+ a tracked artefact (`docs/MIGRATION.md`), and the canary contract is
800
+ create-only, so it reports UNPROVEN rather than green. Discrimination is
801
+ proven instead by paired fixtures in
802
+ `tests/scripts/lint_scheduled_deprecations.test.ts`, verified by mutation:
803
+ widening the overdue threshold fails the overdue-by-construction case, and
804
+ granting `allowEmpty` fails the zero-row case.
805
+
806
+ - id: check_source_size_budget
807
+ argv: []
808
+ min_scanned: 900
809
+ corpus: "TypeScript sources — src/**/*.ts excluding *.d.ts and *.test.ts, the same two kinds _lib/gate_population excludes (1,063 at baseline)"
810
+ status: enforced
811
+ # No `canary:` recipe, and the reason is the canary contract's own shape
812
+ # rather than a gap. The violation this gate detects is a file OVER 1,500
813
+ # lines, so a create-only recipe would have to inline 1,501 lines of content
814
+ # into this manifest — a plant larger than the manifest it lives in. It
815
+ # reports UNPROVEN rather than green, which is the honest state.
816
+ #
817
+ # Discrimination is proven instead by the gate's own `--self-test`, and it is
818
+ # stronger here than a single plant would be: six cases, three rejecting,
819
+ # covering a file one line over the ceiling, one line under, exactly at it
820
+ # (inclusive), a dead scan root, AND — the case a canary could not express —
821
+ # an already-listed file that GREW while the file COUNT stayed at one. That
822
+ # last case is what pins the metric as excess lines rather than a file count;
823
+ # a count-based ratchet passes it, and this gate must not.
@@ -22,6 +22,12 @@
22
22
  "landed": "2026-08-02",
23
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
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
+ },
25
31
  "lint_pack_boundaries": {
26
32
  "count": 337,
27
33
  "landed": "2026-08-02",
@@ -49,6 +55,12 @@
49
55
  "landed": "2026-08-12",
50
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.",
51
57
  "falsifier": "./scripts-run src/scripts/lint_settings_classes"
58
+ },
59
+ "check_source_size_budget": {
60
+ "count": 19725,
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.",
63
+ "falsifier": "./scripts-run src/scripts/check_source_size_budget --quiet"
52
64
  }
53
65
  }
54
66
  }
@@ -27,7 +27,9 @@
27
27
  "reread-guard": 1024,
28
28
  "reread-guard_reason": "one advisory line naming the earlier read / ranged alternative (roadmap 6.1)",
29
29
  "session-eol": 1024,
30
- "session-eol_reason": "one recycle-advisory line, once per session past the committed threshold (road-to-token-economy-recycling 3.2); recording itself emits nothing"
30
+ "session-eol_reason": "one recycle-advisory line, once per session past the committed threshold (road-to-token-economy-recycling 3.2); recording itself emits nothing",
31
+ "skill-route": 512,
32
+ "skill-route_reason": "road-to-inbox-harvest-2026-08-d-runtime-skill-routing 2.1: one line carrying at most three skill ids and their scores - pointers, never bodies. Registered BELOW the 1024 default deliberately: this row is the ceiling that keeps it a pointer line, so a later change that starts injecting descriptions fails the build instead of quietly doubling a per-turn slot that already carries nine other concerns under a 4096-byte sum cap"
31
33
  },
32
34
  "per_slot_sum_caps_bytes": {
33
35
  "_comment": "Cross-concern per-fire sum per slot (roadmap 3.1). session_start is one-time and may carry the restore payloads; per-turn slots stay tight.",
@@ -49,6 +51,11 @@
49
51
  "_comment": "Roadmap 5.3 + 6.4 registrations. These are BEHAVIOUR metrics (advisory adoption), not injection bytes - registered here because this file already carries the owner/review discipline for the same concerns.",
50
52
  "owner": "maintainer",
51
53
  "review_by": "2026-11-10",
54
+ "skill_route_pointer_rate": {
55
+ "definition": "road-to-inbox-harvest-2026-08-d-runtime-skill-routing 2.2: skill-route fires / user_prompt_submit events per session - the share of prompts where the deterministic ranker cleared its measured p90 floor and injected pointers. NO THRESHOLD IS COMMITTED, deliberately and by the house baseline-first rule: the TRIGGER is calibrated (top-1 >= 30/100, the p90 of 496 routing-matrix prompt lines, expected to fire on ~13.9%), the OUTCOME is not. Kill standard as for the sibling nudges: a nudge whose pointers are measurably ignored over the review window gets its floor raised or its line removed. Expected fire rate is the falsifier for the trigger half - a live rate far from 13.9% means the corpus is not representative of real prompts, which is a finding about the corpus and is published either way.",
56
+ "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
+ "threshold": "none committed before data - baseline first, threshold at review date (2026-11-10)"
58
+ },
52
59
  "full_rewrite_small_diff_rate": {
53
60
  "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.",
54
61
  "instrument": "rule-trips.json warn counters (the dispatcher already counts every warn per concern) + conformance audits for adoption"
@@ -58,10 +65,16 @@
58
65
  "instrument": "rule-trips.json warn counters per concern"
59
66
  },
60
67
  "unbounded_output_advisory_rate": {
61
- "definition": "Roadmap 4.3 + 4.4 registration (advisory-degraded per the resolved pretooluse-rewrite-semantics blocker: no updatedInput in the v1 contract, so nothing caps and nothing can be bypassed - the demand signal is the fire count itself). rtk-wrap cap-advisory fires per session from the OUTPUT_CAP_TABLE branch; adoption (did the next run use a bounded form) via conformance audits. HONEST GAP: bytes-into-context from tool RESULTS is not instrumented anywhere (the census covers hook payloads only) - the pre/post bytes half of roadmap 4.4 is registered as unmeasurable until a tool-result byte counter exists.",
68
+ "definition": "Roadmap 4.3 + 4.4 registration (advisory-degraded per the resolved pretooluse-rewrite-semantics blocker: no updatedInput in the v1 contract, so nothing caps and nothing can be bypassed - the demand signal is the fire count itself). rtk-wrap cap-advisory fires per session from the OUTPUT_CAP_TABLE branch; adoption (did the next run use a bounded form) via conformance audits. GAP CLOSED 2026-08-15 (road-to-inbox-harvest-2026-08-d-context-ledger Step 1.1): the tool-result byte counter now exists and its stream is registered below as `tool_result_bytes`. The pre/post bytes half of roadmap 4.4 is measurable from that census once it carries data; it stays unread, not unmeasurable, until a baseline is published.",
62
69
  "instrument": "rule-trips.json warn counters for the rtk-wrap concern + conformance audits",
63
70
  "threshold": "none committed before data - baseline first, threshold at review date"
64
71
  },
72
+ "tool_result_bytes": {
73
+ "definition": "road-to-inbox-harvest-2026-08-d-context-ledger 1.1: UTF-8 bytes of every tool RESULT entering context, per call, keyed by tool name. Closes the gap `unbounded_output_advisory_rate` registered above - the injection census measures what this suite puts into context, this measures what the host's tools do, and the second is the larger term. CAPTURE ONLY: the concern emits zero bytes to the model, so it carries no per-concern injection cap; it is registered here because this file already owns the owner/review discipline for context-cost instruments.",
74
+ "instrument": "the `tool-result-bytes` concern (src/scripts/hooks/tool_result_bytes_hook.ts) on post_tool_use, appending {ts, tool, bytes, measurable} to agents/runtime/state/tool-result-census.jsonl",
75
+ "privacy": "census lines carry a tool NAME, a byte COUNT and a timestamp - the record type has no field able to hold file contents, command output or any free text, so there is no scrubber that can fail",
76
+ "threshold": "none committed before data - baseline first, threshold at review date. A `measurable: false` line means the host's payload carried no readable result; that absence is itself a coverage finding and is never counted as zero bytes"
77
+ },
65
78
  "turns_per_task": {
66
79
  "definition": "user_prompt_submit count keyed to the task envelope - the invisible multiplier (every turn re-pays the accumulated context at read rates).",
67
80
  "instrument": "session-register / chat-history turn counters; HONEST GAP: no task-envelope key exists yet in those counters - the number is per-SESSION until a task key ships; registered with that stated limitation rather than silently redefined",
@@ -35,6 +35,34 @@
35
35
  "p95": 50000,
36
36
  "_comment": "The roadmap's candidate numbers, kept as the destination. Reaching them needs reductions this package has not built yet; scope-dedup (C-3) removes a redundant COPY at the consumer, it does not shrink the suite itself."
37
37
  },
38
+ "target_schedule": {
39
+ "_comment": "road-to-inbox-harvest-2026-08-d-context-ledger Step 5.3. The target above was a destination with no route: the ratchet fails on GROWTH only, so the corpus can drift away from 40k indefinitely without any gate objecting. These milestones give the destination a schedule. They are DATES WITH NUMBERS, not a derivation - no measurement in this tree says the estate can reach 85k by a given day, and pretending otherwise would be the invented-threshold failure this file's own baseline_history is careful to avoid. What they buy is that a miss becomes a published fact with an owner instead of a silent drift.",
40
+ "owner": "maintainer",
41
+ "measured_at_registration": {
42
+ "tokens": 125593,
43
+ "at": "2026-08-15",
44
+ "note": "MEASURED total at registration time, from check_preamble_payload_budget. It is ~23k ABOVE both baseline_tokens (102520) and the derived ceiling, i.e. the gate is red on the tree as it stands. The baseline is deliberately NOT raised to meet it - raising a ratchet baseline to clear a failing check is the config-weakening move this repo blocks by construction. It is recorded here so the milestones below are planned against the real starting point rather than a stale one. Per-rule ranking: agents/evidence/analysis/rule-paths-coverage-census.md"
45
+ },
46
+ "milestones": [
47
+ {
48
+ "by": "2026-11-10",
49
+ "tokens": 102520,
50
+ "why": "Return to the registered baseline. This is a RECOVERY milestone, not progress toward 40k - it undoes drift rather than shrinking the estate. The date is the review_by both budget files already carry, so it needs no new calendar."
51
+ },
52
+ {
53
+ "by": "2027-02-10",
54
+ "tokens": 85000,
55
+ "why": "First real reduction. The rule-paths census shows 92.1% of rule weight sits in the auto tier and the top 20 files carry 35.5% of it, so a scoped tranche is the plausible mechanism - GATED on the paths-scoping consumer decision, which is a human call and may resolve against scoping entirely."
56
+ },
57
+ {
58
+ "by": "2027-05-10",
59
+ "tokens": 60000,
60
+ "why": "Second reduction. No mechanism is committed for this one; it exists so the destination keeps a date after the first tranche lands, and it is the milestone most likely to be revised on evidence."
61
+ }
62
+ ],
63
+ "on_miss": "A missed milestone is PUBLISHED - recorded here with the measured number and the reason - and never silently re-dated. Two consecutive misses mean the schedule was wrong or the destination is unreachable with the mechanisms available; either outcome is a decision to surface, not a date to move again.",
64
+ "revisit_if": "The paths-scoping decision resolves against scoping (removing the only named mechanism for the 85k milestone), or a measured reduction lands ahead of schedule and the remaining milestones are re-derived from it."
65
+ },
38
66
  "reference_measurements": {
39
67
  "measured_cold_start_median_tokens": 230930,
40
68
  "c3_scope_dedup_reduction_pct": 38,
@@ -250,6 +250,11 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
250
250
  router + projection freshness, host bridge status.
251
251
  Flags: --platform <p>, --workspace <path>, --json, --strict,
252
252
  --no-freshness
253
+ workspace:doctor One answer to "where am I": repo root, main worktree, current
254
+ worktree, branch, PR base — each with its provenance — plus
255
+ session claim, conflicting records and worktree pressure.
256
+ Read-only; no disposal path by design.
257
+ Flags: --from <dir>, --json, --strict
253
258
  route:explain Deterministic rule-routing trace for one prompt: matched
254
259
  triggers, tier, injected-vs-pointer disposition, budget
255
260
  consumption, rejected candidates. Trigger-match level only
@@ -899,6 +904,10 @@ cmd_routing_doctor() {
899
904
  exec_hook "src/scripts/routing_doctor" "$@"
900
905
  }
901
906
 
907
+ cmd_workspace_doctor() {
908
+ exec_hook "src/scripts/workspace_doctor" "$@"
909
+ }
910
+
902
911
  cmd_hooks_replay() {
903
912
  exec_hook "src/scripts/hooks/replay_hook" "$@"
904
913
  }
@@ -1422,6 +1431,7 @@ main() {
1422
1431
  hooks:status) cmd_hooks_status "$@" ;;
1423
1432
  hooks:doctor) cmd_hooks_doctor "$@" ;;
1424
1433
  routing:doctor) cmd_routing_doctor "$@" ;;
1434
+ workspace:doctor) cmd_workspace_doctor "$@" ;;
1425
1435
  route:explain) cmd_route_explain "$@" ;;
1426
1436
  route:audit) cmd_route_audit "$@" ;;
1427
1437
  hooks:replay) cmd_hooks_replay "$@" ;;