@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,111 @@
1
+ #!/usr/bin/env python3
2
+ """CI guard for the `roadmap-progress-sync` rule's trackability Iron Law.
3
+
4
+ Every non-draft file under `agents/roadmaps/` (excluding `archive/`,
5
+ `skipped/`, template/README/open-questions) MUST:
6
+
7
+ 1. Be parseable by the dashboard's `PHASE_RE` — i.e. contain at least
8
+ one `## Phase <id>` or `### Phase <id>` heading.
9
+ 2. Have at least one trackable checkbox (`- [ ]`, `[x]`, `[~]`, `[-]`)
10
+ under every parsed phase.
11
+
12
+ A roadmap that fails (1) is invisible to `agents/roadmaps-progress.md`
13
+ and silently lies to the next reader. A phase that fails (2) shows as
14
+ `⬜ empty` and contributes nothing to progress percentages.
15
+
16
+ Both failure modes are rule violations per
17
+ `.augment/rules/roadmap-progress-sync.md` § "Iron Law — every active
18
+ roadmap is trackable". This script is the CI backstop so the rule
19
+ cannot be quietly broken again.
20
+
21
+ Exit codes:
22
+ 0 — every active roadmap has parseable phases with at least one
23
+ checkbox per phase.
24
+ 1 — at least one violation found; details printed to stdout.
25
+
26
+ Invocation (from project root):
27
+ python3 scripts/check_roadmap_trackable.py
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ # Reuse the dashboard's regexes and helpers — single source of truth so
36
+ # the linter cannot drift from what the dashboard actually parses.
37
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / ".augment" / "scripts"))
38
+ from update_roadmap_progress import ( # noqa: E402
39
+ CHECKBOX_RE,
40
+ PHASE_RE,
41
+ is_draft,
42
+ is_roadmap_candidate,
43
+ parse_frontmatter,
44
+ )
45
+
46
+ ROADMAP_ROOT = Path("agents/roadmaps")
47
+
48
+
49
+ def find_active_roadmaps(root: Path) -> list[Path]:
50
+ """Return every non-draft roadmap candidate under root."""
51
+ out: list[Path] = []
52
+ for path in sorted(root.rglob("*.md")):
53
+ if not path.is_file() or not is_roadmap_candidate(path):
54
+ continue
55
+ text = path.read_text(encoding="utf-8")
56
+ if is_draft(parse_frontmatter(text)):
57
+ continue
58
+ out.append(path)
59
+ return out
60
+
61
+
62
+ def violations_for(path: Path) -> list[str]:
63
+ """Return human-readable violation strings for a single roadmap."""
64
+ text = path.read_text(encoding="utf-8")
65
+ matches = list(PHASE_RE.finditer(text))
66
+ if not matches:
67
+ return [
68
+ f"{path}: no `## Phase <id>` or `### Phase <id>` heading "
69
+ "matched the dashboard's PHASE_RE — roadmap is invisible "
70
+ "to agents/roadmaps-progress.md. Either rename headings to "
71
+ "the canonical `Phase <id>` form or add `status: draft` to "
72
+ "the frontmatter."
73
+ ]
74
+ out: list[str] = []
75
+ for i, pm in enumerate(matches):
76
+ start = pm.end()
77
+ end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
78
+ if not CHECKBOX_RE.search(text[start:end]):
79
+ phase_id = pm.group(2)
80
+ name = (pm.group(3) or "").strip() or f"Phase {phase_id}"
81
+ out.append(
82
+ f"{path}: Phase {phase_id} ({name[:60]}) has zero "
83
+ "trackable checkboxes — add at least one `- [ ]` (or "
84
+ "`[x]`/`[~]`/`[-]`) item or remove the phase."
85
+ )
86
+ return out
87
+
88
+
89
+ def main() -> int:
90
+ if not ROADMAP_ROOT.is_dir():
91
+ print(f"❌ {ROADMAP_ROOT} not found — run from project root.", file=sys.stderr)
92
+ return 1
93
+ findings: list[str] = []
94
+ for path in find_active_roadmaps(ROADMAP_ROOT):
95
+ findings.extend(violations_for(path))
96
+ if findings:
97
+ print("❌ Trackable-roadmap rule violations:\n")
98
+ for f in findings:
99
+ print(f" - {f}")
100
+ print(
101
+ "\nRule: .augment/rules/roadmap-progress-sync.md "
102
+ '§ "Iron Law — every active roadmap is trackable"'
103
+ )
104
+ return 1
105
+ count = len(find_active_roadmaps(ROADMAP_ROOT))
106
+ print(f"✅ {count} active roadmap(s) — all parseable, all phases have checkboxes.")
107
+ return 0
108
+
109
+
110
+ if __name__ == "__main__":
111
+ sys.exit(main())
@@ -31,7 +31,7 @@ def is_explicit_slash_invocation(message: str) -> bool:
31
31
 
32
32
  Per the `command-suggestion` rule, explicit slash invocations
33
33
  bypass the suggestion layer entirely \u2014 they're handled by
34
- `slash-commands` directly. The engine should not score in that
34
+ `slash-command-routing-policy` directly. The engine should not score in that
35
35
  case. Helper exposed for the runtime caller and the GT-CS4
36
36
  golden.
37
37
  """
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env python3
2
+ """Generate `agents/index.md` (internal) and `docs/catalog.md` (public).
3
+
4
+ Scans `.agent-src.uncompressed/{skills,rules,commands}/` plus `docs/guidelines/`
5
+ and renders two artefact tables — one for maintainers, one for consumers.
6
+
7
+ Both files are sync-checked in CI via `--check`; drift = build break.
8
+
9
+ Usage:
10
+ python3 scripts/generate_index.py # write both files
11
+ python3 scripts/generate_index.py --check # exit 1 if drift
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import re
17
+ import sys
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+
21
+ ROOT = Path(__file__).resolve().parent.parent
22
+ SRC = ROOT / ".agent-src.uncompressed"
23
+ GUIDELINES = ROOT / "docs" / "guidelines"
24
+ INDEX_PATH = ROOT / "agents" / "index.md"
25
+ CATALOG_PATH = ROOT / "docs" / "catalog.md"
26
+
27
+ # Internal-only rules — excluded from the public catalog.
28
+ INTERNAL_RULES = {
29
+ "augment-source-of-truth",
30
+ "augment-portability",
31
+ "docs-sync",
32
+ }
33
+
34
+ FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class Entry:
39
+ kind: str # skill | rule | command | shim | guideline
40
+ name: str
41
+ description: str
42
+ extra: str # rule type · cluster · sub-folder, etc.
43
+ path: str # repo-relative link target
44
+
45
+
46
+ def _parse_frontmatter(text: str) -> dict[str, str]:
47
+ m = FRONTMATTER_RE.match(text)
48
+ if not m:
49
+ return {}
50
+ out: dict[str, str] = {}
51
+ for line in m.group(1).splitlines():
52
+ if ":" not in line or line.startswith(" "):
53
+ continue
54
+ k, _, v = line.partition(":")
55
+ out[k.strip()] = v.strip().strip('"').strip("'")
56
+ return out
57
+
58
+
59
+ def _truncate(text: str, limit: int = 200) -> str:
60
+ text = text.replace("|", "\\|").replace("\n", " ").strip()
61
+ return text if len(text) <= limit else text[: limit - 1].rstrip() + "…"
62
+
63
+
64
+ def _collect_skills() -> list[Entry]:
65
+ out = []
66
+ for skill_dir in sorted((SRC / "skills").iterdir()):
67
+ skill_md = skill_dir / "SKILL.md"
68
+ if not skill_md.exists():
69
+ continue
70
+ fm = _parse_frontmatter(skill_md.read_text(encoding="utf-8"))
71
+ name = fm.get("name") or skill_dir.name
72
+ out.append(Entry(
73
+ kind="skill",
74
+ name=name,
75
+ description=_truncate(fm.get("description", "")),
76
+ extra="",
77
+ path=f".agent-src.uncompressed/skills/{skill_dir.name}/SKILL.md",
78
+ ))
79
+ return out
80
+
81
+
82
+ def _collect_rules() -> list[Entry]:
83
+ out = []
84
+ for rule_md in sorted((SRC / "rules").glob("*.md")):
85
+ fm = _parse_frontmatter(rule_md.read_text(encoding="utf-8"))
86
+ out.append(Entry(
87
+ kind="rule",
88
+ name=rule_md.stem,
89
+ description=_truncate(fm.get("description", "")),
90
+ extra=fm.get("type", "?"),
91
+ path=f".agent-src.uncompressed/rules/{rule_md.name}",
92
+ ))
93
+ return out
94
+
95
+
96
+ def _collect_commands() -> list[Entry]:
97
+ out = []
98
+ for cmd_md in sorted((SRC / "commands").glob("*.md")):
99
+ fm = _parse_frontmatter(cmd_md.read_text(encoding="utf-8"))
100
+ is_shim = bool(fm.get("superseded_by"))
101
+ extra = ""
102
+ if is_shim:
103
+ extra = f"shim → /{fm['superseded_by']}"
104
+ elif fm.get("cluster"):
105
+ extra = f"cluster: {fm['cluster']}"
106
+ out.append(Entry(
107
+ kind="shim" if is_shim else "command",
108
+ name=fm.get("name") or cmd_md.stem,
109
+ description=_truncate(fm.get("description", "")),
110
+ extra=extra,
111
+ path=f".agent-src.uncompressed/commands/{cmd_md.name}",
112
+ ))
113
+ return out
114
+
115
+
116
+ def _collect_guidelines() -> list[Entry]:
117
+ out = []
118
+ if not GUIDELINES.exists():
119
+ return out
120
+ for g_md in sorted(GUIDELINES.rglob("*.md")):
121
+ rel = g_md.relative_to(ROOT)
122
+ category = g_md.parent.name if g_md.parent != GUIDELINES else "(root)"
123
+ out.append(Entry(
124
+ kind="guideline",
125
+ name=g_md.stem,
126
+ description="",
127
+ extra=category,
128
+ path=str(rel),
129
+ ))
130
+ return out
131
+
132
+
133
+ # Path rewriter for the public catalog: link to the shipped surface
134
+ # (`.agent-src/`) instead of the source-of-truth (`.agent-src.uncompressed/`),
135
+ # which is excluded from `package.json#files` and `composer.json` archives.
136
+ def _to_shipped_path(path: str) -> str:
137
+ return path.replace(".agent-src.uncompressed/", ".agent-src/", 1)
138
+
139
+
140
+ def _render_table(
141
+ entries: list[Entry],
142
+ cols: list[str],
143
+ link_prefix: str,
144
+ path_rewrite=None,
145
+ ) -> str:
146
+ rows = ["| " + " | ".join(cols) + " |", "|" + "|".join(["---"] * len(cols)) + "|"]
147
+ for e in entries:
148
+ path = path_rewrite(e.path) if path_rewrite else e.path
149
+ link = f"[`{e.name}`]({link_prefix}{path})"
150
+ row = [e.kind, link, e.extra, e.description]
151
+ rows.append("| " + " | ".join(row) + " |")
152
+ return "\n".join(rows)
153
+
154
+
155
+
156
+ def _render_index(skills, rules, commands, guidelines) -> str:
157
+ total = len(skills) + len(rules) + len(commands) + len(guidelines)
158
+ parts = [
159
+ "# Agent-Config Internal Index",
160
+ "",
161
+ f"Maintainer-facing index of all **{total} artefacts** in this package.",
162
+ "Auto-generated from `.agent-src.uncompressed/` and `docs/guidelines/`.",
163
+ "",
164
+ "> **Regenerate:** `python3 scripts/generate_index.py`",
165
+ "> **Drift check:** `python3 scripts/generate_index.py --check` (runs in `task ci`)",
166
+ "> Do not edit manually.",
167
+ "",
168
+ f"## Skills ({len(skills)})",
169
+ "",
170
+ _render_table(skills, ["kind", "name", "extra", "description"], "../"),
171
+ "",
172
+ f"## Rules ({len(rules)})",
173
+ "",
174
+ _render_table(rules, ["kind", "name", "type", "description"], "../"),
175
+ "",
176
+ f"## Commands ({len(commands)})",
177
+ "",
178
+ _render_table(commands, ["kind", "name", "cluster/shim", "description"], "../"),
179
+ "",
180
+ f"## Guidelines ({len(guidelines)})",
181
+ "",
182
+ _render_table(guidelines, ["kind", "name", "category", "description"], "../"),
183
+ "",
184
+ ]
185
+ return "\n".join(parts)
186
+
187
+
188
+ def _render_catalog(skills, rules, commands, guidelines) -> str:
189
+ public_rules = [r for r in rules if r.name not in INTERNAL_RULES]
190
+ public_commands = [c for c in commands if c.kind == "command"]
191
+ total = len(skills) + len(public_rules) + len(public_commands) + len(guidelines)
192
+ parts = [
193
+ "# agent-config — Public Catalog",
194
+ "",
195
+ f"Consumer-facing catalog of all **{total} public artefacts** shipped by",
196
+ "this package. Internal package-maintenance rules and deprecation shims",
197
+ "are excluded.",
198
+ "",
199
+ "> **Regenerate:** `python3 scripts/generate_index.py`",
200
+ "> Auto-generated — do not edit manually.",
201
+ "",
202
+ f"## Skills ({len(skills)})",
203
+ "",
204
+ _render_table(skills, ["kind", "name", "extra", "description"], "../", _to_shipped_path),
205
+ "",
206
+ f"## Rules ({len(public_rules)})",
207
+ "",
208
+ _render_table(public_rules, ["kind", "name", "type", "description"], "../", _to_shipped_path),
209
+ "",
210
+ f"## Commands ({len(public_commands)})",
211
+ "",
212
+ _render_table(public_commands, ["kind", "name", "cluster", "description"], "../", _to_shipped_path),
213
+ "",
214
+ f"## Guidelines ({len(guidelines)})",
215
+ "",
216
+ _render_table(guidelines, ["kind", "name", "category", "description"], "../", _to_shipped_path),
217
+ "",
218
+ "---",
219
+ "",
220
+ "← [Back to README](../README.md)",
221
+ "",
222
+ ]
223
+ return "\n".join(parts)
224
+
225
+
226
+ def main() -> int:
227
+ parser = argparse.ArgumentParser(description=__doc__)
228
+ parser.add_argument("--check", action="store_true",
229
+ help="Exit 1 if generated content differs from on-disk files.")
230
+ args = parser.parse_args()
231
+
232
+ skills = _collect_skills()
233
+ rules = _collect_rules()
234
+ commands = _collect_commands()
235
+ guidelines = _collect_guidelines()
236
+
237
+ index_text = _render_index(skills, rules, commands, guidelines)
238
+ catalog_text = _render_catalog(skills, rules, commands, guidelines)
239
+
240
+ if args.check:
241
+ drift = []
242
+ if not INDEX_PATH.exists() or INDEX_PATH.read_text(encoding="utf-8") != index_text:
243
+ drift.append(str(INDEX_PATH.relative_to(ROOT)))
244
+ if not CATALOG_PATH.exists() or CATALOG_PATH.read_text(encoding="utf-8") != catalog_text:
245
+ drift.append(str(CATALOG_PATH.relative_to(ROOT)))
246
+ if drift:
247
+ print("❌ Index drift detected — regenerate with:")
248
+ print(" python3 scripts/generate_index.py")
249
+ for d in drift:
250
+ print(f" - {d}")
251
+ return 1
252
+ print("✅ Index files in sync.")
253
+ return 0
254
+
255
+ INDEX_PATH.parent.mkdir(parents=True, exist_ok=True)
256
+ CATALOG_PATH.parent.mkdir(parents=True, exist_ok=True)
257
+ INDEX_PATH.write_text(index_text, encoding="utf-8")
258
+ CATALOG_PATH.write_text(catalog_text, encoding="utf-8")
259
+ print(f"✅ Wrote {INDEX_PATH.relative_to(ROOT)} ({len(skills)} skills, "
260
+ f"{len(rules)} rules, {len(commands)} commands, {len(guidelines)} guidelines)")
261
+ print(f"✅ Wrote {CATALOG_PATH.relative_to(ROOT)} (public subset)")
262
+ return 0
263
+
264
+
265
+ if __name__ == "__main__":
266
+ sys.exit(main())
@@ -99,3 +99,8 @@ echo "✅ Key installed: ${TARGET_FILE} (mode 0600)."
99
99
  echo " Verify: ls -la ${TARGET_FILE}"
100
100
  echo " Rotate: rerun this script (you'll be prompted to overwrite)."
101
101
  echo " Remove: rm ${TARGET_FILE}"
102
+ echo
103
+ echo "ℹ️ Key install ≠ enable. To use this provider in /council:"
104
+ echo " set ai_council.enabled: true and ai_council.members.anthropic.enabled: true"
105
+ echo " in .agent-settings.yml. Council is a 'full' cost_profile feature; under"
106
+ echo " 'minimal' / 'balanced' the runtime hooks stay inactive."
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env bash
2
+ # Interactive OpenAI-API-key installer for scripts/ai_council/clients.py.
3
+ #
4
+ # Reads the key with `read -s` so it never echoes to the terminal and
5
+ # never lands in shell history or scrollback. Writes atomically to
6
+ # ~/.config/agent-config/openai.key with mode 0600.
7
+ #
8
+ # Contract — companion to scripts/ai_council/clients.py:
9
+ # - File path: $HOME/.config/agent-config/openai.key
10
+ # - File mode: 0600 (owner read/write only)
11
+ # - Key format: must start with `sk-`
12
+ # - No --force, no --yes, no env-var bypass. Piped stdin is rejected.
13
+ #
14
+ # The runner re-checks all of the above at every live invocation and
15
+ # refuses to run if the file drifts from this contract.
16
+
17
+ set -euo pipefail
18
+
19
+ TARGET_DIR="${HOME}/.config/agent-config"
20
+ TARGET_FILE="${TARGET_DIR}/openai.key"
21
+
22
+ # ── controlling-terminal requirement ─────────────────────────────────────
23
+ # We read from /dev/tty directly (fd 3), not from stdin. This is the
24
+ # stricter and more portable contract:
25
+ # - works under `task`, `script`, `sudo`, anything that reattaches stdin
26
+ # - forces every character to come from the user's real keyboard, so a
27
+ # pipe or redirected file cannot smuggle the key into the process
28
+ # - exits cleanly if there is no controlling terminal at all (e.g. CI,
29
+ # cron, agent automation)
30
+ if ! exec 3</dev/tty 2>/dev/null; then
31
+ echo "❌ install_openai_key.sh requires a controlling terminal." >&2
32
+ echo " /dev/tty not available — refusing to run under automation." >&2
33
+ exit 2
34
+ fi
35
+
36
+ # ── overwrite confirmation ───────────────────────────────────────────────
37
+ if [[ -e "${TARGET_FILE}" ]]; then
38
+ echo "⚠️ ${TARGET_FILE} already exists."
39
+ printf "Overwrite? [type 'yes' to replace, anything else aborts]: "
40
+ read -r -u 3 answer
41
+ if [[ "${answer}" != "yes" ]]; then
42
+ echo "Aborted. Existing key untouched."
43
+ exit 0
44
+ fi
45
+ fi
46
+
47
+ # ── read key (no echo, no history) ───────────────────────────────────────
48
+ echo "Paste your OpenAI API key (input is hidden, no echo)."
49
+ echo "The key should start with 'sk-'."
50
+ printf "Key: "
51
+ # -s = silent (no echo), read from fd 3 = /dev/tty, not stdin.
52
+ read -r -s -u 3 API_KEY
53
+ echo
54
+
55
+ if [[ -z "${API_KEY}" ]]; then
56
+ echo "❌ Empty input — no file written." >&2
57
+ exit 2
58
+ fi
59
+
60
+ if [[ "${API_KEY}" != sk-* ]]; then
61
+ echo "❌ Input does not look like an OpenAI key (missing 'sk-' prefix)." >&2
62
+ echo " No file written." >&2
63
+ exit 2
64
+ fi
65
+
66
+ # ── create config dir with 0700, atomic write with 0600 ──────────────────
67
+ mkdir -p "${TARGET_DIR}"
68
+ chmod 0700 "${TARGET_DIR}"
69
+
70
+ TMP_FILE="$(mktemp "${TARGET_DIR}/.openai.key.XXXXXX")"
71
+ cleanup() { rm -f "${TMP_FILE}"; }
72
+ trap cleanup EXIT
73
+
74
+ # chmod the tmpfile BEFORE writing the key, so there is no window where
75
+ # the key sits on disk with group/other-readable permissions.
76
+ chmod 0600 "${TMP_FILE}"
77
+ printf '%s\n' "${API_KEY}" > "${TMP_FILE}"
78
+ mv "${TMP_FILE}" "${TARGET_FILE}"
79
+ trap - EXIT
80
+
81
+ # Clear the variable and the `mv` positional argument — defence in depth
82
+ # against a crash handler that dumps the process environment.
83
+ API_KEY=""
84
+
85
+ # ── verify mode post-write (portable stat: BSD on macOS, GNU on Linux) ───
86
+ if ACTUAL_MODE=$(stat -f '%Lp' "${TARGET_FILE}" 2>/dev/null); then
87
+ : # macOS / BSD
88
+ else
89
+ ACTUAL_MODE=$(stat -c '%a' "${TARGET_FILE}")
90
+ fi
91
+
92
+ if [[ "${ACTUAL_MODE}" != "600" ]]; then
93
+ echo "❌ Permissions verification failed: ${TARGET_FILE} has mode ${ACTUAL_MODE}, expected 600." >&2
94
+ echo " Delete and reinstall: rm ${TARGET_FILE} && $0" >&2
95
+ exit 3
96
+ fi
97
+
98
+ echo "✅ Key installed: ${TARGET_FILE} (mode 0600)."
99
+ echo " Verify: ls -la ${TARGET_FILE}"
100
+ echo " Rotate: rerun this script (you'll be prompted to overwrite)."
101
+ echo " Remove: rm ${TARGET_FILE}"
102
+ echo
103
+ echo "ℹ️ Key install ≠ enable. To use this provider in /council:"
104
+ echo " set ai_council.enabled: true and ai_council.members.openai.enabled: true"
105
+ echo " in .agent-settings.yml. Council is a 'full' cost_profile feature; under"
106
+ echo " 'minimal' / 'balanced' the runtime hooks stay inactive."
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env python3
2
+ """Lint the `load_context:` / `load_context_eager:` frontmatter schema.
3
+
4
+ Validates per docs/contracts/load-context-schema.md:
5
+ - Paths exist and are .md
6
+ - Allowed roots only (.agent-src*/contexts/, agents/contexts/)
7
+ - No public→project-local leak (warn)
8
+ - No circular refs across lazy + eager edges
9
+ - Combined char-budget for eager edges (rule + eager targets ≤ cap)
10
+
11
+ Exits non-zero on error; warnings are reported but do not fail.
12
+ Used in CI via `task lint-load-context`.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Iterable
19
+
20
+ import yaml
21
+
22
+ ROOT = Path(__file__).resolve().parent.parent
23
+
24
+ SCAN_DIRS = [
25
+ ROOT / ".agent-src.uncompressed" / "rules",
26
+ ROOT / ".agent-src.uncompressed" / "contexts",
27
+ ROOT / "agents" / "contexts",
28
+ ]
29
+
30
+ ALLOWED_PREFIXES = (
31
+ ".agent-src.uncompressed/contexts/",
32
+ ".agent-src/contexts/",
33
+ "agents/contexts/",
34
+ )
35
+
36
+ PUBLIC_RULE_PREFIX = ".agent-src.uncompressed/rules/"
37
+ PROJECT_LOCAL_PREFIX = "agents/contexts/"
38
+
39
+ HARD_FLOOR_RULES = {"non-destructive-by-default", "security-sensitive-stop"}
40
+
41
+ CAP_ALWAYS = 2_500
42
+ CAP_AUTO = 4_000
43
+ CAP_SAFETY = 5_000
44
+
45
+
46
+ def parse_frontmatter(path: Path) -> dict:
47
+ text = path.read_text(encoding="utf-8")
48
+ if not text.startswith("---\n"):
49
+ return {}
50
+ end = text.find("\n---\n", 4)
51
+ if end == -1:
52
+ return {}
53
+ try:
54
+ data = yaml.safe_load(text[4:end])
55
+ except yaml.YAMLError:
56
+ return {}
57
+ return data if isinstance(data, dict) else {}
58
+
59
+
60
+ def collect_files() -> Iterable[Path]:
61
+ for d in SCAN_DIRS:
62
+ if d.exists():
63
+ yield from d.rglob("*.md")
64
+
65
+
66
+ def rel(p: Path) -> str:
67
+ return p.relative_to(ROOT).as_posix()
68
+
69
+
70
+ def cap_for(rule_path: Path, fm: dict) -> int:
71
+ if rule_path.stem in HARD_FLOOR_RULES:
72
+ return CAP_SAFETY
73
+ rtype = (fm.get("type") or "").strip('"').strip("'")
74
+ if rtype == "always":
75
+ return CAP_ALWAYS
76
+ if rtype == "auto":
77
+ return CAP_AUTO
78
+ return CAP_AUTO # default for non-rule contexts cited in eager (won't trigger)
79
+
80
+
81
+ def find_cycles(graph: dict[str, list[str]]) -> list[list[str]]:
82
+ cycles: list[list[str]] = []
83
+ visiting: set[str] = set()
84
+ visited: set[str] = set()
85
+ stack: list[str] = []
86
+
87
+ def dfs(node: str) -> None:
88
+ if node in visiting:
89
+ i = stack.index(node)
90
+ cycles.append(stack[i:] + [node])
91
+ return
92
+ if node in visited:
93
+ return
94
+ visiting.add(node)
95
+ stack.append(node)
96
+ for nxt in graph.get(node, []):
97
+ dfs(nxt)
98
+ stack.pop()
99
+ visiting.discard(node)
100
+ visited.add(node)
101
+
102
+ for n in graph:
103
+ dfs(n)
104
+ return cycles
105
+
106
+
107
+ def main() -> int:
108
+ errors: list[str] = []
109
+ warnings: list[str] = []
110
+ graph: dict[str, list[str]] = {}
111
+
112
+ for f in collect_files():
113
+ fm = parse_frontmatter(f)
114
+ lazy = fm.get("load_context") or []
115
+ eager = fm.get("load_context_eager") or []
116
+ if not (lazy or eager):
117
+ continue
118
+ if not isinstance(lazy, list) or not isinstance(eager, list):
119
+ errors.append(f"{rel(f)}: load_context* must be a list")
120
+ continue
121
+
122
+ edges = list(lazy) + list(eager)
123
+ graph[rel(f)] = edges
124
+
125
+ for entry in edges:
126
+ if not isinstance(entry, str) or not entry.endswith(".md"):
127
+ errors.append(f"{rel(f)}: entry not str ending in .md → {entry!r}")
128
+ continue
129
+ if not entry.startswith(ALLOWED_PREFIXES):
130
+ errors.append(f"{rel(f)}: disallowed root → {entry}")
131
+ continue
132
+ target = ROOT / entry
133
+ if not target.exists():
134
+ errors.append(f"{rel(f)}: target missing → {entry}")
135
+ continue
136
+ if rel(f).startswith(PUBLIC_RULE_PREFIX) and entry.startswith(PROJECT_LOCAL_PREFIX):
137
+ warnings.append(f"{rel(f)}: public rule references project-local context → {entry}")
138
+
139
+ if eager:
140
+ cap = cap_for(f, fm)
141
+ total = len(f.read_text(encoding="utf-8"))
142
+ for entry in eager:
143
+ tgt = ROOT / entry
144
+ if tgt.exists():
145
+ total += len(tgt.read_text(encoding="utf-8"))
146
+ if total > cap:
147
+ errors.append(f"{rel(f)}: eager-load combined chars {total} > cap {cap}")
148
+
149
+ for cycle in find_cycles(graph):
150
+ errors.append("circular load_context: " + " → ".join(cycle))
151
+
152
+ for w in warnings:
153
+ print(f"⚠️ {w}")
154
+ for e in errors:
155
+ print(f"❌ {e}")
156
+ if errors:
157
+ return 1
158
+ print(f"✅ load_context schema clean ({len(graph)} declarer(s))")
159
+ return 0
160
+
161
+
162
+ if __name__ == "__main__":
163
+ sys.exit(main())