@event4u/agent-config 1.20.0 → 1.22.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 (268) hide show
  1. package/.agent-src/commands/agents.md +1 -1
  2. package/.agent-src/commands/bug-fix.md +2 -1
  3. package/.agent-src/commands/bug-investigate.md +3 -2
  4. package/.agent-src/commands/challenge-me/vision.md +348 -0
  5. package/.agent-src/commands/challenge-me/with-docs.md +333 -0
  6. package/.agent-src/commands/challenge-me.md +61 -0
  7. package/.agent-src/commands/chat-history/import.md +60 -64
  8. package/.agent-src/commands/compress.md +12 -0
  9. package/.agent-src/commands/context/create.md +2 -2
  10. package/.agent-src/commands/context.md +1 -1
  11. package/.agent-src/commands/copilot-agents.md +1 -1
  12. package/.agent-src/commands/council/default.md +69 -10
  13. package/.agent-src/commands/council.md +1 -1
  14. package/.agent-src/commands/create-pr.md +7 -3
  15. package/.agent-src/commands/e2e-heal.md +1 -1
  16. package/.agent-src/commands/e2e-plan.md +1 -1
  17. package/.agent-src/commands/feature/dev.md +3 -3
  18. package/.agent-src/commands/feature.md +1 -1
  19. package/.agent-src/commands/fix/seeder.md +2 -2
  20. package/.agent-src/commands/fix.md +1 -1
  21. package/.agent-src/commands/grill-me.md +38 -0
  22. package/.agent-src/commands/jira-ticket.md +1 -1
  23. package/.agent-src/commands/judge/steps.md +1 -1
  24. package/.agent-src/commands/judge.md +2 -2
  25. package/.agent-src/commands/memory.md +1 -1
  26. package/.agent-src/commands/mode.md +5 -5
  27. package/.agent-src/commands/module.md +1 -1
  28. package/.agent-src/commands/onboard.md +4 -4
  29. package/.agent-src/commands/optimize/augmentignore.md +1 -1
  30. package/.agent-src/commands/optimize-prompt.md +61 -0
  31. package/.agent-src/commands/optimize.md +1 -1
  32. package/.agent-src/commands/override.md +1 -1
  33. package/.agent-src/commands/review-changes.md +1 -1
  34. package/.agent-src/commands/review-routing.md +1 -1
  35. package/.agent-src/commands/roadmap/ai-council.md +183 -0
  36. package/.agent-src/commands/roadmap/create.md +6 -1
  37. package/.agent-src/commands/roadmap/process-full.md +58 -0
  38. package/.agent-src/commands/roadmap/process-phase.md +69 -0
  39. package/.agent-src/commands/roadmap/process-step.md +57 -0
  40. package/.agent-src/commands/roadmap.md +45 -17
  41. package/.agent-src/commands/set-cost-profile.md +3 -3
  42. package/.agent-src/commands/sync-agent-settings.md +2 -2
  43. package/.agent-src/commands/tests/create.md +2 -2
  44. package/.agent-src/commands/tests.md +1 -1
  45. package/.agent-src/commands/threat-model.md +5 -4
  46. package/.agent-src/contexts/augment-infrastructure.md +1 -1
  47. package/.agent-src/contexts/authority/commit-mechanics.md +14 -1
  48. package/.agent-src/contexts/authority/destructive-mechanics.md +14 -1
  49. package/.agent-src/contexts/authority/scope-mechanics.md +5 -0
  50. package/.agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +76 -0
  51. package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +54 -19
  52. package/.agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +98 -0
  53. package/.agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +93 -0
  54. package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +128 -5
  55. package/.agent-src/contexts/execution/autonomy-mechanics.md +44 -0
  56. package/.agent-src/contexts/execution/roadmap-process-loop.md +125 -0
  57. package/.agent-src/contexts/model-recommendations.md +2 -2
  58. package/.agent-src/contexts/override-system.md +1 -1
  59. package/.agent-src/contexts/skills-and-commands.md +1 -1
  60. package/.agent-src/personas/product-owner.md +2 -2
  61. package/.agent-src/personas/qa.md +1 -1
  62. package/.agent-src/rules/agent-authority.md +5 -6
  63. package/.agent-src/rules/agent-docs.md +11 -53
  64. package/.agent-src/rules/analysis-skill-routing.md +10 -40
  65. package/.agent-src/rules/architecture.md +6 -1
  66. package/.agent-src/rules/artifact-drafting-protocol.md +5 -0
  67. package/.agent-src/rules/artifact-engagement-recording.md +23 -59
  68. package/.agent-src/rules/ask-when-uncertain.md +24 -47
  69. package/.agent-src/rules/augment-portability.md +14 -62
  70. package/.agent-src/rules/augment-source-of-truth.md +10 -1
  71. package/.agent-src/rules/autonomous-execution.md +17 -98
  72. package/.agent-src/rules/capture-learnings.md +9 -80
  73. package/.agent-src/rules/cli-output-handling.md +12 -42
  74. package/.agent-src/rules/command-suggestion-policy.md +25 -73
  75. package/.agent-src/rules/commit-conventions.md +9 -58
  76. package/.agent-src/rules/commit-policy.md +16 -47
  77. package/.agent-src/rules/context-hygiene.md +5 -0
  78. package/.agent-src/rules/direct-answers.md +21 -50
  79. package/.agent-src/rules/docker-commands.md +11 -45
  80. package/.agent-src/rules/docs-sync.md +10 -56
  81. package/.agent-src/rules/downstream-changes.md +5 -0
  82. package/.agent-src/rules/e2e-testing.md +9 -44
  83. package/.agent-src/rules/guidelines.md +13 -75
  84. package/.agent-src/rules/improve-before-implement.md +11 -2
  85. package/.agent-src/rules/invite-challenge.md +71 -0
  86. package/.agent-src/rules/language-and-tone.md +41 -106
  87. package/.agent-src/rules/laravel-translations.md +11 -40
  88. package/.agent-src/rules/markdown-safe-codeblocks.md +4 -0
  89. package/.agent-src/rules/minimal-safe-diff.md +4 -0
  90. package/.agent-src/rules/missing-tool-handling.md +4 -0
  91. package/.agent-src/rules/model-recommendation.md +9 -61
  92. package/.agent-src/rules/no-attribution-footers.md +5 -0
  93. package/.agent-src/rules/no-cheap-questions.md +11 -27
  94. package/.agent-src/rules/no-council-references.md +76 -0
  95. package/.agent-src/rules/no-roadmap-references.md +7 -0
  96. package/.agent-src/rules/non-destructive-by-default.md +13 -43
  97. package/.agent-src/rules/onboarding-gate.md +9 -117
  98. package/.agent-src/rules/package-ci-checks.md +10 -37
  99. package/.agent-src/rules/php-coding.md +10 -55
  100. package/.agent-src/rules/preservation-guard.md +9 -0
  101. package/.agent-src/rules/review-routing-awareness.md +9 -97
  102. package/.agent-src/rules/reviewer-awareness.md +8 -83
  103. package/.agent-src/rules/roadmap-progress-sync.md +7 -170
  104. package/.agent-src/rules/role-mode-adherence.md +6 -2
  105. package/.agent-src/rules/rule-type-governance.md +8 -66
  106. package/.agent-src/rules/runtime-safety.md +5 -0
  107. package/.agent-src/rules/scope-control.md +17 -62
  108. package/.agent-src/rules/security-sensitive-stop.md +7 -1
  109. package/.agent-src/rules/size-enforcement.md +6 -1
  110. package/.agent-src/rules/skill-improvement-trigger.md +9 -49
  111. package/.agent-src/rules/skill-quality.md +7 -113
  112. package/.agent-src/rules/slash-command-routing-policy.md +11 -63
  113. package/.agent-src/rules/think-before-action.md +22 -87
  114. package/.agent-src/rules/token-efficiency.md +10 -74
  115. package/.agent-src/rules/token-optimizer-maintenance.md +68 -0
  116. package/.agent-src/rules/tool-safety.md +4 -0
  117. package/.agent-src/rules/ui-audit-gate.md +25 -61
  118. package/.agent-src/rules/upstream-proposal.md +9 -67
  119. package/.agent-src/rules/user-interaction.md +22 -108
  120. package/.agent-src/rules/verify-before-complete.md +1 -1
  121. package/.agent-src/skills/adversarial-review/SKILL.md +1 -0
  122. package/.agent-src/skills/agent-docs-writing/SKILL.md +1 -1
  123. package/.agent-src/skills/ai-council/SKILL.md +197 -8
  124. package/.agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  125. package/.agent-src/skills/analysis-skill-router/SKILL.md +3 -3
  126. package/.agent-src/skills/artisan-commands/SKILL.md +2 -2
  127. package/.agent-src/skills/authz-review/SKILL.md +1 -1
  128. package/.agent-src/skills/aws-infrastructure/SKILL.md +5 -5
  129. package/.agent-src/skills/blast-radius-analyzer/SKILL.md +8 -8
  130. package/.agent-src/skills/bug-analyzer/SKILL.md +6 -5
  131. package/.agent-src/skills/code-refactoring/SKILL.md +4 -4
  132. package/.agent-src/skills/code-review/SKILL.md +2 -2
  133. package/.agent-src/skills/command-writing/SKILL.md +11 -0
  134. package/.agent-src/skills/composer-packages/SKILL.md +2 -2
  135. package/.agent-src/skills/context-authoring/SKILL.md +11 -0
  136. package/.agent-src/skills/context-document/SKILL.md +1 -1
  137. package/.agent-src/skills/copilot-agents-optimization/SKILL.md +23 -0
  138. package/.agent-src/skills/copilot-config/SKILL.md +1 -1
  139. package/.agent-src/skills/dependency-upgrade/SKILL.md +2 -2
  140. package/.agent-src/skills/devcontainer/SKILL.md +2 -2
  141. package/.agent-src/skills/developer-like-execution/SKILL.md +1 -1
  142. package/.agent-src/skills/docker/SKILL.md +1 -1
  143. package/.agent-src/skills/dto-creator/SKILL.md +1 -1
  144. package/.agent-src/skills/estimate-ticket/SKILL.md +2 -2
  145. package/.agent-src/skills/fe-design/SKILL.md +4 -4
  146. package/.agent-src/skills/feature-planning/SKILL.md +5 -5
  147. package/.agent-src/skills/funnel-analysis/SKILL.md +1 -1
  148. package/.agent-src/skills/laravel/SKILL.md +1 -1
  149. package/.agent-src/skills/laravel-notifications/SKILL.md +5 -5
  150. package/.agent-src/skills/laravel-pennant/SKILL.md +1 -1
  151. package/.agent-src/skills/laravel-pulse/SKILL.md +4 -4
  152. package/.agent-src/skills/laravel-reverb/SKILL.md +2 -2
  153. package/.agent-src/skills/laravel-scheduling/SKILL.md +1 -1
  154. package/.agent-src/skills/migration-creator/SKILL.md +7 -7
  155. package/.agent-src/skills/multi-tenancy/SKILL.md +8 -8
  156. package/.agent-src/skills/performance-analysis/SKILL.md +3 -3
  157. package/.agent-src/skills/pest-testing/SKILL.md +6 -6
  158. package/.agent-src/skills/php-service/SKILL.md +2 -2
  159. package/.agent-src/skills/project-analysis-hypothesis-driven/SKILL.md +3 -3
  160. package/.agent-src/skills/project-analysis-react/SKILL.md +1 -1
  161. package/.agent-src/skills/project-analysis-symfony/SKILL.md +1 -1
  162. package/.agent-src/skills/project-analysis-zend-laminas/SKILL.md +2 -2
  163. package/.agent-src/skills/project-analyzer/SKILL.md +4 -4
  164. package/.agent-src/skills/prompt-optimizer/SKILL.md +108 -0
  165. package/.agent-src/skills/readme-reviewer/SKILL.md +1 -1
  166. package/.agent-src/skills/roadmap-management/SKILL.md +7 -7
  167. package/.agent-src/skills/rule-writing/SKILL.md +33 -0
  168. package/.agent-src/skills/sentry-integration/SKILL.md +1 -1
  169. package/.agent-src/skills/skill-writing/SKILL.md +14 -0
  170. package/.agent-src/skills/systematic-debugging/SKILL.md +22 -2
  171. package/.agent-src/skills/technical-specification/SKILL.md +58 -1
  172. package/.agent-src/skills/terraform/SKILL.md +2 -2
  173. package/.agent-src/skills/terragrunt/SKILL.md +8 -8
  174. package/.agent-src/skills/test-performance/SKILL.md +5 -5
  175. package/.agent-src/skills/threat-modeling/SKILL.md +3 -2
  176. package/.agent-src/skills/token-optimizer/SKILL.md +110 -0
  177. package/.agent-src/skills/universal-project-analysis/SKILL.md +1 -1
  178. package/.agent-src/templates/AGENTS.md +1 -1
  179. package/.agent-src/templates/agent-settings.md +35 -19
  180. package/.agent-src/templates/command.md +17 -1
  181. package/.agent-src/templates/contexts/tenant-boundaries.md +2 -2
  182. package/.agent-src/templates/contexts.md +1 -1
  183. package/.agent-src/templates/copilot-instructions.md +21 -0
  184. package/.agent-src/templates/copilot-review-instructions.md +76 -0
  185. package/.agent-src/templates/features.md +1 -1
  186. package/.agent-src/templates/roadmaps.md +10 -2
  187. package/.agent-src/templates/rule.md +129 -0
  188. package/.agent-src/templates/skill.md +17 -0
  189. package/.claude-plugin/marketplace.json +12 -2
  190. package/AGENTS.md +32 -5
  191. package/CHANGELOG.md +107 -3
  192. package/README.md +22 -21
  193. package/config/agent-settings.template.yml +66 -10
  194. package/config/gitignore-block.txt +7 -0
  195. package/docs/architecture.md +86 -5
  196. package/docs/catalog.md +16 -6
  197. package/docs/contracts/agent-memory-contract.md +1 -1
  198. package/docs/contracts/command-clusters.md +45 -1
  199. package/docs/contracts/context-paths.md +2 -1
  200. package/docs/contracts/file-ownership-matrix.json +354 -500
  201. package/docs/contracts/iron-law-overrides.txt +25 -0
  202. package/docs/contracts/kernel-membership.md +273 -0
  203. package/docs/contracts/load-context-schema.md +26 -11
  204. package/docs/contracts/pilot/agent-authority.md +24 -0
  205. package/docs/contracts/pilot/direct-answers.md +70 -0
  206. package/docs/contracts/pilot/language-and-tone.md +63 -0
  207. package/docs/contracts/rule-classification.md +170 -0
  208. package/docs/contracts/rule-router.md +153 -0
  209. package/docs/customization.md +17 -6
  210. package/docs/decisions/ADR-001-kernel-swap-deferred.md +109 -0
  211. package/docs/decisions/ADR-002-kernel-bucket-overrides.md +124 -0
  212. package/docs/decisions/ADR-003-flat-cluster-subs-and-colon-syntax.md +126 -0
  213. package/docs/decisions/ADR-rule-kernel-and-router.md +122 -0
  214. package/docs/getting-started.md +2 -2
  215. package/docs/guidelines/agent-infra/naming.md +1 -1
  216. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +176 -0
  217. package/docs/guidelines/agent-infra/rule-type-governance.md +73 -0
  218. package/docs/guidelines/agent-infra/size-and-scope.md +13 -2
  219. package/docs/guidelines/agent-infra/skill-quality-checklist.md +119 -0
  220. package/docs/guidelines/augment-portability-patterns.md +68 -0
  221. package/docs/guidelines/php/php-coding-patterns.md +62 -0
  222. package/package.json +1 -1
  223. package/scripts/_p43_bodies.py +235 -0
  224. package/scripts/_p43_compress.py +118 -0
  225. package/scripts/_p4_migrate.py +199 -0
  226. package/scripts/_phase2_shim_helper.py +1 -1
  227. package/scripts/_pilot_council_question.py +57 -0
  228. package/scripts/_pilot_measure.py +53 -0
  229. package/scripts/ai_council/session.py +107 -5
  230. package/scripts/build_linear_digest.py +3 -5
  231. package/scripts/check_always_budget.py +39 -6
  232. package/scripts/check_compressed_paths.py +213 -0
  233. package/scripts/check_compression.py +15 -0
  234. package/scripts/check_context_paths.py +1 -0
  235. package/scripts/check_council_layout.py +105 -0
  236. package/scripts/check_council_references.py +145 -0
  237. package/scripts/check_portability.py +2 -0
  238. package/scripts/check_references.py +2 -0
  239. package/scripts/check_token_optimizer_freshness.py +131 -0
  240. package/scripts/compile_router.py +148 -0
  241. package/scripts/compress.py +219 -11
  242. package/scripts/council_cli.py +132 -11
  243. package/scripts/council_prune.py +81 -0
  244. package/scripts/count_token_optimizer_usage.sh +54 -0
  245. package/scripts/install.sh +44 -2
  246. package/scripts/iron_law_sha.py +98 -0
  247. package/scripts/lint_load_context.py +35 -5
  248. package/scripts/measure_rule_budget.py +314 -0
  249. package/scripts/migrate_command_suggestions.py +2 -2
  250. package/scripts/prototype_lint_contradictions.py +150 -0
  251. package/scripts/schemas/command.schema.json +5 -0
  252. package/scripts/schemas/rule.schema.json +60 -6
  253. package/scripts/schemas/skill.schema.json +5 -0
  254. package/scripts/skill_linter.py +197 -7
  255. package/scripts/smoke_path_resolution.py +93 -0
  256. package/scripts/validate_frontmatter.py +41 -1
  257. package/.agent-src/commands/roadmap/execute.md +0 -109
  258. package/.agent-src/contexts/communication/rules-auto/artifact-engagement-recording-mechanics.md +0 -72
  259. package/.agent-src/contexts/communication/rules-auto/augment-portability-mechanics.md +0 -79
  260. package/.agent-src/contexts/communication/rules-auto/cli-output-handling-mechanics.md +0 -87
  261. package/.agent-src/contexts/communication/rules-auto/command-suggestion-policy-mechanics.md +0 -62
  262. package/.agent-src/contexts/communication/rules-auto/docs-sync-mechanics.md +0 -78
  263. package/.agent-src/contexts/communication/rules-auto/package-ci-checks-mechanics.md +0 -85
  264. package/.agent-src/contexts/communication/rules-auto/review-routing-awareness-mechanics.md +0 -65
  265. package/.agent-src/contexts/communication/rules-auto/roadmap-progress-sync-mechanics.md +0 -78
  266. package/.agent-src/contexts/communication/rules-auto/ui-audit-gate-mechanics.md +0 -53
  267. /package/{docs → .agent-src/contexts}/contracts/artifact-engagement-flow.md +0 -0
  268. /package/{docs → .agent-src/contexts}/contracts/command-suggestion-flow.md +0 -0
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env python3
2
+ """P4.1 + P4.2 migration: rule → skill / guideline / command / contract stub.
3
+
4
+ Replaces 25 rules with thin stubs declaring `triggers:` + `routes_to:`.
5
+ For move-to-guideline, copies the rule body into a new guideline file.
6
+ For move-to-skill / command / contract, the target already carries the
7
+ procedure — stub keeps an Iron-Law one-liner only.
8
+ """
9
+ from __future__ import annotations
10
+ import re, sys, pathlib
11
+
12
+ ROOT = pathlib.Path(__file__).resolve().parents[1]
13
+ RULES = ROOT / ".agent-src.uncompressed" / "rules"
14
+ GUIDELINES = ROOT / "docs" / "guidelines"
15
+
16
+ # (rule_id, route, triggers, iron_law_one_liner)
17
+ # route format: "kind:id"
18
+ SKILL_MIGRATIONS = [
19
+ ("agent-docs", "skill:agent-docs-writing",
20
+ [("path_prefix", "agents/"), ("path_prefix", ".github/copilot-instructions"), ("keyword", "AGENTS.md"), ("keyword", "roadmap")],
21
+ "Read agent docs (`AGENTS.md`, `agents/`, module `agents/`) before work; update them after structural changes."),
22
+ ("analysis-skill-routing", "skill:analysis-skill-router",
23
+ [("keyword", "analyze"), ("keyword", "analysis"), ("phrase", "dig into the codebase")],
24
+ "Route analysis tasks to the narrowest matching `project-analysis-*` skill, not the broad fallback."),
25
+ ("capture-learnings", "skill:learning-to-rule-or-skill",
26
+ [("phrase", "after completing a task"), ("keyword", "learning"), ("keyword", "lesson")],
27
+ "After a task, capture repeated mistakes / successful patterns as a rule or skill — never lose the learning."),
28
+ ("cli-output-handling", "skill:rtk-output-filtering",
29
+ [("keyword", "git"), ("keyword", "phpstan"), ("keyword", "rector"), ("keyword", "phpunit"), ("keyword", "composer")],
30
+ "Wrap verbose CLI output with `rtk` when installed; fall back to `tail`/`grep` only when missing."),
31
+ ("commit-conventions", "skill:conventional-commits-writing",
32
+ [("keyword", "commit"), ("keyword", "branch"), ("phrase", "conventional commits")],
33
+ "Use Conventional Commits (`feat:`, `fix:`, `chore:` …); branches `<type>/<short-slug>`; never invent your own format."),
34
+ ("docker-commands", "skill:docker",
35
+ [("keyword", "docker"), ("keyword", "artisan"), ("keyword", "composer"), ("phrase", "inside the container")],
36
+ "Run PHP / artisan / composer / phpstan / rector / ecs / phpunit inside the project container, never on the host."),
37
+ ("docs-sync", "skill:agent-docs-writing",
38
+ [("path_prefix", ".agent-src.uncompressed/"), ("path_prefix", ".augment/"), ("keyword", "rename"), ("keyword", "delete")],
39
+ "On any add / rename / delete of skill / rule / command / guideline, update counts and cross-references in the same edit."),
40
+ ("e2e-testing", "command:e2e-heal",
41
+ [("keyword", "playwright"), ("keyword", "e2e"), ("phrase", "page object")],
42
+ "Playwright E2E: stable locators, no `waitForTimeout`, Page Objects for shared flows, fixtures over `beforeEach`."),
43
+ ("laravel-translations", "skill:laravel",
44
+ [("path_prefix", "lang/"), ("keyword", "translation"), ("keyword", "__()"), ("keyword", "trans(")],
45
+ "Use `__()`/`trans()` with language keys for every user-visible string; mirror keys across `lang/<locale>/` files."),
46
+ ("model-recommendation", "command:set-cost-profile",
47
+ [("phrase", "switch task"), ("phrase", "new task"), ("phrase", "which model")],
48
+ "On task / model switch, recommend the optimal model for the task complexity before any work begins."),
49
+ ("onboarding-gate", "command:onboard",
50
+ [("phrase", "first turn"), ("keyword", "onboarding"), ("path_prefix", ".agent-settings.yml")],
51
+ "First turn of a project: if `onboarding.onboarded` is false, prompt `/onboard` before executing any other request."),
52
+ ("package-ci-checks", "skill:lint-skills",
53
+ [("phrase", "task ci"), ("phrase", "before push"), ("phrase", "before pr")],
54
+ "Run `task ci` locally and confirm green before pushing or opening a PR in this package."),
55
+ ("review-routing-awareness", "skill:review-routing",
56
+ [("keyword", "reviewer"), ("phrase", "risk hotspot"), ("phrase", "ownership map")],
57
+ "Consult ownership-map and historical-bug-patterns before suggesting reviewers or claiming a change is safe."),
58
+ ("reviewer-awareness", "skill:review-routing",
59
+ [("keyword", "reviewer"), ("phrase", "suggest reviewers")],
60
+ "Anchor reviewer choice in paths and risk, never seniority; medium / high risk requires primary + secondary role."),
61
+ ("skill-improvement-trigger", "skill:skill-improvement-pipeline",
62
+ [("phrase", "after completing"), ("keyword", "improvement"), ("keyword", "pipeline")],
63
+ "After a meaningful task, trigger the post-task learning capture if `pipelines.skill_improvement` is enabled."),
64
+ ("slash-command-routing-policy", "skill:command-routing",
65
+ [("keyword", "/create-pr"), ("keyword", "/commit"), ("keyword", "/fix-ci"), ("phrase", "slash command")],
66
+ "On a slash-command invocation or pasted command body, route to the matching command file; never improvise."),
67
+ ("ui-audit-gate", "skill:existing-ui-audit",
68
+ [("path_prefix", "resources/views/"), ("path_prefix", "resources/js/"), ("keyword", "component"), ("keyword", "design token")],
69
+ "Before any non-trivial UI change, require `state.ui_audit` findings — gate, not suggestion."),
70
+ ("upstream-proposal", "skill:upstream-contribute",
71
+ [("phrase", "after creating"), ("phrase", "after improving"), ("keyword", "upstream")],
72
+ "After creating or significantly improving a skill / rule / guideline / command, ask whether to upstream it."),
73
+ ]
74
+
75
+ GUIDELINE_MIGRATIONS = [
76
+ ("artifact-engagement-recording", "contract:artifact-engagement-flow",
77
+ [("phrase", "/implement-ticket"), ("phrase", "/work"), ("keyword", "telemetry")],
78
+ "After a `/implement-ticket` or `/work` phase-step, emit one `telemetry:record` call with consulted + applied ids when telemetry is enabled."),
79
+ ("augment-portability", "guideline:augment-portability-patterns",
80
+ [("path_prefix", ".augment/"), ("path_prefix", ".agent-src.uncompressed/"), ("keyword", "portable")],
81
+ "Files inside `.augment/` and `.agent-src.uncompressed/` MUST stay project-agnostic — no project names, domains, stacks."),
82
+ ("command-suggestion-policy", "contract:command-suggestion-flow",
83
+ [("phrase", "free-form prompt"), ("phrase", "command suggestion")],
84
+ "When a free-form prompt matches a command, surface matches as numbered options with as-is escape; never auto-execute."),
85
+ ("php-coding", "guideline:php/php-coding-patterns",
86
+ [("file_pattern", "*.php"), ("keyword", "phpstan"), ("keyword", "ecs")],
87
+ "PHP: strict types, named comparisons, early returns, Eloquent conventions — full pattern library in the guideline."),
88
+ ("roadmap-progress-sync", "guideline:agent-infra/roadmap-progress-mechanics",
89
+ [("path_prefix", "agents/roadmaps/")],
90
+ "Any touch to `agents/roadmaps/` regenerates the dashboard in the same response; archive the roadmap when 0 open items remain."),
91
+ ("rule-type-governance", "guideline:agent-infra/rule-type-governance",
92
+ [("path_prefix", ".agent-src.uncompressed/rules/")],
93
+ "Choose `always` vs `auto` per the governance table; over-broad `always` rules degrade the kernel budget."),
94
+ ("skill-quality", "guideline:agent-infra/skill-quality-checklist",
95
+ [("path_prefix", ".agent-src.uncompressed/skills/")],
96
+ "Every skill must be executable, validated, and self-contained — full checklist in the guideline."),
97
+ ]
98
+
99
+
100
+
101
+ def yaml_triggers(triggers: list[tuple[str, str]]) -> str:
102
+ lines = []
103
+ for kind, val in triggers:
104
+ v = val.replace('"', '\\"')
105
+ lines.append(f' - {kind}: "{v}"')
106
+ return "\n".join(lines)
107
+
108
+
109
+ def parse_existing_frontmatter(text: str) -> tuple[dict, str]:
110
+ m = re.match(r"^---\n(.*?)\n---\n(.*)$", text, re.DOTALL)
111
+ if not m:
112
+ return {}, text
113
+ fm_text, body = m.group(1), m.group(2)
114
+ fm = {}
115
+ for line in fm_text.split("\n"):
116
+ if ":" not in line or line.startswith(" "):
117
+ continue
118
+ k, _, v = line.partition(":")
119
+ fm[k.strip()] = v.strip().strip('"')
120
+ return fm, body
121
+
122
+
123
+ def build_stub(rule_id: str, route: str, triggers: list[tuple[str, str]],
124
+ iron_law: str, existing_fm: dict) -> str:
125
+ desc = existing_fm.get("description", "")
126
+ tier = existing_fm.get("tier", "3")
127
+ rtype = existing_fm.get("type", "auto")
128
+ source = existing_fm.get("source", "package")
129
+ new_tier = {"1": "tier-1", "2a": "tier-2", "2b": "tier-2", "3": "tier-2",
130
+ "mechanical-already": "tier-2", "mech": "tier-2"}.get(tier, "tier-2")
131
+ fm_lines = [
132
+ "---",
133
+ f'type: "{rtype}"',
134
+ f'tier: "{new_tier}"',
135
+ f'description: "{desc}"',
136
+ f'source: {source}',
137
+ "triggers:",
138
+ yaml_triggers(triggers),
139
+ "routes_to:",
140
+ f' - "{route}"',
141
+ "---",
142
+ "",
143
+ f"# {rule_id.replace('-', ' ').title()}",
144
+ "",
145
+ f"**Iron Law.** {iron_law}",
146
+ "",
147
+ f"Body migrated to `{route}` (per P4 of `road-to-kernel-and-router.md`).",
148
+ "Trigger-set above activates this routing under the `balanced` and `full` profiles.",
149
+ ]
150
+ return "\n".join(fm_lines) + "\n"
151
+
152
+
153
+ def write_guideline(rule_id: str, route: str, body: str, existing_fm: dict) -> pathlib.Path:
154
+ kind, _, gid = route.partition(":")
155
+ if kind == "guideline":
156
+ target = GUIDELINES / f"{gid}.md"
157
+ elif kind == "contract":
158
+ target = ROOT / "docs" / "contracts" / f"{gid}.md"
159
+ else:
160
+ raise ValueError(f"unsupported route kind for guideline migration: {kind}")
161
+ if target.exists():
162
+ return target
163
+ target.parent.mkdir(parents=True, exist_ok=True)
164
+ desc = existing_fm.get("description", "")
165
+ header = (f"# {rule_id.replace('-', ' ').title()}\n\n"
166
+ f"> {desc}\n\n"
167
+ f"_Origin: migrated from `.agent-src.uncompressed/rules/{rule_id}.md` "
168
+ f"per P4.2 of `road-to-kernel-and-router.md`._\n\n")
169
+ target.write_text(header + body.lstrip())
170
+ return target
171
+
172
+
173
+ def main() -> int:
174
+ written = []
175
+ for rule_id, route, triggers, iron_law in SKILL_MIGRATIONS:
176
+ rule_path = RULES / f"{rule_id}.md"
177
+ if not rule_path.exists():
178
+ print(f" ✗ MISSING rule: {rule_id}"); continue
179
+ fm, body = parse_existing_frontmatter(rule_path.read_text())
180
+ stub = build_stub(rule_id, route, triggers, iron_law, fm)
181
+ rule_path.write_text(stub)
182
+ written.append(("skill-stub", rule_id, route, len(stub)))
183
+ for rule_id, route, triggers, iron_law in GUIDELINE_MIGRATIONS:
184
+ rule_path = RULES / f"{rule_id}.md"
185
+ if not rule_path.exists():
186
+ print(f" ✗ MISSING rule: {rule_id}"); continue
187
+ fm, body = parse_existing_frontmatter(rule_path.read_text())
188
+ write_guideline(rule_id, route, body, fm)
189
+ stub = build_stub(rule_id, route, triggers, iron_law, fm)
190
+ rule_path.write_text(stub)
191
+ written.append(("guideline", rule_id, route, len(stub)))
192
+ print(f"\nMigrated {len(written)} rules:")
193
+ for kind, rid, route, sz in written:
194
+ print(f" {kind:14s} {rid:35s} → {route:55s} stub={sz}c")
195
+ return 0
196
+
197
+
198
+ if __name__ == "__main__":
199
+ sys.exit(main())
@@ -25,7 +25,7 @@ PHASE2_SHIMS: list[tuple[str, str]] = [
25
25
  ("propose-memory", "memory propose"),
26
26
  # roadmap cluster
27
27
  ("roadmap-create", "roadmap create"),
28
- ("roadmap-execute", "roadmap execute"),
28
+ ("roadmap-execute", "roadmap process-phase"),
29
29
  # module cluster
30
30
  ("module-create", "module create"),
31
31
  ("module-explore", "module explore"),
@@ -0,0 +1,57 @@
1
+ """Build the Phase-1 council question file (one-off)."""
2
+ from pathlib import Path
3
+
4
+ OUT = Path("/tmp/council-p1/question.md")
5
+ OUT.parent.mkdir(parents=True, exist_ok=True)
6
+
7
+ framing = """# Cross-check request — Phase 1 of "Road to Rule Kernel and Router"
8
+
9
+ We are about to compress a 56-rule "always-active" set down to a small
10
+ hard-capped kernel + a router-loaded auto bucket. Phase 1 is purely
11
+ classification + projection — no source rules edited yet. Two contracts
12
+ were produced. We want a neutral second opinion BEFORE Phase 2 ships
13
+ compression to the actual rule files.
14
+
15
+ ## What we want from you
16
+
17
+ 1. **Disposition calls.** Look at the 56-rule classification. Are any
18
+ `keep-in-kernel` rules over-included (could safely become auto-tier)?
19
+ Are any `move-to-skill` / `move-to-guideline` calls wrong (e.g.
20
+ carrying an Iron Law that should stay always-on)? Cite the rule id
21
+ and the better disposition.
22
+ 2. **Kernel inclusion criteria.** Section 1 of `kernel-membership.md`
23
+ lists 4 criteria (Iron-Law floor / mode-independent / pre-action
24
+ gate / cross-cutting). Are these the right gates? Is anything
25
+ missing or redundant?
26
+ 3. **Compression-rate r = 0.742.** Three pilots gave r-values 0.677 /
27
+ 0.712 / 0.838. We locked the **mean**. Reasonable, or should we use
28
+ median (0.712) / max (0.838) as a more conservative budget cushion?
29
+ Note: pilot range straddles the typical 0.6-0.75 band; max came
30
+ from the shortest rule (already-lean floor effect).
31
+ 4. **The 1.5k per-rule cap with ADR-override.** Two of the three pilots
32
+ land >1.5k post-compression and would need an `iron-law-override`
33
+ ADR. Is the per-rule cap realistic, or should we raise the per-rule
34
+ ceiling (e.g. 2.5k or 3.5k) and lower the bucket cap (e.g. from 25k
35
+ to 20k)? What's the failure mode you'd be most worried about?
36
+ 5. **Anything we missed.** Risks, blind spots, sequencing problems.
37
+
38
+ Be specific. Cite the rule id or the section number. We will reject
39
+ generic "looks good" reviews.
40
+
41
+ ---
42
+
43
+ ## Contract 1 — docs/contracts/rule-classification.md
44
+
45
+ """
46
+
47
+ OUT.write_text(framing)
48
+ with OUT.open("a") as fh:
49
+ fh.write(Path("docs/contracts/rule-classification.md").read_text())
50
+ fh.write("\n\n---\n\n## Contract 2 — docs/contracts/kernel-membership.md\n\n")
51
+ fh.write(Path("docs/contracts/kernel-membership.md").read_text())
52
+ fh.write("\n\n---\n\n## Pilot inputs (compressed bodies, for reference)\n")
53
+ for rid in ("agent-authority", "direct-answers", "language-and-tone"):
54
+ fh.write(f"\n### Pilot — {rid}\n\n")
55
+ fh.write(Path(f"docs/contracts/pilot/{rid}.md").read_text())
56
+
57
+ print(f"wrote {OUT} — {OUT.stat().st_size} bytes")
@@ -0,0 +1,53 @@
1
+ """Pilot compression ratio + Iron-Law checksum verification (one-off, not CI)."""
2
+ import sys, re, hashlib, statistics
3
+ from pathlib import Path
4
+
5
+ sys.path.insert(0, "scripts")
6
+ from measure_rule_budget import strip_frontmatter
7
+
8
+ FENCE_RE = re.compile(r"```(?:[^\n]*\n)([\s\S]*?)```")
9
+
10
+
11
+ def iron_law_sha(body: str) -> str:
12
+ blocks = FENCE_RE.findall(body)
13
+ norm = "".join(re.sub(r"\s+", " ", b).strip().upper() for b in blocks)
14
+ return hashlib.sha256(norm.encode()).hexdigest()[:16]
15
+
16
+
17
+ pairs = [
18
+ ("agent-authority", ".agent-src.uncompressed/rules/agent-authority.md", "docs/contracts/pilot/agent-authority.md"),
19
+ ("direct-answers", ".agent-src.uncompressed/rules/direct-answers.md", "docs/contracts/pilot/direct-answers.md"),
20
+ ("language-and-tone", ".agent-src.uncompressed/rules/language-and-tone.md", "docs/contracts/pilot/language-and-tone.md"),
21
+ ]
22
+
23
+ header = f"{'rule':25s} {'orig':>6s} {'pilot':>6s} {'r':>6s} {'budget':>7s} {'sha-orig':>16s} {'sha-pilot':>16s} {'IL':>3s}"
24
+ print(header)
25
+ print("-" * len(header))
26
+
27
+ ratios = []
28
+ for rid, orig_path, pilot_path in pairs:
29
+ orig_body, _ = strip_frontmatter(Path(orig_path).read_text())
30
+ pilot_body, _ = strip_frontmatter(Path(pilot_path).read_text())
31
+ o, p = len(orig_body), len(pilot_body)
32
+ r = p / o
33
+ ratios.append(r)
34
+ sha_o = iron_law_sha(orig_body)
35
+ sha_p = iron_law_sha(pilot_body)
36
+ match = "OK" if sha_o == sha_p else "FAIL"
37
+ budget = "OK" if p <= 1500 else f"+{p - 1500}"
38
+ print(f"{rid:25s} {o:6d} {p:6d} {r:6.3f} {budget:>7s} {sha_o:>16s} {sha_p:>16s} {match:>3s}")
39
+
40
+ mean = sum(ratios) / len(ratios)
41
+ median = statistics.median(ratios)
42
+ print()
43
+ print(f"r-values : {[round(x, 3) for x in ratios]}")
44
+ print(f"mean r = {mean:.3f}")
45
+ print(f"median r = {median:.3f}")
46
+ print(f"max r = {max(ratios):.3f}")
47
+ print(f"min r = {min(ratios):.3f}")
48
+ print()
49
+
50
+ TOTAL = 32403
51
+ print(f"Projected always-bucket @ r=mean ({mean:.3f}) : {int(TOTAL * mean):>6d} (target ≤ 25000)")
52
+ print(f"Projected always-bucket @ r=max ({max(ratios):.3f}) : {int(TOTAL * max(ratios)):>6d} (target ≤ 25000)")
53
+ print(f"Projected always-bucket @ r=med ({median:.3f}) : {int(TOTAL * median):>6d} (target ≤ 25000)")
@@ -35,9 +35,15 @@ from scripts.ai_council.orchestrator import render
35
35
 
36
36
  REPO_ROOT = Path(__file__).resolve().parents[2]
37
37
  SESSIONS_DIR = REPO_ROOT / "agents" / "council-sessions"
38
+ QUESTIONS_DIR = REPO_ROOT / "agents" / "council-questions"
39
+ RESPONSES_DIR = REPO_ROOT / "agents" / "council-responses"
38
40
  SETTINGS_FILE = REPO_ROOT / ".agent-settings.yml"
39
41
 
40
- DEFAULT_RETENTION_DAYS = 14
42
+ # Default retention for all council artefacts (questions, responses,
43
+ # sessions). Overridden by `ai_council.session_retention_days`
44
+ # in `.agent-settings.yml`. Council files are local-only scratch — short
45
+ # retention keeps the working tree from accumulating dead weight.
46
+ DEFAULT_RETENTION_DAYS = 7
41
47
  _TS_RE = re.compile(r"^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})Z$")
42
48
 
43
49
 
@@ -152,6 +158,92 @@ def prune_old_sessions(
152
158
  return removed
153
159
 
154
160
 
161
+ def prune_old_artifacts(
162
+ artifact_dir: Path,
163
+ retention_days: int,
164
+ *,
165
+ now: _dt.datetime | None = None,
166
+ ) -> list[Path]:
167
+ """Delete files and timestamp-less directories older than `retention_days`.
168
+
169
+ mtime-based — used for `agents/council-questions/`,
170
+ `agents/council-responses/`, and root-level files in
171
+ `agents/council-sessions/` that don't match the
172
+ timestamp-subdir convention handled by `prune_old_sessions`.
173
+
174
+ Walks the directory non-recursively. For files: deletes when
175
+ mtime predates the cutoff. For sub-directories without a
176
+ timestamp name: deletes recursively when mtime predates the
177
+ cutoff. Never raises — disk failures log to stderr.
178
+
179
+ Returns the list of deleted paths. `retention_days <= 0`
180
+ disables pruning and returns an empty list.
181
+ """
182
+ if retention_days <= 0 or not artifact_dir.exists():
183
+ return []
184
+ cutoff = (now or _dt.datetime.now(_dt.timezone.utc)) - _dt.timedelta(days=retention_days)
185
+ cutoff_ts = cutoff.timestamp()
186
+ removed: list[Path] = []
187
+ try:
188
+ entries = list(artifact_dir.iterdir())
189
+ except OSError as exc: # noqa: BLE001 - never block the report
190
+ print(f"[council:session] artifact iterdir failed: {exc}", file=sys.stderr)
191
+ return removed
192
+ for entry in entries:
193
+ # Timestamp subdirs are owned by prune_old_sessions; skip them
194
+ # so the two pruners don't race.
195
+ if entry.is_dir() and _parse_session_timestamp(entry.name) is not None:
196
+ continue
197
+ try:
198
+ mtime = entry.stat().st_mtime
199
+ except OSError as exc: # noqa: BLE001 - never block the report
200
+ print(f"[council:session] artifact stat failed for {entry}: {exc}",
201
+ file=sys.stderr)
202
+ continue
203
+ if mtime >= cutoff_ts:
204
+ continue
205
+ try:
206
+ if entry.is_dir():
207
+ shutil.rmtree(entry)
208
+ else:
209
+ entry.unlink()
210
+ removed.append(entry)
211
+ except OSError as exc: # noqa: BLE001 - never block the report
212
+ print(f"[council:session] artifact remove failed for {entry}: {exc}",
213
+ file=sys.stderr)
214
+ return removed
215
+
216
+
217
+ def prune_all_council_artifacts(
218
+ retention_days: int | None = None,
219
+ *,
220
+ repo_root: Path | None = None,
221
+ now: _dt.datetime | None = None,
222
+ ) -> dict[str, list[Path]]:
223
+ """Prune every council artefact dir under `repo_root` in one pass.
224
+
225
+ Reads `retention_days` from settings if not supplied. Used by the
226
+ `task council-prune` target and by `save()`. Never raises.
227
+
228
+ Returns a dict keyed by directory label — `sessions`,
229
+ `questions`, `responses` — each mapped to the list of
230
+ paths actually removed.
231
+ """
232
+ root = repo_root or REPO_ROOT
233
+ days = _load_retention_days() if retention_days is None else retention_days
234
+ sessions = root / "agents" / "council-sessions"
235
+ questions = root / "agents" / "council-questions"
236
+ responses = root / "agents" / "council-responses"
237
+ return {
238
+ "sessions": (
239
+ prune_old_sessions(sessions, days, now=now)
240
+ + prune_old_artifacts(sessions, days, now=now)
241
+ ),
242
+ "questions": prune_old_artifacts(questions, days, now=now),
243
+ "responses": prune_old_artifacts(responses, days, now=now),
244
+ }
245
+
246
+
155
247
  def save(
156
248
  *,
157
249
  manifest: SessionManifest,
@@ -167,10 +259,12 @@ def save(
167
259
  - `Iterable[list[CouncilResponse]]` — multi-round, one list per
168
260
  round in execution order.
169
261
 
170
- `retention_days` controls auto-pruning of older sibling sessions
171
- after the new one is written. `None` reads the value from
172
- `.agent-settings.yml` (`ai_council.session_retention_days`,
173
- default `14`); `0` disables pruning.
262
+ `retention_days` controls auto-pruning of older council artefacts
263
+ after the new one is written sibling sessions plus, when
264
+ `sessions_dir` is not overridden, files in `council-questions/`
265
+ and `council-responses/`. `None` reads the value
266
+ from `.agent-settings.yml` (`ai_council.session_retention_days`,
267
+ default `7`); `0` disables pruning.
174
268
 
175
269
  Disk-write failures are surfaced via a stderr line but do not
176
270
  raise; the caller's text report is the source of truth.
@@ -232,5 +326,13 @@ def save(
232
326
 
233
327
  days = _load_retention_days() if retention_days is None else retention_days
234
328
  prune_old_sessions(base, days)
329
+ prune_old_artifacts(base, days)
330
+ # In production (no sessions_dir override), also prune the sibling
331
+ # council artefact dirs so questions/responses aren't left as dead
332
+ # weight. Tests that pass an explicit sessions_dir stay isolated
333
+ # from the wider tree.
334
+ if sessions_dir is None:
335
+ prune_old_artifacts(QUESTIONS_DIR, days)
336
+ prune_old_artifacts(RESPONSES_DIR, days)
235
337
 
236
338
  return session_dir
@@ -76,15 +76,13 @@ WORKSPACE: list[RuleEntry] = [
76
76
  RuleEntry("reviewer-awareness"),
77
77
  RuleEntry("scope-control"),
78
78
  RuleEntry("security-sensitive-stop"),
79
- RuleEntry("think-before-action", "degraded",
80
- strip_sections=["Consult memory before editing"]),
79
+ RuleEntry("think-before-action"),
81
80
  RuleEntry("verify-before-complete"),
82
- RuleEntry("cli-output-handling", "degraded",
83
- strip_sections=["Iron Law — rtk first, tail/grep fallback"]),
81
+ RuleEntry("cli-output-handling"),
84
82
  RuleEntry("downstream-changes"),
85
83
  RuleEntry("improve-before-implement"),
86
84
  RuleEntry("language-and-tone", "degraded",
87
- strip_sections=["`.md` files are ALWAYS English — no exceptions"]),
85
+ strip_sections=["`.md` files ALWAYS English"]),
88
86
  RuleEntry("missing-tool-handling"),
89
87
  RuleEntry("token-efficiency"),
90
88
  RuleEntry("user-interaction"),
@@ -47,6 +47,26 @@ FAIL_THRESHOLD = 0.90
47
47
  CONCENTRATION_SINGLE_PCT = 0.12
48
48
  CONCENTRATION_TOP3_PCT = 0.30
49
49
 
50
+ # Transitional concentration allowlist — non-safety-floor rules whose
51
+ # extended share exceeds CONCENTRATION_SINGLE_PCT after the kernel-trim
52
+ # refactor (commit 4e771da `refactor(kernel): compress 8 kernel rules
53
+ # per P2.2 playbook + lock kernel`). Trimming safety-floor rules shrank
54
+ # the denominator, mechanically lifting non-floor rules' percentage
55
+ # share even though their absolute size did not grow. Each entry pins
56
+ # the measured extended-size ceiling at the day road-to-path-fixes was
57
+ # closed; growth above the ceiling regresses CI. Future kernel-aware
58
+ # trimming work retires entries here.
59
+ KNOWN_CONCENTRATION_BREACHES: dict[str, int] = {
60
+ "language-and-tone.md": 3_985,
61
+ "no-cheap-questions.md": 3_530,
62
+ }
63
+ # Top-3 non-floor concentration ceiling — same rationale as the
64
+ # per-rule allowlist above. The current top-3 sum (language-and-tone +
65
+ # scope-control-allowlisted + non-destructive-allowlisted) clears the
66
+ # 30 % cap; the entry below pins the measured ceiling. Future trim
67
+ # work drops this back to None (default 30 %).
68
+ KNOWN_TOP3_CONCENTRATION_CEILING: int | None = 10_900
69
+
50
70
  # Q3=A locked safety-floor rules — out of scope for slimming and for the
51
71
  # concentration check. Their size is intentional (Iron Laws + obligation
52
72
  # surface), not drift. See road-to-structural-optimization Phase 5.
@@ -221,6 +241,12 @@ def _concentration_check(
221
241
  Returns (single-rule breaches, top-3 breach or None). Q3=A locked
222
242
  safety-floor rules are excluded from both numerator and the top-3
223
243
  selection — their size is intentional, not drift.
244
+
245
+ Allowlisted rules in `KNOWN_CONCENTRATION_BREACHES` are exempted
246
+ from the per-rule cap as long as their extended size does not
247
+ exceed the recorded ceiling (regression guard). The top-3 cap is
248
+ relaxed to `KNOWN_TOP3_CONCENTRATION_CEILING` while that ceiling
249
+ is non-None.
224
250
  """
225
251
  non_floor = [
226
252
  (name, raw, ext) for name, raw, ext in sizes
@@ -229,15 +255,22 @@ def _concentration_check(
229
255
  single_cap = total_ext * CONCENTRATION_SINGLE_PCT
230
256
  top3_cap = total_ext * CONCENTRATION_TOP3_PCT
231
257
 
232
- single_breaches = [
233
- (name, ext, ext / total_ext)
234
- for name, _, ext in non_floor
235
- if ext > single_cap
236
- ]
258
+ single_breaches: list[tuple[str, int, float]] = []
259
+ for name, _, ext in non_floor:
260
+ if ext <= single_cap:
261
+ continue
262
+ ceiling = KNOWN_CONCENTRATION_BREACHES.get(name)
263
+ if ceiling is not None and ext <= ceiling:
264
+ continue
265
+ single_breaches.append((name, ext, ext / total_ext))
266
+
237
267
  top3_sum = sum(ext for _, _, ext in non_floor[:3])
268
+ effective_top3_cap = top3_cap
269
+ if KNOWN_TOP3_CONCENTRATION_CEILING is not None:
270
+ effective_top3_cap = max(top3_cap, KNOWN_TOP3_CONCENTRATION_CEILING)
238
271
  top3_breach = (
239
272
  (top3_sum, top3_sum / total_ext)
240
- if top3_sum > top3_cap else None
273
+ if top3_sum > effective_top3_cap else None
241
274
  )
242
275
  return single_breaches, top3_breach
243
276