@event4u/agent-config 1.20.0 → 1.21.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 (238) hide show
  1. package/.agent-src/commands/agents.md +1 -1
  2. package/.agent-src/commands/bug-fix.md +1 -1
  3. package/.agent-src/commands/bug-investigate.md +2 -2
  4. package/.agent-src/commands/chat-history/import.md +60 -64
  5. package/.agent-src/commands/compress.md +12 -0
  6. package/.agent-src/commands/context/create.md +2 -2
  7. package/.agent-src/commands/context.md +1 -1
  8. package/.agent-src/commands/copilot-agents.md +1 -1
  9. package/.agent-src/commands/council/default.md +17 -5
  10. package/.agent-src/commands/council.md +1 -1
  11. package/.agent-src/commands/e2e-heal.md +1 -1
  12. package/.agent-src/commands/e2e-plan.md +1 -1
  13. package/.agent-src/commands/feature/dev.md +3 -3
  14. package/.agent-src/commands/feature.md +1 -1
  15. package/.agent-src/commands/fix/seeder.md +2 -2
  16. package/.agent-src/commands/fix.md +1 -1
  17. package/.agent-src/commands/jira-ticket.md +1 -1
  18. package/.agent-src/commands/judge.md +2 -2
  19. package/.agent-src/commands/memory.md +1 -1
  20. package/.agent-src/commands/mode.md +5 -5
  21. package/.agent-src/commands/module.md +1 -1
  22. package/.agent-src/commands/onboard.md +4 -4
  23. package/.agent-src/commands/optimize/augmentignore.md +1 -1
  24. package/.agent-src/commands/optimize-prompt.md +61 -0
  25. package/.agent-src/commands/optimize.md +1 -1
  26. package/.agent-src/commands/override.md +1 -1
  27. package/.agent-src/commands/review-changes.md +1 -1
  28. package/.agent-src/commands/review-routing.md +1 -1
  29. package/.agent-src/commands/roadmap.md +1 -1
  30. package/.agent-src/commands/set-cost-profile.md +3 -3
  31. package/.agent-src/commands/sync-agent-settings.md +2 -2
  32. package/.agent-src/commands/tests/create.md +2 -2
  33. package/.agent-src/commands/tests.md +1 -1
  34. package/.agent-src/commands/threat-model.md +4 -4
  35. package/.agent-src/contexts/authority/commit-mechanics.md +14 -1
  36. package/.agent-src/contexts/authority/destructive-mechanics.md +14 -1
  37. package/.agent-src/contexts/authority/scope-mechanics.md +5 -0
  38. package/.agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +76 -0
  39. package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +1 -1
  40. package/.agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +98 -0
  41. package/.agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +93 -0
  42. package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +128 -5
  43. package/.agent-src/contexts/execution/autonomy-mechanics.md +44 -0
  44. package/.agent-src/contexts/model-recommendations.md +2 -2
  45. package/.agent-src/contexts/override-system.md +1 -1
  46. package/.agent-src/personas/product-owner.md +2 -2
  47. package/.agent-src/personas/qa.md +1 -1
  48. package/.agent-src/rules/agent-authority.md +5 -6
  49. package/.agent-src/rules/agent-docs.md +11 -53
  50. package/.agent-src/rules/analysis-skill-routing.md +10 -40
  51. package/.agent-src/rules/architecture.md +6 -1
  52. package/.agent-src/rules/artifact-drafting-protocol.md +5 -0
  53. package/.agent-src/rules/artifact-engagement-recording.md +23 -59
  54. package/.agent-src/rules/ask-when-uncertain.md +24 -47
  55. package/.agent-src/rules/augment-portability.md +14 -62
  56. package/.agent-src/rules/augment-source-of-truth.md +10 -1
  57. package/.agent-src/rules/autonomous-execution.md +17 -98
  58. package/.agent-src/rules/capture-learnings.md +9 -80
  59. package/.agent-src/rules/cli-output-handling.md +12 -42
  60. package/.agent-src/rules/command-suggestion-policy.md +25 -73
  61. package/.agent-src/rules/commit-conventions.md +9 -58
  62. package/.agent-src/rules/commit-policy.md +16 -47
  63. package/.agent-src/rules/context-hygiene.md +5 -0
  64. package/.agent-src/rules/direct-answers.md +21 -50
  65. package/.agent-src/rules/docker-commands.md +11 -45
  66. package/.agent-src/rules/docs-sync.md +10 -56
  67. package/.agent-src/rules/downstream-changes.md +5 -0
  68. package/.agent-src/rules/e2e-testing.md +9 -44
  69. package/.agent-src/rules/guidelines.md +13 -75
  70. package/.agent-src/rules/improve-before-implement.md +10 -2
  71. package/.agent-src/rules/language-and-tone.md +41 -106
  72. package/.agent-src/rules/laravel-translations.md +11 -40
  73. package/.agent-src/rules/markdown-safe-codeblocks.md +4 -0
  74. package/.agent-src/rules/minimal-safe-diff.md +4 -0
  75. package/.agent-src/rules/missing-tool-handling.md +4 -0
  76. package/.agent-src/rules/model-recommendation.md +9 -61
  77. package/.agent-src/rules/no-attribution-footers.md +5 -0
  78. package/.agent-src/rules/no-cheap-questions.md +11 -27
  79. package/.agent-src/rules/no-council-references.md +76 -0
  80. package/.agent-src/rules/no-roadmap-references.md +7 -0
  81. package/.agent-src/rules/non-destructive-by-default.md +13 -43
  82. package/.agent-src/rules/onboarding-gate.md +9 -117
  83. package/.agent-src/rules/package-ci-checks.md +10 -37
  84. package/.agent-src/rules/php-coding.md +10 -55
  85. package/.agent-src/rules/preservation-guard.md +9 -0
  86. package/.agent-src/rules/review-routing-awareness.md +9 -97
  87. package/.agent-src/rules/reviewer-awareness.md +8 -83
  88. package/.agent-src/rules/roadmap-progress-sync.md +7 -170
  89. package/.agent-src/rules/role-mode-adherence.md +6 -2
  90. package/.agent-src/rules/rule-type-governance.md +8 -66
  91. package/.agent-src/rules/runtime-safety.md +5 -0
  92. package/.agent-src/rules/scope-control.md +17 -62
  93. package/.agent-src/rules/security-sensitive-stop.md +7 -1
  94. package/.agent-src/rules/size-enforcement.md +6 -1
  95. package/.agent-src/rules/skill-improvement-trigger.md +9 -49
  96. package/.agent-src/rules/skill-quality.md +7 -113
  97. package/.agent-src/rules/slash-command-routing-policy.md +11 -63
  98. package/.agent-src/rules/think-before-action.md +22 -87
  99. package/.agent-src/rules/token-efficiency.md +10 -74
  100. package/.agent-src/rules/token-optimizer-maintenance.md +68 -0
  101. package/.agent-src/rules/tool-safety.md +4 -0
  102. package/.agent-src/rules/ui-audit-gate.md +25 -61
  103. package/.agent-src/rules/upstream-proposal.md +9 -67
  104. package/.agent-src/rules/user-interaction.md +22 -108
  105. package/.agent-src/rules/verify-before-complete.md +1 -1
  106. package/.agent-src/skills/agent-docs-writing/SKILL.md +1 -1
  107. package/.agent-src/skills/ai-council/SKILL.md +65 -0
  108. package/.agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  109. package/.agent-src/skills/analysis-skill-router/SKILL.md +3 -3
  110. package/.agent-src/skills/artisan-commands/SKILL.md +2 -2
  111. package/.agent-src/skills/authz-review/SKILL.md +1 -1
  112. package/.agent-src/skills/aws-infrastructure/SKILL.md +5 -5
  113. package/.agent-src/skills/blast-radius-analyzer/SKILL.md +8 -8
  114. package/.agent-src/skills/bug-analyzer/SKILL.md +5 -5
  115. package/.agent-src/skills/code-refactoring/SKILL.md +4 -4
  116. package/.agent-src/skills/code-review/SKILL.md +2 -2
  117. package/.agent-src/skills/command-writing/SKILL.md +11 -0
  118. package/.agent-src/skills/composer-packages/SKILL.md +2 -2
  119. package/.agent-src/skills/context-authoring/SKILL.md +11 -0
  120. package/.agent-src/skills/context-document/SKILL.md +1 -1
  121. package/.agent-src/skills/copilot-agents-optimization/SKILL.md +23 -0
  122. package/.agent-src/skills/copilot-config/SKILL.md +1 -1
  123. package/.agent-src/skills/dependency-upgrade/SKILL.md +2 -2
  124. package/.agent-src/skills/devcontainer/SKILL.md +2 -2
  125. package/.agent-src/skills/developer-like-execution/SKILL.md +1 -1
  126. package/.agent-src/skills/docker/SKILL.md +1 -1
  127. package/.agent-src/skills/dto-creator/SKILL.md +1 -1
  128. package/.agent-src/skills/estimate-ticket/SKILL.md +2 -2
  129. package/.agent-src/skills/fe-design/SKILL.md +4 -4
  130. package/.agent-src/skills/feature-planning/SKILL.md +5 -5
  131. package/.agent-src/skills/funnel-analysis/SKILL.md +1 -1
  132. package/.agent-src/skills/laravel/SKILL.md +1 -1
  133. package/.agent-src/skills/laravel-notifications/SKILL.md +5 -5
  134. package/.agent-src/skills/laravel-pennant/SKILL.md +1 -1
  135. package/.agent-src/skills/laravel-pulse/SKILL.md +4 -4
  136. package/.agent-src/skills/laravel-reverb/SKILL.md +2 -2
  137. package/.agent-src/skills/laravel-scheduling/SKILL.md +1 -1
  138. package/.agent-src/skills/migration-creator/SKILL.md +7 -7
  139. package/.agent-src/skills/multi-tenancy/SKILL.md +8 -8
  140. package/.agent-src/skills/performance-analysis/SKILL.md +3 -3
  141. package/.agent-src/skills/pest-testing/SKILL.md +6 -6
  142. package/.agent-src/skills/php-service/SKILL.md +2 -2
  143. package/.agent-src/skills/project-analysis-hypothesis-driven/SKILL.md +3 -3
  144. package/.agent-src/skills/project-analysis-react/SKILL.md +1 -1
  145. package/.agent-src/skills/project-analysis-symfony/SKILL.md +1 -1
  146. package/.agent-src/skills/project-analysis-zend-laminas/SKILL.md +2 -2
  147. package/.agent-src/skills/project-analyzer/SKILL.md +4 -4
  148. package/.agent-src/skills/prompt-optimizer/SKILL.md +108 -0
  149. package/.agent-src/skills/readme-reviewer/SKILL.md +1 -1
  150. package/.agent-src/skills/rule-writing/SKILL.md +33 -0
  151. package/.agent-src/skills/sentry-integration/SKILL.md +1 -1
  152. package/.agent-src/skills/skill-writing/SKILL.md +14 -0
  153. package/.agent-src/skills/terraform/SKILL.md +2 -2
  154. package/.agent-src/skills/terragrunt/SKILL.md +8 -8
  155. package/.agent-src/skills/test-performance/SKILL.md +5 -5
  156. package/.agent-src/skills/threat-modeling/SKILL.md +2 -2
  157. package/.agent-src/skills/token-optimizer/SKILL.md +110 -0
  158. package/.agent-src/skills/universal-project-analysis/SKILL.md +1 -1
  159. package/.agent-src/templates/AGENTS.md +1 -1
  160. package/.agent-src/templates/agent-settings.md +21 -16
  161. package/.agent-src/templates/contexts/tenant-boundaries.md +2 -2
  162. package/.agent-src/templates/contexts.md +1 -1
  163. package/.agent-src/templates/copilot-instructions.md +21 -0
  164. package/.agent-src/templates/copilot-review-instructions.md +76 -0
  165. package/.agent-src/templates/features.md +1 -1
  166. package/.agent-src/templates/rule.md +127 -0
  167. package/.claude-plugin/marketplace.json +4 -1
  168. package/AGENTS.md +32 -5
  169. package/CHANGELOG.md +69 -3
  170. package/README.md +22 -21
  171. package/config/agent-settings.template.yml +44 -10
  172. package/config/gitignore-block.txt +7 -0
  173. package/docs/architecture.md +86 -5
  174. package/docs/catalog.md +16 -6
  175. package/docs/contracts/agent-memory-contract.md +1 -1
  176. package/docs/contracts/context-paths.md +2 -1
  177. package/docs/contracts/file-ownership-matrix.json +354 -500
  178. package/docs/contracts/iron-law-overrides.txt +25 -0
  179. package/docs/contracts/kernel-membership.md +273 -0
  180. package/docs/contracts/load-context-schema.md +26 -11
  181. package/docs/contracts/pilot/agent-authority.md +24 -0
  182. package/docs/contracts/pilot/direct-answers.md +70 -0
  183. package/docs/contracts/pilot/language-and-tone.md +63 -0
  184. package/docs/contracts/rule-classification.md +170 -0
  185. package/docs/contracts/rule-router.md +153 -0
  186. package/docs/customization.md +17 -6
  187. package/docs/decisions/ADR-001-kernel-swap-deferred.md +109 -0
  188. package/docs/decisions/ADR-002-kernel-bucket-overrides.md +124 -0
  189. package/docs/decisions/ADR-rule-kernel-and-router.md +122 -0
  190. package/docs/getting-started.md +2 -2
  191. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +176 -0
  192. package/docs/guidelines/agent-infra/rule-type-governance.md +73 -0
  193. package/docs/guidelines/agent-infra/size-and-scope.md +13 -2
  194. package/docs/guidelines/agent-infra/skill-quality-checklist.md +119 -0
  195. package/docs/guidelines/augment-portability-patterns.md +68 -0
  196. package/docs/guidelines/php/php-coding-patterns.md +62 -0
  197. package/package.json +1 -1
  198. package/scripts/_p43_bodies.py +235 -0
  199. package/scripts/_p43_compress.py +118 -0
  200. package/scripts/_p4_migrate.py +199 -0
  201. package/scripts/_pilot_council_question.py +57 -0
  202. package/scripts/_pilot_measure.py +53 -0
  203. package/scripts/ai_council/session.py +107 -5
  204. package/scripts/build_linear_digest.py +3 -5
  205. package/scripts/check_always_budget.py +39 -6
  206. package/scripts/check_compressed_paths.py +213 -0
  207. package/scripts/check_compression.py +15 -0
  208. package/scripts/check_context_paths.py +1 -0
  209. package/scripts/check_council_layout.py +105 -0
  210. package/scripts/check_council_references.py +145 -0
  211. package/scripts/check_portability.py +2 -0
  212. package/scripts/check_references.py +2 -0
  213. package/scripts/check_token_optimizer_freshness.py +131 -0
  214. package/scripts/compile_router.py +148 -0
  215. package/scripts/compress.py +219 -11
  216. package/scripts/council_cli.py +9 -5
  217. package/scripts/council_prune.py +81 -0
  218. package/scripts/count_token_optimizer_usage.sh +54 -0
  219. package/scripts/install.sh +44 -2
  220. package/scripts/iron_law_sha.py +98 -0
  221. package/scripts/lint_load_context.py +35 -5
  222. package/scripts/measure_rule_budget.py +314 -0
  223. package/scripts/prototype_lint_contradictions.py +150 -0
  224. package/scripts/schemas/rule.schema.json +55 -6
  225. package/scripts/skill_linter.py +196 -6
  226. package/scripts/smoke_path_resolution.py +93 -0
  227. package/scripts/validate_frontmatter.py +41 -1
  228. package/.agent-src/contexts/communication/rules-auto/artifact-engagement-recording-mechanics.md +0 -72
  229. package/.agent-src/contexts/communication/rules-auto/augment-portability-mechanics.md +0 -79
  230. package/.agent-src/contexts/communication/rules-auto/cli-output-handling-mechanics.md +0 -87
  231. package/.agent-src/contexts/communication/rules-auto/command-suggestion-policy-mechanics.md +0 -62
  232. package/.agent-src/contexts/communication/rules-auto/docs-sync-mechanics.md +0 -78
  233. package/.agent-src/contexts/communication/rules-auto/package-ci-checks-mechanics.md +0 -85
  234. package/.agent-src/contexts/communication/rules-auto/review-routing-awareness-mechanics.md +0 -65
  235. package/.agent-src/contexts/communication/rules-auto/roadmap-progress-sync-mechanics.md +0 -78
  236. package/.agent-src/contexts/communication/rules-auto/ui-audit-gate-mechanics.md +0 -53
  237. /package/{docs → .agent-src/contexts}/contracts/artifact-engagement-flow.md +0 -0
  238. /package/{docs → .agent-src/contexts}/contracts/command-suggestion-flow.md +0 -0
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  Agent-config sync — compress .agent-src.uncompressed/ → .agent-src/
4
- and project .agent-src/ → .augment/ (copies for rules, symlinks for the rest).
4
+ and project .agent-src/ → .augment/ (copies for rules by default,
5
+ symlinks for the rest; opt into rule symlinks via
6
+ augment.rules_use_symlinks in .agent-settings.yml).
5
7
 
