@event4u/agent-config 9.32.0 → 9.33.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 (76) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +96 -1
  3. package/dist/agent-src/commands/review/routing.md +3 -4
  4. package/dist/agent-src/contexts/communication/rules-auto/reply-close-mechanics.md +65 -0
  5. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +3 -0
  6. package/dist/agent-src/rules/delegation-policy.md +6 -0
  7. package/dist/agent-src/scripts/roadmap_gates.ts +107 -3
  8. package/dist/agent-src/skills/docker/SKILL.md +11 -0
  9. package/dist/agent-src/skills/laravel/SKILL.md +11 -0
  10. package/dist/agent-src/skills/review-routing/SKILL.md +23 -6
  11. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  12. package/dist/agent-src/templates/scripts/ownership-map.example.yml +1 -1
  13. package/dist/cli/registry.js +4 -4
  14. package/dist/cli/registry.js.map +1 -1
  15. package/dist/cli-delegate/chunk-HTIU3W56.js +66 -0
  16. package/dist/cli-delegate/{chunk-ITIQSGOQ.js → chunk-MA4GFWDY.js} +7 -4
  17. package/dist/cli-delegate/{chunk-542DZR4P.js → chunk-PMQ6Z3T3.js} +1651 -144
  18. package/dist/cli-delegate/{chunk-PFBV6XRT.js → chunk-Q26HT4QT.js} +1 -63
  19. package/dist/cli-delegate/{chunk-LVRYBKTI.js → chunk-YPAWXXXR.js} +158 -0
  20. package/dist/cli-delegate/cmd_conformance.js +256 -226
  21. package/dist/cli-delegate/cmd_doctor.js +3 -2
  22. package/dist/cli-delegate/cmd_export.js +1 -1
  23. package/dist/cli-delegate/cmd_handoff.js +19 -26
  24. package/dist/cli-delegate/cmd_prune.js +1 -1
  25. package/dist/cli-delegate/cmd_session_recycle.js +13 -3
  26. package/dist/cli-delegate/cmd_settings_get.js +4 -2
  27. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  28. package/dist/cli-delegate/cmd_settings_set.js +5 -3
  29. package/dist/cli-delegate/cmd_sync.js +1 -1
  30. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  31. package/dist/cli-delegate/cmd_validate.js +1 -1
  32. package/dist/discovery/deprecation-report.md +1 -1
  33. package/dist/discovery/discovery-manifest.json +8 -8
  34. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  35. package/dist/discovery/discovery-manifest.summary.md +1 -1
  36. package/dist/discovery/orphan-report.md +1 -1
  37. package/dist/discovery/packs.json +2 -2
  38. package/dist/discovery/trust-report.md +1 -1
  39. package/dist/discovery/workspaces.json +2 -2
  40. package/dist/hooks/dispatch.js +18 -0
  41. package/dist/install/install.mjs +7 -4
  42. package/dist/mcp/registry-manifest.json +2 -2
  43. package/dist/mcp/server.json +2 -2
  44. package/dist/mcp/server.mjs +3637 -1158
  45. package/package.json +1 -1
  46. package/src/agent-src/scripts/roadmap_gates.ts +107 -3
  47. package/src/agent-src/templates/scripts/ownership-map.example.yml +1 -1
  48. package/src/config/cost-parity-budget.json +167 -0
  49. package/src/config/gate-coverage.yml +44 -5
  50. package/src/config/quorum-attendance-budget.json +39 -0
  51. package/src/scripts/_cli/cmd_doctor.ts +69 -1
  52. package/src/scripts/_cli/cmd_session_recycle.ts +19 -3
  53. package/src/scripts/_dispatch.bash +5 -3
  54. package/src/scripts/_lib/orchestration_record.ts +45 -0
  55. package/src/scripts/ai_council/clients.ts +31 -0
  56. package/src/scripts/ai_council/events_log.ts +128 -6
  57. package/src/scripts/ai_council/orchestrator.ts +12 -3
  58. package/src/scripts/ai_council/pricing.ts +48 -1
  59. package/src/scripts/ai_council/quorum.ts +20 -0
  60. package/src/scripts/ai_council/session.ts +11 -0
  61. package/src/scripts/ask_transport.ts +20 -1
  62. package/src/scripts/cache_realization_report.ts +38 -0
  63. package/src/scripts/check_release_highlights.ts +7 -1
  64. package/src/scripts/conformance_scan.ts +105 -0
  65. package/src/scripts/cost/track.mjs +39 -0
  66. package/src/scripts/cost_summary.ts +104 -1
  67. package/src/scripts/council_cli.ts +131 -13
  68. package/src/scripts/dispatch_economy_report.ts +5 -2
  69. package/src/scripts/install.ts +20 -6
  70. package/src/scripts/lint_budget_ownership.ts +81 -0
  71. package/src/scripts/lint_skill_frontmatter_safety.ts +55 -1
  72. package/src/scripts/lint_workflow_paths.ts +384 -0
  73. package/src/scripts/preamble_byte_census.ts +9 -1
  74. package/src/scripts/report_carrier_divergence.ts +10 -0
  75. package/src/scripts/report_conformance_funnel.ts +40 -0
  76. package/src/scripts/schemas/subagent.schema.json +3 -2
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": "9.32.0",
4
+ "version": "9.33.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,
@@ -18,10 +18,34 @@
18
18
  * Blocking a pipeline on "the user has an open decision" would turn a
19
19
  * visibility aid into a second thing to fight.
20
20
  *
