devcouncil 0.2.0 → 0.2.1

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 (205) hide show
  1. package/README.md +191 -253
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +6 -17
  4. package/pyproject.toml +0 -66
  5. package/src/devcouncil/__init__.py +0 -0
  6. package/src/devcouncil/__main__.py +0 -4
  7. package/src/devcouncil/app/__init__.py +0 -28
  8. package/src/devcouncil/app/config.py +0 -320
  9. package/src/devcouncil/app/errors.py +0 -23
  10. package/src/devcouncil/app/events.py +0 -44
  11. package/src/devcouncil/app/orchestrator.py +0 -92
  12. package/src/devcouncil/app/project_status.py +0 -29
  13. package/src/devcouncil/app/run_context.py +0 -39
  14. package/src/devcouncil/app/state_machine.py +0 -108
  15. package/src/devcouncil/artifacts/__init__.py +0 -1
  16. package/src/devcouncil/artifacts/coverage.py +0 -96
  17. package/src/devcouncil/artifacts/graph.py +0 -163
  18. package/src/devcouncil/artifacts/migrations.py +0 -20
  19. package/src/devcouncil/artifacts/schemas.py +0 -23
  20. package/src/devcouncil/artifacts/serializer.py +0 -21
  21. package/src/devcouncil/artifacts/validators.py +0 -27
  22. package/src/devcouncil/assets/__init__.py +0 -1
  23. package/src/devcouncil/assets/devcouncil-logo.svg +0 -60
  24. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  25. package/src/devcouncil/cli/__init__.py +0 -0
  26. package/src/devcouncil/cli/commands/__init__.py +0 -0
  27. package/src/devcouncil/cli/commands/agents.py +0 -292
  28. package/src/devcouncil/cli/commands/artifacts.py +0 -54
  29. package/src/devcouncil/cli/commands/ast.py +0 -22
  30. package/src/devcouncil/cli/commands/baseline.py +0 -35
  31. package/src/devcouncil/cli/commands/check.py +0 -209
  32. package/src/devcouncil/cli/commands/config.py +0 -115
  33. package/src/devcouncil/cli/commands/cost.py +0 -57
  34. package/src/devcouncil/cli/commands/dashboard.py +0 -31
  35. package/src/devcouncil/cli/commands/doctor.py +0 -340
  36. package/src/devcouncil/cli/commands/evidence.py +0 -48
  37. package/src/devcouncil/cli/commands/go.py +0 -656
  38. package/src/devcouncil/cli/commands/handoff.py +0 -69
  39. package/src/devcouncil/cli/commands/hook.py +0 -237
  40. package/src/devcouncil/cli/commands/init.py +0 -289
  41. package/src/devcouncil/cli/commands/integrate.py +0 -1515
  42. package/src/devcouncil/cli/commands/lsp.py +0 -20
  43. package/src/devcouncil/cli/commands/map.py +0 -112
  44. package/src/devcouncil/cli/commands/mcp_server.py +0 -18
  45. package/src/devcouncil/cli/commands/plan.py +0 -488
  46. package/src/devcouncil/cli/commands/prompt.py +0 -61
  47. package/src/devcouncil/cli/commands/repair.py +0 -89
  48. package/src/devcouncil/cli/commands/report.py +0 -137
  49. package/src/devcouncil/cli/commands/reset_demo_state.py +0 -33
  50. package/src/devcouncil/cli/commands/rollback.py +0 -59
  51. package/src/devcouncil/cli/commands/run.py +0 -289
  52. package/src/devcouncil/cli/commands/runs.py +0 -223
  53. package/src/devcouncil/cli/commands/scaffold.py +0 -32
  54. package/src/devcouncil/cli/commands/semantic.py +0 -47
  55. package/src/devcouncil/cli/commands/setup.py +0 -362
  56. package/src/devcouncil/cli/commands/shell.py +0 -73
  57. package/src/devcouncil/cli/commands/show.py +0 -76
  58. package/src/devcouncil/cli/commands/skills.py +0 -88
  59. package/src/devcouncil/cli/commands/status.py +0 -141
  60. package/src/devcouncil/cli/commands/tasks.py +0 -55
  61. package/src/devcouncil/cli/commands/trace.py +0 -88
  62. package/src/devcouncil/cli/commands/verify.py +0 -328
  63. package/src/devcouncil/cli/commands/version.py +0 -20
  64. package/src/devcouncil/cli/commands/watch.py +0 -574
  65. package/src/devcouncil/cli/commands/watch_fs.py +0 -40
  66. package/src/devcouncil/cli/main.py +0 -137
  67. package/src/devcouncil/council/__init__.py +0 -0
  68. package/src/devcouncil/council/prompts/__init__.py +0 -0
  69. package/src/devcouncil/council/prompts/arbiter.md +0 -19
  70. package/src/devcouncil/council/prompts/critic_a.md +0 -10
  71. package/src/devcouncil/council/prompts/critic_b.md +0 -10
  72. package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
  73. package/src/devcouncil/council/prompts/planner_a.md +0 -16
  74. package/src/devcouncil/council/prompts/planner_b.md +0 -16
  75. package/src/devcouncil/council/prompts/rebuttal.md +0 -10
  76. package/src/devcouncil/council/prompts/spec_writer.md +0 -12
  77. package/src/devcouncil/domain/__init__.py +0 -0
  78. package/src/devcouncil/domain/assumption.py +0 -17
  79. package/src/devcouncil/domain/critique.py +0 -32
  80. package/src/devcouncil/domain/evidence.py +0 -47
  81. package/src/devcouncil/domain/gap.py +0 -52
  82. package/src/devcouncil/domain/requirement.py +0 -22
  83. package/src/devcouncil/domain/task.py +0 -55
  84. package/src/devcouncil/execution/__init__.py +0 -1
  85. package/src/devcouncil/execution/checkpoints.py +0 -246
  86. package/src/devcouncil/execution/context_builder.py +0 -60
  87. package/src/devcouncil/execution/executor.py +0 -15
  88. package/src/devcouncil/execution/fs_watcher.py +0 -180
  89. package/src/devcouncil/execution/handoff.py +0 -102
  90. package/src/devcouncil/execution/hook_policy.py +0 -253
  91. package/src/devcouncil/execution/patch.py +0 -77
  92. package/src/devcouncil/execution/paths.py +0 -14
  93. package/src/devcouncil/execution/permissions.py +0 -85
  94. package/src/devcouncil/execution/policy_engine.py +0 -343
  95. package/src/devcouncil/execution/prompt_builder.py +0 -671
  96. package/src/devcouncil/execution/shell_session.py +0 -225
  97. package/src/devcouncil/execution/task_runner.py +0 -170
  98. package/src/devcouncil/executors/__init__.py +0 -1
  99. package/src/devcouncil/executors/agent_registry.py +0 -575
  100. package/src/devcouncil/executors/coding_cli.py +0 -736
  101. package/src/devcouncil/executors/mini_swe.py +0 -73
  102. package/src/devcouncil/executors/native/__init__.py +0 -0
  103. package/src/devcouncil/executors/native/agent.py +0 -208
  104. package/src/devcouncil/executors/openhands.py +0 -71
  105. package/src/devcouncil/gating/__init__.py +0 -1
  106. package/src/devcouncil/gating/checks/__init__.py +0 -0
  107. package/src/devcouncil/gating/checks/clean_git.py +0 -52
  108. package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
  109. package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
  110. package/src/devcouncil/gating/checks/secret_scan_check.py +0 -53
  111. package/src/devcouncil/gating/policy.py +0 -338
  112. package/src/devcouncil/hardware.py +0 -184
  113. package/src/devcouncil/indexing/__init__.py +0 -1
  114. package/src/devcouncil/indexing/ast_matcher.py +0 -168
  115. package/src/devcouncil/indexing/graph_index.py +0 -48
  116. package/src/devcouncil/indexing/lsp.py +0 -161
  117. package/src/devcouncil/indexing/repo_mapper.py +0 -1455
  118. package/src/devcouncil/indexing/semantic_index.py +0 -205
  119. package/src/devcouncil/integrations/actions.py +0 -146
  120. package/src/devcouncil/integrations/check.py +0 -423
  121. package/src/devcouncil/integrations/code_review_graph.py +0 -163
  122. package/src/devcouncil/integrations/github.py +0 -39
  123. package/src/devcouncil/integrations/github_intent.py +0 -142
  124. package/src/devcouncil/integrations/gitnexus.py +0 -62
  125. package/src/devcouncil/integrations/graphify.py +0 -34
  126. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  127. package/src/devcouncil/integrations/mcp/server.py +0 -2122
  128. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +0 -24
  129. package/src/devcouncil/integrations/pr_comments.py +0 -62
  130. package/src/devcouncil/live/__init__.py +0 -2
  131. package/src/devcouncil/live/cards.py +0 -349
  132. package/src/devcouncil/live/models.py +0 -63
  133. package/src/devcouncil/live/repair_prompt.py +0 -83
  134. package/src/devcouncil/live/reviewer.py +0 -70
  135. package/src/devcouncil/live/signals.py +0 -135
  136. package/src/devcouncil/live/summary.py +0 -34
  137. package/src/devcouncil/live/tasks.py +0 -18
  138. package/src/devcouncil/live/transcripts.py +0 -141
  139. package/src/devcouncil/llm/__init__.py +0 -1
  140. package/src/devcouncil/llm/cache.py +0 -42
  141. package/src/devcouncil/llm/model_defaults.yaml +0 -44
  142. package/src/devcouncil/llm/provider.py +0 -627
  143. package/src/devcouncil/llm/router.py +0 -310
  144. package/src/devcouncil/optimization/__init__.py +0 -1
  145. package/src/devcouncil/optimization/gepa_agent.py +0 -318
  146. package/src/devcouncil/planning/__init__.py +0 -1
  147. package/src/devcouncil/planning/arbiter_service.py +0 -57
  148. package/src/devcouncil/planning/correction_manifest.py +0 -303
  149. package/src/devcouncil/planning/critique_service.py +0 -71
  150. package/src/devcouncil/planning/plan_service.py +0 -60
  151. package/src/devcouncil/planning/prompt_enhancer_service.py +0 -167
  152. package/src/devcouncil/planning/repair_service.py +0 -39
  153. package/src/devcouncil/planning/spec_service.py +0 -70
  154. package/src/devcouncil/repo/__init__.py +0 -0
  155. package/src/devcouncil/repo/ci_scaffold.py +0 -157
  156. package/src/devcouncil/repo/gitignore.py +0 -123
  157. package/src/devcouncil/repo/sca.py +0 -374
  158. package/src/devcouncil/reporting/__init__.py +0 -0
  159. package/src/devcouncil/reporting/github_check.py +0 -32
  160. package/src/devcouncil/reporting/json_report.py +0 -30
  161. package/src/devcouncil/reporting/markdown_report.py +0 -83
  162. package/src/devcouncil/reporting/report_builder.py +0 -14
  163. package/src/devcouncil/skills/__init__.py +0 -19
  164. package/src/devcouncil/skills/library/README.md +0 -46
  165. package/src/devcouncil/skills/library/ai-training.md +0 -50
  166. package/src/devcouncil/skills/library/android.md +0 -50
  167. package/src/devcouncil/skills/library/backend.md +0 -52
  168. package/src/devcouncil/skills/library/core-engineering.md +0 -95
  169. package/src/devcouncil/skills/library/data-engineering.md +0 -47
  170. package/src/devcouncil/skills/library/desktop.md +0 -46
  171. package/src/devcouncil/skills/library/devops.md +0 -48
  172. package/src/devcouncil/skills/library/game-dev.md +0 -46
  173. package/src/devcouncil/skills/library/ios.md +0 -48
  174. package/src/devcouncil/skills/library/mobile-cross-platform.md +0 -46
  175. package/src/devcouncil/skills/library/security.md +0 -48
  176. package/src/devcouncil/skills/library/systems.md +0 -48
  177. package/src/devcouncil/skills/library/web.md +0 -47
  178. package/src/devcouncil/skills/library/windows.md +0 -47
  179. package/src/devcouncil/skills/registry.py +0 -330
  180. package/src/devcouncil/storage/__init__.py +0 -0
  181. package/src/devcouncil/storage/db.py +0 -147
  182. package/src/devcouncil/storage/models.py +0 -204
  183. package/src/devcouncil/storage/native.py +0 -557
  184. package/src/devcouncil/storage/repositories.py +0 -485
  185. package/src/devcouncil/telemetry/__init__.py +0 -0
  186. package/src/devcouncil/telemetry/cost.py +0 -140
  187. package/src/devcouncil/telemetry/model_pricing.yaml +0 -48
  188. package/src/devcouncil/telemetry/pricing.py +0 -28
  189. package/src/devcouncil/telemetry/traces.py +0 -146
  190. package/src/devcouncil/telemetry/tracker.py +0 -52
  191. package/src/devcouncil/ui/__init__.py +0 -1
  192. package/src/devcouncil/ui/dashboard.py +0 -423
  193. package/src/devcouncil/utils/__init__.py +0 -1
  194. package/src/devcouncil/utils/redaction.py +0 -147
  195. package/src/devcouncil/utils/subprocess_env.py +0 -69
  196. package/src/devcouncil/verification/__init__.py +0 -1
  197. package/src/devcouncil/verification/acceptance_compiler.py +0 -125
  198. package/src/devcouncil/verification/ad_hoc_check.py +0 -129
  199. package/src/devcouncil/verification/diff_coverage.py +0 -353
  200. package/src/devcouncil/verification/implementation_reviewer.py +0 -55
  201. package/src/devcouncil/verification/next_actions.py +0 -189
  202. package/src/devcouncil/verification/sandbox.py +0 -178
  203. package/src/devcouncil/verification/test_resolver.py +0 -91
  204. package/src/devcouncil/verification/verifier.py +0 -1548
  205. package/uv.lock +0 -1226
