@event4u/agent-config 1.14.0 → 1.16.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 (293) hide show
  1. package/.agent-src/commands/agent-handoff.md +1 -1
  2. package/.agent-src/commands/bug-fix.md +3 -3
  3. package/.agent-src/commands/bug-investigate.md +2 -2
  4. package/.agent-src/commands/chat-history-checkpoint.md +3 -3
  5. package/.agent-src/commands/chat-history-clear.md +2 -2
  6. package/.agent-src/commands/chat-history-resume.md +2 -2
  7. package/.agent-src/commands/chat-history.md +3 -3
  8. package/.agent-src/commands/check-current-md.md +44 -33
  9. package/.agent-src/commands/commit-in-chunks.md +43 -23
  10. package/.agent-src/commands/compress.md +34 -2
  11. package/.agent-src/commands/council-design.md +96 -0
  12. package/.agent-src/commands/council-optimize.md +115 -0
  13. package/.agent-src/commands/council-pr.md +123 -0
  14. package/.agent-src/commands/council.md +219 -0
  15. package/.agent-src/commands/create-pr.md +23 -0
  16. package/.agent-src/commands/do-and-judge.md +3 -3
  17. package/.agent-src/commands/do-in-steps.md +4 -4
  18. package/.agent-src/commands/e2e-heal.md +1 -1
  19. package/.agent-src/commands/e2e-plan.md +1 -1
  20. package/.agent-src/commands/feature-dev.md +8 -0
  21. package/.agent-src/commands/feature-explore.md +6 -1
  22. package/.agent-src/commands/feature-plan.md +33 -2
  23. package/.agent-src/commands/feature-refactor.md +5 -0
  24. package/.agent-src/commands/feature-roadmap.md +8 -3
  25. package/.agent-src/commands/feature.md +58 -0
  26. package/.agent-src/commands/fix-ci.md +5 -0
  27. package/.agent-src/commands/fix-portability.md +7 -2
  28. package/.agent-src/commands/fix-pr-bot-comments.md +5 -0
  29. package/.agent-src/commands/fix-pr-comments.md +5 -0
  30. package/.agent-src/commands/fix-pr-developer-comments.md +5 -0
  31. package/.agent-src/commands/fix-references.md +5 -0
  32. package/.agent-src/commands/fix-seeder.md +5 -0
  33. package/.agent-src/commands/fix.md +60 -0
  34. package/.agent-src/commands/jira-ticket.md +1 -1
  35. package/.agent-src/commands/judge.md +1 -1
  36. package/.agent-src/commands/memory-add.md +3 -3
  37. package/.agent-src/commands/memory-full.md +2 -2
  38. package/.agent-src/commands/memory-promote.md +2 -2
  39. package/.agent-src/commands/mode.md +5 -5
  40. package/.agent-src/commands/onboard.md +17 -8
  41. package/.agent-src/commands/optimize-agents.md +6 -1
  42. package/.agent-src/commands/optimize-augmentignore.md +14 -0
  43. package/.agent-src/commands/optimize-rtk-filters.md +5 -0
  44. package/.agent-src/commands/optimize-skills.md +6 -1
  45. package/.agent-src/commands/optimize.md +54 -0
  46. package/.agent-src/commands/propose-memory.md +2 -2
  47. package/.agent-src/commands/refine-ticket.md +9 -7
  48. package/.agent-src/commands/review-changes.md +61 -9
  49. package/.agent-src/commands/review-routing.md +1 -1
  50. package/.agent-src/commands/roadmap-create.md +42 -4
  51. package/.agent-src/commands/roadmap-execute.md +9 -7
  52. package/.agent-src/commands/set-cost-profile.md +11 -3
  53. package/.agent-src/commands/sync-agent-settings.md +11 -2
  54. package/.agent-src/commands/tests-create.md +1 -1
  55. package/.agent-src/commands/tests-execute.md +2 -3
  56. package/.agent-src/commands/upstream-contribute.md +1 -1
  57. package/.agent-src/contexts/authority/commit-mechanics.md +57 -0
  58. package/.agent-src/contexts/authority/destructive-mechanics.md +66 -0
  59. package/.agent-src/contexts/authority/scope-mechanics.md +87 -0
  60. package/.agent-src/contexts/execution/autonomy-detection.md +54 -0
  61. package/.agent-src/contexts/execution/autonomy-examples.md +90 -0
  62. package/.agent-src/contexts/execution/autonomy-mechanics.md +29 -0
  63. package/.agent-src/contexts/execution/verification-mechanics.md +80 -0
  64. package/.agent-src/personas/README.md +1 -1
  65. package/.agent-src/rules/agent-authority.md +24 -0
  66. package/.agent-src/rules/architecture.md +1 -1
  67. package/.agent-src/rules/artifact-drafting-protocol.md +1 -1
  68. package/.agent-src/rules/artifact-engagement-recording.md +2 -2
  69. package/.agent-src/rules/ask-when-uncertain.md +1 -1
  70. package/.agent-src/rules/augment-portability.md +56 -37
  71. package/.agent-src/rules/autonomous-execution.md +78 -114
  72. package/.agent-src/rules/capture-learnings.md +1 -1
  73. package/.agent-src/rules/chat-history-cadence.md +109 -0
  74. package/.agent-src/rules/chat-history-ownership.md +123 -0
  75. package/.agent-src/rules/chat-history-visibility.md +96 -0
  76. package/.agent-src/rules/cli-output-handling.md +1 -1
  77. package/.agent-src/rules/{command-suggestion.md → command-suggestion-policy.md} +10 -9
  78. package/.agent-src/rules/commit-conventions.md +1 -1
  79. package/.agent-src/rules/commit-policy.md +43 -61
  80. package/.agent-src/rules/context-hygiene.md +3 -3
  81. package/.agent-src/rules/direct-answers.md +2 -2
  82. package/.agent-src/rules/docs-sync.md +1 -1
  83. package/.agent-src/rules/e2e-testing.md +1 -1
  84. package/.agent-src/rules/guidelines.md +4 -4
  85. package/.agent-src/rules/improve-before-implement.md +2 -2
  86. package/.agent-src/rules/language-and-tone.md +41 -96
  87. package/.agent-src/rules/minimal-safe-diff.md +3 -3
  88. package/.agent-src/rules/model-recommendation.md +4 -4
  89. package/.agent-src/rules/no-cheap-questions.md +89 -0
  90. package/.agent-src/rules/non-destructive-by-default.md +25 -59
  91. package/.agent-src/rules/onboarding-gate.md +5 -5
  92. package/.agent-src/rules/review-routing-awareness.md +9 -9
  93. package/.agent-src/rules/roadmap-progress-sync.md +132 -80
  94. package/.agent-src/rules/role-mode-adherence.md +3 -3
  95. package/.agent-src/rules/scope-control.md +65 -46
  96. package/.agent-src/rules/security-sensitive-stop.md +2 -2
  97. package/.agent-src/rules/size-enforcement.md +3 -2
  98. package/.agent-src/rules/think-before-action.md +5 -5
  99. package/.agent-src/rules/token-efficiency.md +4 -4
  100. package/.agent-src/rules/{ui-audit-before-build.md → ui-audit-gate.md} +3 -3
  101. package/.agent-src/rules/user-interaction.md +31 -7
  102. package/.agent-src/rules/verify-before-complete.md +12 -67
  103. package/.agent-src/scripts/update_roadmap_progress.py +65 -8
  104. package/.agent-src/skills/ai-council/SKILL.md +333 -0
  105. package/.agent-src/skills/api-endpoint/SKILL.md +2 -2
  106. package/.agent-src/skills/blade-ui/SKILL.md +30 -11
  107. package/.agent-src/skills/blast-radius-analyzer/SKILL.md +1 -1
  108. package/.agent-src/skills/bug-analyzer/SKILL.md +1 -1
  109. package/.agent-src/skills/command-routing/SKILL.md +1 -1
  110. package/.agent-src/skills/command-writing/SKILL.md +16 -5
  111. package/.agent-src/skills/conventional-commits-writing/SKILL.md +1 -1
  112. package/.agent-src/skills/copilot-agents-optimization/SKILL.md +2 -2
  113. package/.agent-src/skills/developer-like-execution/SKILL.md +2 -2
  114. package/.agent-src/skills/existing-ui-audit/SKILL.md +24 -9
  115. package/.agent-src/skills/fe-design/SKILL.md +20 -15
  116. package/.agent-src/skills/file-editor/SKILL.md +9 -0
  117. package/.agent-src/skills/flux/SKILL.md +1 -1
  118. package/.agent-src/skills/git-workflow/SKILL.md +1 -1
  119. package/.agent-src/skills/guideline-writing/SKILL.md +11 -11
  120. package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +4 -4
  121. package/.agent-src/skills/livewire/SKILL.md +27 -8
  122. package/.agent-src/skills/override-management/SKILL.md +2 -2
  123. package/.agent-src/skills/php-coder/SKILL.md +1 -1
  124. package/.agent-src/skills/playwright-testing/SKILL.md +2 -2
  125. package/.agent-src/skills/readme-reviewer/SKILL.md +1 -1
  126. package/.agent-src/skills/readme-writing/SKILL.md +1 -1
  127. package/.agent-src/skills/readme-writing-package/SKILL.md +1 -1
  128. package/.agent-src/skills/receiving-code-review/SKILL.md +1 -1
  129. package/.agent-src/skills/refine-ticket/SKILL.md +30 -24
  130. package/.agent-src/skills/review-routing/SKILL.md +2 -2
  131. package/.agent-src/skills/roadmap-management/SKILL.md +22 -16
  132. package/.agent-src/skills/rule-writing/SKILL.md +1 -1
  133. package/.agent-src/skills/skill-reviewer/SKILL.md +1 -1
  134. package/.agent-src/skills/skill-writing/SKILL.md +6 -6
  135. package/.agent-src/skills/subagent-orchestration/SKILL.md +1 -0
  136. package/.agent-src/skills/systematic-debugging/SKILL.md +1 -1
  137. package/.agent-src/skills/upstream-contribute/SKILL.md +3 -3
  138. package/.agent-src/skills/validate-feature-fit/SKILL.md +2 -2
  139. package/.agent-src/skills/{verify-before-complete → verify-completion-evidence}/SKILL.md +2 -2
  140. package/.agent-src/templates/agent-settings.md +9 -9
  141. package/.agent-src/templates/contexts/auth-model.md +1 -1
  142. package/.agent-src/templates/roadmaps.md +9 -8
  143. package/.agent-src/templates/scripts/README.md +2 -2
  144. package/.agent-src/templates/scripts/memory_lookup.py +1 -1
  145. package/.agent-src/templates/scripts/telemetry/aggregator.py +16 -1
  146. package/.agent-src/templates/scripts/telemetry/engagement.py +59 -0
  147. package/.agent-src/templates/scripts/telemetry/report_renderer.py +28 -1
  148. package/.agent-src/templates/scripts/telemetry_record.py +14 -1
  149. package/.agent-src/templates/scripts/work_engine/__init__.py +2 -2
  150. package/.agent-src/templates/scripts/work_engine/cli.py +64 -461
  151. package/.agent-src/templates/scripts/work_engine/cli_args.py +116 -0
  152. package/.agent-src/templates/scripts/work_engine/delivery_state.py +3 -3
  153. package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +1 -1
  154. package/.agent-src/templates/scripts/work_engine/directives/backend/implement.py +1 -1
  155. package/.agent-src/templates/scripts/work_engine/directives/backend/memory.py +1 -1
  156. package/.agent-src/templates/scripts/work_engine/directives/backend/plan.py +1 -1
  157. package/.agent-src/templates/scripts/work_engine/directives/backend/report.py +1 -1
  158. package/.agent-src/templates/scripts/work_engine/dispatcher.py +1 -1
  159. package/.agent-src/templates/scripts/work_engine/emitters.py +43 -0
  160. package/.agent-src/templates/scripts/work_engine/errors.py +19 -0
  161. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +76 -0
  162. package/.agent-src/templates/scripts/work_engine/input_builders.py +163 -0
  163. package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +34 -2
  164. package/.agent-src/templates/scripts/work_engine/persona_policy.py +1 -1
  165. package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +1 -1
  166. package/.agent-src/templates/scripts/work_engine/state_io.py +202 -0
  167. package/.claude-plugin/marketplace.json +10 -2
  168. package/AGENTS.md +16 -12
  169. package/CHANGELOG.md +206 -9
  170. package/README.md +51 -52
  171. package/config/agent-settings.template.yml +58 -1
  172. package/config/gitignore-block.txt +3 -0
  173. package/docs/MIGRATION.md +122 -0
  174. package/docs/architecture.md +83 -34
  175. package/docs/catalog.md +331 -0
  176. package/docs/contracts/STABILITY.md +134 -0
  177. package/docs/contracts/adr-chat-history-split.md +132 -0
  178. package/docs/contracts/adr-command-suggestion.md +146 -0
  179. package/docs/contracts/adr-implement-ticket-runtime.md +122 -0
  180. package/docs/contracts/adr-product-ui-track.md +384 -0
  181. package/docs/contracts/adr-prompt-driven-execution.md +187 -0
  182. package/docs/contracts/agent-memory-contract.md +149 -0
  183. package/docs/contracts/artifact-engagement-flow.md +262 -0
  184. package/docs/contracts/command-clusters.md +126 -0
  185. package/docs/contracts/command-suggestion-flow.md +148 -0
  186. package/docs/contracts/implement-ticket-flow.md +628 -0
  187. package/docs/contracts/linear-ai-rules-inclusion.md +143 -0
  188. package/docs/contracts/linear-ai-three-layers.md +131 -0
  189. package/docs/contracts/load-context-schema.md +186 -0
  190. package/docs/contracts/rule-interactions.md +107 -0
  191. package/docs/contracts/rule-interactions.yml +238 -0
  192. package/docs/contracts/rule-priority-hierarchy.md +87 -0
  193. package/docs/contracts/ui-stack-extension.md +236 -0
  194. package/docs/contracts/ui-track-flow.md +338 -0
  195. package/docs/customization.md +14 -0
  196. package/docs/end-to-end-walkthroughs.md +165 -0
  197. package/docs/getting-started.md +27 -9
  198. package/docs/github-topics.md +12 -3
  199. package/docs/guidelines/agent-infra/language-and-tone-examples.md +79 -0
  200. package/{.agent-src → docs}/guidelines/docs/readme-size-and-splitting.md +26 -25
  201. package/docs/guidelines/php/git.md +164 -0
  202. package/docs/installation.md +42 -6
  203. package/docs/migrations/commands-1.15.0.md +112 -0
  204. package/docs/showcase.md +9 -4
  205. package/docs/skills-catalog.md +14 -8
  206. package/docs/ui-track-mental-model.md +121 -0
  207. package/llms.txt +13 -7
  208. package/package.json +1 -1
  209. package/scripts/agent-config +23 -0
  210. package/scripts/ai_council/__init__.py +39 -0
  211. package/scripts/ai_council/_default_prices.py +41 -0
  212. package/scripts/ai_council/_one_off_rebalancing_audit.py +149 -0
  213. package/scripts/ai_council/_one_off_roundtrip.py +106 -0
  214. package/scripts/ai_council/budget_guard.py +172 -0
  215. package/scripts/ai_council/bundler.py +261 -0
  216. package/scripts/ai_council/clients.py +381 -0
  217. package/scripts/ai_council/modes.py +127 -0
  218. package/scripts/ai_council/orchestrator.py +350 -0
  219. package/scripts/ai_council/pricing.py +213 -0
  220. package/scripts/ai_council/project_context.py +159 -0
  221. package/scripts/ai_council/prompts.py +232 -0
  222. package/scripts/ai_council/session.py +144 -0
  223. package/scripts/build_linear_digest.py +4 -4
  224. package/scripts/check_always_budget.py +126 -0
  225. package/scripts/check_augmentignore.py +69 -0
  226. package/scripts/check_command_count_messaging.py +120 -0
  227. package/scripts/check_portability.py +57 -0
  228. package/scripts/check_public_catalog_links.py +122 -0
  229. package/scripts/check_public_links.py +185 -0
  230. package/scripts/check_references.py +5 -1
  231. package/scripts/check_roadmap_trackable.py +111 -0
  232. package/scripts/command_suggester/cooldown.py +1 -1
  233. package/scripts/generate_index.py +266 -0
  234. package/scripts/install_anthropic_key.sh +5 -0
  235. package/scripts/install_openai_key.sh +106 -0
  236. package/scripts/lint_load_context.py +163 -0
  237. package/scripts/lint_no_new_atomic_commands.py +179 -0
  238. package/scripts/lint_rule_interactions.py +149 -0
  239. package/scripts/memory_lookup.py +1 -1
  240. package/scripts/release.py +297 -64
  241. package/scripts/schemas/command.schema.json +20 -0
  242. package/scripts/schemas/rule.schema.json +10 -0
  243. package/scripts/skill_linter.py +26 -4
  244. package/scripts/sync_agent_settings.py +1 -1
  245. package/scripts/update_counts.py +19 -4
  246. package/scripts/update_prices.py +124 -0
  247. package/.agent-src/guidelines/php/git.md +0 -96
  248. package/.agent-src/rules/chat-history.md +0 -200
  249. /package/.agent-src/rules/{slash-commands.md → slash-command-routing-policy.md} +0 -0
  250. /package/{.agent-src → docs}/guidelines/agent-infra/agent-interaction-and-decision-quality.md +0 -0
  251. /package/{.agent-src → docs}/guidelines/agent-infra/break-glass-usage.md +0 -0
  252. /package/{.agent-src → docs}/guidelines/agent-infra/developer-judgment.md +0 -0
  253. /package/{.agent-src → docs}/guidelines/agent-infra/engineering-memory-data-format.md +0 -0
  254. /package/{.agent-src → docs}/guidelines/agent-infra/layered-settings.md +0 -0
  255. /package/{.agent-src → docs}/guidelines/agent-infra/memory-access.md +0 -0
  256. /package/{.agent-src → docs}/guidelines/agent-infra/naming.md +0 -0
  257. /package/{.agent-src → docs}/guidelines/agent-infra/output-patterns.md +0 -0
  258. /package/{.agent-src → docs}/guidelines/agent-infra/review-routing-data-format.md +0 -0
  259. /package/{.agent-src → docs}/guidelines/agent-infra/role-contracts.md +0 -0
  260. /package/{.agent-src → docs}/guidelines/agent-infra/role-mode-router.md +0 -0
  261. /package/{.agent-src → docs}/guidelines/agent-infra/runtime-layer.md +0 -0
  262. /package/{.agent-src → docs}/guidelines/agent-infra/self-improvement-pipeline.md +0 -0
  263. /package/{.agent-src → docs}/guidelines/agent-infra/size-and-scope.md +0 -0
  264. /package/{.agent-src → docs}/guidelines/agent-infra/tool-integration.md +0 -0
  265. /package/{.agent-src → docs}/guidelines/e2e/playwright.md +0 -0
  266. /package/{.agent-src → docs}/guidelines/php/api-design.md +0 -0
  267. /package/{.agent-src → docs}/guidelines/php/artisan-commands.md +0 -0
  268. /package/{.agent-src → docs}/guidelines/php/blade-ui.md +0 -0
  269. /package/{.agent-src → docs}/guidelines/php/controllers.md +0 -0
  270. /package/{.agent-src → docs}/guidelines/php/database.md +0 -0
  271. /package/{.agent-src → docs}/guidelines/php/eloquent.md +0 -0
  272. /package/{.agent-src → docs}/guidelines/php/flux.md +0 -0
  273. /package/{.agent-src → docs}/guidelines/php/general.md +0 -0
  274. /package/{.agent-src → docs}/guidelines/php/jobs.md +0 -0
  275. /package/{.agent-src → docs}/guidelines/php/livewire.md +0 -0
  276. /package/{.agent-src → docs}/guidelines/php/logging.md +0 -0
  277. /package/{.agent-src → docs}/guidelines/php/naming.md +0 -0
  278. /package/{.agent-src → docs}/guidelines/php/patterns/dependency-injection.md +0 -0
  279. /package/{.agent-src → docs}/guidelines/php/patterns/dtos.md +0 -0
  280. /package/{.agent-src → docs}/guidelines/php/patterns/events.md +0 -0
  281. /package/{.agent-src → docs}/guidelines/php/patterns/factory.md +0 -0
  282. /package/{.agent-src → docs}/guidelines/php/patterns/pipelines.md +0 -0
  283. /package/{.agent-src → docs}/guidelines/php/patterns/policies.md +0 -0
  284. /package/{.agent-src → docs}/guidelines/php/patterns/repositories.md +0 -0
  285. /package/{.agent-src → docs}/guidelines/php/patterns/service-layer.md +0 -0
  286. /package/{.agent-src → docs}/guidelines/php/patterns/strategy.md +0 -0
  287. /package/{.agent-src → docs}/guidelines/php/patterns.md +0 -0
  288. /package/{.agent-src → docs}/guidelines/php/performance.md +0 -0
  289. /package/{.agent-src → docs}/guidelines/php/resources.md +0 -0
  290. /package/{.agent-src → docs}/guidelines/php/security.md +0 -0
  291. /package/{.agent-src → docs}/guidelines/php/sql.md +0 -0
  292. /package/{.agent-src → docs}/guidelines/php/validations.md +0 -0
  293. /package/{.agent-src → docs}/guidelines/php/websocket.md +0 -0