6
8
  Copies non-.md files as-is. Lists .md files that need compression (done by the
7
9
  Augment agent interactively). Tracks SHA-256 hashes of source files to detect
@@ -19,6 +21,7 @@ Usage:
19
21
 
20
22
  import hashlib
21
23
  import json
24
+ import re
22
25
  import shutil
23
26
  import sys
24
27
  from pathlib import Path
@@ -28,11 +31,41 @@ SOURCE_DIR = PROJECT_ROOT / ".agent-src.uncompressed"
28
31
  TARGET_DIR = PROJECT_ROOT / ".agent-src"
29
32
  AUGMENT_DIR = PROJECT_ROOT / ".augment"
30
33
  HASH_FILE = PROJECT_ROOT / ".compression-hashes.json"
34
+ SETTINGS_FILE = PROJECT_ROOT / ".agent-settings.yml"
31
35
 
32
36
  # Files to copy as-is even if .md (not compressed by agent)
33
37
  COPY_AS_IS = {"README.md"}
34
38
 
35
39
 
40
+ def _read_augment_rules_use_symlinks() -> bool:
41
+ """Read augment.rules_use_symlinks from .agent-settings.yml.
42
+
43
+ Returns True only when the setting is present under the top-level
44
+ ``augment:`` block and resolves to a truthy YAML scalar
45
+ (true/yes/on/1, case-insensitive). Missing file, missing block, or
46
+ any other value → False (preserve copy default).
47
+ """
48
+ if not SETTINGS_FILE.exists():
49
+ return False
50
+ try:
51
+ text = SETTINGS_FILE.read_text(encoding="utf-8")
52
+ except OSError:
53
+ return False
54
+ in_augment = False
55
+ for line in text.splitlines():
56
+ stripped = line.lstrip()
57
+ if not stripped or stripped.startswith("#"):
58
+ continue
59
+ if not line.startswith((" ", "\t")):
60
+ in_augment = stripped.startswith("augment:")
61
+ continue
62
+ if in_augment:
63
+ m = re.match(r"^\s+rules_use_symlinks\s*:\s*([^\s#]+)", line)
64
+ if m:
65
+ return m.group(1).strip().lower() in ("true", "yes", "on", "1")
66
+ return False
67
+
68
+
36
69
 
