devcouncil 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -6
- package/package.json +9 -2
- package/pyproject.toml +34 -2
- package/src/devcouncil/app/config.py +167 -5
- package/src/devcouncil/artifacts/graph.py +23 -3
- package/src/devcouncil/assets/__init__.py +1 -0
- package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/commands/agents.py +292 -0
- package/src/devcouncil/cli/commands/artifacts.py +6 -3
- package/src/devcouncil/cli/commands/check.py +209 -0
- package/src/devcouncil/cli/commands/config.py +43 -4
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +6 -1
- package/src/devcouncil/cli/commands/doctor.py +221 -21
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +452 -33
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +124 -15
- package/src/devcouncil/cli/commands/init.py +154 -18
- package/src/devcouncil/cli/commands/integrate.py +894 -105
- package/src/devcouncil/cli/commands/map.py +80 -10
- package/src/devcouncil/cli/commands/plan.py +212 -51
- package/src/devcouncil/cli/commands/prompt.py +18 -7
- package/src/devcouncil/cli/commands/repair.py +40 -23
- package/src/devcouncil/cli/commands/report.py +8 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
- package/src/devcouncil/cli/commands/rollback.py +27 -28
- package/src/devcouncil/cli/commands/run.py +69 -49
- package/src/devcouncil/cli/commands/runs.py +223 -0
- package/src/devcouncil/cli/commands/scaffold.py +32 -0
- package/src/devcouncil/cli/commands/semantic.py +47 -0
- package/src/devcouncil/cli/commands/setup.py +145 -6
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/skills.py +88 -0
- package/src/devcouncil/cli/commands/status.py +25 -1
- package/src/devcouncil/cli/commands/trace.py +47 -3
- package/src/devcouncil/cli/commands/verify.py +138 -3
- package/src/devcouncil/cli/commands/watch.py +9 -9
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +56 -7
- package/src/devcouncil/domain/evidence.py +22 -2
- package/src/devcouncil/domain/gap.py +27 -1
- package/src/devcouncil/domain/task.py +31 -2
- package/src/devcouncil/execution/checkpoints.py +246 -0
- package/src/devcouncil/execution/context_builder.py +1 -1
- package/src/devcouncil/execution/fs_watcher.py +180 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +162 -74
- package/src/devcouncil/execution/patch.py +59 -10
- package/src/devcouncil/execution/permissions.py +17 -24
- package/src/devcouncil/execution/policy_engine.py +343 -0
- package/src/devcouncil/execution/prompt_builder.py +633 -21
- package/src/devcouncil/execution/shell_session.py +225 -0
- package/src/devcouncil/execution/task_runner.py +6 -2
- package/src/devcouncil/executors/agent_registry.py +575 -0
- package/src/devcouncil/executors/coding_cli.py +663 -39
- package/src/devcouncil/executors/native/agent.py +121 -20
- package/src/devcouncil/gating/checks/clean_git.py +3 -1
- package/src/devcouncil/gating/checks/secret_scan_check.py +40 -21
- package/src/devcouncil/gating/policy.py +158 -10
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/ast_matcher.py +1 -1
- package/src/devcouncil/indexing/lsp.py +45 -4
- package/src/devcouncil/indexing/repo_mapper.py +1256 -9
- package/src/devcouncil/indexing/semantic_index.py +205 -0
- package/src/devcouncil/integrations/actions.py +146 -0
- package/src/devcouncil/integrations/check.py +423 -0
- package/src/devcouncil/integrations/github_intent.py +142 -0
- package/src/devcouncil/integrations/gitnexus.py +35 -0
- package/src/devcouncil/integrations/mcp/server.py +1552 -29
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/live/cards.py +161 -19
- package/src/devcouncil/live/signals.py +2 -2
- package/src/devcouncil/live/transcripts.py +9 -6
- package/src/devcouncil/llm/cache.py +10 -6
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +515 -34
- package/src/devcouncil/llm/router.py +231 -46
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/planning/correction_manifest.py +303 -0
- package/src/devcouncil/planning/critique_service.py +7 -2
- package/src/devcouncil/planning/plan_service.py +17 -3
- package/src/devcouncil/planning/prompt_enhancer_service.py +82 -1
- package/src/devcouncil/planning/spec_service.py +27 -1
- package/src/devcouncil/repo/ci_scaffold.py +157 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +374 -0
- package/src/devcouncil/reporting/json_report.py +11 -1
- package/src/devcouncil/reporting/markdown_report.py +15 -0
- package/src/devcouncil/skills/__init__.py +19 -0
- package/src/devcouncil/skills/library/README.md +46 -0
- package/src/devcouncil/skills/library/ai-training.md +50 -0
- package/src/devcouncil/skills/library/android.md +50 -0
- package/src/devcouncil/skills/library/backend.md +52 -0
- package/src/devcouncil/skills/library/core-engineering.md +95 -0
- package/src/devcouncil/skills/library/data-engineering.md +47 -0
- package/src/devcouncil/skills/library/desktop.md +46 -0
- package/src/devcouncil/skills/library/devops.md +48 -0
- package/src/devcouncil/skills/library/game-dev.md +46 -0
- package/src/devcouncil/skills/library/ios.md +48 -0
- package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
- package/src/devcouncil/skills/library/security.md +48 -0
- package/src/devcouncil/skills/library/systems.md +48 -0
- package/src/devcouncil/skills/library/web.md +47 -0
- package/src/devcouncil/skills/library/windows.md +47 -0
- package/src/devcouncil/skills/registry.py +330 -0
- package/src/devcouncil/storage/db.py +83 -2
- package/src/devcouncil/storage/models.py +121 -0
- package/src/devcouncil/storage/native.py +557 -0
- package/src/devcouncil/storage/repositories.py +137 -75
- package/src/devcouncil/telemetry/cost.py +123 -17
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +12 -9
- package/src/devcouncil/ui/dashboard.py +324 -23
- package/src/devcouncil/utils/redaction.py +9 -3
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/acceptance_compiler.py +125 -0
- package/src/devcouncil/verification/ad_hoc_check.py +129 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +178 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1065 -47
- package/uv.lock +205 -64
- package/src/devcouncil/indexing/symbol_index.py +0 -0
|
@@ -3,26 +3,64 @@ import shlex
|
|
|
3
3
|
import shutil
|
|
4
4
|
import subprocess
|
|
5
5
|
import sys
|
|
6
|
+
from contextlib import contextmanager
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
import typer
|
|
9
|
-
import yaml
|
|
10
|
+
import yaml # type: ignore[import-untyped]
|
|
10
11
|
from rich.console import Console
|
|
11
12
|
from rich.table import Table
|
|
12
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
|
+
|
|
13
36
|
app = typer.Typer(help="Set up DevCouncil integrations with coding CLIs.")
|
|
14
37
|
setup_app = typer.Typer(help="Set up optional external companion integrations.")
|
|
15
38
|
app.add_typer(setup_app, name="setup")
|
|
16
39
|
console = Console()
|
|
17
40
|
|
|
18
|
-
SUPPORTED_TOOLS = ("codex", "gemini", "claude", "cursor")
|
|
19
|
-
SUPPORTED_HOOK_TOOLS = ("codex", "gemini", "claude")
|
|
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"
|
|
20
44
|
PREFERRED_COMMAND = "dev integrate"
|
|
21
45
|
LEGACY_COMMAND = "dev setup --integrate"
|
|
22
46
|
|
|
23
47
|
|
|
24
|
-
def _project_root(path: Path | None) -> Path:
|
|
25
|
-
return (path or
|
|
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
|
+
)
|
|
26
64
|
|
|
27
65
|
|
|
28
66
|
def _server_args(project_root: Path) -> list[str]:
|
|
@@ -57,28 +95,294 @@ def _gemini_command(project_root: Path, scope: str) -> list[str]:
|
|
|
57
95
|
|
|
58
96
|
|
|
59
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.
|
|
60
102
|
return [
|
|
61
103
|
"claude",
|
|
62
104
|
"mcp",
|
|
63
105
|
"add",
|
|
64
106
|
"--scope",
|
|
65
107
|
scope,
|
|
108
|
+
"devcouncil",
|
|
66
109
|
"--env",
|
|
67
110
|
f"DEVCOUNCIL_PROJECT_ROOT={project_root}",
|
|
68
|
-
"devcouncil",
|
|
69
111
|
"--",
|
|
70
112
|
*_server_args(project_root),
|
|
71
113
|
]
|
|
72
114
|
|
|
73
115
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
140
|
}
|
|
81
|
-
|
|
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
|
|
82
386
|
|
|
83
387
|
|
|
84
388
|
def _format_command(command: list[str]) -> str:
|
|
@@ -96,6 +400,14 @@ def _quote_powershell_arg(arg: str) -> str:
|
|
|
96
400
|
return "'" + arg.replace("'", "''") + "'"
|
|
97
401
|
|
|
98
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
|
+
|
|
99
411
|
def _hook_command(project_root: Path, client: str, event: str) -> str:
|
|
100
412
|
return _format_command([
|
|
101
413
|
"devcouncil",
|
|
@@ -108,6 +420,35 @@ def _hook_command(project_root: Path, client: str, event: str) -> str:
|
|
|
108
420
|
])
|
|
109
421
|
|
|
110
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
|
+
|
|
111
452
|
def _run(command: list[str]) -> int:
|
|
112
453
|
executable = shutil.which(command[0])
|
|
113
454
|
if not executable:
|
|
@@ -115,7 +456,10 @@ def _run(command: list[str]) -> int:
|
|
|
115
456
|
resolved = [executable, *command[1:]]
|
|
116
457
|
use_shell = sys.platform == "win32" and Path(executable).suffix.lower() in {".bat", ".cmd", ".ps1"}
|
|
117
458
|
invocation = subprocess.list2cmdline(resolved) if use_shell else resolved
|
|
118
|
-
|
|
459
|
+
try:
|
|
460
|
+
result = subprocess.run(invocation, text=True, shell=use_shell)
|
|
461
|
+
except (FileNotFoundError, OSError):
|
|
462
|
+
return 127
|
|
119
463
|
return result.returncode
|
|
120
464
|
|
|
121
465
|
|
|
@@ -136,9 +480,12 @@ def _run_capture(command: list[str], timeout: int = 10) -> tuple[int, str]:
|
|
|
136
480
|
errors="replace",
|
|
137
481
|
shell=use_shell,
|
|
138
482
|
timeout=timeout,
|
|
483
|
+
env=clean_subprocess_env(),
|
|
139
484
|
)
|
|
140
485
|
except subprocess.TimeoutExpired:
|
|
141
486
|
return 124, "timed out"
|
|
487
|
+
except (FileNotFoundError, OSError) as exc:
|
|
488
|
+
return 127, f"{command[0]} could not be executed: {exc}"
|
|
142
489
|
return result.returncode, (result.stdout + result.stderr).strip()
|
|
143
490
|
|
|
144
491
|
|
|
@@ -272,6 +619,71 @@ def _install_gemini_hooks(project_root: Path) -> list[Path]:
|
|
|
272
619
|
return [path]
|
|
273
620
|
|
|
274
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
|
+
|
|
275
687
|
def _install_claude_hooks(project_root: Path) -> list[Path]:
|
|
276
688
|
path = project_root / ".claude" / "settings.local.json"
|
|
277
689
|
settings = _load_json(path)
|
|
@@ -306,31 +718,52 @@ def _preview_hook_paths(project_root: Path, tool: str) -> list[tuple[str, Path]]
|
|
|
306
718
|
"codex": [project_root / ".codex" / "hooks.json", project_root / ".codex" / "config.toml"],
|
|
307
719
|
"gemini": [project_root / ".gemini" / "settings.json"],
|
|
308
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)],
|
|
309
723
|
}
|
|
310
|
-
selected
|
|
311
|
-
|
|
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, [])]
|
|
312
732
|
|
|
313
733
|
|
|
314
734
|
def _configure_native_hooks(project_root: Path, tool: str = "all", apply: bool = False) -> None:
|
|
315
|
-
|
|
316
|
-
|
|
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]")
|
|
317
738
|
raise typer.Exit(code=2)
|
|
318
739
|
|
|
319
740
|
if not apply:
|
|
320
741
|
console.print("[bold]Native hook config preview[/bold]")
|
|
321
742
|
for client, path in _preview_hook_paths(project_root, tool):
|
|
322
|
-
console.print(f"{client}: {path}")
|
|
743
|
+
console.print(f"{client}: {path}", soft_wrap=True)
|
|
323
744
|
console.print("[yellow]Preview only. Rerun with --apply to write hook config files.[/yellow]")
|
|
324
745
|
return
|
|
325
746
|
|
|
326
|
-
selected
|
|
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,)
|
|
327
754
|
installers = {
|
|
328
755
|
"codex": _install_codex_hooks,
|
|
329
756
|
"gemini": _install_gemini_hooks,
|
|
330
757
|
"claude": _install_claude_hooks,
|
|
758
|
+
"cursor": _install_cursor_hooks,
|
|
759
|
+
"opencode": _install_opencode_hooks,
|
|
331
760
|
}
|
|
332
761
|
for client in selected:
|
|
333
|
-
|
|
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
|
|
334
767
|
console.print(f"[green]{client} native hooks configured:[/green] {', '.join(str(path) for path in written)}")
|
|
335
768
|
|
|
336
769
|
|
|
@@ -379,21 +812,33 @@ def overview(ctx: typer.Context):
|
|
|
379
812
|
table.add_row("Codex CLI", f"{PREFERRED_COMMAND} codex --apply", "Adds DevCouncil as a stdio MCP server.")
|
|
380
813
|
table.add_row("Gemini CLI", f"{PREFERRED_COMMAND} gemini --apply", "Adds DevCouncil as a project-scoped stdio MCP server.")
|
|
381
814
|
table.add_row("Claude Code", f"{PREFERRED_COMMAND} claude --apply", "Adds DevCouncil as a Claude Code MCP server.")
|
|
382
|
-
table.add_row("Cursor", f"{PREFERRED_COMMAND} cursor --apply", "
|
|
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.")
|
|
383
821
|
table.add_row("All", f"{PREFERRED_COMMAND} all --apply", "Runs MCP setup and installs native hooks.")
|
|
384
|
-
table.add_row("Native hooks", f"{PREFERRED_COMMAND} hooks --apply", "Installs Codex, Gemini, and
|
|
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).")
|
|
385
827
|
console.print(table)
|
|
386
828
|
console.print(f"\nIf your install exposes only the setup flow, use: {LEGACY_COMMAND} --apply")
|
|
387
829
|
console.print("\nRun without [bold]--apply[/bold] to preview the exact commands first.")
|
|
388
830
|
|
|
389
831
|
|
|
390
832
|
@app.command("doctor")
|
|
391
|
-
def integrations_doctor(
|
|
833
|
+
def integrations_doctor(
|
|
834
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
835
|
+
):
|
|
392
836
|
"""Check optional integration tools and local client wiring prerequisites."""
|
|
837
|
+
root = _project_root(project_root)
|
|
393
838
|
table = Table(title="DevCouncil Integration Doctor")
|
|
394
|
-
table.add_column("Integration", style="cyan")
|
|
839
|
+
table.add_column("Integration", style="cyan", no_wrap=True)
|
|
395
840
|
table.add_column("Status")
|
|
396
|
-
table.add_column("Notes")
|
|
841
|
+
table.add_column("Notes", overflow="fold")
|
|
397
842
|
|
|
398
843
|
checks = [
|
|
399
844
|
("Agent Flow", "agent-flow-app", "Optional live/replay visualizer for trace JSONL."),
|
|
@@ -401,14 +846,34 @@ def integrations_doctor():
|
|
|
401
846
|
("Claude Code", "claude", "Optional MCP client and native hook runtime for pre-tool-use enforcement."),
|
|
402
847
|
("Codex CLI", "codex", "Optional MCP client, headless executor companion, and native hook runtime."),
|
|
403
848
|
("Gemini CLI", "gemini", "Optional MCP client companion and native hook runtime."),
|
|
404
|
-
("Cursor", "cursor", "Optional MCP client and
|
|
405
|
-
("
|
|
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)."),
|
|
406
854
|
]
|
|
407
855
|
for label, executable, notes in checks:
|
|
408
856
|
found = shutil.which(executable)
|
|
409
857
|
table.add_row(label, "[green]OK[/green]" if found else "[yellow]Missing[/yellow]", found or notes)
|
|
410
858
|
|
|
411
|
-
|
|
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)
|
|
412
877
|
table.add_row(
|
|
413
878
|
"DevCouncil config",
|
|
414
879
|
"[green]OK[/green]" if config.exists() else "[red]Missing[/red]",
|
|
@@ -474,19 +939,202 @@ def claude(
|
|
|
474
939
|
|
|
475
940
|
@app.command("cursor")
|
|
476
941
|
def cursor(
|
|
477
|
-
apply: bool = typer.Option(False, "--apply", help="
|
|
942
|
+
apply: bool = typer.Option(False, "--apply", help="Write project Cursor MCP config instead of printing it."),
|
|
478
943
|
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
479
944
|
):
|
|
480
945
|
"""
|
|
481
946
|
Set up DevCouncil MCP tools for Cursor.
|
|
482
947
|
"""
|
|
483
948
|
root = _project_root(project_root)
|
|
484
|
-
|
|
485
|
-
|
|
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)
|
|
486
957
|
if not ok and apply:
|
|
487
958
|
raise typer.Exit(code=1)
|
|
488
959
|
|
|
489
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
|
+
|
|
490
1138
|
@app.command("all")
|
|
491
1139
|
def all_tools(
|
|
492
1140
|
apply: bool = typer.Option(False, "--apply", help="Run setup commands instead of printing them."),
|
|
@@ -494,6 +1142,11 @@ def all_tools(
|
|
|
494
1142
|
gemini_scope: str = typer.Option("project", "--gemini-scope", help="Gemini MCP config scope: project or user."),
|
|
495
1143
|
claude_scope: str = typer.Option("local", "--claude-scope", help="Claude MCP config scope: local, project, or user."),
|
|
496
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
|
+
),
|
|
497
1150
|
):
|
|
498
1151
|
"""
|
|
499
1152
|
Set up DevCouncil MCP tools and native hooks for every supported coding CLI found on PATH.
|
|
@@ -506,116 +1159,252 @@ def all_tools(
|
|
|
506
1159
|
raise typer.Exit(code=2)
|
|
507
1160
|
|
|
508
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
|
+
|
|
509
1177
|
commands = [
|
|
510
1178
|
("Codex CLI", _codex_command(root)),
|
|
511
1179
|
("Gemini CLI", _gemini_command(root, gemini_scope)),
|
|
512
1180
|
("Claude Code", _claude_command(root, claude_scope)),
|
|
513
|
-
("Cursor", _cursor_command(root)),
|
|
514
1181
|
]
|
|
515
|
-
results = []
|
|
516
1182
|
for tool, command in commands:
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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)
|
|
521
1189
|
if hooks:
|
|
522
1190
|
_configure_native_hooks(root, "all", apply)
|
|
523
|
-
|
|
524
|
-
|
|
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.")
|
|
525
1324
|
|
|
526
1325
|
|
|
527
1326
|
@app.command("hooks")
|
|
528
1327
|
def hooks(
|
|
529
1328
|
apply: bool = typer.Option(False, "--apply", help="Write native hook config files instead of previewing paths."),
|
|
530
1329
|
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
531
|
-
tool: str = typer.Option("all", "--tool", help="Hook target: all, codex, gemini, or
|
|
1330
|
+
tool: str = typer.Option("all", "--tool", help="Hook target: all, codex, gemini, claude, cursor, or opencode."),
|
|
532
1331
|
):
|
|
533
1332
|
"""
|
|
534
|
-
Install DevCouncil
|
|
1333
|
+
Install DevCouncil hook configuration for Codex, Gemini, Claude, Cursor, and OpenCode.
|
|
535
1334
|
"""
|
|
536
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
|
|
537
1343
|
_configure_native_hooks(root, tool, apply)
|
|
538
1344
|
|
|
539
1345
|
|
|
540
1346
|
@app.command("check")
|
|
541
1347
|
def check(
|
|
542
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
|
+
),
|
|
543
1362
|
):
|
|
544
1363
|
"""
|
|
545
1364
|
Check whether DevCouncil is ready to integrate with coding CLIs.
|
|
546
1365
|
"""
|
|
547
1366
|
root = _project_root(project_root)
|
|
1367
|
+
report = build_integration_check_report(root, strict=strict)
|
|
548
1368
|
table = Table(title="DevCouncil Integration Check")
|
|
549
1369
|
table.add_column("Check", style="cyan")
|
|
550
1370
|
table.add_column("Status", style="magenta")
|
|
551
1371
|
table.add_column("Details")
|
|
552
1372
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
from mcp.client.stdio import stdio_client
|
|
584
|
-
|
|
585
|
-
async def _list_tools() -> list[str]:
|
|
586
|
-
import os
|
|
587
|
-
|
|
588
|
-
env = os.environ.copy()
|
|
589
|
-
env["DEVCOUNCIL_PROJECT_ROOT"] = str(root)
|
|
590
|
-
params = StdioServerParameters(
|
|
591
|
-
command=sys.executable,
|
|
592
|
-
args=["-m", "devcouncil", "mcp-server"],
|
|
593
|
-
cwd=str(root),
|
|
594
|
-
env=env,
|
|
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)."
|
|
595
1403
|
)
|
|
596
|
-
async with stdio_client(params) as (read, write):
|
|
597
|
-
async with ClientSession(read, write) as session:
|
|
598
|
-
await session.initialize()
|
|
599
|
-
tools = await session.list_tools()
|
|
600
|
-
return [tool.name for tool in tools.tools]
|
|
601
|
-
|
|
602
|
-
import asyncio
|
|
603
|
-
|
|
604
|
-
tools = asyncio.run(_list_tools())
|
|
605
|
-
expected = {"devcouncil_status", "devcouncil_report", "devcouncil_get_task"}
|
|
606
|
-
add(expected.issubset(set(tools)), "MCP server", ", ".join(tools))
|
|
607
|
-
except Exception as exc:
|
|
608
|
-
add(False, "MCP server", str(exc))
|
|
609
|
-
|
|
610
|
-
console.print(table)
|
|
611
|
-
if failures:
|
|
612
|
-
console.print(
|
|
613
|
-
f"\n[yellow]Fix failed checks, then run:[/yellow] {PREFERRED_COMMAND} all --apply "
|
|
614
|
-
f"(or {LEGACY_COMMAND} --apply)."
|
|
615
|
-
)
|
|
616
1404
|
raise typer.Exit(code=1)
|
|
617
1405
|
|
|
618
|
-
|
|
1406
|
+
if not as_json:
|
|
1407
|
+
console.print(f"\n[green]Ready.[/green] Run: {PREFERRED_COMMAND} all --apply (or {LEGACY_COMMAND} --apply).")
|
|
619
1408
|
|
|
620
1409
|
|
|
621
1410
|
@setup_app.command("agent-flow")
|