@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.
Files changed (126) hide show
  1. package/.agent-src/commands/council/default.md +74 -76
  2. package/.agent-src/commands/feature/roadmap.md +22 -0
  3. package/.agent-src/commands/roadmap/create.md +38 -6
  4. package/.agent-src/commands/roadmap/execute.md +36 -9
  5. package/.agent-src/rules/agent-authority.md +1 -0
  6. package/.agent-src/rules/agent-docs.md +1 -0
  7. package/.agent-src/rules/analysis-skill-routing.md +1 -0
  8. package/.agent-src/rules/architecture.md +1 -0
  9. package/.agent-src/rules/artifact-drafting-protocol.md +1 -0
  10. package/.agent-src/rules/artifact-engagement-recording.md +1 -0
  11. package/.agent-src/rules/ask-when-uncertain.md +1 -0
  12. package/.agent-src/rules/augment-portability.md +1 -0
  13. package/.agent-src/rules/augment-source-of-truth.md +1 -0
  14. package/.agent-src/rules/autonomous-execution.md +1 -0
  15. package/.agent-src/rules/capture-learnings.md +1 -0
  16. package/.agent-src/rules/chat-history-cadence.md +34 -0
  17. package/.agent-src/rules/chat-history-ownership.md +1 -0
  18. package/.agent-src/rules/chat-history-visibility.md +1 -0
  19. package/.agent-src/rules/cli-output-handling.md +2 -2
  20. package/.agent-src/rules/command-suggestion-policy.md +1 -0
  21. package/.agent-src/rules/commit-conventions.md +1 -0
  22. package/.agent-src/rules/commit-policy.md +1 -0
  23. package/.agent-src/rules/context-hygiene.md +22 -0
  24. package/.agent-src/rules/direct-answers.md +1 -0
  25. package/.agent-src/rules/docker-commands.md +1 -0
  26. package/.agent-src/rules/docs-sync.md +1 -0
  27. package/.agent-src/rules/downstream-changes.md +1 -0
  28. package/.agent-src/rules/e2e-testing.md +1 -0
  29. package/.agent-src/rules/guidelines.md +1 -0
  30. package/.agent-src/rules/improve-before-implement.md +1 -0
  31. package/.agent-src/rules/language-and-tone.md +1 -0
  32. package/.agent-src/rules/laravel-translations.md +1 -0
  33. package/.agent-src/rules/markdown-safe-codeblocks.md +1 -0
  34. package/.agent-src/rules/minimal-safe-diff.md +1 -0
  35. package/.agent-src/rules/missing-tool-handling.md +1 -0
  36. package/.agent-src/rules/model-recommendation.md +1 -0
  37. package/.agent-src/rules/no-cheap-questions.md +1 -0
  38. package/.agent-src/rules/no-roadmap-references.md +1 -0
  39. package/.agent-src/rules/non-destructive-by-default.md +1 -0
  40. package/.agent-src/rules/onboarding-gate.md +26 -0
  41. package/.agent-src/rules/package-ci-checks.md +1 -0
  42. package/.agent-src/rules/php-coding.md +1 -0
  43. package/.agent-src/rules/preservation-guard.md +1 -0
  44. package/.agent-src/rules/review-routing-awareness.md +1 -0
  45. package/.agent-src/rules/reviewer-awareness.md +1 -0
  46. package/.agent-src/rules/roadmap-progress-sync.md +22 -0
  47. package/.agent-src/rules/role-mode-adherence.md +2 -2
  48. package/.agent-src/rules/rule-type-governance.md +1 -0
  49. package/.agent-src/rules/runtime-safety.md +1 -0
  50. package/.agent-src/rules/scope-control.md +1 -0
  51. package/.agent-src/rules/security-sensitive-stop.md +1 -0
  52. package/.agent-src/rules/size-enforcement.md +1 -0
  53. package/.agent-src/rules/skill-improvement-trigger.md +1 -0
  54. package/.agent-src/rules/skill-quality.md +1 -0
  55. package/.agent-src/rules/slash-command-routing-policy.md +39 -0
  56. package/.agent-src/rules/think-before-action.md +1 -0
  57. package/.agent-src/rules/token-efficiency.md +1 -0
  58. package/.agent-src/rules/tool-safety.md +1 -0
  59. package/.agent-src/rules/ui-audit-gate.md +1 -0
  60. package/.agent-src/rules/upstream-proposal.md +1 -0
  61. package/.agent-src/rules/user-interaction.md +1 -0
  62. package/.agent-src/rules/verify-before-complete.md +1 -0
  63. package/.agent-src/skills/roadmap-management/SKILL.md +29 -4
  64. package/.agent-src/skills/verify-completion-evidence/SKILL.md +8 -1
  65. package/.agent-src/templates/agent-settings.md +16 -0
  66. package/.agent-src/templates/roadmaps.md +8 -3
  67. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +9 -0
  68. package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +4 -0
  69. package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +4 -0
  70. package/.agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.py +163 -0
  71. package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +111 -0
  72. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +36 -0
  73. package/.agent-src/templates/scripts/work_engine/scoring/decision_trace.py +141 -0
  74. package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +125 -0
  75. package/.claude-plugin/marketplace.json +1 -1
  76. package/CHANGELOG.md +62 -0
  77. package/README.md +19 -19
  78. package/config/agent-settings.template.yml +23 -0
  79. package/docs/catalog.md +5 -2
  80. package/docs/contracts/adr-settings-sync-engine.md +127 -0
  81. package/docs/contracts/decision-trace-v1.md +146 -0
  82. package/docs/contracts/file-ownership-matrix.json +7 -0
  83. package/docs/contracts/hook-architecture-v1.md +213 -0
  84. package/docs/contracts/memory-visibility-v1.md +138 -0
  85. package/docs/contracts/one-off-script-lifecycle.md +109 -0
  86. package/docs/contracts/rule-interactions.yml +22 -0
  87. package/docs/customization.md +1 -0
  88. package/docs/development.md +4 -1
  89. package/docs/guidelines/agent-infra/layered-settings.md +32 -13
  90. package/package.json +1 -1
  91. package/scripts/agent-config +44 -0
  92. package/scripts/ai_council/bundler.py +3 -3
  93. package/scripts/ai_council/clients.py +24 -8
  94. package/scripts/ai_council/one_off_archive/2026-05/README.md +22 -0
  95. package/scripts/ai_council/one_off_archive/2026-05/_one_off_roundtrip.py +13 -8
  96. package/scripts/ai_council/one_off_archive/2026-05/_one_off_tier_retrofit.py +180 -0
  97. package/scripts/ai_council/session.py +92 -0
  98. package/scripts/capture_showcase_session.py +361 -0
  99. package/scripts/chat_history.py +11 -1
  100. package/scripts/check_always_budget.py +7 -2
  101. package/scripts/context_hygiene_hook.py +14 -6
  102. package/scripts/council_cli.py +357 -0
  103. package/scripts/hook_manifest.yaml +184 -0
  104. package/scripts/hooks/__init__.py +1 -0
  105. package/scripts/hooks/augment-dispatcher.sh +72 -0
  106. package/scripts/hooks/cline-dispatcher.sh +86 -0
  107. package/scripts/hooks/cursor-dispatcher.sh +76 -0
  108. package/scripts/hooks/dispatch_hook.py +348 -0
  109. package/scripts/hooks/envelope.py +98 -0
  110. package/scripts/hooks/gemini-dispatcher.sh +117 -0
  111. package/scripts/hooks/state_io.py +122 -0
  112. package/scripts/hooks/windsurf-dispatcher.sh +123 -0
  113. package/scripts/hooks_status.py +146 -0
  114. package/scripts/install.py +725 -87
  115. package/scripts/install.sh +1 -1
  116. package/scripts/lint_hook_manifest.py +216 -0
  117. package/scripts/lint_one_off_age.py +184 -0
  118. package/scripts/lint_rule_tiers.py +78 -0
  119. package/scripts/lint_showcase_sessions.py +148 -0
  120. package/scripts/minimal_safe_diff_hook.py +245 -0
  121. package/scripts/onboarding_gate_hook.py +13 -8
  122. package/scripts/readme_linter.py +12 -3
  123. package/scripts/roadmap_progress_hook.py +5 -0
  124. package/scripts/sync_agent_settings.py +32 -129
  125. package/scripts/sync_yaml_rt.py +734 -0
  126. 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": 7_887,
97
- "scope-control.md": 8_529,
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
- state_dir = consumer_root / STATE_DIR
122
- state_dir.mkdir(parents=True, exist_ok=True)
123
- target = consumer_root / STATE_FILE
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