@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.
- package/.agent-src/commands/agent-handoff.md +1 -1
- package/.agent-src/commands/bug-fix.md +3 -3
- package/.agent-src/commands/bug-investigate.md +2 -2
- package/.agent-src/commands/chat-history-checkpoint.md +3 -3
- package/.agent-src/commands/chat-history-clear.md +2 -2
- package/.agent-src/commands/chat-history-resume.md +2 -2
- package/.agent-src/commands/chat-history.md +3 -3
- package/.agent-src/commands/check-current-md.md +44 -33
- package/.agent-src/commands/commit-in-chunks.md +43 -23
- package/.agent-src/commands/compress.md +34 -2
- package/.agent-src/commands/council-design.md +96 -0
- package/.agent-src/commands/council-optimize.md +115 -0
- package/.agent-src/commands/council-pr.md +123 -0
- package/.agent-src/commands/council.md +219 -0
- package/.agent-src/commands/create-pr.md +23 -0
- package/.agent-src/commands/do-and-judge.md +3 -3
- package/.agent-src/commands/do-in-steps.md +4 -4
- package/.agent-src/commands/e2e-heal.md +1 -1
- package/.agent-src/commands/e2e-plan.md +1 -1
- package/.agent-src/commands/feature-dev.md +8 -0
- package/.agent-src/commands/feature-explore.md +6 -1
- package/.agent-src/commands/feature-plan.md +33 -2
- package/.agent-src/commands/feature-refactor.md +5 -0
- package/.agent-src/commands/feature-roadmap.md +8 -3
- package/.agent-src/commands/feature.md +58 -0
- package/.agent-src/commands/fix-ci.md +5 -0
- package/.agent-src/commands/fix-portability.md +7 -2
- package/.agent-src/commands/fix-pr-bot-comments.md +5 -0
- package/.agent-src/commands/fix-pr-comments.md +5 -0
- package/.agent-src/commands/fix-pr-developer-comments.md +5 -0
- package/.agent-src/commands/fix-references.md +5 -0
- package/.agent-src/commands/fix-seeder.md +5 -0
- package/.agent-src/commands/fix.md +60 -0
- package/.agent-src/commands/jira-ticket.md +1 -1
- package/.agent-src/commands/judge.md +1 -1
- package/.agent-src/commands/memory-add.md +3 -3
- package/.agent-src/commands/memory-full.md +2 -2
- package/.agent-src/commands/memory-promote.md +2 -2
- package/.agent-src/commands/mode.md +5 -5
- package/.agent-src/commands/onboard.md +17 -8
- package/.agent-src/commands/optimize-agents.md +6 -1
- package/.agent-src/commands/optimize-augmentignore.md +14 -0
- package/.agent-src/commands/optimize-rtk-filters.md +5 -0
- package/.agent-src/commands/optimize-skills.md +6 -1
- package/.agent-src/commands/optimize.md +54 -0
- package/.agent-src/commands/propose-memory.md +2 -2
- package/.agent-src/commands/refine-ticket.md +9 -7
- package/.agent-src/commands/review-changes.md +61 -9
- package/.agent-src/commands/review-routing.md +1 -1
- package/.agent-src/commands/roadmap-create.md +42 -4
- package/.agent-src/commands/roadmap-execute.md +9 -7
- package/.agent-src/commands/set-cost-profile.md +11 -3
- package/.agent-src/commands/sync-agent-settings.md +11 -2
- package/.agent-src/commands/tests-create.md +1 -1
- package/.agent-src/commands/tests-execute.md +2 -3
- package/.agent-src/commands/upstream-contribute.md +1 -1
- package/.agent-src/contexts/authority/commit-mechanics.md +57 -0
- package/.agent-src/contexts/authority/destructive-mechanics.md +66 -0
- package/.agent-src/contexts/authority/scope-mechanics.md +87 -0
- package/.agent-src/contexts/execution/autonomy-detection.md +54 -0
- package/.agent-src/contexts/execution/autonomy-examples.md +90 -0
- package/.agent-src/contexts/execution/autonomy-mechanics.md +29 -0
- package/.agent-src/contexts/execution/verification-mechanics.md +80 -0
- package/.agent-src/personas/README.md +1 -1
- package/.agent-src/rules/agent-authority.md +24 -0
- package/.agent-src/rules/architecture.md +1 -1
- package/.agent-src/rules/artifact-drafting-protocol.md +1 -1
- package/.agent-src/rules/artifact-engagement-recording.md +2 -2
- package/.agent-src/rules/ask-when-uncertain.md +1 -1
- package/.agent-src/rules/augment-portability.md +56 -37
- package/.agent-src/rules/autonomous-execution.md +78 -114
- package/.agent-src/rules/capture-learnings.md +1 -1
- package/.agent-src/rules/chat-history-cadence.md +109 -0
- package/.agent-src/rules/chat-history-ownership.md +123 -0
- package/.agent-src/rules/chat-history-visibility.md +96 -0
- package/.agent-src/rules/cli-output-handling.md +1 -1
- package/.agent-src/rules/{command-suggestion.md → command-suggestion-policy.md} +10 -9
- package/.agent-src/rules/commit-conventions.md +1 -1
- package/.agent-src/rules/commit-policy.md +43 -61
- package/.agent-src/rules/context-hygiene.md +3 -3
- package/.agent-src/rules/direct-answers.md +2 -2
- package/.agent-src/rules/docs-sync.md +1 -1
- package/.agent-src/rules/e2e-testing.md +1 -1
- package/.agent-src/rules/guidelines.md +4 -4
- package/.agent-src/rules/improve-before-implement.md +2 -2
- package/.agent-src/rules/language-and-tone.md +41 -96
- package/.agent-src/rules/minimal-safe-diff.md +3 -3
- package/.agent-src/rules/model-recommendation.md +4 -4
- package/.agent-src/rules/no-cheap-questions.md +89 -0
- package/.agent-src/rules/non-destructive-by-default.md +25 -59
- package/.agent-src/rules/onboarding-gate.md +5 -5
- package/.agent-src/rules/review-routing-awareness.md +9 -9
- package/.agent-src/rules/roadmap-progress-sync.md +132 -80
- package/.agent-src/rules/role-mode-adherence.md +3 -3
- package/.agent-src/rules/scope-control.md +65 -46
- package/.agent-src/rules/security-sensitive-stop.md +2 -2
- package/.agent-src/rules/size-enforcement.md +3 -2
- package/.agent-src/rules/think-before-action.md +5 -5
- package/.agent-src/rules/token-efficiency.md +4 -4
- package/.agent-src/rules/{ui-audit-before-build.md → ui-audit-gate.md} +3 -3
- package/.agent-src/rules/user-interaction.md +31 -7
- package/.agent-src/rules/verify-before-complete.md +12 -67
- package/.agent-src/scripts/update_roadmap_progress.py +65 -8
- package/.agent-src/skills/ai-council/SKILL.md +333 -0
- package/.agent-src/skills/api-endpoint/SKILL.md +2 -2
- package/.agent-src/skills/blade-ui/SKILL.md +30 -11
- package/.agent-src/skills/blast-radius-analyzer/SKILL.md +1 -1
- package/.agent-src/skills/bug-analyzer/SKILL.md +1 -1
- package/.agent-src/skills/command-routing/SKILL.md +1 -1
- package/.agent-src/skills/command-writing/SKILL.md +16 -5
- package/.agent-src/skills/conventional-commits-writing/SKILL.md +1 -1
- package/.agent-src/skills/copilot-agents-optimization/SKILL.md +2 -2
- package/.agent-src/skills/developer-like-execution/SKILL.md +2 -2
- package/.agent-src/skills/existing-ui-audit/SKILL.md +24 -9
- package/.agent-src/skills/fe-design/SKILL.md +20 -15
- package/.agent-src/skills/file-editor/SKILL.md +9 -0
- package/.agent-src/skills/flux/SKILL.md +1 -1
- package/.agent-src/skills/git-workflow/SKILL.md +1 -1
- package/.agent-src/skills/guideline-writing/SKILL.md +11 -11
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +4 -4
- package/.agent-src/skills/livewire/SKILL.md +27 -8
- package/.agent-src/skills/override-management/SKILL.md +2 -2
- package/.agent-src/skills/php-coder/SKILL.md +1 -1
- package/.agent-src/skills/playwright-testing/SKILL.md +2 -2
- package/.agent-src/skills/readme-reviewer/SKILL.md +1 -1
- package/.agent-src/skills/readme-writing/SKILL.md +1 -1
- package/.agent-src/skills/readme-writing-package/SKILL.md +1 -1
- package/.agent-src/skills/receiving-code-review/SKILL.md +1 -1
- package/.agent-src/skills/refine-ticket/SKILL.md +30 -24
- package/.agent-src/skills/review-routing/SKILL.md +2 -2
- package/.agent-src/skills/roadmap-management/SKILL.md +22 -16
- package/.agent-src/skills/rule-writing/SKILL.md +1 -1
- package/.agent-src/skills/skill-reviewer/SKILL.md +1 -1
- package/.agent-src/skills/skill-writing/SKILL.md +6 -6
- package/.agent-src/skills/subagent-orchestration/SKILL.md +1 -0
- package/.agent-src/skills/systematic-debugging/SKILL.md +1 -1
- package/.agent-src/skills/upstream-contribute/SKILL.md +3 -3
- package/.agent-src/skills/validate-feature-fit/SKILL.md +2 -2
- package/.agent-src/skills/{verify-before-complete → verify-completion-evidence}/SKILL.md +2 -2
- package/.agent-src/templates/agent-settings.md +9 -9
- package/.agent-src/templates/contexts/auth-model.md +1 -1
- package/.agent-src/templates/roadmaps.md +9 -8
- package/.agent-src/templates/scripts/README.md +2 -2
- package/.agent-src/templates/scripts/memory_lookup.py +1 -1
- package/.agent-src/templates/scripts/telemetry/aggregator.py +16 -1
- package/.agent-src/templates/scripts/telemetry/engagement.py +59 -0
- package/.agent-src/templates/scripts/telemetry/report_renderer.py +28 -1
- package/.agent-src/templates/scripts/telemetry_record.py +14 -1
- package/.agent-src/templates/scripts/work_engine/__init__.py +2 -2
- package/.agent-src/templates/scripts/work_engine/cli.py +64 -461
- package/.agent-src/templates/scripts/work_engine/cli_args.py +116 -0
- package/.agent-src/templates/scripts/work_engine/delivery_state.py +3 -3
- package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/implement.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/memory.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/plan.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/report.py +1 -1
- package/.agent-src/templates/scripts/work_engine/dispatcher.py +1 -1
- package/.agent-src/templates/scripts/work_engine/emitters.py +43 -0
- package/.agent-src/templates/scripts/work_engine/errors.py +19 -0
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +76 -0
- package/.agent-src/templates/scripts/work_engine/input_builders.py +163 -0
- package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +34 -2
- package/.agent-src/templates/scripts/work_engine/persona_policy.py +1 -1
- package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +1 -1
- package/.agent-src/templates/scripts/work_engine/state_io.py +202 -0
- package/.claude-plugin/marketplace.json +10 -2
- package/AGENTS.md +16 -12
- package/CHANGELOG.md +206 -9
- package/README.md +51 -52
- package/config/agent-settings.template.yml +58 -1
- package/config/gitignore-block.txt +3 -0
- package/docs/MIGRATION.md +122 -0
- package/docs/architecture.md +83 -34
- package/docs/catalog.md +331 -0
- package/docs/contracts/STABILITY.md +134 -0
- package/docs/contracts/adr-chat-history-split.md +132 -0
- package/docs/contracts/adr-command-suggestion.md +146 -0
- package/docs/contracts/adr-implement-ticket-runtime.md +122 -0
- package/docs/contracts/adr-product-ui-track.md +384 -0
- package/docs/contracts/adr-prompt-driven-execution.md +187 -0
- package/docs/contracts/agent-memory-contract.md +149 -0
- package/docs/contracts/artifact-engagement-flow.md +262 -0
- package/docs/contracts/command-clusters.md +126 -0
- package/docs/contracts/command-suggestion-flow.md +148 -0
- package/docs/contracts/implement-ticket-flow.md +628 -0
- package/docs/contracts/linear-ai-rules-inclusion.md +143 -0
- package/docs/contracts/linear-ai-three-layers.md +131 -0
- package/docs/contracts/load-context-schema.md +186 -0
- package/docs/contracts/rule-interactions.md +107 -0
- package/docs/contracts/rule-interactions.yml +238 -0
- package/docs/contracts/rule-priority-hierarchy.md +87 -0
- package/docs/contracts/ui-stack-extension.md +236 -0
- package/docs/contracts/ui-track-flow.md +338 -0
- package/docs/customization.md +14 -0
- package/docs/end-to-end-walkthroughs.md +165 -0
- package/docs/getting-started.md +27 -9
- package/docs/github-topics.md +12 -3
- package/docs/guidelines/agent-infra/language-and-tone-examples.md +79 -0
- package/{.agent-src → docs}/guidelines/docs/readme-size-and-splitting.md +26 -25
- package/docs/guidelines/php/git.md +164 -0
- package/docs/installation.md +42 -6
- package/docs/migrations/commands-1.15.0.md +112 -0
- package/docs/showcase.md +9 -4
- package/docs/skills-catalog.md +14 -8
- package/docs/ui-track-mental-model.md +121 -0
- package/llms.txt +13 -7
- package/package.json +1 -1
- package/scripts/agent-config +23 -0
- package/scripts/ai_council/__init__.py +39 -0
- package/scripts/ai_council/_default_prices.py +41 -0
- package/scripts/ai_council/_one_off_rebalancing_audit.py +149 -0
- package/scripts/ai_council/_one_off_roundtrip.py +106 -0
- package/scripts/ai_council/budget_guard.py +172 -0
- package/scripts/ai_council/bundler.py +261 -0
- package/scripts/ai_council/clients.py +381 -0
- package/scripts/ai_council/modes.py +127 -0
- package/scripts/ai_council/orchestrator.py +350 -0
- package/scripts/ai_council/pricing.py +213 -0
- package/scripts/ai_council/project_context.py +159 -0
- package/scripts/ai_council/prompts.py +232 -0
- package/scripts/ai_council/session.py +144 -0
- package/scripts/build_linear_digest.py +4 -4
- package/scripts/check_always_budget.py +126 -0
- package/scripts/check_augmentignore.py +69 -0
- package/scripts/check_command_count_messaging.py +120 -0
- package/scripts/check_portability.py +57 -0
- package/scripts/check_public_catalog_links.py +122 -0
- package/scripts/check_public_links.py +185 -0
- package/scripts/check_references.py +5 -1
- package/scripts/check_roadmap_trackable.py +111 -0
- package/scripts/command_suggester/cooldown.py +1 -1
- package/scripts/generate_index.py +266 -0
- package/scripts/install_anthropic_key.sh +5 -0
- package/scripts/install_openai_key.sh +106 -0
- package/scripts/lint_load_context.py +163 -0
- package/scripts/lint_no_new_atomic_commands.py +179 -0
- package/scripts/lint_rule_interactions.py +149 -0
- package/scripts/memory_lookup.py +1 -1
- package/scripts/release.py +297 -64
- package/scripts/schemas/command.schema.json +20 -0
- package/scripts/schemas/rule.schema.json +10 -0
- package/scripts/skill_linter.py +26 -4
- package/scripts/sync_agent_settings.py +1 -1
- package/scripts/update_counts.py +19 -4
- package/scripts/update_prices.py +124 -0
- package/.agent-src/guidelines/php/git.md +0 -96
- package/.agent-src/rules/chat-history.md +0 -200
- /package/.agent-src/rules/{slash-commands.md → slash-command-routing-policy.md} +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/agent-interaction-and-decision-quality.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/break-glass-usage.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/developer-judgment.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/engineering-memory-data-format.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/layered-settings.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/memory-access.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/naming.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/output-patterns.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/review-routing-data-format.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/role-contracts.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/role-mode-router.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/runtime-layer.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/self-improvement-pipeline.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/size-and-scope.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/tool-integration.md +0 -0
- /package/{.agent-src → docs}/guidelines/e2e/playwright.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/api-design.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/artisan-commands.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/blade-ui.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/controllers.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/database.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/eloquent.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/flux.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/general.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/jobs.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/livewire.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/logging.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/naming.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/dependency-injection.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/dtos.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/events.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/factory.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/pipelines.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/policies.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/repositories.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/service-layer.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/strategy.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/performance.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/resources.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/security.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/sql.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/validations.md +0 -0
- /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
|
+
)
|