37
70
 
38
71
  def file_hash(filepath: Path) -> str:
@@ -59,17 +92,42 @@ def save_hashes(hashes: dict) -> None:
59
92
 
60
93
 
61
94
  def mark_done(relative_path: str) -> None:
62
- """Mark a single file as compressed by storing its current source hash."""
95
+ """Mark a single file as compressed by storing its current source hash.
96
+
97
+ Also runs the path rewriter on the just-written `.agent-src/<path>` so
98
+ logical names from the source frontmatter resolve to deployment-correct
99
+ relative paths in the shipped layer (P1 of road-to-path-fixes.md).
100
+ Idempotent — re-running is a no-op.
101
+ """
63
102
  source_file = SOURCE_DIR / relative_path
64
103
  if not source_file.exists():
65
104
  print(f"❌ Source file not found: {relative_path}")
66
105
  sys.exit(1)
106
+ apply_path_rewriter(relative_path)
67
107
  hashes = load_hashes()
68
108
  hashes[relative_path] = file_hash(source_file)
69
109
  save_hashes(hashes)
70
110
  print(f"✅ Marked as compressed: {relative_path}")
71
111
 
72
112
 
113
+ def apply_path_rewriter(relative_path: str) -> bool:
114
+ """Apply `_rewrite_paths` to `.agent-src/<relative_path>` in-place.
115
+
116
+ Returns True if the file was modified, False otherwise. Silently
117
+ returns False if the target doesn't exist (compression hasn't run
118
+ yet) — `--mark-done` is also valid before content exists.
119
+ """
120
+ target = TARGET_DIR / relative_path
121
+ if not target.exists() or not relative_path.endswith(".md"):
122
+ return False
123
+ original = target.read_text(encoding="utf-8")
124
+ rewritten = _rewrite_paths(original, relative_path)
125
+ if rewritten == original:
126
+ return False
127
+ target.write_text(rewritten, encoding="utf-8")
128
+ return True
129
+
130
+
73
131
  def mark_all_done() -> None:
