@event4u/agent-config 1.18.0 → 1.19.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/council/default.md +74 -76
- package/.agent-src/commands/feature/roadmap.md +22 -0
- package/.agent-src/commands/roadmap/create.md +38 -6
- package/.agent-src/commands/roadmap/execute.md +36 -9
- package/.agent-src/rules/agent-authority.md +1 -0
- package/.agent-src/rules/agent-docs.md +1 -0
- package/.agent-src/rules/analysis-skill-routing.md +1 -0
- package/.agent-src/rules/architecture.md +1 -0
- package/.agent-src/rules/artifact-drafting-protocol.md +1 -0
- package/.agent-src/rules/artifact-engagement-recording.md +1 -0
- package/.agent-src/rules/ask-when-uncertain.md +1 -0
- package/.agent-src/rules/augment-portability.md +1 -0
- package/.agent-src/rules/augment-source-of-truth.md +1 -0
- package/.agent-src/rules/autonomous-execution.md +1 -0
- package/.agent-src/rules/capture-learnings.md +1 -0
- package/.agent-src/rules/chat-history-cadence.md +34 -0
- package/.agent-src/rules/chat-history-ownership.md +1 -0
- package/.agent-src/rules/chat-history-visibility.md +1 -0
- package/.agent-src/rules/cli-output-handling.md +2 -2
- package/.agent-src/rules/command-suggestion-policy.md +1 -0
- package/.agent-src/rules/commit-conventions.md +1 -0
- package/.agent-src/rules/commit-policy.md +1 -0
- package/.agent-src/rules/context-hygiene.md +22 -0
- package/.agent-src/rules/direct-answers.md +1 -0
- package/.agent-src/rules/docker-commands.md +1 -0
- package/.agent-src/rules/docs-sync.md +1 -0
- package/.agent-src/rules/downstream-changes.md +1 -0
- package/.agent-src/rules/e2e-testing.md +1 -0
- package/.agent-src/rules/guidelines.md +1 -0
- package/.agent-src/rules/improve-before-implement.md +1 -0
- package/.agent-src/rules/language-and-tone.md +1 -0
- package/.agent-src/rules/laravel-translations.md +1 -0
- package/.agent-src/rules/markdown-safe-codeblocks.md +1 -0
- package/.agent-src/rules/minimal-safe-diff.md +1 -0
- package/.agent-src/rules/missing-tool-handling.md +1 -0
- package/.agent-src/rules/model-recommendation.md +1 -0
- package/.agent-src/rules/no-cheap-questions.md +1 -0
- package/.agent-src/rules/no-roadmap-references.md +1 -0
- package/.agent-src/rules/non-destructive-by-default.md +1 -0
- package/.agent-src/rules/onboarding-gate.md +26 -0
- package/.agent-src/rules/package-ci-checks.md +1 -0
- package/.agent-src/rules/php-coding.md +1 -0
- package/.agent-src/rules/preservation-guard.md +1 -0
- package/.agent-src/rules/review-routing-awareness.md +1 -0
- package/.agent-src/rules/reviewer-awareness.md +1 -0
- package/.agent-src/rules/roadmap-progress-sync.md +22 -0
- package/.agent-src/rules/role-mode-adherence.md +2 -2
- package/.agent-src/rules/rule-type-governance.md +1 -0
- package/.agent-src/rules/runtime-safety.md +1 -0
- package/.agent-src/rules/scope-control.md +1 -0
- package/.agent-src/rules/security-sensitive-stop.md +1 -0
- package/.agent-src/rules/size-enforcement.md +1 -0
- package/.agent-src/rules/skill-improvement-trigger.md +1 -0
- package/.agent-src/rules/skill-quality.md +1 -0
- package/.agent-src/rules/slash-command-routing-policy.md +39 -0
- package/.agent-src/rules/think-before-action.md +1 -0
- package/.agent-src/rules/token-efficiency.md +1 -0
- package/.agent-src/rules/tool-safety.md +1 -0
- package/.agent-src/rules/ui-audit-gate.md +1 -0
- package/.agent-src/rules/upstream-proposal.md +1 -0
- package/.agent-src/rules/user-interaction.md +1 -0
- package/.agent-src/rules/verify-before-complete.md +1 -0
- package/.agent-src/skills/roadmap-management/SKILL.md +29 -4
- package/.agent-src/skills/verify-completion-evidence/SKILL.md +8 -1
- package/.agent-src/templates/agent-settings.md +16 -0
- package/.agent-src/templates/roadmaps.md +8 -3
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +9 -0
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +4 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +4 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.py +163 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +111 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +36 -0
- package/.agent-src/templates/scripts/work_engine/scoring/decision_trace.py +141 -0
- package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +125 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +62 -0
- package/README.md +19 -19
- package/config/agent-settings.template.yml +23 -0
- package/docs/catalog.md +5 -2
- package/docs/contracts/adr-settings-sync-engine.md +127 -0
- package/docs/contracts/decision-trace-v1.md +146 -0
- package/docs/contracts/file-ownership-matrix.json +7 -0
- package/docs/contracts/hook-architecture-v1.md +213 -0
- package/docs/contracts/memory-visibility-v1.md +138 -0
- package/docs/contracts/one-off-script-lifecycle.md +109 -0
- package/docs/contracts/rule-interactions.yml +22 -0
- package/docs/customization.md +1 -0
- package/docs/development.md +4 -1
- package/docs/guidelines/agent-infra/layered-settings.md +32 -13
- package/package.json +1 -1
- package/scripts/agent-config +44 -0
- package/scripts/ai_council/bundler.py +3 -3
- package/scripts/ai_council/clients.py +24 -8
- package/scripts/ai_council/one_off_archive/2026-05/README.md +22 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_roundtrip.py +13 -8
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_tier_retrofit.py +180 -0
- package/scripts/ai_council/session.py +92 -0
- package/scripts/capture_showcase_session.py +361 -0
- package/scripts/chat_history.py +11 -1
- package/scripts/check_always_budget.py +7 -2
- package/scripts/context_hygiene_hook.py +14 -6
- package/scripts/council_cli.py +357 -0
- package/scripts/hook_manifest.yaml +184 -0
- package/scripts/hooks/__init__.py +1 -0
- package/scripts/hooks/augment-dispatcher.sh +72 -0
- package/scripts/hooks/cline-dispatcher.sh +86 -0
- package/scripts/hooks/cursor-dispatcher.sh +76 -0
- package/scripts/hooks/dispatch_hook.py +348 -0
- package/scripts/hooks/envelope.py +98 -0
- package/scripts/hooks/gemini-dispatcher.sh +117 -0
- package/scripts/hooks/state_io.py +122 -0
- package/scripts/hooks/windsurf-dispatcher.sh +123 -0
- package/scripts/hooks_status.py +146 -0
- package/scripts/install.py +725 -87
- package/scripts/install.sh +1 -1
- package/scripts/lint_hook_manifest.py +216 -0
- package/scripts/lint_one_off_age.py +184 -0
- package/scripts/lint_rule_tiers.py +78 -0
- package/scripts/lint_showcase_sessions.py +148 -0
- package/scripts/minimal_safe_diff_hook.py +245 -0
- package/scripts/onboarding_gate_hook.py +13 -8
- package/scripts/readme_linter.py +12 -3
- package/scripts/roadmap_progress_hook.py +5 -0
- package/scripts/sync_agent_settings.py +32 -129
- package/scripts/sync_yaml_rt.py +734 -0
- package/scripts/verify_before_complete_hook.py +216 -0
|
@@ -92,9 +92,14 @@ BASELINE_FILE = REPO_ROOT / ".github" / "budget-baseline.txt"
|
|
|
92
92
|
# growth above the ceiling fails CI even while the entry remains.
|
|
93
93
|
# When Phase 2A retires a rule, drop its entry here AND in
|
|
94
94
|
# `tests/test_always_budget.py::KNOWN_PER_RULE_BREACHES`.
|
|
95
|
+
#
|
|
96
|
+
# Phase 2 of road-to-feedback-consolidation.md added a single-line
|
|
97
|
+
# `tier: "safety-floor"` frontmatter key (21 chars) to every safety-floor
|
|
98
|
+
# rule. Both ceilings below were re-baselined +21 to absorb that
|
|
99
|
+
# frontmatter-only growth without trimming Iron-Law content.
|
|
95
100
|
KNOWN_PER_RULE_BREACHES: dict[str, int] = {
|
|
96
|
-
"non-destructive-by-default.md":
|
|
97
|
-
"scope-control.md":
|
|
101
|
+
"non-destructive-by-default.md": 7_908,
|
|
102
|
+
"scope-control.md": 8_550,
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
|
|
@@ -30,6 +30,12 @@ import json
|
|
|
30
30
|
import sys
|
|
31
31
|
from pathlib import Path
|
|
32
32
|
|
|
33
|
+
# Re-use the shared atomic-write helper so concerns honour the single
|
|
34
|
+
# `agents/state/.dispatcher.lock` discipline (hook-architecture-v1.md
|
|
35
|
+
# § Concurrency, Phase 7.4).
|
|
36
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
37
|
+
from hooks.state_io import atomic_write_json # noqa: E402
|
|
38
|
+
|
|
33
39
|
STATE_DIR = Path("agents") / "state"
|
|
34
40
|
STATE_FILE = STATE_DIR / "context-hygiene.json"
|
|
35
41
|
|
|
@@ -118,12 +124,9 @@ def _update(state: dict, tool: str | None) -> dict:
|
|
|
118
124
|
|
|
119
125
|
|
|
120
126
|
def _write_state(consumer_root: Path, state: dict) -> None:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
tmp = target.with_suffix(".json.tmp")
|
|
125
|
-
tmp.write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8")
|
|
126
|
-
tmp.replace(target)
|
|
127
|
+
"""Write the state file atomically under the shared dispatcher lock
|
|
128
|
+
(hook-architecture-v1.md § Concurrency, Phase 7.4)."""
|
|
129
|
+
atomic_write_json(consumer_root / STATE_FILE, state)
|
|
127
130
|
|
|
128
131
|
|
|
129
132
|
def run(stdin_text: str, *, consumer_root: Path, verbose: bool = False) -> int:
|
|
@@ -136,6 +139,11 @@ def run(stdin_text: str, *, consumer_root: Path, verbose: bool = False) -> int:
|
|
|
136
139
|
except json.JSONDecodeError:
|
|
137
140
|
pass # silent no-op, never block
|
|
138
141
|
|
|
142
|
+
# Unwrap dispatcher envelope (Phase 7.3, hook-architecture-v1.md).
|
|
143
|
+
if all(k in payload for k in ("schema_version", "platform", "event", "payload")):
|
|
144
|
+
inner = payload.get("payload")
|
|
145
|
+
payload = inner if isinstance(inner, dict) else {}
|
|
146
|
+
|
|
139
147
|
target = consumer_root / STATE_FILE
|
|
140
148
|
state = _load_state(target)
|
|
141
149
|
state = _update(state, _extract_tool(payload))
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Council CLI — `./agent-config council:{estimate,run,render}`.
|
|
3
|
+
|
|
4
|
+
Wraps `scripts.ai_council.orchestrator` for non-interactive callers.
|
|
5
|
+
Subcommands:
|
|
6
|
+
|
|
7
|
+
estimate Bundle + estimate per-member cost (no API call, no spend).
|
|
8
|
+
run Same + estimate, then call the council. Requires --confirm.
|
|
9
|
+
render Re-render a saved responses JSON to the markdown report.
|
|
10
|
+
|
|
11
|
+
`./agent-config` is non-interactive by contract — the cost gate is an
|
|
12
|
+
explicit `--confirm` flag, never an interactive y/n.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from dataclasses import asdict
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import yaml
|
|
24
|
+
|
|
25
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
26
|
+
SETTINGS_FILE = REPO_ROOT / ".agent-settings.yml"
|
|
27
|
+
|
|
28
|
+
sys.path.insert(0, str(REPO_ROOT))
|
|
29
|
+
|
|
30
|
+
from scripts.ai_council.bundler import ( # noqa: E402
|
|
31
|
+
BundleTooLarge, bundle_prompt, bundle_roadmap,
|
|
32
|
+
)
|
|
33
|
+
from scripts.ai_council.clients import ( # noqa: E402
|
|
34
|
+
AnthropicClient, CouncilResponse, ExternalAIClient, ManualClient,
|
|
35
|
+
OpenAIClient, load_anthropic_key, load_openai_key,
|
|
36
|
+
)
|
|
37
|
+
from scripts.ai_council.modes import ( # noqa: E402
|
|
38
|
+
InvalidModeError, resolve_mode,
|
|
39
|
+
)
|
|
40
|
+
from scripts.ai_council.orchestrator import ( # noqa: E402
|
|
41
|
+
CostBudget, CouncilQuestion, consult, estimate, render,
|
|
42
|
+
)
|
|
43
|
+
from scripts.ai_council.pricing import ( # noqa: E402
|
|
44
|
+
PriceTable, estimate_cost, load_prices,
|
|
45
|
+
)
|
|
46
|
+
from scripts.ai_council.project_context import detect_project_context # noqa: E402
|
|
47
|
+
|
|
48
|
+
SCHEMA_VERSION = 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CouncilDisabledError(RuntimeError):
|
|
52
|
+
"""Raised when ai_council.enabled is false or no member is enabled."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_settings(path: Path = SETTINGS_FILE) -> dict[str, Any]:
|
|
56
|
+
if not path.exists():
|
|
57
|
+
return {}
|
|
58
|
+
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_members(
|
|
62
|
+
settings: dict[str, Any],
|
|
63
|
+
*,
|
|
64
|
+
invocation_mode: str | None = None,
|
|
65
|
+
) -> list[ExternalAIClient]:
|
|
66
|
+
"""Construct enabled council members from settings.
|
|
67
|
+
|
|
68
|
+
Honours `ai_council.enabled` (master switch) and per-member
|
|
69
|
+
`enabled` flags. Raises `CouncilDisabledError` when the council is
|
|
70
|
+
off or no member is wired up.
|
|
71
|
+
"""
|
|
72
|
+
ai = (settings.get("ai_council") or {}) if isinstance(settings, dict) else {}
|
|
73
|
+
if not ai.get("enabled"):
|
|
74
|
+
raise CouncilDisabledError(
|
|
75
|
+
"ai_council.enabled is false in .agent-settings.yml — "
|
|
76
|
+
"flip it on before invoking council:* commands."
|
|
77
|
+
)
|
|
78
|
+
members_cfg = ai.get("members") or {}
|
|
79
|
+
global_mode = ai.get("mode")
|
|
80
|
+
members: list[ExternalAIClient] = []
|
|
81
|
+
for name, cfg in members_cfg.items():
|
|
82
|
+
cfg = cfg or {}
|
|
83
|
+
if not cfg.get("enabled"):
|
|
84
|
+
continue
|
|
85
|
+
mode = resolve_mode(
|
|
86
|
+
name,
|
|
87
|
+
invocation_mode=invocation_mode,
|
|
88
|
+
member_settings=cfg,
|
|
89
|
+
global_mode=global_mode,
|
|
90
|
+
)
|
|
91
|
+
model = cfg.get("model")
|
|
92
|
+
if mode == "api" and name == "anthropic":
|
|
93
|
+
members.append(AnthropicClient(model=model or "claude-sonnet-4-5",
|
|
94
|
+
api_key=load_anthropic_key()))
|
|
95
|
+
elif mode == "api" and name == "openai":
|
|
96
|
+
members.append(OpenAIClient(model=model or "gpt-4o",
|
|
97
|
+
api_key=load_openai_key()))
|
|
98
|
+
elif mode == "manual":
|
|
99
|
+
members.append(ManualClient(name=name, model=model or "manual"))
|
|
100
|
+
elif mode == "playwright":
|
|
101
|
+
raise CouncilDisabledError(
|
|
102
|
+
f"member {name!r} resolves to mode=playwright (Phase 2c, not wired)."
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
raise CouncilDisabledError(
|
|
106
|
+
f"member {name!r} has no transport — mode={mode}, name not in {{anthropic,openai}}."
|
|
107
|
+
)
|
|
108
|
+
if not members:
|
|
109
|
+
raise CouncilDisabledError(
|
|
110
|
+
"no council member has `enabled: true` — enable at least one in "
|
|
111
|
+
".agent-settings.yml under ai_council.members.*."
|
|
112
|
+
)
|
|
113
|
+
return members
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build_question(
|
|
117
|
+
*,
|
|
118
|
+
input_path: Path,
|
|
119
|
+
input_mode: str,
|
|
120
|
+
max_tokens: int,
|
|
121
|
+
) -> tuple[CouncilQuestion, str]:
|
|
122
|
+
"""Bundle the input file. Returns (question, artefact_label)."""
|
|
123
|
+
if input_mode == "prompt":
|
|
124
|
+
text = input_path.read_text(encoding="utf-8")
|
|
125
|
+
ctx = bundle_prompt(text)
|
|
126
|
+
artefact = str(input_path)
|
|
127
|
+
elif input_mode == "roadmap":
|
|
128
|
+
ctx = bundle_roadmap(input_path)
|
|
129
|
+
artefact = str(input_path)
|
|
130
|
+
else:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"unsupported input mode: {input_mode!r} (use prompt | roadmap)"
|
|
133
|
+
)
|
|
134
|
+
return CouncilQuestion(mode=ctx.mode, user_prompt=ctx.text,
|
|
135
|
+
max_tokens=max_tokens), artefact
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def format_estimate_table(
|
|
139
|
+
members: list[ExternalAIClient],
|
|
140
|
+
estimates: list[Any],
|
|
141
|
+
) -> str:
|
|
142
|
+
rows = [
|
|
143
|
+
f" {m.name}/{m.model}: "
|
|
144
|
+
f"~{e.input_tokens} in + {e.output_tokens} out = ${e.total_usd:.4f}"
|
|
145
|
+
for m, e in zip(members, estimates)
|
|
146
|
+
]
|
|
147
|
+
total = sum(e.total_usd for e in estimates)
|
|
148
|
+
rows.append(f" TOTAL: ${total:.4f}")
|
|
149
|
+
return "\n".join(rows)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ── subcommands ─────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def cmd_estimate(
|
|
156
|
+
args: argparse.Namespace,
|
|
157
|
+
*,
|
|
158
|
+
settings: dict[str, Any] | None = None,
|
|
159
|
+
members: list[ExternalAIClient] | None = None,
|
|
160
|
+
table: PriceTable | None = None,
|
|
161
|
+
) -> int:
|
|
162
|
+
"""Print per-member cost preview. No API calls."""
|
|
163
|
+
if settings is None:
|
|
164
|
+
settings = load_settings()
|
|
165
|
+
if members is None:
|
|
166
|
+
members = build_members(settings, invocation_mode=args.mode_override)
|
|
167
|
+
if table is None:
|
|
168
|
+
table = load_prices()
|
|
169
|
+
question, _ = build_question(
|
|
170
|
+
input_path=Path(args.question), input_mode=args.input_mode,
|
|
171
|
+
max_tokens=args.max_tokens,
|
|
172
|
+
)
|
|
173
|
+
project = detect_project_context(REPO_ROOT)
|
|
174
|
+
billable = [m for m in members if getattr(m, "billable", True)]
|
|
175
|
+
estimates = estimate(question, billable, table,
|
|
176
|
+
project=project, original_ask=args.original_ask)
|
|
177
|
+
sys.stdout.write(
|
|
178
|
+
f"council:estimate · mode={question.mode} · members={len(members)} "
|
|
179
|
+
f"(billable={len(billable)})\n"
|
|
180
|
+
)
|
|
181
|
+
sys.stdout.write(format_estimate_table(billable, estimates) + "\n")
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _serialise_responses(responses: list[CouncilResponse]) -> list[dict[str, Any]]:
|
|
186
|
+
out: list[dict[str, Any]] = []
|
|
187
|
+
for r in responses:
|
|
188
|
+
d = asdict(r)
|
|
189
|
+
# `metadata` may contain non-JSON types; coerce.
|
|
190
|
+
d["metadata"] = {k: str(v) for k, v in (d.get("metadata") or {}).items()}
|
|
191
|
+
out.append(d)
|
|
192
|
+
return out
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _deserialise_responses(items: list[dict[str, Any]]) -> list[CouncilResponse]:
|
|
196
|
+
out: list[CouncilResponse] = []
|
|
197
|
+
for d in items:
|
|
198
|
+
out.append(CouncilResponse(
|
|
199
|
+
provider=d.get("provider", ""),
|
|
200
|
+
model=d.get("model", ""),
|
|
201
|
+
text=d.get("text", ""),
|
|
202
|
+
input_tokens=int(d.get("input_tokens", 0) or 0),
|
|
203
|
+
output_tokens=int(d.get("output_tokens", 0) or 0),
|
|
204
|
+
latency_ms=int(d.get("latency_ms", 0) or 0),
|
|
205
|
+
error=d.get("error"),
|
|
206
|
+
metadata=dict(d.get("metadata") or {}),
|
|
207
|
+
))
|
|
208
|
+
return out
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def cmd_run(
|
|
212
|
+
args: argparse.Namespace,
|
|
213
|
+
*,
|
|
214
|
+
settings: dict[str, Any] | None = None,
|
|
215
|
+
members: list[ExternalAIClient] | None = None,
|
|
216
|
+
table: PriceTable | None = None,
|
|
217
|
+
) -> int:
|
|
218
|
+
"""Estimate, then run the council. Requires --confirm to spend."""
|
|
219
|
+
if settings is None:
|
|
220
|
+
settings = load_settings()
|
|
221
|
+
if members is None:
|
|
222
|
+
members = build_members(settings, invocation_mode=args.mode_override)
|
|
223
|
+
if table is None:
|
|
224
|
+
table = load_prices()
|
|
225
|
+
question, artefact = build_question(
|
|
226
|
+
input_path=Path(args.question), input_mode=args.input_mode,
|
|
227
|
+
max_tokens=args.max_tokens,
|
|
228
|
+
)
|
|
229
|
+
project = detect_project_context(REPO_ROOT)
|
|
230
|
+
billable = [m for m in members if getattr(m, "billable", True)]
|
|
231
|
+
estimates = estimate(question, billable, table,
|
|
232
|
+
project=project, original_ask=args.original_ask)
|
|
233
|
+
sys.stdout.write(
|
|
234
|
+
f"council:run · mode={question.mode} · members={len(members)} "
|
|
235
|
+
f"(billable={len(billable)})\n"
|
|
236
|
+
)
|
|
237
|
+
sys.stdout.write(format_estimate_table(billable, estimates) + "\n")
|
|
238
|
+
|
|
239
|
+
if not args.confirm:
|
|
240
|
+
sys.stdout.write(
|
|
241
|
+
"\nNo --confirm flag — estimate only. Re-run with --confirm to "
|
|
242
|
+
"invoke the council and write the response.\n"
|
|
243
|
+
)
|
|
244
|
+
return 0
|
|
245
|
+
|
|
246
|
+
cost_cfg = (settings.get("ai_council") or {}).get("cost_budget") or {}
|
|
247
|
+
budget = CostBudget(
|
|
248
|
+
max_input_tokens=int(cost_cfg.get("max_input_tokens", 50_000)),
|
|
249
|
+
max_output_tokens=int(cost_cfg.get("max_output_tokens", 20_000)),
|
|
250
|
+
max_calls=int(cost_cfg.get("max_calls", 10)),
|
|
251
|
+
max_total_usd=float(cost_cfg.get("max_total_usd", 0.0) or 0.0),
|
|
252
|
+
)
|
|
253
|
+
responses = consult(
|
|
254
|
+
members, question, budget,
|
|
255
|
+
table=table, project=project,
|
|
256
|
+
original_ask=args.original_ask, rounds=args.rounds,
|
|
257
|
+
)
|
|
258
|
+
estimated_total = sum(e.total_usd for e in estimates)
|
|
259
|
+
actual_total = 0.0
|
|
260
|
+
for r in responses:
|
|
261
|
+
if r.error:
|
|
262
|
+
continue
|
|
263
|
+
ce = estimate_cost(r.provider, r.model, r.input_tokens, r.output_tokens, table)
|
|
264
|
+
actual_total += ce.total_usd
|
|
265
|
+
payload = {
|
|
266
|
+
"schema_version": SCHEMA_VERSION,
|
|
267
|
+
"mode": question.mode,
|
|
268
|
+
"artefact": artefact,
|
|
269
|
+
"original_ask": args.original_ask,
|
|
270
|
+
"members": [f"{m.name}/{m.model}" for m in members],
|
|
271
|
+
"rounds": args.rounds,
|
|
272
|
+
"cost_usd_estimated": round(estimated_total, 6),
|
|
273
|
+
"cost_usd_actual": round(actual_total, 6),
|
|
274
|
+
"responses": _serialise_responses(responses),
|
|
275
|
+
}
|
|
276
|
+
out_path = Path(args.output)
|
|
277
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
278
|
+
out_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
279
|
+
sys.stdout.write(
|
|
280
|
+
f"\ncouncil:run · wrote {out_path} "
|
|
281
|
+
f"(estimated ${estimated_total:.4f} / actual ${actual_total:.4f})\n"
|
|
282
|
+
)
|
|
283
|
+
errors = [r for r in responses if r.error]
|
|
284
|
+
return 1 if errors and len(errors) == len(responses) else 0
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def cmd_render(args: argparse.Namespace) -> int:
|
|
288
|
+
"""Re-render a saved responses JSON to the markdown report."""
|
|
289
|
+
payload = json.loads(Path(args.responses).read_text(encoding="utf-8"))
|
|
290
|
+
items = payload.get("responses") or []
|
|
291
|
+
sys.stdout.write(render(_deserialise_responses(items)) + "\n")
|
|
292
|
+
return 0
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ── argparse + main ─────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _add_common_input_args(p: argparse.ArgumentParser) -> None:
|
|
299
|
+
p.add_argument("question", type=str,
|
|
300
|
+
help="Path to the question file (text or roadmap).")
|
|
301
|
+
p.add_argument("--input-mode", choices=["prompt", "roadmap"],
|
|
302
|
+
default="prompt",
|
|
303
|
+
help="How to bundle the file (default: prompt).")
|
|
304
|
+
p.add_argument("--max-tokens", type=int, default=1024,
|
|
305
|
+
help="Per-member output budget (default: 1024).")
|
|
306
|
+
p.add_argument("--mode-override", choices=["api", "manual"], default=None,
|
|
307
|
+
help="Override every member's transport mode.")
|
|
308
|
+
p.add_argument("--original-ask", default="",
|
|
309
|
+
help="The user's framing sentence (flows into handoff).")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
313
|
+
parser = argparse.ArgumentParser(
|
|
314
|
+
prog="agent-config council",
|
|
315
|
+
description="Non-interactive council orchestration.",
|
|
316
|
+
)
|
|
317
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
318
|
+
|
|
319
|
+
p_est = sub.add_parser("estimate", help="Pre-call cost preview (no spend).")
|
|
320
|
+
_add_common_input_args(p_est)
|
|
321
|
+
|
|
322
|
+
p_run = sub.add_parser("run", help="Run the council; --confirm required to spend.")
|
|
323
|
+
_add_common_input_args(p_run)
|
|
324
|
+
p_run.add_argument("--output", required=True,
|
|
325
|
+
help="Path to write the responses JSON.")
|
|
326
|
+
p_run.add_argument("--confirm", action="store_true",
|
|
327
|
+
help="Required to actually invoke the council.")
|
|
328
|
+
p_run.add_argument("--rounds", type=int, default=1,
|
|
329
|
+
help="Number of debate rounds (1-3).")
|
|
330
|
+
|
|
331
|
+
p_ren = sub.add_parser("render", help="Re-render a saved responses JSON.")
|
|
332
|
+
p_ren.add_argument("responses",
|
|
333
|
+
help="Path to the JSON written by `council run`.")
|
|
334
|
+
|
|
335
|
+
return parser
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def main(argv: list[str] | None = None) -> int:
|
|
339
|
+
args = build_parser().parse_args(argv)
|
|
340
|
+
try:
|
|
341
|
+
if args.cmd == "estimate":
|
|
342
|
+
return cmd_estimate(args)
|
|
343
|
+
if args.cmd == "run":
|
|
344
|
+
return cmd_run(args)
|
|
345
|
+
if args.cmd == "render":
|
|
346
|
+
return cmd_render(args)
|
|
347
|
+
except CouncilDisabledError as exc:
|
|
348
|
+
sys.stderr.write(f"❌ council:{args.cmd}: {exc}\n")
|
|
349
|
+
return 2
|
|
350
|
+
except (BundleTooLarge, InvalidModeError, FileNotFoundError) as exc:
|
|
351
|
+
sys.stderr.write(f"❌ council:{args.cmd}: {exc}\n")
|
|
352
|
+
return 2
|
|
353
|
+
return 1
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
if __name__ == "__main__":
|
|
357
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Hook manifest — single source of truth for which concerns fire on which
|
|
2
|
+
# (platform, event) tuples. Consumed by:
|
|
3
|
+
# - scripts/hooks/dispatch_hook.py (runtime resolver)
|
|
4
|
+
# - scripts/lint_hook_manifest.py (CI gate; Phase 7.10)
|
|
5
|
+
# - scripts/install.py (per-platform config writer)
|
|
6
|
+
#
|
|
7
|
+
# Schema and event vocabulary: docs/contracts/hook-architecture-v1.md.
|
|
8
|
+
# Per-platform event-name mapping: agents/contexts/chat-history-platform-hooks.md.
|
|
9
|
+
#
|
|
10
|
+
# When you add or remove a concern, also update:
|
|
11
|
+
# - scripts/agent-config (CLI subcommand wiring, if exposed)
|
|
12
|
+
# - the source rule's "Copilot fallback" section (Phase 7.9)
|
|
13
|
+
# - the relevant snapshot tests under tests/hooks/ (Phase 7.11)
|
|
14
|
+
schema_version: 1
|
|
15
|
+
|
|
16
|
+
concerns:
|
|
17
|
+
chat-history:
|
|
18
|
+
script: scripts/chat_history.py
|
|
19
|
+
args: [hook-dispatch]
|
|
20
|
+
fail_closed: false
|
|
21
|
+
roadmap-progress:
|
|
22
|
+
script: scripts/roadmap_progress_hook.py
|
|
23
|
+
args: []
|
|
24
|
+
fail_closed: false
|
|
25
|
+
onboarding-gate:
|
|
26
|
+
script: scripts/onboarding_gate_hook.py
|
|
27
|
+
args: []
|
|
28
|
+
fail_closed: false
|
|
29
|
+
context-hygiene:
|
|
30
|
+
script: scripts/context_hygiene_hook.py
|
|
31
|
+
args: []
|
|
32
|
+
fail_closed: false
|
|
33
|
+
# Phase 5 — Tier-1 hook for the verify-before-complete rule. Records
|
|
34
|
+
# observable evidence of verification commands (tests, quality tools,
|
|
35
|
+
# builds) into agents/state/verify-before-complete.json. The rule body
|
|
36
|
+
# cites that file as the source of truth for "verified this turn?".
|
|
37
|
+
verify-before-complete:
|
|
38
|
+
script: scripts/verify_before_complete_hook.py
|
|
39
|
+
args: []
|
|
40
|
+
fail_closed: false
|
|
41
|
+
# Phase 5 — Tier-1 hook for the minimal-safe-diff rule. Counts unique
|
|
42
|
+
# files touched per turn into agents/state/minimal-safe-diff.json and
|
|
43
|
+
# flips warning=true past hooks.minimal_safe_diff.threshold.
|
|
44
|
+
minimal-safe-diff:
|
|
45
|
+
script: scripts/minimal_safe_diff_hook.py
|
|
46
|
+
args: []
|
|
47
|
+
fail_closed: false
|
|
48
|
+
|
|
49
|
+
platforms:
|
|
50
|
+
augment:
|
|
51
|
+
session_start: [chat-history, onboarding-gate, verify-before-complete, minimal-safe-diff]
|
|
52
|
+
session_end: [chat-history]
|
|
53
|
+
stop: [chat-history, verify-before-complete]
|
|
54
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff]
|
|
55
|
+
|
|
56
|
+
claude:
|
|
57
|
+
session_start: [chat-history, onboarding-gate, verify-before-complete, minimal-safe-diff]
|
|
58
|
+
session_end: [chat-history]
|
|
59
|
+
stop: [chat-history, verify-before-complete]
|
|
60
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
61
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff]
|
|
62
|
+
|
|
63
|
+
# Phase 7.5 — Cursor. `.cursor/hooks.json` (project) is read by the
|
|
64
|
+
# IDE and CLI; `~/.cursor/hooks.json` (user) is opt-in via
|
|
65
|
+
# `install.py --cursor-user-hooks` and uses scripts/hooks/cursor-dispatcher.sh
|
|
66
|
+
# to route into the active workspace. UserPromptSubmit maps to
|
|
67
|
+
# `beforeSubmitPrompt` (Cursor third-party-hooks table). Stop is
|
|
68
|
+
# IDE-only — CLI-only users fall back to /checkpoint per
|
|
69
|
+
# agents/contexts/chat-history-platform-hooks.md.
|
|
70
|
+
cursor:
|
|
71
|
+
session_start: [chat-history, onboarding-gate, verify-before-complete, minimal-safe-diff]
|
|
72
|
+
session_end: [chat-history]
|
|
73
|
+
stop: [chat-history, verify-before-complete]
|
|
74
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
75
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff]
|
|
76
|
+
|
|
77
|
+
# Phase 7.6 — Cline. Hooks live under `.clinerules/hooks/<HookName>`
|
|
78
|
+
# (project, no file extension, must be executable per Cline docs) or
|
|
79
|
+
# `~/Documents/Cline/Hooks/<HookName>` (global). Each script reads a
|
|
80
|
+
# JSON payload from stdin (`taskId`, `hookName`, `workspaceRoots`,
|
|
81
|
+
# `model`, plus a hook-specific field). Cline emits two distinct
|
|
82
|
+
# task-start events — TaskStart (new) and TaskResume (continue) —
|
|
83
|
+
# both map to session_start. TaskCancel maps to stop because the
|
|
84
|
+
# session is interrupted with partial state (mirrors Augment Stop).
|
|
85
|
+
cline:
|
|
86
|
+
session_start: [chat-history, onboarding-gate, verify-before-complete, minimal-safe-diff]
|
|
87
|
+
session_end: [chat-history]
|
|
88
|
+
stop: [chat-history, verify-before-complete]
|
|
89
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
90
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff]
|
|
91
|
+
|
|
92
|
+
# Phase 7.7 — Windsurf (Cascade). Hooks live at `.windsurf/hooks.json`
|
|
93
|
+
# (project) or `~/.codeium/windsurf/hooks.json` (user). Cascade has
|
|
94
|
+
# no generic post-tool-use surface — roadmap-progress and
|
|
95
|
+
# context-hygiene therefore have no Windsurf binding (documented
|
|
96
|
+
# platform limitation). chat-history wires through the per-turn
|
|
97
|
+
# cycle: pre_user_prompt for the turn-check, post_cascade_response
|
|
98
|
+
# for the per-turn append, and post_setup_worktree for first-run
|
|
99
|
+
# init (rare, fires only on worktree creation; advisory not
|
|
100
|
+
# blocking — Windsurf docs).
|
|
101
|
+
# Windsurf — verify-before-complete tracks lifecycle only (no post_tool_use
|
|
102
|
+
# surface to record verification commands; documented limitation).
|
|
103
|
+
# minimal-safe-diff is omitted entirely on Windsurf for the same reason.
|
|
104
|
+
windsurf:
|
|
105
|
+
session_start: [chat-history, onboarding-gate, verify-before-complete]
|
|
106
|
+
stop: [chat-history, verify-before-complete]
|
|
107
|
+
user_prompt_submit: [chat-history, verify-before-complete]
|
|
108
|
+
|
|
109
|
+
# Phase 7.8 — Gemini CLI. Hooks live at `.gemini/settings.json`
|
|
110
|
+
# (project) or `~/.gemini/settings.json` (user). Per Gemini docs
|
|
111
|
+
# (geminicli.com/docs/hooks/reference/), each event maps to an
|
|
112
|
+
# array of hook groups; each group has a `matcher` (regex for
|
|
113
|
+
# tool events, exact string for lifecycle) and a `hooks` array of
|
|
114
|
+
# `{type: "command", command: "..."}`. SessionStart/SessionEnd are
|
|
115
|
+
# advisory (cannot block); BeforeAgent fires after the user
|
|
116
|
+
# submits a prompt and before agent planning, so it carries the
|
|
117
|
+
# turn-check semantics. AfterAgent fires when the agent loop ends
|
|
118
|
+
# — this is our `stop` slot.
|
|
119
|
+
gemini:
|
|
120
|
+
session_start: [chat-history, onboarding-gate, verify-before-complete, minimal-safe-diff]
|
|
121
|
+
session_end: [chat-history]
|
|
122
|
+
stop: [chat-history, verify-before-complete]
|
|
123
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
124
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff]
|
|
125
|
+
|
|
126
|
+
# Phase 7.9 — Copilot has no hook surface. Concerns route through
|
|
127
|
+
# rule-only fallback; the dispatcher silently no-ops on --platform copilot.
|
|
128
|
+
copilot:
|
|
129
|
+
fallback_only: true
|
|
130
|
+
|
|
131
|
+
# Native-event → agent-config-event translation table. Used by per-platform
|
|
132
|
+
# trampolines when the platform speaks a different vocabulary. Trampolines
|
|
133
|
+
# pass --native-event for traceability; the dispatcher does NOT branch on it.
|
|
134
|
+
native_event_aliases:
|
|
135
|
+
augment:
|
|
136
|
+
SessionStart: session_start
|
|
137
|
+
SessionEnd: session_end
|
|
138
|
+
Stop: stop
|
|
139
|
+
PostToolUse: post_tool_use
|
|
140
|
+
PreToolUse: pre_tool_use
|
|
141
|
+
claude:
|
|
142
|
+
SessionStart: session_start
|
|
143
|
+
SessionEnd: session_end
|
|
144
|
+
Stop: stop
|
|
145
|
+
UserPromptSubmit: user_prompt_submit
|
|
146
|
+
PostToolUse: post_tool_use
|
|
147
|
+
PreToolUse: pre_tool_use
|
|
148
|
+
PreCompact: pre_compact
|
|
149
|
+
cursor:
|
|
150
|
+
sessionStart: session_start
|
|
151
|
+
sessionEnd: session_end
|
|
152
|
+
stop: stop
|
|
153
|
+
beforeSubmitPrompt: user_prompt_submit
|
|
154
|
+
postToolUse: post_tool_use
|
|
155
|
+
preToolUse: pre_tool_use
|
|
156
|
+
preCompact: pre_compact
|
|
157
|
+
cline:
|
|
158
|
+
TaskStart: session_start
|
|
159
|
+
TaskResume: session_start
|
|
160
|
+
TaskComplete: session_end
|
|
161
|
+
TaskCancel: stop
|
|
162
|
+
UserPromptSubmit: user_prompt_submit
|
|
163
|
+
PostToolUse: post_tool_use
|
|
164
|
+
PreToolUse: pre_tool_use
|
|
165
|
+
PreCompact: pre_compact
|
|
166
|
+
# Windsurf (Cascade) — snake_case event names per
|
|
167
|
+
# docs.windsurf.com/windsurf/cascade/hooks. post_cascade_response is
|
|
168
|
+
# async (off the critical path) so the per-turn append safely lands
|
|
169
|
+
# in the `stop` slot rather than `session_end`.
|
|
170
|
+
windsurf:
|
|
171
|
+
pre_user_prompt: user_prompt_submit
|
|
172
|
+
post_cascade_response: stop
|
|
173
|
+
post_setup_worktree: session_start
|
|
174
|
+
# Gemini CLI — PascalCase event names per geminicli.com/docs/hooks/.
|
|
175
|
+
# BeforeAgent fires after the user submits a prompt and before
|
|
176
|
+
# planning (carries the turn-check / append-on-prompt semantics).
|
|
177
|
+
# AfterAgent fires when the agent loop ends — our `stop` slot.
|
|
178
|
+
gemini:
|
|
179
|
+
SessionStart: session_start
|
|
180
|
+
SessionEnd: session_end
|
|
181
|
+
BeforeAgent: user_prompt_submit
|
|
182
|
+
AfterAgent: stop
|
|
183
|
+
BeforeTool: pre_tool_use
|
|
184
|
+
AfterTool: post_tool_use
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Augment Code universal hook trampoline (Phase 7.3, hook-architecture-v1.md).
|
|
3
|
+
#
|
|
4
|
+
# Replaces the four per-concern trampolines (augment-chat-history.sh,
|
|
5
|
+
# augment-roadmap-progress.sh, augment-onboarding-gate.sh,
|
|
6
|
+
# augment-context-hygiene.sh). One script, dispatched per (platform, event)
|
|
7
|
+
# tuple via scripts/hooks/dispatch_hook.py reading scripts/hook_manifest.yaml.
|
|
8
|
+
#
|
|
9
|
+
# Augment requires hook scripts to use the .sh extension and live at user
|
|
10
|
+
# scope (~/.augment/hooks/) — same constraint as the legacy trampolines.
|
|
11
|
+
#
|
|
12
|
+
# Behaviour:
|
|
13
|
+
# - Read the JSON event from stdin into a buffer.
|
|
14
|
+
# - Extract workspace_roots[0]; bail silently when missing.
|
|
15
|
+
# - cd into that workspace; bail silently when it is not a directory
|
|
16
|
+
# or does not contain ./agent-config.
|
|
17
|
+
# - Re-pipe the original JSON into
|
|
18
|
+
# ./agent-config dispatch:hook --platform augment \
|
|
19
|
+
# --event $1 --native-event $2
|
|
20
|
+
# - Always exit 0 — Augment hooks must never block the agent loop
|
|
21
|
+
# (chat-history / roadmap-progress / context-hygiene are observe-only;
|
|
22
|
+
# onboarding-gate writes state but does not deny SessionStart).
|
|
23
|
+
|
|
24
|
+
set -u
|
|
25
|
+
|
|
26
|
+
# Args from the platform's settings.json hook entry:
|
|
27
|
+
# $1 = agent-config event name (session_start, post_tool_use, …)
|
|
28
|
+
# $2 = Augment-native event name (SessionStart, PostToolUse, …)
|
|
29
|
+
EVENT="${1-}"
|
|
30
|
+
NATIVE_EVENT="${2-}"
|
|
31
|
+
|
|
32
|
+
if [ -z "$EVENT" ]; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
EVENT_DATA="$(cat)"
|
|
37
|
+
|
|
38
|
+
WORKSPACE=""
|
|
39
|
+
if command -v jq >/dev/null 2>&1; then
|
|
40
|
+
WORKSPACE="$(printf '%s' "$EVENT_DATA" \
|
|
41
|
+
| jq -r '.workspace_roots[0] // empty' 2>/dev/null)"
|
|
42
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
43
|
+
WORKSPACE="$(printf '%s' "$EVENT_DATA" | python3 -c '
|
|
44
|
+
import json, sys
|
|
45
|
+
try:
|
|
46
|
+
data = json.load(sys.stdin)
|
|
47
|
+
except Exception:
|
|
48
|
+
sys.exit(0)
|
|
49
|
+
roots = data.get("workspace_roots") or []
|
|
50
|
+
if roots:
|
|
51
|
+
print(roots[0])
|
|
52
|
+
' 2>/dev/null)"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
if [ -z "$WORKSPACE" ] || [ ! -d "$WORKSPACE" ]; then
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
cd "$WORKSPACE" 2>/dev/null || exit 0
|
|
60
|
+
|
|
61
|
+
if [ ! -x ./agent-config ]; then
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
printf '%s' "$EVENT_DATA" \
|
|
66
|
+
| ./agent-config dispatch:hook \
|
|
67
|
+
--platform augment \
|
|
68
|
+
--event "$EVENT" \
|
|
69
|
+
--native-event "$NATIVE_EVENT" \
|
|
70
|
+
>/dev/null 2>&1 || true
|
|
71
|
+
|
|
72
|
+
exit 0
|