@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
@@ -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
 
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env python3
2
+ """Validate compressed-output paths in `.agent-src/rules/*.md`.
3
+
4
+ Runs after `scripts/compress.py` projects sources to `.agent-src/`. The
5
+ rewriter in `compress.py` is the load-bearing primitive (road-to-path-fixes
6
+ P1.2); this script is the post-condition gate (P5.1) — every `load_context:`
7
+ entry in `.agent-src/rules/*.md` must resolve relative to the rule file's
8
+ directory to an existing file, and forbidden substrings must not survive
9
+ the rewrite (unless declared in `validator_ignore`).
10
+
11
+ Forbidden substrings (load_context + body):
12
+ - `.agent-src.uncompressed/` unless declared in validator_ignore
13
+ - `../../docs/` body-link two-up form (rewriter
14
+ collapses to single-up)
15
+ - `../../agents/` same shape, different root
16
+
17
+ Body-link checks (Council Decision 2, 2026-05-06):
18
+ - `load_context:` entries MUST resolve to an existing file under
19
+ `.agent-src/`.
20
+ - Body markdown links to `../contexts/...md` MUST resolve.
21
+ - Body markdown links to `../docs/guidelines/...md` are NOT checked
22
+ (P3.1 was cancelled; resolution is intentionally out of scope, the
23
+ Copilot suppression floor in P6 is the silencer).
24
+
25
+ `validator_ignore:` frontmatter primitive:
26
+ - Per-rule allowlist for rules that *describe* a forbidden substring as
27
+ their subject matter (e.g. `augment-source-of-truth` documents the
28
+ `.agent-src.uncompressed/` boundary). Each entry: `{type, pattern,
29
+ reason}`. The validator emits an audit line per matched ignore so
30
+ drift cannot hide.
31
+
32
+ Exit codes: 0 = clean, 1 = violations found, 3 = internal error.
33
+ """
34
+ from __future__ import annotations
35
+
36
+ import re
37
+ import sys
38
+ from dataclasses import dataclass
39
+ from pathlib import Path
40
+
41
+ import yaml
42
+
43
+ ROOT = Path(__file__).resolve().parent.parent
44
+ RULES_DIR = ROOT / ".agent-src" / "rules"
45
+
46
+ FORBIDDEN_SUBSTRINGS = (
47
+ ".agent-src.uncompressed/",
48
+ "../../docs/",
49
+ "../../agents/",
50
+ )
51
+
52
+ # Markdown links: `[text](path)` — capture path. Skip URLs and anchors.
53
+ _LINK_RE = re.compile(r'\[[^\]]*\]\(([^)#\s]+)(?:#[^)]*)?\)')
54
+
55
+
56
+ # Body-link prefixes whose resolution is intentionally out of scope.
57
+ # Council Decision 2 (2026-05-06): P3.1 was cancelled, so guideline links
58
+ # under `.agent-src/rules/` cannot resolve in the projected tree. Copilot
59
+ # suppression (P6) is the silencer for the noise.
60
+ UNCHECKED_LINK_PREFIXES = (
61
+ "../docs/guidelines/",
62
+ "../../docs/guidelines/",
63
+ )
64
+
65
+
66
+ @dataclass
67
+ class Violation:
68
+ file: str
69
+ line: int
70
+ kind: str
71
+ detail: str
72
+
73
+
74
+ @dataclass
75
+ class IgnoreEntry:
76
+ """Frontmatter `validator_ignore:` entry."""
77
+ kind: str # "substring" | "link"
78
+ pattern: str # exact substring or link prefix to ignore
79
+ reason: str # human-readable rationale (audited)
80
+
81
+
82
+ def _split_frontmatter(text: str):
83
+ if not text.startswith("---\n"):
84
+ return None, text
85
+ end = text.find("\n---\n", 4)
86
+ if end == -1:
87
+ return None, text
88
+ fm_text = text[4:end]
89
+ body = text[end + len("\n---\n"):]
90
+ try:
91
+ fm = yaml.safe_load(fm_text)
92
+ except yaml.YAMLError:
93
+ return None, text
94
+ return fm if isinstance(fm, dict) else {}, body
95
+
96
+
97
+ def _parse_ignores(fm: dict) -> list[IgnoreEntry]:
98
+ entries = fm.get("validator_ignore") or []
99
+ if not isinstance(entries, list):
100
+ return []
101
+ out: list[IgnoreEntry] = []
102
+ for raw in entries:
103
+ if not isinstance(raw, dict):
104
+ continue
105
+ kind = str(raw.get("type") or "").strip()
106
+ pattern = str(raw.get("pattern") or "").strip()
107
+ reason = str(raw.get("reason") or "").strip()
108
+ if kind in ("substring", "link") and pattern and reason:
109
+ out.append(IgnoreEntry(kind=kind, pattern=pattern, reason=reason))
110
+ return out
111
+
112
+
113
+ def _ignored(needle: str, ignores: list[IgnoreEntry], kind: str) -> IgnoreEntry | None:
114
+ for ig in ignores:
115
+ if ig.kind == kind and ig.pattern == needle:
116
+ return ig
117
+ return None
118
+
119
+
120
+ def _check_load_context(rule_file: Path, fm: dict, viols: list[Violation],
121
+ ignores: list[IgnoreEntry], audited: list[tuple[str, IgnoreEntry]]) -> None:
122
+ rule_dir = rule_file.parent
123
+ for key in ("load_context", "load_context_eager"):
124
+ entries = fm.get(key) or []
125
+ if not isinstance(entries, list):
126
+ continue
127
+ for entry in entries:
128
+ if not isinstance(entry, str):
129
+ continue
130
+ blocked = False
131
+ for needle in FORBIDDEN_SUBSTRINGS:
132
+ if needle in entry:
133
+ ig = _ignored(needle, ignores, "substring")
134
+ if ig:
135
+ audited.append((str(rule_file.relative_to(ROOT)), ig))
136
+ continue
137
+ viols.append(Violation(
138
+ str(rule_file.relative_to(ROOT)), 0, f"{key}-forbidden",
139
+ f"forbidden substring {needle!r} in entry {entry!r}",
140
+ ))
141
+ blocked = True
142
+ break
143
+ if blocked:
144
+ continue
145
+ target = (rule_dir / entry).resolve()
146
+ if not target.is_file():
147
+ viols.append(Violation(
148
+ str(rule_file.relative_to(ROOT)), 0, f"{key}-missing",
149
+ f"{entry!r} does not resolve to an existing file",
150
+ ))
151
+
152
+
153
+ def _check_body(rule_file: Path, body: str, viols: list[Violation],
154
+ ignores: list[IgnoreEntry], audited: list[tuple[str, IgnoreEntry]]) -> None:
155
+ rule_dir = rule_file.parent
156
+ for line_num, line in enumerate(body.splitlines(), start=1):
157
+ for needle in FORBIDDEN_SUBSTRINGS:
158
+ if needle in line:
159
+ ig = _ignored(needle, ignores, "substring")
160
+ if ig:
161
+ audited.append((f"{rule_file.relative_to(ROOT)}:{line_num}", ig))
162
+ continue
163
+ viols.append(Violation(
164
+ str(rule_file.relative_to(ROOT)), line_num, "body-forbidden",
165
+ f"forbidden substring {needle!r}",
166
+ ))
167
+ for m in _LINK_RE.finditer(line):
168
+ link = m.group(1)
169
+ if link.startswith(("http://", "https://", "mailto:", "#")):
170
+ continue
171
+ if not link.endswith(".md"):
172
+ continue
173
+ if any(link.startswith(p) for p in UNCHECKED_LINK_PREFIXES):
174
+ continue
175
+ target = (rule_dir / link).resolve()
176
+ if not target.is_file():
177
+ viols.append(Violation(
178
+ str(rule_file.relative_to(ROOT)), line_num, "body-link-missing",
179
+ f"link target {link!r} does not resolve",
180
+ ))
181
+
182
+
183
+ def main() -> int:
184
+ if not RULES_DIR.is_dir():
185
+ print(f"❌ {RULES_DIR} not found — run compression first", file=sys.stderr)
186
+ return 3
187
+ viols: list[Violation] = []
188
+ audited: list[tuple[str, IgnoreEntry]] = []
189
+ for rule_file in sorted(RULES_DIR.glob("*.md")):
190
+ text = rule_file.read_text(encoding="utf-8")
191
+ fm, body = _split_frontmatter(text)
192
+ ignores: list[IgnoreEntry] = _parse_ignores(fm) if fm is not None else []
193
+ if fm is not None:
194
+ _check_load_context(rule_file, fm, viols, ignores, audited)
195
+ _check_body(rule_file, body, viols, ignores, audited)
196
+ if audited:
197
+ print("ℹ️ validator_ignore audit:")
198
+ for loc, ig in audited:
199
+ print(f" {loc} — [{ig.kind}] {ig.pattern!r} → {ig.reason}")
200
+ print()
201
+ if viols:
202
+ for v in viols:
203
+ loc = f"{v.file}:{v.line}" if v.line else v.file
204
+ print(f"❌ [{v.kind}] {loc} — {v.detail}")
205
+ print(f"\n{len(viols)} violation(s) in .agent-src/rules/")
206
+ return 1
207
+ rule_count = len(list(RULES_DIR.glob('*.md')))
208
+ print(f"✅ compressed-path check clean ({rule_count} rules, {len(audited)} ignore(s) audited)")
209
+ return 0
210
+
211
+
212
+ if __name__ == "__main__":
213
+ sys.exit(main())
@@ -24,6 +24,15 @@ from dataclasses import dataclass, asdict
24
24
  from pathlib import Path
