@event4u/agent-config 2.25.0 → 2.26.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 (122) hide show
  1. package/.agent-src/commands/bug-fix.md +1 -0
  2. package/.agent-src/commands/feature/roadmap.md +2 -2
  3. package/.agent-src/commands/fix/seeder.md +3 -2
  4. package/.agent-src/commands/memory/add.md +3 -3
  5. package/.agent-src/commands/module/create.md +1 -0
  6. package/.agent-src/commands/module/explore.md +10 -6
  7. package/.agent-src/commands/onboard.md +9 -1
  8. package/.agent-src/commands/optimize/augmentignore.md +52 -20
  9. package/.agent-src/commands/optimize/rtk.md +56 -30
  10. package/.agent-src/commands/package-test.md +86 -10
  11. package/.agent-src/commands/quality-fix.md +49 -27
  12. package/.agent-src/commands/update-form-request-messages.md +2 -1
  13. package/.agent-src/contexts/augment-infrastructure.md +4 -7
  14. package/.agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -1
  15. package/.agent-src/contexts/contracts/research-schema.md +1 -1
  16. package/.agent-src/contexts/execution/interrupt-examples.md +34 -0
  17. package/.agent-src/contexts/skills-and-commands.md +2 -2
  18. package/.agent-src/rules/architecture.md +24 -10
  19. package/.agent-src/rules/artifact-drafting-protocol.md +6 -0
  20. package/.agent-src/rules/augment-edit-discipline.md +28 -0
  21. package/.agent-src/rules/augment-source-of-truth.md +2 -2
  22. package/.agent-src/rules/autonomous-execution.md +31 -0
  23. package/.agent-src/rules/context-hygiene.md +1 -1
  24. package/.agent-src/rules/domain-adoption-policy.md +4 -5
  25. package/.agent-src/rules/domain-safety-disclaimer.md +114 -0
  26. package/.agent-src/rules/domain-safety-pii.md +142 -0
  27. package/.agent-src/rules/domain-safety-retention.md +86 -0
  28. package/.agent-src/rules/downstream-changes.md +4 -4
  29. package/.agent-src/rules/framework-neutrality-in-generic-skills.md +130 -0
  30. package/.agent-src/rules/git-history-discipline.md +99 -0
  31. package/.agent-src/rules/minimal-safe-diff.md +6 -0
  32. package/.agent-src/rules/no-roadmap-references.md +4 -2
  33. package/.agent-src/rules/user-interrupt-priority.md +46 -0
  34. package/.agent-src/rules/verify-before-complete.md +11 -2
  35. package/.agent-src/skills/adversarial-review/SKILL.md +1 -1
  36. package/.agent-src/skills/ai-council/SKILL.md +1 -0
  37. package/.agent-src/skills/api-endpoint/SKILL.md +58 -154
  38. package/.agent-src/skills/api-testing/SKILL.md +11 -0
  39. package/.agent-src/skills/code-refactoring/SKILL.md +36 -30
  40. package/.agent-src/skills/code-review/SKILL.md +41 -36
  41. package/.agent-src/skills/context-authoring/SKILL.md +1 -1
  42. package/.agent-src/skills/dashboard-design/SKILL.md +1 -2
  43. package/.agent-src/skills/database/SKILL.md +8 -3
  44. package/.agent-src/skills/dependency-upgrade/SKILL.md +65 -19
  45. package/.agent-src/skills/developer-like-execution/SKILL.md +25 -14
  46. package/.agent-src/skills/eloquent/SKILL.md +1 -1
  47. package/.agent-src/skills/feature-planning/SKILL.md +1 -1
  48. package/.agent-src/skills/file-editor/SKILL.md +45 -19
  49. package/.agent-src/skills/finishing-a-development-branch/SKILL.md +2 -2
  50. package/.agent-src/skills/git-workflow/SKILL.md +4 -4
  51. package/.agent-src/skills/laravel-api-endpoint/SKILL.md +187 -0
  52. package/.agent-src/skills/{dto-creator → laravel-dto}/SKILL.md +5 -4
  53. package/.agent-src/skills/{migration-creator → laravel-migration}/SKILL.md +11 -10
  54. package/.agent-src/skills/laravel-reverb/SKILL.md +3 -3
  55. package/.agent-src/skills/{websocket → laravel-websocket}/SKILL.md +4 -3
  56. package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
  57. package/.agent-src/skills/merge-conflicts/SKILL.md +49 -17
  58. package/.agent-src/skills/migration-architect/SKILL.md +6 -6
  59. package/.agent-src/skills/module-management/SKILL.md +1 -0
  60. package/.agent-src/skills/multi-tenancy/SKILL.md +15 -8
  61. package/.agent-src/skills/pest-testing/SKILL.md +18 -0
  62. package/.agent-src/skills/php-debugging/SKILL.md +28 -0
  63. package/.agent-src/skills/php-service/SKILL.md +3 -3
  64. package/.agent-src/skills/playwright-testing/SKILL.md +16 -1
  65. package/.agent-src/skills/project-analyzer/SKILL.md +68 -42
  66. package/.agent-src/skills/readme-writing-package/SKILL.md +94 -23
  67. package/.agent-src/skills/roadmap-management/SKILL.md +1 -1
  68. package/.agent-src/skills/rtk-output-filtering/SKILL.md +23 -8
  69. package/.agent-src/skills/rule-refactor/SKILL.md +145 -0
  70. package/.agent-src/skills/rule-writing/SKILL.md +34 -8
  71. package/.agent-src/skills/security/SKILL.md +38 -29
  72. package/.agent-src/skills/skill-reviewer/SKILL.md +1 -1
  73. package/.agent-src/skills/test-driven-development/SKILL.md +4 -4
  74. package/.agent-src/skills/test-performance/SKILL.md +6 -5
  75. package/.agent-src/skills/verify-completion-evidence/SKILL.md +24 -27
  76. package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  77. package/.agent-src/templates/copilot-instructions.md +2 -2
  78. package/.agent-src/templates/rule.md +2 -2
  79. package/.claude-plugin/marketplace.json +6 -4
  80. package/AGENTS.md +1 -1
  81. package/CHANGELOG.md +74 -170
  82. package/README.md +2 -2
  83. package/docs/architecture.md +2 -2
  84. package/docs/archive/CHANGELOG-pre-2.25.0.md +191 -0
  85. package/docs/catalog.md +17 -12
  86. package/docs/contracts/file-ownership-matrix.json +473 -43
  87. package/docs/contracts/kernel-membership.md +17 -0
  88. package/docs/contracts/smoke-contracts.md +8 -8
  89. package/docs/getting-started.md +1 -1
  90. package/docs/guidelines/php/api-design.md +1 -1
  91. package/docs/guidelines/php/controllers.md +1 -1
  92. package/docs/guidelines/php/resources.md +1 -1
  93. package/docs/guidelines/php/validations.md +1 -1
  94. package/package.json +1 -1
  95. package/scripts/build_linear_digest.py +0 -1
  96. package/scripts/lint_framework_leakage.py +348 -0
  97. package/scripts/lint_framework_leakage_allowlist.json +476 -0
  98. package/scripts/measure_augment_budget.py +6 -0
  99. package/scripts/schemas/command.schema.json +5 -0
  100. package/scripts/schemas/skill.schema.json +5 -0
  101. package/scripts/skill_linter.py +60 -7
  102. package/scripts/smoke/kernel.sh +4 -4
  103. package/scripts/smoke/router.sh +2 -2
  104. package/.agent-src/rules/agent-docs.md +0 -20
  105. package/.agent-src/rules/augment-portability.md +0 -23
  106. package/.agent-src/rules/capture-learnings.md +0 -19
  107. package/.agent-src/rules/docs-sync.md +0 -20
  108. package/.agent-src/rules/domain-safety-disclaimer-consulting.md +0 -52
  109. package/.agent-src/rules/domain-safety-disclaimer-financial.md +0 -54
  110. package/.agent-src/rules/domain-safety-disclaimer-legal.md +0 -49
  111. package/.agent-src/rules/domain-safety-disclaimer-medical.md +0 -56
  112. package/.agent-src/rules/domain-safety-export-redact.md +0 -65
  113. package/.agent-src/rules/domain-safety-logging-pii-floor.md +0 -55
  114. package/.agent-src/rules/domain-safety-pii-finance.md +0 -57
  115. package/.agent-src/rules/domain-safety-pii-marketing.md +0 -60
  116. package/.agent-src/rules/domain-safety-pii-recruiting.md +0 -56
  117. package/.agent-src/rules/domain-safety-pii-support.md +0 -57
  118. package/.agent-src/rules/domain-safety-retention-finance.md +0 -48
  119. package/.agent-src/rules/domain-safety-retention-support.md +0 -55
  120. package/.agent-src/rules/e2e-testing.md +0 -19
  121. package/.agent-src/rules/no-unsolicited-rebase.md +0 -107
  122. package/.agent-src/rules/post-push-rewrite-discipline.md +0 -70
