@event4u/agent-config 2.10.0 → 2.11.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 (82) hide show
  1. package/.agent-src/commands/agents.md +1 -0
  2. package/.agent-src/commands/challenge-me.md +1 -0
  3. package/.agent-src/commands/chat-history.md +1 -0
  4. package/.agent-src/commands/context.md +1 -0
  5. package/.agent-src/commands/council.md +1 -0
  6. package/.agent-src/commands/feature.md +1 -0
  7. package/.agent-src/commands/fix.md +1 -0
  8. package/.agent-src/commands/grill-me.md +1 -0
  9. package/.agent-src/commands/judge.md +1 -0
  10. package/.agent-src/commands/memory.md +1 -0
  11. package/.agent-src/commands/module.md +1 -0
  12. package/.agent-src/commands/onboard.md +32 -4
  13. package/.agent-src/commands/optimize.md +1 -0
  14. package/.agent-src/commands/override.md +1 -0
  15. package/.agent-src/commands/roadmap.md +1 -0
  16. package/.agent-src/commands/tests.md +1 -0
  17. package/.agent-src/skills/nextjs-patterns/SKILL.md +203 -0
  18. package/.agent-src/skills/symfony-workflow/SKILL.md +173 -0
  19. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +4 -0
  20. package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +3 -0
  21. package/.agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.py +162 -0
  22. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +24 -6
  23. package/.agent-src/templates/scripts/work_engine/scoring/decision_engine.py +351 -0
  24. package/.claude-plugin/marketplace.json +3 -1
  25. package/CHANGELOG.md +37 -0
  26. package/README.md +37 -8
  27. package/config/agent-settings.template.yml +57 -0
  28. package/docs/architecture.md +1 -1
  29. package/docs/contracts/STABILITY.md +16 -0
  30. package/docs/contracts/adr-chat-history-split.md +1 -0
  31. package/docs/contracts/adr-forecast-construction-shape.md +1 -0
  32. package/docs/contracts/adr-gtm-context-spine.md +1 -0
  33. package/docs/contracts/adr-level-6-productization.md +147 -0
  34. package/docs/contracts/adr-settings-sync-engine.md +1 -0
  35. package/docs/contracts/adr-wing4-context-spine.md +1 -0
  36. package/docs/contracts/agent-memory-contract.md +1 -0
  37. package/docs/contracts/agents-md-tech-stack.md +1 -0
  38. package/docs/contracts/audit-log-v1.md +1 -0
  39. package/docs/contracts/command-clusters.md +1 -0
  40. package/docs/contracts/command-surface-tiers.md +1 -0
  41. package/docs/contracts/context-paths.md +1 -0
  42. package/docs/contracts/cost-profile-defaults.md +105 -0
  43. package/docs/contracts/cross-wing-handoff.md +1 -0
  44. package/docs/contracts/decision-engine-gates.md +115 -0
  45. package/docs/contracts/decision-trace-v1.md +1 -0
  46. package/docs/contracts/file-ownership-matrix.md +1 -0
  47. package/docs/contracts/hook-architecture-v1.md +1 -0
  48. package/docs/contracts/implement-ticket-flow.md +1 -0
  49. package/docs/contracts/installed-tools-lockfile.md +1 -0
  50. package/docs/contracts/kernel-membership.md +1 -0
  51. package/docs/contracts/linear-ai-rules-inclusion.md +1 -0
  52. package/docs/contracts/linear-ai-three-layers.md +1 -0
  53. package/docs/contracts/linter-structural-model.md +1 -0
  54. package/docs/contracts/load-context-budget-model.md +1 -0
  55. package/docs/contracts/load-context-schema.md +1 -0
  56. package/docs/contracts/memory-visibility-v1.md +1 -0
  57. package/docs/contracts/one-off-script-lifecycle.md +1 -0
  58. package/docs/contracts/orchestration-dsl-v1.md +1 -0
  59. package/docs/contracts/package-self-orientation.md +1 -0
  60. package/docs/contracts/persona-schema.md +1 -0
  61. package/docs/contracts/release-trunk-sync.md +104 -0
  62. package/docs/contracts/roadmap-complexity-standard.md +1 -0
  63. package/docs/contracts/rule-classification.md +1 -0
  64. package/docs/contracts/rule-interactions.md +26 -0
  65. package/docs/contracts/rule-priority-hierarchy.md +1 -0
  66. package/docs/contracts/rule-router.md +1 -0
  67. package/docs/contracts/settings-sync-yaml-subset.md +1 -0
  68. package/docs/contracts/skill-domains.md +1 -0
  69. package/docs/contracts/tier-3-contrib-plugin.md +1 -0
  70. package/docs/contracts/ui-stack-extension.md +1 -0
  71. package/docs/contracts/ui-track-flow.md +1 -0
  72. package/docs/customization.md +1 -1
  73. package/docs/getting-started.md +3 -1
  74. package/docs/installation.md +8 -6
  75. package/package.json +1 -1
  76. package/scripts/check_beta_review_markers.py +127 -0
  77. package/scripts/check_release_trunk_sync.py +152 -0
  78. package/scripts/install.py +3 -3
  79. package/scripts/schemas/command.schema.json +5 -0
  80. package/scripts/skill_linter.py +11 -2
  81. package/scripts/smoke_quickstart.py +134 -0
  82. package/scripts/validate_decision_engine.py +124 -0
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Hook architecture v1
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # `/implement-ticket` — Flow Contract
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Installed-Tools Lockfile — Wire Contract
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Linear AI — rules inclusion list
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Linear AI — three-layer split rationale
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Linter Structural Model
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # `load_context:` Budget Accounting Model
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # `load_context:` Frontmatter Schema
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Memory-visibility v1
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # One-off-script lifecycle
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Orchestration DSL v1
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Package Self-Orientation
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Persona Schema — two-tier (Core / Specialist)
@@ -0,0 +1,104 @@
1
+ ---
2
+ stability: beta
3
+ keep-beta-until: 2026-08-12
4
+ ---
5
+
6
+ # ADR — Release-trunk sync: main fast-forwards on every tag
7
+
8
+ > **Status:** Decided · 2026-05-14
9
+ > **Context:** PR #43 feedback (Level-5/6 product rating) and PR #143
10
+ > revealed `main` lagging the latest tag by N skills + rules at multiple
11
+ > points across the 2.x cycle. External readers landing on `main`
12
+ > consistently saw stale README counts and missing skill catalogues
13
+ > relative to the npm/Packagist artefact.
14
+ > **Closes:** [Road to Productization](../../agents/roadmaps/road-to-productization.md) § P1.2.
15
+
16
+ ## Decision
17
+
18
+ Every tagged release (`X.Y.Z`) **fast-forwards `main` to the tag's
19
+ commit as the final step of the release pipeline**. No exceptions. No
20
+ grace period.
21
+
22
+ The fast-forward is owned by [`scripts/release.py`](../../scripts/release.py)
23
+ and runs after the GitHub Release is published. The release pipeline
24
+ is **not green** until `main == <new-tag>` at the remote.
25
+
26
+ `main` is therefore a **moving stable trunk pointer**, not a feature
27
+ branch. External readers (README, AGENTS.md, marketplace metadata, npm
28
+ tarball provenance) reading `main` see the artefact that was last
29
+ published, not work-in-progress.
30
+
31
+ ## Protocol
32
+
33
+ 1. `scripts/release.py` cuts `release/X.Y.Z`, bumps version files,
34
+ opens a release PR against `main`, waits for CI, merges.
35
+ 2. The merge commit on `main` becomes the tag's commit; the tag is
36
+ pushed.
37
+ 3. `publish-npm.yml` and the marketplace flow trigger on the tag.
38
+ 4. The release pipeline asserts `git rev-parse origin/main ==
39
+ git rev-parse refs/tags/X.Y.Z` before exit-0.
40
+ 5. If a hotfix lands on `release/X.Y.Z` after step 1 but before step 4,
41
+ the FF still happens — release-branch commits are part of the
42
+ release, not a separate trunk.
43
+
44
+ ### Why fast-forward, not merge
45
+
46
+ Fast-forward keeps `main` linear with the tag history. A merge-commit
47
+ on top of the tag would put `main` at a SHA that is **not** the tag's
48
+ SHA, re-introducing the exact divergence this contract closes.
49
+
50
+ If a fast-forward is impossible (force-push to `main`, divergent
51
+ history, abandoned release-prep), the pipeline **fails loudly**; the
52
+ operator either resets `main` manually with an audit trail or aborts
53
+ the release.
54
+
55
+ ## CI Gate (P1.3)
56
+
57
+ [`scripts/check_release_trunk_sync.py`](../../scripts/check_release_trunk_sync.py)
58
+ runs on every `release/X.Y.Z` branch (detected by `git rev-parse
59
+ --abbrev-ref HEAD` matching `^release/\d+\.\d+\.\d+$`).
60
+
61
+ It enforces: **`main` is at most ONE tagged release behind the
62
+ release-prep branch's target version.**
63
+
64
+ - On `release/2.11.0`: `main` may be at `2.10.0` or `2.11.0`. `2.9.0`
65
+ or older → **hard fail**.
66
+ - On any other branch class (feature, fix, chore, docs, the agent's
67
+ own `feat/road-to-productization` branch): the check is a **no-op**
68
+ exit-0 — feature branches never trip the gate.
69
+ - Wired into `task ci` as `check-release-trunk-sync`. No warning-only
70
+ mode; the exit code is the gate.
71
+
72
+ ### Bootstrap mode
73
+
74
+ When the repo state does not yet match the gate (transitional first
75
+ run after this contract lands), the check reads
76
+ `docs/contracts/release-trunk-sync.bootstrap` for an opt-out window
77
+ keyed by current version. The bootstrap file is purged at the next
78
+ release. Absence of the file = gate is live.
79
+
80
+ ## Rollback
81
+
82
+ Revertible by removing `check-release-trunk-sync` from `Taskfile.yml`
83
+ and deleting `scripts/check_release_trunk_sync.py`. No state, no
84
+ schema, no migration. Branch-detection key (`release/X.Y.Z`) is
85
+ already used by `scripts/release.py` so removing this contract does
86
+ not orphan the convention.
87
+
88
+ ## Risks
89
+
90
+ | # | Risk | Mitigation |
91
+ |---|---|---|
92
+ | 1 | Gate fires on feature branches mid-PR | Branch-name regex; non-`release/` branches no-op exit-0 |
93
+ | 2 | Hotfix release leaves `main` behind | FF runs **after** hotfix commits land on the release branch |
94
+ | 3 | Manual tag (no `scripts/release.py`) skips the FF | Out of scope of this contract — covered by `release-guard.yml` which fails on tag/version mismatch; manual tags already break the pipeline |
95
+ | 4 | Detached HEAD or shallow checkout breaks detection | Check gracefully exits-0 with a `::warning::` line when `git rev-parse --abbrev-ref HEAD == HEAD` (detached) |
96
+
97
+ ## See also
98
+
99
+ - [`scripts/release.py`](../../scripts/release.py) — release pipeline owner.
100
+ - [`.github/workflows/release-guard.yml`](../../.github/workflows/release-guard.yml)
101
+ — tag/version-file integrity gate (orthogonal: this contract handles
102
+ trunk position, release-guard handles version-string integrity).
103
+ - [`agents/roadmaps/road-to-productization.md`](../../agents/roadmaps/road-to-productization.md)
104
+ § Phase 1.
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Roadmap Complexity Standard
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Rule-Interaction Matrix
@@ -99,6 +100,31 @@ junior (yields). For `complements`, ordering is documentary only.
99
100
  in the rule files.