25
25
  from typing import List, Literal
26
26
 
27
+ # Import the rewriter so frontmatter comparison can normalise the source
28
+ # side through the same path transformations the compressor applies. Without
29
+ # this, every `load_context:` logical name (e.g. `contexts/foo.md`) and every
30
+ # `../../docs/...` body link looks like a frontmatter / body mismatch even
31
+ # though the rewriter is doing exactly what road-to-path-fixes.md P2/P3
32
+ # specified.
33
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
34
+ from compress import _rewrite_paths # noqa: E402
35
+
27
36
  Severity = Literal["error", "warning", "info"]
28
37
 
29
38
  SOURCE_DIR = Path(".agent-src.uncompressed")
@@ -275,6 +284,12 @@ def scan_all(root: Path) -> List[Issue]:
275
284
 
276
285
  source_text = source_file.read_text(encoding="utf-8")
277
286
  target_text = target_file.read_text(encoding="utf-8")
287
+ # Normalise source through the path rewriter (idempotent) so logical
288
+ # `load_context:` names and `../../docs/...` body links match the
289
+ # depth-aware form the compressor produced. Compression word-count
290
+ # checks downstream are unaffected because rewriting only edits
291
+ # frontmatter list values and link targets, not prose tokens.
292
+ source_text = _rewrite_paths(source_text, rel_str)
278
293
  issues.extend(check_pair(rel_str, source_text, target_text))
