@event4u/agent-config 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +67 -20
  3. package/MIGRATION.md +14 -0
  4. package/README.md +2 -2
  5. package/dist/agent-src/commands/explain-run.md +88 -0
  6. package/dist/agent-src/commands/pr/create/description-only.md +138 -8
  7. package/dist/agent-src/commands/pr/create.md +16 -5
  8. package/dist/agent-src/skills/llm-provider-knowledge/SKILL.md +146 -0
  9. package/dist/agent-src/skills/llm-provider-knowledge/evals/triggers.json +20 -0
  10. package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +11 -0
  11. package/dist/agent-src/skills/token-optimizer/SKILL.md +10 -4
  12. package/dist/agent-src/templates/agent-settings.md +28 -0
  13. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  14. package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +4 -1
  15. package/dist/discovery/deprecation-report.md +1 -1
  16. package/dist/discovery/discovery-manifest.json +68 -14
  17. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  18. package/dist/discovery/discovery-manifest.summary.md +3 -3
  19. package/dist/discovery/orphan-report.md +1 -1
  20. package/dist/discovery/packs.json +8 -6
  21. package/dist/discovery/trust-report.md +8 -8
  22. package/dist/discovery/workspaces.json +6 -4
  23. package/dist/install/install.mjs +38 -31
  24. package/dist/mcp/registry-manifest.json +3 -3
  25. package/dist/scripts/_lib/agent_settings.js +4 -1
  26. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  27. package/dist/server/schemas/settings.js +5 -0
  28. package/dist/server/schemas/settings.js.map +1 -1
  29. package/docs/CLAIMS.md +2 -2
  30. package/docs/MIGRATION.md +20 -0
  31. package/docs/architecture.md +3 -3
  32. package/docs/command-flows.md +1 -1
  33. package/docs/contracts/agents-layout.md +1 -1
  34. package/docs/contracts/consumer-bridge.md +55 -63
  35. package/docs/contracts/install-layout.md +1 -1
  36. package/docs/contracts/migrate-command.md +1 -1
  37. package/docs/customization.md +22 -1
  38. package/docs/decisions/ADR-020-global-only-consumer-scope.md +34 -0
  39. package/docs/featured-skills.md +2 -2
  40. package/docs/getting-started-by-role.md +1 -1
  41. package/docs/getting-started.md +1 -1
  42. package/docs/governance-advantage.md +1 -1
  43. package/docs/guidelines/agent-infra/api-cost-levers.md +126 -0
  44. package/docs/installation.md +2 -1
  45. package/docs/maintainers/dev-mode.md +3 -2
  46. package/docs/proof.md +5 -5
  47. package/package.json +1 -1
  48. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +4 -1
  49. package/src/config/agent-settings.template.yml +39 -0
  50. package/src/config/gitignore-block.txt +16 -6
  51. package/src/scripts/_cli/cmd_conformance.ts +2 -2
  52. package/src/scripts/_cli/cmd_doctor.ts +162 -13
  53. package/src/scripts/_cli/cmd_refresh.ts +35 -118
  54. package/src/scripts/_lib/agent_settings.ts +4 -1
  55. package/src/scripts/ai_council/clients.ts +68 -6
  56. package/src/scripts/ai_council/pricing.ts +62 -0
  57. package/src/scripts/council_cli.ts +19 -3
  58. package/src/scripts/install.ts +44 -45
  59. package/src/scripts/lint_agents_layout.ts +10 -4
  60. package/src/scripts/lint_breaking_changes_index.ts +303 -0
  61. package/src/scripts/mcp_server/tools.ts +1 -3
  62. package/src/scripts/schemas/agent-settings.schema.json +17 -0
  63. package/src/scripts/self_review_gate.test.ts +11 -0
  64. package/src/scripts/self_review_gate.ts +7 -1
  65. package/src/scripts/skill_eval_coverage.ts +4 -1
  66. package/src/server/schemas/settings.ts +15 -0