21
+ * `--reply` is the same projection shaped for the END OF A CHAT REPLY rather
22
+ * than a terminal pane, and it exists because the pull channel above is only
23
+ * read by someone who already knows to ask. Transcript forensics over three
24
+ * consumer sessions found five "what do I do now?" follow-ups, every one of
25
+ * them next to a blocker the agent HAD reported — as a file reference, as a
26
+ * bare option count, or as a choice of substitute work. What the same corpus
27
+ * also shows is that a tool's output gets carried into the reply verbatim
28
+ * (the dashboard's step count appears word-for-word inside eight option
29
+ * blocks). So the fix is not to make the agent format better from memory; it
30
+ * is to hand it the finished text. See ADR-222.
31
+ *
32
+ * Two properties carry the contract mechanically:
33
+ * - Nothing owned by the user open ⇒ EMPTY output. "No blocker → no block"
34
+ * stops being a judgement call, so the command is safe to call
35
+ * unconditionally at reply-close.
36
+ * - Exactly ONE blocker is rendered in full — the one that unblocks most.
37
+ * Every other one is a single trailing line. The user is never handed a
38
+ * flat list to weigh themselves.
39
+ *
40
+ * Labels are English like the rest of the CLI surface; the agent mirrors them
41
+ * into the user's language when it lifts them into a reply, exactly as it does
42
+ * for `Recommendation:` / `Empfehlung:`.
43
+ *
21
44
  * Invocation (from project root):
22
45
  * ./agent-config gates # decisions owned by the user
23
46
  * ./agent-config gates --all # every open blocker, grouped by owner
24
47
  * ./agent-config gates --json # machine-readable
48
+ * ./agent-config gates --reply # reply-close form; empty when none
25
49
  */
26
50
 
27
51
  import * as path from 'node:path';
@@ -209,6 +233,76 @@ function render(entries: readonly Entry[], all: boolean): string {
209
233
  return lines.join('\n') + '\n';
210
234
  }
211
235
 