@@ -0,0 +1,159 @@
1
+ """Lightweight project-context detector for the council handoff preamble.
2
+
3
+ Council members do better critique when they know what the project IS,
4
+ not just what the artefact looks like. This module reads the bare
5
+ minimum from the repo root — `composer.json`, `package.json`, root
6
+ `README.md` — and returns a neutral `ProjectContext`. All fields are
7
+ optional; missing data is `None` and the preamble silently omits the
8
+ line.
9
+
10
+ Iron law of neutrality (`ai-council` skill): nothing here may carry
11
+ host-agent identity, prior reasoning, or framing. Manifest fields and
12
+ README prose only.
13
+
14
+ Truncation strategy (locked by council review, 2026-05-02): the
15
+ ``repo_purpose`` field is capped at ``REPO_PURPOSE_MAX_CHARS`` by
16
+ stopping at the **last full sentence ≤ 400 chars**, with an ellipsis
17
+ when truncation occurred. We never cut mid-sentence — a half-sentence
18
+ reads as broken and adds noise to the council preamble.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import re
25
+ from dataclasses import dataclass
26
+ from pathlib import Path
27
+
28
+ REPO_PURPOSE_MAX_CHARS = 400
29
+ _HEADING_RE = re.compile(r"^\s*#")
30
+ _BADGE_RE = re.compile(r"^\s*(\[!\[|!\[|<).*")
31
+ _HTML_RE = re.compile(r"<[^>]+>")
32
+
33
+
34
+ @dataclass
35
+ class ProjectContext:
36
+ """Neutral project description for the council handoff preamble."""
37
+
38
+ name: str | None = None
39
+ stack: str | None = None
40
+ repo_purpose: str | None = None
41
+
42
+ def is_empty(self) -> bool:
43
+ return self.name is None and self.stack is None and self.repo_purpose is None
44
+
45
+
46
+ def _read_json(path: Path) -> dict | None:
47
+ if not path.exists():
48
+ return None
49
+ try:
50
+ data = json.loads(path.read_text(encoding="utf-8"))
51
+ except (json.JSONDecodeError, OSError):
52
+ return None
53
+ return data if isinstance(data, dict) else None
54
+
55
+
56
+ def _name_from(composer: dict | None, package: dict | None, root: Path) -> str | None:
57
+ for src in (composer, package):
58
+ if src and isinstance(src.get("name"), str) and src["name"].strip():
59
+ return src["name"].strip()
60
+ # Fall back to the directory name; useful for repos without manifests.
61
+ try:
62
+ return root.resolve().name or None
63
+ except OSError:
64
+ return None
65
+
66
+
67
+ def _stack_from(composer: dict | None, package: dict | None) -> str | None:
68
+ parts: list[str] = []
69
+ if composer:
70
+ php_v = (composer.get("require") or {}).get("php")
71
+ if isinstance(php_v, str):
72
+ parts.append(f"PHP {php_v}")
73
+ # Detect well-known frameworks without claiming the project IS one.
74
+ require = {**(composer.get("require") or {}), **(composer.get("require-dev") or {})}
75
+ for needle, label in (
76
+ ("laravel/framework", "Laravel"),
77
+ ("symfony/framework-bundle", "Symfony"),
78
+ ("laminas/laminas-mvc", "Laminas"),
79
+ ):
80
+ if needle in require:
81
+ parts.append(label)
82
+ break
83
+ if package:
84
+ engines = package.get("engines") or {}
85
+ if isinstance(engines, dict) and isinstance(engines.get("node"), str):
86
+ parts.append(f"Node {engines['node']}")
87
+ deps = {**(package.get("dependencies") or {}), **(package.get("devDependencies") or {})}
88
+ for needle, label in (
89
+ ("next", "Next.js"),
90
+ ("react", "React"),
91
+ ("vue", "Vue"),
92
+ ("@angular/core", "Angular"),
93
+ ):
94
+ if needle in deps:
95
+ parts.append(label)
96
+ break
97
+ if not parts:
98
+ return None
99
+ return " · ".join(parts)
100
+
101
+
102
+ def _purpose_from_readme(path: Path) -> str | None:
103
+ if not path.exists():
104
+ return None
105
+ try:
106
+ text = path.read_text(encoding="utf-8")
107
+ except OSError:
108
+ return None
109
+ paragraph: list[str] = []
110
+ for raw in text.splitlines():
111
+ line = raw.rstrip()
112
+ stripped = line.strip()
113
+ if not stripped:
114
+ if paragraph:
115
+ break
116
+ continue
117
+ if _HEADING_RE.match(stripped) or _BADGE_RE.match(stripped):
118
+ if paragraph:
119
+ break
120
+ continue
121
+ paragraph.append(stripped)
122
+ if not paragraph:
123
+ return None
124
+ joined = " ".join(paragraph)
125
+ joined = _HTML_RE.sub("", joined).strip()
126
+ if not joined:
127
+ return None
128
+ if len(joined) > REPO_PURPOSE_MAX_CHARS:
129
+ joined = _truncate_at_sentence(joined, REPO_PURPOSE_MAX_CHARS)
130
+ return joined
131
+
132
+
133
+ def _truncate_at_sentence(text: str, limit: int) -> str:
134
+ """Truncate at the last full sentence ≤ limit chars; append an ellipsis.
135
+
136
+ Total return length is always ≤ ``limit`` (ellipsis included).
137
+ """
138
+ budget = max(1, limit - 2)
139
+ head = text[:budget]
140
+ cut = max(head.rfind(". "), head.rfind("! "), head.rfind("? "))
141
+ if cut >= 0:
142
+ return head[: cut + 1].rstrip() + " …"
143
+ return head.rstrip() + " …"
144
+
145
+
146
+ def detect_project_context(root: Path | None = None) -> ProjectContext:
147
+ """Return a `ProjectContext` for `root` (default: cwd).
148
+
149
+ Always returns — never raises. Missing manifest files / README → the
150
+ matching field is `None`, and `handoff_preamble()` will omit the line.
151
+ """
152
+ root = (root or Path.cwd()).resolve()
153
+ composer = _read_json(root / "composer.json")
154
+ package = _read_json(root / "package.json")
155
+ return ProjectContext(
156
+ name=_name_from(composer, package, root),
157
+ stack=_stack_from(composer, package),
158
+ repo_purpose=_purpose_from_readme(root / "README.md"),
159
+ )
@@ -0,0 +1,232 @@
1
+ """Neutrality system prompts for the council.
2
+
3
+ Council members must NOT see the host agent's reasoning, internal
4
+ state, or framing language. Each prompt asks for an independent
5
+ critique on the artefact's own merits.
6
+
7
+ Anti-patterns guarded against in tests (test_prompts.py):
8
+ - No leak of host-agent identity ("Augment", "Claude Code", etc.).
9
+ - No "the agent thinks X" framing.
10
+ - No instructions that bias toward agreement.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from scripts.ai_council.project_context import ProjectContext
16
+
17
+ NEUTRALITY_PREAMBLE = """\
18
+ You are an independent reviewer. You have NOT seen any prior reasoning,
19
+ agent output, or commentary on the artefact below. Critique it on its
20
+ own merits. Disagree if warranted. Cite specific lines or sections.
21
+ Do not assume the artefact is correct just because it was sent to you.
22
+ """.strip()
23
+
24
+ # Host-agent identity strings that must never leak into a council member's
25
+ # view. Lines containing any of these (case-insensitive substring) are
26
+ # dropped before assembly. See `ai-council` skill § Neutrality.
27
+ HOST_AGENT_IDENTITY_PATTERNS = (
28
+ "augment",
29
+ "claude code",
30
+ "cursor agent",
31
+ "cursor ide",
32
+ "cline",
33
+ "windsurf",
34
+ "copilot agent",
35
+ )
36
+
37
+ # Per-mode addenda — appended after the preamble.
38
+
39
+ PROMPT_MODE = """\
40
+ The artefact is a free-form question or proposal from a developer.
41
+ Respond with:
42
+ 1. Your honest assessment (agree / disagree / mixed).
43
+ 2. The single strongest argument for your position.
44
+ 3. The single strongest counter-argument the developer should consider.
45
+ 4. Concrete next steps if you agree, or concrete alternatives if you disagree.
46
+ """.strip()
47
+
48
+ ROADMAP_MODE = """\
49
+ The artefact is a proposed implementation roadmap. Critique it as if
50
+ you were a senior engineer asked to greenlight it. Focus on:
51
+ 1. Hidden coupling between phases that the roadmap glosses over.
52
+ 2. Steps that are too coarse to verify ("implement X" vs "X with Y test").
53
+ 3. Missing rollback or kill-switch criteria.
54
+ 4. Sequencing risks — does step N really not block step N+1?
55
+ 5. Open questions disguised as decisions, or vice versa.
56
+ """.strip()
57
+
58
+ DIFF_MODE = """\
59
+ The artefact is a code diff. Review it for:
60
+ 1. Correctness — bugs, off-by-one, null-safety, type drift.
61
+ 2. Security — injection, secrets, unsafe deserialization, authZ gaps.
62
+ 3. Test coverage — uncovered branches, missing regression tests.
63
+ 4. Maintainability — surprise dependencies, naming drift, dead code.
64
+ End with: APPROVE / REQUEST_CHANGES / REJECT and one sentence why.
65
+ """.strip()
66
+
67
+ FILES_MODE = """\
68
+ The artefact is a set of source files for an architectural review.
69
+ Map out:
70
+ 1. The boundaries you see (modules, layers, trust zones).
71
+ 2. The strongest design decision present.
72
+ 3. The weakest design decision present.
73
+ 4. The single change that would most reduce future maintenance cost.
74
+ """.strip()
75
+
76
+ # Specialised modes — used by /council-pr, /council-design,
77
+ # /council-optimize. Selected via `mode_override=` in `/council` so the
78
+ # base modes (`prompt`, `roadmap`, `diff`, `files`) keep their v2 byte
79
+ # shape for back-compat with existing callers.
80
+
81
+ PR_MODE = """\
82
+ The artefact is a code diff from a pull request. Review with both a
83
+ correctness lens AND a shipping-risk lens:
84
+ 1. Correctness — bugs, off-by-one, null-safety, type drift.
85
+ 2. Security — injection, secrets, unsafe deserialization, authZ gaps.
86
+ 3. Test coverage — uncovered branches, missing regression tests.
87
+ 4. Shipping risk — does this PR mix concerns that should be split?
88
+ Is the blast radius bigger than the title implies?
89
+ 5. Reviewer fatigue — is anything in the diff that a tired reviewer
90
+ would rubber-stamp but should not?
91
+ End with: APPROVE / REQUEST_CHANGES / REJECT, one sentence why, and
92
+ the single highest-leverage change the PR author should make before
93
+ merge.
94
+ """.strip()
95
+
96
+ DESIGN_MODE = """\
97
+ The artefact is a design document, ADR, or architecture proposal.
98
+ Critique it as if you were greenlighting it as a senior engineer.
99
+ Focus on:
100
+ 1. Trust boundaries and module coupling the design glosses over.
101
+ 2. Rollback / kill-switch criteria the design omits.
102
+ 3. Sequencing risk — does step N really not block step N+1?
103
+ 4. Open questions disguised as decisions, or decisions disguised as
104
+ open questions.
105
+ 5. The single architectural call you would push back on the hardest,
106
+ and what evidence would change your mind.
107
+ """.strip()
108
+
109
+ OPTIMIZE_MODE = """\
110
+ The artefact is an optimization target — code, a query, a profile,
111
+ or an existing optimization report. Produce ranked, evidence-based
112
+ suggestions for the metric stated in the user's original ask. You
113
+ MUST:
114
+ 1. Rank suggestions by expected impact on the stated metric, not by
115
+ effort or cleverness.
116
+ 2. Cite the evidence (line, query plan, profile entry) for every
117
+ suggestion. No hand-wave "this is probably slow".
118
+ 3. State at least one suggestion you explicitly REJECT as
119
+ low-leverage, so the user does not over-engineer.
120
+ 4. Mark at least one suggestion as hypothesis (requires measurement
121
+ before committing) versus confirmed (already supported by the
122
+ evidence in the artefact).
123
+ """.strip()
124
+
125
+
126
+ _MODE_TABLE = {
127
+ "prompt": PROMPT_MODE,
128
+ "roadmap": ROADMAP_MODE,
129
+ "diff": DIFF_MODE,
130
+ "files": FILES_MODE,
131
+ "pr": PR_MODE,
132
+ "design": DESIGN_MODE,
133
+ "optimize": OPTIMIZE_MODE,
134
+ }
135
+
136
+
137
+ def _strip_host_identity(text: str) -> str:
138
+ """Drop any *whole line* containing a host-agent identity substring.
139
+
140
+ Strategy (locked by council review, 2026-05-02): a line is dropped
141
+ in full as soon as any host-identity needle (Augment / Claude Code
142
+ / Cursor / Cline / Windsurf, etc.) appears anywhere on it. We err
143
+ toward false-positive — slightly less context — over false-negative
144
+ — a neutrality leak. Substring-only stripping was rejected because
145
+ it can leave dangling clauses that still hint at the host.
146
+ """
147
+ if not text:
148
+ return text
149
+ kept: list[str] = []
150
+ for line in text.splitlines():
151
+ low = line.lower()
152
+ if any(needle in low for needle in HOST_AGENT_IDENTITY_PATTERNS):
153
+ continue
154
+ kept.append(line)
155
+ return "\n".join(kept)
156
+
157
+
158
+ def handoff_preamble(
159
+ project: ProjectContext | None,
160
+ original_ask: str,
161
+ ) -> str:
162
+ """Neutral context-handoff for council members.
163
+
164
+ Layout (any block omitted when its inputs are empty):
165
+
166
+ Project: <name>
167
+ Stack: <stack>
168
+ Purpose: <repo_purpose>
169
+
170
+ The user originally asked:
171
+ > <original_ask>
172
+
173
+ <NEUTRALITY_PREAMBLE>
174
+
175
+ Iron Law of Neutrality (`ai-council` skill): lines containing a
176
+ host-agent identity string (Augment, Claude Code, Cursor, Cline,
177
+ Windsurf, Copilot agent) are dropped from `project` fields and
178
+ `original_ask` BEFORE assembly so they cannot leak.
179
+
180
+ `project=None` and/or `original_ask=""` collapses the output to
181
+ `NEUTRALITY_PREAMBLE` alone (back-compat with v1 callers).
182
+ """
183
+ blocks: list[str] = []
184
+
185
+ if project is not None and not project.is_empty():
186
+ ctx_lines: list[str] = []
187
+ if project.name:
188
+ ctx_lines.append(f"Project: {project.name}")
189
+ if project.stack:
190
+ ctx_lines.append(f"Stack: {project.stack}")
191
+ if project.repo_purpose:
192
+ ctx_lines.append(f"Purpose: {project.repo_purpose}")
193
+ ctx = _strip_host_identity("\n".join(ctx_lines)).strip()
194
+ if ctx:
195
+ blocks.append(ctx)
196
+
197
+ cleaned_ask = _strip_host_identity(original_ask or "").strip()
198
+ if cleaned_ask:
199
+ quoted = "\n".join(f"> {ln}" for ln in cleaned_ask.splitlines())
200
+ blocks.append(f"The user originally asked:\n{quoted}")
201
+
202
+ blocks.append(NEUTRALITY_PREAMBLE)
203
+ return "\n\n".join(blocks)
204
+
205
+
206
+ def system_prompt_for(
207
+ mode: str,
208
+ *,
209
+ project: ProjectContext | None = None,
210
+ original_ask: str = "",
211
+ ) -> str:
212
+ """Build the full system prompt for one of the four input modes.
213
+
214
+ Raises ValueError on an unknown mode — callers must use one of
215
+ `prompt`, `roadmap`, `diff`, `files`.
216
+
217
+ When `project` and `original_ask` are both omitted, the result is
218
+ `NEUTRALITY_PREAMBLE` + per-mode addendum (v1 shape, byte-identical
219
+ to pre-2a output). When either is supplied, the neutral handoff
220
+ preamble replaces the bare `NEUTRALITY_PREAMBLE`.
221
+ """
222
+ if mode not in _MODE_TABLE:
223
+ raise ValueError(
224
+ f"Unknown council mode {mode!r}. "
225
+ f"Expected one of: {sorted(_MODE_TABLE)}"
226
+ )
227
+ head = handoff_preamble(project, original_ask)
228
+ return f"{head}\n\n{_MODE_TABLE[mode]}"
229
+
230
+
231
+ def all_modes() -> list[str]:
232
+ return sorted(_MODE_TABLE)
@@ -0,0 +1,144 @@
1
+ """Session persistence for council consultations (D2).
2
+
3
+ Every `/council` call that completes (success or partial) writes an
4
+ audit artefact under `agents/council-sessions/<UTC-timestamp>/`:
5
+
6
+ - `manifest.json` — input mode, members, token + USD totals, original
7
+ ask, neutrality preamble fingerprint.
8
+ - `response.md` — `orchestrator.render()` output (per-member
9
+ sections + Convergence/Divergence slot).
10
+ - `raw-text.md` — concatenated raw text per member, separated by
11
+ ASCII rules so a later `grep` is trivial.
12
+
13
+ Hard rules:
14
+ - Never raises on the project — disk write failures are logged and
15
+ swallowed; the council is text-only and the report is the contract.
16
+ - Never writes secrets. The bundle has already been redacted by
17
+ `bundler.py` before the orchestrator receives it.
18
+ - Never writes outside `agents/council-sessions/`. Path traversal in
19
+ the timestamp is impossible (we generate it from `datetime.utcnow`).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import datetime as _dt
25
+ import json
26
+ import sys
27
+ from dataclasses import dataclass, field
28
+ from pathlib import Path
29
+ from typing import Iterable
30
+
31
+ from scripts.ai_council.clients import CouncilResponse
32
+ from scripts.ai_council.orchestrator import render
33
+
34
+ REPO_ROOT = Path(__file__).resolve().parents[2]
35
+ SESSIONS_DIR = REPO_ROOT / "agents" / "council-sessions"
36
+
37
+
38
+ @dataclass
39
+ class SessionManifest:
40
+ """Structured record of a single council call.
41
+
42
+ Round 2+ debate calls (D1) pass `rounds > 1`; each round's
43
+ per-member response is appended in `responses_per_round`.
44
+ """
45
+
46
+ mode: str # bundle mode: prompt|roadmap|diff|files
47
+ artefact: str # human-readable artefact descriptor (path or "<inline>")
48
+ original_ask: str
49
+ members: list[str] # "provider/model" pairs
50
+ rounds: int = 1
51
+ cost_usd_estimated: float = 0.0
52
+ cost_usd_actual: float = 0.0
53
+ extra: dict[str, object] = field(default_factory=dict)
54
+
55
+
56
+ def _utc_timestamp() -> str:
57
+ """UTC timestamp safe for filesystem use (Z suffix preserved)."""
58
+ return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H-%M-%SZ")
59
+
60
+
61
+ def _serialise_response(r: CouncilResponse) -> dict[str, object]:
62
+ return {
63
+ "provider": r.provider,
64
+ "model": r.model,
65
+ "input_tokens": r.input_tokens,
66
+ "output_tokens": r.output_tokens,
67
+ "latency_ms": r.latency_ms,
68
+ "error": r.error,
69
+ }
70
+
71
+
72
+ def save(
73
+ *,
74
+ manifest: SessionManifest,
75
+ responses: list[CouncilResponse] | Iterable[list[CouncilResponse]],
76
+ sessions_dir: Path | None = None,
77
+ timestamp: str | None = None,
78
+ ) -> Path:
79
+ """Persist a council call. Returns the session directory.
80
+
81
+ `responses` accepts either:
82
+ - `list[CouncilResponse]` — single round (round 1 only).
83
+ - `Iterable[list[CouncilResponse]]` — multi-round, one list per
84
+ round in execution order.
85
+
86
+ Disk-write failures are surfaced via a stderr line but do not
87
+ raise; the caller's text report is the source of truth.
88
+ """
89
+ rounds_data: list[list[CouncilResponse]]
90
+ if responses and isinstance(responses, list) and isinstance(responses[0], CouncilResponse):
91
+ rounds_data = [responses] # type: ignore[list-item]
92
+ else:
93
+ rounds_data = list(responses) # type: ignore[arg-type]
94
+
95
+ base = sessions_dir or SESSIONS_DIR
96
+ ts = timestamp or _utc_timestamp()
97
+ session_dir = base / ts
98
+
99
+ try:
100
+ session_dir.mkdir(parents=True, exist_ok=True)
101
+ except OSError as exc: # noqa: BLE001 - never block the report
102
+ print(f"[council:session] mkdir failed: {exc}", file=sys.stderr)
103
+ return session_dir
104
+
105
+ manifest_payload = {
106
+ "timestamp_utc": ts,
107
+ "mode": manifest.mode,
108
+ "artefact": manifest.artefact,
109
+ "original_ask": manifest.original_ask,
110
+ "members": manifest.members,
111
+ "rounds": manifest.rounds,
112
+ "cost_usd_estimated": round(manifest.cost_usd_estimated, 6),
113
+ "cost_usd_actual": round(manifest.cost_usd_actual, 6),
114
+ "responses_per_round": [
115
+ [_serialise_response(r) for r in round_responses]
116
+ for round_responses in rounds_data
117
+ ],
118
+ **manifest.extra,
119
+ }
120
+
121
+ try:
122
+ (session_dir / "manifest.json").write_text(
123
+ json.dumps(manifest_payload, indent=2) + "\n", encoding="utf-8",
124
+ )
125
+ # Render uses the LAST round (the moderator-facing summary).
126
+ last_round = rounds_data[-1] if rounds_data else []
127
+ (session_dir / "response.md").write_text(
128
+ render(last_round) + "\n", encoding="utf-8",
129
+ )
130
+ raw_blocks: list[str] = []
131
+ for round_idx, round_responses in enumerate(rounds_data, start=1):
132
+ for r in round_responses:
133
+ raw_blocks.append(
134
+ f"=== round {round_idx} · {r.provider}/{r.model} ===\n\n"
135
+ f"{r.text}\n",
136
+ )
137
+ (session_dir / "raw-text.md").write_text(
138
+ "\n".join(raw_blocks) + ("\n" if raw_blocks else ""),
139
+ encoding="utf-8",
140
+ )
141
+ except OSError as exc: # noqa: BLE001 - never block the report
142
+ print(f"[council:session] write failed: {exc}", file=sys.stderr)
143
+
144
+ return session_dir
@@ -10,7 +10,7 @@ Concatenates a curated set of cloud-safe rules from
10
10
  personal.md — empty stub for individual preferences
11
11
 
12
12
  Per-rule inclusion + mode is the source of truth in
13
- `agents/contexts/linear-ai-rules-inclusion.md`. This script encodes the
13
+ `docs/contracts/linear-ai-rules-inclusion.md`. This script encodes the
14
14
  same lists so a drift between the two surfaces is caught by the digest
15
15
  audit (Phase 3 Step 4) — the markdown doc is the human-readable spec,
16
16
  this script is the executable.
@@ -44,7 +44,7 @@ from pathlib import Path
44
44
  ROOT = Path(__file__).resolve().parent.parent
45
45
  # Compressed source is the shipped form — denser, sharper section
46
46
  # structure; better fit for a guidance field than the verbose authoring
47
- # layer. The inclusion list at agents/contexts/linear-ai-rules-inclusion.md
47
+ # layer. The inclusion list at docs/contracts/linear-ai-rules-inclusion.md
48
48
  # remains the human-readable spec.
49
49
  SOURCE = ROOT / ".agent-src" / "rules"
50
50
  OUT_DIR = ROOT / "dist" / "linear"
@@ -64,7 +64,7 @@ class RuleEntry:
64
64
 
65
65
 
66
66
  # Workspace digest — universal coding posture. Maps 1:1 to the
67
- # "Workspace digest" table in agents/contexts/linear-ai-rules-inclusion.md.
67
+ # "Workspace digest" table in docs/contracts/linear-ai-rules-inclusion.md.
68
68
  WORKSPACE: list[RuleEntry] = [
69
69
  RuleEntry("ask-when-uncertain"),
70
70
  RuleEntry("commit-conventions"),
@@ -169,7 +169,7 @@ def render_digest(layer: str, entries: list[RuleEntry]) -> tuple[str, dict]:
169
169
  parts.append(
170
170
  "> Auto-generated by `scripts/build_linear_digest.py` from "
171
171
  "`.agent-src/rules/` (compressed source) plus the inclusion list "
172
- "at `agents/contexts/linear-ai-rules-inclusion.md`. Do not edit "
172
+ "at `docs/contracts/linear-ai-rules-inclusion.md`. Do not edit "
173
173
  "this file by hand — re-run `task build-linear-digest` to "
174
174
  "regenerate.\n"
175
175
  )
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env python3
2
+ """Always-rule budget gate (Phases 7.1 + 7.4 of road-to-pr-34-followups).
3
+
4
+ Enforces a stricter budget contract than `tests/test_always_budget.py`:
5
+ the test suite fails only at 100% utilization (49,000 chars). This
6
+ script lives in CI as:
7
+
8
+ - Warn-at-80% / fail-at-90% global trend gate (Phase 7.1).
9
+ - Per-rule cap (≤ 6,000 chars per always-rule, Phase 7.4).
10
+ - Top-3 cap (top-3 combined ≤ 50% of TOTAL_CAP, Phase 7.4).
11
+
12
+ The same caps are enforced as hard assertions in
13
+ `tests/test_always_budget.py`; this script duplicates them so a
14
+ contributor sees a single, fast pre-test signal during local edits.
15
+
16
+ Exit codes: 0 = pass (or warn), 1 = fail (≥ 90% utilization,
17
+ per-rule breach, or top-3 breach), 3 = internal error.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ REPO_ROOT = Path(__file__).resolve().parents[1]
27
+ RULES_DIR = REPO_ROOT / ".agent-src" / "rules"
28
+
29
+ TOTAL_CAP = 49_000
30
+ WARN_THRESHOLD = 0.80 # 80% — emit warning, exit 0
31
+ FAIL_THRESHOLD = 0.90 # 90% — emit error, exit 1
32
+ PER_RULE_CAP = 6_000 # Phase 7.4 — no single always-rule may exceed this
33
+ TOP3_CAP = TOTAL_CAP // 2 # Phase 7.4 — top-3 combined ≤ 50% of total budget
34
+
35
+
36
+ def _always_rules() -> list[Path]:
37
+ rules: list[Path] = []
38
+ for path in sorted(RULES_DIR.glob("*.md")):
39
+ head = path.read_text(encoding="utf-8").splitlines()[1:2]
40
+ if head == ['type: "always"']:
41
+ rules.append(path)
42
+ return rules
43
+
44
+
45
+ def _summary(rules: list[Path]) -> tuple[int, list[tuple[str, int]]]:
46
+ sizes = [(p.name, p.stat().st_size) for p in rules]
47
+ return sum(s for _, s in sizes), sorted(sizes, key=lambda x: -x[1])
48
+
49
+
50
+ def main() -> int:
51
+ parser = argparse.ArgumentParser(description=__doc__)
52
+ parser.add_argument(
53
+ "--quiet",
54
+ action="store_true",
55
+ help="suppress the per-rule breakdown unless threshold is crossed",
56
+ )
57
+ args = parser.parse_args()
58
+
59
+ if not RULES_DIR.is_dir():
60
+ print(f"❌ rules dir missing: {RULES_DIR}", file=sys.stderr)
61
+ return 3
62
+
63
+ rules = _always_rules()
64
+ if not rules:
65
+ print(f"❌ no always-rules found under {RULES_DIR}", file=sys.stderr)
66
+ return 3
67
+
68
+ total, sizes = _summary(rules)
69
+ pct = total / TOTAL_CAP
70
+ over_per_rule = [(n, s) for n, s in sizes if s > PER_RULE_CAP]
71
+ top3 = sum(s for _, s in sizes[:3])
72
+ top3_breach = top3 > TOP3_CAP
73
+
74
+ if pct >= FAIL_THRESHOLD or over_per_rule or top3_breach:
75
+ status = "❌ FAIL"
76
+ rc = 1
77
+ elif pct >= WARN_THRESHOLD:
78
+ status = "⚠️ WARN"
79
+ rc = 0
80
+ else:
81
+ status = "✅ OK"
82
+ rc = 0
83
+
84
+ print(
85
+ f"{status} always-rule budget: {total:,} / {TOTAL_CAP:,} chars "
86
+ f"({pct * 100:.1f}%) across {len(rules)} rule(s)"
87
+ )
88
+ print(
89
+ f" thresholds: warn {WARN_THRESHOLD * 100:.0f}% · "
90
+ f"fail {FAIL_THRESHOLD * 100:.0f}% · "
91
+ f"per-rule ≤ {PER_RULE_CAP:,} · top-3 ≤ {TOP3_CAP:,}"
92
+ )
93
+
94
+ if rc != 0 or pct >= WARN_THRESHOLD or not args.quiet:
95
+ print()
96
+ print(f" breakdown (largest first; top-3 sum = {top3:,}):")
97
+ for i, (name, size) in enumerate(sizes):
98
+ mark = " ❌" if size > PER_RULE_CAP else ""
99
+ tag = " (top-3)" if i < 3 else ""
100
+ print(f" {size:>5} {name}{tag}{mark}")
101
+
102
+ if over_per_rule:
103
+ names = ", ".join(f"{n}={s:,}" for n, s in over_per_rule)
104
+ print(
105
+ f"\n Per-rule cap breach (> {PER_RULE_CAP:,} chars): {names}"
106
+ )
107
+
108
+ if top3_breach:
109
+ print(
110
+ f"\n Top-3 cap breach: {top3:,} > {TOP3_CAP:,} chars "
111
+ f"(top-3 must stay ≤ 50% of {TOTAL_CAP:,} total budget)."
112
+ )
113
+
114
+ if rc == 1:
115
+ print(
116
+ f"\n Action: trim the offending rule(s) via load_context: "
117
+ f"extraction (see contexts/execution + contexts/authority) "
118
+ f"until utilization drops below {FAIL_THRESHOLD * 100:.0f}% "
119
+ f"and all per-rule / top-3 caps hold."
120
+ )
121
+
122
+ return rc
123
+
124
+
125
+ if __name__ == "__main__":
126
+ sys.exit(main())