devcouncil 0.1.0 → 0.1.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.
- package/LICENSE +201 -201
- package/README.md +62 -543
- package/package.json +1 -1
- package/pyproject.toml +29 -26
- package/src/devcouncil/__main__.py +4 -4
- package/src/devcouncil/app/__init__.py +28 -28
- package/src/devcouncil/app/config.py +135 -108
- package/src/devcouncil/app/errors.py +23 -23
- package/src/devcouncil/app/events.py +44 -44
- package/src/devcouncil/app/orchestrator.py +67 -67
- package/src/devcouncil/app/project_status.py +29 -0
- package/src/devcouncil/app/run_context.py +39 -39
- package/src/devcouncil/app/state_machine.py +108 -108
- package/src/devcouncil/artifacts/__init__.py +1 -1
- package/src/devcouncil/artifacts/coverage.py +96 -96
- package/src/devcouncil/artifacts/graph.py +143 -143
- package/src/devcouncil/artifacts/migrations.py +20 -20
- package/src/devcouncil/artifacts/schemas.py +23 -23
- package/src/devcouncil/artifacts/serializer.py +21 -21
- package/src/devcouncil/artifacts/validators.py +27 -27
- package/src/devcouncil/cli/commands/artifacts.py +51 -48
- package/src/devcouncil/cli/commands/ast.py +22 -0
- package/src/devcouncil/cli/commands/baseline.py +35 -32
- package/src/devcouncil/cli/commands/config.py +76 -54
- package/src/devcouncil/cli/commands/dashboard.py +26 -0
- package/src/devcouncil/cli/commands/doctor.py +86 -42
- package/src/devcouncil/cli/commands/go.py +237 -0
- package/src/devcouncil/cli/commands/hook.py +96 -29
- package/src/devcouncil/cli/commands/init.py +67 -56
- package/src/devcouncil/cli/commands/integrate.py +320 -14
- package/src/devcouncil/cli/commands/lsp.py +20 -0
- package/src/devcouncil/cli/commands/map.py +25 -21
- package/src/devcouncil/cli/commands/plan.py +257 -206
- package/src/devcouncil/cli/commands/prompt.py +36 -33
- package/src/devcouncil/cli/commands/repair.py +72 -69
- package/src/devcouncil/cli/commands/report.py +112 -54
- package/src/devcouncil/cli/commands/reset_demo_state.py +31 -28
- package/src/devcouncil/cli/commands/rollback.py +49 -47
- package/src/devcouncil/cli/commands/run.py +252 -207
- package/src/devcouncil/cli/commands/setup.py +159 -18
- package/src/devcouncil/cli/commands/show.py +76 -57
- package/src/devcouncil/cli/commands/status.py +117 -105
- package/src/devcouncil/cli/commands/tasks.py +55 -41
- package/src/devcouncil/cli/commands/trace.py +2 -1
- package/src/devcouncil/cli/commands/verify.py +158 -128
- package/src/devcouncil/cli/commands/version.py +20 -20
- package/src/devcouncil/cli/commands/watch.py +574 -0
- package/src/devcouncil/cli/main.py +42 -24
- package/src/devcouncil/council/prompts/arbiter.md +19 -19
- package/src/devcouncil/council/prompts/critic_a.md +10 -10
- package/src/devcouncil/council/prompts/critic_b.md +10 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
- package/src/devcouncil/council/prompts/planner_a.md +16 -16
- package/src/devcouncil/council/prompts/planner_b.md +16 -16
- package/src/devcouncil/council/prompts/rebuttal.md +10 -10
- package/src/devcouncil/council/prompts/spec_writer.md +12 -12
- package/src/devcouncil/domain/assumption.py +17 -17
- package/src/devcouncil/domain/critique.py +32 -32
- package/src/devcouncil/domain/evidence.py +27 -27
- package/src/devcouncil/domain/gap.py +26 -26
- package/src/devcouncil/domain/requirement.py +22 -22
- package/src/devcouncil/domain/task.py +26 -26
- package/src/devcouncil/execution/__init__.py +1 -1
- package/src/devcouncil/execution/context_builder.py +54 -54
- package/src/devcouncil/execution/executor.py +15 -15
- package/src/devcouncil/execution/hook_policy.py +24 -3
- package/src/devcouncil/execution/patch.py +28 -28
- package/src/devcouncil/execution/permissions.py +44 -44
- package/src/devcouncil/execution/prompt_builder.py +23 -23
- package/src/devcouncil/execution/task_runner.py +63 -63
- package/src/devcouncil/executors/__init__.py +1 -1
- package/src/devcouncil/executors/coding_cli.py +112 -0
- package/src/devcouncil/executors/mini_swe.py +63 -63
- package/src/devcouncil/executors/native/agent.py +81 -81
- package/src/devcouncil/executors/openhands.py +56 -56
- package/src/devcouncil/gating/__init__.py +1 -1
- package/src/devcouncil/gating/checks/clean_git.py +50 -45
- package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +34 -34
- package/src/devcouncil/gating/policy.py +157 -157
- package/src/devcouncil/indexing/__init__.py +1 -1
- package/src/devcouncil/indexing/ast_matcher.py +168 -0
- package/src/devcouncil/indexing/graph_index.py +48 -48
- package/src/devcouncil/indexing/lsp.py +120 -0
- package/src/devcouncil/indexing/repo_mapper.py +208 -204
- package/src/devcouncil/integrations/github.py +35 -35
- package/src/devcouncil/integrations/gitnexus.py +27 -27
- package/src/devcouncil/integrations/graphify.py +34 -34
- package/src/devcouncil/integrations/mcp/server.py +549 -96
- package/src/devcouncil/integrations/pr_comments.py +62 -0
- package/src/devcouncil/live/__init__.py +2 -0
- package/src/devcouncil/live/cards.py +207 -0
- package/src/devcouncil/live/models.py +63 -0
- package/src/devcouncil/live/repair_prompt.py +83 -0
- package/src/devcouncil/live/reviewer.py +70 -0
- package/src/devcouncil/live/signals.py +135 -0
- package/src/devcouncil/live/summary.py +34 -0
- package/src/devcouncil/live/tasks.py +18 -0
- package/src/devcouncil/live/transcripts.py +138 -0
- package/src/devcouncil/llm/__init__.py +1 -1
- package/src/devcouncil/llm/cache.py +38 -38
- package/src/devcouncil/llm/provider.py +146 -125
- package/src/devcouncil/llm/router.py +111 -111
- package/src/devcouncil/planning/__init__.py +1 -1
- package/src/devcouncil/planning/arbiter_service.py +57 -57
- package/src/devcouncil/planning/critique_service.py +66 -66
- package/src/devcouncil/planning/plan_service.py +46 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +86 -0
- package/src/devcouncil/planning/repair_service.py +39 -39
- package/src/devcouncil/planning/spec_service.py +44 -44
- package/src/devcouncil/reporting/github_check.py +32 -32
- package/src/devcouncil/reporting/json_report.py +20 -17
- package/src/devcouncil/reporting/markdown_report.py +68 -46
- package/src/devcouncil/reporting/report_builder.py +14 -14
- package/src/devcouncil/storage/db.py +66 -66
- package/src/devcouncil/storage/models.py +83 -83
- package/src/devcouncil/storage/repositories.py +299 -222
- package/src/devcouncil/telemetry/cost.py +34 -34
- package/src/devcouncil/telemetry/tracker.py +49 -49
- package/src/devcouncil/ui/__init__.py +1 -0
- package/src/devcouncil/ui/dashboard.py +122 -0
- package/src/devcouncil/utils/__init__.py +1 -1
- package/src/devcouncil/utils/redaction.py +141 -141
- package/src/devcouncil/verification/__init__.py +1 -1
- package/src/devcouncil/verification/implementation_reviewer.py +55 -55
- package/src/devcouncil/verification/verifier.py +319 -302
- package/uv.lock +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import shlex
|
|
1
3
|
import shutil
|
|
2
4
|
import subprocess
|
|
3
5
|
import sys
|
|
@@ -13,7 +15,10 @@ setup_app = typer.Typer(help="Set up optional external companion integrations.")
|
|
|
13
15
|
app.add_typer(setup_app, name="setup")
|
|
14
16
|
console = Console()
|
|
15
17
|
|
|
16
|
-
SUPPORTED_TOOLS = ("codex", "gemini")
|
|
18
|
+
SUPPORTED_TOOLS = ("codex", "gemini", "claude", "cursor")
|
|
19
|
+
SUPPORTED_HOOK_TOOLS = ("codex", "gemini", "claude")
|
|
20
|
+
PREFERRED_COMMAND = "dev integrate"
|
|
21
|
+
LEGACY_COMMAND = "dev setup --integrate"
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
def _project_root(path: Path | None) -> Path:
|
|
@@ -51,8 +56,56 @@ def _gemini_command(project_root: Path, scope: str) -> list[str]:
|
|
|
51
56
|
]
|
|
52
57
|
|
|
53
58
|
|
|
59
|
+
def _claude_command(project_root: Path, scope: str) -> list[str]:
|
|
60
|
+
return [
|
|
61
|
+
"claude",
|
|
62
|
+
"mcp",
|
|
63
|
+
"add",
|
|
64
|
+
"--scope",
|
|
65
|
+
scope,
|
|
66
|
+
"--env",
|
|
67
|
+
f"DEVCOUNCIL_PROJECT_ROOT={project_root}",
|
|
68
|
+
"devcouncil",
|
|
69
|
+
"--",
|
|
70
|
+
*_server_args(project_root),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _cursor_command(project_root: Path) -> list[str]:
|
|
75
|
+
server = {
|
|
76
|
+
"name": "devcouncil",
|
|
77
|
+
"command": "devcouncil",
|
|
78
|
+
"args": ["mcp-server"],
|
|
79
|
+
"env": {"DEVCOUNCIL_PROJECT_ROOT": str(project_root)},
|
|
80
|
+
}
|
|
81
|
+
return ["cursor", "--add-mcp", json.dumps(server, separators=(",", ":"))]
|
|
82
|
+
|
|
83
|
+
|
|
54
84
|
def _format_command(command: list[str]) -> str:
|
|
55
|
-
|
|
85
|
+
if sys.platform == "win32":
|
|
86
|
+
return " ".join(_quote_powershell_arg(arg) for arg in command)
|
|
87
|
+
return shlex.join(command)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _quote_powershell_arg(arg: str) -> str:
|
|
91
|
+
if arg == "":
|
|
92
|
+
return "''"
|
|
93
|
+
special_chars = set(" \t\r\n'\"{}[](),;|&<>")
|
|
94
|
+
if not any(char in special_chars for char in arg):
|
|
95
|
+
return arg
|
|
96
|
+
return "'" + arg.replace("'", "''") + "'"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _hook_command(project_root: Path, client: str, event: str) -> str:
|
|
100
|
+
return _format_command([
|
|
101
|
+
"devcouncil",
|
|
102
|
+
"hook",
|
|
103
|
+
event,
|
|
104
|
+
"--client",
|
|
105
|
+
client,
|
|
106
|
+
"--project-root",
|
|
107
|
+
str(project_root),
|
|
108
|
+
])
|
|
56
109
|
|
|
57
110
|
|
|
58
111
|
def _run(command: list[str]) -> int:
|
|
@@ -106,6 +159,181 @@ def _save_raw_config(project_root: Path, config: dict) -> None:
|
|
|
106
159
|
path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8")
|
|
107
160
|
|
|
108
161
|
|
|
162
|
+
def _load_json(path: Path) -> dict:
|
|
163
|
+
if not path.exists():
|
|
164
|
+
return {}
|
|
165
|
+
try:
|
|
166
|
+
return json.loads(path.read_text(encoding="utf-8")) or {}
|
|
167
|
+
except json.JSONDecodeError:
|
|
168
|
+
return {}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _save_json(path: Path, data: dict) -> None:
|
|
172
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _upsert_hook(settings: dict, event: str, matcher: str, command: str, name: str) -> None:
|
|
177
|
+
hooks = settings.setdefault("hooks", {})
|
|
178
|
+
groups = hooks.setdefault(event, [])
|
|
179
|
+
for group in groups:
|
|
180
|
+
if group.get("matcher") == matcher:
|
|
181
|
+
group_hooks = group.setdefault("hooks", [])
|
|
182
|
+
if not any(hook.get("command") == command for hook in group_hooks):
|
|
183
|
+
group_hooks.append({
|
|
184
|
+
"type": "command",
|
|
185
|
+
"name": name,
|
|
186
|
+
"command": command,
|
|
187
|
+
"timeout": 10000,
|
|
188
|
+
})
|
|
189
|
+
return
|
|
190
|
+
groups.append({
|
|
191
|
+
"matcher": matcher,
|
|
192
|
+
"hooks": [{
|
|
193
|
+
"type": "command",
|
|
194
|
+
"name": name,
|
|
195
|
+
"command": command,
|
|
196
|
+
"timeout": 10000,
|
|
197
|
+
}],
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _ensure_codex_hooks_enabled(project_root: Path) -> Path:
|
|
202
|
+
config_path = project_root / ".codex" / "config.toml"
|
|
203
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
existing = config_path.read_text(encoding="utf-8") if config_path.exists() else ""
|
|
205
|
+
if "codex_hooks" not in existing:
|
|
206
|
+
if "[features]" in existing:
|
|
207
|
+
lines = existing.splitlines()
|
|
208
|
+
updated: list[str] = []
|
|
209
|
+
in_features = False
|
|
210
|
+
inserted = False
|
|
211
|
+
for line in lines:
|
|
212
|
+
stripped = line.strip()
|
|
213
|
+
if stripped == "[features]":
|
|
214
|
+
in_features = True
|
|
215
|
+
updated.append(line)
|
|
216
|
+
continue
|
|
217
|
+
if in_features and stripped.startswith("[") and stripped.endswith("]"):
|
|
218
|
+
updated.append("codex_hooks = true")
|
|
219
|
+
inserted = True
|
|
220
|
+
in_features = False
|
|
221
|
+
updated.append(line)
|
|
222
|
+
if in_features and not inserted:
|
|
223
|
+
updated.append("codex_hooks = true")
|
|
224
|
+
config_path.write_text("\n".join(updated) + "\n", encoding="utf-8")
|
|
225
|
+
else:
|
|
226
|
+
separator = "\n" if existing and not existing.endswith("\n") else ""
|
|
227
|
+
config_path.write_text(f"{existing}{separator}\n[features]\ncodex_hooks = true\n", encoding="utf-8")
|
|
228
|
+
return config_path
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _install_codex_hooks(project_root: Path) -> list[Path]:
|
|
232
|
+
path = project_root / ".codex" / "hooks.json"
|
|
233
|
+
settings = _load_json(path)
|
|
234
|
+
matcher = "Bash|shell_command|exec_command|local_shell|Write|Edit|MultiEdit|write_file|edit_file|apply_patch"
|
|
235
|
+
_upsert_hook(
|
|
236
|
+
settings,
|
|
237
|
+
"PreToolUse",
|
|
238
|
+
matcher,
|
|
239
|
+
_hook_command(project_root, "codex", "pre-tool-use"),
|
|
240
|
+
"devcouncil-pre-tool-use",
|
|
241
|
+
)
|
|
242
|
+
_upsert_hook(
|
|
243
|
+
settings,
|
|
244
|
+
"PostToolUse",
|
|
245
|
+
matcher,
|
|
246
|
+
_hook_command(project_root, "codex", "post-tool-use"),
|
|
247
|
+
"devcouncil-post-tool-use",
|
|
248
|
+
)
|
|
249
|
+
_save_json(path, settings)
|
|
250
|
+
return [path, _ensure_codex_hooks_enabled(project_root)]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _install_gemini_hooks(project_root: Path) -> list[Path]:
|
|
254
|
+
path = project_root / ".gemini" / "settings.json"
|
|
255
|
+
settings = _load_json(path)
|
|
256
|
+
matcher = "run_shell_command|shell_command|write_file|edit_file|replace|apply_patch"
|
|
257
|
+
_upsert_hook(
|
|
258
|
+
settings,
|
|
259
|
+
"BeforeTool",
|
|
260
|
+
matcher,
|
|
261
|
+
_hook_command(project_root, "gemini", "pre-tool-use"),
|
|
262
|
+
"devcouncil-pre-tool-use",
|
|
263
|
+
)
|
|
264
|
+
_upsert_hook(
|
|
265
|
+
settings,
|
|
266
|
+
"AfterTool",
|
|
267
|
+
matcher,
|
|
268
|
+
_hook_command(project_root, "gemini", "post-tool-use"),
|
|
269
|
+
"devcouncil-post-tool-use",
|
|
270
|
+
)
|
|
271
|
+
_save_json(path, settings)
|
|
272
|
+
return [path]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _install_claude_hooks(project_root: Path) -> list[Path]:
|
|
276
|
+
path = project_root / ".claude" / "settings.local.json"
|
|
277
|
+
settings = _load_json(path)
|
|
278
|
+
matcher = "Bash|Write|Edit|MultiEdit"
|
|
279
|
+
_upsert_hook(
|
|
280
|
+
settings,
|
|
281
|
+
"PreToolUse",
|
|
282
|
+
matcher,
|
|
283
|
+
_hook_command(project_root, "claude", "pre-tool-use"),
|
|
284
|
+
"devcouncil-pre-tool-use",
|
|
285
|
+
)
|
|
286
|
+
_upsert_hook(
|
|
287
|
+
settings,
|
|
288
|
+
"PostToolUse",
|
|
289
|
+
matcher,
|
|
290
|
+
_hook_command(project_root, "claude", "post-tool-use"),
|
|
291
|
+
"devcouncil-post-tool-use",
|
|
292
|
+
)
|
|
293
|
+
_upsert_hook(
|
|
294
|
+
settings,
|
|
295
|
+
"Stop",
|
|
296
|
+
"",
|
|
297
|
+
_hook_command(project_root, "claude", "agent-response"),
|
|
298
|
+
"devcouncil-agent-response-ready",
|
|
299
|
+
)
|
|
300
|
+
_save_json(path, settings)
|
|
301
|
+
return [path]
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _preview_hook_paths(project_root: Path, tool: str) -> list[tuple[str, Path]]:
|
|
305
|
+
paths = {
|
|
306
|
+
"codex": [project_root / ".codex" / "hooks.json", project_root / ".codex" / "config.toml"],
|
|
307
|
+
"gemini": [project_root / ".gemini" / "settings.json"],
|
|
308
|
+
"claude": [project_root / ".claude" / "settings.local.json"],
|
|
309
|
+
}
|
|
310
|
+
selected = SUPPORTED_HOOK_TOOLS if tool == "all" else (tool,)
|
|
311
|
+
return [(client, path) for client in selected for path in paths[client]]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _configure_native_hooks(project_root: Path, tool: str = "all", apply: bool = False) -> None:
|
|
315
|
+
if tool not in {"all", *SUPPORTED_HOOK_TOOLS}:
|
|
316
|
+
console.print("[red]--tool must be one of: all, codex, gemini, claude.[/red]")
|
|
317
|
+
raise typer.Exit(code=2)
|
|
318
|
+
|
|
319
|
+
if not apply:
|
|
320
|
+
console.print("[bold]Native hook config preview[/bold]")
|
|
321
|
+
for client, path in _preview_hook_paths(project_root, tool):
|
|
322
|
+
console.print(f"{client}: {path}")
|
|
323
|
+
console.print("[yellow]Preview only. Rerun with --apply to write hook config files.[/yellow]")
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
selected = SUPPORTED_HOOK_TOOLS if tool == "all" else (tool,)
|
|
327
|
+
installers = {
|
|
328
|
+
"codex": _install_codex_hooks,
|
|
329
|
+
"gemini": _install_gemini_hooks,
|
|
330
|
+
"claude": _install_claude_hooks,
|
|
331
|
+
}
|
|
332
|
+
for client in selected:
|
|
333
|
+
written = installers[client](project_root)
|
|
334
|
+
console.print(f"[green]{client} native hooks configured:[/green] {', '.join(str(path) for path in written)}")
|
|
335
|
+
|
|
336
|
+
|
|
109
337
|
def _print_command(tool: str, command: list[str], apply: bool):
|
|
110
338
|
if apply:
|
|
111
339
|
console.print(f"[cyan]Configuring {tool} MCP integration...[/cyan]")
|
|
@@ -148,10 +376,14 @@ def overview(ctx: typer.Context):
|
|
|
148
376
|
table.add_column("Tool", style="cyan")
|
|
149
377
|
table.add_column("Setup command", style="green")
|
|
150
378
|
table.add_column("Notes")
|
|
151
|
-
table.add_row("Codex CLI", "
|
|
152
|
-
table.add_row("Gemini CLI", "
|
|
153
|
-
table.add_row("
|
|
379
|
+
table.add_row("Codex CLI", f"{PREFERRED_COMMAND} codex --apply", "Adds DevCouncil as a stdio MCP server.")
|
|
380
|
+
table.add_row("Gemini CLI", f"{PREFERRED_COMMAND} gemini --apply", "Adds DevCouncil as a project-scoped stdio MCP server.")
|
|
381
|
+
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", "Adds DevCouncil as a Cursor MCP server.")
|
|
383
|
+
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 Claude hook files.")
|
|
154
385
|
console.print(table)
|
|
386
|
+
console.print(f"\nIf your install exposes only the setup flow, use: {LEGACY_COMMAND} --apply")
|
|
155
387
|
console.print("\nRun without [bold]--apply[/bold] to preview the exact commands first.")
|
|
156
388
|
|
|
157
389
|
|
|
@@ -166,9 +398,11 @@ def integrations_doctor():
|
|
|
166
398
|
checks = [
|
|
167
399
|
("Agent Flow", "agent-flow-app", "Optional live/replay visualizer for trace JSONL."),
|
|
168
400
|
("code-review-graph", "code-review-graph", "Optional structural graph context adapter."),
|
|
169
|
-
("Claude Code", "claude", "Optional hook runtime for pre-tool-use enforcement."),
|
|
170
|
-
("Codex CLI", "codex", "Optional MCP client
|
|
171
|
-
("Gemini CLI", "gemini", "Optional MCP client companion."),
|
|
401
|
+
("Claude Code", "claude", "Optional MCP client and native hook runtime for pre-tool-use enforcement."),
|
|
402
|
+
("Codex CLI", "codex", "Optional MCP client, headless executor companion, and native hook runtime."),
|
|
403
|
+
("Gemini CLI", "gemini", "Optional MCP client companion and native hook runtime."),
|
|
404
|
+
("Cursor", "cursor", "Optional MCP client and agent companion."),
|
|
405
|
+
("Aider", "aider", "Optional prompt/stdin sidecar; no first-party MCP setup command."),
|
|
172
406
|
]
|
|
173
407
|
for label, executable, notes in checks:
|
|
174
408
|
found = shutil.which(executable)
|
|
@@ -218,28 +452,91 @@ def gemini(
|
|
|
218
452
|
raise typer.Exit(code=1)
|
|
219
453
|
|
|
220
454
|
|
|
455
|
+
@app.command("claude")
|
|
456
|
+
def claude(
|
|
457
|
+
apply: bool = typer.Option(False, "--apply", help="Run the setup command instead of printing it."),
|
|
458
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
459
|
+
scope: str = typer.Option("local", "--scope", help="Claude MCP config scope: local, project, or user."),
|
|
460
|
+
):
|
|
461
|
+
"""
|
|
462
|
+
Set up DevCouncil MCP tools for Claude Code.
|
|
463
|
+
"""
|
|
464
|
+
if scope not in {"local", "project", "user"}:
|
|
465
|
+
console.print("[red]--scope must be 'local', 'project', or 'user'.[/red]")
|
|
466
|
+
raise typer.Exit(code=2)
|
|
467
|
+
|
|
468
|
+
root = _project_root(project_root)
|
|
469
|
+
command = _claude_command(root, scope)
|
|
470
|
+
ok = _configure("Claude Code", command, apply)
|
|
471
|
+
if not ok and apply:
|
|
472
|
+
raise typer.Exit(code=1)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@app.command("cursor")
|
|
476
|
+
def cursor(
|
|
477
|
+
apply: bool = typer.Option(False, "--apply", help="Run the setup command instead of printing it."),
|
|
478
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
479
|
+
):
|
|
480
|
+
"""
|
|
481
|
+
Set up DevCouncil MCP tools for Cursor.
|
|
482
|
+
"""
|
|
483
|
+
root = _project_root(project_root)
|
|
484
|
+
command = _cursor_command(root)
|
|
485
|
+
ok = _configure("Cursor", command, apply)
|
|
486
|
+
if not ok and apply:
|
|
487
|
+
raise typer.Exit(code=1)
|
|
488
|
+
|
|
489
|
+
|
|
221
490
|
@app.command("all")
|
|
222
491
|
def all_tools(
|
|
223
492
|
apply: bool = typer.Option(False, "--apply", help="Run setup commands instead of printing them."),
|
|
224
493
|
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
225
494
|
gemini_scope: str = typer.Option("project", "--gemini-scope", help="Gemini MCP config scope: project or user."),
|
|
495
|
+
claude_scope: str = typer.Option("local", "--claude-scope", help="Claude MCP config scope: local, project, or user."),
|
|
496
|
+
hooks: bool = typer.Option(True, "--hooks/--no-hooks", help="Include native Codex, Gemini, and Claude hook setup."),
|
|
226
497
|
):
|
|
227
498
|
"""
|
|
228
|
-
Set up DevCouncil MCP tools for every supported coding CLI found on PATH.
|
|
499
|
+
Set up DevCouncil MCP tools and native hooks for every supported coding CLI found on PATH.
|
|
229
500
|
"""
|
|
230
501
|
if gemini_scope not in {"project", "user"}:
|
|
231
502
|
console.print("[red]--gemini-scope must be 'project' or 'user'.[/red]")
|
|
232
503
|
raise typer.Exit(code=2)
|
|
504
|
+
if claude_scope not in {"local", "project", "user"}:
|
|
505
|
+
console.print("[red]--claude-scope must be 'local', 'project', or 'user'.[/red]")
|
|
506
|
+
raise typer.Exit(code=2)
|
|
233
507
|
|
|
234
508
|
root = _project_root(project_root)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
509
|
+
commands = [
|
|
510
|
+
("Codex CLI", _codex_command(root)),
|
|
511
|
+
("Gemini CLI", _gemini_command(root, gemini_scope)),
|
|
512
|
+
("Claude Code", _claude_command(root, claude_scope)),
|
|
513
|
+
("Cursor", _cursor_command(root)),
|
|
238
514
|
]
|
|
515
|
+
results = []
|
|
516
|
+
for tool, command in commands:
|
|
517
|
+
if apply and not shutil.which(command[0]):
|
|
518
|
+
console.print(f"[yellow]{tool} CLI not found on PATH. Skipping optional integration.[/yellow]")
|
|
519
|
+
continue
|
|
520
|
+
results.append(_configure(tool, command, apply))
|
|
521
|
+
if hooks:
|
|
522
|
+
_configure_native_hooks(root, "all", apply)
|
|
239
523
|
if apply and not all(results):
|
|
240
524
|
raise typer.Exit(code=1)
|
|
241
525
|
|
|
242
526
|
|
|
527
|
+
@app.command("hooks")
|
|
528
|
+
def hooks(
|
|
529
|
+
apply: bool = typer.Option(False, "--apply", help="Write native hook config files instead of previewing paths."),
|
|
530
|
+
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 claude."),
|
|
532
|
+
):
|
|
533
|
+
"""
|
|
534
|
+
Install DevCouncil native hook configuration for hook-capable coding CLIs.
|
|
535
|
+
"""
|
|
536
|
+
root = _project_root(project_root)
|
|
537
|
+
_configure_native_hooks(root, tool, apply)
|
|
538
|
+
|
|
539
|
+
|
|
243
540
|
@app.command("check")
|
|
244
541
|
def check(
|
|
245
542
|
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
@@ -275,6 +572,12 @@ def check(
|
|
|
275
572
|
code, output = _run_capture(["gemini", "--version"])
|
|
276
573
|
add(code == 0, "Gemini CLI", output.splitlines()[0] if output else "Optional; install Gemini CLI to use this integration.")
|
|
277
574
|
|
|
575
|
+
code, output = _run_capture(["claude", "--version"])
|
|
576
|
+
add(code == 0, "Claude Code", output.splitlines()[0] if output else "Optional; install Claude Code to use this integration.")
|
|
577
|
+
|
|
578
|
+
code, output = _run_capture(["cursor", "--version"])
|
|
579
|
+
add(code == 0, "Cursor", output.splitlines()[0] if output else "Optional; install Cursor to use this integration.")
|
|
580
|
+
|
|
278
581
|
try:
|
|
279
582
|
from mcp import ClientSession, StdioServerParameters
|
|
280
583
|
from mcp.client.stdio import stdio_client
|
|
@@ -306,10 +609,13 @@ def check(
|
|
|
306
609
|
|
|
307
610
|
console.print(table)
|
|
308
611
|
if failures:
|
|
309
|
-
console.print(
|
|
612
|
+
console.print(
|
|
613
|
+
f"\n[yellow]Fix failed checks, then run:[/yellow] {PREFERRED_COMMAND} all --apply "
|
|
614
|
+
f"(or {LEGACY_COMMAND} --apply)."
|
|
615
|
+
)
|
|
310
616
|
raise typer.Exit(code=1)
|
|
311
617
|
|
|
312
|
-
console.print("\n[green]Ready.[/green] Run:
|
|
618
|
+
console.print(f"\n[green]Ready.[/green] Run: {PREFERRED_COMMAND} all --apply (or {LEGACY_COMMAND} --apply).")
|
|
313
619
|
|
|
314
620
|
|
|
315
621
|
@setup_app.command("agent-flow")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from devcouncil.indexing.lsp import LspInspector
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(help="Inspect optional LSP integration readiness.")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command("inspect")
|
|
12
|
+
def inspect(
|
|
13
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root."),
|
|
14
|
+
):
|
|
15
|
+
"""Print detected language servers and initialize payloads."""
|
|
16
|
+
root = project_root.expanduser().resolve()
|
|
17
|
+
if not root.exists():
|
|
18
|
+
typer.echo(json.dumps({"languages": [], "servers": [], "initialize_requests": {}, "error": f"{root} does not exist"}, indent=2))
|
|
19
|
+
raise typer.Exit(code=1)
|
|
20
|
+
typer.echo(LspInspector(root).summary_json())
|
|
@@ -2,32 +2,36 @@ import json
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
|
-
from rich.console import Console
|
|
6
|
-
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
7
8
|
from devcouncil.indexing.repo_mapper import RepoMapper
|
|
8
9
|
from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
|
|
9
10
|
from devcouncil.storage.db import get_db
|
|
10
|
-
|
|
11
|
+
|
|
11
12
|
console = Console()
|
|
12
13
|
status_console = Console(stderr=True)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def map_repo(
|
|
16
|
-
goal: str = typer.Argument("", help="Goal text used for candidate-file ranking."),
|
|
17
|
-
output: Path = typer.Option(
|
|
18
|
-
Path(".devcouncil/repo_map.json"),
|
|
19
|
-
"--output",
|
|
20
|
-
"-o",
|
|
21
|
-
help="Path to write repo_map.json.",
|
|
22
|
-
),
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def map_repo(
|
|
17
|
+
goal: str = typer.Argument("", help="Goal text used for candidate-file ranking."),
|
|
18
|
+
output: Path = typer.Option(
|
|
19
|
+
Path(".devcouncil/repo_map.json"),
|
|
20
|
+
"--output",
|
|
21
|
+
"-o",
|
|
22
|
+
help="Path to write repo_map.json.",
|
|
23
|
+
),
|
|
24
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
25
|
+
):
|
|
26
|
+
"""Build the deterministic repository map without calling an LLM."""
|
|
27
|
+
root = project_root.expanduser().resolve()
|
|
28
|
+
initialize_project(root, quiet=True)
|
|
29
|
+
if not get_db(root):
|
|
30
|
+
raise typer.Exit(code=1)
|
|
31
|
+
|
|
32
|
+
repo_map = RepoMapper(root).map_repo(goal)
|
|
33
|
+
graph_context = CodeReviewGraphAdapter(root).get_context()
|
|
34
|
+
output = output if output.is_absolute() else root / output
|
|
31
35
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
32
36
|
output.write_text(repo_map.model_dump_json(indent=2), encoding="utf-8")
|
|
33
37
|
if graph_context.available:
|