@@ -0,0 +1,126 @@
1
+ # API Cost Levers
2
+
3
+ Reference material for a downstream AI-coding agent that wants to cut its own
4
+ Claude API dollar bill. Four platform-level billing levers, the exact economics
5
+ of each, and how they interact. Read on demand — not always-loaded.
6
+
7
+ Facts sourced from the in-repo `claude-api` plugin skill plus platform research
8
+ (2026-07-14). Do not invent an API shape beyond what is stated here.
9
+
10
+ ## The four levers at a glance
11
+
12
+ | Lever | Cuts | Best for | Watch out for |
13
+ |---|---|---|---|
14
+ | Prompt caching | Repeated input tokens | Stable prefix reused across calls (system prompt, tools, repo map) | Any byte change in the prefix invalidates the rest |
15
+ | Batch API | 50% input + output | Non-interactive bulk work (classification, summarization, evals) | Async, up to 24h, results unordered |
16
+ | Model tiering | Per-token price | Routine work on a cheaper model | Switching model is a cache miss |
17
+ | Effort / output sizing | Thinking + output tokens | Routine turns that don't need deep reasoning | Oversized `max_tokens` risks runaway output |
18
+
19
+ ## 1. Prompt caching
20
+
21
+ Put the large, byte-**stable** context first — system prompt, tool definitions,
22
+ a corpus or repo map, few-shot examples — behind a
23
+ `cache_control: {type: 'ephemeral'}` breakpoint. Keep volatile content (the
24
+ varying question, timestamps, per-request IDs) **after** the last breakpoint.
25
+ The cache is a **prefix match**: any byte change in the cached prefix
26
+ invalidates everything after it. Render order is tools then system then
27
+ messages.
28
+
29
+ Caching is GA — **no beta header needed**.
30
+
31
+ Economics:
32
+
33
+ - Cache reads cost approximately 0.1x the base input price.
34
+ - Cache writes cost 1.25x (5-minute TTL) or 2x (1-hour TTL).
35
+ - Break-even is after **one** read on the 5-minute TTL.
36
+
37
+ Constraints:
38
+
39
+ - The minimum cacheable prefix is model-dependent (approximately 1024–4096
40
+ tokens). Shorter prefixes silently do not cache.
41
+ - The cache is workspace-isolated (since February 2026).
42
+
43
+ Verify a hit with `usage.cache_read_input_tokens > 0`.
44
+
45
+ ```json
46
+ {
47
+ "system": [
48
+ { "type": "text", "text": "<large stable system prompt>",
49
+ "cache_control": { "type": "ephemeral" } }
50
+ ],
51
+ "messages": [
52
+ { "role": "user", "content": "<the varying question>" }
53
+ ]
54
+ }
55
+ ```
56
+
57
+ ## 2. Batch API
58
+
59
+ For **non-interactive, non-latency-sensitive bulk work** — bulk classification,
60
+ mass file summarization, eval runs. Not chatbots or interactive assistants.
61
+
62
+ - **50% off both input and output tokens.**
63
+ - Async: results within 24h (usually much less), keyed by `custom_id`, arriving
64
+ **unordered** — key results by `custom_id`, never by position.
65
+ - **Stacks with prompt caching**: a cached-batch input token can reach
66
+ approximately 0.05x the standard input price.
67
+
68
+ ## 3. Model tiering
69
+
70
+ Treat models like a team — a cheap model for mechanical or routine work, the
71
+ expensive model only for hard judgment. Approximate input / output price per 1M
72
+ tokens:
73
+
74
+ | Model | Input | Output |
75
+ |---|---|---|
76
+ | Haiku | $1 | $5 |
77
+ | Sonnet | $3 | $15 |
78
+ | Opus | $5 | $25 |
79
+ | Fable | $10 | $50 |
80
+
81
+ Opus input is approximately 5x Haiku. Tier by task **difficulty**, not blindly:
82
+ a cheaper model that gives wrong answers on judgment tasks costs correction
83
+ cycles, which erases the saving.
84
+
85
+ **Interaction with caching.** The prompt cache is keyed by `(model, prefix)`, so
86
+ switching a call to a cheaper model is a cache **miss**. Weigh the downgrade
87
+ saving against the lost cache-read saving before routing a cached call to a
88
+ different model.
89
+
90
+ ## 4. Effort and output right-sizing
91
+
92
+ - `output_config.effort` (low through max) controls thinking and output depth,
93
+ and therefore spend. Dial effort **down** on routine turns; reserve high / max
94
+ for hard reasoning.
95
+ - Right-size `max_tokens` — do not leave a huge ceiling. An unrealistically
96
+ large `max_tokens` does not cost tokens by itself, but it risks runaway output
97
+ and defeats pre-flight budgeting.
98
+
99
+ ## When to reach for which
100
+
101
+ | Situation | Lever |
102
+ |---|---|
103
+ | Same large context reused across many calls | Prompt caching |
104
+ | Thousands of independent, non-urgent jobs | Batch API (+ caching) |
105
+ | Turn is mechanical / narrow / well-specified | Cheaper model tier |
106
+ | Turn is routine and does not need deep thinking | Lower `effort` |
107
+ | Output could balloon unnecessarily | Cap `max_tokens` |
108
+ | Interactive latency matters | Caching + tiering; not Batch |
109
+
110
+ ## Claude Code note
111
+
112
+ In Claude Code, prompt caching is automatic — resending the same context prefix
113
+ is cheap, so keeping a stable prefix pays off without any explicit
114
+ `cache_control`. The session-level hygiene levers are `/clear` and `/compact` to
115
+ shed accumulated context, plus subagent delegation for verbose output so it
116
+ never lands in the main context window.
117
+
118
+ ## See also
119
+
120
+ - [`model-recommendation.md`](model-recommendation.md) — tier routing (the
121
+ capability band behind lever 3).
122
+ - [`size-and-scope.md`](size-and-scope.md) — token discipline for the artifacts
123
+ that become the cached prefix.
124
+ - The Claude Code `claude-api` plugin skill (external, not vendored here) — the
125
+ authoritative, versioned API reference these figures derive from; re-check it
126
+ when Anthropic pricing or the caching/batch surface changes.
@@ -3,7 +3,8 @@
3
3
  **Principle:** Global is the only consumer scope. `init` writes to