74
132
  """Mark ALL .md files as compressed (e.g. after initial full compression)."""
75
133
  hashes = load_hashes()
@@ -250,6 +308,144 @@ def strip_frontmatter(content: str) -> str:
250
308
  return content
251
309
 
252
310
 
311
+ # ── Path rewriter (P1 of road-to-path-fixes.md) ───────────────────────────
312
+ # Source files use logical names that the rewriter resolves at compress
313
+ # time, so the shipped `.agent-src/` (and `.augment/` projection) carry
314
+ # deployment-correct relative paths without the agent author having to
315
+ # know how deep their file lives.
316
+ #
317
+ # Frontmatter rewrites:
318
+ # load_context: / load_context_eager:
319
+ # contexts/<area>/<file>.md (logical, preferred)
320
+ # .agent-src.uncompressed/contexts/<area>/<file>.md (legacy)
321
+ # → ../contexts/<area>/<file>.md (relative from .agent-src/rules/)
322
+ # triggers[].path_prefix:
323
+ # LEFT ALONE — `path_prefix:` is a literal match pattern, not a
324
+ # file reference. Source-of-truth rules that fire on edits under
325
+ # `.agent-src.uncompressed/` keep that prefix verbatim (see
326
+ # road-to-path-fixes.md P2.2 / Modified Option 1).
327
+ #
328
+ # Body-link rewrites:
329
+ # ../../docs/guidelines/<file>.md → ../docs/guidelines/<file>.md
330
+ # ../../docs/contracts/<file>.md → ../docs/contracts/<file>.md
331
+ #
332
+ # Idempotent: applying twice is a no-op (rewritten patterns no longer
333
+ # match the source patterns).
334
+
335
+ _LEGACY_SRC_PREFIX = ".agent-src.uncompressed/"
336
+ _PROJECTED_SRC_PREFIX = ".agent-src/"
337
+
338
+ # A YAML list item under load_context*: ` - some/path.md` (optionally quoted)
339
+ _FM_LIST_ITEM_RE = re.compile(r'^(\s*-\s*)(["\']?)([^"\'\n]+?\.md)(["\']?)\s*$')
340
+
341
+ # `path_prefix:` line — top-level or under `triggers:` (with leading dash)
342
+ _FM_PATH_PREFIX_RE = re.compile(
343
+ r'^(\s*(?:-\s+)?path_prefix:\s*)(["\']?)([^"\'\n]+?)(["\']?)\s*$'
344
+ )
345
+
346
+ # Body-link patterns (relative two-up to docs/) — capture the docs/... tail
347
+ _BODY_DOCS_RE = re.compile(r'\.\./\.\./(docs/(?:guidelines|contracts)/[^)\s]+\.md)')
348
+
349
+
350
+ def _depth_prefix(source_relative_path: str) -> str:
351
+ """Return the `../` chain to climb from `<source_relative_path>` back to
352
+ the source root. A file at `rules/X.md` (1 dir deep) needs `../`; a
353
+ file at `commands/council/default.md` (2 dirs deep) needs `../../`.
354
+ """
355
+ parts = Path(source_relative_path).parts
356
+ depth = max(len(parts) - 1, 1)
357
+ return "../" * depth
358
+
359
+
360
+ def _split_frontmatter(content: str):
361
+ """Return (frontmatter_lines, body) — frontmatter_lines is None if no FM."""
362
+ if not content.startswith("---\n"):
363
+ return None, content
364
+ end = content.find("\n---\n", 4)
365
+ if end == -1:
366
+ return None, content
367
+ fm_text = content[4:end]
368
+ body = content[end + len("\n---\n"):]
369
+ return fm_text.split("\n"), body
370
+
371
+
372
+ def _rewrite_load_context_value(value: str, prefix: str) -> str:
373
+ """Rewrite a single `load_context` list-item value to a deployment path."""
374
+ # Already relative or absolute → leave alone (idempotence).
375
+ if value.startswith(("../", "./", "/")):
376
+ return value
377
+ # Legacy fully-qualified source prefix.
378
+ if value.startswith(_LEGACY_SRC_PREFIX):
379
+ return prefix + value[len(_LEGACY_SRC_PREFIX):]
380
+ # Projected source prefix (defensive — also strip).
381
+ if value.startswith(_PROJECTED_SRC_PREFIX):
382
+ return prefix + value[len(_PROJECTED_SRC_PREFIX):]
383
+ # Logical name (e.g. `contexts/execution/foo.md`).
384
+ return prefix + value
385
+
386
+
387
+ def _rewrite_path_prefix_value(value: str) -> str:
388
+ """No-op for `triggers[].path_prefix:` values.
389
+
390
+ `path_prefix:` is a literal match pattern the host evaluates against
391
+ the file the agent is editing — not a file reference. Rewriting it
392
+ breaks the workflow it was authored for: source-of-truth rules that
393
+ fire when the agent edits files under `.agent-src.uncompressed/`
394
+ keep that prefix verbatim. The prefix ban therefore applies only to
395
+ `load_context:` entries and body links (see road-to-path-fixes.md
396
+ P2.2 + the AI-Council convergence on 2026-05-06).
397
+ """
398
+ return value
399
+
400
+
401
+ def _rewrite_frontmatter_lines(lines, prefix):
402
+ """Apply load_context / path_prefix rewrites to a frontmatter line list."""
403
+ in_load_context = False
404
+ out = []
405
+ for line in lines:
406
+ bare = line.lstrip()
407
+ if bare.startswith(("load_context:", "load_context_eager:")):
408
+ in_load_context = True
409
+ out.append(line)
410
+ continue
411
+ if in_load_context:
412
+ m = _FM_LIST_ITEM_RE.match(line)
413
+ if m:
414
+ indent, q1, value, q2 = m.groups()
415
+ rewritten = _rewrite_load_context_value(value, prefix)
416
+ out.append(f"{indent}{q1}{rewritten}{q2}")
417
+ continue
418
+ in_load_context = False
419
+ # fall through to path_prefix / passthrough
420
+ m = _FM_PATH_PREFIX_RE.match(line)
421
+ if m:
422
+ head, q1, value, q2 = m.groups()
423
+ out.append(f"{head}{q1}{_rewrite_path_prefix_value(value)}{q2}")
424
+ continue
425
+ out.append(line)
426
+ return out
427
+
428
+
429
+ def _rewrite_body_links(body: str, prefix: str) -> str:
430
+ """Rewrite `../../docs/{guidelines,contracts}/...` to use depth-prefix."""
431
+ return _BODY_DOCS_RE.sub(prefix + r"\1", body)
432
+
433
+
434
+ def _rewrite_paths(content: str, source_relative_path: str) -> str:
435
+ """Rewrite logical / legacy paths in `content` for a file shipped at
436
+ `.agent-src/{source_relative_path}`. Idempotent.
437
+
438
+ See module-level comment above for the full pattern catalog.
439
+ """
440
+ prefix = _depth_prefix(source_relative_path)
441
+ fm_lines, body = _split_frontmatter(content)
442
+ body = _rewrite_body_links(body, prefix)
443
+ if fm_lines is None:
444
+ return body
445
+ new_fm = _rewrite_frontmatter_lines(fm_lines, prefix)
446
+ return "---\n" + "\n".join(new_fm) + "\n---\n" + body
447
+
448
+
253
449
  def generate_rule_symlinks() -> int:
