devcouncil 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/README.md +191 -253
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +6 -17
  4. package/pyproject.toml +0 -66
  5. package/src/devcouncil/__init__.py +0 -0
  6. package/src/devcouncil/__main__.py +0 -4
  7. package/src/devcouncil/app/__init__.py +0 -28
  8. package/src/devcouncil/app/config.py +0 -320
  9. package/src/devcouncil/app/errors.py +0 -23
  10. package/src/devcouncil/app/events.py +0 -44
  11. package/src/devcouncil/app/orchestrator.py +0 -92
  12. package/src/devcouncil/app/project_status.py +0 -29
  13. package/src/devcouncil/app/run_context.py +0 -39
  14. package/src/devcouncil/app/state_machine.py +0 -108
  15. package/src/devcouncil/artifacts/__init__.py +0 -1
  16. package/src/devcouncil/artifacts/coverage.py +0 -96
  17. package/src/devcouncil/artifacts/graph.py +0 -163
  18. package/src/devcouncil/artifacts/migrations.py +0 -20
  19. package/src/devcouncil/artifacts/schemas.py +0 -23
  20. package/src/devcouncil/artifacts/serializer.py +0 -21
  21. package/src/devcouncil/artifacts/validators.py +0 -27
  22. package/src/devcouncil/assets/__init__.py +0 -1
  23. package/src/devcouncil/assets/devcouncil-logo.svg +0 -60
  24. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  25. package/src/devcouncil/cli/__init__.py +0 -0
  26. package/src/devcouncil/cli/commands/__init__.py +0 -0
  27. package/src/devcouncil/cli/commands/agents.py +0 -292
  28. package/src/devcouncil/cli/commands/artifacts.py +0 -54
  29. package/src/devcouncil/cli/commands/ast.py +0 -22
  30. package/src/devcouncil/cli/commands/baseline.py +0 -35
  31. package/src/devcouncil/cli/commands/check.py +0 -209
  32. package/src/devcouncil/cli/commands/config.py +0 -115
  33. package/src/devcouncil/cli/commands/cost.py +0 -57
  34. package/src/devcouncil/cli/commands/dashboard.py +0 -31
  35. package/src/devcouncil/cli/commands/doctor.py +0 -340
  36. package/src/devcouncil/cli/commands/evidence.py +0 -48
  37. package/src/devcouncil/cli/commands/go.py +0 -656
  38. package/src/devcouncil/cli/commands/handoff.py +0 -69
  39. package/src/devcouncil/cli/commands/hook.py +0 -237
  40. package/src/devcouncil/cli/commands/init.py +0 -289
  41. package/src/devcouncil/cli/commands/integrate.py +0 -1515
  42. package/src/devcouncil/cli/commands/lsp.py +0 -20
  43. package/src/devcouncil/cli/commands/map.py +0 -112
  44. package/src/devcouncil/cli/commands/mcp_server.py +0 -18
  45. package/src/devcouncil/cli/commands/plan.py +0 -488
  46. package/src/devcouncil/cli/commands/prompt.py +0 -61
  47. package/src/devcouncil/cli/commands/repair.py +0 -89
  48. package/src/devcouncil/cli/commands/report.py +0 -137
  49. package/src/devcouncil/cli/commands/reset_demo_state.py +0 -33
  50. package/src/devcouncil/cli/commands/rollback.py +0 -59
  51. package/src/devcouncil/cli/commands/run.py +0 -289
  52. package/src/devcouncil/cli/commands/runs.py +0 -223
  53. package/src/devcouncil/cli/commands/scaffold.py +0 -32
  54. package/src/devcouncil/cli/commands/semantic.py +0 -47
  55. package/src/devcouncil/cli/commands/setup.py +0 -362
  56. package/src/devcouncil/cli/commands/shell.py +0 -73
  57. package/src/devcouncil/cli/commands/show.py +0 -76
  58. package/src/devcouncil/cli/commands/skills.py +0 -88
  59. package/src/devcouncil/cli/commands/status.py +0 -141
  60. package/src/devcouncil/cli/commands/tasks.py +0 -55
  61. package/src/devcouncil/cli/commands/trace.py +0 -88
  62. package/src/devcouncil/cli/commands/verify.py +0 -328
  63. package/src/devcouncil/cli/commands/version.py +0 -20
  64. package/src/devcouncil/cli/commands/watch.py +0 -574
  65. package/src/devcouncil/cli/commands/watch_fs.py +0 -40
  66. package/src/devcouncil/cli/main.py +0 -137
  67. package/src/devcouncil/council/__init__.py +0 -0
  68. package/src/devcouncil/council/prompts/__init__.py +0 -0
  69. package/src/devcouncil/council/prompts/arbiter.md +0 -19
  70. package/src/devcouncil/council/prompts/critic_a.md +0 -10
  71. package/src/devcouncil/council/prompts/critic_b.md +0 -10
  72. package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
  73. package/src/devcouncil/council/prompts/planner_a.md +0 -16
  74. package/src/devcouncil/council/prompts/planner_b.md +0 -16
  75. package/src/devcouncil/council/prompts/rebuttal.md +0 -10
  76. package/src/devcouncil/council/prompts/spec_writer.md +0 -12
  77. package/src/devcouncil/domain/__init__.py +0 -0
  78. package/src/devcouncil/domain/assumption.py +0 -17
  79. package/src/devcouncil/domain/critique.py +0 -32
  80. package/src/devcouncil/domain/evidence.py +0 -47
  81. package/src/devcouncil/domain/gap.py +0 -52
  82. package/src/devcouncil/domain/requirement.py +0 -22
  83. package/src/devcouncil/domain/task.py +0 -55
  84. package/src/devcouncil/execution/__init__.py +0 -1
  85. package/src/devcouncil/execution/checkpoints.py +0 -246
  86. package/src/devcouncil/execution/context_builder.py +0 -60
  87. package/src/devcouncil/execution/executor.py +0 -15
  88. package/src/devcouncil/execution/fs_watcher.py +0 -180
  89. package/src/devcouncil/execution/handoff.py +0 -102
  90. package/src/devcouncil/execution/hook_policy.py +0 -253
  91. package/src/devcouncil/execution/patch.py +0 -77
  92. package/src/devcouncil/execution/paths.py +0 -14
  93. package/src/devcouncil/execution/permissions.py +0 -85
  94. package/src/devcouncil/execution/policy_engine.py +0 -343
  95. package/src/devcouncil/execution/prompt_builder.py +0 -671
  96. package/src/devcouncil/execution/shell_session.py +0 -225
  97. package/src/devcouncil/execution/task_runner.py +0 -170
  98. package/src/devcouncil/executors/__init__.py +0 -1
  99. package/src/devcouncil/executors/agent_registry.py +0 -575
  100. package/src/devcouncil/executors/coding_cli.py +0 -736
  101. package/src/devcouncil/executors/mini_swe.py +0 -73
  102. package/src/devcouncil/executors/native/__init__.py +0 -0
  103. package/src/devcouncil/executors/native/agent.py +0 -208
  104. package/src/devcouncil/executors/openhands.py +0 -71
  105. package/src/devcouncil/gating/__init__.py +0 -1
  106. package/src/devcouncil/gating/checks/__init__.py +0 -0
  107. package/src/devcouncil/gating/checks/clean_git.py +0 -52
  108. package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
  109. package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
  110. package/src/devcouncil/gating/checks/secret_scan_check.py +0 -53
  111. package/src/devcouncil/gating/policy.py +0 -338
  112. package/src/devcouncil/hardware.py +0 -184
  113. package/src/devcouncil/indexing/__init__.py +0 -1
  114. package/src/devcouncil/indexing/ast_matcher.py +0 -168
  115. package/src/devcouncil/indexing/graph_index.py +0 -48
  116. package/src/devcouncil/indexing/lsp.py +0 -161
  117. package/src/devcouncil/indexing/repo_mapper.py +0 -1455
  118. package/src/devcouncil/indexing/semantic_index.py +0 -205
  119. package/src/devcouncil/integrations/actions.py +0 -146
  120. package/src/devcouncil/integrations/check.py +0 -423
  121. package/src/devcouncil/integrations/code_review_graph.py +0 -163
  122. package/src/devcouncil/integrations/github.py +0 -39
  123. package/src/devcouncil/integrations/github_intent.py +0 -142
  124. package/src/devcouncil/integrations/gitnexus.py +0 -62
  125. package/src/devcouncil/integrations/graphify.py +0 -34
  126. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  127. package/src/devcouncil/integrations/mcp/server.py +0 -2122
  128. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +0 -24
  129. package/src/devcouncil/integrations/pr_comments.py +0 -62
  130. package/src/devcouncil/live/__init__.py +0 -2
  131. package/src/devcouncil/live/cards.py +0 -349
  132. package/src/devcouncil/live/models.py +0 -63
  133. package/src/devcouncil/live/repair_prompt.py +0 -83
  134. package/src/devcouncil/live/reviewer.py +0 -70
  135. package/src/devcouncil/live/signals.py +0 -135
  136. package/src/devcouncil/live/summary.py +0 -34
  137. package/src/devcouncil/live/tasks.py +0 -18
  138. package/src/devcouncil/live/transcripts.py +0 -141
  139. package/src/devcouncil/llm/__init__.py +0 -1
  140. package/src/devcouncil/llm/cache.py +0 -42
  141. package/src/devcouncil/llm/model_defaults.yaml +0 -44
  142. package/src/devcouncil/llm/provider.py +0 -627
  143. package/src/devcouncil/llm/router.py +0 -310
  144. package/src/devcouncil/optimization/__init__.py +0 -1
  145. package/src/devcouncil/optimization/gepa_agent.py +0 -318
  146. package/src/devcouncil/planning/__init__.py +0 -1
  147. package/src/devcouncil/planning/arbiter_service.py +0 -57
  148. package/src/devcouncil/planning/correction_manifest.py +0 -303
  149. package/src/devcouncil/planning/critique_service.py +0 -71
  150. package/src/devcouncil/planning/plan_service.py +0 -60
  151. package/src/devcouncil/planning/prompt_enhancer_service.py +0 -167
  152. package/src/devcouncil/planning/repair_service.py +0 -39
  153. package/src/devcouncil/planning/spec_service.py +0 -70
  154. package/src/devcouncil/repo/__init__.py +0 -0
  155. package/src/devcouncil/repo/ci_scaffold.py +0 -157
  156. package/src/devcouncil/repo/gitignore.py +0 -123
  157. package/src/devcouncil/repo/sca.py +0 -374
  158. package/src/devcouncil/reporting/__init__.py +0 -0
  159. package/src/devcouncil/reporting/github_check.py +0 -32
  160. package/src/devcouncil/reporting/json_report.py +0 -30
  161. package/src/devcouncil/reporting/markdown_report.py +0 -83
  162. package/src/devcouncil/reporting/report_builder.py +0 -14
  163. package/src/devcouncil/skills/__init__.py +0 -19
  164. package/src/devcouncil/skills/library/README.md +0 -46
  165. package/src/devcouncil/skills/library/ai-training.md +0 -50
  166. package/src/devcouncil/skills/library/android.md +0 -50
  167. package/src/devcouncil/skills/library/backend.md +0 -52
  168. package/src/devcouncil/skills/library/core-engineering.md +0 -95
  169. package/src/devcouncil/skills/library/data-engineering.md +0 -47
  170. package/src/devcouncil/skills/library/desktop.md +0 -46
  171. package/src/devcouncil/skills/library/devops.md +0 -48
  172. package/src/devcouncil/skills/library/game-dev.md +0 -46
  173. package/src/devcouncil/skills/library/ios.md +0 -48
  174. package/src/devcouncil/skills/library/mobile-cross-platform.md +0 -46
  175. package/src/devcouncil/skills/library/security.md +0 -48
  176. package/src/devcouncil/skills/library/systems.md +0 -48
  177. package/src/devcouncil/skills/library/web.md +0 -47
  178. package/src/devcouncil/skills/library/windows.md +0 -47
  179. package/src/devcouncil/skills/registry.py +0 -330
  180. package/src/devcouncil/storage/__init__.py +0 -0
  181. package/src/devcouncil/storage/db.py +0 -147
  182. package/src/devcouncil/storage/models.py +0 -204
  183. package/src/devcouncil/storage/native.py +0 -557
  184. package/src/devcouncil/storage/repositories.py +0 -485
  185. package/src/devcouncil/telemetry/__init__.py +0 -0
  186. package/src/devcouncil/telemetry/cost.py +0 -140
  187. package/src/devcouncil/telemetry/model_pricing.yaml +0 -48
  188. package/src/devcouncil/telemetry/pricing.py +0 -28
  189. package/src/devcouncil/telemetry/traces.py +0 -146
  190. package/src/devcouncil/telemetry/tracker.py +0 -52
  191. package/src/devcouncil/ui/__init__.py +0 -1
  192. package/src/devcouncil/ui/dashboard.py +0 -423
  193. package/src/devcouncil/utils/__init__.py +0 -1
  194. package/src/devcouncil/utils/redaction.py +0 -147
  195. package/src/devcouncil/utils/subprocess_env.py +0 -69
  196. package/src/devcouncil/verification/__init__.py +0 -1
  197. package/src/devcouncil/verification/acceptance_compiler.py +0 -125
  198. package/src/devcouncil/verification/ad_hoc_check.py +0 -129
  199. package/src/devcouncil/verification/diff_coverage.py +0 -353
  200. package/src/devcouncil/verification/implementation_reviewer.py +0 -55
  201. package/src/devcouncil/verification/next_actions.py +0 -189
  202. package/src/devcouncil/verification/sandbox.py +0 -178
  203. package/src/devcouncil/verification/test_resolver.py +0 -91
  204. package/src/devcouncil/verification/verifier.py +0 -1548
  205. package/uv.lock +0 -1226
