@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
@@ -536,7 +536,7 @@ function die(msg: string, code = 2): never {
536
536
  throw new SystemExitError(code);
537
537
  }
538
538
 
539
- interface RunResult {
539
+ export interface RunResult {
540
540
  returncode: number;
541
541
  stdout: string;
542
542
  stderr: string;
@@ -879,14 +879,14 @@ function watch_pr_checks(branch: string): void {
879
879
  *
880
880
  * git's wording for this is stable across versions: the hint is `fetch first`
881
881
  * / `Updates were rejected`, and the ref line carries `[rejected]` with
882
- * `non-fast-forward` or `fetch first`.
882
+ * `non-fast-forward` or `fetch first`. A stale lease is a SECOND wording —
883
+ * `[remote rejected]` + `cannot lock ref … but expected`; pinned in the tests.
883
884
  */
884
885
  export function _is_non_fast_forward(stderr: string, stdout: string): boolean {
885
886
  const text = `${stderr}\n${stdout}`;
886
- return (
887
- /\[rejected\]/i.test(text) &&
888
- /(non-fast-forward|fetch first|behind its remote counterpart)/i.test(text)
889
- );
887
+ if (!/\[(?:remote )?rejected\]/i.test(text)) return false;
888
+ if (/cannot lock ref[^\n]*but expected/i.test(text)) return true;
889
+ return /(non-fast-forward|fetch first|behind its remote counterpart)/i.test(text);
890
890
  }
891
891
 
892
892
  function push_release_branch(branch: string): void {
@@ -1623,6 +1623,93 @@ function set_template_pin(p: string, version: string): void {
1623
1623
  fs.writeFileSync(p, lines.join('\n'), 'utf-8');
1624
1624
  }
1625
1625
 
1626
+ /**
1627
+ * Refuse a MAJOR cut that carries a scheduled deprecation due at or before it.
1628
+ *
1629
+ * **The trigger is the shape of the TARGET, not a comparison with the current
1630
+ * version.** A major target is `X.0.0` — and that one predicate covers every
1631
+ * path to a major: the `--as major` flag, an explicit `--version 13.0.0`,
1632
+ * auto-detection from a `feat!:` commit, AND `--resume`, where
1633
+ * `_detect_in_flight_target()` returns the already-bumped `package.json`
1634
+ * version so `target === current` and any current-vs-target comparison
1635
+ * silently returns. Resume was the fourth path an earlier version of this
1636
+ * guard missed while its own comment claimed three paths converged.
1637
+ *
1638
+ * Refusing a resumed release can strand a partially-completed one, and that is
1639
+ * the deliberate trade: a stranded release is recoverable by fixing the table
1640
+ * and resuming again, whereas a major shipped over a missed commitment is the
1641
+ * failure this whole surface exists to prevent.
1642
+ *
1643
+ * The target version is PASSED to the gate. Without it the gate falls back to
1644
+ * `package.json`, which at the cut to N still reads N-1 — so a row committed
1645
+ * to N reads as one major early and passes, and only rows already a major late
1646
+ * could ever be refused. That is the lateness being prevented, so measuring
1647
+ * against the shipped version would have made the refusal fire exactly one
1648
+ * major too late, forever.
1649
+ *
1650
+ * Runs under `--dry-run` too, unlike the rest of preflight: this check is one
1651
+ * subprocess reading two files, so the "keep a preview fast" rationale that
1652
+ * excludes the ~15s test-trend collection does not apply — and a preview that
1653
+ * reports green for the single condition that will refuse the real run is the
1654
+ * case an operator runs a preview to discover. It REPORTS there rather than
1655
+ * dying: `--dry-run` exiting 0 before `execute()` and before `preflight()` is a
1656
+ * contract this file's own tests assert, and an earlier revision of this guard
1657
+ * broke it by sitting above the dry-run branch with no preview mode.
1658
+ *
1659
+ * @param runner Seam for the gate invocation. Production passes nothing and
1660
+ * gets the real `run`; tests inject a stub, because the alternative — reaching
1661
+ * this branch only by mutating `docs/MIGRATION.md` — would make the refusal
1662
+ * path testable exclusively through a tracked-file edit.
1663
+ */
1664
+ export function assert_scheduled_deprecations_clear(
1665
+ target: string,
1666
+ runner: (args: readonly string[]) => RunResult = (args) => run(args, { check: false, capture: true }),
1667
+ opts: { previewOnly?: boolean } = {},
1668
+ ): void {
1669
+ const [, minor, patch] = parse_version(target);
1670
+ if (minor !== 0 || patch !== 0) {
1671
+ return;
1672
+ }
1673
+ const res = runner([
1674
+ './scripts-run',
1675
+ 'src/scripts/lint_scheduled_deprecations',
1676
+ '--cutting',
1677
+ target,
1678
+ ]);
1679
+ if (res.returncode === 0) {
1680
+ return;
1681
+ }
1682
+ process.stderr.write(res.stdout);
1683
+ process.stderr.write(res.stderr);
1684
+ if (res.returncode !== 1) {
1685
+ // The gate reserves 1 for a finding and 2 for a usage/environment
1686
+ // failure. Diagnosing the latter as "the table has an overdue row"
1687
+ // sends the releaser to edit a file that is not the problem.
1688
+ die(
1689
+ `refusing the ${target} cut: the scheduled-deprecations check could not run ` +
1690
+ `(exit ${String(res.returncode)}). That is an environment or usage failure, not a ` +
1691
+ 'finding in docs/MIGRATION.md — fix the invocation or the checkout, then re-run.',
1692
+ );
1693
+ }
1694
+ if (opts.previewOnly === true) {
1695
+ // The preview's job is to SHOW what the real run will refuse. Dying
1696
+ // here would break the `--dry-run exits 0` contract asserted elsewhere
1697
+ // in this file's tests — trading a documented exit code for a message
1698
+ // that has already been printed above.
1699
+ process.stderr.write(
1700
+ `\n(dry-run) the ${target} cut WOULD BE REFUSED for the reason above. ` +
1701
+ 'Previewing only; exit code unchanged.\n',
1702
+ );
1703
+ return;
1704
+ }
1705
+ die(
1706
+ `refusing the ${target} cut: the scheduled-deprecations table in docs/MIGRATION.md ` +
1707
+ 'has a row due at or before this major, or one that cannot be resolved. Act on ' +
1708
+ "it — perform the removal in its own change, or revise the row's commitment and " +
1709
+ 'record why the surface stays — then re-run.',
1710
+ );
1711
+ }
1712
+
1626
1713
  // ─── preflight ────────────────────────────────────────────────────────────────
1627
1714
 
1628
1715
  /**
@@ -2578,6 +2665,13 @@ function main(argv: readonly string[] | null = null): number {
2578
2665
  }
2579
2666
  parse_version(target);
2580
2667
 
2668
+ // Runs in dry-run too — a preview that hides the one condition which will
2669
+ // refuse the real run is the case previews are for — but REPORTS there
2670
+ // instead of dying: `--dry-run returns 0` is a contract this file's tests
2671
+ // assert, and breaking it to surface a warning would trade a documented
2672
+ // exit code for a message.
2673
+ assert_scheduled_deprecations_clear(target, undefined, { previewOnly: args.dry_run });
2674
+
2581
2675
  if (!args.dry_run) {
2582
2676
  preflight(target, { resume: args.resume, ci: args.ci });
2583
2677
  }
@@ -63,7 +63,7 @@ import {
63
63
  classifyTask,
64
64
  type ActivationInputs,
65
65
  } from "./_lib/auto_dispatch.js";
66
- import { TIER_ORDER, readCooldowns } from "./_lib/tier_budget_routing.js";
66
+ import { COOLDOWN_FILE, TIER_ORDER, readCooldowns } from "./_lib/tier_budget_routing.js";
67
67
  import {
68
68
  instructionsLoadedRecord,
69
69
  measureStandingDelivery,
@@ -111,6 +111,13 @@ export interface OrchestrationReport {
111
111
  downshift: boolean;
112
112
  /** cost.budgets.per_tier ceilings + live cool-down state per tier. */
113
113
  tier_budgets: Record<string, { ceiling_usd: number | null; cooldown_until_ms: number }>;
114
+ /**
115
+ * Whether the cool-down store exists on disk. `readCooldowns` returns all
116
+ * zeros both when no tier is cooling AND when nothing has ever written the
117
+ * file, so a bare zero cannot distinguish "measured: live" from "no reading
118
+ * exists". This flag is what separates them.
119
+ */
120
+ cooldown_store_present: boolean;
114
121
  host_manifest: HostCapabilityManifest;
115
122
  /**
116
123
  * The host identifier the capability registry was keyed on, and whether it
@@ -158,14 +165,23 @@ export interface OrchestrationReport {
158
165
  }
159
166
 
160
167
  /**
161
- * Adherence ≠ delivery: budget-aware tier routing has no on/off setting any
162
- * more (always-on orchestration removed `subagents.budget_routing`), so the
163
- * doctor always checks the DELIVERY evidence — orchestration audit lines
164
- * that carry a tier decision. Dispatches recorded with ZERO tier-carrying
165
- * lines → WARN with the exact reason. Read-only over
166
- * agents/runtime/state/audit/*.jsonl; no new state. This closes the same
167
- * blind-spot class the session-canary incident exposed (policy present,
168
- * delivery silently absent).
168
+ * Tier-decision coverage in the orchestration record.
169
+ *
170
+ * REPOINTED 2026-08-16. This used to check "delivery evidence" for budget-aware
171
+ * tier routing, on the reasoning that policy can be present while delivery is
172
+ * silently absent — the session-canary blind-spot class. That layer is now
173
+ * ARCHIVED (`docs/contracts/budget-routing.md`), so the old rationale no longer
174
+ * holds and the old warning would have been permanently true and un-actionable:
175
+ * nothing dispatches through a tier decision by design, so telling the operator
176
+ * "the relation may not be running" is a dead advisory of exactly the shape this
177
+ * repository keeps removing elsewhere.
178
+ *
179
+ * What it measures now is narrower and still worth having: how many recorded
180
+ * dispatches carry a `tier` at all. That is the SAME field the archived layer's
181
+ * revisit-if depends on — a saving can only be computed once both the chosen and
182
+ * the realized tier are recorded — so this is the instrument that would show the
183
+ * reopen condition arriving. It reports coverage rather than warning about it.
184
+ * Read-only over agents/runtime/state/audit/*.jsonl; no new state.
169
185
  */
170
186
  export function check_budget_delivery(
171
187
  workspace_root: string,
@@ -202,9 +218,10 @@ export function check_budget_delivery(
202
218
  }
203
219
  if (out.eligible_dispatches > 0 && out.budget_evidence_lines === 0) {
204
220
  out.warning =
205
- `budget-aware tier routing shows no delivery evidence: ` +
206
- `${out.eligible_dispatches} orchestration dispatch(es) recorded, 0 carry a tier decision — ` +
207
- `the relation may not be running (adherence != delivery)`;
221
+ `tier-decision coverage is 0 of ${out.eligible_dispatches} recorded dispatch(es) — ` +
222
+ `expected while budget routing stays archived (docs/contracts/budget-routing.md). ` +
223
+ `This is the counter its revisit-if reads: a non-zero value here is the signal ` +
224
+ `that per-tier dispatch has reappeared and the decision can be reopened`;
208
225
  }
209
226
  return out;
210
227
  }
@@ -356,6 +373,7 @@ export function collect_orchestration(
356
373
  const ledger_present = fs.existsSync(path.join(tracking_dir, "sessions.jsonl"));
357
374
  const per_tier = (cost_budgets["per_tier"] ?? {}) as Record<string, unknown>;
358
375
  const cooldowns = readCooldowns(tracking_dir);
376
+ const cooldown_store_present = fs.existsSync(path.join(tracking_dir, COOLDOWN_FILE));
359
377
  const tier_budgets: OrchestrationReport["tier_budgets"] = {};
360
378
  for (const t of TIER_ORDER) {
361
379
  const v = per_tier[t];
@@ -382,6 +400,7 @@ export function collect_orchestration(
382
400
  halted,
383
401
  downshift,
384
402
  tier_budgets,
403
+ cooldown_store_present,
385
404
  host_manifest,
386
405
  host_platform: platform,
387
406
  host_platform_assumed: platform_assumed,
@@ -636,10 +655,28 @@ function _render(report: DoctorReport): string {
636
655
  );
637
656
  const tierBits = Object.entries(o.tier_budgets).map(([t, s]) => {
638
657
  const cap = s.ceiling_usd === null ? "no cap" : `$${s.ceiling_usd}`;
639
- const cool = s.cooldown_until_ms > 0 ? " COOLING" : "";
658
+ // `> Date.now()`, not `> 0`. The purger went with the archived permit
659
+ // lifecycle (`tripCooldown` rewrote the map on every write), so a stale
660
+ // entry now has nothing to clear it and `> 0` would render COOLING for
661
+ // ever. Latent rather than live — there is no writer either — but the
662
+ // comparison should be the one that stays correct if a writer returns.
663
+ const cool = s.cooldown_until_ms > Date.now() ? " COOLING" : "";
640
664
  return `${t}=${cap}${cool}`;
641
665
  });
642
666
  lines.push(` tier budgets (rolling-24h): ${tierBits.join(" · ")}`);
667
+ // `readCooldowns` returns all zeros both when no tier is cooling and when
668
+ // nothing has ever written the store, so a missing COOLING marker alone
669
+ // cannot be read as a measured "this tier is live". Report which of the two
670
+ // it is — the same unavailable-vs-false distinction the capability-
671
+ // provenance line above makes two lines earlier. Derived from the store's
672
+ // existence rather than from a caller count, so it stays true if the
673
+ // cool-down producer is ever wired.
674
+ if (!o.cooldown_store_present) {
675
+ lines.push(
676
+ ` cool-down state: no reading on disk (${COOLDOWN_FILE} absent) — ` +
677
+ `absence of COOLING above is not evidence of a live tier`,
678
+ );
679
+ }
643
680
  if (o.delivery.warning) {
644
681
  lines.push(` ⚠️ ${o.delivery.warning}`);
645
682
  } else if (o.delivery.eligible_dispatches > 0) {
@@ -158,8 +158,8 @@
158
158
  },
159
159
  "model_tier": {
160
160
  "type": "string",
161
- "enum": ["lite", "medium", "high", "inherit"],
162
- "description": "Vendor-neutral capability band the command needs (ADR-035). Commands project as Claude skills, so high/medium/lite produce a native Claude `model:` (high→opus, medium→sonnet, lite→haiku) when model.auto_switch=auto; the generator owns that mapping. inherit keeps the session model. Non-Claude agents are suggestion-only. No vendor model names here."
161
+ "enum": ["lite", "medium", "high", "frontier", "inherit"],
162
+ "description": "Vendor-neutral capability band the command needs (ADR-035, amended by ADR-232). Commands project as Claude skills, so frontier/high/medium/lite produce a native Claude `model:` when model.auto_switch=auto; the generator owns that mapping. inherit keeps the session model. `frontier` is the band above the strongest generally-recommended one - opt-in and expected to stay rare. Non-Claude agents are suggestion-only. No vendor model names here."
163
163
  },
164
164
  "context": {
165
165
  "type": "string",
@@ -254,7 +254,7 @@
254
254
  },
255
255
  "requires_confirmation": {
256
256
  "type": "boolean",
257
- "description": "Optional, additive: the command's action STAGES rather than executes, so an irreversible step waits for an explicit human confirmation naming the exact object (non-destructive-by-default § Never act while asking). Twin of the same key in skill.schema.json's execution block; commands carry no execution block, so it sits top-level. A declaration only — it names the action class, never the object, which exists at stage time and not in frontmatter. Mechanism: stageAction/confirmOnce in work_engine/hooks/builtin/staged_confirmation.ts; enumerate with `agent-config gates --pending`. Nothing BINDS on it yet (blocker confirmation-degraded-host-semantics: 5 of 8 hosts have no pre_tool_use slot)."
257
+ "description": "Optional, additive: the command's action STAGES rather than executes, so an irreversible step waits for an explicit human confirmation naming the exact object (non-destructive-by-default § Never act while asking). Twin of the same key in skill.schema.json's execution block; commands carry no execution block, so it sits top-level. A declaration only — it names the action class, never the object, which exists at stage time and not in frontmatter. Mechanism: stageAction/confirmOnce in work_engine/hooks/builtin/staged_confirmation.ts; enumerate with `agent-config gates --pending`. Nothing BINDS on it yet (blocker confirmation-degraded-host-semantics: only claude both binds pre_tool_use and honours a deny — docs/contracts/hook-architecture-v1.md, section Which hosts carry pre_tool_use)."
258
258
  },
259
259
  "auto_detect": {
260
260
  "type": "boolean",
@@ -168,9 +168,10 @@
168
168
  "lite",
169
169
  "medium",
170
170
  "high",
171
+ "frontier",
171
172
  "inherit"
172
173
  ],
173
- "description": "Vendor-neutral capability band the skill needs (ADR-035). high/medium/lite project to a native Claude `model:` (high→opus, medium→sonnet, lite→haiku) when model.auto_switch=auto; the generator owns that mapping exclusively. inherit keeps the session model. Non-Claude agents are suggestion-only — the model-recommendation rule names the tier and the agent resolves its own model. No vendor model names here."
174
+ "description": "Vendor-neutral capability band the skill needs (ADR-035, amended by ADR-232). frontier/high/medium/lite project to a native Claude `model:` when model.auto_switch=auto; the generator owns that mapping exclusively. inherit keeps the session model. `frontier` means reasoning capability above the strongest generally-recommended band - opt-in and expected to stay rare, so work that genuinely needs it says so instead of inheriting it silently. Non-Claude agents are suggestion-only - the model-recommendation rule names the tier and the agent resolves its own model. No vendor model names here."
174
175
  },
175
176
  "context": {
176
177
  "type": "string",
@@ -275,7 +276,7 @@
275
276
  },
276
277
  "requires_confirmation": {
277
278
  "type": "boolean",
278
- "description": "Optional, additive: the skill's action STAGES rather than executes, so an irreversible step waits for an explicit human confirmation naming the exact object (non-destructive-by-default § Never act while asking). A declaration only — it names the action class, never the object, which exists at stage time and not in frontmatter. The mechanism is stageAction/confirmOnce in work_engine/hooks/builtin/staged_confirmation.ts; enumerate what is pending with `agent-config gates --pending`. Nothing BINDS on this flag yet: on 5 of 8 hosts there is no pre_tool_use slot, so a guard would degrade to prose while the flag reads like a control — that decision is deferred behind blocker confirmation-degraded-host-semantics."
279
+ "description": "Optional, additive: the skill's action STAGES rather than executes, so an irreversible step waits for an explicit human confirmation naming the exact object (non-destructive-by-default § Never act while asking). A declaration only — it names the action class, never the object, which exists at stage time and not in frontmatter. The mechanism is stageAction/confirmOnce in work_engine/hooks/builtin/staged_confirmation.ts; enumerate what is pending with `agent-config gates --pending`. Nothing BINDS on this flag yet: only claude both binds pre_tool_use and honours a deny (docs/contracts/hook-architecture-v1.md, section Which hosts carry pre_tool_use — 2 further hosts bind it and ignore the verdict, 3 alias it unbound, 2 have no pre-tool surface), so a guard would degrade to prose on 7 of 8 hosts while the flag reads like a control — that decision is deferred behind blocker confirmation-degraded-host-semantics."
279
280
  },
280
281
  "command": {
281
282
  "type": "array",
@@ -286,6 +287,23 @@
286
287
  }
287
288
  }
288
289
  },
290
+ "triggers": {
291
+ "type": "array",
292
+ "minItems": 1,
293
+ "items": {
294
+ "type": "object",
295
+ "additionalProperties": false,
296
+ "properties": {
297
+ "keyword": {"type": "string", "minLength": 1},
298
+ "phrase": {"type": "string", "minLength": 1},
299
+ "file_pattern": {"type": "string", "minLength": 1},
300
+ "path_prefix": {"type": "string", "minLength": 1},
301
+ "command": {"type": "string", "minLength": 1},
302
+ "reason": {"type": "string"}
303
+ }
304
+ },
305
+ "description": "Activation triggers for the SKILL catalogue, shaped after the rule-side field so the two stay legible together — same five match keys (keyword, phrase, file_pattern, path_prefix, command), same additionalProperties:false, and deliberately no `intent` key, which was removed rule-side for giving authors false confidence that an activation path existed. OPTIONAL and empty of adopters on the day it ships: the capability becomes declarable before anything declares it, so a seeded tranche is measurable against the router corpus before it is written. Read by scripts/trigger_coverage.ts --scope skill through the same router_match.ts matcher the rule scope uses — not a fork. Schema: docs/contracts/rule-router.md."
306
+ },
289
307
  "workspaces": {
290
308
  "type": "array",
291
309
  "minItems": 1,
@@ -34,8 +34,8 @@
34
34
  },
35
35
  "model_tier": {
36
36
  "type": "string",
37
- "enum": ["lite", "medium", "high", "inherit"],
38
- "description": "Vendor-neutral capability band (ADR-034/035). high/medium/lite project to a native Claude `model:` (high→opus, medium→sonnet, lite→haiku) via model_tier.ts; `inherit` is a documented passthrough sentinel (→ CC `model: inherit`), not a new mapping."
37
+ "enum": ["lite", "medium", "high", "frontier", "inherit"],
38
+ "description": "Vendor-neutral capability band (ADR-034/035, amended by ADR-232). frontier/high/medium/lite project to a native Claude `model:` via model_tier.ts; `inherit` is a documented passthrough sentinel (→ CC `model: inherit`), not a new mapping. `frontier` is the band above the strongest generally-recommended one - opt-in, expected to stay rare, and never the resolution of `inherit`."
39
39
  },
40
40
  "effort": {
41
41
  "type": "string",
@@ -61,6 +61,33 @@ export interface EolReport {
61
61
  compact_summary_records: number;
62
62
  marker_drift: boolean;
63
63
  };
64
+ /**
65
+ * State-destroyed vs state-captured, as ONE reading
66
+ * (road-to-inbox-harvest-2026-08-d-context-ledger Step 2.2).
67
+ *
68
+ * Before this, a compaction count and an advisory count sat in two places
69
+ * and nobody correlated them, so "how often was state destroyed with no
70
+ * capture in front of it" had no answer at all.
71
+ *
72
+ * HONEST LIMIT, stated in the shape rather than in prose only: the two
73
+ * sides come from different corpora — compactions are scanned out of
74
+ * transcripts, the advisory stamps live in per-session state files under a
75
+ * hashed key — and no field joins a transcript to its state file. So this
76
+ * is an AGGREGATE comparison over the same store, not a per-session join,
77
+ * and `join_basis` says so on every emitted report. A reader may compare
78
+ * the totals; a reader may NOT conclude that a specific compaction lacked
79
+ * a capture.
80
+ */
81
+ capture: {
82
+ /** Session-eol state files found. `null` ⇒ the directory is absent. */
83
+ state_files: number | null;
84
+ /** State files carrying an `advisory_fired_at` stamp. */
85
+ advisory_fired: number;
86
+ /** State files that additionally recorded "advised, but no envelope". */
87
+ advised_without_envelope: number;
88
+ /** Always `"aggregate"` today. See the doc comment. */
89
+ join_basis: 'aggregate';
90
+ };
64
91
  /** Pearson r of file bytes vs final context tokens over the readable set. */
65
92
  bytes_tokens_pearson_r: number | null;
66
93
  threshold_crossings: Record<string, number>;
@@ -130,6 +157,47 @@ export function scanSessionFile(file: string): SessionSummary | null {
130
157
  return { file, bytes, counters, last_ts };
131
158
  }
132
159
 
160
+ /** Where `session_eol_hook` persists its per-session state (gitignored). */
161
+ export const SESSION_EOL_STATE_REL = path.join('agents', 'runtime', 'state', 'session-eol');
162
+
163
+ /**
164
+ * The capture side of the Step-2.2 reading.
165
+ *
166
+ * `state_files: null` distinguishes "the directory is not there" from "it is
167
+ * there and empty" — an instrument that reports 0 for both cannot tell a quiet
168
+ * estate from one it cannot see, which is the failure the sibling ledger fixed
169
+ * for its own open-record set.
170
+ */
171
+ export function scanCaptureState(root: string): EolReport['capture'] {
172
+ const dir = path.join(root, SESSION_EOL_STATE_REL);
173
+ let names: string[];
174
+ try {
175
+ names = fs.readdirSync(dir).filter((n) => n.endsWith('.json'));
176
+ } catch {
177
+ return { state_files: null, advisory_fired: 0, advised_without_envelope: 0, join_basis: 'aggregate' };
178
+ }
179
+
180
+ let advisoryFired = 0;
181
+ let advisedWithoutEnvelope = 0;
182
+ for (const name of names) {
183
+ try {
184
+ const parsed: unknown = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf-8'));
185
+ if (typeof parsed !== 'object' || parsed === null) continue;
186
+ const rec = parsed as Record<string, unknown>;
187
+ if (typeof rec['advisory_fired_at'] === 'string') advisoryFired++;
188
+ if (typeof rec['missing_envelope_warned_at'] === 'string') advisedWithoutEnvelope++;
189
+ } catch {
190
+ // A torn or hand-edited state file is data we do not have.
191
+ }
192
+ }
193
+ return {
194
+ state_files: names.length,
195
+ advisory_fired: advisoryFired,
196
+ advised_without_envelope: advisedWithoutEnvelope,
197
+ join_basis: 'aggregate',
198
+ };
199
+ }
200
+
133
201
  export function buildReport(root: string, now: Date = new Date()): EolReport {
134
202
  const files = listTranscriptFiles({ root });
135
203
  const sessions: SessionSummary[] = [];
@@ -176,6 +244,7 @@ export function buildReport(root: string, now: Date = new Date()): EolReport {
176
244
  compact_summary_records: summaryRecords,
177
245
  marker_drift: summaryRecords !== allEvents.length,
178
246
  },
247
+ capture: scanCaptureState(root),
179
248
  bytes_tokens_pearson_r: pearson(
180
249
  withUsage
181
250
  .filter((s) => (s.counters.final_context_tokens as number) > 1_000)
@@ -216,6 +285,18 @@ export function renderText(r: EolReport): string {
216
285
  ` trigger pre-tokens: min=${fmt(c.pre_tokens.min)} median=${fmt(c.pre_tokens.median)} max=${fmt(c.pre_tokens.max)}`,
217
286
  );
218
287
  }
288
+ // Step 2.2: the capture side, printed directly under the destruction side
289
+ // so the two are read together. `join_basis` is printed every time — a
290
+ // reader must not mistake an aggregate comparison for a per-session join.
291
+ const cap = r.capture;
292
+ if (cap.state_files === null) {
293
+ lines.push(` capture: no session-eol state directory — capture side UNOBSERVED (not zero)`);
294
+ } else {
295
+ lines.push(
296
+ ` capture: ${cap.advisory_fired} of ${cap.state_files} state file(s) advised, ` +
297
+ `${cap.advised_without_envelope} advised with no envelope [join: ${cap.join_basis}]`,
298
+ );
299
+ }
219
300
  if (c.post_tokens) {
220
301
  lines.push(` post-tokens median=${fmt(c.post_tokens.median)}`);
221
302
  }