@event4u/agent-config 2.24.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 (161) hide show
  1. package/.agent-src/commands/bug-fix.md +1 -0
  2. package/.agent-src/commands/create-pr/description-only.md +39 -11
  3. package/.agent-src/commands/create-pr.md +59 -5
  4. package/.agent-src/commands/feature/roadmap.md +2 -2
  5. package/.agent-src/commands/fix/seeder.md +3 -2
  6. package/.agent-src/commands/memory/add.md +3 -3
  7. package/.agent-src/commands/module/create.md +1 -0
  8. package/.agent-src/commands/module/explore.md +10 -6
  9. package/.agent-src/commands/onboard.md +9 -1
  10. package/.agent-src/commands/optimize/augmentignore.md +52 -20
  11. package/.agent-src/commands/optimize/rtk.md +56 -30
  12. package/.agent-src/commands/package-test.md +86 -10
  13. package/.agent-src/commands/quality-fix.md +49 -27
  14. package/.agent-src/commands/update-form-request-messages.md +2 -1
  15. package/.agent-src/commands/video/from-script.md +5 -5
  16. package/.agent-src/commands/video/storyboard.md +1 -1
  17. package/.agent-src/contexts/augment-infrastructure.md +4 -7
  18. package/.agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -1
  19. package/.agent-src/contexts/contracts/research-schema.md +1 -1
  20. package/.agent-src/contexts/execution/interrupt-examples.md +34 -0
  21. package/.agent-src/contexts/execution/roadmap-process-loop.md +69 -14
  22. package/.agent-src/contexts/skills-and-commands.md +2 -2
  23. package/.agent-src/personas/README.md +3 -2
  24. package/.agent-src/personas/ai-video-technical-director.md +2 -2
  25. package/.agent-src/personas/hollywood-director.md +3 -3
  26. package/.agent-src/profiles/content_creator.yml +5 -0
  27. package/.agent-src/rules/architecture.md +24 -10
  28. package/.agent-src/rules/artifact-drafting-protocol.md +6 -0
  29. package/.agent-src/rules/augment-edit-discipline.md +28 -0
  30. package/.agent-src/rules/augment-source-of-truth.md +2 -2
  31. package/.agent-src/rules/autonomous-execution.md +31 -0
  32. package/.agent-src/rules/context-hygiene.md +1 -1
  33. package/.agent-src/rules/domain-adoption-policy.md +4 -5
  34. package/.agent-src/rules/domain-safety-disclaimer.md +114 -0
  35. package/.agent-src/rules/domain-safety-pii.md +142 -0
  36. package/.agent-src/rules/domain-safety-retention.md +86 -0
  37. package/.agent-src/rules/downstream-changes.md +4 -4
  38. package/.agent-src/rules/framework-neutrality-in-generic-skills.md +130 -0
  39. package/.agent-src/rules/git-history-discipline.md +99 -0
  40. package/.agent-src/rules/media-governance-routing.md +82 -0
  41. package/.agent-src/rules/minimal-safe-diff.md +6 -0
  42. package/.agent-src/rules/no-roadmap-references.md +4 -2
  43. package/.agent-src/rules/persona-governance.md +90 -0
  44. package/.agent-src/rules/provider-lifecycle-discipline.md +75 -0
  45. package/.agent-src/rules/roadmap-ci-steps-policy.md +145 -0
  46. package/.agent-src/rules/roadmap-progress-sync.md +11 -5
  47. package/.agent-src/rules/user-interrupt-priority.md +46 -0
  48. package/.agent-src/rules/verify-before-complete.md +11 -2
  49. package/.agent-src/skills/adversarial-review/SKILL.md +1 -1
  50. package/.agent-src/skills/ai-council/SKILL.md +1 -0
  51. package/.agent-src/skills/api-endpoint/SKILL.md +58 -154
  52. package/.agent-src/skills/api-testing/SKILL.md +11 -0
  53. package/.agent-src/skills/character-consistency/SKILL.md +12 -1
  54. package/.agent-src/skills/code-refactoring/SKILL.md +36 -30
  55. package/.agent-src/skills/code-review/SKILL.md +41 -36
  56. package/.agent-src/skills/context-authoring/SKILL.md +1 -1
  57. package/.agent-src/skills/dashboard-design/SKILL.md +1 -2
  58. package/.agent-src/skills/database/SKILL.md +8 -3
  59. package/.agent-src/skills/dependency-upgrade/SKILL.md +65 -19
  60. package/.agent-src/skills/developer-like-execution/SKILL.md +25 -14
  61. package/.agent-src/skills/eloquent/SKILL.md +1 -1
  62. package/.agent-src/skills/feature-planning/SKILL.md +1 -1
  63. package/.agent-src/skills/file-editor/SKILL.md +45 -19
  64. package/.agent-src/skills/finishing-a-development-branch/SKILL.md +2 -2
  65. package/.agent-src/skills/git-workflow/SKILL.md +135 -2
  66. package/.agent-src/skills/laravel-api-endpoint/SKILL.md +187 -0
  67. package/.agent-src/skills/{dto-creator → laravel-dto}/SKILL.md +5 -4
  68. package/.agent-src/skills/{migration-creator → laravel-migration}/SKILL.md +11 -10
  69. package/.agent-src/skills/laravel-reverb/SKILL.md +3 -3
  70. package/.agent-src/skills/{websocket → laravel-websocket}/SKILL.md +4 -3
  71. package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
  72. package/.agent-src/skills/merge-conflicts/SKILL.md +49 -17
  73. package/.agent-src/skills/migration-architect/SKILL.md +6 -6
  74. package/.agent-src/skills/module-management/SKILL.md +1 -0
  75. package/.agent-src/skills/motion-choreographer/SKILL.md +12 -0
  76. package/.agent-src/skills/multi-tenancy/SKILL.md +15 -8
  77. package/.agent-src/skills/pest-testing/SKILL.md +18 -0
  78. package/.agent-src/skills/php-debugging/SKILL.md +28 -0
  79. package/.agent-src/skills/php-service/SKILL.md +3 -3
  80. package/.agent-src/skills/pixar-storyteller/SKILL.md +19 -6
  81. package/.agent-src/skills/playwright-testing/SKILL.md +16 -1
  82. package/.agent-src/skills/project-analyzer/SKILL.md +68 -42
  83. package/.agent-src/skills/readme-writing-package/SKILL.md +94 -23
  84. package/.agent-src/skills/roadmap-management/SKILL.md +1 -1
  85. package/.agent-src/skills/roadmap-writing/SKILL.md +10 -0
  86. package/.agent-src/skills/rtk-output-filtering/SKILL.md +23 -8
  87. package/.agent-src/skills/rule-refactor/SKILL.md +145 -0
  88. package/.agent-src/skills/rule-writing/SKILL.md +34 -8
  89. package/.agent-src/skills/scene-expander/SKILL.md +22 -7
  90. package/.agent-src/skills/security/SKILL.md +38 -29
  91. package/.agent-src/skills/skill-reviewer/SKILL.md +1 -1
  92. package/.agent-src/skills/test-driven-development/SKILL.md +4 -4
  93. package/.agent-src/skills/test-performance/SKILL.md +6 -5
  94. package/.agent-src/skills/verify-completion-evidence/SKILL.md +24 -27
  95. package/.agent-src/skills/video-director/SKILL.md +13 -0
  96. package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  97. package/.agent-src/templates/copilot-instructions.md +2 -2
  98. package/.agent-src/templates/roadmaps.md +16 -0
  99. package/.agent-src/templates/rule.md +2 -2
  100. package/.claude-plugin/marketplace.json +6 -4
  101. package/AGENTS.md +1 -1
  102. package/CHANGELOG.md +80 -133
  103. package/README.md +6 -4
  104. package/config/agent-settings.template.yml +26 -0
  105. package/docs/architecture.md +2 -2
  106. package/docs/archive/CHANGELOG-pre-2.25.0.md +191 -0
  107. package/docs/catalog.md +20 -12
  108. package/docs/contracts/file-ownership-matrix.json +588 -90
  109. package/docs/contracts/kernel-membership.md +17 -0
  110. package/docs/contracts/provider-lifecycle.md +122 -0
  111. package/docs/contracts/smoke-contracts.md +8 -8
  112. package/docs/decisions/ADR-011-domain-pack-readiness.md +213 -0
  113. package/docs/decisions/INDEX.md +1 -0
  114. package/docs/getting-started-by-role.md +10 -0
  115. package/docs/getting-started.md +1 -1
  116. package/docs/guidelines/php/api-design.md +1 -1
  117. package/docs/guidelines/php/controllers.md +1 -1
  118. package/docs/guidelines/php/resources.md +1 -1
  119. package/docs/guidelines/php/validations.md +1 -1
  120. package/docs/personas.md +73 -26
  121. package/docs/profiles.md +9 -4
  122. package/package.json +1 -1
  123. package/scripts/_tmp_scan_framework_leakage.py +119 -0
  124. package/scripts/ai-video/adapters/gemini-veo.sh +5 -0
  125. package/scripts/ai-video/adapters/higgsfield.sh +6 -0
  126. package/scripts/ai-video/adapters/kling.sh +5 -0
  127. package/scripts/ai-video/adapters/openai-images.sh +5 -0
  128. package/scripts/ai-video/adapters/sora.sh +6 -0
  129. package/scripts/build_linear_digest.py +0 -1
  130. package/scripts/check_portability.py +6 -0
  131. package/scripts/lint_framework_leakage.py +348 -0
  132. package/scripts/lint_framework_leakage_allowlist.json +476 -0
  133. package/scripts/lint_media_policy_linkage.py +140 -0
  134. package/scripts/lint_persona_governance.py +164 -0
  135. package/scripts/lint_roadmap_ci_steps.py +182 -0
  136. package/scripts/measure_augment_budget.py +6 -0
  137. package/scripts/schemas/command.schema.json +5 -0
  138. package/scripts/schemas/skill.schema.json +5 -0
  139. package/scripts/skill_linter.py +60 -7
  140. package/scripts/smoke/kernel.sh +4 -4
  141. package/scripts/smoke/router.sh +2 -2
  142. package/scripts/smoke/schema.sh +1 -1
  143. package/.agent-src/personas/pixar-storyboard-artist.md +0 -98
  144. package/.agent-src/rules/agent-docs.md +0 -20
  145. package/.agent-src/rules/augment-portability.md +0 -23
  146. package/.agent-src/rules/capture-learnings.md +0 -19
  147. package/.agent-src/rules/docs-sync.md +0 -20
  148. package/.agent-src/rules/domain-safety-disclaimer-consulting.md +0 -52
  149. package/.agent-src/rules/domain-safety-disclaimer-financial.md +0 -54
  150. package/.agent-src/rules/domain-safety-disclaimer-legal.md +0 -49
  151. package/.agent-src/rules/domain-safety-disclaimer-medical.md +0 -56
  152. package/.agent-src/rules/domain-safety-export-redact.md +0 -65
  153. package/.agent-src/rules/domain-safety-logging-pii-floor.md +0 -55
  154. package/.agent-src/rules/domain-safety-pii-finance.md +0 -57
  155. package/.agent-src/rules/domain-safety-pii-marketing.md +0 -60
  156. package/.agent-src/rules/domain-safety-pii-recruiting.md +0 -56
  157. package/.agent-src/rules/domain-safety-pii-support.md +0 -57
  158. package/.agent-src/rules/domain-safety-retention-finance.md +0 -48
  159. package/.agent-src/rules/domain-safety-retention-support.md +0 -55
  160. package/.agent-src/rules/e2e-testing.md +0 -19
  161. package/.agent-src/rules/no-unsolicited-rebase.md +0 -107
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env python3
2
+ """Scan generic skills/rules/commands for framework/language leakage.
3
+
4
+ Carve-out criterion: artifact filename or directory path matches an explicit
5
+ framework/language marker. Everything else MUST be framework-neutral.
6
+ TEMPORARY scanner — delete after audit roadmap is drafted.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ ROOT = Path(".agent-src.uncompressed")
15
+
16
+ CARVE_OUT_PATTERNS = [
17
+ r"laravel", r"^php-", r"^eloquent", r"^blade", r"^livewire", r"^flux",
18
+ r"^pest-", r"^artisan-", r"^composer-", r"^jobs-events$", r"^symfony",
19
+ r"^nextjs", r"^react-", r"^async-python", r"^openapi$", r"^quality-tools",
20
+ r"^sql-writing", r"^tailwind", r"^terraform", r"^terragrunt", r"^traefik",
21
+ r"^mobile-e2e",
22
+ r"^project-analysis-(laravel|symfony|nextjs|react|node-express|zend-laminas)",
23
+ r"^docker", r"^aws-", r"^grafana", r"^playwright",
24
+ r"^laravel-", r"^docker-", r"^symfony-", r"^copilot-", r"^devcontainer",
25
+ r"-routing$",
26
+ ]
27
+ CARVE_OUT_RE = re.compile("|".join(CARVE_OUT_PATTERNS), re.IGNORECASE)
28
+
29
+ LEAKAGE = {
30
+ "Laravel": [
31
+ r"\bLaravel\b", r"\bEloquent\b", r"\bArtisan\b", r"\bFormRequest\b",
32
+ r"\bForm Request\b", r"\bBlade\b(?! Runner)", r"\bLivewire\b",
33
+ r"\bResource::(make|collection)\b", r"\bModel::\b",
34
+ r"\bapp/Http/", r"\broutes/(api|web)\.php",
35
+ r"\bdatabase/(migrations|seeders|factories)\b",
36
+ r"\bphp artisan\b", r"\bIlluminate\\\\", r"\bIlluminate\\",
37
+ r"\bbootstrap/app\.php",
38
+ ],
39
+ "PHP": [
40
+ r"\bPHPStan\b", r"\bPest\b(?! Control)", r"\bPHPUnit\b", r"\bRector\b",
41
+ r"\bECS\b", r"\bcomposer\.json\b", r"\bvendor/bin/",
42
+ r"\bdeclare\(strict_types=1\)", r"\.php\b",
43
+ r"\bnamespace App\\\\", r"\bnamespace App\\",
44
+ r"\bcomposer (require|install|update|dump-autoload)\b",
45
+ ],
46
+ "Symfony": [
47
+ r"\bSymfony\b", r"\bbin/console\b", r"\bDoctrine\b", r"\bTwig\b",
48
+ ],
49
+ "JS-specific": [
50
+ r"\bpackage\.json\b",
51
+ r"\bnpm (install|run|test|ci)\b",
52
+ r"\byarn (install|add|test)\b",
53
+ r"\bpnpm (install|add|run|test)\b",
54
+ r"\bnode_modules\b",
55
+ ],
56
+ "Python-specific": [
57
+ r"\bpyproject\.toml\b", r"\brequirements\.txt\b",
58
+ r"\bpip install\b", r"\bpytest\b",
59
+ ],
60
+ }
61
+
62
+
63
+ def is_carve_out(path: Path) -> bool:
64
+ for p in path.parts:
65
+ stem = p.removesuffix(".md")
66
+ if CARVE_OUT_RE.search(stem):
67
+ return True
68
+ return False
69
+
70
+
71
+ def scan_file(path: Path) -> dict:
72
+ text = path.read_text(encoding="utf-8", errors="ignore")
73
+ lines = text.splitlines()
74
+ hits: dict[str, list[tuple[int, str, str]]] = {}
75
+ for category, patterns in LEAKAGE.items():
76
+ for pat in patterns:
77
+ rx = re.compile(pat)
78
+ for i, line in enumerate(lines, start=1):
79
+ if rx.search(line):
80
+ hits.setdefault(category, []).append(
81
+ (i, pat, line.strip()[:160])
82
+ )
83
+ return hits
84
+
85
+
86
+ def scan_dir(subdir: str) -> list[tuple[Path, dict]]:
87
+ target = ROOT / subdir
88
+ out = []
89
+ for f in sorted(target.rglob("*.md")):
90
+ if is_carve_out(f):
91
+ continue
92
+ if f.name.startswith("_"):
93
+ continue
94
+ hits = scan_file(f)
95
+ if hits:
96
+ out.append((f, hits))
97
+ return out
98
+
99
+
100
+ def main():
101
+ for label, sub in [("SKILLS", "skills"), ("RULES", "rules"), ("COMMANDS", "commands")]:
102
+ print(f"\n===== {label} (generic, non-carve-out) =====")
103
+ results = scan_dir(sub)
104
+ if not results:
105
+ print(" (clean)")
106
+ continue
107
+ for path, hits in results:
108
+ total = sum(len(v) for v in hits.values())
109
+ print(f"\n [{total:3d}] {path}")
110
+ for cat, items in hits.items():
111
+ print(f" {cat}: {len(items)}")
112
+ for line_no, pat, snippet in items[:6]:
113
+ print(f" L{line_no:4d} /{pat}/ {snippet}")
114
+ if len(items) > 6:
115
+ print(f" ... +{len(items) - 6} more")
116
+
117
+
118
+ if __name__ == "__main__":
119
+ main()
@@ -9,6 +9,11 @@
9
9
  # Contract: scripts/ai-video/lib/adapter-contract.md
10
10
  # Provider: top-level <provider id="gemini-veo" kind="video"> in
11
11
  # agents/.ai-video.xml.
12
+ #
13
+ # Lifecycle: experimental — structural shape conformant; no maintainer
14
+ # real-API smoke trace captured yet. See docs/contracts/provider-lifecycle.md
15
+ # for promotion criteria. The agent must surface this tier and ask
16
+ # before defaulting to this adapter.
12
17
 
13
18
  set -euo pipefail
14
19
 
@@ -11,6 +11,12 @@
11
11
  # agents/.ai-video.xml. Preset → motion-choreographer profile mapping
12
12
  # is documented in agents/ai-video/prompts/motion-choreography.md
13
13
  # (Phase 6).
14
+ #
15
+ # Lifecycle: experimental — capability-discovery path conformant; no
16
+ # maintainer real-API smoke trace captured yet. See
17
+ # docs/contracts/provider-lifecycle.md for promotion criteria. The
18
+ # agent must surface this tier and ask before defaulting to this
19
+ # adapter.
14
20
 
15
21
  set -euo pipefail
16
22
 
@@ -9,6 +9,11 @@
9
9
  # Contract: scripts/ai-video/lib/adapter-contract.md
10
10
  # Provider: top-level <provider id="kling" kind="video"> in
11
11
  # agents/.ai-video.xml.
12
+ #
13
+ # Lifecycle: experimental — async submit/poll/fetch contract conformant;
14
+ # no maintainer real-API smoke trace captured yet. See
15
+ # docs/contracts/provider-lifecycle.md for promotion criteria. The agent
16
+ # must surface this tier and ask before defaulting to this adapter.
12
17
 
13
18
  set -euo pipefail
14
19
 
@@ -9,6 +9,11 @@
9
9
  # Contract: scripts/ai-video/lib/adapter-contract.md
10
10
  # Provider: top-level <provider id="openai-images" kind="image"> in
11
11
  # agents/.ai-video.xml.
12
+ #
13
+ # Lifecycle: experimental — structural shape conformant; no maintainer
14
+ # real-API smoke trace captured yet. See docs/contracts/provider-lifecycle.md
15
+ # for promotion criteria. The agent must surface this tier and ask
16
+ # before defaulting to this adapter.
12
17
 
13
18
  set -euo pipefail
14
19
 
@@ -9,6 +9,12 @@
9
9
  # Contract: scripts/ai-video/lib/adapter-contract.md
10
10
  # Provider: top-level <provider id="sora" kind="video"> in
11
11
  # agents/.ai-video.xml.
12
+ #
13
+ # Lifecycle: experimental — structural-prompt path conformant; no
14
+ # maintainer real-API smoke trace captured yet. See
15
+ # docs/contracts/provider-lifecycle.md for promotion criteria. The
16
+ # agent must surface this tier and ask before defaulting to this
17
+ # adapter.
12
18
 
13
19
  set -euo pipefail
14
20
 
@@ -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
 
@@ -319,6 +319,12 @@ _TASK_DETECTOR_SKIP = (
319
319
  "rules/package-ci-checks.md",
320
320
  "contexts/communication/rules-auto/package-ci-checks-mechanics.md",
321
321
  "contexts/contracts/agents-md-anatomy.md",
322
+ # roadmap-ci-steps-policy defines the gate by listing the forbidden
323
+ # CI-shaped literals; its mechanics doc and the execution loop +
324
+ # authoring skill enumerate the same literals to detect them.
325
+ "rules/roadmap-ci-steps-policy.md",
326
+ "contexts/execution/roadmap-process-loop.md",
327
+ "skills/roadmap-writing/SKILL.md",
322
328
  )
323
329
 
324
330
 
@@ -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())