100
101
  - Skill ↔ rule interactions — the matrix is rule-only. Skills are
101
102
  invoked, not always-active.
103
+ - **Orchestration-layer surfaces** (AI Council, Memory, Work-Engine /
104
+ Decision-Engine): these are runtime systems, not `always`-rules.
105
+ Their interactions are governed by their own contracts and stay
106
+ out of this matrix by design — see "Out of scope" below.
107
+
108
+ ## Out of scope — orchestration surfaces (Council × Memory × Work-Engine)
109
+
110
+ The matrix is **rule-only**. The orchestration layer is governed by
111
+ dedicated contracts; cross-referencing them here would duplicate the
112
+ source of truth and weaken it. Canonical contracts:
113
+
114
+ | Surface | Canonical contract |
115
+ |---|---|
116
+ | Decision-Engine gates (`min_confidence`, `block_on_risk`, `require_memory_hits`, `on_block`) | [`decision-engine-gates.md`](decision-engine-gates.md) |
117
+ | Decision-trace shape (what the engine emits per phase) | [`decision-trace-v1.md`](decision-trace-v1.md) |
118
+ | Memory contract (entries, scopes, retention) | [`agent-memory-contract.md`](agent-memory-contract.md) |
119
+ | Memory visibility in the trace (`affected` keys) | [`memory-visibility-v1.md`](memory-visibility-v1.md) |
120
+ | AI-Council consultation flow | [`../skills/ai-council/SKILL.md`](../../.agent-src.uncompressed/skills/ai-council/SKILL.md) |
121
+
122
+ Where an `always`-rule **does** interact with one of these surfaces
123
+ (e.g. `non-destructive-by-default` gating a memory-driven action), the
124
+ gate lives in the rule and the precedence is captured in this matrix
125
+ as a rule-pair (the orchestration surface is the *occasion*, not a
126
+ participant). For Council ↔ Memory ↔ Work-Engine interactions among
127
+ themselves, the dedicated contracts above are authoritative.
102
128
 