@@ -1,423 +0,0 @@
1
- """Shared integration readiness checks for `dev integrate check`."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import shutil
7
- import subprocess
8
- import sys
9
- from dataclasses import dataclass
10
- from pathlib import Path
11
- from typing import Any
12
-
13
- from devcouncil.executors.agent_registry import (
14
- CODING_CLI_INTEGRATION_INFO,
15
- CODING_CLI_VERSION_COMMANDS,
16
- detect_available_coding_cli,
17
- resolve_automated_executor,
18
- resolve_coding_cli_executable,
19
- resolve_coding_cli_probe_order,
20
- )
21
- from devcouncil.utils.subprocess_env import clean_subprocess_env
22
-
23
-
24
- @dataclass(frozen=True)
25
- class IntegrationCheckRow:
26
- name: str
27
- status: str
28
- details: str
29
-
30
- def as_dict(self) -> dict[str, str]:
31
- return {"name": self.name, "status": self.status, "details": self.details}
32
-
33
-
34
- @dataclass(frozen=True)
35
- class IntegrationCheckReport:
36
- checks: tuple[IntegrationCheckRow, ...]
37
- recommended_executor: str | None
38
- failures: int
39
-
40
- @property
41
- def ok(self) -> bool:
42
- return self.failures == 0
43
-
44
- def as_dict(self) -> dict[str, Any]:
45
- return {
46
- "ok": self.ok,
47
- "failures": self.failures,
48
- "recommended_executor": self.recommended_executor,
49
- "checks": [row.as_dict() for row in self.checks],
50
- }
51
-
52
- def to_json(self, *, indent: int | None = 2) -> str:
53
- return json.dumps(self.as_dict(), indent=indent)
54
-
55
-
56
- CODING_CLI_CHECK_LABELS: dict[str, str] = {
57
- "codex": "Codex CLI",
58
- "gemini": "Gemini CLI",
59
- "claude": "Claude Code",
60
- "cursor": "Cursor",
61
- "opencode": "OpenCode",
62
- "antigravity": "Google Antigravity CLI",
63
- "warp": "Warp / Oz",
64
- "aider": "Aider",
65
- "copilot": "GitHub Copilot CLI",
66
- "goose": "Goose",
67
- "amp": "Amp (Sourcegraph)",
68
- "qwen": "Qwen Code",
69
- "crush": "Crush (Charm)",
70
- }
71
-
72
-
73
- def probe_cli_version(command: list[str], *, timeout: int = 10) -> tuple[int, str]:
74
- executable = shutil.which(command[0])
75
- if not executable:
76
- return 127, f"{command[0]} not found on PATH"
77
-
78
- resolved = [executable, *command[1:]]
79
- use_shell = sys.platform == "win32" and Path(executable).suffix.lower() in {".bat", ".cmd", ".ps1"}
80
- invocation = subprocess.list2cmdline(resolved) if use_shell else resolved
81
- try:
82
- result = subprocess.run(
83
- invocation,
84
- capture_output=True,
85
- text=True,
86
- encoding="utf-8",
87
- errors="replace",
88
- shell=use_shell,
89
- timeout=timeout,
90
- env=clean_subprocess_env(),
91
- )
92
- except subprocess.TimeoutExpired:
93
- return 124, "timed out"
94
- except (FileNotFoundError, OSError) as exc:
95
- return 127, f"{command[0]} could not be executed: {exc}"
96
- return result.returncode, (result.stdout + result.stderr).strip()
97
-
98
-
99
- def probe_coding_cli_version(client: str) -> tuple[bool, str]:
100
- label = CODING_CLI_CHECK_LABELS.get(client, client)
101
- commands = CODING_CLI_VERSION_COMMANDS.get(client, ())
102
- for command in commands:
103
- code, output = probe_cli_version(list(command))
104
- if code == 0:
105
- first_line = output.splitlines()[0] if output else "installed"
106
- return True, first_line
107
- return False, f"Optional; install {label} to use this integration."
108
-
109
-
110
- def recommended_executor_status(project_root: Path) -> tuple[bool, str]:
111
- detected = detect_available_coding_cli(project_root)
112
- if not detected:
113
- return False, "No built-in coding CLI on PATH. Run dev integrate recommend after installing one."
114
- resolved = resolve_automated_executor(project_root, None)
115
- return True, f"Use --executor {resolved} for dev go / dev run (detected: {detected})."
116
-
117
-
118
- def coding_clis_on_path(project_root: Path) -> list[str]:
119
- order = resolve_coding_cli_probe_order(project_root)
120
- return [client for client in order if resolve_coding_cli_executable(project_root, client)]
121
-
122
-
123
- def _load_json_file(path: Path) -> dict[str, Any]:
124
- if not path.exists():
125
- return {}
126
- try:
127
- loaded = json.loads(path.read_text(encoding="utf-8")) or {}
128
- except json.JSONDecodeError:
129
- return {}
130
- return loaded if isinstance(loaded, dict) else {}
131
-
132
-
133
- def _cursor_config_status(project_root: Path) -> tuple[str, bool, list[str]]:
134
- path = project_root / ".cursor" / "mcp.json"
135
- data = _load_json_file(path)
136
- server = ((data.get("mcpServers") or {}).get("devcouncil") or {}) if data else {}
137
- ok = (
138
- server.get("type") == "stdio"
139
- and server.get("command") == "devcouncil"
140
- and server.get("args") == ["mcp-server"]
141
- and (server.get("env") or {}).get("DEVCOUNCIL_PROJECT_ROOT") == str(project_root)
142
- )
143
- if ok:
144
- return "ok", False, [str(path)]
145
- return ("missing" if not path.exists() else "drifted"), True, [str(path)]
146
-
147
-
148
- def _opencode_config_status(project_root: Path) -> tuple[str, bool, list[str]]:
149
- path = project_root / "opencode.json"
150
- data = _load_json_file(path)
151
- server = ((data.get("mcp") or {}).get("devcouncil") or {}) if data else {}
152
- plugin = data.get("plugin") or []
153
- plugin_ok = "./.devcouncil/integrations/opencode_devcouncil_plugin.mjs" in plugin
154
- ok = (
155
- server.get("type") == "local"
156
- and server.get("command") == ["devcouncil", "mcp-server"]
157
- and (server.get("environment") or {}).get("DEVCOUNCIL_PROJECT_ROOT") == str(project_root)
158
- )
159
- status = "ok" if ok else ("missing" if not path.exists() else "drifted")
160
- return status, not ok or not plugin_ok, [str(path), str(project_root / ".devcouncil" / "integrations" / "opencode_devcouncil_plugin.mjs")]
161
-
162
-
163
- def _antigravity_config_status(project_root: Path) -> tuple[str, bool, list[str]]:
164
- path = project_root / ".agents" / "mcp_config.json"
165
- data = _load_json_file(path)
166
- server = ((data.get("mcpServers") or {}).get("devcouncil") or {}) if data else {}
167
- ok = (
168
- server.get("command") == "devcouncil"
169
- and server.get("args") == ["mcp-server"]
170
- and server.get("cwd") == str(project_root)
171
- and (server.get("env") or {}).get("DEVCOUNCIL_PROJECT_ROOT") == str(project_root)
172
- )
173
- if ok:
174
- return "ok", False, [str(path)]
175
- return ("missing" if not path.exists() else "drifted"), True, [str(path)]
176
-
177
-
178
- def _warp_config_status(project_root: Path) -> tuple[str, bool, list[str]]:
179
- path = project_root / ".devcouncil" / "integrations" / "warp-mcp.json"
180
- data = _load_json_file(path)
181
- server = data.get("devcouncil") or {}
182
- ok = (
183
- "mcpServers" not in data
184
- and server.get("command") == "devcouncil"
185
- and server.get("args") == ["mcp-server"]
186
- and (server.get("env") or {}).get("DEVCOUNCIL_PROJECT_ROOT") == str(project_root)
187
- )
188
- if ok:
189
- return "ok", False, [str(path)]
190
- return ("missing" if not path.exists() else "drifted"), True, [str(path)]
191
-
192
-
193
- def integration_capability_rows(project_root: Path) -> list[dict[str, object]]:
194
- order = resolve_coding_cli_probe_order(project_root)
195
- rows: list[dict[str, object]] = []
196
- for client in order:
197
- info = CODING_CLI_INTEGRATION_INFO.get(client)
198
- if info is None:
199
- continue
200
- config_status = "not_applicable"
201
- fixable = bool(info.mcp or info.hooks or info.launcher_shim)
202
- paths: list[str] = []
203
- if client == "cursor":
204
- config_status, fixable, paths = _cursor_config_status(project_root)
205
- elif client == "opencode":
206
- config_status, fixable, paths = _opencode_config_status(project_root)
207
- elif client == "antigravity":
208
- config_status, fixable, paths = _antigravity_config_status(project_root)
209
- elif client == "warp":
210
- config_status, fixable, paths = _warp_config_status(project_root)
211
-
212
- rows.append({
213
- "name": info.name,
214
- "label": info.label,
215
- "on_path": resolve_coding_cli_executable(project_root, client) is not None,
216
- "tier": info.tier,
217
- "headless": info.headless,
218
- "mcp": info.mcp,
219
- "hooks": info.hooks,
220
- "enforcement": info.enforcement,
221
- "launcher_shim": info.launcher_shim,
222
- "notes": info.notes,
223
- "configured": config_status == "ok",
224
- "config_status": config_status,
225
- "fixable": fixable,
226
- "paths": paths,
227
- "apply_target": client,
228
- })
229
- return rows
230
-
231
-
232
- def _hook_config_references_devcouncil(path: Path) -> bool | None:
233
- """Return whether a client hook config still wires DevCouncil's gate.
234
-
235
- ``True`` -> the file exists and references ``devcouncil`` somewhere in its hooks.
236
- ``False`` -> the file exists but no longer references it (tampered/disarmed).
237
- ``None`` -> the file does not exist (client was never integrated here).
238
-
239
- Reads the raw text rather than parsing each client's bespoke schema so it works
240
- uniformly across JSON hook files and is resilient to format drift; the goal is a
241
- tamper tripwire, not full schema validation."""
242
- if not path.exists():
243
- return None
244
- try:
245
- text = path.read_text(encoding="utf-8")
246
- except OSError:
247
- return False
248
- return "devcouncil" in text.lower()
249
-
250
-
251
- def _hook_config_tamper_targets(project_root: Path) -> list[tuple[str, Path]]:
252
- return [
253
- ("Claude", project_root / ".claude" / "settings.local.json"),
254
- ("Codex", project_root / ".codex" / "hooks.json"),
255
- ("Gemini", project_root / ".gemini" / "settings.json"),
256
- ("Cursor", project_root / ".cursor" / "hooks.json"),
257
- ]
258
-
259
-
260
- def build_integration_check_report(project_root: Path, *, strict: bool = False) -> IntegrationCheckReport:
261
- from devcouncil.cli.commands import integrate
262
-
263
- rows: list[IntegrationCheckRow] = []
264
- failures = 0
265
-
266
- def add(ok: bool, name: str, details: str) -> None:
267
- nonlocal failures
268
- rows.append(IntegrationCheckRow(name=name, status="ok" if ok else "fail", details=details))
269
- if not ok:
270
- failures += 1
271
-
272
- def add_optional(ok: bool, name: str, details: str) -> None:
273
- if strict and not ok:
274
- add(False, name, details)
275
- return
276
- rows.append(IntegrationCheckRow(name=name, status="ok" if ok else "missing", details=details))
277
-
278
- def add_skip(name: str, details: str) -> None:
279
- rows.append(IntegrationCheckRow(name=name, status="skip", details=details))
280
-
281
- root = project_root.expanduser().resolve()
282
- add((root / ".devcouncil").exists(), "Project state", str(root / ".devcouncil"))
283
- devcouncil_path = shutil.which("devcouncil")
284
- add(
285
- devcouncil_path is not None or Path(sys.executable).exists(),
286
- "devcouncil CLI",
287
- devcouncil_path or f"{sys.executable} -m devcouncil",
288
- )
289
-
290
- devcouncil_launch = [devcouncil_path] if devcouncil_path else [sys.executable, "-m", "devcouncil"]
291
- code, output = integrate._run_capture([*devcouncil_launch, "--help"])
292
- add(code == 0, "devcouncil command", output.splitlines()[0] if output else "No output")
293
-
294
- for client in resolve_coding_cli_probe_order(root):
295
- cli_ok, cli_details = probe_coding_cli_version(client)
296
- add_optional(cli_ok, CODING_CLI_CHECK_LABELS.get(client, client), cli_details)
297
-
298
- rec_ok, rec_details = recommended_executor_status(root)
299
- add_optional(rec_ok, "Recommended coding CLI", rec_details)
300
-
301
- for row in integration_capability_rows(root):
302
- name = str(row["name"])
303
- status = str(row["config_status"])
304
- if status == "ok":
305
- paths = row.get("paths")
306
- path_list = paths if isinstance(paths, list) else []
307
- add(True, f"{row['label']} config", ", ".join(str(path) for path in path_list))
308
- elif status in {"missing", "drifted"}:
309
- add_skip(f"{row['label']} config", f"Run dev integrate {name} --apply to repair.")
310
-
311
- raw_config = integrate._load_raw_config(root) if (root / ".devcouncil").exists() else {}
312
-
313
- cursor_hooks = root / ".cursor" / "hooks.json"
314
- cursor_enabled = bool(raw_config.get("integrations", {}).get("cursor", {}).get("enabled"))
315
- if cursor_enabled or cursor_hooks.exists():
316
- hooks_ok = False
317
- if cursor_hooks.exists():
318
- try:
319
- hooks_data = json.loads(cursor_hooks.read_text(encoding="utf-8")) or {}
320
- hooks_ok = "preToolUse" in hooks_data.get("hooks", {})
321
- except json.JSONDecodeError:
322
- hooks_ok = False
323
- add(
324
- hooks_ok,
325
- "Cursor hooks",
326
- str(cursor_hooks) if hooks_ok else "Run dev integrate hooks --apply --tool cursor.",
327
- )
328
-
329
- opencode_config = integrate._opencode_config_path(root)
330
- opencode_enabled = bool(raw_config.get("integrations", {}).get("opencode", {}).get("enabled"))
331
- opencode_plugin = integrate._opencode_plugin_path(root)
332
- if opencode_enabled or opencode_plugin.exists():
333
- plugin_ok = opencode_plugin.exists()
334
- plugin_registered = False
335
- if plugin_ok and opencode_config.exists():
336
- try:
337
- opencode_data = json.loads(opencode_config.read_text(encoding="utf-8")) or {}
338
- plugin_registered = (
339
- f"./.devcouncil/integrations/{integrate.OPENCODE_HOOK_PLUGIN_NAME}"
340
- in (opencode_data.get("plugin") or [])
341
- )
342
- except json.JSONDecodeError:
343
- plugin_registered = False
344
- add(
345
- plugin_ok and plugin_registered,
346
- "OpenCode hook plugin",
347
- str(opencode_plugin) if plugin_ok and plugin_registered else "Run dev integrate hooks --apply --tool opencode.",
348
- )
349
-
350
- bundled_plugin = integrate._opencode_plugin_source()
351
- add(
352
- bundled_plugin.exists(),
353
- "Bundled OpenCode hook plugin",
354
- str(bundled_plugin) if bundled_plugin.exists() else "Reinstall DevCouncil; package asset is missing.",
355
- )
356
-
357
- custom_agents = raw_config.get("integrations", {}).get("cli_agents", {}).get("agents", {})
358
- if custom_agents:
359
- for name, agent in sorted(custom_agents.items()):
360
- command = str(agent.get("command", "")).strip()
361
- found = shutil.which(command) if command else None
362
- add(found is not None, f"CLI agent: {name}", found or f"{command or 'command'} not found on PATH")
363
- else:
364
- add_skip("Custom CLI agents", "No agents registered.")
365
-
366
- try:
367
- tools = integrate._probe_mcp_tools(root)
368
- expected = {"devcouncil_status", "devcouncil_report", "devcouncil_get_task"}
369
- add(expected.issubset(set(tools)), "MCP server", ", ".join(tools))
370
- except Exception as exc:
371
- add(False, "MCP server", str(exc))
372
-
373
- # Tamper tripwire: any installed client hook config must still reference the
374
- # DevCouncil gate. A present-but-unreferenced file means the pre-action gate was
375
- # disarmed (by an agent or by hand) and is reported as a failure.
376
- for label, hook_path in _hook_config_tamper_targets(root):
377
- references = _hook_config_references_devcouncil(hook_path)
378
- if references is None:
379
- continue
380
- add(
381
- references,
382
- f"{label} hook integrity",
383
- str(hook_path) if references else f"{hook_path} no longer references devcouncil (tampered/disarmed).",
384
- )
385
-
386
- detected = detect_available_coding_cli(root)
387
- recommended = resolve_automated_executor(root, None) if detected else None
388
- return IntegrationCheckReport(tuple(rows), recommended, failures)
389
-
390
-
391
- def integration_status_summary(project_root: Path) -> dict[str, Any]:
392
- from devcouncil.app.config import load_config
393
-
394
- probe_order = resolve_coding_cli_probe_order(project_root)
395
- on_path = coding_clis_on_path(project_root)
396
- detected = on_path[0] if on_path else None
397
- try:
398
- execution = load_config(project_root).execution
399
- default_executor = execution.default_executor
400
- stream_cli_output = execution.stream_cli_output
401
- cursor_resume_mode = execution.cursor_resume_mode
402
- custom_probe_order = list(execution.coding_cli_probe_order)
403
- except Exception:
404
- default_executor = "manual"
405
- stream_cli_output = False
406
- cursor_resume_mode = "off"
407
- custom_probe_order = []
408
-
409
- resolved = resolve_automated_executor(project_root, None) if detected or default_executor != "manual" else "manual"
410
- config_path = project_root / ".devcouncil" / "config.yaml"
411
- return {
412
- "project_initialized": (project_root / ".devcouncil").is_dir(),
413
- "config_path": str(config_path) if config_path.exists() else None,
414
- "default_executor": default_executor,
415
- "resolved_executor": resolved,
416
- "detected_executor": detected,
417
- "coding_clis_on_path": on_path,
418
- "probe_order": list(probe_order),
419
- "custom_probe_order": custom_probe_order,
420
- "stream_cli_output": stream_cli_output,
421
- "cursor_resume_mode": cursor_resume_mode,
422
- "capabilities": integration_capability_rows(project_root),
423
- }
@@ -1,163 +0,0 @@
1
- import json
2
- import shutil
3
- import subprocess
4
- from pathlib import Path
5
- from typing import Any, Iterable
6
-
7
- from pydantic import BaseModel, Field
8
-
9
- from devcouncil.app.config import load_config
10
-
11
-
12
- class CodeReviewGraphContext(BaseModel):
13
- available: bool
14
- summary: str
15
- command: str = "code-review-graph"
16
- changed_files: list[str] = Field(default_factory=list)
17
- impacted_files: list[str] = Field(default_factory=list)
18
- related_tests: list[str] = Field(default_factory=list)
19
- raw: str = ""
20
-
21
-
22
- class CodeReviewGraphAdapter:
23
- """Optional shell adapter for code-review-graph without a hard dependency."""
24
-
25
- def __init__(self, project_root: Path, command: str | None = None):
26
- self.project_root = project_root
27
- self.command = command or self._configured_command()
28
-
29
- def is_enabled(self) -> bool:
30
- try:
31
- return load_config(self.project_root).integrations.code_review_graph.enabled
32
- except Exception:
33
- return False
34
-
35
- def is_available(self) -> bool:
36
- return shutil.which(self.command) is not None
37
-
38
- def get_context(self, files: Iterable[str] = ()) -> CodeReviewGraphContext:
39
- changed_files = [file for file in files if file]
40
- if not self.is_enabled():
41
- return CodeReviewGraphContext(
42
- available=False,
43
- summary="code-review-graph integration is disabled.",
44
- command=self.command,
45
- changed_files=changed_files,
46
- )
47
- if not self.is_available():
48
- return CodeReviewGraphContext(
49
- available=False,
50
- summary="code-review-graph command was not found on PATH.",
51
- command=self.command,
52
- changed_files=changed_files,
53
- )
54
-
55
- commands = self._candidate_commands(changed_files)
56
- errors: list[str] = []
57
- for command in commands:
58
- result = self._run(command)
59
- if result.returncode == 0 and result.output.strip():
60
- return self._parse_context(result.output, changed_files)
61
- errors.append(result.output.strip() or f"exit {result.returncode}")
62
-
63
- return CodeReviewGraphContext(
64
- available=True,
65
- summary="code-review-graph ran but did not return context.",
66
- command=self.command,
67
- changed_files=changed_files,
68
- raw="\n".join(errors),
69
- )
70
-
71
- def prompt_section(self, files: Iterable[str] = ()) -> str:
72
- context = self.get_context(files)
73
- if not context.available:
74
- return ""
75
-
76
- lines = ["## Structural graph context", context.summary]
77
- if context.impacted_files:
78
- lines.append("\nImpacted files:")
79
- lines.extend(f"- `{path}`" for path in context.impacted_files[:20])
80
- if context.related_tests:
81
- lines.append("\nRelated tests:")
82
- lines.extend(f"- `{path}`" for path in context.related_tests[:20])
83
- return "\n".join(lines).strip() + "\n"
84
-
85
- def _configured_command(self) -> str:
86
- try:
87
- return load_config(self.project_root).integrations.code_review_graph.command
88
- except Exception:
89
- return "code-review-graph"
90
-
91
- def _candidate_commands(self, files: list[str]) -> list[list[str]]:
92
- commands = [[self.command, "detect-changes", "--json"]]
93
- if files:
94
- commands.append([self.command, "get-review-context", "--json", *files])
95
- commands.append([self.command, "status", "--json"])
96
- return commands
97
-
98
- def _parse_context(self, output: str, changed_files: list[str]) -> CodeReviewGraphContext:
99
- data: dict[str, Any] = {}
100
- try:
101
- parsed = json.loads(output)
102
- if isinstance(parsed, dict):
103
- data = parsed
104
- except json.JSONDecodeError:
105
- pass
106
-
107
- impacted = self._string_list(
108
- data.get("impacted_files")
109
- or data.get("impact_radius")
110
- or data.get("files")
111
- or []
112
- )
113
- tests = self._string_list(data.get("related_tests") or data.get("tests") or [])
114
- summary = str(data.get("summary") or "code-review-graph context available.")
115
- return CodeReviewGraphContext(
116
- available=True,
117
- summary=summary,
118
- command=self.command,
119
- changed_files=changed_files,
120
- impacted_files=impacted,
121
- related_tests=tests,
122
- raw=output,
123
- )
124
-
125
- def _string_list(self, value: Any) -> list[str]:
126
- if isinstance(value, list):
127
- results: list[str] = []
128
- for item in value:
129
- if isinstance(item, (str, int)):
130
- results.append(str(item))
131
- elif isinstance(item, dict):
132
- path = item.get("path") or item.get("file") or item.get("name") or item.get("id")
133
- if path:
134
- results.append(str(path))
135
- return results
136
- if isinstance(value, dict):
137
- if "path" in value or "file" in value:
138
- return [str(value.get("path") or value.get("file"))]
139
- return [str(key) for key in value.keys()]
140
- return []
141
-
142
- def _run(self, command: list[str]) -> "_RunResult":
143
- try:
144
- result = subprocess.run(
145
- command,
146
- cwd=self.project_root,
147
- capture_output=True,
148
- text=True,
149
- encoding="utf-8",
150
- errors="replace",
151
- timeout=20,
152
- )
153
- return _RunResult(
154
- returncode=result.returncode,
155
- output=(result.stdout or "") + (result.stderr or ""),
156
- )
157
- except Exception as exc:
158
- return _RunResult(returncode=1, output=str(exc))
159
-
160
-
161
- class _RunResult(BaseModel):
162
- returncode: int
163
- output: str
@@ -1,39 +0,0 @@
1
- import httpx
2
- import logging
3
- from devcouncil.artifacts.graph import ArtifactGraph
4
- from devcouncil.reporting.github_check import GitHubCheckGenerator
5
-
6
- logger = logging.getLogger(__name__)
7
-
8
- class GitHubIntegration:
9
- """Manages interactions with GitHub API, specifically PR Checks."""
10
-
11
- def __init__(self, github_token: str, repository: str, commit_sha: str):
12
- self.github_token = github_token
13
- self.repository = repository
14
- self.commit_sha = commit_sha
15
- self.base_url = f"https://api.github.com/repos/{repository}"
16
-
17
- async def report_verification(self, graph: ArtifactGraph):
18
- """Creates or updates a GitHub Check Run with the current verification status."""
19
- payload = GitHubCheckGenerator.generate(graph)
20
- payload["head_sha"] = self.commit_sha
21
-
22
- headers = {
23
- "Authorization": f"Bearer {self.github_token}",
24
- "Accept": "application/vnd.github.v3+json",
25
- "Content-Type": "application/json"
26
- }
27
-
28
- async with httpx.AsyncClient() as client:
29
- try:
30
- response = await client.post(
31
- f"{self.base_url}/check-runs",
32
- headers=headers,
33
- json=payload
34
- )
35
- response.raise_for_status()
36
- logger.info(f"GitHub PR Check updated for {self.repository} at {self.commit_sha}")
37
- except Exception as e:
38
- logger.error(f"Failed to report to GitHub: {e}")
39
- raise