devcouncil 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +218 -96
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +7 -11
  4. package/pyproject.toml +0 -34
  5. package/src/devcouncil/__init__.py +0 -0
  6. package/src/devcouncil/__main__.py +0 -4
  7. package/src/devcouncil/app/__init__.py +0 -28
  8. package/src/devcouncil/app/config.py +0 -158
  9. package/src/devcouncil/app/errors.py +0 -23
  10. package/src/devcouncil/app/events.py +0 -44
  11. package/src/devcouncil/app/orchestrator.py +0 -92
  12. package/src/devcouncil/app/project_status.py +0 -29
  13. package/src/devcouncil/app/run_context.py +0 -39
  14. package/src/devcouncil/app/state_machine.py +0 -108
  15. package/src/devcouncil/artifacts/__init__.py +0 -1
  16. package/src/devcouncil/artifacts/coverage.py +0 -96
  17. package/src/devcouncil/artifacts/graph.py +0 -143
  18. package/src/devcouncil/artifacts/migrations.py +0 -20
  19. package/src/devcouncil/artifacts/schemas.py +0 -23
  20. package/src/devcouncil/artifacts/serializer.py +0 -21
  21. package/src/devcouncil/artifacts/validators.py +0 -27
  22. package/src/devcouncil/cli/__init__.py +0 -0
  23. package/src/devcouncil/cli/commands/__init__.py +0 -0
  24. package/src/devcouncil/cli/commands/artifacts.py +0 -51
  25. package/src/devcouncil/cli/commands/ast.py +0 -22
  26. package/src/devcouncil/cli/commands/baseline.py +0 -35
  27. package/src/devcouncil/cli/commands/config.py +0 -76
  28. package/src/devcouncil/cli/commands/dashboard.py +0 -26
  29. package/src/devcouncil/cli/commands/doctor.py +0 -140
  30. package/src/devcouncil/cli/commands/go.py +0 -237
  31. package/src/devcouncil/cli/commands/hook.py +0 -128
  32. package/src/devcouncil/cli/commands/init.py +0 -153
  33. package/src/devcouncil/cli/commands/integrate.py +0 -726
  34. package/src/devcouncil/cli/commands/lsp.py +0 -20
  35. package/src/devcouncil/cli/commands/map.py +0 -42
  36. package/src/devcouncil/cli/commands/mcp_server.py +0 -18
  37. package/src/devcouncil/cli/commands/plan.py +0 -327
  38. package/src/devcouncil/cli/commands/prompt.py +0 -50
  39. package/src/devcouncil/cli/commands/repair.py +0 -72
  40. package/src/devcouncil/cli/commands/report.py +0 -129
  41. package/src/devcouncil/cli/commands/reset_demo_state.py +0 -31
  42. package/src/devcouncil/cli/commands/rollback.py +0 -60
  43. package/src/devcouncil/cli/commands/run.py +0 -269
  44. package/src/devcouncil/cli/commands/setup.py +0 -223
  45. package/src/devcouncil/cli/commands/show.py +0 -76
  46. package/src/devcouncil/cli/commands/status.py +0 -117
  47. package/src/devcouncil/cli/commands/tasks.py +0 -55
  48. package/src/devcouncil/cli/commands/trace.py +0 -44
  49. package/src/devcouncil/cli/commands/verify.py +0 -193
  50. package/src/devcouncil/cli/commands/version.py +0 -20
  51. package/src/devcouncil/cli/commands/watch.py +0 -574
  52. package/src/devcouncil/cli/main.py +0 -88
  53. package/src/devcouncil/council/__init__.py +0 -0
  54. package/src/devcouncil/council/prompts/__init__.py +0 -0
  55. package/src/devcouncil/council/prompts/arbiter.md +0 -19
  56. package/src/devcouncil/council/prompts/critic_a.md +0 -10
  57. package/src/devcouncil/council/prompts/critic_b.md +0 -10
  58. package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
  59. package/src/devcouncil/council/prompts/planner_a.md +0 -16
  60. package/src/devcouncil/council/prompts/planner_b.md +0 -16
  61. package/src/devcouncil/council/prompts/rebuttal.md +0 -10
  62. package/src/devcouncil/council/prompts/spec_writer.md +0 -12
  63. package/src/devcouncil/domain/__init__.py +0 -0
  64. package/src/devcouncil/domain/assumption.py +0 -17
  65. package/src/devcouncil/domain/critique.py +0 -32
  66. package/src/devcouncil/domain/evidence.py +0 -27
  67. package/src/devcouncil/domain/gap.py +0 -26
  68. package/src/devcouncil/domain/requirement.py +0 -22
  69. package/src/devcouncil/domain/task.py +0 -26
  70. package/src/devcouncil/execution/__init__.py +0 -1
  71. package/src/devcouncil/execution/context_builder.py +0 -60
  72. package/src/devcouncil/execution/executor.py +0 -15
  73. package/src/devcouncil/execution/hook_policy.py +0 -165
  74. package/src/devcouncil/execution/patch.py +0 -28
  75. package/src/devcouncil/execution/paths.py +0 -14
  76. package/src/devcouncil/execution/permissions.py +0 -92
  77. package/src/devcouncil/execution/prompt_builder.py +0 -59
  78. package/src/devcouncil/execution/task_runner.py +0 -166
  79. package/src/devcouncil/executors/__init__.py +0 -1
  80. package/src/devcouncil/executors/coding_cli.py +0 -112
  81. package/src/devcouncil/executors/mini_swe.py +0 -73
  82. package/src/devcouncil/executors/native/__init__.py +0 -0
  83. package/src/devcouncil/executors/native/agent.py +0 -107
  84. package/src/devcouncil/executors/openhands.py +0 -71
  85. package/src/devcouncil/gating/__init__.py +0 -1
  86. package/src/devcouncil/gating/checks/__init__.py +0 -0
  87. package/src/devcouncil/gating/checks/clean_git.py +0 -50
  88. package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
  89. package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
  90. package/src/devcouncil/gating/checks/secret_scan_check.py +0 -34
  91. package/src/devcouncil/gating/policy.py +0 -190
  92. package/src/devcouncil/indexing/__init__.py +0 -1
  93. package/src/devcouncil/indexing/ast_matcher.py +0 -168
  94. package/src/devcouncil/indexing/graph_index.py +0 -48
  95. package/src/devcouncil/indexing/lsp.py +0 -120
  96. package/src/devcouncil/indexing/repo_mapper.py +0 -208
  97. package/src/devcouncil/indexing/symbol_index.py +0 -0
  98. package/src/devcouncil/integrations/code_review_graph.py +0 -163
  99. package/src/devcouncil/integrations/github.py +0 -39
  100. package/src/devcouncil/integrations/gitnexus.py +0 -27
  101. package/src/devcouncil/integrations/graphify.py +0 -34
  102. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  103. package/src/devcouncil/integrations/mcp/server.py +0 -599
  104. package/src/devcouncil/integrations/pr_comments.py +0 -62
  105. package/src/devcouncil/live/__init__.py +0 -2
  106. package/src/devcouncil/live/cards.py +0 -207
  107. package/src/devcouncil/live/models.py +0 -63
  108. package/src/devcouncil/live/repair_prompt.py +0 -83
  109. package/src/devcouncil/live/reviewer.py +0 -70
  110. package/src/devcouncil/live/signals.py +0 -135
  111. package/src/devcouncil/live/summary.py +0 -34
  112. package/src/devcouncil/live/tasks.py +0 -18
  113. package/src/devcouncil/live/transcripts.py +0 -138
  114. package/src/devcouncil/llm/__init__.py +0 -1
  115. package/src/devcouncil/llm/cache.py +0 -38
  116. package/src/devcouncil/llm/provider.py +0 -146
  117. package/src/devcouncil/llm/router.py +0 -125
  118. package/src/devcouncil/planning/__init__.py +0 -1
  119. package/src/devcouncil/planning/arbiter_service.py +0 -57
  120. package/src/devcouncil/planning/critique_service.py +0 -66
  121. package/src/devcouncil/planning/plan_service.py +0 -46
  122. package/src/devcouncil/planning/prompt_enhancer_service.py +0 -86
  123. package/src/devcouncil/planning/repair_service.py +0 -39
  124. package/src/devcouncil/planning/spec_service.py +0 -44
  125. package/src/devcouncil/repo/__init__.py +0 -0
  126. package/src/devcouncil/reporting/__init__.py +0 -0
  127. package/src/devcouncil/reporting/github_check.py +0 -32
  128. package/src/devcouncil/reporting/json_report.py +0 -20
  129. package/src/devcouncil/reporting/markdown_report.py +0 -68
  130. package/src/devcouncil/reporting/report_builder.py +0 -14
  131. package/src/devcouncil/storage/__init__.py +0 -0
  132. package/src/devcouncil/storage/db.py +0 -66
  133. package/src/devcouncil/storage/models.py +0 -83
  134. package/src/devcouncil/storage/repositories.py +0 -423
  135. package/src/devcouncil/telemetry/__init__.py +0 -0
  136. package/src/devcouncil/telemetry/cost.py +0 -34
  137. package/src/devcouncil/telemetry/traces.py +0 -91
  138. package/src/devcouncil/telemetry/tracker.py +0 -49
  139. package/src/devcouncil/ui/__init__.py +0 -1
  140. package/src/devcouncil/ui/dashboard.py +0 -122
  141. package/src/devcouncil/utils/__init__.py +0 -1
  142. package/src/devcouncil/utils/redaction.py +0 -141
  143. package/src/devcouncil/verification/__init__.py +0 -1
  144. package/src/devcouncil/verification/implementation_reviewer.py +0 -55
  145. package/src/devcouncil/verification/verifier.py +0 -530
  146. package/uv.lock +0 -1085