103
129
  ## See also
104
130
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Rule Priority Hierarchy
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Settings-sync YAML subset
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Skill Domains — 6-domain taxonomy
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # Tier-3 contrib plugin pattern
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # UI Stack Extension — adding a new frontend stack to the UI track
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  stability: beta
3
+ keep-beta-until: 2026-08-12
3
4
  ---
4
5
 
5
6
  # UI Track — Flow Contract
@@ -139,7 +139,7 @@ Rules:
139
139
  | Setting | Default | Description |
140
140
  |---|---|---|
141
141
  | `agent_config_version` | *(empty)* | Exact semver pin of the agent-config release (see above). Empty = unpinned. |
142
- | `cost_profile` | `minimal` | Token budget (`minimal`, `balanced`, `full`, `custom`) |
142
+ | `cost_profile` | `balanced` | Token budget (`minimal`, `balanced`, `full`, `custom`) — rationale: [`docs/contracts/cost-profile-defaults.md`](contracts/cost-profile-defaults.md) |
143
143
  | `personal.user_name` | *(empty)* | User's first name for personalized responses |
144
144
  | `personal.minimal_output` | `true` | Suppress intermediate output |
145
145
  | `personal.play_by_play` | `false` | Share intermediate findings during analysis |
@@ -123,9 +123,11 @@ The system supports four configuration profiles:
123
123
  Set your profile in `.agent-settings.yml`:
124
124
 
125
125
  ```yaml
126
- cost_profile: minimal
126
+ cost_profile: balanced
127
127
  ```
128
128
 
129
+ `balanced` is the default — kernel + tier-1 auto-rules. Rationale:
130
+ [`docs/contracts/cost-profile-defaults.md`](contracts/cost-profile-defaults.md).
129
131
  You can override any individual setting. See [Customization](customization.md) for details.
130
132
 
131
133
  ---
@@ -240,8 +240,8 @@ wrapper (`./agent-config`) can fall through to it when no
240
240
  The orchestrator chains payload sync and bridge generation:
241
241
 
242
242
  ```bash
243
- bash scripts/install # defaults to cost_profile=minimal
244
- bash scripts/install --profile=balanced
243
+ bash scripts/install # defaults to cost_profile=balanced
244
+ bash scripts/install --profile=minimal
245
245
  bash scripts/install --force # overwrite existing bridges
246
246
  bash scripts/install --skip-bridges # payload only
247
247
  bash scripts/install --skip-sync # bridges only
@@ -287,7 +287,7 @@ regardless of which AI tool they use.** No per-developer plugin installation nee
287
287
  After initial setup, commit these files:
288
288
 
289
289
  ```
290
- .agent-settings.yml ← shared profile (e.g., cost_profile: minimal)
290
+ .agent-settings.yml ← shared profile (e.g., cost_profile: balanced)
291
291
  agents/installed-tools.lock ← AI bill of materials (ADR-008, Phase 3)