279
294
 
280
295
  return issues
@@ -35,6 +35,7 @@ LOCKED_SUBTREES = (
35
35
  "chat-history",
36
36
  "execution",
37
37
  "authority",
38
+ "contracts",
38
39
  )
39
40
 
40
41
  # Files allowed to remain at the contexts root. Anything else at the root
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+ """CI guard for the `ai-council` skill's output-path convention.
3
+
4
+ Council artefacts (questions, responses, sessions) belong in three
5
+ canonical directories under `agents/`:
6
+
7
+ - agents/council-questions/<topic-slug>.md (paired with roadmap/ADR)
8
+ - agents/council-responses/<topic-slug>.json (paired with question)
9
+ - agents/council-sessions/<UTC-timestamp>.json (ad-hoc sessions)
10
+
11
+ The three canonical dirs are gitignored — the linter therefore only
12
+ catches **misplacement**, not naming-conventions inside the dirs:
13
+
14
+ - Files at agents/ root with a council-* or .council-* prefix
15
+ (e.g. agents/council-question-foo.md, agents/.council-foo.md).
16
+ - council-* files under any other subdirectory of agents/.
17
+
18
+ Failure modes are enforced by `.agent-src.uncompressed/skills/ai-council/SKILL.md`
19
+ § "Output path convention".
20
+
21
+ Exit codes:
22
+ 0 — layout is clean.
23
+ 1 — at least one violation found; details printed to stdout.
24
+
25
+ Invocation (from project root):
26
+ python3 scripts/check_council_layout.py
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import re
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ AGENTS_ROOT = Path("agents")
36
+ CANONICAL_DIRS = {
37
+ "council-questions": ".md",
38
+ "council-responses": ".json",
39
+ "council-sessions": ".json",
40
+ }
41
+ # A council artefact is a file whose name starts with `council-` or
42
+ # `.council-`. This intentionally excludes roadmaps like
43
+ # `road-to-ai-council.md` whose stem only contains the word "council".
44
+ COUNCIL_PREFIX_RE = re.compile(r"^\.?council-")
45
+
46
+
47
+ def is_council_artefact(path: Path) -> bool:
48
+ return bool(COUNCIL_PREFIX_RE.match(path.name))
49
+
50
+
51
+ def find_violations(root: Path) -> list[str]:
52
+ findings: list[str] = []
53
+ if not root.is_dir():
54
+ return findings
55
+
56
+ # 1. Stray council artefacts at agents/ root
57
+ for path in sorted(root.iterdir()):
58
+ if not path.is_file():
59
+ continue
60
+ if is_council_artefact(path):
61
+ findings.append(
62
+ f"{path}: council artefact at agents/ root — move to "
63
+ f"agents/council-questions/, agents/council-responses/, "
64
+ f"or agents/council-sessions/ per ai-council § Output path "
65
+ f"convention."
66
+ )
67
+
68
+ # 2. Council artefacts in non-canonical subdirectories
69
+ for path in sorted(root.rglob("*")):
70
+ if not path.is_file() or not is_council_artefact(path):
71
+ continue
72
+ try:
73
+ rel = path.relative_to(root)
74
+ except ValueError:
75
+ continue
76
+ if len(rel.parts) == 1:
77
+ continue # already handled above
78
+ if rel.parts[0] in CANONICAL_DIRS:
79
+ continue
80
+ findings.append(
81
+ f"{path}: council artefact in non-canonical directory "
82
+ f"agents/{rel.parts[0]}/ — only council-questions/, "
83
+ f"council-responses/, council-sessions/ are allowed."
84
+ )
85
+
86
+ return findings
87
+
88
+
89
+ def main() -> int:
90
+ findings = find_violations(AGENTS_ROOT)
91
+ if findings:
92
+ print("❌ Council layout violations:\n")
93
+ for f in findings:
94
+ print(f" - {f}")
95
+ print(
96
+ "\nRule: .agent-src.uncompressed/skills/ai-council/SKILL.md "
97
+ '§ "Output path convention"'
98
+ )
99
+ return 1
100
+ print("✅ Council layout clean.")
101
+ return 0
102
+
103
+
104
+ if __name__ == "__main__":
105
+ sys.exit(main())