4
4
  user-scope paths (`~/.event4u/agent-config/`, `~/.claude/`,
5
5
  `~/.cursor/`, …); the project tree only gets `agents/overrides/`
6
- and `agents/.event4u-bridge.yml`. Opt-in `export` if a team wants
6
+ (the bridge marker was retired — ADR-020 amendment 2026-07-13; the
7
+ global root resolves from `~/.event4u/agent-config`). Opt-in `export` if a team wants
7
8
  a tool's config committed to a repo. No Task, no Make, no build
8
9
  tools required.
9
10
 
@@ -23,8 +23,9 @@ With the flag set, the installer:
23
23
  1. Allows project-scope writes back into the repo tree (so a
24
24
  maintainer can run `task dev:install-global` and iterate on the
25
25
  working copy).
26
- 2. **Skips** writing `agents/.event4u-bridge.yml` into the package
27
- repo (per `consumer-bridge § Writer contract`). The repo is the
26
+ 2. **Skips** consumer-surface writes into the package repo. (The
27
+ `agents/.event4u-bridge.yml` marker is retired — ADR-020 amendment
28
+ 2026-07-13 — and no longer written at all.) The repo is the
28
29
  source, not a consumer of itself.
29
30
  3. Treats `~/.event4u/agent-config/` as a peer install — touches are
30
31
  limited to the working-copy projection.
package/docs/proof.md CHANGED
@@ -27,7 +27,7 @@ evidence pointer, or `task check-claims` fails the build.
27
27
  | Claim | Kind | Evidence | Resolves |
28
28
  |---|---|---|---|
29
29
  | The release process is documented as an inheritable runbook + succession doc, and the project's bus-factor (trailing-90-day distinct human reviewers) is tracked and reported truthfully — currently 1, not implied to be more. | qual | `docs/succession.md#trailing 90 days` | ✅ |
30
- | 183 commands. | quant | `src/scripts/check_artefact_count_messaging.ts#Artefact-count messaging gate` | ✅ |
30
+ | 184 commands. | quant | `src/scripts/check_artefact_count_messaging.ts#Artefact-count messaging gate` | ✅ |
31
31
  | A MEASURED-BUT-NOT-SHIPPED experiment — the thin rule projection reduced eager rule load 78,513 → 13,881 GPT-tokens (whole always-loaded projection 98,529 → 33,897, ~65.6%), but FAILED the quality gate (thin win-rate 36.2% vs required 48%) and does not ship; it un-defers only behind `discipline_profile: essential`. Shipped behavior does NOT include this reduction. Method: `agent-config benchmark` over the pinned token baseline; the baseline is the honest "what the user pays if everything loads eagerly", NOT a synthetic full-corpus strawman (council Q4); quality gate per the Phase-0 paired judge run. | quant | `internal/bench/reports/token-baseline.json#eager_rule_load` | ✅ |
32
32
  | The first cross-vendor parity pass (5 orchestration-corpus tasks × 2 vendors × 3 repeats, identical prompts via the council transport, $0.16) measured real per-host finding-count differences — claude-sonnet-4-5 surfaced ~2× the findings of gpt-4o on the multi-file analysis task (median 11 vs 5) while both vendors were identical on the planted hollow-implementation task (2 vs 2) and perfectly silent on the clean-code negative control (0 vs 0, no spurious findings). The per-task `finding_floor` values are calibrated from the cross-host lower envelope and the gate is armed. | quant | `internal/bench/reports/parity-count.json#min over hosts of median` | ✅ |
33
33
  | On a weak host (claude-haiku-4-5) the package produces a significant, placebo-controlled discipline lift on scope/downstream traps; on a strong host the same measurement is a published null — the package transplants discipline a weak model lacks, not model intelligence. | quant | `docs/benchmark.md#weak-host-specific` | ✅ |
