@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,106 @@
1
+ """One-off Phase-1 round-trip runner.
2
+
3
+ Used exactly once to generate the evidence artefact required to lift
4
+ the capture-only fence on `road-to-ai-council.md` Phase 2+ and the
5
+ end-to-end verification on `road-to-council-modes.md` Phase 2a.
6
+
7
+ Not part of the public CLI surface — `/council` remains the supported
8
+ entry point. This script is committed under `scripts/ai_council/` so
9
+ the evidence is reproducible from the git history alone.
10
+
11
+ Invocation:
12
+ .venv/bin/python -m scripts.ai_council._one_off_roundtrip
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ from scripts.ai_council.bundler import bundle_roadmap
20
+ from scripts.ai_council.clients import AnthropicClient, load_anthropic_key
21
+ from scripts.ai_council.orchestrator import (
22
+ CostBudget,
23
+ CouncilQuestion,
24
+ consult,
25
+ estimate,
26
+ )
27
+ from scripts.ai_council.pricing import estimate_cost, load_prices
28
+ from scripts.ai_council.project_context import detect_project_context
29
+ from scripts.ai_council.session import SessionManifest, save as save_session
30
+
31
+ REPO_ROOT = Path(__file__).resolve().parents[2]
32
+ ROADMAP_PATH = REPO_ROOT / "agents/roadmaps/road-to-council-modes.md"
33
+
34
+ ORIGINAL_ASK = (
35
+ "Bitte review die folgende Roadmap (council-modes Phase 2c "
36
+ "Playwright). Die Maintainer-Recommendations für Q1-Q5 sind im "
37
+ "Block 'Decisions Required' bereits hinterlegt. Frage: sollten "
38
+ "wir die Recommendations annehmen wie sie sind, oder gibt es "
39
+ "blinde Flecken die wir vor dem Lift der capture-only fence "
40
+ "kläeren sollten?"
41
+ )
42
+
43
+
44
+ def main() -> int:
45
+ api_key = load_anthropic_key()
46
+ client = AnthropicClient(api_key=api_key)
47
+
48
+ context = bundle_roadmap(ROADMAP_PATH)
49
+ project = detect_project_context(REPO_ROOT)
50
+ table = load_prices()
51
+
52
+ question = CouncilQuestion(
53
+ mode="roadmap",
54
+ user_prompt=context.text,
55
+ max_tokens=2048,
56
+ )
57
+
58
+ estimates = estimate(
59
+ question, [client], table,
60
+ project=project, original_ask=ORIGINAL_ASK,
61
+ )
62
+ print(f"[estimate] {client.name}/{client.model}: "
63
+ f"~{estimates[0].input_tokens} in + {estimates[0].output_tokens} out "
64
+ f"= ${estimates[0].total_usd:.4f}")
65
+
66
+ budget = CostBudget(
67
+ max_input_tokens=50_000,
68
+ max_output_tokens=20_000,
69
+ max_calls=10,
70
+ max_total_usd=0.50,
71
+ )
72
+
73
+ print(f"[consult] calling {client.name}/{client.model} ...")
74
+ responses = consult(
75
+ [client], question, budget,
76
+ table=table, project=project, original_ask=ORIGINAL_ASK,
77
+ )
78
+
79
+ if not responses or responses[0].error:
80
+ err = responses[0].error if responses else "no response"
81
+ print(f"[error] {err}", file=sys.stderr)
82
+ return 1
83
+
84
+ r = responses[0]
85
+ actual = estimate_cost(r.provider, r.model, r.input_tokens, r.output_tokens, table)
86
+ actual_usd = actual.total_usd
87
+ print(f"[done] tokens: {r.input_tokens} in / {r.output_tokens} out · "
88
+ f"latency: {r.latency_ms} ms · actual ${actual_usd:.4f}")
89
+
90
+ manifest = SessionManifest(
91
+ mode="roadmap",
92
+ artefact=str(ROADMAP_PATH.relative_to(REPO_ROOT)),
93
+ original_ask=ORIGINAL_ASK,
94
+ members=[f"{r.provider}/{r.model}"],
95
+ rounds=1,
96
+ cost_usd_estimated=estimates[0].total_usd,
97
+ cost_usd_actual=actual_usd,
98
+ extra={"purpose": "Phase 1 ai-council round-trip + Phase 2a council-modes E2E evidence"},
99
+ )
100
+ session_dir = save_session(manifest=manifest, responses=responses)
101
+ print(f"[saved] {session_dir.relative_to(REPO_ROOT)}/")
102
+ return 0
103
+
104
+
105
+ if __name__ == "__main__":
106
+ raise SystemExit(main())
@@ -0,0 +1,172 @@
1
+ """Per-day rolling cost-budget guard for the council (D3).
2
+
3
+ Adds a 24h-rolling-window USD limit on top of the per-session caps in
4
+ `orchestrator.CostBudget`. Persists a small JSONL ledger in
5
+ ``~/.config/agent-config/council-spend.jsonl`` (mode 0600, same
6
+ permission discipline as the API keys).
7
+
8
+ Contract
9
+ - The ledger is **append-only**. Each line is ``{"ts": ISO-8601 UTC,
10
+ "usd": float, "provider": str, "model": str}``.
11
+ - ``today_spend_usd()`` sums entries within the last 24h from "now"
12
+ (true rolling window — not midnight UTC, never resets at boundary
13
+ surprise).
14
+ - ``would_exceed(limit_usd, next_call_usd)`` returns True iff the next
15
+ call would push the rolling window past the limit.
16
+ - ``record_spend(usd, provider, model)`` appends a single entry; never
17
+ raises on disk failure (logs to stderr, returns False).
18
+
19
+ The guard is **advisory** to the orchestrator: it provides a check
20
+ function the host agent can call before each council member; the
21
+ orchestrator's per-session cost gate stays the primary defence.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import datetime as _dt
27
+ import json
28
+ import os
29
+ import stat
30
+ import sys
31
+ from dataclasses import dataclass
32
+ from pathlib import Path
33
+
34
+ LEDGER_PATH = Path.home() / ".config" / "agent-config" / "council-spend.jsonl"
35
+ ROLLING_WINDOW_HOURS = 24
36
+
37
+
38
+ @dataclass
39
+ class SpendEntry:
40
+ ts: _dt.datetime # UTC, tz-aware
41
+ usd: float
42
+ provider: str
43
+ model: str
44
+
45
+
46
+ def _now_utc() -> _dt.datetime:
47
+ return _dt.datetime.now(_dt.timezone.utc)
48
+
49
+
50
+ def _ensure_ledger_dir(path: Path) -> bool:
51
+ """Create the ledger's parent directory mode 0700 if missing."""
52
+ try:
53
+ path.parent.mkdir(parents=True, exist_ok=True)
54
+ if (path.parent.stat().st_mode & 0o777) != 0o700:
55
+ try:
56
+ os.chmod(path.parent, 0o700)
57
+ except OSError:
58
+ # On macOS ~/.config may inherit umask perms; do not block.
59
+ pass
60
+ return True
61
+ except OSError as exc: # noqa: BLE001 - never block the orchestrator
62
+ print(f"[council:budget_guard] mkdir failed: {exc}", file=sys.stderr)
63
+ return False
64
+
65
+
66
+ def _ensure_ledger_file_mode(path: Path) -> None:
67
+ """Make sure an existing ledger file is mode 0600. Best-effort."""
68
+ if not path.exists():
69
+ return
70
+ current = path.stat().st_mode & 0o777
71
+ if current != 0o600:
72
+ try:
73
+ os.chmod(path, 0o600)
74
+ except OSError:
75
+ pass
76
+
77
+
78
+ def _parse_iso(ts: str) -> _dt.datetime | None:
79
+ try:
80
+ # `fromisoformat` accepts "+00:00"; we always write with "+00:00".
81
+ return _dt.datetime.fromisoformat(ts)
82
+ except ValueError:
83
+ return None
84
+
85
+
86
+ def read_entries(path: Path | None = None) -> list[SpendEntry]:
87
+ """Read every well-formed entry from the ledger.
88
+
89
+ Malformed lines are skipped silently. Empty/missing ledger → [].
90
+ """
91
+ p = path or LEDGER_PATH
92
+ if not p.exists():
93
+ return []
94
+ out: list[SpendEntry] = []
95
+ for line in p.read_text(encoding="utf-8").splitlines():
96
+ line = line.strip()
97
+ if not line:
98
+ continue
99
+ try:
100
+ obj = json.loads(line)
101
+ except json.JSONDecodeError:
102
+ continue
103
+ ts = _parse_iso(str(obj.get("ts", "")))
104
+ if ts is None:
105
+ continue
106
+ try:
107
+ usd = float(obj.get("usd", 0))
108
+ except (TypeError, ValueError):
109
+ continue
110
+ out.append(SpendEntry(
111
+ ts=ts, usd=usd,
112
+ provider=str(obj.get("provider", "")),
113
+ model=str(obj.get("model", "")),
114
+ ))
115
+ return out
116
+
117
+
118
+ def today_spend_usd(
119
+ *,
120
+ path: Path | None = None,
121
+ now: _dt.datetime | None = None,
122
+ window_hours: int = ROLLING_WINDOW_HOURS,
123
+ ) -> float:
124
+ """Sum of USD spent in the last `window_hours` (rolling window)."""
125
+ cutoff = (now or _now_utc()) - _dt.timedelta(hours=window_hours)
126
+ return sum(e.usd for e in read_entries(path) if e.ts >= cutoff)
127
+
128
+
129
+ def would_exceed(
130
+ limit_usd: float,
131
+ next_call_usd: float,
132
+ *,
133
+ path: Path | None = None,
134
+ now: _dt.datetime | None = None,
135
+ window_hours: int = ROLLING_WINDOW_HOURS,
136
+ ) -> bool:
137
+ """True iff appending `next_call_usd` would push the window past `limit_usd`.
138
+
139
+ `limit_usd <= 0` disables the guard (returns False). Mirrors the
140
+ `CostBudget.max_total_usd` convention.
141
+ """
142
+ if limit_usd <= 0:
143
+ return False
144
+ spent = today_spend_usd(path=path, now=now, window_hours=window_hours)
145
+ return (spent + next_call_usd) > limit_usd
146
+
147
+
148
+ def record_spend(
149
+ usd: float,
150
+ provider: str,
151
+ model: str,
152
+ *,
153
+ path: Path | None = None,
154
+ now: _dt.datetime | None = None,
155
+ ) -> bool:
156
+ """Append one entry to the ledger. Returns True on success."""
157
+ if usd <= 0:
158
+ return True # zero-cost calls (manual mode) skip the ledger
159
+ p = path or LEDGER_PATH
160
+ if not _ensure_ledger_dir(p):
161
+ return False
162
+ ts = (now or _now_utc()).isoformat()
163
+ entry = json.dumps({"ts": ts, "usd": round(usd, 6),
164
+ "provider": provider, "model": model}) + "\n"
165
+ try:
166
+ with p.open("a", encoding="utf-8") as fh:
167
+ fh.write(entry)
168
+ except OSError as exc: # noqa: BLE001 - never block the orchestrator
169
+ print(f"[council:budget_guard] write failed: {exc}", file=sys.stderr)
170
+ return False
171
+ _ensure_ledger_file_mode(p)
172
+ return True
@@ -0,0 +1,261 @@
1
+ """Context bundling for council consultations.
2
+
3
+ Takes a raw artefact (free-form prompt, roadmap path, diff range, or
4
+ file set) and produces a `CouncilContext` — a redacted, size-bounded
5
+ text bundle plus a manifest describing exactly what was included.
6
+
7
+ Hard rules:
8
+ - Redaction is fail-closed. If a redaction pattern fires, the line is
9
+ scrubbed *before* the bundle is built.
10
+ - Size guard is fail-loud. > MAX_BUNDLE_BYTES → raises BundleTooLarge,
11
+ never silently truncates (would mislead council members).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ import subprocess
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+
21
+ MAX_BUNDLE_BYTES = 50 * 1024 # 50 KB hard ceiling; user must narrow scope on hit.
22
+
23
+
24
+ class BundleTooLarge(RuntimeError):
25
+ """Raised when the assembled bundle exceeds MAX_BUNDLE_BYTES."""
26
+
27
+
28
+ @dataclass
29
+ class CouncilContext:
30
+ mode: str # one of: prompt, roadmap, diff, files
31
+ text: str
32
+ manifest: list[str] = field(default_factory=list)
33
+ excluded: list[str] = field(default_factory=list)
34
+
35
+
36
+ # ── redaction patterns ───────────────────────────────────────────────────
37
+ # Each pattern is matched line-wise; matching lines are replaced with the
38
+ # placeholder. Order matters — the most specific pattern goes first.
39
+
40
+ _REDACTION_LINE_PATTERNS: list[tuple[re.Pattern[str], str]] = [
41
+ (re.compile(r".*~?/?\.config/agent-config/[^/\s]+\.key.*"),
42
+ "[redacted: agent-config key path]"),
43
+ (re.compile(r"^\s*Authorization:\s.*", re.IGNORECASE),
44
+ "[redacted: Authorization header]"),
45
+ (re.compile(r"(?i).*(api[_-]?key|secret|token|password)\s*[:=].*"),
46
+ "[redacted: secret-like assignment]"),
47
+ (re.compile(r"sk-ant-[A-Za-z0-9_\-]{8,}"), "[redacted: anthropic-key-like token]"),
48
+ (re.compile(r"sk-[A-Za-z0-9_\-]{20,}"), "[redacted: openai-key-like token]"),
49
+ ]
50
+
51
+
52
+ def redact(text: str) -> str:
53
+ """Apply redaction patterns to a multi-line text buffer."""
54
+ out: list[str] = []
55
+ for line in text.splitlines():
56
+ replaced = line
57
+ for pattern, placeholder in _REDACTION_LINE_PATTERNS:
58
+ if pattern.search(replaced):
59
+ replaced = placeholder
60
+ break
61
+ out.append(replaced)
62
+ return "\n".join(out)
63
+
64
+
65
+ def _enforce_size(text: str, mode: str) -> str:
66
+ encoded = text.encode("utf-8")
67
+ if len(encoded) > MAX_BUNDLE_BYTES:
68
+ raise BundleTooLarge(
69
+ f"Bundle for {mode!r} mode is {len(encoded)} bytes "
70
+ f"(> {MAX_BUNDLE_BYTES} hard ceiling). "
71
+ "Narrow the scope (smaller diff, fewer files, shorter prompt)."
72
+ )
73
+ return text
74
+
75
+
76
+ def bundle_prompt(text: str) -> CouncilContext:
77
+ redacted = redact(text)
78
+ return CouncilContext(
79
+ mode="prompt",
80
+ text=_enforce_size(redacted, "prompt"),
81
+ manifest=["<inline prompt>"],
82
+ )
83
+
84
+
85
+ def bundle_roadmap(path: str | Path) -> CouncilContext:
86
+ p = Path(path)
87
+ if not p.exists():
88
+ raise FileNotFoundError(f"Roadmap not found: {p}")
89
+ raw = p.read_text(encoding="utf-8")
90
+ redacted = redact(raw)
91
+ return CouncilContext(
92
+ mode="roadmap",
93
+ text=_enforce_size(redacted, "roadmap"),
94
+ manifest=[str(p)],
95
+ excluded=["<linked contracts/skills not included by default>"],
96
+ )
97
+
98
+
99
+ def bundle_diff(base_ref: str, head_ref: str = "HEAD", cwd: str | Path | None = None) -> CouncilContext:
100
+ cmd = ["git", "diff", f"{base_ref}..{head_ref}"]
101
+ try:
102
+ proc = subprocess.run(
103
+ cmd, cwd=cwd, check=True, capture_output=True, text=True,
104
+ )
105
+ except subprocess.CalledProcessError as exc:
106
+ raise RuntimeError(
107
+ f"git diff {base_ref}..{head_ref} failed: {exc.stderr.strip()}"
108
+ ) from exc
109
+ redacted = redact(proc.stdout)
110
+ return CouncilContext(
111
+ mode="diff",
112
+ text=_enforce_size(redacted, "diff"),
113
+ manifest=[f"git diff {base_ref}..{head_ref}"],
114
+ )
115
+
116
+
117
+ # ── smart diff context (D4) ─────────────────────────────────────────────────
118
+ # Language-agnostic signature detection. Order matters — most specific first.
119
+
120
+ _SIGNATURE_PATTERNS: list[re.Pattern[str]] = [
121
+ re.compile(r"^\s*(?:async\s+)?def\s+\w+\s*\("), # Python
122
+ re.compile(r"^\s*class\s+\w+\b"), # Python / PHP / JS class
123
+ re.compile(r"^\s*(?:public|protected|private|static|abstract|final)\s+(?:static\s+)?function\s+\w+"), # PHP method
124
+ re.compile(r"^\s*function\s+\w+\s*\("), # PHP free function / JS
125
+ re.compile(r"^\s*export\s+(?:default\s+)?(?:async\s+)?function\s+\w+"), # TS/JS export fn
126
+ re.compile(r"^\s*export\s+(?:default\s+)?class\s+\w+"), # TS/JS export class
127
+ re.compile(r"^\s*(?:export\s+)?(?:const|let)\s+\w+\s*=\s*(?:async\s+)?\("), # TS arrow fn
128
+ re.compile(r"^\s*(?:public|private|protected)\s+\w+\s*\("), # TS method
129
+ ]
130
+
131
+ _HUNK_HEADER = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@")
132
+ _DIFF_FILE = re.compile(r"^\+\+\+ b/(.+)$")
133
+
134
+
135
+ def _parse_diff_hunks(diff_text: str) -> list[tuple[str, int]]:
136
+ """Return [(file_path, new_start_line), ...] per hunk in input order."""
137
+ out: list[tuple[str, int]] = []
138
+ current_file: str | None = None
139
+ for line in diff_text.splitlines():
140
+ m = _DIFF_FILE.match(line)
141
+ if m:
142
+ current_file = m.group(1)
143
+ continue
144
+ h = _HUNK_HEADER.match(line)
145
+ if h and current_file and current_file != "/dev/null":
146
+ out.append((current_file, int(h.group(1))))
147
+ return out
148
+
149
+
150
+ def _enclosing_signature(
151
+ file_text: str, target_line: int,
152
+ ) -> tuple[int, str] | None:
153
+ """Walk backwards from `target_line` (1-based) to nearest signature."""
154
+ lines = file_text.splitlines()
155
+ start = min(target_line - 1, len(lines) - 1)
156
+ for idx in range(start, -1, -1):
157
+ line = lines[idx]
158
+ for pat in _SIGNATURE_PATTERNS:
159
+ if pat.match(line):
160
+ return (idx + 1, line.rstrip())
161
+ return None
162
+
163
+
164
+ def bundle_diff_with_context(
165
+ base_ref: str,
166
+ head_ref: str = "HEAD",
167
+ cwd: str | Path | None = None,
168
+ *,
169
+ max_context_bytes: int = 8 * 1024,
170
+ ) -> CouncilContext:
171
+ """Bundle a diff plus the nearest enclosing signatures for each hunk.
172
+
173
+ Appends a `## Surrounding signatures` section after the raw diff.
174
+ Signatures are detected by regex across PY / PHP / JS / TS. Reads
175
+ files from the working tree (correct when `head_ref` == HEAD); if
176
+ a touched file is missing on disk it is silently dropped from the
177
+ context section (the diff itself still shows the change).
178
+
179
+ Hard cap: `max_context_bytes` for the signature section. Combined
180
+ output still goes through `_enforce_size`, so the `BundleTooLarge`
181
+ behaviour is unchanged.
182
+ """
183
+ base = bundle_diff(base_ref, head_ref, cwd=cwd)
184
+ hunks = _parse_diff_hunks(base.text)
185
+ if not hunks:
186
+ return base
187
+
188
+ root = Path(cwd) if cwd else Path(".")
189
+ seen: set[tuple[str, int]] = set() # (file, signature_line)
190
+ by_file: dict[str, list[tuple[int, str]]] = {}
191
+
192
+ for file_path, new_start in hunks:
193
+ target = root / file_path
194
+ try:
195
+ file_text = target.read_text(encoding="utf-8")
196
+ except (OSError, UnicodeDecodeError):
197
+ continue
198
+ sig = _enclosing_signature(file_text, new_start)
199
+ if sig is None:
200
+ continue
201
+ key = (file_path, sig[0])
202
+ if key in seen:
203
+ continue
204
+ seen.add(key)
205
+ by_file.setdefault(file_path, []).append(sig)
206
+
207
+ if not by_file:
208
+ return base
209
+
210
+ out_lines: list[str] = ["", "## Surrounding signatures", ""]
211
+ truncated = False
212
+ used = 0
213
+ for file_path, sigs in by_file.items():
214
+ header = f"### {file_path}"
215
+ sig_block = "\n".join(f" L{ln}: {text}" for ln, text in sorted(sigs))
216
+ chunk = f"{header}\n\n{sig_block}\n\n"
217
+ if used + len(chunk.encode("utf-8")) > max_context_bytes:
218
+ truncated = True
219
+ break
220
+ out_lines.append(header)
221
+ out_lines.append("")
222
+ out_lines.append(sig_block)
223
+ out_lines.append("")
224
+ used += len(chunk.encode("utf-8"))
225
+
226
+ if truncated:
227
+ out_lines.append(f"[truncated: signature section capped at {max_context_bytes} bytes]")
228
+
229
+ combined = base.text + "\n" + "\n".join(out_lines)
230
+ redacted = redact(combined)
231
+ return CouncilContext(
232
+ mode="diff",
233
+ text=_enforce_size(redacted, "diff"),
234
+ manifest=base.manifest + [f"+ surrounding signatures for {len(by_file)} file(s)"],
235
+ )
236
+
237
+
238
+ def bundle_files(paths: list[str | Path]) -> CouncilContext:
239
+ parts: list[str] = []
240
+ manifest: list[str] = []
241
+ excluded: list[str] = []
242
+ for raw_path in paths:
243
+ p = Path(raw_path)
244
+ if not p.exists():
245
+ excluded.append(f"{p} (not found)")
246
+ continue
247
+ try:
248
+ content = p.read_text(encoding="utf-8")
249
+ except (OSError, UnicodeDecodeError) as exc:
250
+ excluded.append(f"{p} ({type(exc).__name__})")
251
+ continue
252
+ parts.append(f"### {p}\n\n{content}\n")
253
+ manifest.append(str(p))
254
+ bundled = "\n".join(parts)
255
+ redacted = redact(bundled)
256
+ return CouncilContext(
257
+ mode="files",
258
+ text=_enforce_size(redacted, "files"),
259
+ manifest=manifest,
260
+ excluded=excluded,
261
+ )