@@ -1,726 +0,0 @@
1
- import json
2
- import shlex
3
- import shutil
4
- import subprocess
5
- import sys
6
- from pathlib import Path
7
-
8
- import typer
9
- import yaml
10
- from rich.console import Console
11
- from rich.table import Table
12
-
13
- app = typer.Typer(help="Set up DevCouncil integrations with coding CLIs.")
14
- setup_app = typer.Typer(help="Set up optional external companion integrations.")
15
- app.add_typer(setup_app, name="setup")
16
- console = Console()
17
-
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"
22
-
23
-
24
- def _project_root(path: Path | None) -> Path:
25
- return (path or Path(".")).expanduser().resolve()
26
-
27
-
28
- def _server_args(project_root: Path) -> list[str]:
29
- return ["devcouncil", "mcp-server"]
30
-
31
-
32
- def _codex_command(project_root: Path) -> list[str]:
33
- return [
34
- "codex",
35
- "mcp",
36
- "add",
37
- "devcouncil",
38
- "--env",
39
- f"DEVCOUNCIL_PROJECT_ROOT={project_root}",
40
- "--",
41
- *_server_args(project_root),
42
- ]
43
-
44
-
45
- def _gemini_command(project_root: Path, scope: str) -> list[str]:
46
- return [
47
- "gemini",
48
- "mcp",
49
- "add",
50
- "--scope",
51
- scope,
52
- "--env",
53
- f"DEVCOUNCIL_PROJECT_ROOT={project_root}",
54
- "devcouncil",
55
- *_server_args(project_root),
56
- ]
57
-
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
-
84
- def _format_command(command: list[str]) -> str:
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
- ])
109
-
110
-
111
- def _run(command: list[str]) -> int:
112
- executable = shutil.which(command[0])
113
- if not executable:
114
- return 127
115
- resolved = [executable, *command[1:]]
116
- use_shell = sys.platform == "win32" and Path(executable).suffix.lower() in {".bat", ".cmd", ".ps1"}
117
- invocation = subprocess.list2cmdline(resolved) if use_shell else resolved
118
- result = subprocess.run(invocation, text=True, shell=use_shell)
119
- return result.returncode
120
-
121
-
122
- def _run_capture(command: list[str], timeout: int = 10) -> tuple[int, str]:
123
- executable = shutil.which(command[0])
124
- if not executable:
125
- return 127, f"{command[0]} not found on PATH"
126
-
127
- resolved = [executable, *command[1:]]
128
- use_shell = sys.platform == "win32" and Path(executable).suffix.lower() in {".bat", ".cmd", ".ps1"}
129
- invocation = subprocess.list2cmdline(resolved) if use_shell else resolved
130
- try:
131
- result = subprocess.run(
132
- invocation,
133
- capture_output=True,
134
- text=True,
135
- encoding="utf-8",
136
- errors="replace",
137
- shell=use_shell,
138
- timeout=timeout,
139
- )
140
- except subprocess.TimeoutExpired:
141
- return 124, "timed out"
142
- return result.returncode, (result.stdout + result.stderr).strip()
143
-
144
-
145
- def _config_path(project_root: Path) -> Path:
146
- return project_root / ".devcouncil" / "config.yaml"
147
-
148
-
149
- def _load_raw_config(project_root: Path) -> dict:
150
- path = _config_path(project_root)
151
- if not path.exists():
152
- return {}
153
- return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
154
-
155
-
156
- def _save_raw_config(project_root: Path, config: dict) -> None:
157
- path = _config_path(project_root)
158
- path.parent.mkdir(parents=True, exist_ok=True)
159
- path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8")
160
-
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
-
337
- def _print_command(tool: str, command: list[str], apply: bool):
338
- if apply:
339
- console.print(f"[cyan]Configuring {tool} MCP integration...[/cyan]")
340
- else:
341
- console.print(f"[bold]{tool}[/bold]")
342
- console.print(_format_command(command), soft_wrap=True)
343
-
344
-
345
- def _configure(tool: str, command: list[str], apply: bool) -> bool:
346
- executable = command[0]
347
- if not shutil.which(executable):
348
- console.print(f"[yellow]{tool} CLI not found on PATH. Install it first, then rerun this command.[/yellow]")
349
- console.print(_format_command(command), soft_wrap=True)
350
- return False
351
-
352
- _print_command(tool, command, apply)
353
- if not apply:
354
- return True
355
-
356
- code = _run(command)
357
- if code == 0:
358
- console.print(f"[green]{tool} integration configured.[/green]")
359
- return True
360
-
361
- console.print(f"[red]{tool} integration command failed with exit code {code}.[/red]")
362
- console.print("You can rerun it manually:")
363
- console.print(_format_command(command), soft_wrap=True)
364
- return False
365
-
366
-
367
- @app.callback(invoke_without_command=True)
368
- def overview(ctx: typer.Context):
369
- """
370
- Show integration options for supported coding CLIs.
371
- """
372
- if ctx.invoked_subcommand is not None:
373
- return
374
-
375
- table = Table(title="DevCouncil Coding CLI Integrations")
376
- table.add_column("Tool", style="cyan")
377
- table.add_column("Setup command", style="green")
378
- table.add_column("Notes")
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.")
385
- console.print(table)
386
- console.print(f"\nIf your install exposes only the setup flow, use: {LEGACY_COMMAND} --apply")
387
- console.print("\nRun without [bold]--apply[/bold] to preview the exact commands first.")
388
-
389
-
390
- @app.command("doctor")
391
- def integrations_doctor():
392
- """Check optional integration tools and local client wiring prerequisites."""
393
- table = Table(title="DevCouncil Integration Doctor")
394
- table.add_column("Integration", style="cyan")
395
- table.add_column("Status")
396
- table.add_column("Notes")
397
-
398
- checks = [
399
- ("Agent Flow", "agent-flow-app", "Optional live/replay visualizer for trace JSONL."),
400
- ("code-review-graph", "code-review-graph", "Optional structural graph context adapter."),
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."),
406
- ]
407
- for label, executable, notes in checks:
408
- found = shutil.which(executable)
409
- table.add_row(label, "[green]OK[/green]" if found else "[yellow]Missing[/yellow]", found or notes)
410
-
411
- config = _config_path(Path("."))
412
- table.add_row(
413
- "DevCouncil config",
414
- "[green]OK[/green]" if config.exists() else "[red]Missing[/red]",
415
- str(config) if config.exists() else "Run dev init first.",
416
- )
417
- console.print(table)
418
-
419
-
420
- @app.command("codex")
421
- def codex(
422
- apply: bool = typer.Option(False, "--apply", help="Run the setup command instead of printing it."),
423
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
424
- ):
425
- """
426
- Set up DevCouncil MCP tools for Codex CLI.
427
- """
428
- root = _project_root(project_root)
429
- command = _codex_command(root)
430
- ok = _configure("Codex CLI", command, apply)
431
- if not ok and apply:
432
- raise typer.Exit(code=1)
433
-
434
-
435
- @app.command("gemini")
436
- def gemini(
437
- apply: bool = typer.Option(False, "--apply", help="Run the setup command instead of printing it."),
438
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
439
- scope: str = typer.Option("project", "--scope", help="Gemini MCP config scope: project or user."),
440
- ):
441
- """
442
- Set up DevCouncil MCP tools for Gemini CLI.
443
- """
444
- if scope not in {"project", "user"}:
445
- console.print("[red]--scope must be 'project' or 'user'.[/red]")
446
- raise typer.Exit(code=2)
447
-
448
- root = _project_root(project_root)
449
- command = _gemini_command(root, scope)
450
- ok = _configure("Gemini CLI", command, apply)
451
- if not ok and apply:
452
- raise typer.Exit(code=1)
453
-
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
-
490
- @app.command("all")
491
- def all_tools(
492
- apply: bool = typer.Option(False, "--apply", help="Run setup commands instead of printing them."),
493
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
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."),
497
- ):
498
- """
499
- Set up DevCouncil MCP tools and native hooks for every supported coding CLI found on PATH.
500
- """
501
- if gemini_scope not in {"project", "user"}:
502
- console.print("[red]--gemini-scope must be 'project' or 'user'.[/red]")
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)
507
-
508
- root = _project_root(project_root)
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)),
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)
523
- if apply and not all(results):
524
- raise typer.Exit(code=1)
525
-
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
-
540
- @app.command("check")
541
- def check(
542
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
543
- ):
544
- """
545
- Check whether DevCouncil is ready to integrate with coding CLIs.
546
- """
547
- root = _project_root(project_root)
548
- table = Table(title="DevCouncil Integration Check")
549
- table.add_column("Check", style="cyan")
550
- table.add_column("Status", style="magenta")
551
- table.add_column("Details")
552
-
553
- failures = 0
554
-
555
- def add(ok: bool, name: str, details: str):
556
- nonlocal failures
557
- table.add_row(name, "[green]OK[/green]" if ok else "[red]FAIL[/red]", details)
558
- if not ok:
559
- failures += 1
560
-
561
- add((root / ".devcouncil").exists(), "Project state", str(root / ".devcouncil"))
562
-
563
- devcouncil_path = shutil.which("devcouncil")
564
- add(devcouncil_path is not None, "devcouncil CLI", devcouncil_path or "Install DevCouncil first.")
565
-
566
- code, output = _run_capture(["devcouncil", "--help"])
567
- add(code == 0, "devcouncil command", output.splitlines()[0] if output else "No output")
568
-
569
- code, output = _run_capture(["codex", "--version"])
570
- add(code == 0, "Codex CLI", output.splitlines()[0] if output else "Optional; install Codex to use this integration.")
571
-
572
- code, output = _run_capture(["gemini", "--version"])
573
- add(code == 0, "Gemini CLI", output.splitlines()[0] if output else "Optional; install Gemini CLI to use this integration.")
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
-
581
- try:
582
- from mcp import ClientSession, StdioServerParameters
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,
595
- )
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
- raise typer.Exit(code=1)
617
-
618
- console.print(f"\n[green]Ready.[/green] Run: {PREFERRED_COMMAND} all --apply (or {LEGACY_COMMAND} --apply).")
619
-
620
-
621
- @setup_app.command("agent-flow")
622
- def setup_agent_flow(
623
- apply: bool = typer.Option(False, "--apply", help="Write DevCouncil config instead of previewing."),
624
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
625
- ):
626
- """Configure DevCouncil trace output for Agent Flow-style JSONL replay."""
627
- root = _project_root(project_root)
628
- trace_path = root / ".devcouncil" / "logs" / "traces.jsonl"
629
- console.print("[bold]Agent Flow setup[/bold]")
630
- console.print(f"Trace JSONL: {trace_path}")
631
- console.print("Replay/tail locally with: dev trace tail --follow")
632
- console.print("External visualizers can watch the trace JSONL path above.")
633
-
634
- if not apply:
635
- console.print("[yellow]Preview only. Rerun with --apply to record this integration in config.[/yellow]")
636
- return
637
-
638
- config = _load_raw_config(root)
639
- integrations = config.setdefault("integrations", {})
640
- integrations["agent_flow"] = {
641
- "enabled": True,
642
- "trace_path": str(trace_path),
643
- "mode": "jsonl",
644
- }
645
- _save_raw_config(root, config)
646
- docs_dir = root / ".devcouncil" / "integrations"
647
- docs_dir.mkdir(parents=True, exist_ok=True)
648
- (docs_dir / "agent-flow.md").write_text(
649
- "\n".join([
650
- "# Agent Flow",
651
- "",
652
- f"DevCouncil writes trace events to `{trace_path}`.",
653
- "",
654
- "Local replay:",
655
- "",
656
- "```bash",
657
- "dev trace tail --follow",
658
- "```",
659
- "",
660
- "External visualizers can watch the JSONL file directly. DevCouncil does not modify global editor or Claude Code settings from this setup command.",
661
- "",
662
- ]),
663
- encoding="utf-8",
664
- )
665
- console.print("[green]Agent Flow trace integration recorded in .devcouncil/config.yaml.[/green]")
666
-
667
-
668
- @setup_app.command("code-review-graph")
669
- def setup_code_review_graph(
670
- apply: bool = typer.Option(False, "--apply", help="Write DevCouncil config and ignore file."),
671
- project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
672
- ):
673
- """Configure optional code-review-graph context enrichment."""
674
- root = _project_root(project_root)
675
- executable = shutil.which("code-review-graph")
676
- ignore_path = root / ".code-review-graphignore"
677
- console.print("[bold]code-review-graph setup[/bold]")
678
- console.print(f"Binary: {executable or 'not found on PATH'}")
679
- console.print("Install separately with: pipx install code-review-graph")
680
- console.print("Build graph separately with: code-review-graph build")
681
-
682
- if not apply:
683
- console.print("[yellow]Preview only. Rerun with --apply to record this integration.[/yellow]")
684
- return
685
-
686
- if not ignore_path.exists():
687
- ignore_path.write_text(
688
- "\n".join([
689
- ".devcouncil/**",
690
- ".git/**",
691
- ".venv/**",
692
- "dist/**",
693
- "node_modules/**",
694
- "",
695
- ]),
696
- encoding="utf-8",
697
- )
698
- console.print(f"[green]Created {ignore_path}.[/green]")
699
-
700
- config = _load_raw_config(root)
701
- integrations = config.setdefault("integrations", {})
702
- integrations["code_review_graph"] = {
703
- "enabled": True,
704
- "command": "code-review-graph",
705
- "optional": True,
706
- }
707
- _save_raw_config(root, config)
708
- docs_dir = root / ".devcouncil" / "integrations"
709
- docs_dir.mkdir(parents=True, exist_ok=True)
710
- (docs_dir / "code-review-graph.md").write_text(
711
- "\n".join([
712
- "# code-review-graph",
713
- "",
714
- "Install and build the graph outside DevCouncil:",
715
- "",
716
- "```bash",
717
- "pipx install code-review-graph",
718
- "code-review-graph build",
719
- "```",
720
- "",
721
- "DevCouncil uses this as an optional context adapter for mapping, prompts, verification traces, and MCP graph context.",
722
- "",
723
- ]),
724
- encoding="utf-8",
725
- )
726
- console.print("[green]code-review-graph adapter recorded in .devcouncil/config.yaml.[/green]")