@@ -45,7 +45,7 @@ evidence pointer, or `task check-claims` fails the build.
45
45
  | On a deterministic multi-session recall corpus, the memory substrate produces a measured, placebo-controlled recall lift — memory-on 27/27 vs no-memory 10/27 and vs equal-byte placebo 9/27 (claude-haiku-4-5, n=9 tasks x 3 seeds, sign test p=0.031 for BOTH pairings). Scoped honestly: this is the context-value upper bound (perfect retrieval on a one-fact-per-task corpus), not retrieval precision under a large store. | quant | `internal/bench/reports/second-brain-delta.json` | ✅ |
46
46
  | Removing the perfect-retrieval assumption, the substrate's REAL keyword retrieval recalls the needed decision into the top-5 under keyword-overlapping confusers (precision@5 9/9) and the model disambiguates it from the co-injected confusers — retrieval-on 27/27 vs no-memory 5/27 and vs equal-count placebo 5/27 (claude-haiku-4-5, 9 tasks x 3 seeds, sign test p=0.008 both). Named limit: retrieval RECALLS but does not RANK (mean tie-set 3.3, ties broken by store order, not relevance) — the discrimination gap that motivates the SQLite-FTS5 activation path (ADR-116) at larger scale. | quant | `internal/bench/reports/second-brain-retrieval.json` | ✅ |
47
47
  | Every artifact the package ships — source AND the condensed projection that reaches consumers — is machine-scanned in CI for hidden-Unicode, mixed-script-confusable, and instruction-smuggling payloads (the rules-file-backdoor class); a finding blocks the release before `npm publish`, not just the merge. | qual | `.github/workflows/publish-npm.yml#lint_agent_security` | ✅ |
48
- | 274 skills. | quant | `src/scripts/check_artefact_count_messaging.ts#Artefact-count messaging gate` | ✅ |
48
+ | 275 skills. | quant | `src/scripts/check_artefact_count_messaging.ts#Artefact-count messaging gate` | ✅ |
49
49
  | Removes only its own keys from a shared host config (matched by JSON-pointer + SHA-256), never a neighbour tool's entries. | qual | `docs/contracts/install-layout.md#JSON-pointer` | ✅ |
50
50
 
51
51
  **21 backed claim(s)** — all evidence pointers resolve in CI.
@@ -77,11 +77,11 @@ shipping theater; the full per-cell data is committed at
77
77
  `internal/bench/reports/persona-placebo.json`.
78
78
 
79
79
  **Behavioural-eval coverage — the honest baseline.** Skill *quality* is only
80
- as good as its measurement. Today **42 of 274** skills carry a behavioural
80
+ as good as its measurement. Today **42 of 275** skills carry a behavioural
81
81
  `evals.json`; the highest-traffic / highest-cost tiers (default-surface +
82
82
  `rich` + routers) are **fully covered (35 of 35)**, the long tail
83
- (7 of 239) is not. We publish that gap rather than imply
84
- "274 evaluated skills": coverage is measured per tier
83
+ (7 of 240) is not. We publish that gap rather than imply
84
+ "275 evaluated skills": coverage is measured per tier
85
85
  (`./scripts-run src/scripts/skill_eval_coverage`), **CI-ratcheted so it can
86
86
  only rise**, and the priority tiers carry a hard **tier floor**: every
87
87
  rich / default-surface / router skill MUST have a behavioural eval or an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "9.0.0",
3
+ "version": "9.1.0",
4
4
  "description": "Universal AI Agent OS — audited skills, governance rules, commands, and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -252,7 +252,10 @@ const _AGENTS_DIR_MARKERS: readonly string[] = [
252
252
  'roadmaps',
253
253
  'settings/.ai-council.yml',
254
254
  'roadmaps-progress.md',
255
- '.event4u-bridge.yml',
255
+ // `overrides/` is the guaranteed minimal-consumer surface (ADR-020).
256
+ // Replaced the retired `.event4u-bridge.yml` marker (ADR-020 amendment
257
+ // 2026-07-13) so a bare consumer `agents/` dir stays anchorable.
258
+ 'overrides',
256
259
  ];
257
260
 