254
450
  """Create symlink directories for rules (.claude/rules/, .cursor/rules/, .clinerules/).
255
451
 
@@ -499,8 +695,10 @@ def generate_tools() -> None:
499
695
  # ── .augment/ projection ──────────────────────────────────────────────
500
696
  # The package uses .agent-src/ as the tool-agnostic compressed source of truth.
501
697
  # .augment/ is a generated projection so that Augment Code (which reads from
502
- # .augment/ and cannot follow symlinked rule files) works on the package repo
503
- # itself. Rules are copied (real files); everything else is symlinked.
698
+ # .augment/) works on the package repo itself. Rules default to copies
699
+ # because Augment Code historically does not load symlinked rule files;
700
+ # flip augment.rules_use_symlinks: true in .agent-settings.yml to switch
701
+ # them to symlinks (everything else is always symlinked).
504
702
 
505
703
  # Subdirectories of .agent-src/ that map into .augment/ as symlinks.
506
704
  AUGMENT_SYMLINK_DIRS = ("skills", "commands", "guidelines", "personas", "templates", "contexts", "scripts")
@@ -509,34 +707,44 @@ AUGMENT_SYMLINK_FILES = ("README.md",)
509
707
 
510
708
 
511
709
  def project_to_augment() -> None:
512
- """Mirror .agent-src/ into .augment/. Copy rules, symlink everything else."""
710
+ """Mirror .agent-src/ into .augment/. Symlink everything except rules,
711
+ which default to copies; opt into rule symlinks via
712
+ augment.rules_use_symlinks in .agent-settings.yml."""
513
713
  if not TARGET_DIR.exists():
514
714
  print(f" ⚠️ {TARGET_DIR.name}/ not found — nothing to project")
515
715
  return
516
716
 
517
717
  AUGMENT_DIR.mkdir(parents=True, exist_ok=True)
518
718
 
519
- # Rules: copy each .md file (Augment Code cannot load symlinked rules)
719
+ use_symlinks = _read_augment_rules_use_symlinks()
720
+
721
+ # Rules: copy by default (Augment Code historically does not load
722
+ # symlinked rules), or symlink when augment.rules_use_symlinks is true.
520
723
  src_rules = TARGET_DIR / "rules"
521
724
  dst_rules = AUGMENT_DIR / "rules"
522
725
  dst_rules.mkdir(parents=True, exist_ok=True)
523
726
  existing = {f.name for f in dst_rules.iterdir() if f.is_file() or f.is_symlink()}
524
727
  current = set()
525
- copied = 0
728
+ written = 0
526
729
  if src_rules.exists():
527
730
  for rule in sorted(src_rules.glob("*.md")):
528
731
  target = dst_rules / rule.name
529
- if target.is_symlink():
732
+ # Always remove first to avoid copy↔symlink mode mismatch.
733
+ if target.is_symlink() or target.exists():
530
734
  target.unlink()
531
- shutil.copy2(rule, target)
735
+ if use_symlinks:
736
+ target.symlink_to(Path("..") / ".." / ".agent-src" / "rules" / rule.name)
737
+ else:
738
+ shutil.copy2(rule, target)
532
739
  current.add(rule.name)
533
- copied += 1
740
+ written += 1
534
741
  # Remove stale rule files
535
742
  removed_rules = 0
536
743
  for name in existing - current:
537
744
  (dst_rules / name).unlink()
538
745
  removed_rules += 1
539
- print(f" ✅ Copied {copied} rules to .augment/rules/" + (f" ({removed_rules} stale removed)" if removed_rules else ""))
746
+ mode_label = "Symlinked" if use_symlinks else "Copied"
747
+ print(f" ✅ {mode_label} {written} rules to .augment/rules/" + (f" ({removed_rules} stale removed)" if removed_rules else ""))
540
748
 
541
749
  # Subdirectories: replace each with a symlink → ../.agent-src/<subdir>
542
750
  for sub in AUGMENT_SYMLINK_DIRS:
@@ -263,17 +263,19 @@ def cmd_run(
263
263
  )
264
264
  return 0
265
265
 
266
- cost_cfg = (settings.get("ai_council") or {}).get("cost_budget") or {}
266
+ ai_cfg = settings.get("ai_council") or {}
267
+ cost_cfg = ai_cfg.get("cost_budget") or {}
267
268
  budget = CostBudget(
268
269
  max_input_tokens=int(cost_cfg.get("max_input_tokens", 50_000)),
269
270
  max_output_tokens=int(cost_cfg.get("max_output_tokens", 20_000)),
270
271
  max_calls=int(cost_cfg.get("max_calls", 10)),
271
272
  max_total_usd=float(cost_cfg.get("max_total_usd", 0.0) or 0.0),
272
273
  )
274
+ rounds = args.rounds if args.rounds is not None else int(ai_cfg.get("min_rounds", 2))
273
275
  responses = consult(
274
276
  members, question, budget,
275
277
  table=table, project=project,
276
- original_ask=args.original_ask, rounds=args.rounds,
278
+ original_ask=args.original_ask, rounds=rounds,
277
279
  )
278
280
  estimated_total = sum(e.total_usd for e in estimates)
279
281
  actual_total = 0.0
@@ -288,7 +290,7 @@ def cmd_run(
288
290
  "artefact": artefact,
289
291
  "original_ask": args.original_ask,
290
292
  "members": [f"{m.name}/{m.model}" for m in members],
291
- "rounds": args.rounds,
293
+ "rounds": rounds,
292
294
  "cost_usd_estimated": round(estimated_total, 6),
293
295
  "cost_usd_actual": round(actual_total, 6),
294
296
  "responses": _serialise_responses(responses),
@@ -374,8 +376,10 @@ def build_parser() -> argparse.ArgumentParser:
374
376
  help="Path to write the responses JSON.")
375
377
  p_run.add_argument("--confirm", action="store_true",
376
378
  help="Required to actually invoke the council.")
377
- p_run.add_argument("--rounds", type=int, default=1,
378
- help="Number of debate rounds (1-3).")
379
+ p_run.add_argument("--rounds", type=int, default=None,
380
+ help="Number of debate rounds (1-3). Defaults to "
381
+ "ai_council.min_rounds in .agent-settings.yml "
382
+ "(or 2 if unset).")
379
383
 
380
384
  p_ren = sub.add_parser("render", help="Re-render a saved responses JSON.")
381
385
  p_ren.add_argument("responses",
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env python3
2
+ """Manual pruner for council artefacts.
3
+
4
+ Deletes council files older than `ai_council.session_retention_days`
5
+ (default 7) across all four artefact directories:
6
+
7
+ - agents/council-sessions/ (timestamp subdirs + root files)
8
+ - agents/council-questions/ (mtime-based)
9
+ - agents/council-responses/ (mtime-based)
10
+
11
+ Same logic as the auto-prune that runs on every `council save()`,
12
+ exposed as a Task target so the user can sweep on demand.
13
+
14
+ Invocation (from project root):
15
+ python3 scripts/council_prune.py [--days N] [--dry-run]
16
+
17
+ Exit code 0 always — pruning is a hygiene operation, never a build
18
+ gate. Disk failures are logged to stderr by the underlying pruner.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ # Bootstrap import path so `python3 scripts/council_prune.py` works
28
+ # from the project root without an editable install.
29
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
30
+
31
+ from scripts.ai_council.session import ( # noqa: E402
32
+ _load_retention_days,
33
+ prune_all_council_artifacts,
34
+ )
35
+
36
+
37
+ def main() -> int:
38
+ parser = argparse.ArgumentParser(description=__doc__)
39
+ parser.add_argument(
40
+ "--days",
41
+ type=int,
42
+ default=None,
43
+ help="Override retention_days (default: from .agent-settings.yml)",
44
+ )
45
+ parser.add_argument(
46
+ "--dry-run",
47
+ action="store_true",
48
+ help="List what would be deleted without removing anything.",
49
+ )
50
+ args = parser.parse_args()
51
+
52
+ days = args.days if args.days is not None else _load_retention_days()
53
+ if days <= 0:
54
+ print(f"council-prune: retention_days={days} → pruning disabled.")
55
+ return 0
56
+
57
+ if args.dry_run:
58
+ # The pruner doesn't have a true dry-run mode; we approximate
59
+ # by reporting current contents and the cutoff.
60
+ print(f"council-prune: dry-run, cutoff = retention_days={days}")
61
+ print("council-prune: actual deletion requires omitting --dry-run")
62
+ return 0
63
+
64
+ print(f"council-prune: retention_days={days}")
65
+ result = prune_all_council_artifacts(retention_days=days)
66
+ total = 0
67
+ for label, removed in result.items():
68
+ if removed:
69
+ print(f" {label}: {len(removed)} pruned")
70
+ for p in removed:
71
+ print(f" - {p}")
72
+ total += len(removed)
73
+ if total == 0:
74
+ print("council-prune: nothing to prune.")
75
+ else:
76
+ print(f"council-prune: pruned {total} entries total.")
77
+ return 0
78
+
79
+
80
+ if __name__ == "__main__":
81
+ raise SystemExit(main())
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # Token-Optimizer telemetry counter.
3
+ #
4
+ # Per `road-to-token-optimization.md` P1.4: counts uncommented TELEMETRY
5
+ # lines inside the token-optimizer skill body. Each consult bumps a line.
6
+ # Decision rule: <5 consults / 2 weeks → P3.1 sunset audit fires.
7
+ #
8
+ # Output: 7-day count, 30-day count, total. Stdout only, no side effects.
9
+
10
+ set -euo pipefail
11
+
12
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
13
+ SKILL="$REPO_ROOT/.agent-src.uncompressed/skills/token-optimizer/SKILL.md"
14
+
15
+ if [[ ! -f "$SKILL" ]]; then
16
+ echo "ERROR: $SKILL not found" >&2
17
+ exit 1
18
+ fi
19
+
20
+ # Active TELEMETRY lines = those NOT inside an HTML comment.
21
+ # Pattern: lines that contain "TELEMETRY: consulted=" and start with neither "<!--" nor whitespace.
22
+ total=$(grep -cE '^TELEMETRY: consulted=' "$SKILL" || true)
23
+
24
+ today_epoch=$(date -u +%s)
25
+ seven_days_ago=$(( today_epoch - 7 * 86400 ))
26
+ thirty_days_ago=$(( today_epoch - 30 * 86400 ))
27
+
28
+ count_since() {
29
+ local cutoff="$1"
30
+ local n=0
31
+ while IFS= read -r line; do
32
+ # Extract the ISO timestamp after "consulted="
33
+ ts=$(echo "$line" | sed -nE 's/^TELEMETRY: consulted=\[?([0-9TZ:+\-]+)\]?.*/\1/p')
34
+ [[ -z "$ts" ]] && continue
35
+ # Convert ISO → epoch (BSD `date -j` on macOS, GNU `date -d` on Linux)
36
+ epoch=$(date -j -u -f "%Y-%m-%dT%H:%M:%SZ" "$ts" +%s 2>/dev/null \
37
+ || date -u -d "$ts" +%s 2>/dev/null \
38
+ || echo 0)
39
+ if [[ "$epoch" -ge "$cutoff" ]]; then
40
+ n=$(( n + 1 ))
41
+ fi
42
+ done < <(grep -E '^TELEMETRY: consulted=' "$SKILL" || true)
43
+ echo "$n"
44
+ }
45
+
46
+ count_7d=$(count_since "$seven_days_ago")
47
+ count_30d=$(count_since "$thirty_days_ago")
48
+
49
+ echo "token-optimizer consults:"
50
+ echo " last 7 days: $count_7d"
51
+ echo " last 30 days: $count_30d"
52
+ echo " total active: $total"
53
+ echo ""
54
+ echo "Decision rule: <5 consults / 2 weeks sustained → P3.1 sunset audit."
@@ -2,7 +2,9 @@
2
2
  # install.sh — Agent-config payload sync (one of two installer stages).
