@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,159 @@
|
|
|
1
|
+
"""Lightweight project-context detector for the council handoff preamble.
|
|
2
|
+
|
|
3
|
+
Council members do better critique when they know what the project IS,
|
|
4
|
+
not just what the artefact looks like. This module reads the bare
|
|
5
|
+
minimum from the repo root — `composer.json`, `package.json`, root
|
|
6
|
+
`README.md` — and returns a neutral `ProjectContext`. All fields are
|
|
7
|
+
optional; missing data is `None` and the preamble silently omits the
|
|
8
|
+
line.
|
|
9
|
+
|
|
10
|
+
Iron law of neutrality (`ai-council` skill): nothing here may carry
|
|
11
|
+
host-agent identity, prior reasoning, or framing. Manifest fields and
|
|
12
|
+
README prose only.
|
|
13
|
+
|
|
14
|
+
Truncation strategy (locked by council review, 2026-05-02): the
|
|
15
|
+
``repo_purpose`` field is capped at ``REPO_PURPOSE_MAX_CHARS`` by
|
|
16
|
+
stopping at the **last full sentence ≤ 400 chars**, with an ellipsis
|
|
17
|
+
when truncation occurred. We never cut mid-sentence — a half-sentence
|
|
18
|
+
reads as broken and adds noise to the council preamble.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
REPO_PURPOSE_MAX_CHARS = 400
|
|
29
|
+
_HEADING_RE = re.compile(r"^\s*#")
|
|
30
|
+
_BADGE_RE = re.compile(r"^\s*(\[!\[|!\[|<).*")
|
|
31
|
+
_HTML_RE = re.compile(r"<[^>]+>")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ProjectContext:
|
|
36
|
+
"""Neutral project description for the council handoff preamble."""
|
|
37
|
+
|
|
38
|
+
name: str | None = None
|
|
39
|
+
stack: str | None = None
|
|
40
|
+
repo_purpose: str | None = None
|
|
41
|
+
|
|
42
|
+
def is_empty(self) -> bool:
|
|
43
|
+
return self.name is None and self.stack is None and self.repo_purpose is None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _read_json(path: Path) -> dict | None:
|
|
47
|
+
if not path.exists():
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
51
|
+
except (json.JSONDecodeError, OSError):
|
|
52
|
+
return None
|
|
53
|
+
return data if isinstance(data, dict) else None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _name_from(composer: dict | None, package: dict | None, root: Path) -> str | None:
|
|
57
|
+
for src in (composer, package):
|
|
58
|
+
if src and isinstance(src.get("name"), str) and src["name"].strip():
|
|
59
|
+
return src["name"].strip()
|
|
60
|
+
# Fall back to the directory name; useful for repos without manifests.
|
|
61
|
+
try:
|
|
62
|
+
return root.resolve().name or None
|
|
63
|
+
except OSError:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _stack_from(composer: dict | None, package: dict | None) -> str | None:
|
|
68
|
+
parts: list[str] = []
|
|
69
|
+
if composer:
|
|
70
|
+
php_v = (composer.get("require") or {}).get("php")
|
|
71
|
+
if isinstance(php_v, str):
|
|
72
|
+
parts.append(f"PHP {php_v}")
|
|
73
|
+
# Detect well-known frameworks without claiming the project IS one.
|
|
74
|
+
require = {**(composer.get("require") or {}), **(composer.get("require-dev") or {})}
|
|
75
|
+
for needle, label in (
|
|
76
|
+
("laravel/framework", "Laravel"),
|
|
77
|
+
("symfony/framework-bundle", "Symfony"),
|
|
78
|
+
("laminas/laminas-mvc", "Laminas"),
|
|
79
|
+
):
|
|
80
|
+
if needle in require:
|
|
81
|
+
parts.append(label)
|
|
82
|
+
break
|
|
83
|
+
if package:
|
|
84
|
+
engines = package.get("engines") or {}
|
|
85
|
+
if isinstance(engines, dict) and isinstance(engines.get("node"), str):
|
|
86
|
+
parts.append(f"Node {engines['node']}")
|
|
87
|
+
deps = {**(package.get("dependencies") or {}), **(package.get("devDependencies") or {})}
|
|
88
|
+
for needle, label in (
|
|
89
|
+
("next", "Next.js"),
|
|
90
|
+
("react", "React"),
|
|
91
|
+
("vue", "Vue"),
|
|
92
|
+
("@angular/core", "Angular"),
|
|
93
|
+
):
|
|
94
|
+
if needle in deps:
|
|
95
|
+
parts.append(label)
|
|
96
|
+
break
|
|
97
|
+
if not parts:
|
|
98
|
+
return None
|
|
99
|
+
return " · ".join(parts)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _purpose_from_readme(path: Path) -> str | None:
|
|
103
|
+
if not path.exists():
|
|
104
|
+
return None
|
|
105
|
+
try:
|
|
106
|
+
text = path.read_text(encoding="utf-8")
|
|
107
|
+
except OSError:
|
|
108
|
+
return None
|
|
109
|
+
paragraph: list[str] = []
|
|
110
|
+
for raw in text.splitlines():
|
|
111
|
+
line = raw.rstrip()
|
|
112
|
+
stripped = line.strip()
|
|
113
|
+
if not stripped:
|
|
114
|
+
if paragraph:
|
|
115
|
+
break
|
|
116
|
+
continue
|
|
117
|
+
if _HEADING_RE.match(stripped) or _BADGE_RE.match(stripped):
|
|
118
|
+
if paragraph:
|
|
119
|
+
break
|
|
120
|
+
continue
|
|
121
|
+
paragraph.append(stripped)
|
|
122
|
+
if not paragraph:
|
|
123
|
+
return None
|
|
124
|
+
joined = " ".join(paragraph)
|
|
125
|
+
joined = _HTML_RE.sub("", joined).strip()
|
|
126
|
+
if not joined:
|
|
127
|
+
return None
|
|
128
|
+
if len(joined) > REPO_PURPOSE_MAX_CHARS:
|
|
129
|
+
joined = _truncate_at_sentence(joined, REPO_PURPOSE_MAX_CHARS)
|
|
130
|
+
return joined
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _truncate_at_sentence(text: str, limit: int) -> str:
|
|
134
|
+
"""Truncate at the last full sentence ≤ limit chars; append an ellipsis.
|
|
135
|
+
|
|
136
|
+
Total return length is always ≤ ``limit`` (ellipsis included).
|
|
137
|
+
"""
|
|
138
|
+
budget = max(1, limit - 2)
|
|
139
|
+
head = text[:budget]
|
|
140
|
+
cut = max(head.rfind(". "), head.rfind("! "), head.rfind("? "))
|
|
141
|
+
if cut >= 0:
|
|
142
|
+
return head[: cut + 1].rstrip() + " …"
|
|
143
|
+
return head.rstrip() + " …"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def detect_project_context(root: Path | None = None) -> ProjectContext:
|
|
147
|
+
"""Return a `ProjectContext` for `root` (default: cwd).
|
|
148
|
+
|
|
149
|
+
Always returns — never raises. Missing manifest files / README → the
|
|
150
|
+
matching field is `None`, and `handoff_preamble()` will omit the line.
|
|
151
|
+
"""
|
|
152
|
+
root = (root or Path.cwd()).resolve()
|
|
153
|
+
composer = _read_json(root / "composer.json")
|
|
154
|
+
package = _read_json(root / "package.json")
|
|
155
|
+
return ProjectContext(
|
|
156
|
+
name=_name_from(composer, package, root),
|
|
157
|
+
stack=_stack_from(composer, package),
|
|
158
|
+
repo_purpose=_purpose_from_readme(root / "README.md"),
|
|
159
|
+
)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Neutrality system prompts for the council.
|
|
2
|
+
|
|
3
|
+
Council members must NOT see the host agent's reasoning, internal
|
|
4
|
+
state, or framing language. Each prompt asks for an independent
|
|
5
|
+
critique on the artefact's own merits.
|
|
6
|
+
|
|
7
|
+
Anti-patterns guarded against in tests (test_prompts.py):
|
|
8
|
+
- No leak of host-agent identity ("Augment", "Claude Code", etc.).
|
|
9
|
+
- No "the agent thinks X" framing.
|
|
10
|
+
- No instructions that bias toward agreement.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from scripts.ai_council.project_context import ProjectContext
|
|
16
|
+
|
|
17
|
+
NEUTRALITY_PREAMBLE = """\
|
|
18
|
+
You are an independent reviewer. You have NOT seen any prior reasoning,
|
|
19
|
+
agent output, or commentary on the artefact below. Critique it on its
|
|
20
|
+
own merits. Disagree if warranted. Cite specific lines or sections.
|
|
21
|
+
Do not assume the artefact is correct just because it was sent to you.
|
|
22
|
+
""".strip()
|
|
23
|
+
|
|
24
|
+
# Host-agent identity strings that must never leak into a council member's
|
|
25
|
+
# view. Lines containing any of these (case-insensitive substring) are
|
|
26
|
+
# dropped before assembly. See `ai-council` skill § Neutrality.
|
|
27
|
+
HOST_AGENT_IDENTITY_PATTERNS = (
|
|
28
|
+
"augment",
|
|
29
|
+
"claude code",
|
|
30
|
+
"cursor agent",
|
|
31
|
+
"cursor ide",
|
|
32
|
+
"cline",
|
|
33
|
+
"windsurf",
|
|
34
|
+
"copilot agent",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Per-mode addenda — appended after the preamble.
|
|
38
|
+
|
|
39
|
+
PROMPT_MODE = """\
|
|
40
|
+
The artefact is a free-form question or proposal from a developer.
|
|
41
|
+
Respond with:
|
|
42
|
+
1. Your honest assessment (agree / disagree / mixed).
|
|
43
|
+
2. The single strongest argument for your position.
|
|
44
|
+
3. The single strongest counter-argument the developer should consider.
|
|
45
|
+
4. Concrete next steps if you agree, or concrete alternatives if you disagree.
|
|
46
|
+
""".strip()
|
|
47
|
+
|
|
48
|
+
ROADMAP_MODE = """\
|
|
49
|
+
The artefact is a proposed implementation roadmap. Critique it as if
|
|
50
|
+
you were a senior engineer asked to greenlight it. Focus on:
|
|
51
|
+
1. Hidden coupling between phases that the roadmap glosses over.
|
|
52
|
+
2. Steps that are too coarse to verify ("implement X" vs "X with Y test").
|
|
53
|
+
3. Missing rollback or kill-switch criteria.
|
|
54
|
+
4. Sequencing risks — does step N really not block step N+1?
|
|
55
|
+
5. Open questions disguised as decisions, or vice versa.
|
|
56
|
+
""".strip()
|
|
57
|
+
|
|
58
|
+
DIFF_MODE = """\
|
|
59
|
+
The artefact is a code diff. Review it for:
|
|
60
|
+
1. Correctness — bugs, off-by-one, null-safety, type drift.
|
|
61
|
+
2. Security — injection, secrets, unsafe deserialization, authZ gaps.
|
|
62
|
+
3. Test coverage — uncovered branches, missing regression tests.
|
|
63
|
+
4. Maintainability — surprise dependencies, naming drift, dead code.
|
|
64
|
+
End with: APPROVE / REQUEST_CHANGES / REJECT and one sentence why.
|
|
65
|
+
""".strip()
|
|
66
|
+
|
|
67
|
+
FILES_MODE = """\
|
|
68
|
+
The artefact is a set of source files for an architectural review.
|
|
69
|
+
Map out:
|
|
70
|
+
1. The boundaries you see (modules, layers, trust zones).
|
|
71
|
+
2. The strongest design decision present.
|
|
72
|
+
3. The weakest design decision present.
|
|
73
|
+
4. The single change that would most reduce future maintenance cost.
|
|
74
|
+
""".strip()
|
|
75
|
+
|
|
76
|
+
# Specialised modes — used by /council-pr, /council-design,
|
|
77
|
+
# /council-optimize. Selected via `mode_override=` in `/council` so the
|
|
78
|
+
# base modes (`prompt`, `roadmap`, `diff`, `files`) keep their v2 byte
|
|
79
|
+
# shape for back-compat with existing callers.
|
|
80
|
+
|
|
81
|
+
PR_MODE = """\
|
|
82
|
+
The artefact is a code diff from a pull request. Review with both a
|
|
83
|
+
correctness lens AND a shipping-risk lens:
|
|
84
|
+
1. Correctness — bugs, off-by-one, null-safety, type drift.
|
|
85
|
+
2. Security — injection, secrets, unsafe deserialization, authZ gaps.
|
|
86
|
+
3. Test coverage — uncovered branches, missing regression tests.
|
|
87
|
+
4. Shipping risk — does this PR mix concerns that should be split?
|
|
88
|
+
Is the blast radius bigger than the title implies?
|
|
89
|
+
5. Reviewer fatigue — is anything in the diff that a tired reviewer
|
|
90
|
+
would rubber-stamp but should not?
|
|
91
|
+
End with: APPROVE / REQUEST_CHANGES / REJECT, one sentence why, and
|
|
92
|
+
the single highest-leverage change the PR author should make before
|
|
93
|
+
merge.
|
|
94
|
+
""".strip()
|
|
95
|
+
|
|
96
|
+
DESIGN_MODE = """\
|
|
97
|
+
The artefact is a design document, ADR, or architecture proposal.
|
|
98
|
+
Critique it as if you were greenlighting it as a senior engineer.
|
|
99
|
+
Focus on:
|
|
100
|
+
1. Trust boundaries and module coupling the design glosses over.
|
|
101
|
+
2. Rollback / kill-switch criteria the design omits.
|
|
102
|
+
3. Sequencing risk — does step N really not block step N+1?
|
|
103
|
+
4. Open questions disguised as decisions, or decisions disguised as
|
|
104
|
+
open questions.
|
|
105
|
+
5. The single architectural call you would push back on the hardest,
|
|
106
|
+
and what evidence would change your mind.
|
|
107
|
+
""".strip()
|
|
108
|
+
|
|
109
|
+
OPTIMIZE_MODE = """\
|
|
110
|
+
The artefact is an optimization target — code, a query, a profile,
|
|
111
|
+
or an existing optimization report. Produce ranked, evidence-based
|
|
112
|
+
suggestions for the metric stated in the user's original ask. You
|
|
113
|
+
MUST:
|
|
114
|
+
1. Rank suggestions by expected impact on the stated metric, not by
|
|
115
|
+
effort or cleverness.
|
|
116
|
+
2. Cite the evidence (line, query plan, profile entry) for every
|
|
117
|
+
suggestion. No hand-wave "this is probably slow".
|
|
118
|
+
3. State at least one suggestion you explicitly REJECT as
|
|
119
|
+
low-leverage, so the user does not over-engineer.
|
|
120
|
+
4. Mark at least one suggestion as hypothesis (requires measurement
|
|
121
|
+
before committing) versus confirmed (already supported by the
|
|
122
|
+
evidence in the artefact).
|
|
123
|
+
""".strip()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
_MODE_TABLE = {
|
|
127
|
+
"prompt": PROMPT_MODE,
|
|
128
|
+
"roadmap": ROADMAP_MODE,
|
|
129
|
+
"diff": DIFF_MODE,
|
|
130
|
+
"files": FILES_MODE,
|
|
131
|
+
"pr": PR_MODE,
|
|
132
|
+
"design": DESIGN_MODE,
|
|
133
|
+
"optimize": OPTIMIZE_MODE,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _strip_host_identity(text: str) -> str:
|
|
138
|
+
"""Drop any *whole line* containing a host-agent identity substring.
|
|
139
|
+
|
|
140
|
+
Strategy (locked by council review, 2026-05-02): a line is dropped
|
|
141
|
+
in full as soon as any host-identity needle (Augment / Claude Code
|
|
142
|
+
/ Cursor / Cline / Windsurf, etc.) appears anywhere on it. We err
|
|
143
|
+
toward false-positive — slightly less context — over false-negative
|
|
144
|
+
— a neutrality leak. Substring-only stripping was rejected because
|
|
145
|
+
it can leave dangling clauses that still hint at the host.
|
|
146
|
+
"""
|
|
147
|
+
if not text:
|
|
148
|
+
return text
|
|
149
|
+
kept: list[str] = []
|
|
150
|
+
for line in text.splitlines():
|
|
151
|
+
low = line.lower()
|
|
152
|
+
if any(needle in low for needle in HOST_AGENT_IDENTITY_PATTERNS):
|
|
153
|
+
continue
|
|
154
|
+
kept.append(line)
|
|
155
|
+
return "\n".join(kept)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def handoff_preamble(
|
|
159
|
+
project: ProjectContext | None,
|
|
160
|
+
original_ask: str,
|
|
161
|
+
) -> str:
|
|
162
|
+
"""Neutral context-handoff for council members.
|
|
163
|
+
|
|
164
|
+
Layout (any block omitted when its inputs are empty):
|
|
165
|
+
|
|
166
|
+
Project: <name>
|
|
167
|
+
Stack: <stack>
|
|
168
|
+
Purpose: <repo_purpose>
|
|
169
|
+
|
|
170
|
+
The user originally asked:
|
|
171
|
+
> <original_ask>
|
|
172
|
+
|
|
173
|
+
<NEUTRALITY_PREAMBLE>
|
|
174
|
+
|
|
175
|
+
Iron Law of Neutrality (`ai-council` skill): lines containing a
|
|
176
|
+
host-agent identity string (Augment, Claude Code, Cursor, Cline,
|
|
177
|
+
Windsurf, Copilot agent) are dropped from `project` fields and
|
|
178
|
+
`original_ask` BEFORE assembly so they cannot leak.
|
|
179
|
+
|
|
180
|
+
`project=None` and/or `original_ask=""` collapses the output to
|
|
181
|
+
`NEUTRALITY_PREAMBLE` alone (back-compat with v1 callers).
|
|
182
|
+
"""
|
|
183
|
+
blocks: list[str] = []
|
|
184
|
+
|
|
185
|
+
if project is not None and not project.is_empty():
|
|
186
|
+
ctx_lines: list[str] = []
|
|
187
|
+
if project.name:
|
|
188
|
+
ctx_lines.append(f"Project: {project.name}")
|
|
189
|
+
if project.stack:
|
|
190
|
+
ctx_lines.append(f"Stack: {project.stack}")
|
|
191
|
+
if project.repo_purpose:
|
|
192
|
+
ctx_lines.append(f"Purpose: {project.repo_purpose}")
|
|
193
|
+
ctx = _strip_host_identity("\n".join(ctx_lines)).strip()
|
|
194
|
+
if ctx:
|
|
195
|
+
blocks.append(ctx)
|
|
196
|
+
|
|
197
|
+
cleaned_ask = _strip_host_identity(original_ask or "").strip()
|
|
198
|
+
if cleaned_ask:
|
|
199
|
+
quoted = "\n".join(f"> {ln}" for ln in cleaned_ask.splitlines())
|
|
200
|
+
blocks.append(f"The user originally asked:\n{quoted}")
|
|
201
|
+
|
|
202
|
+
blocks.append(NEUTRALITY_PREAMBLE)
|
|
203
|
+
return "\n\n".join(blocks)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def system_prompt_for(
|
|
207
|
+
mode: str,
|
|
208
|
+
*,
|
|
209
|
+
project: ProjectContext | None = None,
|
|
210
|
+
original_ask: str = "",
|
|
211
|
+
) -> str:
|
|
212
|
+
"""Build the full system prompt for one of the four input modes.
|
|
213
|
+
|
|
214
|
+
Raises ValueError on an unknown mode — callers must use one of
|
|
215
|
+
`prompt`, `roadmap`, `diff`, `files`.
|
|
216
|
+
|
|
217
|
+
When `project` and `original_ask` are both omitted, the result is
|
|
218
|
+
`NEUTRALITY_PREAMBLE` + per-mode addendum (v1 shape, byte-identical
|
|
219
|
+
to pre-2a output). When either is supplied, the neutral handoff
|
|
220
|
+
preamble replaces the bare `NEUTRALITY_PREAMBLE`.
|
|
221
|
+
"""
|
|
222
|
+
if mode not in _MODE_TABLE:
|
|
223
|
+
raise ValueError(
|
|
224
|
+
f"Unknown council mode {mode!r}. "
|
|
225
|
+
f"Expected one of: {sorted(_MODE_TABLE)}"
|
|
226
|
+
)
|
|
227
|
+
head = handoff_preamble(project, original_ask)
|
|
228
|
+
return f"{head}\n\n{_MODE_TABLE[mode]}"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def all_modes() -> list[str]:
|
|
232
|
+
return sorted(_MODE_TABLE)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Session persistence for council consultations (D2).
|
|
2
|
+
|
|
3
|
+
Every `/council` call that completes (success or partial) writes an
|
|
4
|
+
audit artefact under `agents/council-sessions/<UTC-timestamp>/`:
|
|
5
|
+
|
|
6
|
+
- `manifest.json` — input mode, members, token + USD totals, original
|
|
7
|
+
ask, neutrality preamble fingerprint.
|
|
8
|
+
- `response.md` — `orchestrator.render()` output (per-member
|
|
9
|
+
sections + Convergence/Divergence slot).
|
|
10
|
+
- `raw-text.md` — concatenated raw text per member, separated by
|
|
11
|
+
ASCII rules so a later `grep` is trivial.
|
|
12
|
+
|
|
13
|
+
Hard rules:
|
|
14
|
+
- Never raises on the project — disk write failures are logged and
|
|
15
|
+
swallowed; the council is text-only and the report is the contract.
|
|
16
|
+
- Never writes secrets. The bundle has already been redacted by
|
|
17
|
+
`bundler.py` before the orchestrator receives it.
|
|
18
|
+
- Never writes outside `agents/council-sessions/`. Path traversal in
|
|
19
|
+
the timestamp is impossible (we generate it from `datetime.utcnow`).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import datetime as _dt
|
|
25
|
+
import json
|
|
26
|
+
import sys
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Iterable
|
|
30
|
+
|
|
31
|
+
from scripts.ai_council.clients import CouncilResponse
|
|
32
|
+
from scripts.ai_council.orchestrator import render
|
|
33
|
+
|
|
34
|
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
35
|
+
SESSIONS_DIR = REPO_ROOT / "agents" / "council-sessions"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class SessionManifest:
|
|
40
|
+
"""Structured record of a single council call.
|
|
41
|
+
|
|
42
|
+
Round 2+ debate calls (D1) pass `rounds > 1`; each round's
|
|
43
|
+
per-member response is appended in `responses_per_round`.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
mode: str # bundle mode: prompt|roadmap|diff|files
|
|
47
|
+
artefact: str # human-readable artefact descriptor (path or "<inline>")
|
|
48
|
+
original_ask: str
|
|
49
|
+
members: list[str] # "provider/model" pairs
|
|
50
|
+
rounds: int = 1
|
|
51
|
+
cost_usd_estimated: float = 0.0
|
|
52
|
+
cost_usd_actual: float = 0.0
|
|
53
|
+
extra: dict[str, object] = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _utc_timestamp() -> str:
|
|
57
|
+
"""UTC timestamp safe for filesystem use (Z suffix preserved)."""
|
|
58
|
+
return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H-%M-%SZ")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _serialise_response(r: CouncilResponse) -> dict[str, object]:
|
|
62
|
+
return {
|
|
63
|
+
"provider": r.provider,
|
|
64
|
+
"model": r.model,
|
|
65
|
+
"input_tokens": r.input_tokens,
|
|
66
|
+
"output_tokens": r.output_tokens,
|
|
67
|
+
"latency_ms": r.latency_ms,
|
|
68
|
+
"error": r.error,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def save(
|
|
73
|
+
*,
|
|
74
|
+
manifest: SessionManifest,
|
|
75
|
+
responses: list[CouncilResponse] | Iterable[list[CouncilResponse]],
|
|
76
|
+
sessions_dir: Path | None = None,
|
|
77
|
+
timestamp: str | None = None,
|
|
78
|
+
) -> Path:
|
|
79
|
+
"""Persist a council call. Returns the session directory.
|
|
80
|
+
|
|
81
|
+
`responses` accepts either:
|
|
82
|
+
- `list[CouncilResponse]` — single round (round 1 only).
|
|
83
|
+
- `Iterable[list[CouncilResponse]]` — multi-round, one list per
|
|
84
|
+
round in execution order.
|
|
85
|
+
|
|
86
|
+
Disk-write failures are surfaced via a stderr line but do not
|
|
87
|
+
raise; the caller's text report is the source of truth.
|
|
88
|
+
"""
|
|
89
|
+
rounds_data: list[list[CouncilResponse]]
|
|
90
|
+
if responses and isinstance(responses, list) and isinstance(responses[0], CouncilResponse):
|
|
91
|
+
rounds_data = [responses] # type: ignore[list-item]
|
|
92
|
+
else:
|
|
93
|
+
rounds_data = list(responses) # type: ignore[arg-type]
|
|
94
|
+
|
|
95
|
+
base = sessions_dir or SESSIONS_DIR
|
|
96
|
+
ts = timestamp or _utc_timestamp()
|
|
97
|
+
session_dir = base / ts
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
except OSError as exc: # noqa: BLE001 - never block the report
|
|
102
|
+
print(f"[council:session] mkdir failed: {exc}", file=sys.stderr)
|
|
103
|
+
return session_dir
|
|
104
|
+
|
|
105
|
+
manifest_payload = {
|
|
106
|
+
"timestamp_utc": ts,
|
|
107
|
+
"mode": manifest.mode,
|
|
108
|
+
"artefact": manifest.artefact,
|
|
109
|
+
"original_ask": manifest.original_ask,
|
|
110
|
+
"members": manifest.members,
|
|
111
|
+
"rounds": manifest.rounds,
|
|
112
|
+
"cost_usd_estimated": round(manifest.cost_usd_estimated, 6),
|
|
113
|
+
"cost_usd_actual": round(manifest.cost_usd_actual, 6),
|
|
114
|
+
"responses_per_round": [
|
|
115
|
+
[_serialise_response(r) for r in round_responses]
|
|
116
|
+
for round_responses in rounds_data
|
|
117
|
+
],
|
|
118
|
+
**manifest.extra,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
(session_dir / "manifest.json").write_text(
|
|
123
|
+
json.dumps(manifest_payload, indent=2) + "\n", encoding="utf-8",
|
|
124
|
+
)
|
|
125
|
+
# Render uses the LAST round (the moderator-facing summary).
|
|
126
|
+
last_round = rounds_data[-1] if rounds_data else []
|
|
127
|
+
(session_dir / "response.md").write_text(
|
|
128
|
+
render(last_round) + "\n", encoding="utf-8",
|
|
129
|
+
)
|
|
130
|
+
raw_blocks: list[str] = []
|
|
131
|
+
for round_idx, round_responses in enumerate(rounds_data, start=1):
|
|
132
|
+
for r in round_responses:
|
|
133
|
+
raw_blocks.append(
|
|
134
|
+
f"=== round {round_idx} · {r.provider}/{r.model} ===\n\n"
|
|
135
|
+
f"{r.text}\n",
|
|
136
|
+
)
|
|
137
|
+
(session_dir / "raw-text.md").write_text(
|
|
138
|
+
"\n".join(raw_blocks) + ("\n" if raw_blocks else ""),
|
|
139
|
+
encoding="utf-8",
|
|
140
|
+
)
|
|
141
|
+
except OSError as exc: # noqa: BLE001 - never block the report
|
|
142
|
+
print(f"[council:session] write failed: {exc}", file=sys.stderr)
|
|
143
|
+
|
|
144
|
+
return session_dir
|
|
@@ -10,7 +10,7 @@ Concatenates a curated set of cloud-safe rules from
|
|
|
10
10
|
personal.md — empty stub for individual preferences
|
|
11
11
|
|
|
12
12
|
Per-rule inclusion + mode is the source of truth in
|
|
13
|
-
`
|
|
13
|
+
`docs/contracts/linear-ai-rules-inclusion.md`. This script encodes the
|
|
14
14
|
same lists so a drift between the two surfaces is caught by the digest
|
|
15
15
|
audit (Phase 3 Step 4) — the markdown doc is the human-readable spec,
|
|
16
16
|
this script is the executable.
|
|
@@ -44,7 +44,7 @@ from pathlib import Path
|
|
|
44
44
|
ROOT = Path(__file__).resolve().parent.parent
|
|
45
45
|
# Compressed source is the shipped form — denser, sharper section
|
|
46
46
|
# structure; better fit for a guidance field than the verbose authoring
|
|
47
|
-
# layer. The inclusion list at
|
|
47
|
+
# layer. The inclusion list at docs/contracts/linear-ai-rules-inclusion.md
|
|
48
48
|
# remains the human-readable spec.
|
|
49
49
|
SOURCE = ROOT / ".agent-src" / "rules"
|
|
50
50
|
OUT_DIR = ROOT / "dist" / "linear"
|
|
@@ -64,7 +64,7 @@ class RuleEntry:
|
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
# Workspace digest — universal coding posture. Maps 1:1 to the
|
|
67
|
-
# "Workspace digest" table in
|
|
67
|
+
# "Workspace digest" table in docs/contracts/linear-ai-rules-inclusion.md.
|
|
68
68
|
WORKSPACE: list[RuleEntry] = [
|
|
69
69
|
RuleEntry("ask-when-uncertain"),
|
|
70
70
|
RuleEntry("commit-conventions"),
|
|
@@ -169,7 +169,7 @@ def render_digest(layer: str, entries: list[RuleEntry]) -> tuple[str, dict]:
|
|
|
169
169
|
parts.append(
|
|
170
170
|
"> Auto-generated by `scripts/build_linear_digest.py` from "
|
|
171
171
|
"`.agent-src/rules/` (compressed source) plus the inclusion list "
|
|
172
|
-
"at `
|
|
172
|
+
"at `docs/contracts/linear-ai-rules-inclusion.md`. Do not edit "
|
|
173
173
|
"this file by hand — re-run `task build-linear-digest` to "
|
|
174
174
|
"regenerate.\n"
|
|
175
175
|
)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Always-rule budget gate (Phases 7.1 + 7.4 of road-to-pr-34-followups).
|
|
3
|
+
|
|
4
|
+
Enforces a stricter budget contract than `tests/test_always_budget.py`:
|
|
5
|
+
the test suite fails only at 100% utilization (49,000 chars). This
|
|
6
|
+
script lives in CI as:
|
|
7
|
+
|
|
8
|
+
- Warn-at-80% / fail-at-90% global trend gate (Phase 7.1).
|
|
9
|
+
- Per-rule cap (≤ 6,000 chars per always-rule, Phase 7.4).
|
|
10
|
+
- Top-3 cap (top-3 combined ≤ 50% of TOTAL_CAP, Phase 7.4).
|
|
11
|
+
|
|
12
|
+
The same caps are enforced as hard assertions in
|
|
13
|
+
`tests/test_always_budget.py`; this script duplicates them so a
|
|
14
|
+
contributor sees a single, fast pre-test signal during local edits.
|
|
15
|
+
|
|
16
|
+
Exit codes: 0 = pass (or warn), 1 = fail (≥ 90% utilization,
|
|
17
|
+
per-rule breach, or top-3 breach), 3 = internal error.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
27
|
+
RULES_DIR = REPO_ROOT / ".agent-src" / "rules"
|
|
28
|
+
|
|
29
|
+
TOTAL_CAP = 49_000
|
|
30
|
+
WARN_THRESHOLD = 0.80 # 80% — emit warning, exit 0
|
|
31
|
+
FAIL_THRESHOLD = 0.90 # 90% — emit error, exit 1
|
|
32
|
+
PER_RULE_CAP = 6_000 # Phase 7.4 — no single always-rule may exceed this
|
|
33
|
+
TOP3_CAP = TOTAL_CAP // 2 # Phase 7.4 — top-3 combined ≤ 50% of total budget
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _always_rules() -> list[Path]:
|
|
37
|
+
rules: list[Path] = []
|
|
38
|
+
for path in sorted(RULES_DIR.glob("*.md")):
|
|
39
|
+
head = path.read_text(encoding="utf-8").splitlines()[1:2]
|
|
40
|
+
if head == ['type: "always"']:
|
|
41
|
+
rules.append(path)
|
|
42
|
+
return rules
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _summary(rules: list[Path]) -> tuple[int, list[tuple[str, int]]]:
|
|
46
|
+
sizes = [(p.name, p.stat().st_size) for p in rules]
|
|
47
|
+
return sum(s for _, s in sizes), sorted(sizes, key=lambda x: -x[1])
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main() -> int:
|
|
51
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--quiet",
|
|
54
|
+
action="store_true",
|
|
55
|
+
help="suppress the per-rule breakdown unless threshold is crossed",
|
|
56
|
+
)
|
|
57
|
+
args = parser.parse_args()
|
|
58
|
+
|
|
59
|
+
if not RULES_DIR.is_dir():
|
|
60
|
+
print(f"❌ rules dir missing: {RULES_DIR}", file=sys.stderr)
|
|
61
|
+
return 3
|
|
62
|
+
|
|
63
|
+
rules = _always_rules()
|
|
64
|
+
if not rules:
|
|
65
|
+
print(f"❌ no always-rules found under {RULES_DIR}", file=sys.stderr)
|
|
66
|
+
return 3
|
|
67
|
+
|
|
68
|
+
total, sizes = _summary(rules)
|
|
69
|
+
pct = total / TOTAL_CAP
|
|
70
|
+
over_per_rule = [(n, s) for n, s in sizes if s > PER_RULE_CAP]
|
|
71
|
+
top3 = sum(s for _, s in sizes[:3])
|
|
72
|
+
top3_breach = top3 > TOP3_CAP
|
|
73
|
+
|
|
74
|
+
if pct >= FAIL_THRESHOLD or over_per_rule or top3_breach:
|
|
75
|
+
status = "❌ FAIL"
|
|
76
|
+
rc = 1
|
|
77
|
+
elif pct >= WARN_THRESHOLD:
|
|
78
|
+
status = "⚠️ WARN"
|
|
79
|
+
rc = 0
|
|
80
|
+
else:
|
|
81
|
+
status = "✅ OK"
|
|
82
|
+
rc = 0
|
|
83
|
+
|
|
84
|
+
print(
|
|
85
|
+
f"{status} always-rule budget: {total:,} / {TOTAL_CAP:,} chars "
|
|
86
|
+
f"({pct * 100:.1f}%) across {len(rules)} rule(s)"
|
|
87
|
+
)
|
|
88
|
+
print(
|
|
89
|
+
f" thresholds: warn {WARN_THRESHOLD * 100:.0f}% · "
|
|
90
|
+
f"fail {FAIL_THRESHOLD * 100:.0f}% · "
|
|
91
|
+
f"per-rule ≤ {PER_RULE_CAP:,} · top-3 ≤ {TOP3_CAP:,}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if rc != 0 or pct >= WARN_THRESHOLD or not args.quiet:
|
|
95
|
+
print()
|
|
96
|
+
print(f" breakdown (largest first; top-3 sum = {top3:,}):")
|
|
97
|
+
for i, (name, size) in enumerate(sizes):
|
|
98
|
+
mark = " ❌" if size > PER_RULE_CAP else ""
|
|
99
|
+
tag = " (top-3)" if i < 3 else ""
|
|
100
|
+
print(f" {size:>5} {name}{tag}{mark}")
|
|
101
|
+
|
|
102
|
+
if over_per_rule:
|
|
103
|
+
names = ", ".join(f"{n}={s:,}" for n, s in over_per_rule)
|
|
104
|
+
print(
|
|
105
|
+
f"\n Per-rule cap breach (> {PER_RULE_CAP:,} chars): {names}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if top3_breach:
|
|
109
|
+
print(
|
|
110
|
+
f"\n Top-3 cap breach: {top3:,} > {TOP3_CAP:,} chars "
|
|
111
|
+
f"(top-3 must stay ≤ 50% of {TOTAL_CAP:,} total budget)."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if rc == 1:
|
|
115
|
+
print(
|
|
116
|
+
f"\n Action: trim the offending rule(s) via load_context: "
|
|
117
|
+
f"extraction (see contexts/execution + contexts/authority) "
|
|
118
|
+
f"until utilization drops below {FAIL_THRESHOLD * 100:.0f}% "
|
|
119
|
+
f"and all per-rule / top-3 caps hold."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return rc
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
sys.exit(main())
|