@@ -145,6 +145,23 @@ Future edits to any kernel rule must keep the Iron-Law SHA stable
145
145
  (or land a deliberate ADR-tracked SHA update). Cap re-raise requires
146
146
  a new ADR.
147
147
 
148
+ ### § 4.2 — Post-P2.2 kernel addition (`user-interrupt-priority`)
149
+
150
+ After the P2.2 lock, `user-interrupt-priority` was admitted as the
151
+ 10th kernel rule. It satisfies criterion (1) (Iron Law: stop → ask
152
+ → resume on user-interrupt signals) and criterion (3a) (pre-send
153
+ gate — must fire before continuing the current task). The smoke
154
+ baseline is bumped accordingly:
155
+
156
+ - `scripts/smoke/kernel.sh` — `EXPECTED_KERNEL_COUNT=10`,
157
+ `EXPECTED_FENCE_CARRIERS=9`.
158
+ - `docs/contracts/smoke-contracts.md` § 3.1 — `10 kernel rules · 9
159
+ carry Iron-Law fences · 1 dispatch index · ≤ 2 budget breaches`.
160
+
161
+ The § 4 / § 4.1 tables remain the locked P2.2 baseline (9-rule
162
+ snapshot, 2026-05-06); the 10th rule is tracked separately here
163
+ until the next kernel re-measurement.
164
+
148
165
  † **agent-authority swap candidate (P1.4 ADR).** Sonnet 4.5 argues
