@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,350 @@
1
+ """Council orchestrator — fan out one question to multiple members.
2
+
3
+ v2 contract (sequential + interactive overrun prompt):
4
+
5
+ - Members are called **sequentially** in input order. The previous
6
+ parallel ThreadPoolExecutor was traded for predictable mid-flow
7
+ user prompts; with 2-3 council members the latency cost is small.
8
+ - `estimate(question, members, table)` returns a pre-call cost preview
9
+ (input tokens + max-output ceiling + USD per member). The host
10
+ agent shows this before invoking `consult()`.
11
+ - `consult(..., on_overrun=...)` invokes the callback BEFORE each
12
+ member's actual API call when the projected total cost would push
13
+ past the cost budget. The callback decides whether to proceed for
14
+ this single member; the next member triggers the callback again.
15
+
16
+ Failure normalisation (one member's exception → `error`-set
17
+ CouncilResponse, never raise) is unchanged.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass
23
+ from typing import Callable
24
+
25
+ from scripts.ai_council.budget_guard import (
26
+ record_spend as _record_daily_spend,
27
+ today_spend_usd as _today_spend_usd,
28
+ would_exceed as _would_exceed_daily,
29
+ )
30
+ from scripts.ai_council.clients import CouncilResponse, ExternalAIClient
31
+ from scripts.ai_council.pricing import (
32
+ CostEstimate,
33
+ PriceTable,
34
+ estimate_cost,
35
+ estimate_input_tokens,
36
+ )
37
+ from scripts.ai_council.project_context import ProjectContext
38
+ from scripts.ai_council.prompts import system_prompt_for
39
+
40
+
41
+ @dataclass
42
+ class CostBudget:
43
+ max_input_tokens: int = 50_000
44
+ max_output_tokens: int = 20_000
45
+ max_calls: int = 10
46
+ max_total_usd: float = 0.0 # 0 = USD ceiling disabled (token caps still apply)
47
+ daily_limit_usd: float = 0.0 # 0 = rolling 24h cap disabled (D3)
48
+
49
+
50
+ @dataclass
51
+ class CouncilQuestion:
52
+ mode: str # one of: prompt, roadmap, diff, files
53
+ user_prompt: str # bundled artefact text
54
+ max_tokens: int = 1024
55
+
56
+
57
+ @dataclass
58
+ class OverrunEvent:
59
+ """Passed to `on_overrun` when projected spend exceeds the budget."""
60
+
61
+ member_index: int
62
+ member: ExternalAIClient
63
+ next_estimate: CostEstimate # this member's projected cost
64
+ spent_input_tokens: int # already-billed totals BEFORE this member
65
+ spent_output_tokens: int
66
+ spent_usd: float
67
+ projected_total_usd: float # spent_usd + next_estimate.total_usd
68
+ daily_spent_usd: float = 0.0 # rolling 24h spend BEFORE this member (D3)
69
+ daily_limit_usd: float = 0.0 # the configured daily cap (0 = disabled)
70
+ breach_kind: str = "session" # "session" | "daily" | "tokens"
71
+
72
+
73
+ # Callback signature: receive event → return True (proceed) or False (skip + tag error).
74
+ OnOverrunCallback = Callable[[OverrunEvent], bool]
75
+
76
+
77
+ def estimate(
78
+ question: CouncilQuestion,
79
+ members: list[ExternalAIClient],
80
+ table: PriceTable,
81
+ *,
82
+ project: ProjectContext | None = None,
83
+ original_ask: str = "",
84
+ ) -> list[CostEstimate]:
85
+ """Return a pre-call cost estimate per member, in input order.
86
+
87
+ `project` and `original_ask` are passed through to
88
+ `system_prompt_for()` so the estimate covers the handoff preamble
89
+ bytes too. Both default to v1-shape (no preamble extension).
90
+ """
91
+ sys_prompt = system_prompt_for(
92
+ question.mode, project=project, original_ask=original_ask,
93
+ )
94
+ input_tokens = estimate_input_tokens(question.user_prompt) + estimate_input_tokens(sys_prompt)
95
+ return [
96
+ estimate_cost(m.name, m.model, input_tokens, question.max_tokens, table)
97
+ for m in members
98
+ ]
99
+
100
+
101
+ def consult(
102
+ members: list[ExternalAIClient],
103
+ question: CouncilQuestion,
104
+ budget: CostBudget | None = None,
105
+ *,
106
+ table: PriceTable | None = None,
107
+ on_overrun: OnOverrunCallback | None = None,
108
+ project: ProjectContext | None = None,
109
+ original_ask: str = "",
110
+ rounds: int = 1,
111
+ on_round_complete: Callable[[int, list[CouncilResponse]], None] | None = None,
112
+ ) -> list[CouncilResponse]:
113
+ """Sequentially fan out `question` to every enabled member.
114
+
115
+ - If `table` is provided, USD spend is tracked against
116
+ `budget.max_total_usd` (when > 0). Without `table`, only the
117
+ token caps apply (back-compat with v1 callers).
118
+ - When the projected next-member spend would breach any cap,
119
+ `on_overrun` is consulted. Returning False marks that member as
120
+ `cost_budget_exceeded`; True proceeds with the call.
121
+ - Without `on_overrun`, breaching caps short-circuits remaining
122
+ members with `cost_budget_exceeded` (v1 behaviour preserved).
123
+ - `project` + `original_ask` flow into `handoff_preamble()` so the
124
+ council member receives a neutral context-handoff alongside the
125
+ artefact. Both default to v1 shape (no preamble extension).
126
+ - `rounds >= 2` enables multi-round debate (D1). Each subsequent
127
+ round augments the user prompt with anonymised prior-round
128
+ responses (provider/model identity stripped). Token + USD caps
129
+ accumulate across rounds. Returns the FINAL round's responses;
130
+ use `on_round_complete(round_idx, responses)` to capture
131
+ intermediate rounds.
132
+ """
133
+ if rounds < 1:
134
+ raise ValueError(f"rounds must be >= 1 (got {rounds})")
135
+ if not members:
136
+ return []
137
+ budget = budget or CostBudget()
138
+ if len(members) > budget.max_calls:
139
+ raise ValueError(
140
+ f"Council has {len(members)} members but budget caps at "
141
+ f"{budget.max_calls} calls."
142
+ )
143
+
144
+ spent: dict[str, float] = {"input": 0, "output": 0, "usd": 0.0}
145
+ last_results: list[CouncilResponse] = []
146
+ current_user_prompt = question.user_prompt
147
+
148
+ for round_idx in range(rounds):
149
+ round_question = (
150
+ question if round_idx == 0
151
+ else CouncilQuestion(
152
+ mode=question.mode,
153
+ user_prompt=current_user_prompt,
154
+ max_tokens=question.max_tokens,
155
+ )
156
+ )
157
+ last_results = _run_round(
158
+ members, round_question, budget, spent,
159
+ table=table, on_overrun=on_overrun,
160
+ project=project, original_ask=original_ask,
161
+ )
162
+ if on_round_complete is not None:
163
+ on_round_complete(round_idx, last_results)
164
+ if round_idx + 1 < rounds:
165
+ current_user_prompt = _augment_for_next_round(
166
+ question.user_prompt, last_results, round_idx + 2,
167
+ )
168
+
169
+ return last_results
170
+
171
+
172
+ def _run_round(
173
+ members: list[ExternalAIClient],
174
+ question: CouncilQuestion,
175
+ budget: CostBudget,
176
+ spent: dict[str, float],
177
+ *,
178
+ table: PriceTable | None,
179
+ on_overrun: OnOverrunCallback | None,
180
+ project: ProjectContext | None,
181
+ original_ask: str,
182
+ ) -> list[CouncilResponse]:
183
+ """Run a single round; mutate `spent` with cumulative totals."""
184
+ system_prompt = system_prompt_for(
185
+ question.mode, project=project, original_ask=original_ask,
186
+ )
187
+ results: list[CouncilResponse] = []
188
+ estimates = (
189
+ estimate(question, members, table, project=project, original_ask=original_ask)
190
+ if table is not None
191
+ else None
192
+ )
193
+
194
+ for idx, member in enumerate(members):
195
+ # ── non-billable members skip the cost gate entirely ─────────
196
+ # ManualClient (and future PlaywrightClient) cost us $0; their
197
+ # token counts are still tracked from the response below for
198
+ # observability, but no projection / budget breach can apply.
199
+ if not getattr(member, "billable", True):
200
+ try:
201
+ response = member.ask(system_prompt, question.user_prompt, question.max_tokens)
202
+ except Exception as exc: # noqa: BLE001 - last-resort safety net
203
+ response = CouncilResponse(
204
+ provider=member.name, model=member.model, text="",
205
+ error=f"{type(exc).__name__}: {exc}",
206
+ )
207
+ results.append(response)
208
+ spent["input"] += response.input_tokens
209
+ spent["output"] += response.output_tokens
210
+ continue
211
+
212
+ # ── projected spend check ────────────────────────────────────
213
+ proj_input = spent["input"] + (estimates[idx].input_tokens if estimates else 0)
214
+ proj_output = spent["output"] + (estimates[idx].output_tokens if estimates else 0)
215
+ proj_usd = spent["usd"] + (estimates[idx].total_usd if estimates else 0.0)
216
+ next_call_usd = estimates[idx].total_usd if estimates else 0.0
217
+
218
+ breaches_tokens = (
219
+ proj_input > budget.max_input_tokens
220
+ or proj_output > budget.max_output_tokens
221
+ )
222
+ breaches_usd = budget.max_total_usd > 0 and proj_usd > budget.max_total_usd
223
+ breaches_daily = (
224
+ budget.daily_limit_usd > 0
225
+ and _would_exceed_daily(budget.daily_limit_usd, next_call_usd)
226
+ )
227
+
228
+ if breaches_tokens or breaches_usd or breaches_daily:
229
+ breach_kind = (
230
+ "tokens" if breaches_tokens
231
+ else "daily" if breaches_daily
232
+ else "session"
233
+ )
234
+ error_tag = (
235
+ "daily_budget_exceeded" if breach_kind == "daily"
236
+ else "cost_budget_exceeded"
237
+ )
238
+ if on_overrun is not None and estimates is not None:
239
+ event = OverrunEvent(
240
+ member_index=idx,
241
+ member=member,
242
+ next_estimate=estimates[idx],
243
+ spent_input_tokens=int(spent["input"]),
244
+ spent_output_tokens=int(spent["output"]),
245
+ spent_usd=spent["usd"],
246
+ projected_total_usd=proj_usd,
247
+ daily_spent_usd=(
248
+ _today_spend_usd() if budget.daily_limit_usd > 0 else 0.0
249
+ ),
250
+ daily_limit_usd=budget.daily_limit_usd,
251
+ breach_kind=breach_kind,
252
+ )
253
+ if not on_overrun(event):
254
+ results.append(_aborted(member, error_tag))
255
+ continue
256
+ else:
257
+ # v1 behaviour: short-circuit all remaining members.
258
+ for left in members[idx:]:
259
+ results.append(_aborted(left, error_tag))
260
+ return results
261
+
262
+ # ── actual call ──────────────────────────────────────────────
263
+ try:
264
+ response = member.ask(system_prompt, question.user_prompt, question.max_tokens)
265
+ except Exception as exc: # noqa: BLE001 - last-resort safety net
266
+ response = CouncilResponse(
267
+ provider=member.name, model=member.model, text="",
268
+ error=f"{type(exc).__name__}: {exc}",
269
+ )
270
+ results.append(response)
271
+ spent["input"] += response.input_tokens
272
+ spent["output"] += response.output_tokens
273
+ if estimates is not None and table is not None:
274
+ # Bill the actual output against the budget using the
275
+ # member's per-1M output rate. Re-use estimate_cost with
276
+ # the *real* token count.
277
+ actual = estimate_cost(
278
+ member.name, member.model,
279
+ response.input_tokens, response.output_tokens, table,
280
+ )
281
+ spent["usd"] += actual.total_usd
282
+ # Persist to the rolling 24h ledger when the daily cap is
283
+ # active. Errors are swallowed inside record_spend.
284
+ if budget.daily_limit_usd > 0 and not response.error:
285
+ _record_daily_spend(
286
+ actual.total_usd, member.name, member.model,
287
+ )
288
+
289
+ return results
290
+
291
+
292
+ def _aborted(member: ExternalAIClient, reason: str) -> CouncilResponse:
293
+ return CouncilResponse(
294
+ provider=member.name, model=member.model, text="", error=reason,
295
+ )
296
+
297
+
298
+ def _augment_for_next_round(
299
+ original_prompt: str,
300
+ prior_responses: list[CouncilResponse],
301
+ next_round_number: int,
302
+ ) -> str:
303
+ """Build the round-N user prompt: original artefact + anonymised prior round.
304
+
305
+ Provider/model identifiers are stripped (Iron Law of Neutrality §
306
+ multi-round). Reviewers are labelled "Reviewer A / B / C…" in the
307
+ order they appeared. Errors are skipped — they reveal nothing
308
+ useful and can leak provider error formats.
309
+ """
310
+ blocks: list[str] = []
311
+ label_idx = 0
312
+ for r in prior_responses:
313
+ if r.error or not r.text.strip():
314
+ continue
315
+ label = chr(ord("A") + label_idx)
316
+ label_idx += 1
317
+ blocks.append(f"### Reviewer {label}\n\n{r.text.strip()}")
318
+ if not blocks:
319
+ return original_prompt
320
+ prior_block = "\n\n".join(blocks)
321
+ return (
322
+ f"{original_prompt}\n\n"
323
+ f"---\n\n"
324
+ f"## Prior round critiques (round {next_round_number - 1})\n\n"
325
+ f"You are now in round {next_round_number}. Below are anonymised\n"
326
+ f"critiques from independent reviewers in the previous round.\n"
327
+ f"You do NOT know which model produced which critique. Read them,\n"
328
+ f"then respond with:\n\n"
329
+ f"1. Which prior points you agree with (cite reviewer label).\n"
330
+ f"2. Which you disagree with and why.\n"
331
+ f"3. New points or refinements not raised in round 1.\n\n"
332
+ f"{prior_block}"
333
+ )
334
+
335
+
336
+ def render(responses: list[CouncilResponse]) -> str:
337
+ """Render stacked sections + a Convergence/Divergence summary slot."""
338
+ blocks: list[str] = []
339
+ for r in responses:
340
+ header = f"## {r.provider} · {r.model}"
341
+ if r.error:
342
+ blocks.append(f"{header}\n\n*ERROR:* `{r.error}`")
343
+ continue
344
+ meta = (
345
+ f"*tokens: {r.input_tokens} in / {r.output_tokens} out · "
346
+ f"{r.latency_ms} ms*"
347
+ )
348
+ blocks.append(f"{header}\n\n{meta}\n\n{r.text}")
349
+ blocks.append("## Convergence / Divergence\n\n*to be summarised by the host agent*")
350
+ return "\n\n---\n\n".join(blocks)
@@ -0,0 +1,213 @@
1
+ """Runtime pricing layer for the AI Council.
2
+
3
+ Reads `.agent-prices.md` from the repo root, parses YAML frontmatter
4
+ and the Markdown table, and exposes:
5
+
6
+ - `load_prices()` — parse `.agent-prices.md` (bootstraps if missing)
7
+ - `estimate_input_tokens()` — chars / 4 heuristic
8
+ - `estimate_cost()` — input + output USD for a single member
9
+ - `is_stale()` — True if `last_updated` is older than the
10
+ most recent UTC Monday 00:00
11
+ - `bootstrap_from_defaults()` — write a fresh `.agent-prices.md` from
12
+ `_default_prices.DEFAULT_PRICES`
13
+
14
+ The orchestrator never reads `_default_prices` directly. It always
15
+ goes through `load_prices()` so user edits to `.agent-prices.md` win.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import datetime as _dt
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+
24
+ from scripts.ai_council._default_prices import DEFAULT_PRICES, LAST_UPDATED, as_rows
25
+
26
+ REPO_ROOT = Path(__file__).resolve().parents[2]
27
+ PRICES_FILE = REPO_ROOT / ".agent-prices.md"
28
+
29
+ # Heuristic: 1 token ≈ 4 characters of English text. OpenAI's tiktoken
30
+ # is more accurate but pulls in a heavy dep we explicitly avoid.
31
+ _CHARS_PER_TOKEN = 4
32
+
33
+
34
+ @dataclass
35
+ class Price:
36
+ provider: str
37
+ model: str
38
+ input_per_1m_usd: float
39
+ output_per_1m_usd: float
40
+
41
+
42
+ @dataclass
43
+ class PriceTable:
44
+ last_updated: str # YYYY-MM-DD
45
+ currency: str
46
+ unit: str # "per_1M_tokens"
47
+ source: str
48
+ prices: dict[tuple[str, str], Price]
49
+
50
+ def lookup(self, provider: str, model: str) -> Price | None:
51
+ return self.prices.get((provider, model))
52
+
53
+
54
+ @dataclass
55
+ class CostEstimate:
56
+ provider: str
57
+ model: str
58
+ input_tokens: int
59
+ output_tokens: int # max_tokens budget — worst-case ceiling
60
+ input_usd: float
61
+ output_usd: float
62
+
63
+ @property
64
+ def total_usd(self) -> float:
65
+ return self.input_usd + self.output_usd
66
+
67
+
68
+ # ── token + cost arithmetic ────────────────────────────────────────
69
+
70
+
71
+ def estimate_input_tokens(text: str) -> int:
72
+ """chars / 4 heuristic. Always returns ≥ 1 for non-empty strings."""
73
+ if not text:
74
+ return 0
75
+ return max(1, len(text) // _CHARS_PER_TOKEN)
76
+
77
+
78
+ def estimate_cost(
79
+ provider: str,
80
+ model: str,
81
+ input_tokens: int,
82
+ max_output_tokens: int,
83
+ table: PriceTable,
84
+ ) -> CostEstimate:
85
+ price = table.lookup(provider, model)
86
+ if price is None:
87
+ # Unknown model — return zero-cost estimate; caller decides what
88
+ # to do (warn user, skip, ...). Never silently invent a price.
89
+ return CostEstimate(provider, model, input_tokens, max_output_tokens, 0.0, 0.0)
90
+ input_usd = (input_tokens / 1_000_000) * price.input_per_1m_usd
91
+ output_usd = (max_output_tokens / 1_000_000) * price.output_per_1m_usd
92
+ return CostEstimate(provider, model, input_tokens, max_output_tokens, input_usd, output_usd)
93
+
94
+
95
+ # ── staleness ──────────────────────────────────────────────────────
96
+
97
+
98
+ def last_monday_utc(now: _dt.datetime | None = None) -> _dt.date:
99
+ """Return the most recent Monday 00:00 UTC as a date."""
100
+ now = now or _dt.datetime.now(_dt.timezone.utc)
101
+ weekday = now.weekday() # Mon=0 ... Sun=6
102
+ return (now - _dt.timedelta(days=weekday)).date()
103
+
104
+
105
+ def is_stale(table: PriceTable, now: _dt.datetime | None = None) -> bool:
106
+ """True if `last_updated` is older than the most recent UTC Monday."""
107
+ try:
108
+ last = _dt.date.fromisoformat(table.last_updated)
109
+ except ValueError:
110
+ return True
111
+ return last < last_monday_utc(now)
112
+
113
+
114
+ # ── parser + bootstrap ─────────────────────────────────────────────
115
+
116
+
117
+ def load_prices(path: Path = PRICES_FILE) -> PriceTable:
118
+ """Parse `.agent-prices.md`; bootstrap from defaults if missing."""
119
+ if not path.exists():
120
+ bootstrap_from_defaults(path)
121
+ return _parse(path.read_text(encoding="utf-8"))
122
+
123
+
124
+ def bootstrap_from_defaults(path: Path = PRICES_FILE) -> None:
125
+ """Write a fresh `.agent-prices.md` from `_default_prices.py`."""
126
+ rows = as_rows()
127
+ body = _render_markdown(LAST_UPDATED, "shipped-default", rows)
128
+ path.write_text(body, encoding="utf-8")
129
+
130
+
131
+ def _render_markdown(
132
+ last_updated: str,
133
+ source: str,
134
+ rows: list[tuple[str, str, float, float]],
135
+ ) -> str:
136
+ lines = [
137
+ "---",
138
+ f"last_updated: {last_updated}",
139
+ "currency: USD",
140
+ "unit: per_1M_tokens",
141
+ f"source: {source}",
142
+ "---",
143
+ "",
144
+ "# Agent prices",
145
+ "",
146
+ "| provider | model | input | output |",
147
+ "|-----------|---------------------|--------|--------|",
148
+ ]
149
+ for provider, model, inp, outp in rows:
150
+ lines.append(f"| {provider:<9} | {model:<19} | {inp:>6.2f} | {outp:>6.2f} |")
151
+ lines.append("")
152
+ return "\n".join(lines)
153
+
154
+
155
+ def _parse(text: str) -> PriceTable:
156
+ front, body = _split_frontmatter(text)
157
+ meta = _parse_frontmatter(front)
158
+ prices = _parse_table(body)
159
+ return PriceTable(
160
+ last_updated=meta.get("last_updated", "1970-01-01"),
161
+ currency=meta.get("currency", "USD"),
162
+ unit=meta.get("unit", "per_1M_tokens"),
163
+ source=meta.get("source", "unknown"),
164
+ prices=prices,
165
+ )
166
+
167
+
168
+ def _split_frontmatter(text: str) -> tuple[str, str]:
169
+ if not text.startswith("---"):
170
+ return "", text
171
+ parts = text.split("---", 2)
172
+ if len(parts) < 3:
173
+ return "", text
174
+ return parts[1], parts[2]
175
+
176
+
177
+ def _parse_frontmatter(front: str) -> dict[str, str]:
178
+ out: dict[str, str] = {}
179
+ for line in front.splitlines():
180
+ line = line.strip()
181
+ if not line or ":" not in line:
182
+ continue
183
+ k, _, v = line.partition(":")
184
+ out[k.strip()] = v.strip()
185
+ return out
186
+
187
+
188
+ def _parse_table(body: str) -> dict[tuple[str, str], Price]:
189
+ out: dict[tuple[str, str], Price] = {}
190
+ for line in body.splitlines():
191
+ line = line.strip()
192
+ if not line.startswith("|") or line.startswith("|--") or line.startswith("|-"):
193
+ continue
194
+ cells = [c.strip() for c in line.strip("|").split("|")]
195
+ if len(cells) != 4:
196
+ continue
197
+ provider, model, inp, outp = cells
198
+ if provider == "provider": # header row
199
+ continue
200
+ try:
201
+ out[(provider, model)] = Price(provider, model, float(inp), float(outp))
202
+ except ValueError:
203
+ continue
204
+ return out
205
+
206
+
207
+ __all__ = [
208
+ "Price", "PriceTable", "CostEstimate",
209
+ "PRICES_FILE", "DEFAULT_PRICES",
210
+ "load_prices", "bootstrap_from_defaults",
211
+ "estimate_input_tokens", "estimate_cost",
212
+ "last_monday_utc", "is_stale",
213
+ ]