258
261
  /**
@@ -761,6 +761,45 @@ commands:
761
761
  create_pr:
762
762
  preview_description: false
763
763
 
764
+ # Verbosity tier for the generated PR description body.
765
+ # min (default) = title + 2-3 sentence what/why/impact + linked ticket.
766
+ # Token-frugal. Critical info (breaking changes,
767
+ # migrations, security, rollback) is ALWAYS included
768
+ # regardless of tier — the tier governs explanatory
769
+ # depth, never whether a critical callout appears.
770
+ # med = min + grouped logical changes + tests note.
771
+ # max = med + how-to-test + edge cases / trade-offs +
772
+ # reviewer guidance (the richest, ~legacy output).
773
+ detail_level: min
774
+
775
+ # JSON request/response examples for API-endpoint changes.
776
+ # true (default) = include a fenced example ONLY when it can be
777
+ # grounded in a real source (response DTO/resource,
778
+ # OpenAPI/schema, test fixture, or an actual probe).
779
+ # No grounded source → a one-line pointer, never an
780
+ # invented example (a hallucinated contract misleads
781
+ # reviewers, so it is worse than none).
782
+ # false = never add API examples.
783
+ api_examples: true
784
+
785
+ # Screenshots for frontend changes. Default false (off). Capability-gated:
786
+ # requires the host to have browser/preview tooling (Playwright / a
787
+ # dev-server URL). When true + a frontend change + no such tooling, the
788
+ # agent emits a one-line note and leaves the template placeholder — it
789
+ # never fails or blocks PR creation, and never claims an embed that did
790
+ # not happen. Before/after + changed-region highlighting is best-effort.
791
+ # false (default) = never attempt screenshots.
792
+ # true = attempt when capable (see command contract).
793
+ screenshots: false
794
+
795
+ # Optional glob lists that make frontend / API-endpoint detection
796
+ # explicit instead of heuristic. Empty (default) = the command uses a
797
+ # light path/extension heuristic and fails open (no false enrichment
798
+ # when the surface is ambiguous). Populate to pin your project's paths,
799
+ # e.g. ui_paths: ["resources/views/**", "src/pages/**"].
800
+ ui_paths: []
801
+ api_paths: []
802
+
764
803
  # --- Memory ---
765
804
  #
766
805
  # Engineering memory consolidation behaviour. Memory is entirely
@@ -1,12 +1,9 @@
1
1
  # Agent config — tool-scope leftovers (ADR-020 § global-only consumer
2
2
  # install). After the Phase-5 migration the entire `.augment/`, `.claude/`,
3
3
  # `.cursor/` trees live under `~/.event4u/agent-config/`; consumers carry
4
- # only `agents/overrides/` + `agents/.event4u-bridge.yml`. The per-tool
5
- # bridge pointer files for Windsurf / Cline / Gemini-CLI live under
6
- # `.windsurf/`, `.clinerules/`, `.gemini/` and ARE committed — those
7
- # directories stay tracked (not listed here). Pre-Phase-5 entries kept
8
- # as a defence-in-depth ignore list for legacy installs that have not
9
- # migrated yet — Phase 5 unstages them on first migrate run.
4
+ # only `agents/overrides/`. Pre-Phase-5 entries kept as a defence-in-depth
5
+ # ignore list for legacy installs that have not migrated yet — Phase 5
6
+ # unstages them on first migrate run.
10
7
  .augment/skills/
11
8
  .augment/commands/
12
9
  .augment/guidelines/
@@ -21,6 +18,19 @@
21
18
  .claude/CLAUDE.md
22
19
  .cursor/rules/
23
20
 
21
+ # Agent config — consumer bridge marker + per-tool project anchors.
22
+ # The bridge marker was retired (ADR-020 amendment 2026-07-13): it carried
23
+ # no project-specific data (global_root resolves from `~/.event4u/agent-config`)
24
+ # yet was committed and rewritten by every developer's install. The global
25
+ # root is now the well-known path. The Windsurf / Cline / Gemini-CLI anchors
26
+ # remain (those tools cannot read user scope) but are gitignored — each
27
+ # developer regenerates them on install; they are never shared. The install
28
+ # deletes any legacy committed marker; this entry keeps a stale one ignored.
29
+ /agents/.event4u-bridge.yml
30
+ .windsurf/agent-config.bridge.yml
31
+ .clinerules/agent-config.bridge.yml
32
+ .gemini/agent-config.bridge.yml
33
+
24
34
  # Agent config — Phase 5 migration snapshot (ADR-020). Written by
25
35
  # `agent-config migrate-to-global` as a copy-then-move safety net so a
26
36
  # migration can be rolled back. Local-only — never commit.
@@ -47,7 +47,7 @@ import {
47
47
  _run_checks_no_manifest,
48
48
  _scan_foreign,
49
49
  ArgparseExit,
50
- BRIDGE_MARKER_RELATIVE,
50
+ _is_global_only_consumer,
51
51
  STATUS_SYMBOLS,
52
52
  } from './cmd_doctor.js';
53
53
 
@@ -630,7 +630,7 @@ function main(argv: string[] | null = null): number {
630
630
  let doctorChecks: Dict[];
631
631
  let drift: Record<string, Dict[]> = { missing: [], modified: [], foreign: [], tag_drift: [] };
632
632
  if (manifest === null) {
633
- const bridge_present = fs.existsSync(path.join(project_root, BRIDGE_MARKER_RELATIVE));
633
+ const bridge_present = _is_global_only_consumer(project_root);
634
634
  doctorChecks = _run_checks_no_manifest(project_root, bridge_present, null);
635
635
  } else {
636
636
  const [records, known] = _collect_manifest_entries(project_root, manifest);
@@ -100,6 +100,12 @@ import {
100
100
  import * as ai_council_clients from '../ai_council/clients.js';
101
101
  import * as ai_council_config from '../ai_council/config.js';
102
102
  import { review_gate_doctor_signal } from '../ai_team/review_gate.js';
103
+ import {
104
+ LEGACY_ALL,
105
+ excludedRuleBasenames,
106
+ ruleScopeFromSettings,
107
+ type RuleScope,
108
+ } from '../../install/rule_scope.js';
103
109
 
104
110
  type Dict = Record<string, unknown>;
105
111
 
@@ -738,6 +744,7 @@ const CHECK_IDS = [
738
744
  'surface-state',
739
745
  'hook-wiring',
740
746
  'stale-orphans',
747
+ 'rule-scope-drift',
741
748
  'manifest-integrity',
742
749
  'lockfile-freshness',
743
750
  'bridge-drift',
@@ -764,6 +771,7 @@ const GLOBAL_CHECK_IDS: ReadonlySet<string> = new Set([
764
771
  'surface-state',
765
772
  'hook-wiring',
766
773
  'stale-orphans',
774
+ 'rule-scope-drift',
767
775
  'mcp-mode',
768
776
  'mcp-beta-readiness',
769
777
  'offline-readiness',
@@ -783,8 +791,20 @@ const MANIFEST_REQUIRED_CHECK_IDS: ReadonlySet<string> = new Set([
783
791
  'unsupported-combos',
784
792
  ]);
785
793
 
786
- /** Project-root-relative path of the ADR-020 global-only consumer marker. */
787
- const BRIDGE_MARKER_RELATIVE = 'agents/.event4u-bridge.yml';
794
+ /**
795
+ * Signals a set-up global-only consumer. The `.event4u-bridge.yml` marker was
796
+ * retired (ADR-020 amendment 2026-07-13); the durable project-side signal is
797
+ * the install-mode marker (written on every install) or the `agents/overrides/`
798
+ * scaffold. Either is sufficient.
799
+ */
800
+ const INSTALL_MODE_MARKER_RELATIVE = 'agents/.agent-state/install-mode.txt';
801
+
802
+ function _is_global_only_consumer(project_root: string): boolean {
803
+ return (
804
+ isFile(path.join(project_root, INSTALL_MODE_MARKER_RELATIVE)) ||
805
+ isDir(path.join(project_root, 'agents', 'overrides'))
806
+ );
807
+ }
788
808
 