149
166
  this is a routing index (zero Iron Law fences, dispatches to other
150
167
  kernel rules) and should be `compress-and-keep` (auto-tier-3),
@@ -51,11 +51,11 @@ constant in the script body and the row below.
51
51
  ### § 3.1 — Kernel (`scripts/smoke/kernel.sh`)
52
52
 
53
53
  ```
54
- 9 kernel rules · 8 carry Iron-Law fences · 1 dispatch index · ≤ 2 budget breaches
54
+ 10 kernel rules · 9 carry Iron-Law fences · 1 dispatch index · ≤ 2 budget breaches
55
55
  ```
56
56
 
57
- - **9 kernel rules** — fixed by [`kernel-membership.md`](kernel-membership.md).
58
- - **8 carry Iron-Law fences** — measured 2026-05-16. `agent-authority`
57
+ - **10 kernel rules** — fixed by [`kernel-membership.md`](kernel-membership.md).
58
+ - **9 carry Iron-Law fences** — measured 2026-05-16. `agent-authority`
59
59
  is the **dispatch index** (priority table pointing at the other four
60
60
  authority rules); it is structurally exempt from the Iron-Law-fence
61
61
  requirement and listed in the script's `EXEMPT_FROM_FENCE` set.
@@ -70,13 +70,13 @@ constant in the script body and the row below.
70
70
  ### § 3.2 — Router (`scripts/smoke/router.sh`)
71
71
 
72
72
  ```
73
- 75 router ids · 0 broken rule pointers · 35 routes_to refs · 2 missing contracts
73
+ 68 router ids · 0 broken rule pointers · 36 routes_to refs · 2 missing contracts
74
74
  ```
75
75
 
76
- - **75 ids** — 9 kernel + 24 tier_1 + 42 tier_2; every id resolves to
76
+ - **68 ids** — 10 kernel + 23 tier_1 + 35 tier_2; every id resolves to
77
77
  `.agent-src/rules/<id>.md`.