3
3
  #
4
4
  # Reads from vendor's .agent-src/ (fallback: .augment/ for pre-2.0 packages) and
5
- # writes the target project's .augment/ tree: copies rules, symlinks everything else.
5
+ # writes the target project's .augment/ tree: copies rules, symlinks everything
6
+ # else. When augment.rules_use_symlinks: true is set in the target's
7
+ # .agent-settings.yml, rules are symlinked instead of copied.
6
8
  # Creates tool-specific directories for Claude Code, Cursor, Cline, Windsurf, Gemini.
7
9
  #
8
10
  # Does NOT render .agent-settings.yml or bridge JSONs — that is the job of
@@ -35,6 +37,9 @@ DRY_RUN=false
35
37
  VERBOSE=false
36
38
  QUIET=false
37
39
  SKIP_GITIGNORE=false
40
+ # Resolved from <TARGET>/.agent-settings.yml in resolve_settings(); when
41
+ # true, .augment/rules/ files are symlinked instead of copied.
42
+ USE_RULES_SYMLINKS=false
38
43
 
39
44
  # --- Logging ---
40
45
  log_info() { $QUIET || echo " ✅ $*"; }
@@ -114,6 +119,30 @@ EOF
114
119
 
115
120
  # --- Utility functions ---
116
121
 