789
809
  /** Repair targets that `--repair <id>` accepts. */
790
810
  const REPAIR_IDS = ['wizard-state'] as const;
@@ -1091,15 +1111,16 @@ function _check_global_binary(project_root: string): Dict {
1091
1111
  remedy: 'agent-config upgrade (refresh the global install + plugin)',
1092
1112
  };
1093
1113
  }
1094
- const bridge = path.join(project_root, 'agents', '.event4u-bridge.yml');
1095
- const bridge_note = isFile(bridge) ? '' : ' · no project bridge marker';
1114
+ const consumer_note = _is_global_only_consumer(project_root)
1115
+ ? ''
1116
+ : ' · no project install marker';
1096
1117
  return {
1097
1118
  id: 'global-binary',
1098
1119
  status: 'ok',
1099
- message: `on PATH (${binary}); version ${binary_v || 'unknown'}${bridge_note}`,
1100
- remedy: !bridge_note
1120
+ message: `on PATH (${binary}); version ${binary_v || 'unknown'}${consumer_note}`,
1121
+ remedy: !consumer_note
1101
1122
  ? ''
1102
- : 'agent-config refresh --project (scaffold the bridge marker)',
1123
+ : 'agent-config refresh --project (scaffold agents/overrides/)',
1103
1124
  };
1104
1125
  }
1105
1126
 
@@ -1590,6 +1611,130 @@ function _check_stale_orphans(): Dict {
1590
1611
  };
1591
1612
  }
1592
1613
 