78
78
  - **0 broken rule pointers** — hard assertion; smoke fails on any miss.
79
- - **35 routes_to refs** across tier_1 + tier_2; resolver honours the
79
+ - **36 routes_to refs** across tier_1 + tier_2; resolver honours the
80
80
  four prefixes (`skill:`, `command:`, `guideline:`, `contract:`).
81
81
  - **2 missing contracts** — measured 2026-05-16:
82
82
  `contract:artifact-engagement-flow`,
@@ -130,7 +130,7 @@ the final baseline line) for CI summary parsing.
130
130
 
131
131
  | Symptom | Likely cause | Fix |
132
132
  |---|---|---|
133
- | `kernel.sh` reports > 8 missing fences | Kernel rule lost its Iron Law block during edit | Restore the fence; update `EXEMPT_FROM_FENCE` only for new dispatch indexes |
133
+ | `kernel.sh` reports > 9 missing fences | Kernel rule lost its Iron Law block during edit | Restore the fence; update `EXEMPT_FROM_FENCE` only for new dispatch indexes |
134
134
  | `router.sh` reports > 0 broken pointers | `router.json` references an id without a rule file | Add the rule or remove the route — never edit the smoke baseline up |
135
135
  | `schema.sh` reports FAILs | A skill / rule lost a required field | Restore via [`scripts/schemas/skill.schema.json`](../../scripts/schemas/skill.schema.json) |
136
136
  | `skills.sh` 5/5 random sample fails | Hand-edit broke frontmatter or renamed directory without updating `name:` | Restore filename ↔ slug coupling |
@@ -139,6 +139,6 @@ the final baseline line) for CI summary parsing.
139
139
 
140
140
  - [`measurement-baseline.md`](measurement-baseline.md) — measurement substrate.
141
141
  - [`cost-enforcement.md`](cost-enforcement.md) — cost ladder, sibling smoke surface.
142
- - [`kernel-membership.md`](kernel-membership.md) — the 9-rule kernel set.
142
+ - [`kernel-membership.md`](kernel-membership.md) — the 10-rule kernel set.
143
143
  - [`rule-router.md`](rule-router.md) — router contract.
144
144
  - `road-to-kernel-and-router.md` — kernel budget reduction path.
@@ -106,7 +106,7 @@ Your agent is now:
106
106
  - **Respecting your codebase** — no conflicting patterns
107
107
  - **Following standards** — consistent code quality
108
108
 
109
- This is enforced automatically by 84 rules. No configuration needed.
109
+ This is enforced automatically by 72 rules. No configuration needed.
110
110
 
111
111
  ---
112
112
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > API conventions — response format, status codes, pagination, error handling, rate limiting, route naming.
4
4
 
5
- **Related Skills:** `api-design`, `api-endpoint`, `api-testing`
5
+ **Related Skills:** `api-design`, `laravel-api-endpoint`, `api-testing`
6
6
  **Related Guidelines:** [controllers.md](controllers.md), [resources.md](resources.md)
7
7
 
8
8
  ## Response Format
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Project-specific controller conventions. Thin controllers, single-action pattern, OpenAPI annotations.
4
4
 
5
- **Related Skills:** `api-endpoint`, `laravel`, `openapi`
5
+ **Related Skills:** `laravel-api-endpoint`, `laravel`, `openapi`
6
6
  **Related Guidelines:** [validations.md](validations.md), [resources.md](resources.md)
7
7
 
8
8
  ## Core Rules
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Project-specific API Resource conventions. Base class, versioning (v1/v2), OpenAPI schemas.
4
4
 
5
- **Related Skills:** `api-endpoint`, `api-design`, `openapi`
5
+ **Related Skills:** `laravel-api-endpoint`, `api-design`, `openapi`
6
6
  **Related Guidelines:** [controllers.md](controllers.md)
7
7
 
8
8
  ## Core Rule
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Project-specific FormRequest conventions. Array syntax, route params, property mapping.
4
4
 