292
292
  .augment/ ← rules, skills, commands (symlinks)
293
293
  .cursor/rules/ ← Cursor rules (symlinks)
@@ -517,16 +517,18 @@ The system works immediately with sensible defaults. Optionally, create `.agent-
517
517
  to choose a profile:
518
518
 
519
519
  ```yaml
520
- cost_profile: minimal
520
+ cost_profile: balanced
521
521
  ```
522
522
 
523
523
  | Profile | What's active | For whom |
524
524
  |---|---|---|
525
- | `minimal` (default) | Rules + Skills only, zero overhead | New users, solo devs |
525
+ | `minimal` | Kernel only Iron-Law floor, zero router | Token-constrained agents |
526
526
  | `balanced` | + Runtime dispatcher + shell handler | Most teams |
527
527
  | `full` | + Tool adapters (GitHub, Jira) | Platform teams |
528
528
 
529
- No profile configured = `minimal` behavior. → [Full profile details](customization.md)
529
+ No profile configured = `balanced` behavior (default). Rationale:
530
+ [`docs/contracts/cost-profile-defaults.md`](contracts/cost-profile-defaults.md).
531
+ → [Full profile details](customization.md)
530
532
 
531
533
  ---
532
534
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "Shared agent configuration \u2014 skills, rules, commands, guidelines, and templates for AI coding tools",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Beta-review-marker checker for `docs/contracts/`.
4
+
5
+ Every contract whose frontmatter declares `stability: beta` MUST carry
6
+ exactly one of the following frontmatter markers (per
7
+ `docs/contracts/STABILITY.md` § Beta-review markers, ratified in
8
+ `road-to-productization.md` § P5.4):
9
+
10
+ - `promote-to: stable`
11
+ - `keep-beta-until: YYYY-MM-DD` (max 90 days from the last review)
12
+ - `superseded-by: <contract-id>`
13
+
14
+ Exit codes: 0 = clean, 1 = violations found, 3 = internal error.
15
+
16
+ Usage:
17
+ python3 scripts/check_beta_review_markers.py
18
+ python3 scripts/check_beta_review_markers.py --json
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import re
26
+ import sys
27
+ from dataclasses import asdict, dataclass
28
+ from datetime import date, timedelta
29
+ from pathlib import Path
30
+
31
+ ROOT = Path(__file__).resolve().parent.parent
32
+ CONTRACTS_DIR = Path("docs/contracts")
33
+
34
+ FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
35
+ STABILITY_RE = re.compile(r"^stability:\s*(\w+)\s*$", re.MULTILINE)
36
+ PROMOTE_RE = re.compile(r"^promote-to:\s*stable\s*$", re.MULTILINE)
37
+ KEEP_RE = re.compile(r"^keep-beta-until:\s*(\d{4}-\d{2}-\d{2})\s*$", re.MULTILINE)
38
+ SUPERSEDED_RE = re.compile(r"^superseded-by:\s*\S+\s*$", re.MULTILINE)
39
+
40
+ MAX_REVIEW_WINDOW_DAYS = 90
41
+
42
+
43
+ @dataclass
44
+ class Violation:
45
+ file: str
46
+ reason: str
47
+ severity: str # "error" | "warning"
48
+
49
+
50
+ def read_frontmatter(path: Path) -> str | None:
51
+ if not path.exists():
52
+ return None
53
+ txt = path.read_text(encoding="utf-8")
54
+ m = FRONTMATTER_RE.match(txt)
55
+ return m.group(1) if m else None
56
+
57
+
58
+ def check_one(path: Path, today: date) -> list[Violation]:
59
+ fm = read_frontmatter(path)
60
+ if fm is None:
61
+ return []
62
+ sm = STABILITY_RE.search(fm)
63
+ if not sm or sm.group(1) != "beta":
64
+ return []
65
+ markers = [
66
+ ("promote-to", bool(PROMOTE_RE.search(fm))),
67
+ ("keep-beta-until", bool(KEEP_RE.search(fm))),
68
+ ("superseded-by", bool(SUPERSEDED_RE.search(fm))),
69
+ ]
70
+ set_markers = [name for name, present in markers if present]
71
+ rel = str(path.relative_to(ROOT))
72
+ if not set_markers:
73
+ return [Violation(
74
+ file=rel,
75
+ reason="stability=beta but no review marker; add one of "
76
+ "`promote-to: stable` | `keep-beta-until: <date>` | "
77
+ "`superseded-by: <id>` (see STABILITY.md § Beta-review markers)",
78
+ severity="error",
79
+ )]
80
+ if len(set_markers) > 1:
81
+ return [Violation(
82
+ file=rel,
83
+ reason=f"multiple beta-review markers set ({', '.join(set_markers)}); "
84
+ "exactly one is allowed",
85
+ severity="error",
86
+ )]
87
+ km = KEEP_RE.search(fm)
88
+ if km:
89
+ review_date = date.fromisoformat(km.group(1))
90
+ max_date = today + timedelta(days=MAX_REVIEW_WINDOW_DAYS)
91
+ if review_date > max_date:
92
+ return [Violation(
93
+ file=rel,
94
+ reason=f"keep-beta-until={review_date} exceeds the "
95
+ f"{MAX_REVIEW_WINDOW_DAYS}-day window (max: {max_date})",
96
+ severity="error",
97
+ )]
98
+ return []
99
+
100
+
101
+ def main() -> int:
102
+ ap = argparse.ArgumentParser()
103
+ ap.add_argument("--json", action="store_true", help="machine-readable output")
104
+ args = ap.parse_args()
105
+ today = date.today()
106
+ violations: list[Violation] = []
107
+ for p in sorted((ROOT / CONTRACTS_DIR).glob("*.md")):
108
+ violations.extend(check_one(p, today))
109
+ if args.json:
110
+ print(json.dumps({"violations": [asdict(v) for v in violations]}, indent=2))
111
+ else:
112
+ if not violations:
113
+ print("✅ All beta contracts carry a valid review marker.")
114
+ else:
115
+ for v in violations:
116
+ icon = "❌" if v.severity == "error" else "⚠️ "
117
+ print(f"{icon} {v.file}: {v.reason}")
118
+ print(f"\n{len(violations)} violation(s).")
119
+ return 1 if any(v.severity == "error" for v in violations) else 0
120
+
121
+
122
+ if __name__ == "__main__":
123
+ try:
124
+ sys.exit(main())
125
+ except Exception as exc: # pragma: no cover
126
+ print(f"internal error: {exc}", file=sys.stderr)
127
+ sys.exit(3)