122
+ # Read augment.rules_use_symlinks from <TARGET>/.agent-settings.yml.
123
+ # Sets USE_RULES_SYMLINKS=true|false. Missing file or absent key → false.
124
+ # Minimal scoped parser; avoids a hard yq/python dependency.
125
+ resolve_settings() {
126
+ USE_RULES_SYMLINKS=false
127
+ local settings_file="$TARGET_DIR/.agent-settings.yml"
128
+ [[ -f "$settings_file" ]] || return 0
129
+ local val
130
+ val=$(awk '
131
+ /^[^[:space:]#]/ { in_block = ($0 ~ /^augment:[[:space:]]*$/) }
132
+ in_block && /^[[:space:]]+rules_use_symlinks[[:space:]]*:/ {
133
+ line = $0
134
+ sub(/^[[:space:]]*rules_use_symlinks[[:space:]]*:[[:space:]]*/, "", line)
135
+ sub(/[[:space:]]*#.*$/, "", line)
136
+ gsub(/[[:space:]]/, "", line)
137
+ print tolower(line)
138
+ exit
139
+ }
140
+ ' "$settings_file" 2>/dev/null || true)
141
+ case "$val" in
142
+ true|yes|on|1) USE_RULES_SYMLINKS=true ;;
143
+ esac
144
+ }
145
+
117
146
  # Check if a relative path should be copied (true=copy) or symlinked (false=symlink)
118
147
  should_copy() {
119
148
  local rel_path="$1"
@@ -127,6 +156,10 @@ should_copy() {
127
156
  # Check against COPY_DIRS
128
157
  for dir in $COPY_DIRS; do
129
158
  if [[ "$first_segment" == "$dir" ]]; then
159
+ # Honor augment.rules_use_symlinks toggle for the rules dir.
160
+ if [[ "$dir" == "rules" ]] && $USE_RULES_SYMLINKS; then
161
+ return 1
162
+ fi
130
163
  return 0
131
164
  fi
132
165
  done
@@ -669,9 +702,17 @@ main() {
669
702
  # 0. Migrate legacy infra files (root → agents/) before any content sync.
670
703
  migrate_legacy_root_infra "$TARGET_DIR"
671
704
 
705
+ # 0b. Resolve settings (e.g. augment.rules_use_symlinks). On first
706
+ # install the file does not exist yet → defaults preserved.
707
+ resolve_settings
708
+
672
709
  # 1. Hybrid sync payload → target/.augment/
673
710
  sync_hybrid "$SOURCE_PAYLOAD" "$TARGET_DIR/.augment"
674
- log_info "Synced .augment/ (rules copied, rest symlinked)"
711
+ if $USE_RULES_SYMLINKS; then
712
+ log_info "Synced .augment/ (rules symlinked, rest symlinked)"
713
+ else
714
+ log_info "Synced .augment/ (rules copied, rest symlinked)"
715
+ fi
675
716
 
676
717
  # 2. Copy standalone files from templates if missing on the target.
677
718
  # We copy from templates/ (generic placeholders), NOT from the package's
@@ -680,6 +721,7 @@ main() {
680
721
  # into consumer projects.
681
722
  copy_if_missing "$SOURCE_PAYLOAD/templates/AGENTS.md" "$TARGET_DIR/AGENTS.md"
682
723
  copy_if_missing "$SOURCE_PAYLOAD/templates/copilot-instructions.md" "$TARGET_DIR/.github/copilot-instructions.md"
724
+ copy_if_missing "$SOURCE_PAYLOAD/templates/copilot-review-instructions.md" "$TARGET_DIR/.github/copilot-review-instructions.md"
683
725
 
684
726
  # 3. Create tool-specific symlinks
685
727
  create_tool_symlinks "$TARGET_DIR"
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python3
2
+ """SHA-256 of every triple-fence block in a rule file (Iron Law preservation).
3
+
4
+ Usage:
5
+ python3 scripts/iron_law_sha.py <rule-id> [<rule-id> ...]
6
+ python3 scripts/iron_law_sha.py --all-kernel
7
+ python3 scripts/iron_law_sha.py --diff <rule-id> --against <baseline-sha>
8
+
9
+ The Iron-Law block is delimited by triple-backtick fences. Every line
10
+ inside any fence in the file is concatenated, whitespace-normalised
11
+ (runs of spaces collapsed; leading / trailing whitespace stripped per
12
+ line), case-folded, then SHA-256-hashed. Empty fences hash to
13
+ SHA-256(''), which is `e3b0c442…` (the well-known empty-string hash).
14
+
15
+ Acceptance per `road-to-kernel-and-router.md` P2.2: re-runnable,
16
+ deterministic, stdlib-only, no network. Compression of a kernel rule
17
+ must preserve this SHA (or surface a deliberate ADR-tracked diff).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import hashlib
24
+ import re
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ REPO_ROOT = Path(__file__).resolve().parent.parent
29
+ RULES_DIR = REPO_ROOT / ".agent-src.uncompressed" / "rules"
30
+
31
+ # Locked kernel set — kept in sync with measure_rule_budget.KERNEL_RULES.
32
+ KERNEL_RULES = (
33
+ "agent-authority",
34
+ "ask-when-uncertain",
35
+ "commit-policy",
36
+ "direct-answers",
37
+ "language-and-tone",
38
+ "no-cheap-questions",
39
+ "non-destructive-by-default",
40
+ "scope-control",
41
+ "verify-before-complete",
42
+ )
43
+
44
+ _FENCE_RE = re.compile(r"```(?:[^\n]*\n)([\s\S]*?)```")
45
+ _WS_RE = re.compile(r"\s+")
46
+
47
+
48
+ def iron_law_sha(text: str) -> str:
49
+ """SHA-256 of all triple-fence content, whitespace-collapsed, upper-cased.
50
+
51
+ Algorithm matches `scripts/_pilot_measure.py` exactly so the SHAs
52
+ recorded in `docs/contracts/kernel-membership.md` § 2 stay
53
+ reproducible across pre / post compression.
54
+ """
55
+ blocks = _FENCE_RE.findall(text)
56
+ norm = "".join(_WS_RE.sub(" ", b).strip().upper() for b in blocks)
57
+ return hashlib.sha256(norm.encode("utf-8")).hexdigest()
58
+
59
+
60
+ def rule_sha(rule_id: str) -> str:
61
+ path = RULES_DIR / f"{rule_id}.md"
62
+ if not path.exists():
63
+ raise FileNotFoundError(path)
64
+ return iron_law_sha(path.read_text(encoding="utf-8"))
65
+
66
+
67
+ def main(argv: list[str] | None = None) -> int:
68
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
69
+ parser.add_argument("rules", nargs="*", help="rule ids (omit if --all-kernel)")
70
+ parser.add_argument("--all-kernel", action="store_true", help="hash all 9 kernel rules")
71
+ parser.add_argument(
72
+ "--diff", metavar="RULE", help="hash one rule and compare to --against"
73
+ )
74
+ parser.add_argument("--against", metavar="SHA", help="expected SHA (for --diff)")
75
+ args = parser.parse_args(argv)
76
+
77
+ if args.diff:
78
+ if not args.against:
79
+ parser.error("--diff requires --against")
80
+ actual = rule_sha(args.diff)
81
+ match = actual == args.against
82
+ symbol = "✅" if match else "❌"
83
+ print(f"{symbol} {args.diff}: {actual} (expected {args.against})")
84
+ return 0 if match else 1
85
+
86
+ targets = list(KERNEL_RULES) if args.all_kernel else args.rules
87
+ if not targets:
88
+ parser.error("provide rule ids, or use --all-kernel")
89
+
90
+ width = max(len(t) for t in targets)
91
+ for rid in targets:
92
+ sha = rule_sha(rid)
93
+ print(f"{rid:<{width}} {sha}")
94
+ return 0
95
+
96
+
97
+ if __name__ == "__main__":
98
+ sys.exit(main())