5
- **Related Skills:** `laravel-validation`, `api-endpoint`
5
+ **Related Skills:** `laravel-validation`, `laravel-api-endpoint`
6
6
  **Related Guidelines:** [controllers.md](controllers.md)
7
7
 
8
8
  ## Core Rules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "2.25.0",
3
+ "version": "2.26.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,
@@ -93,7 +93,6 @@ WORKSPACE: list[RuleEntry] = [
93
93
  TEAM: list[RuleEntry] = [
94
94
  RuleEntry("docker-commands"),
95
95
  RuleEntry("laravel-translations"),
96
- RuleEntry("e2e-testing"),
97
96
  RuleEntry("php-coding"),
98
97
  ]
99
98
 
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env python3
2
+ """Lint generic skills/rules/commands for framework/language leakage.
3
+
4
+ Exits 1 on hit; CI-blocking. Enforces
5
+ `.agent-src.uncompressed/rules/framework-neutrality-in-generic-skills.md`.
6
+
7
+ Allowlist legitimate cross-stack docs in
8
+ `scripts/lint_framework_leakage_allowlist.json`.
9
+
10
+ Carve-out semantics: an artifact whose filename or any parent directory
11
+ matches an explicit framework/language marker (e.g. `laravel-*`,
12
+ `nextjs-*`, `pest-*`) is exempt — these are correctly framework-specific.
13
+
14
+ Inventory exemption: descriptive files that name carve-outs as
15
+ *catalog entries* rather than mandating them in a generic skill are
16
+ exempt. This covers `contexts/**/*.md` (cross-reference tables,
17
+ guideline indexes) and the top-level `README.md` (skills inventory).
18
+ A linter that targets mandate-leakage cannot meaningfully scan an
19
+ inventory of mandate-bearing artifacts.
20
+
21
+ Auto cross-stack detection (Step 0.5 of audit roadmap): when a hit's
22
+ line OR any of the ±2 surrounding lines contains a pattern from a
23
+ different ecosystem family (php / js / python), the hit is marked
24
+ `cross_stack=True` and skipped without consulting the allowlist.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import json
30
+ import re
31
+ import sys
32
+ from pathlib import Path
33
+ from typing import Iterable
34
+
35
+ REPO_ROOT = Path(__file__).resolve().parent.parent
36
+ DEFAULT_PATHS = (
37
+ ".agent-src.uncompressed/skills",
38
+ ".agent-src.uncompressed/rules",
39
+ ".agent-src.uncompressed/commands",
40
+ )
41
+ ALLOWLIST_FILE = REPO_ROOT / "scripts/lint_framework_leakage_allowlist.json"
42
+
43
+ CARVE_OUT_PATTERNS = [
44
+ r"laravel", r"^php-", r"^eloquent", r"^blade", r"^livewire", r"^flux",
45
+ r"^pest-", r"^artisan-", r"^composer-", r"^jobs-events$", r"^symfony",
46
+ r"^nextjs", r"^react-", r"^async-python", r"^openapi$", r"^quality-tools",
47
+ r"^sql-writing", r"^tailwind", r"^terraform", r"^terragrunt", r"^traefik",
48
+ r"^mobile-e2e",
49
+ r"^project-analysis-(laravel|symfony|nextjs|react|node-express|zend-laminas)",
50
+ r"^docker", r"^aws-", r"^grafana", r"^playwright",
51
+ r"^laravel-", r"^docker-", r"^symfony-", r"^copilot-", r"^devcontainer",
52
+ r"-routing$",
53
+ ]
54
+ CARVE_OUT_RE = re.compile("|".join(CARVE_OUT_PATTERNS), re.IGNORECASE)
55
+
56
+ LEAKAGE: dict[str, list[str]] = {
57
+ "Laravel": [
58
+ r"\bLaravel\b", r"\bEloquent\b", r"\bArtisan\b", r"\bFormRequest\b",
59
+ r"\bForm Request\b", r"\bBlade\b(?! Runner)", r"\bLivewire\b",
60
+ r"\bResource::(make|collection)\b", r"\bModel::\b",
61
+ r"\bapp/Http/", r"\broutes/(api|web)\.php",
62
+ r"\bdatabase/(migrations|seeders|factories)\b",
63
+ r"\bphp artisan\b", r"\bIlluminate\\\\", r"\bIlluminate\\",
64
+ r"\bbootstrap/app\.php",
65
+ ],
66
+ "PHP": [
67
+ r"\bPHPStan\b", r"\bPest\b(?! Control)", r"\bPHPUnit\b", r"\bRector\b",
68
+ r"\bECS\b", r"\bcomposer\.json\b", r"\bvendor/bin/",
69
+ r"\bdeclare\(strict_types=1\)", r"\.php\b",
70
+ r"\bnamespace App\\\\", r"\bnamespace App\\",
71
+ r"\bcomposer (require|install|update|dump-autoload)\b",
72
+ ],
73
+ "Symfony": [
74
+ r"\bSymfony\b", r"\bbin/console\b", r"\bDoctrine\b", r"\bTwig\b",
75
+ ],
76
+ "JS-specific": [
77
+ r"\bpackage\.json\b",
78
+ r"\bnpm (install|run|test|ci)\b",
79
+ r"\byarn (install|add|test)\b",
80
+ r"\bpnpm (install|add|run|test)\b",
81
+ r"\bnode_modules\b",
82
+ ],
83
+ "Python-specific": [
84
+ r"\bpyproject\.toml\b", r"\brequirements\.txt\b",
85
+ r"\bpip install\b", r"\bpytest\b",
86
+ ],
87
+ }
88
+
89
+ FAMILY: dict[str, str] = {
90
+ "Laravel": "php", "PHP": "php", "Symfony": "php",
91
+ "JS-specific": "js", "Python-specific": "python",
92
+ }
93
+
94
+
95
+ # Cross-stack hint keywords. Their presence near a hit signals legitimate
96
+ # multi-stack documentation. They do NOT themselves produce hits.
97
+ CROSS_STACK_HINTS: dict[str, list[str]] = {
98
+ "ruby": [r"\bRails\b", r"\bbin/rails\b", r"\bGemfile\b", r"\bbundle exec\b"],
99
+ "python": [r"\bDjango\b", r"\bFastAPI\b", r"\bFlask\b", r"\bpoetry\b",
100
+ r"\buv (add|sync|run|pip)\b", r"\bvenv\b"],
101
+ "node": [r"\bExpress\b", r"\bNext\.?js\b", r"\bNode\.?js\b", r"\bnpx\b",
102
+ r"\bvitest\b", r"\bjest\b", r"\beslint\b", r"\bprettier\b"],
103
+ "go": [r"\bgo (test|build|run|mod)\b", r"\bgolangci-lint\b", r"\bGoLand\b"],
104
+ "rust": [r"\bcargo (test|build|run|check|fmt|clippy|add|update)\b",
105
+ r"\bClippy\b", r"\brustfmt\b", r"\bCargo\.toml\b"],
106
+ "dotnet": [r"\bdotnet (test|build|run|add|restore)\b", r"\b\.NET\b"],
107
+ "java": [r"\bSpring\b", r"\bmvn (test|clean|install|package)\b",
108
+ r"\bgradle\b", r"\bMaven\b"],
109
+ }
110
+ CROSS_STACK_RE = {fam: re.compile("|".join(pats)) for fam, pats in CROSS_STACK_HINTS.items()}
111
+
112
+ FRONTMATTER_FRAMEWORK_RE = re.compile(
113
+ r"^---\s*\n(.*?)\n---", re.DOTALL | re.MULTILINE
114
+ )
115
+ # Match top-level `framework:` or nested `scope.framework:` (one or more
116
+ # leading spaces tolerated for the nested form).
117
+ FRAMEWORK_KEY_RE = re.compile(
118
+ r"^(?:framework|\s+framework)\s*:\s*(\S+)", re.MULTILINE
119
+ )
120
+
121
+
122
+ def is_carve_out(path: Path) -> bool:
123
+ for p in path.parts:
124
+ stem = p.removesuffix(".md")
125
+ if CARVE_OUT_RE.search(stem):
126
+ return True
127
+ return False
128
+
129
+
130
+ def is_inventory_file(path: Path) -> bool:
131
+ """Descriptive files that name carve-outs in catalog tables.
132
+
133
+ A leakage linter targets *mandates* in generic skills/rules/commands.
134
+ Files that list carve-outs (skills inventory, guideline indexes,
135
+ cross-reference tables) name `laravel-*`, PHPStan, npm, etc. as data,
136
+ not as the only path. Scanning them produces structural false
137
+ positives that no allowlist can sensibly cover.
138
+
139
+ Exempt scopes:
140
+ - `<src>/contexts/**/*.md` — cross-reference tables, guideline
141
+ catalogs, infrastructure maps.
142
+ - top-level `README.md` directly under `.agent-src.uncompressed/`
143
+ or `.agent-src/` — package surface inventory.
144
+ """
145
+ try:
146
+ rel = path.relative_to(REPO_ROOT)
147
+ except ValueError:
148
+ return False
149
+ parts = rel.parts
150
+ if "contexts" in parts:
151
+ return True
152
+ if rel.name == "README.md" and len(parts) == 2 and parts[0] in {
153
+ ".agent-src.uncompressed", ".agent-src",
154
+ }:
155
+ return True
156
+ return False
157
+
158
+
159
+ def has_framework_frontmatter(path: Path) -> str | None:
160
+ """Return the framework name if the file declares one in YAML frontmatter."""
161
+ try:
162
+ text = path.read_text(encoding="utf-8", errors="ignore")
163
+ except OSError:
164
+ return None
165
+ m = FRONTMATTER_FRAMEWORK_RE.match(text)
166
+ if not m:
167
+ return None
168
+ fm = m.group(1)
169
+ key = FRAMEWORK_KEY_RE.search(fm)
170
+ if key:
171
+ val = key.group(1).strip().strip('"').strip("'")
172
+ if val and val.lower() not in {"none", "null", "~", ""}:
173
+ return val
174
+ return None
175
+
176
+
177
+ def _load_allowlist() -> dict:
178
+ if not ALLOWLIST_FILE.is_file():
179
+ return {"entries": []}
180
+ try:
181
+ data = json.loads(ALLOWLIST_FILE.read_text(encoding="utf-8"))
182
+ except (OSError, json.JSONDecodeError):
183
+ return {"entries": []}
184
+ return data
185
+
186
+
187
+ def _allowlisted(rel_path: str, line_no: int, allowlist: dict) -> bool:
188
+ for entry in allowlist.get("entries", []):
189
+ if entry.get("file") != rel_path:
190
+ continue
191
+ lines = entry.get("lines")
192
+ if lines == "*":
193
+ return True
194
+ if isinstance(lines, list) and line_no in lines:
195
+ return True
196
+ return False
197
+
198
+
199
+ def _families_in_window(lines: list[str], idx: int, radius: int = 10) -> set[str]:
200
+ """Families found within ±radius lines.
201
+
202
+ The radius is intentionally wider than a tight paragraph so multi-stack
203
+ sections (e.g. composer / npm / pip blocks separated by a few lines of
204
+ prose) are reliably detected as cross-stack documentation.
205
+ """
206
+ families: set[str] = set()
207
+ lo = max(0, idx - radius)
208
+ hi = min(len(lines), idx + radius + 1)
209
+ for j in range(lo, hi):
210
+ line = lines[j]
211
+ for category, patterns in LEAKAGE.items():
212
+ fam = FAMILY[category]
213
+ if fam in families:
214
+ continue
215
+ for pat in patterns:
216
+ if re.search(pat, line):
217
+ families.add(fam)
218
+ break
219
+ # Cross-stack hints — keywords that signal multi-stack docs without
220
+ # themselves being leakage patterns (Rails, Django, Express, Go, Rust…).
221
+ for fam, rx in CROSS_STACK_RE.items():
222
+ if fam in families:
223
+ continue
224
+ if rx.search(line):
225
+ families.add(fam)
226
+ return families
227
+
228
+
229
+ def scan_file(path: Path) -> list[dict]:
230
+ text = path.read_text(encoding="utf-8", errors="ignore")
231
+ lines = text.splitlines()
232
+ hits: list[dict] = []
233
+ for category, patterns in LEAKAGE.items():
234
+ for pat in patterns:
235
+ rx = re.compile(pat)
236
+ for i, line in enumerate(lines, start=1):
237
+ if rx.search(line):
238
+ families = _families_in_window(lines, i - 1)
239
+ hits.append({
240
+ "line": i,
241
+ "category": category,
242
+ "pattern": pat,
243
+ "snippet": line.strip()[:160],
244
+ "cross_stack": len(families) >= 2,
245
+ })
246
+ return hits
247
+
248
+
249
+ def iter_md_files(paths: Iterable[str]) -> Iterable[Path]:
250
+ for raw in paths:
251
+ target = (REPO_ROOT / raw) if not Path(raw).is_absolute() else Path(raw)
252
+ if not target.exists():
253
+ print(f"error: path does not exist: {raw}", file=sys.stderr)
254
+ sys.exit(2)
255
+ if target.is_file() and target.suffix == ".md":
256
+ yield target
257
+ continue
258
+ for f in sorted(target.rglob("*.md")):
259
+ if f.name.startswith("_"):
260
+ continue
261
+ yield f
262
+
263
+
264
+ def main(argv: list[str] | None = None) -> int:
265
+ parser = argparse.ArgumentParser(
266
+ description="Lint generic skills/rules/commands for framework leakage."
267
+ )
268
+ parser.add_argument("--json", action="store_true", help="emit JSON to stdout")
269
+ parser.add_argument("--quiet", action="store_true", help="only print summary line")
270
+ parser.add_argument(
271
+ "--paths",
272
+ nargs="+",
273
+ default=list(DEFAULT_PATHS),
274
+ help="paths to scan (default: the three generic dirs)",
275
+ )
276
+ args = parser.parse_args(argv)
277
+
278
+ allowlist = _load_allowlist()
279
+ file_hits: list[tuple[Path, list[dict]]] = []
280
+ total_hits = 0
281
+ allowlisted_total = 0
282
+
283
+ for f in iter_md_files(args.paths):
284
+ if is_carve_out(f):
285
+ continue
286
+ if is_inventory_file(f):
287
+ continue
288
+ if has_framework_frontmatter(f):
289
+ continue
290
+ rel = str(f.relative_to(REPO_ROOT))
291
+ raw_hits = scan_file(f)
292
+ if not raw_hits:
293
+ continue
294
+ kept: list[dict] = []
295
+ for h in raw_hits:
296
+ if h["cross_stack"]:
297
+ continue
298
+ if _allowlisted(rel, h["line"], allowlist):
299
+ h["allowlisted"] = True
300
+ allowlisted_total += 1
301
+ continue
302
+ h["allowlisted"] = False
303
+ kept.append(h)
304
+ if kept:
305
+ file_hits.append((f, kept))
306
+ total_hits += len(kept)
307
+
308
+ summary = {
309
+ "total_hits": total_hits,
310
+ "files": len(file_hits),
311
+ "allowlisted": allowlisted_total,
312
+ }
313
+
314
+ if args.json:
315
+ out = {
316
+ "version": 1,
317
+ "hits": [
318
+ {
319
+ "file": str(p.relative_to(REPO_ROOT)),
320
+ **h,
321
+ }
322
+ for p, hits in file_hits
323
+ for h in hits
324
+ ],
325
+ "summary": summary,
326
+ }
327
+ print(json.dumps(out, indent=2))
328
+ return 1 if total_hits else 0
329
+
330
+ if not args.quiet:
331
+ for path, hits in file_hits:
332
+ rel = path.relative_to(REPO_ROOT)
333
+ print(f"\n{rel}")
334
+ for h in hits:
335
+ print(
336
+ f" L{h['line']:4d} {h['category']:<16s}"
337
+ f" /{h['pattern']}/ {h['snippet']}"
338
+ )
339
+
340
+ print(
341
+ f"\n{total_hits} hits across {len(file_hits)} files "
342
+ f"({allowlisted_total} allowlisted)"
343
+ )
344
+ return 1 if total_hits else 0
345
+
346
+
347
+ if __name__ == "__main__":
348
+ sys.exit(main())