@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.
- package/.agent-src/commands/bug-fix.md +1 -0
- package/.agent-src/commands/feature/roadmap.md +2 -2
- package/.agent-src/commands/fix/seeder.md +3 -2
- package/.agent-src/commands/memory/add.md +3 -3
- package/.agent-src/commands/module/create.md +1 -0
- package/.agent-src/commands/module/explore.md +10 -6
- package/.agent-src/commands/onboard.md +9 -1
- package/.agent-src/commands/optimize/augmentignore.md +52 -20
- package/.agent-src/commands/optimize/rtk.md +56 -30
- package/.agent-src/commands/package-test.md +86 -10
- package/.agent-src/commands/quality-fix.md +49 -27
- package/.agent-src/commands/update-form-request-messages.md +2 -1
- package/.agent-src/contexts/augment-infrastructure.md +4 -7
- package/.agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -1
- package/.agent-src/contexts/contracts/research-schema.md +1 -1
- package/.agent-src/contexts/execution/interrupt-examples.md +34 -0
- package/.agent-src/contexts/skills-and-commands.md +2 -2
- package/.agent-src/rules/architecture.md +24 -10
- package/.agent-src/rules/artifact-drafting-protocol.md +6 -0
- package/.agent-src/rules/augment-edit-discipline.md +28 -0
- package/.agent-src/rules/augment-source-of-truth.md +2 -2
- package/.agent-src/rules/autonomous-execution.md +31 -0
- package/.agent-src/rules/context-hygiene.md +1 -1
- package/.agent-src/rules/domain-adoption-policy.md +4 -5
- package/.agent-src/rules/domain-safety-disclaimer.md +114 -0
- package/.agent-src/rules/domain-safety-pii.md +142 -0
- package/.agent-src/rules/domain-safety-retention.md +86 -0
- package/.agent-src/rules/downstream-changes.md +4 -4
- package/.agent-src/rules/framework-neutrality-in-generic-skills.md +130 -0
- package/.agent-src/rules/git-history-discipline.md +99 -0
- package/.agent-src/rules/minimal-safe-diff.md +6 -0
- package/.agent-src/rules/no-roadmap-references.md +4 -2
- package/.agent-src/rules/user-interrupt-priority.md +46 -0
- package/.agent-src/rules/verify-before-complete.md +11 -2
- package/.agent-src/skills/adversarial-review/SKILL.md +1 -1
- package/.agent-src/skills/ai-council/SKILL.md +1 -0
- package/.agent-src/skills/api-endpoint/SKILL.md +58 -154
- package/.agent-src/skills/api-testing/SKILL.md +11 -0
- package/.agent-src/skills/code-refactoring/SKILL.md +36 -30
- package/.agent-src/skills/code-review/SKILL.md +41 -36
- package/.agent-src/skills/context-authoring/SKILL.md +1 -1
- package/.agent-src/skills/dashboard-design/SKILL.md +1 -2
- package/.agent-src/skills/database/SKILL.md +8 -3
- package/.agent-src/skills/dependency-upgrade/SKILL.md +65 -19
- package/.agent-src/skills/developer-like-execution/SKILL.md +25 -14
- package/.agent-src/skills/eloquent/SKILL.md +1 -1
- package/.agent-src/skills/feature-planning/SKILL.md +1 -1
- package/.agent-src/skills/file-editor/SKILL.md +45 -19
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +2 -2
- package/.agent-src/skills/git-workflow/SKILL.md +4 -4
- package/.agent-src/skills/laravel-api-endpoint/SKILL.md +187 -0
- package/.agent-src/skills/{dto-creator → laravel-dto}/SKILL.md +5 -4
- package/.agent-src/skills/{migration-creator → laravel-migration}/SKILL.md +11 -10
- package/.agent-src/skills/laravel-reverb/SKILL.md +3 -3
- package/.agent-src/skills/{websocket → laravel-websocket}/SKILL.md +4 -3
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
- package/.agent-src/skills/merge-conflicts/SKILL.md +49 -17
- package/.agent-src/skills/migration-architect/SKILL.md +6 -6
- package/.agent-src/skills/module-management/SKILL.md +1 -0
- package/.agent-src/skills/multi-tenancy/SKILL.md +15 -8
- package/.agent-src/skills/pest-testing/SKILL.md +18 -0
- package/.agent-src/skills/php-debugging/SKILL.md +28 -0
- package/.agent-src/skills/php-service/SKILL.md +3 -3
- package/.agent-src/skills/playwright-testing/SKILL.md +16 -1
- package/.agent-src/skills/project-analyzer/SKILL.md +68 -42
- package/.agent-src/skills/readme-writing-package/SKILL.md +94 -23
- package/.agent-src/skills/roadmap-management/SKILL.md +1 -1
- package/.agent-src/skills/rtk-output-filtering/SKILL.md +23 -8
- package/.agent-src/skills/rule-refactor/SKILL.md +145 -0
- package/.agent-src/skills/rule-writing/SKILL.md +34 -8
- package/.agent-src/skills/security/SKILL.md +38 -29
- package/.agent-src/skills/skill-reviewer/SKILL.md +1 -1
- package/.agent-src/skills/test-driven-development/SKILL.md +4 -4
- package/.agent-src/skills/test-performance/SKILL.md +6 -5
- package/.agent-src/skills/verify-completion-evidence/SKILL.md +24 -27
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.agent-src/templates/copilot-instructions.md +2 -2
- package/.agent-src/templates/rule.md +2 -2
- package/.claude-plugin/marketplace.json +6 -4
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +74 -170
- package/README.md +2 -2
- package/docs/architecture.md +2 -2
- package/docs/archive/CHANGELOG-pre-2.25.0.md +191 -0
- package/docs/catalog.md +17 -12
- package/docs/contracts/file-ownership-matrix.json +473 -43
- package/docs/contracts/kernel-membership.md +17 -0
- package/docs/contracts/smoke-contracts.md +8 -8
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/php/api-design.md +1 -1
- package/docs/guidelines/php/controllers.md +1 -1
- package/docs/guidelines/php/resources.md +1 -1
- package/docs/guidelines/php/validations.md +1 -1
- package/package.json +1 -1
- package/scripts/build_linear_digest.py +0 -1
- package/scripts/lint_framework_leakage.py +348 -0
- package/scripts/lint_framework_leakage_allowlist.json +476 -0
- package/scripts/measure_augment_budget.py +6 -0
- package/scripts/schemas/command.schema.json +5 -0
- package/scripts/schemas/skill.schema.json +5 -0
- package/scripts/skill_linter.py +60 -7
- package/scripts/smoke/kernel.sh +4 -4
- package/scripts/smoke/router.sh +2 -2
- package/.agent-src/rules/agent-docs.md +0 -20
- package/.agent-src/rules/augment-portability.md +0 -23
- package/.agent-src/rules/capture-learnings.md +0 -19
- package/.agent-src/rules/docs-sync.md +0 -20
- package/.agent-src/rules/domain-safety-disclaimer-consulting.md +0 -52
- package/.agent-src/rules/domain-safety-disclaimer-financial.md +0 -54
- package/.agent-src/rules/domain-safety-disclaimer-legal.md +0 -49
- package/.agent-src/rules/domain-safety-disclaimer-medical.md +0 -56
- package/.agent-src/rules/domain-safety-export-redact.md +0 -65
- package/.agent-src/rules/domain-safety-logging-pii-floor.md +0 -55
- package/.agent-src/rules/domain-safety-pii-finance.md +0 -57
- package/.agent-src/rules/domain-safety-pii-marketing.md +0 -60
- package/.agent-src/rules/domain-safety-pii-recruiting.md +0 -56
- package/.agent-src/rules/domain-safety-pii-support.md +0 -57
- package/.agent-src/rules/domain-safety-retention-finance.md +0 -48
- package/.agent-src/rules/domain-safety-retention-support.md +0 -55
- package/.agent-src/rules/e2e-testing.md +0 -19
- package/.agent-src/rules/no-unsolicited-rebase.md +0 -107
- 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
|
-
|
|
54
|
+
10 kernel rules · 9 carry Iron-Law fences · 1 dispatch index · ≤ 2 budget breaches
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
- **
|
|
58
|
-
- **
|
|
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
|
-
|
|
73
|
+
68 router ids · 0 broken rule pointers · 36 routes_to refs · 2 missing contracts
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
- **
|
|
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
|
-
- **
|
|
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 >
|
|
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
|
|
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.
|
package/docs/getting-started.md
CHANGED
|
@@ -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
|
|
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
|
@@ -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())
|