@@ -1,1515 +0,0 @@
1
- import json
2
- import shlex
3
- import shutil
4
- import subprocess
5
- import sys
6
- from contextlib import contextmanager
7
- from pathlib import Path
8
-
9
- import typer
10
- import yaml # type: ignore[import-untyped]
11
- from rich.console import Console
12
- from rich.table import Table
13
-
14
- from devcouncil.executors.agent_registry import (
15
- BUILTIN_CODING_EXECUTOR_NAMES,
16
- CODING_CLI_INTEGRATION_INFO,
17
- VALID_INPUT_MODES,
18
- agent_config_entry,
19
- detect_available_coding_cli,
20
- integration_tier_label,
21
- is_reserved_agent_name,
22
- load_agent_profiles,
23
- load_cli_agent_specs,
24
- normalize_agent_name,
25
- resolve_automated_executor,
26
- resolve_coding_cli_executable,
27
- resolve_coding_cli_probe_order,
28
- )
29
- from devcouncil.integrations.actions import apply_integration_target
30
- from devcouncil.utils.subprocess_env import clean_subprocess_env
31
- from devcouncil.integrations.check import (
32
- build_integration_check_report,
33
- integration_status_summary,
34
- )
35
-
36
- app = typer.Typer(help="Set up DevCouncil integrations with coding CLIs.")
37
- setup_app = typer.Typer(help="Set up optional external companion integrations.")
38
- app.add_typer(setup_app, name="setup")
39
- console = Console()
40
-
41
- SUPPORTED_TOOLS = ("codex", "gemini", "claude", "cursor", "opencode", "antigravity", "warp", "aider")
42
- SUPPORTED_HOOK_TOOLS = ("codex", "gemini", "claude", "cursor")
43
- OPENCODE_HOOK_PLUGIN_NAME = "opencode_devcouncil_plugin.mjs"
44
- PREFERRED_COMMAND = "dev integrate"
45
- LEGACY_COMMAND = "dev setup --integrate"
46
-
47
-
48
- def _project_root(path: str | Path | None) -> Path:
49
- return Path(path or ".").expanduser().resolve()
50
-
51
-
52
- def _warn_if_verify_only(client: str) -> None:
53
- """Print a prominent containment warning when wiring a verify-only client.
54
-
55
- Verify-only clients have no native pre-tool-use hook, so DevCouncil cannot block a
56
- forbidden write or command before it happens — it is only caught post-hoc at verify
57
- time. Surface this loudly so users don't assume hard containment."""
58
- info = CODING_CLI_INTEGRATION_INFO.get(normalize_agent_name(client))
59
- if info is not None and not info.hooks:
60
- console.print(
61
- f"[bold yellow]Warning ({info.label}): No pre-action containment — "
62
- "forbidden writes/commands are caught only at verify time.[/bold yellow]"
63
- )
64
-
65
-
66
- def _server_args(project_root: Path) -> list[str]:
67
- return ["devcouncil", "mcp-server"]
68
-
69
-
70
- def _codex_command(project_root: Path) -> list[str]:
71
- return [
72
- "codex",
73
- "mcp",
74
- "add",
75
- "devcouncil",
76
- "--env",
77
- f"DEVCOUNCIL_PROJECT_ROOT={project_root}",
78
- "--",
79
- *_server_args(project_root),
80
- ]
81
-
82
-
83
- def _gemini_command(project_root: Path, scope: str) -> list[str]:
84
- return [
85
- "gemini",
86
- "mcp",
87
- "add",
88
- "--scope",
89
- scope,
90
- "--env",
91
- f"DEVCOUNCIL_PROJECT_ROOT={project_root}",
92
- "devcouncil",
93
- *_server_args(project_root),
94
- ]
95
-
96
-
97
- def _claude_command(project_root: Path, scope: str) -> list[str]:
98
- # The server name must come BEFORE --env: the current Claude CLI treats --env
99
- # as variadic, so `--env KEY=VALUE devcouncil` swallows the name `devcouncil`
100
- # as a second (invalid) env var. Putting the name first — matching the working
101
- # codex form — and terminating options with `--` avoids that.
102
- return [
103
- "claude",
104
- "mcp",
105
- "add",
106
- "--scope",
107
- scope,
108
- "devcouncil",
109
- "--env",
110
- f"DEVCOUNCIL_PROJECT_ROOT={project_root}",
111
- "--",
112
- *_server_args(project_root),
113
- ]
114
-
115
-
116
- def _cursor_config_path(project_root: Path) -> Path:
117
- return project_root / ".cursor" / "mcp.json"
118
-
119
-
120
- def _cursor_mcp_config(project_root: Path) -> dict:
121
- return {
122
- "mcpServers": {
123
- "devcouncil": {
124
- "type": "stdio",
125
- "command": "devcouncil",
126
- "args": ["mcp-server"],
127
- "env": {"DEVCOUNCIL_PROJECT_ROOT": str(project_root)},
128
- }
129
- }
130
- }
131
-
132
-
133
- def _warp_mcp_config(project_root: Path) -> dict:
134
- return {
135
- "devcouncil": {
136
- "command": "devcouncil",
137
- "args": ["mcp-server"],
138
- "env": {"DEVCOUNCIL_PROJECT_ROOT": str(project_root)},
139
- }
140
- }
141
-
142
-
143
- def _warp_mcp_path(project_root: Path) -> Path:
144
- return project_root / ".devcouncil" / "integrations" / "warp-mcp.json"
145
-
146
-
147
- def _opencode_config_path(project_root: Path) -> Path:
148
- return project_root / "opencode.json"
149
-
150
-
151
- def _opencode_mcp_entry(project_root: Path) -> dict:
152
- return {
153
- "type": "local",
154
- "command": ["devcouncil", "mcp-server"],
155
- "environment": {"DEVCOUNCIL_PROJECT_ROOT": str(project_root)},
156
- "enabled": True,
157
- "timeout": 10000,
158
- }
159
-
160
-
161
- def _antigravity_mcp_path(project_root: Path) -> Path:
162
- return project_root / ".agents" / "mcp_config.json"
163
-
164
-
165
- def _antigravity_mcp_config(project_root: Path) -> dict:
166
- return {
167
- "mcpServers": {
168
- "devcouncil": {
169
- "command": "devcouncil",
170
- "args": ["mcp-server"],
171
- "env": {"DEVCOUNCIL_PROJECT_ROOT": str(project_root)},
172
- "cwd": str(project_root),
173
- }
174
- }
175
- }
176
-
177
-
178
- def _write_warp_mcp_config(project_root: Path) -> Path:
179
- path = _warp_mcp_path(project_root)
180
- _save_json(path, _warp_mcp_config(project_root))
181
- return path
182
-
183
-
184
- def _write_cursor_config(project_root: Path) -> Path:
185
- path = _cursor_config_path(project_root)
186
- data = _load_json_strict(path, "Cursor")
187
- mcp_servers = data.setdefault("mcpServers", {})
188
- mcp_servers["devcouncil"] = _cursor_mcp_config(project_root)["mcpServers"]["devcouncil"]
189
- _save_json(path, data)
190
- return path
191
-
192
-
193
- # When set, _mutate_raw_config applies record mutations in memory and
194
- # _batched_raw_config saves config.yaml once at the end (used by
195
- # `dev integrate all --apply`, which otherwise re-parses YAML per tool).
196
- _PENDING_RAW_CONFIG: dict | None = None
197
-
198
-
199
- @contextmanager
200
- def _batched_raw_config(project_root: Path):
201
- global _PENDING_RAW_CONFIG
202
- _PENDING_RAW_CONFIG = _load_raw_config(project_root)
203
- try:
204
- yield
205
- _save_raw_config(project_root, _PENDING_RAW_CONFIG)
206
- finally:
207
- _PENDING_RAW_CONFIG = None
208
-
209
-
210
- def _mutate_raw_config(project_root: Path, mutate) -> None:
211
- if _PENDING_RAW_CONFIG is not None:
212
- mutate(_PENDING_RAW_CONFIG)
213
- return
214
- config = _load_raw_config(project_root)
215
- mutate(config)
216
- _save_raw_config(project_root, config)
217
-
218
-
219
- def _record_cursor_config(project_root: Path) -> None:
220
- def mutate(config: dict) -> None:
221
- cursor = config.setdefault("integrations", {}).setdefault("cursor", {})
222
- cursor.update({
223
- "enabled": True,
224
- "config_path": str(_cursor_config_path(project_root).relative_to(project_root)),
225
- })
226
-
227
- _mutate_raw_config(project_root, mutate)
228
-
229
-
230
- def _record_warp_config(project_root: Path) -> None:
231
- def mutate(config: dict) -> None:
232
- warp = config.setdefault("integrations", {}).setdefault("warp", {})
233
- warp.update({
234
- "enabled": True,
235
- "command": warp.get("command", "oz"),
236
- "run_mode": warp.get("run_mode", "local"),
237
- "mcp_config_path": str(_warp_mcp_path(project_root).relative_to(project_root)),
238
- })
239
-
240
- _mutate_raw_config(project_root, mutate)
241
-
242
-
243
- def _record_opencode_config(project_root: Path) -> None:
244
- def mutate(config: dict) -> None:
245
- opencode = config.setdefault("integrations", {}).setdefault("opencode", {})
246
- opencode.update({
247
- "enabled": True,
248
- "config_path": str(_opencode_config_path(project_root).relative_to(project_root)),
249
- })
250
-
251
- _mutate_raw_config(project_root, mutate)
252
-
253
-
254
- def _record_antigravity_config(project_root: Path) -> None:
255
- def mutate(config: dict) -> None:
256
- antigravity = config.setdefault("integrations", {}).setdefault("antigravity", {})
257
- antigravity.update({
258
- "enabled": True,
259
- "mcp_config_path": str(_antigravity_mcp_path(project_root).relative_to(project_root)),
260
- })
261
-
262
- _mutate_raw_config(project_root, mutate)
263
-
264
-
265
- def _load_json_strict(path: Path, label: str = "JSON") -> dict:
266
- if not path.exists():
267
- return {}
268
- try:
269
- return json.loads(path.read_text(encoding="utf-8")) or {}
270
- except json.JSONDecodeError as exc:
271
- raise ValueError(f"{path} is not valid JSON. Fix the {label} config before rerunning integration setup.") from exc
272
-
273
-
274
- def _write_opencode_config(project_root: Path) -> Path:
275
- path = _opencode_config_path(project_root)
276
- data = _load_json_strict(path, "OpenCode")
277
- data.setdefault("$schema", "https://opencode.ai/config.json")
278
- mcp = data.setdefault("mcp", {})
279
- mcp["devcouncil"] = _opencode_mcp_entry(project_root)
280
- _save_json(path, data)
281
- return path
282
-
283
-
284
- def _write_antigravity_mcp_config(project_root: Path) -> Path:
285
- path = _antigravity_mcp_path(project_root)
286
- data = _load_json_strict(path, "Antigravity")
287
- mcp_servers = data.setdefault("mcpServers", {})
288
- mcp_servers["devcouncil"] = _antigravity_mcp_config(project_root)["mcpServers"]["devcouncil"]
289
- _save_json(path, data)
290
- return path
291
-
292
-
293
- def _configure_cursor(project_root: Path, apply: bool) -> bool:
294
- path = _cursor_config_path(project_root)
295
- config = _cursor_mcp_config(project_root)
296
- if not apply:
297
- console.print("[bold]Cursor[/bold]")
298
- console.print(f"Project MCP config file: [dim]{path}[/dim]")
299
- console.print(json.dumps(config, separators=(",", ":")), soft_wrap=True)
300
- console.print("Verify in Cursor CLI with: [dim]cursor-agent mcp list[/dim]")
301
- return True
302
-
303
- if not shutil.which("cursor") and not shutil.which("cursor-agent"):
304
- console.print("[yellow]Cursor CLI not found on PATH. Project MCP config will still be available to Cursor.[/yellow]")
305
- try:
306
- written = _write_cursor_config(project_root)
307
- except ValueError as exc:
308
- console.print(f"[red]{exc}[/red]")
309
- return False
310
- _record_cursor_config(project_root)
311
- console.print(f"[green]Cursor MCP config written:[/green] {written}")
312
- return True
313
-
314
-
315
- def _configure_opencode(project_root: Path, apply: bool) -> bool:
316
- path = _opencode_config_path(project_root)
317
- config = {
318
- "$schema": "https://opencode.ai/config.json",
319
- "mcp": {"devcouncil": _opencode_mcp_entry(project_root)},
320
- }
321
- if not apply:
322
- console.print("[bold]OpenCode[/bold]")
323
- console.print(f"Project config file: [dim]{path}[/dim]")
324
- console.print(json.dumps(config, separators=(",", ":")), soft_wrap=True)
325
- console.print(
326
- "Direct executor command: "
327
- "[dim]opencode run --file .devcouncil/TASK-001-opencode-task.md "
328
- '"Execute the DevCouncil task described in the attached prompt file."[/dim]'
329
- )
330
- return True
331
-
332
- if not shutil.which("opencode"):
333
- console.print("[yellow]OpenCode CLI not found on PATH. Install it before using `dev run --executor opencode`.[/yellow]")
334
- try:
335
- written = _write_opencode_config(project_root)
336
- except ValueError as exc:
337
- console.print(f"[red]{exc}[/red]")
338
- return False
339
- _record_opencode_config(project_root)
340
- console.print(f"[green]OpenCode MCP config written:[/green] {written}")
341
- return True
342
-
343
-
344
- def _configure_antigravity(project_root: Path, apply: bool) -> bool:
345
- path = _antigravity_mcp_path(project_root)
346
- config = _antigravity_mcp_config(project_root)
347
- if not apply:
348
- console.print("[bold]Google Antigravity CLI[/bold]")
349
- console.print(f"Project MCP config file: [dim]{path}[/dim]")
350
- console.print(json.dumps(config, separators=(",", ":")), soft_wrap=True)
351
- console.print(
352
- "Direct executor command: "
353
- "[dim]agy --print --print-timeout 30m "
354
- '"Read and execute the DevCouncil task prompt at .devcouncil/TASK-001-antigravity-task.md."[/dim]'
355
- )
356
- return True
357
-
358
- if not shutil.which("agy"):
359
- console.print("[yellow]Antigravity CLI (`agy`) not found on PATH. Install it before using `dev run --executor antigravity`.[/yellow]")
360
- try:
361
- written = _write_antigravity_mcp_config(project_root)
362
- except ValueError as exc:
363
- console.print(f"[red]{exc}[/red]")
364
- return False
365
- _record_antigravity_config(project_root)
366
- console.print(f"[green]Antigravity MCP config written:[/green] {written}")
367
- return True
368
-
369
-
370
- def _configure_warp(project_root: Path, apply: bool) -> bool:
371
- path = _warp_mcp_path(project_root)
372
- config = _warp_mcp_config(project_root)
373
- if not apply:
374
- console.print("[bold]Warp / Oz[/bold]")
375
- console.print(f"MCP config file: [dim]{path}[/dim]")
376
- console.print(json.dumps(config, separators=(",", ":")), soft_wrap=True)
377
- console.print(f"Direct executor command: [dim]oz agent run --cwd {project_root} --mcp {path} --prompt <task prompt>[/dim]")
378
- return True
379
-
380
- written = _write_warp_mcp_config(project_root)
381
- _record_warp_config(project_root)
382
- console.print(f"[green]Warp MCP config written:[/green] {written}")
383
- if not shutil.which("oz"):
384
- console.print("[yellow]oz CLI not found on PATH. Install Warp/Oz before using `dev run --executor warp`.[/yellow]")
385
- return True
386
-
387
-
388
- def _format_command(command: list[str]) -> str:
389
- if sys.platform == "win32":
390
- return " ".join(_quote_powershell_arg(arg) for arg in command)
391
- return shlex.join(command)
392
-
393
-
394
- def _quote_powershell_arg(arg: str) -> str:
395
- if arg == "":
396
- return "''"
397
- special_chars = set(" \t\r\n'\"{}[](),;|&<>")
398
- if not any(char in special_chars for char in arg):
399
- return arg
400
- return "'" + arg.replace("'", "''") + "'"
401
-
402
-
403
- def _opencode_plugin_source() -> Path:
404
- return Path(__file__).resolve().parents[2] / "integrations" / OPENCODE_HOOK_PLUGIN_NAME
405
-
406
-
407
- def _opencode_plugin_path(project_root: Path) -> Path:
408
- return project_root / ".devcouncil" / "integrations" / OPENCODE_HOOK_PLUGIN_NAME
409
-
410
-
411
- def _hook_command(project_root: Path, client: str, event: str) -> str:
412
- return _format_command([
413
- "devcouncil",
414
- "hook",
415
- event,
416
- "--client",
417
- client,
418
- "--project-root",
419
- str(project_root),
420
- ])
421
-
422
-
423
- def _probe_mcp_tools(root: Path, *, timeout_seconds: float = 30.0) -> list[str]:
424
- from mcp import ClientSession, StdioServerParameters
425
- from mcp.client.stdio import stdio_client
426
- import asyncio
427
- import os
428
-
429
- async def _list_tools() -> list[str]:
430
- env = os.environ.copy()
431
- env["DEVCOUNCIL_PROJECT_ROOT"] = str(root)
432
- params = StdioServerParameters(
433
- command=sys.executable,
434
- args=["-m", "devcouncil", "mcp-server"],
435
- cwd=str(root),
436
- env=env,
437
- )
438
- async with stdio_client(params) as (read, write):
439
- async with ClientSession(read, write) as session:
440
- await session.initialize()
441
- tools = await session.list_tools()
442
- return [tool.name for tool in tools.tools]
443
-
444
- async def _list_tools_with_deadline() -> list[str]:
445
- # A wedged server process would otherwise block `dev integrate check`
446
- # indefinitely; the caller treats TimeoutError as a failed probe.
447
- return await asyncio.wait_for(_list_tools(), timeout=timeout_seconds)
448
-
449
- return asyncio.run(_list_tools_with_deadline())
450
-
451
-
452
- def _run(command: list[str]) -> int:
453
- executable = shutil.which(command[0])
454
- if not executable:
455
- return 127
456
- resolved = [executable, *command[1:]]
457
- use_shell = sys.platform == "win32" and Path(executable).suffix.lower() in {".bat", ".cmd", ".ps1"}
458
- invocation = subprocess.list2cmdline(resolved) if use_shell else resolved
459
- try:
460
- result = subprocess.run(invocation, text=True, shell=use_shell)
461
- except (FileNotFoundError, OSError):
462
- return 127
463
- return result.returncode
464
-
465
-
466
- def _run_capture(command: list[str], timeout: int = 10) -> tuple[int, str]:
467
- executable = shutil.which(command[0])
468
- if not executable:
469
- return 127, f"{command[0]} not found on PATH"
470
-
471
- resolved = [executable, *command[1:]]
472
- use_shell = sys.platform == "win32" and Path(executable).suffix.lower() in {".bat", ".cmd", ".ps1"}
473
- invocation = subprocess.list2cmdline(resolved) if use_shell else resolved
474
- try:
475
- result = subprocess.run(
476
- invocation,
477
- capture_output=True,
478
- text=True,
479
- encoding="utf-8",
480
- errors="replace",
481
- shell=use_shell,
482
- timeout=timeout,
483
- env=clean_subprocess_env(),
484
- )
485
- except subprocess.TimeoutExpired:
486
- return 124, "timed out"
487
- except (FileNotFoundError, OSError) as exc:
488
- return 127, f"{command[0]} could not be executed: {exc}"
489
- return result.returncode, (result.stdout + result.stderr).strip()
490
-
491
-
492
- def _config_path(project_root: Path) -> Path:
493
- return project_root / ".devcouncil" / "config.yaml"
494
-
495
-
496
- def _load_raw_config(project_root: Path) -> dict:
497
- path = _config_path(project_root)
498
- if not path.exists():
499
- return {}
500
- return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
501
-
502
-
503
- def _save_raw_config(project_root: Path, config: dict) -> None:
504
- path = _config_path(project_root)
505
- path.parent.mkdir(parents=True, exist_ok=True)
506
- path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8")
507
-
508
-
509
- def _load_json(path: Path) -> dict:
510
- if not path.exists():
511
- return {}
512
- try:
513
- return json.loads(path.read_text(encoding="utf-8")) or {}
514
- except json.JSONDecodeError:
515
- return {}
516
-
517
-
518
- def _save_json(path: Path, data: dict) -> None:
519
- path.parent.mkdir(parents=True, exist_ok=True)
520
- path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
521
-
522
-
523
- def _upsert_hook(settings: dict, event: str, matcher: str, command: str, name: str) -> None:
524
- hooks = settings.setdefault("hooks", {})
525
- groups = hooks.setdefault(event, [])
526
- for group in groups:
527
- if group.get("matcher") == matcher:
528
- group_hooks = group.setdefault("hooks", [])
529
- if not any(hook.get("command") == command for hook in group_hooks):
530
- group_hooks.append({
531
- "type": "command",
532
- "name": name,
533
- "command": command,
534
- "timeout": 10000,
535
- })
536
- return
537
- groups.append({
538
- "matcher": matcher,
539
- "hooks": [{
540
- "type": "command",
541
- "name": name,
542
- "command": command,
543
- "timeout": 10000,
544
- }],
545
- })
546
-
547
-
548
- def _ensure_codex_hooks_enabled(project_root: Path) -> Path:
549
- config_path = project_root / ".codex" / "config.toml"
550
- config_path.parent.mkdir(parents=True, exist_ok=True)
551
- existing = config_path.read_text(encoding="utf-8") if config_path.exists() else ""
552
- if "codex_hooks" not in existing:
553
- if "[features]" in existing:
554
- lines = existing.splitlines()
555
- updated: list[str] = []
556
- in_features = False
557
- inserted = False
558
- for line in lines:
559
- stripped = line.strip()
560
- if stripped == "[features]":
561
- in_features = True
562
- updated.append(line)
563
- continue
564
- if in_features and stripped.startswith("[") and stripped.endswith("]"):
565
- updated.append("codex_hooks = true")
566
- inserted = True
567
- in_features = False
568
- updated.append(line)
569
- if in_features and not inserted:
570
- updated.append("codex_hooks = true")
571
- config_path.write_text("\n".join(updated) + "\n", encoding="utf-8")
572
- else:
573
- separator = "\n" if existing and not existing.endswith("\n") else ""
574
- config_path.write_text(f"{existing}{separator}\n[features]\ncodex_hooks = true\n", encoding="utf-8")
575
- return config_path
576
-
577
-
578
- def _install_codex_hooks(project_root: Path) -> list[Path]:
579
- path = project_root / ".codex" / "hooks.json"
580
- settings = _load_json(path)
581
- matcher = "Bash|shell_command|exec_command|local_shell|Write|Edit|MultiEdit|write_file|edit_file|apply_patch"
582
- _upsert_hook(
583
- settings,
584
- "PreToolUse",
585
- matcher,
586
- _hook_command(project_root, "codex", "pre-tool-use"),
587
- "devcouncil-pre-tool-use",
588
- )
589
- _upsert_hook(
590
- settings,
591
- "PostToolUse",
592
- matcher,
593
- _hook_command(project_root, "codex", "post-tool-use"),
594
- "devcouncil-post-tool-use",
595
- )
596
- _save_json(path, settings)
597
- return [path, _ensure_codex_hooks_enabled(project_root)]
598
-
599
-
600
- def _install_gemini_hooks(project_root: Path) -> list[Path]:
601
- path = project_root / ".gemini" / "settings.json"
602
- settings = _load_json(path)
603
- matcher = "run_shell_command|shell_command|write_file|edit_file|replace|apply_patch"
604
- _upsert_hook(
605
- settings,
606
- "BeforeTool",
607
- matcher,
608
- _hook_command(project_root, "gemini", "pre-tool-use"),
609
- "devcouncil-pre-tool-use",
610
- )
611
- _upsert_hook(
612
- settings,
613
- "AfterTool",
614
- matcher,
615
- _hook_command(project_root, "gemini", "post-tool-use"),
616
- "devcouncil-post-tool-use",
617
- )
618
- _save_json(path, settings)
619
- return [path]
620
-
621
-
622
- def _upsert_cursor_hook(settings: dict, event: str, matcher: str, command: str) -> None:
623
- hooks = settings.setdefault("hooks", {})
624
- entries = hooks.setdefault(event, [])
625
- for entry in entries:
626
- if entry.get("command") == command:
627
- return
628
- payload: dict = {"command": command}
629
- if matcher:
630
- payload["matcher"] = matcher
631
- entries.append(payload)
632
-
633
-
634
- def _install_cursor_hooks(project_root: Path) -> list[Path]:
635
- path = project_root / ".cursor" / "hooks.json"
636
- settings = _load_json(path)
637
- settings.setdefault("version", 1)
638
- matcher = "Shell|Write|Edit|MultiEdit|Read|Task"
639
- _upsert_cursor_hook(
640
- settings,
641
- "preToolUse",
642
- matcher,
643
- _hook_command(project_root, "cursor", "pre-tool-use"),
644
- )
645
- _upsert_cursor_hook(
646
- settings,
647
- "postToolUse",
648
- matcher,
649
- _hook_command(project_root, "cursor", "post-tool-use"),
650
- )
651
- _save_json(path, settings)
652
- config = _load_raw_config(project_root)
653
- integrations = config.setdefault("integrations", {})
654
- cursor = integrations.setdefault("cursor", {})
655
- cursor.update({
656
- "hooks_path": str(path.relative_to(project_root)),
657
- })
658
- _save_raw_config(project_root, config)
659
- return [path]
660
-
661
-
662
- def _install_opencode_hooks(project_root: Path) -> list[Path]:
663
- source = _opencode_plugin_source()
664
- if not source.exists():
665
- raise FileNotFoundError(f"Missing bundled OpenCode hook plugin: {source}")
666
- destination = _opencode_plugin_path(project_root)
667
- destination.parent.mkdir(parents=True, exist_ok=True)
668
- destination.write_text(source.read_text(encoding="utf-8"), encoding="utf-8")
669
-
670
- path = _opencode_config_path(project_root)
671
- data = _load_json_strict(path, "OpenCode") if path.exists() else {"$schema": "https://opencode.ai/config.json"}
672
- data.setdefault("$schema", "https://opencode.ai/config.json")
673
- plugins_raw = data.setdefault("plugin", [])
674
- if not isinstance(plugins_raw, list):
675
- plugins_raw = []
676
- data["plugin"] = plugins_raw
677
- plugins: list[str] = [str(item) for item in plugins_raw]
678
- data["plugin"] = plugins
679
- plugin_ref = f"./.devcouncil/integrations/{OPENCODE_HOOK_PLUGIN_NAME}"
680
- if plugin_ref not in plugins:
681
- plugins.append(plugin_ref)
682
- _save_json(path, data)
683
- _record_opencode_config(project_root)
684
- return [destination, path]
685
-
686
-
687
- def _install_claude_hooks(project_root: Path) -> list[Path]:
688
- path = project_root / ".claude" / "settings.local.json"
689
- settings = _load_json(path)
690
- matcher = "Bash|Write|Edit|MultiEdit"
691
- _upsert_hook(
692
- settings,
693
- "PreToolUse",
694
- matcher,
695
- _hook_command(project_root, "claude", "pre-tool-use"),
696
- "devcouncil-pre-tool-use",
697
- )
698
- _upsert_hook(
699
- settings,
700
- "PostToolUse",
701
- matcher,
702
- _hook_command(project_root, "claude", "post-tool-use"),
703
- "devcouncil-post-tool-use",
704
- )
705
- _upsert_hook(
706
- settings,
707
- "Stop",
708
- "",
709
- _hook_command(project_root, "claude", "agent-response"),
710
- "devcouncil-agent-response-ready",
711
- )
712
- _save_json(path, settings)
713
- return [path]
714
-
715
-
716
- def _preview_hook_paths(project_root: Path, tool: str) -> list[tuple[str, Path]]:
717
- paths = {
718
- "codex": [project_root / ".codex" / "hooks.json", project_root / ".codex" / "config.toml"],
719
- "gemini": [project_root / ".gemini" / "settings.json"],
720
- "claude": [project_root / ".claude" / "settings.local.json"],
721
- "cursor": [project_root / ".cursor" / "hooks.json"],
722
- "opencode": [_opencode_plugin_path(project_root), _opencode_config_path(project_root)],
723
- }
724
- selected: tuple[str, ...]
725
- if tool == "all":
726
- selected = (*SUPPORTED_HOOK_TOOLS, "opencode")
727
- elif tool == "opencode":
728
- selected = ("opencode",)
729
- else:
730
- selected = (tool,)
731
- return [(client, path) for client in selected for path in paths.get(client, [])]
732
-
733
-
734
- def _configure_native_hooks(project_root: Path, tool: str = "all", apply: bool = False) -> None:
735
- allowed = {"all", *SUPPORTED_HOOK_TOOLS, "opencode"}
736
- if tool not in allowed:
737
- console.print("[red]--tool must be one of: all, codex, gemini, claude, cursor, opencode.[/red]")
738
- raise typer.Exit(code=2)
739
-
740
- if not apply:
741
- console.print("[bold]Native hook config preview[/bold]")
742
- for client, path in _preview_hook_paths(project_root, tool):
743
- console.print(f"{client}: {path}", soft_wrap=True)
744
- console.print("[yellow]Preview only. Rerun with --apply to write hook config files.[/yellow]")
745
- return
746
-
747
- selected: tuple[str, ...]
748
- if tool == "all":
749
- selected = (*SUPPORTED_HOOK_TOOLS, "opencode")
750
- elif tool == "opencode":
751
- selected = ("opencode",)
752
- else:
753
- selected = (tool,)
754
- installers = {
755
- "codex": _install_codex_hooks,
756
- "gemini": _install_gemini_hooks,
757
- "claude": _install_claude_hooks,
758
- "cursor": _install_cursor_hooks,
759
- "opencode": _install_opencode_hooks,
760
- }
761
- for client in selected:
762
- try:
763
- written = installers[client](project_root)
764
- except (ValueError, FileNotFoundError) as exc:
765
- console.print(f"[red]{client} hook setup failed: {exc}[/red]")
766
- raise typer.Exit(code=1) from exc
767
- console.print(f"[green]{client} native hooks configured:[/green] {', '.join(str(path) for path in written)}")
768
-
769
-
770
- def _print_command(tool: str, command: list[str], apply: bool):
771
- if apply:
772
- console.print(f"[cyan]Configuring {tool} MCP integration...[/cyan]")
773
- else:
774
- console.print(f"[bold]{tool}[/bold]")
775
- console.print(_format_command(command), soft_wrap=True)
776
-
777
-
778
- def _configure(tool: str, command: list[str], apply: bool) -> bool:
779
- executable = command[0]
780
- if not shutil.which(executable):
781
- console.print(f"[yellow]{tool} CLI not found on PATH. Install it first, then rerun this command.[/yellow]")
782
- console.print(_format_command(command), soft_wrap=True)
783
- return False
784
-
785
- _print_command(tool, command, apply)
786
- if not apply:
787
- return True
788
-
789
- code = _run(command)
790
- if code == 0:
791
- console.print(f"[green]{tool} integration configured.[/green]")
792
- return True
793
-
794
- console.print(f"[red]{tool} integration command failed with exit code {code}.[/red]")
795
- console.print("You can rerun it manually:")
796
- console.print(_format_command(command), soft_wrap=True)
797
- return False
798
-
799
-
800
- @app.callback(invoke_without_command=True)
801
- def overview(ctx: typer.Context):
802
- """
803
- Show integration options for supported coding CLIs.
804
- """
805
- if ctx.invoked_subcommand is not None:
806
- return
807
-
808
- table = Table(title="DevCouncil Coding CLI Integrations")
809
- table.add_column("Tool", style="cyan")
810
- table.add_column("Setup command", style="green")
811
- table.add_column("Notes")
812
- table.add_row("Codex CLI", f"{PREFERRED_COMMAND} codex --apply", "Adds DevCouncil as a stdio MCP server.")
813
- table.add_row("Gemini CLI", f"{PREFERRED_COMMAND} gemini --apply", "Adds DevCouncil as a project-scoped stdio MCP server.")
814
- table.add_row("Claude Code", f"{PREFERRED_COMMAND} claude --apply", "Adds DevCouncil as a Claude Code MCP server.")
815
- table.add_row("Cursor", f"{PREFERRED_COMMAND} cursor --apply", "Writes project .cursor/mcp.json for Cursor editor and cursor-agent.")
816
- table.add_row("OpenCode", f"{PREFERRED_COMMAND} opencode --apply", "Adds DevCouncil as a project-scoped OpenCode MCP server and executor.")
817
- table.add_row("Google Antigravity CLI", f"{PREFERRED_COMMAND} antigravity --apply", "Writes project .agents/mcp_config.json and enables the agy executor.")
818
- table.add_row("Warp / Oz", f"{PREFERRED_COMMAND} warp --apply", "Writes a Warp-compatible MCP JSON file for local agents and Oz CLI.")
819
- table.add_row("Aider", f"{PREFERRED_COMMAND} aider --apply", "Enables the built-in Aider headless executor (no MCP).")
820
- table.add_row("Bring your own CLI", f"{PREFERRED_COMMAND} cli-agent NAME --command TOOL --apply", "Registers any prompt-taking CLI as a DevCouncil executor.")
821
- table.add_row("All", f"{PREFERRED_COMMAND} all --apply", "Runs MCP setup and installs native hooks.")
822
- table.add_row("Native hooks", f"{PREFERRED_COMMAND} hooks --apply", "Installs Codex, Gemini, Claude, Cursor, and OpenCode hook files.")
823
- table.add_row("Recommend", f"{PREFERRED_COMMAND} recommend", "Show the best executor for this machine and project.")
824
- table.add_row("Status", f"{PREFERRED_COMMAND} status", "Compact PATH + config summary (no MCP probe).")
825
- table.add_row("Matrix", f"{PREFERRED_COMMAND} matrix", "Print built-in coding CLI integration tiers.")
826
- table.add_row("Check", f"{PREFERRED_COMMAND} check", "Verify MCP, hooks, and optional CLIs (--strict, --json for CI).")
827
- console.print(table)
828
- console.print(f"\nIf your install exposes only the setup flow, use: {LEGACY_COMMAND} --apply")
829
- console.print("\nRun without [bold]--apply[/bold] to preview the exact commands first.")
830
-
831
-
832
- @app.command("doctor")
833
- def integrations_doctor(
834
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
835
- ):
836
- """Check optional integration tools and local client wiring prerequisites."""
837
- root = _project_root(project_root)
838
- table = Table(title="DevCouncil Integration Doctor")
839
- table.add_column("Integration", style="cyan", no_wrap=True)
840
- table.add_column("Status")
841
- table.add_column("Notes", overflow="fold")
842
-
843
- checks = [
844
- ("Agent Flow", "agent-flow-app", "Optional live/replay visualizer for trace JSONL."),
845
- ("code-review-graph", "code-review-graph", "Optional structural graph context adapter."),
846
- ("Claude Code", "claude", "Optional MCP client and native hook runtime for pre-tool-use enforcement."),
847
- ("Codex CLI", "codex", "Optional MCP client, headless executor companion, and native hook runtime."),
848
- ("Gemini CLI", "gemini", "Optional MCP client companion and native hook runtime."),
849
- ("Cursor", "cursor-agent", "Optional MCP client, cursor-agent executor, and native hooks."),
850
- ("OpenCode", "opencode", "Optional MCP client and headless coding-agent executor."),
851
- ("Google Antigravity CLI", "agy", "Optional Antigravity CLI companion and headless coding-agent executor."),
852
- ("Warp / Oz", "oz", "Optional Warp/Oz CLI companion and agent executor."),
853
- ("Aider", "aider", "Optional headless executor via `dev run --executor aider` (no MCP)."),
854
- ]
855
- for label, executable, notes in checks:
856
- found = shutil.which(executable)
857
- table.add_row(label, "[green]OK[/green]" if found else "[yellow]Missing[/yellow]", found or notes)
858
-
859
- profiles = load_agent_profiles(root)
860
- for name, spec in load_cli_agent_specs(root).items():
861
- if spec.built_in:
862
- continue
863
- found = shutil.which(spec.executable)
864
- mode_ok = spec.input_mode in VALID_INPUT_MODES
865
- profile_ok = spec.default_profile in profiles
866
- status = "[green]OK[/green]" if found and mode_ok and profile_ok else "[red]Invalid[/red]"
867
- if not found:
868
- status = "[yellow]Missing[/yellow]"
869
- details = found or f"{spec.executable} not found on PATH"
870
- if not mode_ok:
871
- details = f"invalid input_mode={spec.input_mode}"
872
- if not profile_ok:
873
- details = f"{details}; missing profile={spec.default_profile}"
874
- table.add_row(f"CLI agent: {name}", status, details)
875
-
876
- config = _config_path(root)
877
- table.add_row(
878
- "DevCouncil config",
879
- "[green]OK[/green]" if config.exists() else "[red]Missing[/red]",
880
- str(config) if config.exists() else "Run dev init first.",
881
- )
882
- console.print(table)
883
-
884
-
885
- @app.command("codex")
886
- def codex(
887
- apply: bool = typer.Option(False, "--apply", help="Run the setup command instead of printing it."),
888
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
889
- ):
890
- """
891
- Set up DevCouncil MCP tools for Codex CLI.
892
- """
893
- root = _project_root(project_root)
894
- command = _codex_command(root)
895
- ok = _configure("Codex CLI", command, apply)
896
- if not ok and apply:
897
- raise typer.Exit(code=1)
898
-
899
-
900
- @app.command("gemini")
901
- def gemini(
902
- apply: bool = typer.Option(False, "--apply", help="Run the setup command instead of printing it."),
903
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
904
- scope: str = typer.Option("project", "--scope", help="Gemini MCP config scope: project or user."),
905
- ):
906
- """
907
- Set up DevCouncil MCP tools for Gemini CLI.
908
- """
909
- if scope not in {"project", "user"}:
910
- console.print("[red]--scope must be 'project' or 'user'.[/red]")
911
- raise typer.Exit(code=2)
912
-
913
- root = _project_root(project_root)
914
- command = _gemini_command(root, scope)
915
- ok = _configure("Gemini CLI", command, apply)
916
- if not ok and apply:
917
- raise typer.Exit(code=1)
918
-
919
-
920
- @app.command("claude")
921
- def claude(
922
- apply: bool = typer.Option(False, "--apply", help="Run the setup command instead of printing it."),
923
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
924
- scope: str = typer.Option("local", "--scope", help="Claude MCP config scope: local, project, or user."),
925
- ):
926
- """
927
- Set up DevCouncil MCP tools for Claude Code.
928
- """
929
- if scope not in {"local", "project", "user"}:
930
- console.print("[red]--scope must be 'local', 'project', or 'user'.[/red]")
931
- raise typer.Exit(code=2)
932
-
933
- root = _project_root(project_root)
934
- command = _claude_command(root, scope)
935
- ok = _configure("Claude Code", command, apply)
936
- if not ok and apply:
937
- raise typer.Exit(code=1)
938
-
939
-
940
- @app.command("cursor")
941
- def cursor(
942
- apply: bool = typer.Option(False, "--apply", help="Write project Cursor MCP config instead of printing it."),
943
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
944
- ):
945
- """
946
- Set up DevCouncil MCP tools for Cursor.
947
- """
948
- root = _project_root(project_root)
949
- if apply:
950
- report = apply_integration_target(root, "cursor")
951
- if not report.ok:
952
- console.print(report.to_json())
953
- raise typer.Exit(code=1)
954
- console.print("[green]Cursor integration configured.[/green]")
955
- return
956
- ok = _configure_cursor(root, apply)
957
- if not ok and apply:
958
- raise typer.Exit(code=1)
959
-
960
-
961
- @app.command("opencode")
962
- def opencode(
963
- apply: bool = typer.Option(False, "--apply", help="Write project OpenCode config instead of printing it."),
964
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
965
- ):
966
- """
967
- Set up DevCouncil MCP tools for OpenCode.
968
- """
969
- root = _project_root(project_root)
970
- if apply:
971
- report = apply_integration_target(root, "opencode")
972
- if not report.ok:
973
- console.print(report.to_json())
974
- raise typer.Exit(code=1)
975
- console.print("[green]OpenCode integration configured.[/green]")
976
- return
977
- ok = _configure_opencode(root, apply)
978
- if not ok and apply:
979
- raise typer.Exit(code=1)
980
-
981
-
982
- @app.command("agy")
983
- @app.command("antigravity")
984
- def antigravity(
985
- apply: bool = typer.Option(False, "--apply", help="Write project Antigravity MCP config instead of printing it."),
986
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
987
- ):
988
- """
989
- Set up DevCouncil MCP tools for Google Antigravity CLI.
990
- """
991
- root = _project_root(project_root)
992
- if apply:
993
- report = apply_integration_target(root, "antigravity")
994
- if not report.ok:
995
- console.print(report.to_json())
996
- raise typer.Exit(code=1)
997
- console.print("[green]Antigravity integration configured.[/green]")
998
- _warn_if_verify_only("antigravity")
999
- return
1000
- ok = _configure_antigravity(root, apply)
1001
- if not ok and apply:
1002
- raise typer.Exit(code=1)
1003
-
1004
-
1005
- @app.command("warp")
1006
- def warp(
1007
- apply: bool = typer.Option(False, "--apply", help="Write Warp MCP config instead of printing it."),
1008
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
1009
- ):
1010
- """
1011
- Set up DevCouncil MCP tools for Warp local agents and the Oz CLI.
1012
- """
1013
- root = _project_root(project_root)
1014
- if apply:
1015
- report = apply_integration_target(root, "warp")
1016
- if not report.ok:
1017
- console.print(report.to_json())
1018
- raise typer.Exit(code=1)
1019
- console.print("[green]Warp integration configured.[/green]")
1020
- _warn_if_verify_only("warp")
1021
- return
1022
- _configure_warp(root, apply)
1023
-
1024
-
1025
- def _record_aider_config(project_root: Path) -> None:
1026
- def mutate(config: dict) -> None:
1027
- config.setdefault("integrations", {}).setdefault("aider", {}).update({"enabled": True})
1028
-
1029
- _mutate_raw_config(project_root, mutate)
1030
-
1031
-
1032
- def _configure_aider(project_root: Path, apply: bool) -> bool:
1033
- command = ["aider", "--yes", "--no-show-model-warnings", "--message", "<task prompt>"]
1034
- if not apply:
1035
- console.print("[bold]Aider[/bold]")
1036
- console.print("Built-in executor: [dim]dev run TASK-001 --executor aider[/dim]")
1037
- console.print("Launch command: [dim]" + _format_command(command) + "[/dim]")
1038
- console.print("Aider does not expose a first-party DevCouncil MCP server.")
1039
- return True
1040
-
1041
- if not shutil.which("aider"):
1042
- console.print("[yellow]Aider CLI not found on PATH. Install it before using `dev run --executor aider`.[/yellow]")
1043
- _record_aider_config(project_root)
1044
- console.print("[green]Aider executor enabled in .devcouncil/config.yaml.[/green]")
1045
- return True
1046
-
1047
-
1048
- @app.command("aider")
1049
- def aider(
1050
- apply: bool = typer.Option(False, "--apply", help="Record the built-in Aider executor in DevCouncil config."),
1051
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
1052
- ):
1053
- """
1054
- Enable the built-in Aider headless executor (no MCP integration).
1055
- """
1056
- root = _project_root(project_root)
1057
- if apply:
1058
- report = apply_integration_target(root, "aider")
1059
- if not report.ok:
1060
- console.print(report.to_json())
1061
- raise typer.Exit(code=1)
1062
- console.print("[green]Aider integration configured.[/green]")
1063
- _warn_if_verify_only("aider")
1064
- return
1065
- ok = _configure_aider(root, apply)
1066
- if not ok and apply:
1067
- raise typer.Exit(code=1)
1068
-
1069
-
1070
- @app.command("cli-agent")
1071
- def cli_agent(
1072
- name: str = typer.Argument(..., help="Executor name to register, for example opencode or aider."),
1073
- command: str = typer.Option(..., "--command", help="Executable to launch."),
1074
- arg: list[str] | None = typer.Option(None, "--arg", help="Argument to pass to the CLI. Repeat for multiple args."),
1075
- input_mode: str = typer.Option("stdin", "--input-mode", help="Prompt input mode: stdin, argument, or prompt-file."),
1076
- prompt_arg: str | None = typer.Option(None, "--prompt-arg", help="Flag used before the prompt or prompt file, for example --prompt."),
1077
- timeout_seconds: int | None = typer.Option(None, "--timeout-seconds", help="Agent-specific timeout override."),
1078
- display_name: str | None = typer.Option(None, "--display-name", help="Human-readable agent name."),
1079
- kind: str = typer.Option("custom", "--kind", help="Agent kind, for example coding-cli or review-cli."),
1080
- supports_mcp: bool = typer.Option(False, "--supports-mcp", help="Mark this agent as MCP-capable."),
1081
- supports_diff_review: bool = typer.Option(False, "--supports-diff-review", help="Mark this agent as able to review diffs."),
1082
- default_profile: str = typer.Option("default", "--default-profile", help="Default execution profile for this agent."),
1083
- help_arg: list[str] | None = typer.Option(None, "--help-arg", help="Argument for the agent help command. Repeat for multiple args."),
1084
- apply: bool = typer.Option(False, "--apply", help="Write .devcouncil/config.yaml instead of previewing."),
1085
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
1086
- ):
1087
- """
1088
- Register an arbitrary prompt-taking CLI as a DevCouncil executor.
1089
- """
1090
- if input_mode not in VALID_INPUT_MODES:
1091
- console.print("[red]--input-mode must be one of: stdin, argument, prompt-file.[/red]")
1092
- raise typer.Exit(code=2)
1093
- if not name.strip():
1094
- console.print("[red]Agent name cannot be empty.[/red]")
1095
- raise typer.Exit(code=2)
1096
- if not command.strip():
1097
- console.print("[red]--command cannot be empty.[/red]")
1098
- raise typer.Exit(code=2)
1099
-
1100
- root = _project_root(project_root)
1101
- if is_reserved_agent_name(name):
1102
- console.print(f"[red]'{name}' is reserved for a built-in DevCouncil agent.[/red]")
1103
- raise typer.Exit(code=2)
1104
- if default_profile not in load_agent_profiles(root):
1105
- console.print(f"[red]Unknown --default-profile '{default_profile}'.[/red]")
1106
- raise typer.Exit(code=2)
1107
-
1108
- normalized = normalize_agent_name(name)
1109
- entry = agent_config_entry(
1110
- command=command,
1111
- args=arg or [],
1112
- input_mode=input_mode,
1113
- prompt_arg=prompt_arg,
1114
- timeout_seconds=timeout_seconds,
1115
- display_name=display_name,
1116
- kind=kind,
1117
- supports_mcp=supports_mcp,
1118
- supports_diff_review=supports_diff_review,
1119
- default_profile=default_profile,
1120
- help_command=[command, *(help_arg or [])] if help_arg else [],
1121
- )
1122
-
1123
- if not apply:
1124
- console.print("[bold]Bring your own CLI executor preview[/bold]")
1125
- console.print(f"Executor: [cyan]{normalized}[/cyan]")
1126
- console.print(json.dumps(entry, indent=2), soft_wrap=True)
1127
- console.print(f"Run with: [dim]dev run TASK-001 --executor {normalized}[/dim]")
1128
- console.print("[yellow]Preview only. Rerun with --apply to update .devcouncil/config.yaml.[/yellow]")
1129
- return
1130
-
1131
- config = _load_raw_config(root)
1132
- agents = config.setdefault("integrations", {}).setdefault("cli_agents", {}).setdefault("agents", {})
1133
- agents[normalized] = entry
1134
- _save_raw_config(root, config)
1135
- console.print(f"[green]Registered CLI executor '{normalized}' in .devcouncil/config.yaml.[/green]")
1136
-
1137
-
1138
- @app.command("all")
1139
- def all_tools(
1140
- apply: bool = typer.Option(False, "--apply", help="Run setup commands instead of printing them."),
1141
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
1142
- gemini_scope: str = typer.Option("project", "--gemini-scope", help="Gemini MCP config scope: project or user."),
1143
- claude_scope: str = typer.Option("local", "--claude-scope", help="Claude MCP config scope: local, project, or user."),
1144
- hooks: bool = typer.Option(True, "--hooks/--no-hooks", help="Include native Codex, Gemini, and Claude hook setup."),
1145
- strict: bool = typer.Option(
1146
- False,
1147
- "--strict",
1148
- help="After --apply, run dev integrate check --strict and fail on missing optional CLIs.",
1149
- ),
1150
- ):
1151
- """
1152
- Set up DevCouncil MCP tools and native hooks for every supported coding CLI found on PATH.
1153
- """
1154
- if gemini_scope not in {"project", "user"}:
1155
- console.print("[red]--gemini-scope must be 'project' or 'user'.[/red]")
1156
- raise typer.Exit(code=2)
1157
- if claude_scope not in {"local", "project", "user"}:
1158
- console.print("[red]--claude-scope must be 'local', 'project', or 'user'.[/red]")
1159
- raise typer.Exit(code=2)
1160
-
1161
- root = _project_root(project_root)
1162
- if apply:
1163
- report = apply_integration_target(
1164
- root,
1165
- "all",
1166
- include_hooks=hooks,
1167
- strict=strict,
1168
- gemini_scope=gemini_scope,
1169
- claude_scope=claude_scope,
1170
- )
1171
- if not report.ok:
1172
- console.print(report.to_json())
1173
- raise typer.Exit(code=1)
1174
- console.print("[green]Coding CLI integrations configured.[/green]")
1175
- return
1176
-
1177
- commands = [
1178
- ("Codex CLI", _codex_command(root)),
1179
- ("Gemini CLI", _gemini_command(root, gemini_scope)),
1180
- ("Claude Code", _claude_command(root, claude_scope)),
1181
- ]
1182
- for tool, command in commands:
1183
- _configure(tool, command, apply)
1184
- _configure_cursor(root, apply)
1185
- _configure_opencode(root, apply)
1186
- _configure_antigravity(root, apply)
1187
- _configure_warp(root, apply)
1188
- _configure_aider(root, apply)
1189
- if hooks:
1190
- _configure_native_hooks(root, "all", apply)
1191
-
1192
-
1193
- @app.command("recommend")
1194
- def recommend(
1195
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
1196
- ):
1197
- """Recommend a coding CLI executor for this machine and project."""
1198
- root = _project_root(project_root)
1199
- probe_order = resolve_coding_cli_probe_order(root)
1200
- detected = detect_available_coding_cli(root, probe_order=probe_order)
1201
- resolved = resolve_automated_executor(root, None)
1202
-
1203
- table = Table(title="DevCouncil Integration Recommendations")
1204
- table.add_column("Client", style="cyan")
1205
- table.add_column("PATH")
1206
- table.add_column("Tier")
1207
- table.add_column("MCP")
1208
- table.add_column("Hooks")
1209
-
1210
- for client in probe_order:
1211
- info = CODING_CLI_INTEGRATION_INFO.get(client)
1212
- on_path = resolve_coding_cli_executable(root, client)
1213
- table.add_row(
1214
- client,
1215
- "[green]yes[/green]" if on_path else "[dim]no[/dim]",
1216
- integration_tier_label(client),
1217
- "yes" if info and info.mcp else "no",
1218
- "yes" if info and info.hooks else "no",
1219
- )
1220
-
1221
- console.print(table)
1222
- if summary := integration_status_summary(root):
1223
- if summary.get("custom_probe_order"):
1224
- console.print(
1225
- f"\n[dim]Probe order:[/dim] {', '.join(summary['probe_order'])} "
1226
- f"(from execution.coding_cli_probe_order)"
1227
- )
1228
- else:
1229
- console.print(f"\n[dim]Probe order:[/dim] {', '.join(summary['probe_order'])} (default)")
1230
- if detected:
1231
- console.print(f"\n[bold]Recommended executor:[/bold] [cyan]{resolved}[/cyan]")
1232
- console.print(f"Run: [dim]dev run TASK-001 --executor {resolved}[/dim]")
1233
- console.print(f"Or: [dim]dev go \"Your goal\" --executor {resolved}[/dim]")
1234
- console.print(f"Setup: [dim]{PREFERRED_COMMAND} {resolved} --apply[/dim]")
1235
- else:
1236
- console.print("\n[yellow]No built-in coding CLI was found on PATH.[/yellow]")
1237
- console.print("Install Codex, Gemini, Claude Code, Cursor Agent, OpenCode, or register a custom CLI:")
1238
- console.print(f"[dim]{PREFERRED_COMMAND} cli-agent NAME --command TOOL --apply[/dim]")
1239
-
1240
-
1241
- @app.command("status")
1242
- def status(
1243
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
1244
- as_json: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
1245
- ):
1246
- """Show a compact integration summary without running the MCP server probe."""
1247
- root = _project_root(project_root)
1248
- summary = integration_status_summary(root)
1249
- raw_config = _load_raw_config(root) if (root / ".devcouncil").exists() else {}
1250
- integrations = raw_config.get("integrations", {})
1251
-
1252
- if as_json:
1253
- payload = {
1254
- **summary,
1255
- "integrations_enabled": {
1256
- name: bool(integrations.get(name, {}).get("enabled"))
1257
- for name in ("cursor", "opencode", "antigravity", "warp", "aider")
1258
- },
1259
- }
1260
- typer.echo(json.dumps(payload, indent=2))
1261
- return
1262
-
1263
- table = Table(title="DevCouncil Integration Status")
1264
- table.add_column("Setting", style="cyan")
1265
- table.add_column("Value")
1266
-
1267
- table.add_row("Project", "[green]initialized[/green]" if summary["project_initialized"] else "[yellow]not initialized[/yellow]")
1268
- table.add_row("Default executor", summary["default_executor"])
1269
- table.add_row("Resolved executor", summary["resolved_executor"])
1270
- table.add_row("CLIs on PATH", ", ".join(summary["coding_clis_on_path"]) or "[dim]none[/dim]")
1271
- table.add_row("Probe order", ", ".join(summary["probe_order"]))
1272
- table.add_row("Stream CLI output", "yes" if summary["stream_cli_output"] else "no")
1273
- table.add_row("Cursor resume mode", summary["cursor_resume_mode"])
1274
-
1275
- for name in ("cursor", "opencode", "antigravity", "warp", "aider"):
1276
- enabled = bool(integrations.get(name, {}).get("enabled"))
1277
- table.add_row(f"{name} integration", "[green]enabled[/green]" if enabled else "[dim]off[/dim]")
1278
-
1279
- console.print(table)
1280
- if summary["resolved_executor"] not in {"", "manual"}:
1281
- console.print(
1282
- f"\n[dim]Next:[/dim] dev run TASK-001 --executor {summary['resolved_executor']} "
1283
- f"| {PREFERRED_COMMAND} check for full readiness"
1284
- )
1285
- else:
1286
- console.print(f"\n[dim]Next:[/dim] {PREFERRED_COMMAND} recommend | {PREFERRED_COMMAND} check")
1287
-
1288
-
1289
- @app.command("matrix")
1290
- def matrix(
1291
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
1292
- ):
1293
- """Print built-in coding CLI integration tiers and capabilities."""
1294
- root = _project_root(project_root)
1295
- _ = root
1296
- table = Table(title="DevCouncil Coding CLI Integration Matrix")
1297
- table.add_column("Client", style="cyan")
1298
- table.add_column("Tier")
1299
- table.add_column("Headless")
1300
- table.add_column("MCP setup")
1301
- table.add_column("Native hooks")
1302
- table.add_column("Enforcement")
1303
- table.add_column("Notes")
1304
-
1305
- for client in sorted(BUILTIN_CODING_EXECUTOR_NAMES):
1306
- info = CODING_CLI_INTEGRATION_INFO.get(client)
1307
- posture = info.enforcement if info else "verify-only"
1308
- posture_render = "[green]pre-action[/green]" if posture == "pre-action" else "[yellow]verify-only[/yellow]"
1309
- table.add_row(
1310
- client,
1311
- integration_tier_label(client),
1312
- "yes" if info and info.tier == 1 else "no",
1313
- "yes" if info and info.mcp else "no",
1314
- "yes" if info and info.hooks else "verify only",
1315
- posture_render,
1316
- info.notes if info else "",
1317
- )
1318
- console.print(table)
1319
- console.print(
1320
- "\n[dim]Enforcement:[/dim] [green]pre-action[/green] blocks forbidden writes/commands "
1321
- "before they happen; [yellow]verify-only[/yellow] catches them only at verify time."
1322
- )
1323
- console.print("\nSee [dim]docs/integration-tiers.md[/dim] for workflow guidance.")
1324
-
1325
-
1326
- @app.command("hooks")
1327
- def hooks(
1328
- apply: bool = typer.Option(False, "--apply", help="Write native hook config files instead of previewing paths."),
1329
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
1330
- tool: str = typer.Option("all", "--tool", help="Hook target: all, codex, gemini, claude, cursor, or opencode."),
1331
- ):
1332
- """
1333
- Install DevCouncil hook configuration for Codex, Gemini, Claude, Cursor, and OpenCode.
1334
- """
1335
- root = _project_root(project_root)
1336
- if apply and tool == "all":
1337
- report = apply_integration_target(root, "hooks")
1338
- if not report.ok:
1339
- console.print(report.to_json())
1340
- raise typer.Exit(code=1)
1341
- console.print("[green]Native hooks configured.[/green]")
1342
- return
1343
- _configure_native_hooks(root, tool, apply)
1344
-
1345
-
1346
- @app.command("check")
1347
- def check(
1348
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
1349
- strict: bool = typer.Option(
1350
- False,
1351
- "--strict",
1352
- help="Treat missing optional coding CLIs as failures instead of warnings.",
1353
- ),
1354
- as_json: bool = typer.Option(False, "--json", help="Emit machine-readable JSON for CI."),
1355
- report_file: Path | None = typer.Option(
1356
- None,
1357
- "--report-file",
1358
- "--output",
1359
- "-o",
1360
- help="Write the JSON integration report to this file (implies structured output).",
1361
- ),
1362
- ):
1363
- """
1364
- Check whether DevCouncil is ready to integrate with coding CLIs.
1365
- """
1366
- root = _project_root(project_root)
1367
- report = build_integration_check_report(root, strict=strict)
1368
- table = Table(title="DevCouncil Integration Check")
1369
- table.add_column("Check", style="cyan")
1370
- table.add_column("Status", style="magenta")
1371
- table.add_column("Details")
1372
-
1373
- for row in report.checks:
1374
- if row.status == "ok":
1375
- rendered = "[green]OK[/green]"
1376
- elif row.status == "skip":
1377
- rendered = "[dim]SKIP[/dim]"
1378
- elif row.status == "missing":
1379
- rendered = "[yellow]Missing[/yellow]"
1380
- else:
1381
- rendered = "[red]FAIL[/red]"
1382
- table.add_row(row.name, rendered, row.details)
1383
-
1384
- write_json = as_json or report_file is not None
1385
- if write_json:
1386
- json_text = report.to_json()
1387
- if report_file is not None:
1388
- report_path = Path(report_file).expanduser().resolve()
1389
- report_path.parent.mkdir(parents=True, exist_ok=True)
1390
- report_path.write_text(json_text + "\n", encoding="utf-8")
1391
- if not as_json:
1392
- console.print(f"[dim]Wrote integration report to[/dim] {report_path}")
1393
- if as_json:
1394
- typer.echo(json_text)
1395
- if not write_json or not as_json:
1396
- console.print(table)
1397
-
1398
- if report.failures:
1399
- if not as_json:
1400
- console.print(
1401
- f"\n[yellow]Fix failed checks, then run:[/yellow] {PREFERRED_COMMAND} all --apply "
1402
- f"(or {LEGACY_COMMAND} --apply)."
1403
- )
1404
- raise typer.Exit(code=1)
1405
-
1406
- if not as_json:
1407
- console.print(f"\n[green]Ready.[/green] Run: {PREFERRED_COMMAND} all --apply (or {LEGACY_COMMAND} --apply).")
1408
-
1409
-
1410
- @setup_app.command("agent-flow")
1411
- def setup_agent_flow(
1412
- apply: bool = typer.Option(False, "--apply", help="Write DevCouncil config instead of previewing."),
1413
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
1414
- ):
1415
- """Configure DevCouncil trace output for Agent Flow-style JSONL replay."""
1416
- root = _project_root(project_root)
1417
- trace_path = root / ".devcouncil" / "logs" / "traces.jsonl"
1418
- console.print("[bold]Agent Flow setup[/bold]")
1419
- console.print(f"Trace JSONL: {trace_path}")
1420
- console.print("Replay/tail locally with: dev trace tail --follow")
1421
- console.print("External visualizers can watch the trace JSONL path above.")
1422
-
1423
- if not apply:
1424
- console.print("[yellow]Preview only. Rerun with --apply to record this integration in config.[/yellow]")
1425
- return
1426
-
1427
- config = _load_raw_config(root)
1428
- integrations = config.setdefault("integrations", {})
1429
- integrations["agent_flow"] = {
1430
- "enabled": True,
1431
- "trace_path": str(trace_path),
1432
- "mode": "jsonl",
1433
- }
1434
- _save_raw_config(root, config)
1435
- docs_dir = root / ".devcouncil" / "integrations"
1436
- docs_dir.mkdir(parents=True, exist_ok=True)
1437
- (docs_dir / "agent-flow.md").write_text(
1438
- "\n".join([
1439
- "# Agent Flow",
1440
- "",
1441
- f"DevCouncil writes trace events to `{trace_path}`.",
1442
- "",
1443
- "Local replay:",
1444
- "",
1445
- "```bash",
1446
- "dev trace tail --follow",
1447
- "```",
1448
- "",
1449
- "External visualizers can watch the JSONL file directly. DevCouncil does not modify global editor or Claude Code settings from this setup command.",
1450
- "",
1451
- ]),
1452
- encoding="utf-8",
1453
- )
1454
- console.print("[green]Agent Flow trace integration recorded in .devcouncil/config.yaml.[/green]")
1455
-
1456
-
1457
- @setup_app.command("code-review-graph")
1458
- def setup_code_review_graph(
1459
- apply: bool = typer.Option(False, "--apply", help="Write DevCouncil config and ignore file."),
1460
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
1461
- ):
1462
- """Configure optional code-review-graph context enrichment."""
1463
- root = _project_root(project_root)
1464
- executable = shutil.which("code-review-graph")
1465
- ignore_path = root / ".code-review-graphignore"
1466
- console.print("[bold]code-review-graph setup[/bold]")
1467
- console.print(f"Binary: {executable or 'not found on PATH'}")
1468
- console.print("Install separately with: pipx install code-review-graph")
1469
- console.print("Build graph separately with: code-review-graph build")
1470
-
1471
- if not apply:
1472
- console.print("[yellow]Preview only. Rerun with --apply to record this integration.[/yellow]")
1473
- return
1474
-
1475
- if not ignore_path.exists():
1476
- ignore_path.write_text(
1477
- "\n".join([
1478
- ".devcouncil/**",
1479
- ".git/**",
1480
- ".venv/**",
1481
- "dist/**",
1482
- "node_modules/**",
1483
- "",
1484
- ]),
1485
- encoding="utf-8",
1486
- )
1487
- console.print(f"[green]Created {ignore_path}.[/green]")
1488
-
1489
- config = _load_raw_config(root)
1490
- integrations = config.setdefault("integrations", {})
1491
- integrations["code_review_graph"] = {
1492
- "enabled": True,
1493
- "command": "code-review-graph",
1494
- "optional": True,
1495
- }
1496
- _save_raw_config(root, config)
1497
- docs_dir = root / ".devcouncil" / "integrations"
1498
- docs_dir.mkdir(parents=True, exist_ok=True)
1499
- (docs_dir / "code-review-graph.md").write_text(
1500
- "\n".join([
1501
- "# code-review-graph",
1502
- "",
1503
- "Install and build the graph outside DevCouncil:",
1504
- "",
1505
- "```bash",
1506
- "pipx install code-review-graph",
1507
- "code-review-graph build",
1508
- "```",
1509
- "",
1510
- "DevCouncil uses this as an optional context adapter for mapping, prompts, verification traces, and MCP graph context.",
1511
- "",
1512
- ]),
1513
- encoding="utf-8",
1514
- )
1515
- console.print("[green]code-review-graph adapter recorded in .devcouncil/config.yaml.[/green]")