1614
+ /**
1615
+ * Per-file rule projection trees a consumer install writes (basenames match
1616
+ * the shipped `dist/agent-src/rules` tree). The first one that exists and
1617
+ * holds ≥ 1 `.md` rule is the surface we diff against the consumer's scope.
1618
+ */
1619
+ const _RULE_PROJECTION_DIRS: readonly string[] = [
1620
+ '.augment/rules',
1621
+ '.windsurf/rules',
1622
+ '.cursor/rules',
1623
+ '.clinerules',
1624
+ ];
1625
+
1626
+ /**
1627
+ * Rule-scope drift (road-to-request-scoped-rule-load, 9.0 consumer flip).
1628
+ *
1629
+ * After the 9.0 default flip to consumer-scoped rule projection, a consumer
1630
+ * whose settings now exclude maintainer-only workspaces may still carry a
1631
+ * pre-flip FULL/GLOBAL rule projection on disk. This check diffs the scope
1632
+ * the consumer's settings imply against the rules actually projected and
1633
+ * reports the leftover set as an ACTIONABLE diff (not a bare boolean).
1634
+ *
1635
+ * The scope-derived expected set reuses the ONE install-time scoping surface
1636
+ * (`rule_scope.ts`), so doctor's diagnosis can never drift from the install
1637
+ * semantics. `excludedRuleBasenames` returns exactly the rules PRESENT in the
1638
+ * projected tree that the current scope says should NOT arrive.
1639
+ *
1640
+ * Degrades gracefully: `skipped` in the agent-config source repo (the in-repo
1641
+ * `.augment/rules` is the full generated projection, not a consumer surface)
1642
+ * and `skipped` when no projected rule tree is present to inspect.
1643
+ */
1644
+ function _check_rule_scope_drift(project_root: string): Dict {
1645
+ if (_is_source_repo(project_root)) {
1646
+ return {
1647
+ id: 'rule-scope-drift',
1648
+ status: 'skipped',
1649
+ message: 'agent-config source repo — no consumer rule projection to inspect',
1650
+ remedy: '',
1651
+ };
1652
+ }
1653
+
1654
+ // Consumer projection scope (absent / empty settings → legacy-all: every
1655
+ // rule ships, so nothing is ever out of scope).
1656
+ let scope: RuleScope = LEGACY_ALL;
1657
+ const settings_file = project_settings_path(project_root);
1658
+ if (isFile(settings_file)) {
1659
+ const loaded = yamlSafeLoad(readText0(settings_file));
1660
+ const raw =
1661
+ typeof loaded === 'object' && loaded !== null && !Array.isArray(loaded)
1662
+ ? (loaded as Dict)
1663
+ : {};
1664
+ scope = ruleScopeFromSettings(raw);
1665
+ }
1666
+
1667
+ // First projected rule tree that exists and holds a `.md` rule.
1668
+ let rules_dir: string | null = null;
1669
+ let found = 0;
1670
+ for (const rel of _RULE_PROJECTION_DIRS) {
1671
+ const cand = path.join(project_root, rel);
1672
+ if (!isDir(cand)) {
1673
+ continue;
1674
+ }
1675
+ let count = 0;
1676
+ try {
1677
+ for (const name of fs.readdirSync(cand)) {
1678
+ if (name.endsWith('.md') && isFile(path.join(cand, name))) {
1679
+ count += 1;
1680
+ }
1681
+ }
1682
+ } catch {
1683
+ continue;
1684
+ }
1685
+ if (count > 0) {
1686
+ rules_dir = cand;
1687
+ found = count;
1688
+ break;
1689
+ }
1690
+ }
1691
+ if (rules_dir === null) {
1692
+ return {
1693
+ id: 'rule-scope-drift',
1694
+ status: 'skipped',
1695
+ message: 'no projected rule tree found (.augment/rules, .windsurf/rules, …) — nothing to inspect',
1696
+ remedy: '',
1697
+ };
1698
+ }
1699
+
1700
+ let unexpected: string[];
1701
+ try {
1702
+ unexpected = excludedRuleBasenames(rules_dir, scope);
1703
+ } catch (exc) {
1704
+ const rel = relativeTo(rules_dir, project_root) ?? rules_dir;
1705
+ return {
1706
+ id: 'rule-scope-drift',
1707
+ status: 'fail',
1708
+ message: `cannot inspect ${rel}: ${osErrorStr(exc)}`,
1709
+ remedy: 'check read permissions on the projected rule tree',
1710
+ };
1711
+ }
1712
+
1713
+ const rel_dir = relativeTo(rules_dir, project_root) ?? rules_dir;
1714
+ if (unexpected.length === 0) {
1715
+ return {
1716
+ id: 'rule-scope-drift',
1717
+ status: 'ok',
1718
+ message: `rule projection matches scope (${found} rules in ${rel_dir})`,
1719
+ remedy: '',
1720
+ };
1721
+ }
1722
+ const expected = found - unexpected.length;
1723
+ const sample = unexpected.slice(0, 8);
1724
+ const more = unexpected.length - sample.length;
1725
+ const names = sample.join(', ') + (more > 0 ? `, +${more} more` : '');
1726
+ return {
1727
+ id: 'rule-scope-drift',
1728
+ status: 'warn',
1729
+ message:
1730
+ `rule-scope drift in ${rel_dir}: expected ${expected} rules, found ${found} — ` +
1731
+ `${unexpected.length} unexpected (out of scope): ${names}`,
1732
+ remedy:
1733
+ 're-run `agent-config init --project` (or `agent-config global`) to re-project rules ' +
1734
+ 'under the current scope — these are leftover from a pre-9.0 full/global projection',
1735
+ };
1736
+ }
1737
+
1593
1738
  function _check_python_runtime(): Dict {
1594
1739
  // Post-teardown: the package runtime is TypeScript-on-`tsx`. python3 is no
1595
1740
  // longer a runtime dependency, so this check no longer probes for an
@@ -2525,6 +2670,7 @@ function _run_checks(
2525
2670
  'surface-state': _check_surface_state,
2526
2671
  'hook-wiring': _check_hook_wiring,
2527
2672
  'stale-orphans': _check_stale_orphans,
2673
+ 'rule-scope-drift': () => _check_rule_scope_drift(project_root),
2528
2674
  'manifest-integrity': () => _check_manifest_integrity(manifest),
2529
2675
  'lockfile-freshness': () => _check_lockfile_freshness(manifest),
2530
2676
  'bridge-drift': () =>
@@ -2582,8 +2728,8 @@ function _check_bridge_drift_no_manifest(bridge_present: boolean): Dict {
2582
2728
  id: 'bridge-drift',
2583
2729
  status: 'skipped',
2584
2730
  message:
2585
- 'no project lockfile and no bridge marker → drift check ' +
2586
- 'not applicable',
2731
+ 'no project lockfile and no consumer install marker → drift ' +
2732
+ 'check not applicable',
2587
2733
  remedy:
2588
2734
  'run `agent-config init` (project install) or ' +
2589
2735
  '`agent-config refresh --project` (global-only consumer)',
@@ -2604,6 +2750,7 @@ function _run_checks_no_manifest(
2604
2750
  'surface-state': _check_surface_state,
2605
2751
  'hook-wiring': _check_hook_wiring,
2606
2752
  'stale-orphans': _check_stale_orphans,
2753
+ 'rule-scope-drift': () => _check_rule_scope_drift(project_root),
2607
2754
  'manifest-integrity': () => _skipped_manifest_check('manifest-integrity'),
2608
2755
  'lockfile-freshness': () => _skipped_manifest_check('lockfile-freshness'),
2609
2756
  'bridge-drift': () => _check_bridge_drift_no_manifest(bridge_present),
@@ -2647,7 +2794,7 @@ function _run_no_manifest(
2647
2794
  print(` 📍 project_root: ${project_root} (origin: ${origin})`);
2648
2795
  if (bridge_present) {
2649
2796
  print(
2650
- ' ℹ️ global-only consumer: bridge marker present, no ' +
2797
+ ' ℹ️ global-only consumer: install marker present, no ' +
2651
2798
  'project lockfile (expected under ADR-020)',
2652
2799
  );
2653
2800
  print(
@@ -2655,7 +2802,7 @@ function _run_no_manifest(
2655
2802
  'to project-local distributed tools',
2656
2803
  );
2657
2804
  } else {
2658
- eprint(` ⚠️ no project lockfile and no bridge marker at ${project_root}`);
2805
+ eprint(` ⚠️ no project lockfile and no consumer install marker at ${project_root}`);
2659
2806
  eprint(
2660
2807
  ' run `agent-config init` (project install) or ' +
2661
2808
  '`agent-config refresh --project` (global-only consumer)',
@@ -2998,7 +3145,7 @@ function main(argv: string[] | null = null): number {
2998
3145
  const manifest_pth = installed_tools.manifest_path(project_root);
2999
3146
  const manifest = installed_tools.read_manifest(manifest_pth);
3000
3147
  if (manifest === null) {
3001
- const bridge_present = isFile(path.join(project_root, BRIDGE_MARKER_RELATIVE));
3148
+ const bridge_present = _is_global_only_consumer(project_root);
3002
3149
  return _run_no_manifest(opts, project_root, origin, bridge_present);
3003
3150
  }
3004
3151
 
@@ -3099,6 +3246,7 @@ export {
3099
3246
  _check_mcp_mode,
3100
3247
  _check_offline_readiness,
3101
3248
  _check_stale_orphans,
3249
+ _check_rule_scope_drift,
3102
3250
  _check_python_runtime,
3103
3251
  _check_humanizer_runtime,
3104
3252
  _check_mcp_beta_readiness,
@@ -3122,7 +3270,8 @@ export {
3122
3270
  CHECK_IDS,
3123
3271
  GLOBAL_CHECK_IDS,
3124
3272
  MANIFEST_REQUIRED_CHECK_IDS,
3125
- BRIDGE_MARKER_RELATIVE,
3273
+ INSTALL_MODE_MARKER_RELATIVE,
3274
+ _is_global_only_consumer,
3126
3275
  REPAIR_IDS,
3127
3276
  MCP_BETA_GATES,
3128
3277
  STATUS_SYMBOLS,