236
+ /**
237
+ * Reply-close form: the one blocking decision in full, the rest as one line.
238
+ *
239
+ * Deliberately narrower than `render()`. `Status` and `Owner` are roadmap-file
240
+ * metadata that a reader of a reply cannot act on, so they are dropped; what
241
+ * survives is what the user has to DO and how they will know it is done. The
242
+ * five-field shape stays where it is enforced — in the roadmap file — and does
243
+ * not follow the blocker into prose it would only pad.
244
+ *
245
+ * Returns '' when nothing is owned by the user, so the caller can append the
246
+ * result unconditionally and get silence when silence is correct.
247
+ */
248
+ function renderReply(entries: readonly Entry[]): string {
249
+ const mine = entries.filter((e) => needsUser(e.blocker.owner));
250
+ if (mine.length === 0) {
251
+ return '';
252
+ }
253
+
254
+ // `collectEntries` already sorted by unblocking weight, so the first entry
255
+ // IS the one that matters most. Picking it here rather than asking the
256
+ // caller to choose is the point: a flat list is the failure mode.
257
+ const lead = mine[0] as Entry;
258
+ const rest = mine.slice(1);
259
+ const lines: string[] = [];
260
+
261
+ lines.push(`Blocked: ${lead.blocker.blocks} — ${lead.roadmapRel}`);
262
+
263
+ const steps = regroupTodo(lead.blocker.todo);
264
+ const doLabel = isLegacy(lead.blocker) ? 'Blocked until:' : 'Do:';
265
+ if (steps.length === 0) {
266
+ lines.push(
267
+ `${doLabel} (no steps recorded — the blocker entry needs a **What to do:** list)`,
268
+ );
269
+ } else if (steps.length === 1) {
270
+ lines.push(...wrap(`${doLabel} ${steps[0] as string}`, WIDTH, ' '));
271
+ } else {
272
+ // Emitted as authored, never re-numbered: real blocker entries already
273
+ // carry their own `1.` / `2.` markers (the CI-enforced `What to do:`
274
+ // shape), so adding our own would produce "1. 1. …". An entry that
275
+ // arrives unnumbered stays unnumbered — that is a fact about the
276
+ // roadmap entry, and papering over it here would hide it from the
277
+ // person who could fix it.
278
+ lines.push(`${doLabel}`);
279
+ for (const s of steps) {
280
+ lines.push(...wrap(` ${s}`, WIDTH, ' '));
281
+ }
282
+ }
283
+ lines.push(...wrap(`Done when: ${lead.blocker.resolvedWhen}`, WIDTH, ' '));
284
+
285
+ if (rest.length > 0) {
286
+ // A count, not a roster. Naming all of them reproduces the exact
287
+ // failure this form exists to end — "zwölf Entscheidungen bei Dir …
288
+ // rund 850 Zeilen", which the transcript shows produced no decision at
289
+ // all. The one that blocks is written out above; the rest are one
290
+ // command away, and saying so is shorter than listing them and more
291
+ // honest than hiding them.
292
+ lines.push('');
293
+ lines.push(
294
+ ...wrap(
295
+ `${rest.length} other decision${rest.length !== 1 ? 's' : ''} also wait${rest.length === 1 ? 's' : ''} on you, ` +
296
+ 'none blocking this — `agent-config gates`.',
297
+ WIDTH,
298
+ ' ',
299
+ ),
300
+ );
301
+ }
302
+
303
+ return lines.join('\n') + '\n';
304
+ }
305
+
212
306
  function renderJson(entries: readonly Entry[], all: boolean): string {
213
307
  const pick = all ? entries : entries.filter((e) => needsUser(e.blocker.owner));
214
308
  return (
@@ -270,18 +364,28 @@ function main(argv?: readonly string[]): number {
270
364
  const args = argv ?? process.argv.slice(2);
271
365
  const all = args.includes('--all');
272
366
  const json = args.includes('--json');
367
+ const reply = args.includes('--reply');
273
368
 
274
369
  const repoRoot = _resolveRepoRoot(process.cwd());
275
370
  const roadmapRoot = path.join(repoRoot, 'agents', 'roadmaps');
276
371
  if (!_isDir(roadmapRoot)) {
372
+ // `--reply` stays silent here too: a project without roadmaps has no
373
+ // blocker to hand over, and a reply-close line saying so would be the
374
+ // ceremony the form exists to avoid.
277
375
  process.stdout.write(
278
- json ? '{"needsYou":0,"other":0,"blockers":[]}\n' : 'No roadmaps directory — nothing to report.\n',
376
+ reply
377
+ ? ''
378
+ : json
379
+ ? '{"needsYou":0,"other":0,"blockers":[]}\n'
380
+ : 'No roadmaps directory — nothing to report.\n',
279
381
  );
280
382
  return 0;
281
383
  }
282
384
 
283
385
  const entries = collectEntries(roadmapRoot);
284
- process.stdout.write(json ? renderJson(entries, all) : render(entries, all));
386
+ process.stdout.write(
387
+ reply ? renderReply(entries) : json ? renderJson(entries, all) : render(entries, all),
388
+ );
285
389
  return 0;
286
390
  }
287
391
 
@@ -308,5 +412,5 @@ if (_isCliEntry()) {
308
412
  process.exitCode = main();
309
413
  }
310
414
 
311
- export { main, needsUser, regroupTodo, wrap, collectEntries, render, renderJson };
415
+ export { main, needsUser, regroupTodo, wrap, collectEntries, render, renderJson, renderReply };
312
416
  export type { Entry };
@@ -3,7 +3,7 @@
3
3
  # Copy this file to `.github/ownership-map.yml` (preferred) or
4
4
  # `agents/ownership-map.yml` and adapt the entries to your stack.
5
5
  # Omitting the file entirely is fine — routing falls back to the
6
- # generic role vocabulary from `reviewer-awareness`.
6
+ # common role vocabulary in `skill:review-routing`.
7
7
  #
8
8
  # First match wins per file. List narrower globs before broader ones.
9
9
 
@@ -0,0 +1,167 @@
1
+ {
2
+ "_comment": "Committed target table for the road-to-cost-parity-* family (part 0, Phase 1). Registers what a session costs today, what the family is steering toward, and what the rule payload actually is PER HOST — the correction that matters most, because every diet target in the source drafts was set against one aggregate number that describes no host. Nothing here changes behaviour: no carrier ships for these thresholds (part 0 Phase 4.5), and no gate reads the target columns. What IS gated is the file's own shape — lint_budget_ownership rejects a row without a `source`, and rejects a `revised_from` without `revision_evidence`, so a number cannot enter or move without a traceable origin.",
3
+ "schema_version": 1,
4
+ "registered_at": "2026-08-10",
5
+ "owner": "maintainer",
6
+ "review_by": "2026-11-10",
7
+ "row_schema": {
8
+ "_comment": "Reading of part 0 § 1.2b: `schema_version` / `registered_at` / `owner` / `review_by` are FILE-level (as in every sibling budget file — a per-row copy would be four constants repeated N times), and every ROW carries `source` plus the three honest-null fields from § 1.3. The validator enforces exactly that split.",
9
+ "required_row_fields": ["id", "unit", "baseline", "source", "revisable", "revision_evidence", "revised_from"],
10
+ "honest_null_contract": "A target bends to a measurement, never to a preference. `revisable: true` says the number is revisable at all; `revised_from` records what it used to be once it moves; `revision_evidence` must then point at the evidence note that forced the move. The validator fails the file when `revised_from` is set and `revision_evidence` is empty — a revision without published evidence is the one form of drift this schema exists to refuse."
11
+ },
12
+ "measurement": {
13
+ "_comment": "Both measurement rules from part 0's Context are fields here, not prose: a rule that lives only in a paragraph is a rule the next author does not inherit.",
14
+ "measurement_date": "2026-08-10",
15
+ "commit": "c073d5732fb890bbf8b41ec879f62f651c7ba293",
16
+ "projection_regenerated": true,
17
+ "regeneration_command": "task sync && task generate-tools — verified no-op afterwards (git status clean), so the payload rows below measure the commit and not when someone last ran a generator",
18
+ "emitter_generation": "real, frontmatter-less projection files (25 of 110 .claude/rules entries carry a `paths:` block; the rest carry no frontmatter). NOT the older emitter that symlinked .claude/rules/* into dist/agent-src/rules/ and so inherited dist's full frontmatter. This is the THIRD variable behind the 92-vs-110 file-count spread: a projection count is a fact about which emitter last wrote the tree, on top of how stale that tree is.",
19
+ "carrier_divergence_admitted": false,
20
+ "carrier_divergence_reason": "report_carrier_divergence compares a globally installed carrier against the project projection on disk; both can be stale independently, so it measures relative cache staleness rather than any property of the source. It may diagnose a behind-the-times install; it may never gate a phase, and no row here is derived from it."
21
+ },
22
+ "session_cost_targets": [
23
+ {
24
+ "id": "median_final_context_tokens",
25
+ "unit": "billable input tokens of the last main-chain assistant record (per cc_transcript.ts)",
26
+ "baseline": 519349,
27
+ "interim": 300000,
28
+ "target": 200000,
29
+ "direction": "lower_is_better",
30
+ "source": "agents/evidence/analysis/token-economy-recycling-phase1.md:45 — median over 201 sessions with parseable final usage (:44) out of 205 non-empty (:10)",
31
+ "revisable": true,
32
+ "revision_evidence": "",
33
+ "revised_from": null
34
+ },
35
+ {
36
+ "id": "p90_final_context_tokens",
37
+ "unit": "billable input tokens of the last main-chain assistant record",
38
+ "baseline": 807937,
39
+ "interim": 550000,
40
+ "target": 400000,
41
+ "direction": "lower_is_better",
42
+ "source": "agents/evidence/analysis/token-economy-recycling-phase1.md:45 — same 201-session corpus",
43
+ "revisable": true,
44
+ "revision_evidence": "",
45
+ "revised_from": null
46
+ },
47
+ {
48
+ "id": "auto_compact_incidence",
49
+ "unit": "share of sessions with >= 1 auto-compaction event",
50
+ "baseline": 0.112,
51
+ "interim": 0.05,
52
+ "target": 0.0,
53
+ "direction": "lower_is_better",
54
+ "source": "agents/evidence/analysis/token-economy-recycling-phase1.md:59 — 23 of 205 sessions, 31 events (:60), all trigger:\"auto\"",
55
+ "revisable": true,
56
+ "revision_evidence": "",
57
+ "revised_from": null
58
+ },
59
+ {
60
+ "id": "late_early_per_call_cost_ratio",
61
+ "unit": "median billable input per call over the last 20 calls / the first 20 calls of a session",
62
+ "baseline": 2.1,
63
+ "interim": 1.6,
64
+ "target": 1.3,
65
+ "direction": "lower_is_better",
66
+ "source": "agents/evidence/analysis/token-economy-recycling-phase1.md:69-75 — 145 sessions with >= 200 main-chain calls; median early 279,859 vs late 594,985; p90 ratio 3.3x",
67
+ "revisable": true,
68
+ "revision_evidence": "",
69
+ "revised_from": null
70
+ }
71
+ ],
72
+ "payload_rows": [
73
+ {
74
+ "_comment": "Per host, never one number. The aggregate the source drafts dieted against (\"419 KB always-loaded layer\") describes no host that exists: it is the whole rule tree, and only 9 of 116 rules are type: always.",
75
+ "id": "src_rules_maintained",
76
+ "unit": "bytes across src/rules/*.md",
77
+ "baseline": 424896,
78
+ "files": 116,
79
+ "target_bytes": null,
80
+ "target_owner": "none — this is the maintained source, not a delivered payload; it shrinks only as a consequence of a diet elsewhere",
81
+ "source": "measured 2026-08-10 at commit c073d5732, post-regeneration (see `measurement`)",
82
+ "revisable": true,
83
+ "revision_evidence": "",
84
+ "revised_from": null
85
+ },
86
+ {
87
+ "id": "dist_agent_src_rules",
88
+ "unit": "bytes across dist/agent-src/rules/*.md",
89
+ "baseline": 420051,
90
+ "files": 115,
91
+ "target_bytes": null,
92
+ "target_owner": "none — the tracked projection source. It is the ONLY rule-payload figure that is stable across checkouts, because it is committed; every other row below is regenerated and therefore machine-dependent.",
93
+ "source": "measured 2026-08-10 at commit c073d5732, post-regeneration",
94
+ "revisable": true,
95
+ "revision_evidence": "",
96
+ "revised_from": null
97
+ },
98
+ {
99
+ "id": "augment_rules_payload",
100
+ "unit": "bytes across .augment/rules/*.md",
101
+ "baseline": 420051,
102
+ "files": 115,
103
+ "target_bytes": null,
104
+ "target_owner": "src/config/budgets.yml § standing_rule_delivery (total_cap_tokens 110000) already owns the delivered-rule ceiling. This family does not open a second, byte-denominated target for the same surface — a metric with two owners is a metric with none.",
105
+ "source": "measured 2026-08-10 at commit c073d5732, post-regeneration; mirrors dist/ by symlink",
106
+ "revisable": true,
107
+ "revision_evidence": "",
108
+ "revised_from": null
109
+ },
110
+ {
111
+ "id": "claude_rules_payload",
112
+ "unit": "bytes across .claude/rules/*.md",
113
+ "baseline": 339035,
114
+ "files": 110,
115
+ "target_bytes": null,
116
+ "target_owner": "src/config/budgets.yml § standing_rule_delivery — same ceiling, same reason as the augment row",
117
+ "source": "measured 2026-08-10 at commit c073d5732, post-regeneration; 25 of 110 files carry a `paths:` block, the rest none",
118
+ "revisable": true,
119
+ "revision_evidence": "",
120
+ "revised_from": null
121
+ },
122
+ {
123
+ "_comment": "CORRECTION to part 0's triage table, which recorded these two as \"~3 KB / ~0 KB\" and therefore as already-lean hosts needing no target. Re-measured post-regeneration they are 408 KB and 330 KB — a full payload each. The earlier reading is a stale-projection artefact of exactly the class measurement rule 1 warns about: both carriers are UNTRACKED and written only by `task generate-tools`, so a checkout that has not run it reads them as near-empty. Risk 3's mitigation (\"the lean hosts need no target\") does not survive the re-measure and is not encoded here.",
124
+ "id": "clinerules_payload",
125
+ "unit": "bytes across .clinerules/*.md",
126
+ "baseline": 417703,
127
+ "files": 111,
128
+ "target_bytes": null,
129
+ "target_owner": "unowned — a diet for this host belongs to the parked rule-payload part of this family, not to part 0",
130
+ "source": "measured 2026-08-10 at commit c073d5732, post-regeneration; untracked, generated by task generate-tools",
131
+ "revisable": true,
132
+ "revision_evidence": "",
133
+ "revised_from": null
134
+ },
135
+ {
136
+ "id": "windsurfrules_payload",
137
+ "unit": "bytes of the single .windsurfrules file",
138
+ "baseline": 338160,
139
+ "files": 1,
140
+ "target_bytes": null,
141
+ "target_owner": "unowned — see the clinerules row; same correction, same parked owner",
142
+ "source": "measured 2026-08-10 at commit c073d5732, post-regeneration; untracked, generated by task generate-tools",
143
+ "revisable": true,
144
+ "revision_evidence": "",
145
+ "revised_from": null
146
+ },
147
+ {
148
+ "id": "gemini_md_payload",
149
+ "unit": "bytes of GEMINI.md",
150
+ "baseline": 2982,
151
+ "files": 1,
152
+ "target_bytes": null,
153
+ "target_owner": "none — genuinely lean, and the only host in this table for which that is true. It is a pointer file, not a rule payload.",
154
+ "source": "measured 2026-08-10 at commit c073d5732, post-regeneration",
155
+ "revisable": true,
156
+ "revision_evidence": "",
157
+ "revised_from": null
158
+ }
159
+ ],
160
+ "anchors": {
161
+ "premium_band_tokens": 200000,
162
+ "rationale": "Above ~200k a session sits in the premium pricing band, which is why the median target is set there rather than at a round fraction of the baseline.",
163
+ "known_noise_problem": "187 of 205 observed sessions end at or above 200k, so a session-end hint at this threshold would fire on ~91% of sessions — a constant line, not an advisory. The 200k figure is a TARGET here and nothing else.",
164
+ "no_carrier_ships": "Part 0 § 4.5: no session-end advisory ships for this anchor. The existing 800k recycle threshold (src/config/recycle-threshold-budget.json) stays the only strong line, and any carrier for 200k is a separate, separately-argued change.",
165
+ "source": "agents/evidence/analysis/token-economy-recycling-phase1.md — session-final distribution (:45) over the 205-session corpus (:10)"
166
+ }
167
+ }
@@ -40,13 +40,16 @@
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-05)
43
+ # COVERAGE — THE HONEST DENOMINATOR (re-measured 2026-08-10)
44
44
  # ----------------------------------------------------------
45
- # 240 `lint_*` / `check_*` / `audit_*` scripts live under `src/scripts/`
45
+ # 251 `lint_*` / `check_*` / `audit_*` scripts live under `src/scripts/`
46
46
  # (the count is computed, not asserted — `check_gate_coverage.count_gate_scripts`).
47
- # **32 of them emit the machine-readable `scanned: <N>` line under their
48
- # CI-identical argv, and all 31 are listed below.** The manifest is therefore
47
+ # **33 of them emit the machine-readable `scanned: <N>` line under their
48
+ # CI-identical argv, and all 33 are listed below.** The manifest is therefore
49
49
  # complete with respect to its own contract, and covers ~13 % of the population.
50
+ # (The 2026-08-05 reading said "32 … and all 31 are listed": the two halves of
51
+ # one sentence disagreed by one, which is the drift this paragraph exists to
52
+ # make visible. Both are recounted above, not adjusted toward each other.)
50
53
  #
51
54
  # Two revisions of this header have now gone stale the same way. It claimed
52
55
  # "8 of 211", then "22 of 223" while 26 entries were listed. Both halves drift
@@ -98,7 +101,7 @@
98
101
  # which is the shape this repo tells a gate author to use — writes the line from
99
102
  # inside the library, so its own source never matches and the test cannot see it.
100
103
  # Live split: **1 unregistered gate emits the inline shape (`check_gate_coverage`
101
- # itself), and 175 of 244 gate scripts emit through the helper.** So for the
104
+ # itself), and 205 of 251 gate scripts emit through the helper.** So for the
102
105
  # recommended shape the sentence above is false, and the list CAN fall behind.
103
106
  #
104
107
  # Not widened here, deliberately. Widening the regex to include the helper turns
@@ -557,6 +560,42 @@ gates:
557
560
  untouched original stayed clean. Floor at 450 against a live 503 leaves
558
561
  room for skill retirement without redding the change that retires one.
559
562
 
563
+ - id: lint_workflow_paths
564
+ argv: []
565
+ min_scanned: 20
566
+ corpus: "every workflow under .github/workflows (28 at baseline); the unit is the file, and each file's `paths:` / `paths-ignore:` entries are resolved against `git ls-files`"
567
+ status: enforced
568
+ canary:
569
+ class: undetected-antipattern
570
+ path: .github/workflows/zz-canary-workflow-paths.yml
571
+ content: |
572
+ name: Canary
573
+ on:
574
+ pull_request:
575
+ paths:
576
+ - "retired/tree/that/never/existed/**"
577
+ jobs:
578
+ noop:
579
+ runs-on: ubuntu-latest
580
+ steps:
581
+ - run: echo canary
582
+ note: >
583
+ Registered STRICT on day one for the same reason `lint_example_fences`
584
+ was, and on the same evidence standard: the classification pass ran
585
+ BEFORE the gate existed and returned **20 dead entries across 6 workflow
586
+ files**, each classified by hand into one of four causes — a tree retired
587
+ by ADR-051, a gitignored projection that can never appear in a diff, two
588
+ paths moved by the Python-to-TS migration, and bare root paths whose real
589
+ files sit under `src/`. All 20 were repaired in the change that added the
590
+ gate, so the corpus is verified empty rather than unexamined, and an
591
+ advisory gate over it could only ever be ignored. Floor at 20 against a
592
+ live 28 so retiring a workflow does not red the change that retires it.
593
+ Worth recording that the highest-severity hit was invisible to the hand
594
+ census and only the gate found it: `release-validation.yml` filtered on a
595
+ root `marketplace.json` that has never existed, so a version drift in the
596
+ real manifest did not re-trigger the `version-consistency` job whose
597
+ entire purpose is catching that drift.
598
+
560
599
  - id: check_depth_budget
561
600
  argv: []
562
601
  min_scanned: 140
@@ -0,0 +1,39 @@
1
+ {
2
+ "_comment": "Pre-registered council ATTENDANCE metrics (road-to-inbox-harvest-2026-08-b Phase 1.4). Registered here rather than in roadmap prose because a roadmap is archived when it closes and a metric outlives it — the sibling budget files (hook-token-budget.json, recycle-threshold-budget.json) already carry the owner / review_by / honest-gap discipline this reuses. The instrument is the quorum_result line appendQuorumEvent writes to agents/runtime/council/events.log (schema v2); nothing here is measured yet, and no threshold is committed before data exists.",
3
+ "schema_version": 1,
4
+ "registered_at": "2026-08-10",
5
+ "owner": "maintainer",
6
+ "review_by": "2026-11-10",
7
+ "unit": "quorum_result event lines in agents/runtime/council/events.log",
8
+ "privacy": "a quorum_result line carries {phase, command, dispatch, verdict, threshold, configured_total, total, present, solo, absent[{member, reason}]} — counts, closed vocabularies, and member ids only. The type has no field able to hold free-form content, so the CLI's own `detail` string (provider error text, which can embed paths and prompt fragments) is dropped at the boundary rather than scrubbed.",
9
+ "phase_split": "A pass emits `pre_run` (build_members — who could be constructed) and, on the `run` path, `post_run` (_postRunQuorum — who actually returned a usable response). They disagree whenever a member constructs and then fails mid-flight, so a rate computed over both double-counts. A `pre_run` line is NOT evidence a pass ran: `command: estimate` is a spend-free preview, and a `run` / `debate` line is written before the `--confirm` gate (which defaults to false and returns before any provider call) and before the necessity and size-fit gates. A `post_run` line is the only line that proves providers were called — which is why every rate below is post_run-scoped, and why `command` exists at all.",
10
+ "attendance_rate": {
11
+ "definition": "of the members that entered the pass, how many answered: present / total per post_run pass. Bounded by 1 by construction (evaluateQuorum clamps present into [0, total]).",
12
+ "instrument": "quorum_result lines, phase=post_run, command=run, fields present + total",
13
+ "threshold": "none committed before data — baseline first, threshold at review date"
14
+ },
15
+ "roster_shortfall_rate": {
16
+ "definition": "how often the council shrank BEFORE the pass: share of post_run passes with total < configured_total, plus the size of the gap. This is the half attendance_rate cannot see — a pass with 3 configured, 2 failing to construct and 1 answering reads 1/1 = 100 % attendance and a shortfall of 2. Both numbers are needed and neither substitutes for the other; a single ratio over mixed rosters is the error this pair replaces.",
17
+ "instrument": "quorum_result lines, phase=post_run, command=run, configured_total vs total",
18
+ "threshold": "none committed before data — baseline first, threshold at review date",
19
+ "not_an_error": "total > configured_total is legitimate and not a shortfall: `--siblings` fans ONE enabled config entry into N clients, so the client roster can exceed the entry count. configured_total counts enabled config ENTRIES; total counts CLIENTS. Compare them for direction, never divide one by the other."
20
+ },
21
+ "solo_conclusion_rate": {
22
+ "definition": "share of post_run passes with `solo: true` — written by the isSoloConcluded predicate in quorum.ts, never re-derived from the numbers here, so one change to the rule cannot leave this definition stale. ceil(n/2) makes 1-of-2 a legitimate conclusion; this measures how often that legitimacy is used, not whether it should be. THREE cases carry solo: true and they are not the same finding — a degraded multi-member council (configured_total > 1), a council configured with one member (configured_total = 1, dispatch = full), and a deliberate --single dispatch (dispatch = single). Split on dispatch and configured_total before reading the rate.",
23
+ "instrument": "quorum_result lines, phase=post_run, command=run, field solo, split by dispatch + configured_total",
24
+ "threshold": "none committed before data. The quorum-solo-floor blocker pre-registers the three outcomes this rate decides between — a third CLI member, a gate-scoped min_present: 2, or a published null under 5 % — and that choice is made against real data, never ahead of it. It is decided on the degraded case only; a --single pass is the operator asking for one voice, not the council failing to assemble."
25
+ },
26
+ "absent_reason_distribution": {
27
+ "definition": "count per reason over absent[] entries — no_binary / no_auth / timeout / quota (AbsentReason) plus the two runtime fallbacks the CLI writes directly, `unavailable` (transport resolved to nothing) and `binary_missing` (a CLI client refused to construct). Distinguishes a council degraded by configuration from one degraded by provider behaviour, which have opposite fixes.",
28
+ "instrument": "quorum_result lines, absent[].reason, split by phase (a pre_run absence is configuration; a post_run absence is mid-flight). Excluding `command: estimate` is NOT sufficient: `run` and `debate` also reach construction and return at the `--confirm` gate, which defaults to false. The reliable filter is a pre_run line whose pass produced a matching post_run line; a pre_run line with no post_run partner reached construction and stopped there, whatever its command says.",
29
+ "threshold": "none committed before data — the distribution IS the finding at review date",
30
+ "merge_before_reading": "`no_binary` and `binary_missing` are two spellings of one condition — the CLI binary is unusable — written by different code paths (transport_resolver's static classification vs the CLI client's construction failure). They are recorded separately because that is what the tree emits, not because they are separate findings; merge them before reading the distribution, or the largest bucket is split in two."
31
+ },
32
+ "honest_null_consequence": "If the log accumulates and the solo-conclusion rate sits near zero, that is the published answer to the quorum-solo-floor blocker and 1.5 is cancelled against it — not quietly dropped. If attendance data never accumulates at all (nobody runs a metered council pass in the window), that is reported as an unmeasured metric at the review date, never as a passing one.",
33
+ "honest_gaps": [
34
+ "`lens` and `invocation` are empty on every pre_run line — neither is known at member-construction time — so any per-lens breakdown is post_run-only.",
35
+ "`command: debate` carries the pre_run half ONLY. There is no _postRunQuorum on the debate path, and defining `usable response` for a multi-round debate is a separate question this registration does not answer. So debate contributes to absent_reason_distribution and to nothing else: attendance_rate and solo_conclusion_rate cover the `run` path, and a reader must not read them as covering every metered pass.",
36
+ "A pre_run line proves construction happened, never that a pass followed — the `--confirm` gate (default false), the necessity gate and the size-fit gate all abort afterwards and nothing on the line records it. Any count over pre_run lines is an upper bound; pair it with a post_run line before treating it as a pass.",
37
+ "The CLI emit wiring itself has no test. The two modules are covered directly, but nothing exercises build_members / cmd_run / cmd_debate end-to-end against a captured log — and `_emitQuorumEvent` forwards no AppendEventOptions, so there is no target to capture into. Two defects in this file's first version (a `dispatch` written from the flag rather than the outcome, and a rate mixing config entries with clients) were found by review rather than by a test, which is what that gap costs."
38
+ ]
39
+ }
@@ -90,6 +90,8 @@ import * as installed_tools from '../_lib/installed_tools.js';
90
90
  import * as user_global_paths from '../_lib/user_global_paths.js';
91
91
  import * as global_deploy_inventory from '../_lib/global_deploy_inventory.js';
92
92
  import { censusDuplicateScope } from '../_lib/duplicate_scope_census.js';
93
+ import * as preamble_byte_census from '../preamble_byte_census.js';
94
+ import * as dispatch_economy_report from '../dispatch_economy_report.js';
93
95
  import { git_common_dir } from '../_lib/git_common_dir.js';
94
96
  import * as sync_gitattributes from '../sync_gitattributes.js';
95
97
  import {
@@ -3244,6 +3246,7 @@ interface Options {
3244
3246
  check: string | null;
3245
3247
  trace_root: boolean;
3246
3248
  context: boolean;
3249
+ anatomy: boolean;
3247
3250
  repair: string | null;
3248
3251
  }
3249
3252
 
@@ -3254,13 +3257,15 @@ const PROG = 'agent-config doctor';
3254
3257
  // terminal width; golden tests assert the `usage:` token + exit code only.
3255
3258
  const USAGE =
3256
3259
  `usage: ${PROG} [-h] [--project PROJECT] [--json] [--ci] [--check ID]\n` +
3257
- ' [--trace-root] [--context] [--repair ID]\n';
3260
+ ' [--trace-root] [--context] [--anatomy]\n' +
3261
+ ' [--repair ID]\n';
3258
3262
 
3259
3263
  const _STORE_TRUE_FLAGS: Record<string, keyof Options> = {
3260
3264
  '--json': 'json',
3261
3265
  '--ci': 'ci',
3262
3266
  '--trace-root': 'trace_root',
3263
3267
  '--context': 'context',
3268
+ '--anatomy': 'anatomy',
3264
3269
  };
3265
3270
 
3266
3271
  const _VALUE_FLAGS: Record<string, keyof Options> = {
@@ -3283,6 +3288,7 @@ function _parse(argv: string[]): Options {
3283
3288
  check: null,
3284
3289
  trace_root: false,
3285
3290
  context: false,
3291
+ anatomy: false,
3286
3292
  repair: null,
3287
3293
  };
3288
3294
 
@@ -3408,6 +3414,65 @@ function _run_context(opts: Options): number {
3408
3414
  return 0;
3409
3415
  }
3410
3416
 
3417
+ /**
3418
+ * `--anatomy` — the injection anatomy, rendered from measurements that already
3419
+ * exist. Composes `preamble_byte_census` (what occupies the always-loaded
3420
+ * preamble) with `dispatch_economy_report` (what the dispatch legs cost). It
3421
+ * takes NO new measurement and adds no threshold: both sources are advisory
3422
+ * reports and stay advisory here.
3423
+ *
3424
+ * Neither module runs anything at import time — `preamble_byte_census` gained
3425
+ * the `__AGENT_CONFIG_BUNDLE__` guard for exactly this import, since inside the
3426
+ * `build:cli-delegate` bundle its old entry check would have matched whenever
3427
+ * this command was invoked directly.
3428
+ */
3429
+ function _run_anatomy(opts: Options): number {
3430
+ const census = preamble_byte_census;
3431
+ const economy = dispatch_economy_report;
3432
+
3433
+ const byteCensus = census.buildReport(census.parseArgs([]));
3434
+
3435
+ // The economy half reads transcripts and a registration file; either can be
3436
+ // absent on a consumer machine. An absent half is reported as absent, never
3437
+ // as a zero — a fabricated zero is the failure both source reports already
3438
+ // refuse to make.
3439
+ let economyReport: ReturnType<typeof economy.buildReport> | null = null;
3440
+ let economyError: string | null = null;
3441
+ try {
3442
+ economyReport = economy.buildReport({
3443
+ root: economy.DEFAULT_PROJECTS_ROOT,
3444
+ auditDir: path.join(process.cwd(), economy.DEFAULT_AUDIT_DIR),
3445
+ maxAgeDays: 14,
3446
+ });
3447
+ } catch (err) {
3448
+ economyError = err instanceof Error ? err.message : String(err);
3449
+ }
3450
+
3451
+ if (opts.json) {
3452
+ print(
3453
+ _jsonDumpsIndentAscii(
3454
+ {
3455
+ preamble_byte_census: byteCensus,
3456
+ dispatch_economy: economyReport,
3457
+ dispatch_economy_unavailable: economyError,
3458
+ } as unknown as Dict,
3459
+ 2,
3460
+ ),
3461
+ );
3462
+ return 0;
3463
+ }
3464
+
3465
+ print('── injection anatomy ─────────────────────────────────────────');
3466
+ print(census.renderText(byteCensus));
3467
+ print('');
3468
+ print(
3469
+ economyReport !== null
3470
+ ? economy.renderText(economyReport)
3471
+ : `dispatch-economy report unavailable: ${economyError}`,
3472
+ );
3473
+ return 0;
3474
+ }
3475
+
3411
3476
  function _run_repair(opts: Options): number {
3412
3477
  const target = opts.repair;
3413
3478
  if (target === 'wizard-state') {
@@ -3464,6 +3529,9 @@ function main(argv: string[] | null = null): number {
3464
3529
  if (opts.context) {
3465
3530
  return _run_context(opts);
3466
3531
  }
3532
+ if (opts.anatomy) {
3533
+ return _run_anatomy(opts);
3534
+ }
3467
3535
  let project_root: string;
3468
3536
  let origin: string;
3469
3537
  try {
@@ -73,6 +73,7 @@ interface ParsedArgv {
73
73
  message?: string;
74
74
  file?: string;
75
75
  template?: boolean;
76
+ verify?: boolean;
76
77
  }
77
78
 
78
79
  export function parseArgv(argv: readonly string[]): ParsedArgv {
@@ -81,6 +82,8 @@ export function parseArgv(argv: readonly string[]): ParsedArgv {
81
82
  const a = argv[i] as string;
82
83
  if (a === '--template') {
83
84
  parsed.template = true;
85
+ } else if (a === '--verify') {
86
+ parsed.verify = true;
84
87
  } else if (a === '--file') {
85
88
  const value = argv[i + 1];
86
89
  if (!value) return { ok: false, message: '--file requires a path' };
@@ -102,7 +105,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgv {
102
105
  */
103
106
  export function runSessionRecycle(
104
107
  input: string,
105
- opts: { cwd: string; now?: Date },
108
+ opts: { cwd: string; now?: Date; verify?: boolean },
106
109
  ): RecycleResult {
107
110
  const out: string[] = [];
108
111
  const err: string[] = [];
@@ -171,6 +174,15 @@ export function runSessionRecycle(
171
174
  };
172
175
  }
173
176
 
177
+ // `--verify` stops HERE: every rejection above has already run, and the
178
+ // only thing skipped is the write. Validating through a different path
179
+ // than the one that writes would make the check a second implementation
180
+ // to keep in sync — the failure this suite refuses elsewhere.
181
+ if (opts.verify === true) {
182
+ out.push(`recycle envelope VALID — ${bytes} bytes, not written (--verify).`);
183
+ return { code: 0, out, err };
184
+ }
185
+
174
186
  const target = path.join(projectRoot, RECYCLE_ENVELOPE_REL);
175
187
  try {
176
188
  atomic_write_json(target, envelope);
@@ -193,9 +205,10 @@ export function main(argv: string[] = process.argv.slice(2)): number {
193
205
  const parsed = parseArgv(argv);
194
206
  if (!parsed.ok) {
195
207
  const usage = [
196
- 'usage: agent-config session:recycle [--file <envelope.json>] [--template]',
208
+ 'usage: agent-config session:recycle [--file <envelope.json>] [--template] [--verify]',
197
209
  ' --file <path> read the envelope JSON from a file (default: stdin)',
198
210
  ' --template print a skeleton envelope and exit',
211
+ ' --verify validate only — same rejections, no write',
199
212
  ].join('\n');
200
213
  if (parsed.message === 'usage') {
201
214
  process.stdout.write(`${usage}\n`);
@@ -225,7 +238,10 @@ export function main(argv: string[] = process.argv.slice(2)): number {
225
238
  return 2;
226
239
  }
227
240
 
228
- const result = runSessionRecycle(input, { cwd: process.cwd() });
241
+ const result = runSessionRecycle(input, {
242
+ cwd: process.cwd(),
243
+ ...(parsed.verify === true ? { verify: true } : {}),
244
+ });
229
245
  for (const line of result.out) process.stdout.write(`${line}\n`);
230
246
  for (const line of result.err) process.stderr.write(`${line}\n`);
231
247
  return result.code;