devcouncil 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +201 -6
  2. package/package.json +9 -2
  3. package/pyproject.toml +34 -2
  4. package/src/devcouncil/app/config.py +348 -12
  5. package/src/devcouncil/app/orchestrator.py +10 -6
  6. package/src/devcouncil/app/state_machine.py +4 -0
  7. package/src/devcouncil/artifacts/graph.py +32 -5
  8. package/src/devcouncil/assets/__init__.py +1 -0
  9. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  10. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  11. package/src/devcouncil/cli/commands/agents.py +292 -0
  12. package/src/devcouncil/cli/commands/artifacts.py +6 -3
  13. package/src/devcouncil/cli/commands/check.py +220 -0
  14. package/src/devcouncil/cli/commands/config.py +43 -4
  15. package/src/devcouncil/cli/commands/cost.py +57 -0
  16. package/src/devcouncil/cli/commands/dashboard.py +6 -1
  17. package/src/devcouncil/cli/commands/design.py +186 -0
  18. package/src/devcouncil/cli/commands/doctor.py +379 -22
  19. package/src/devcouncil/cli/commands/evidence.py +48 -0
  20. package/src/devcouncil/cli/commands/go.py +532 -33
  21. package/src/devcouncil/cli/commands/handoff.py +69 -0
  22. package/src/devcouncil/cli/commands/hook.py +296 -15
  23. package/src/devcouncil/cli/commands/init.py +161 -20
  24. package/src/devcouncil/cli/commands/integrate.py +1371 -124
  25. package/src/devcouncil/cli/commands/logs.py +106 -0
  26. package/src/devcouncil/cli/commands/map.py +80 -10
  27. package/src/devcouncil/cli/commands/okf.py +245 -0
  28. package/src/devcouncil/cli/commands/plan.py +256 -55
  29. package/src/devcouncil/cli/commands/prompt.py +18 -7
  30. package/src/devcouncil/cli/commands/repair.py +50 -24
  31. package/src/devcouncil/cli/commands/report.py +8 -0
  32. package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
  33. package/src/devcouncil/cli/commands/rollback.py +27 -28
  34. package/src/devcouncil/cli/commands/run.py +195 -54
  35. package/src/devcouncil/cli/commands/runs.py +223 -0
  36. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  37. package/src/devcouncil/cli/commands/semantic.py +47 -0
  38. package/src/devcouncil/cli/commands/setup.py +145 -6
  39. package/src/devcouncil/cli/commands/shell.py +73 -0
  40. package/src/devcouncil/cli/commands/skills.py +267 -0
  41. package/src/devcouncil/cli/commands/status.py +30 -15
  42. package/src/devcouncil/cli/commands/trace.py +47 -3
  43. package/src/devcouncil/cli/commands/verify.py +144 -3
  44. package/src/devcouncil/cli/commands/watch.py +32 -12
  45. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  46. package/src/devcouncil/cli/main.py +91 -7
  47. package/src/devcouncil/domain/evidence.py +29 -2
  48. package/src/devcouncil/domain/gap.py +27 -1
  49. package/src/devcouncil/domain/task.py +31 -2
  50. package/src/devcouncil/execution/checkpoints.py +256 -0
  51. package/src/devcouncil/execution/context_builder.py +1 -1
  52. package/src/devcouncil/execution/fs_watcher.py +205 -0
  53. package/src/devcouncil/execution/handoff.py +102 -0
  54. package/src/devcouncil/execution/hook_policy.py +162 -74
  55. package/src/devcouncil/execution/patch.py +65 -10
  56. package/src/devcouncil/execution/permissions.py +24 -24
  57. package/src/devcouncil/execution/policy_engine.py +350 -0
  58. package/src/devcouncil/execution/prompt_builder.py +751 -23
  59. package/src/devcouncil/execution/shell_session.py +231 -0
  60. package/src/devcouncil/execution/task_runner.py +24 -9
  61. package/src/devcouncil/executors/agent_registry.py +596 -0
  62. package/src/devcouncil/executors/coding_cli.py +791 -39
  63. package/src/devcouncil/executors/mini_swe.py +6 -0
  64. package/src/devcouncil/executors/native/agent.py +135 -19
  65. package/src/devcouncil/executors/openhands.py +6 -0
  66. package/src/devcouncil/gating/checks/clean_git.py +3 -1
  67. package/src/devcouncil/gating/checks/secret_scan_check.py +47 -21
  68. package/src/devcouncil/gating/policy.py +190 -11
  69. package/src/devcouncil/hardware.py +184 -0
  70. package/src/devcouncil/indexing/ast_matcher.py +17 -7
  71. package/src/devcouncil/indexing/lsp.py +45 -4
  72. package/src/devcouncil/indexing/repo_mapper.py +1284 -15
  73. package/src/devcouncil/indexing/semantic_index.py +221 -0
  74. package/src/devcouncil/integrations/actions.py +166 -0
  75. package/src/devcouncil/integrations/check.py +426 -0
  76. package/src/devcouncil/integrations/claude_assets.py +444 -0
  77. package/src/devcouncil/integrations/code_review_graph.py +13 -2
  78. package/src/devcouncil/integrations/github_intent.py +149 -0
  79. package/src/devcouncil/integrations/gitnexus.py +45 -2
  80. package/src/devcouncil/integrations/mcp/server.py +1944 -32
  81. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  82. package/src/devcouncil/integrations/pr_comments.py +9 -0
  83. package/src/devcouncil/knowledge/__init__.py +23 -0
  84. package/src/devcouncil/knowledge/design.py +374 -0
  85. package/src/devcouncil/knowledge/design_conformance.py +317 -0
  86. package/src/devcouncil/knowledge/fetch.py +223 -0
  87. package/src/devcouncil/knowledge/frontmatter.py +51 -0
  88. package/src/devcouncil/knowledge/okf.py +202 -0
  89. package/src/devcouncil/knowledge/skill_bridge.py +96 -0
  90. package/src/devcouncil/knowledge/sources.py +239 -0
  91. package/src/devcouncil/live/cards.py +181 -25
  92. package/src/devcouncil/live/repair_prompt.py +29 -6
  93. package/src/devcouncil/live/reviewer.py +72 -13
  94. package/src/devcouncil/live/signals.py +2 -2
  95. package/src/devcouncil/live/summary.py +18 -8
  96. package/src/devcouncil/live/transcripts.py +47 -11
  97. package/src/devcouncil/llm/cache.py +20 -8
  98. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  99. package/src/devcouncil/llm/provider.py +617 -49
  100. package/src/devcouncil/llm/router.py +337 -53
  101. package/src/devcouncil/optimization/__init__.py +1 -0
  102. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  103. package/src/devcouncil/optimization/skillopt.py +673 -0
  104. package/src/devcouncil/planning/arbiter_service.py +10 -2
  105. package/src/devcouncil/planning/correction_manifest.py +346 -0
  106. package/src/devcouncil/planning/critique_service.py +16 -4
  107. package/src/devcouncil/planning/plan_service.py +86 -6
  108. package/src/devcouncil/planning/prompt_enhancer_service.py +206 -1
  109. package/src/devcouncil/planning/repair_service.py +8 -2
  110. package/src/devcouncil/planning/spec_service.py +37 -3
  111. package/src/devcouncil/repo/ci_scaffold.py +165 -0
  112. package/src/devcouncil/repo/gitignore.py +123 -0
  113. package/src/devcouncil/repo/sca.py +384 -0
  114. package/src/devcouncil/reporting/json_report.py +22 -1
  115. package/src/devcouncil/reporting/markdown_report.py +29 -1
  116. package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
  117. package/src/devcouncil/reporting/okf_html.py +323 -0
  118. package/src/devcouncil/reporting/report_builder.py +18 -1
  119. package/src/devcouncil/skills/__init__.py +19 -0
  120. package/src/devcouncil/skills/library/README.md +46 -0
  121. package/src/devcouncil/skills/library/ai-training.md +50 -0
  122. package/src/devcouncil/skills/library/android.md +50 -0
  123. package/src/devcouncil/skills/library/backend.md +52 -0
  124. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  125. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  126. package/src/devcouncil/skills/library/desktop.md +46 -0
  127. package/src/devcouncil/skills/library/devops.md +48 -0
  128. package/src/devcouncil/skills/library/game-dev.md +46 -0
  129. package/src/devcouncil/skills/library/ios.md +48 -0
  130. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  131. package/src/devcouncil/skills/library/security.md +48 -0
  132. package/src/devcouncil/skills/library/systems.md +48 -0
  133. package/src/devcouncil/skills/library/web.md +47 -0
  134. package/src/devcouncil/skills/library/windows.md +47 -0
  135. package/src/devcouncil/skills/registry.py +408 -0
  136. package/src/devcouncil/storage/db.py +140 -3
  137. package/src/devcouncil/storage/models.py +125 -0
  138. package/src/devcouncil/storage/native.py +559 -0
  139. package/src/devcouncil/storage/repositories.py +157 -78
  140. package/src/devcouncil/telemetry/cost.py +123 -17
  141. package/src/devcouncil/telemetry/logging_setup.py +244 -0
  142. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  143. package/src/devcouncil/telemetry/pricing.py +28 -0
  144. package/src/devcouncil/telemetry/stages.py +141 -0
  145. package/src/devcouncil/telemetry/traces.py +62 -7
  146. package/src/devcouncil/telemetry/tracker.py +24 -10
  147. package/src/devcouncil/ui/dashboard.py +393 -28
  148. package/src/devcouncil/utils/redaction.py +9 -3
  149. package/src/devcouncil/utils/subprocess_env.py +69 -0
  150. package/src/devcouncil/verification/acceptance_compiler.py +253 -0
  151. package/src/devcouncil/verification/ad_hoc_check.py +135 -0
  152. package/src/devcouncil/verification/diff_coverage.py +353 -0
  153. package/src/devcouncil/verification/implementation_reviewer.py +11 -2
  154. package/src/devcouncil/verification/next_actions.py +189 -0
  155. package/src/devcouncil/verification/sandbox.py +181 -0
  156. package/src/devcouncil/verification/test_resolver.py +91 -0
  157. package/src/devcouncil/verification/verifier.py +1549 -143
  158. package/uv.lock +205 -64
  159. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -0,0 +1,426 @@
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(
111
+ project_root: Path, detected: str | None = None
112
+ ) -> tuple[bool, str]:
113
+ if detected is None:
114
+ detected = detect_available_coding_cli(project_root)
115
+ if not detected:
116
+ return False, "No built-in coding CLI on PATH. Run dev integrate recommend after installing one."
117
+ resolved = resolve_automated_executor(project_root, None)
118
+ return True, f"Use --executor {resolved} for dev go / dev run (detected: {detected})."
119
+
120
+
121
+ def coding_clis_on_path(project_root: Path) -> list[str]:
122
+ order = resolve_coding_cli_probe_order(project_root)
123
+ return [client for client in order if resolve_coding_cli_executable(project_root, client)]
124
+
125
+
126
+ def _load_json_file(path: Path) -> dict[str, Any]:
127
+ if not path.exists():
128
+ return {}
129
+ try:
130
+ loaded = json.loads(path.read_text(encoding="utf-8")) or {}
131
+ except json.JSONDecodeError:
132
+ return {}
133
+ return loaded if isinstance(loaded, dict) else {}
134
+
135
+
136
+ def _cursor_config_status(project_root: Path) -> tuple[str, bool, list[str]]:
137
+ path = project_root / ".cursor" / "mcp.json"
138
+ data = _load_json_file(path)
139
+ server = ((data.get("mcpServers") or {}).get("devcouncil") or {}) if data else {}
140
+ ok = (
141
+ server.get("type") == "stdio"
142
+ and server.get("command") == "devcouncil"
143
+ and server.get("args") == ["mcp-server"]
144
+ and (server.get("env") or {}).get("DEVCOUNCIL_PROJECT_ROOT") == str(project_root)
145
+ )
146
+ if ok:
147
+ return "ok", False, [str(path)]
148
+ return ("missing" if not path.exists() else "drifted"), True, [str(path)]
149
+
150
+
151
+ def _opencode_config_status(project_root: Path) -> tuple[str, bool, list[str]]:
152
+ path = project_root / "opencode.json"
153
+ data = _load_json_file(path)
154
+ server = ((data.get("mcp") or {}).get("devcouncil") or {}) if data else {}
155
+ plugin = data.get("plugin") or []
156
+ plugin_ok = "./.devcouncil/integrations/opencode_devcouncil_plugin.mjs" in plugin
157
+ ok = (
158
+ server.get("type") == "local"
159
+ and server.get("command") == ["devcouncil", "mcp-server"]
160
+ and (server.get("environment") or {}).get("DEVCOUNCIL_PROJECT_ROOT") == str(project_root)
161
+ )
162
+ status = "ok" if ok else ("missing" if not path.exists() else "drifted")
163
+ return status, not ok or not plugin_ok, [str(path), str(project_root / ".devcouncil" / "integrations" / "opencode_devcouncil_plugin.mjs")]
164
+
165
+
166
+ def _antigravity_config_status(project_root: Path) -> tuple[str, bool, list[str]]:
167
+ path = project_root / ".agents" / "mcp_config.json"
168
+ data = _load_json_file(path)
169
+ server = ((data.get("mcpServers") or {}).get("devcouncil") or {}) if data else {}
170
+ ok = (
171
+ server.get("command") == "devcouncil"
172
+ and server.get("args") == ["mcp-server"]
173
+ and server.get("cwd") == str(project_root)
174
+ and (server.get("env") or {}).get("DEVCOUNCIL_PROJECT_ROOT") == str(project_root)
175
+ )
176
+ if ok:
177
+ return "ok", False, [str(path)]
178
+ return ("missing" if not path.exists() else "drifted"), True, [str(path)]
179
+
180
+
181
+ def _warp_config_status(project_root: Path) -> tuple[str, bool, list[str]]:
182
+ path = project_root / ".devcouncil" / "integrations" / "warp-mcp.json"
183
+ data = _load_json_file(path)
184
+ server = data.get("devcouncil") or {}
185
+ ok = (
186
+ "mcpServers" not in data
187
+ and server.get("command") == "devcouncil"
188
+ and server.get("args") == ["mcp-server"]
189
+ and (server.get("env") or {}).get("DEVCOUNCIL_PROJECT_ROOT") == str(project_root)
190
+ )
191
+ if ok:
192
+ return "ok", False, [str(path)]
193
+ return ("missing" if not path.exists() else "drifted"), True, [str(path)]
194
+
195
+
196
+ def integration_capability_rows(project_root: Path) -> list[dict[str, object]]:
197
+ order = resolve_coding_cli_probe_order(project_root)
198
+ rows: list[dict[str, object]] = []
199
+ for client in order:
200
+ info = CODING_CLI_INTEGRATION_INFO.get(client)
201
+ if info is None:
202
+ continue
203
+ config_status = "not_applicable"
204
+ fixable = bool(info.mcp or info.hooks or info.launcher_shim)
205
+ paths: list[str] = []
206
+ if client == "cursor":
207
+ config_status, fixable, paths = _cursor_config_status(project_root)
208
+ elif client == "opencode":
209
+ config_status, fixable, paths = _opencode_config_status(project_root)
210
+ elif client == "antigravity":
211
+ config_status, fixable, paths = _antigravity_config_status(project_root)
212
+ elif client == "warp":
213
+ config_status, fixable, paths = _warp_config_status(project_root)
214
+
215
+ rows.append({
216
+ "name": info.name,
217
+ "label": info.label,
218
+ "on_path": resolve_coding_cli_executable(project_root, client) is not None,
219
+ "tier": info.tier,
220
+ "headless": info.headless,
221
+ "mcp": info.mcp,
222
+ "hooks": info.hooks,
223
+ "enforcement": info.enforcement,
224
+ "launcher_shim": info.launcher_shim,
225
+ "notes": info.notes,
226
+ "configured": config_status == "ok",
227
+ "config_status": config_status,
228
+ "fixable": fixable,
229
+ "paths": paths,
230
+ "apply_target": client,
231
+ })
232
+ return rows
233
+
234
+
235
+ def _hook_config_references_devcouncil(path: Path) -> bool | None:
236
+ """Return whether a client hook config still wires DevCouncil's gate.
237
+
238
+ ``True`` -> the file exists and references ``devcouncil`` somewhere in its hooks.
239
+ ``False`` -> the file exists but no longer references it (tampered/disarmed).
240
+ ``None`` -> the file does not exist (client was never integrated here).
241
+
242
+ Reads the raw text rather than parsing each client's bespoke schema so it works
243
+ uniformly across JSON hook files and is resilient to format drift; the goal is a
244
+ tamper tripwire, not full schema validation."""
245
+ if not path.exists():
246
+ return None
247
+ try:
248
+ text = path.read_text(encoding="utf-8")
249
+ except OSError:
250
+ return False
251
+ return "devcouncil" in text.lower()
252
+
253
+
254
+ def _hook_config_tamper_targets(project_root: Path) -> list[tuple[str, Path]]:
255
+ return [
256
+ ("Claude", project_root / ".claude" / "settings.local.json"),
257
+ ("Codex", project_root / ".codex" / "hooks.json"),
258
+ ("Gemini", project_root / ".gemini" / "settings.json"),
259
+ ("Cursor", project_root / ".cursor" / "hooks.json"),
260
+ ]
261
+
262
+
263
+ def build_integration_check_report(project_root: Path, *, strict: bool = False) -> IntegrationCheckReport:
264
+ from devcouncil.cli.commands import integrate
265
+
266
+ rows: list[IntegrationCheckRow] = []
267
+ failures = 0
268
+
269
+ def add(ok: bool, name: str, details: str) -> None:
270
+ nonlocal failures
271
+ rows.append(IntegrationCheckRow(name=name, status="ok" if ok else "fail", details=details))
272
+ if not ok:
273
+ failures += 1
274
+
275
+ def add_optional(ok: bool, name: str, details: str) -> None:
276
+ if strict and not ok:
277
+ add(False, name, details)
278
+ return
279
+ rows.append(IntegrationCheckRow(name=name, status="ok" if ok else "missing", details=details))
280
+
281
+ def add_skip(name: str, details: str) -> None:
282
+ rows.append(IntegrationCheckRow(name=name, status="skip", details=details))
283
+
284
+ root = project_root.expanduser().resolve()
285
+ detected = detect_available_coding_cli(root)
286
+ add((root / ".devcouncil").exists(), "Project state", str(root / ".devcouncil"))
287
+ devcouncil_path = shutil.which("devcouncil")
288
+ add(
289
+ devcouncil_path is not None or Path(sys.executable).exists(),
290
+ "devcouncil CLI",
291
+ devcouncil_path or f"{sys.executable} -m devcouncil",
292
+ )
293
+
294
+ devcouncil_launch = [devcouncil_path] if devcouncil_path else [sys.executable, "-m", "devcouncil"]
295
+ code, output = integrate._run_capture([*devcouncil_launch, "--help"])
296
+ add(code == 0, "devcouncil command", output.splitlines()[0] if output else "No output")
297
+
298
+ for client in resolve_coding_cli_probe_order(root):
299
+ cli_ok, cli_details = probe_coding_cli_version(client)
300
+ add_optional(cli_ok, CODING_CLI_CHECK_LABELS.get(client, client), cli_details)
301
+
302
+ rec_ok, rec_details = recommended_executor_status(root, detected)
303
+ add_optional(rec_ok, "Recommended coding CLI", rec_details)
304
+
305
+ for row in integration_capability_rows(root):
306
+ name = str(row["name"])
307
+ status = str(row["config_status"])
308
+ if status == "ok":
309
+ paths = row.get("paths")
310
+ path_list = paths if isinstance(paths, list) else []
311
+ add(True, f"{row['label']} config", ", ".join(str(path) for path in path_list))
312
+ elif status in {"missing", "drifted"}:
313
+ add_skip(f"{row['label']} config", f"Run dev integrate {name} --apply to repair.")
314
+
315
+ raw_config = integrate._load_raw_config(root) if (root / ".devcouncil").exists() else {}
316
+
317
+ cursor_hooks = root / ".cursor" / "hooks.json"
318
+ cursor_enabled = bool(raw_config.get("integrations", {}).get("cursor", {}).get("enabled"))
319
+ if cursor_enabled or cursor_hooks.exists():
320
+ hooks_ok = False
321
+ if cursor_hooks.exists():
322
+ try:
323
+ hooks_data = json.loads(cursor_hooks.read_text(encoding="utf-8")) or {}
324
+ hooks_ok = "preToolUse" in hooks_data.get("hooks", {})
325
+ except json.JSONDecodeError:
326
+ hooks_ok = False
327
+ add(
328
+ hooks_ok,
329
+ "Cursor hooks",
330
+ str(cursor_hooks) if hooks_ok else "Run dev integrate hooks --apply --tool cursor.",
331
+ )
332
+
333
+ opencode_config = integrate._opencode_config_path(root)
334
+ opencode_enabled = bool(raw_config.get("integrations", {}).get("opencode", {}).get("enabled"))
335
+ opencode_plugin = integrate._opencode_plugin_path(root)
336
+ if opencode_enabled or opencode_plugin.exists():
337
+ plugin_ok = opencode_plugin.exists()
338
+ plugin_registered = False
339
+ if plugin_ok and opencode_config.exists():
340
+ try:
341
+ opencode_data = json.loads(opencode_config.read_text(encoding="utf-8")) or {}
342
+ plugin_registered = (
343
+ f"./.devcouncil/integrations/{integrate.OPENCODE_HOOK_PLUGIN_NAME}"
344
+ in (opencode_data.get("plugin") or [])
345
+ )
346
+ except json.JSONDecodeError:
347
+ plugin_registered = False
348
+ add(
349
+ plugin_ok and plugin_registered,
350
+ "OpenCode hook plugin",
351
+ str(opencode_plugin) if plugin_ok and plugin_registered else "Run dev integrate hooks --apply --tool opencode.",
352
+ )
353
+
354
+ bundled_plugin = integrate._opencode_plugin_source()
355
+ add(
356
+ bundled_plugin.exists(),
357
+ "Bundled OpenCode hook plugin",
358
+ str(bundled_plugin) if bundled_plugin.exists() else "Reinstall DevCouncil; package asset is missing.",
359
+ )
360
+
361
+ custom_agents = raw_config.get("integrations", {}).get("cli_agents", {}).get("agents", {})
362
+ if custom_agents:
363
+ for name, agent in sorted(custom_agents.items()):
364
+ command = str(agent.get("command", "")).strip()
365
+ found = shutil.which(command) if command else None
366
+ add(found is not None, f"CLI agent: {name}", found or f"{command or 'command'} not found on PATH")
367
+ else:
368
+ add_skip("Custom CLI agents", "No agents registered.")
369
+
370
+ try:
371
+ tools = integrate._probe_mcp_tools(root)
372
+ expected = {"devcouncil_status", "devcouncil_report", "devcouncil_get_task"}
373
+ add(expected.issubset(set(tools)), "MCP server", ", ".join(tools))
374
+ except Exception as exc:
375
+ add(False, "MCP server", str(exc))
376
+
377
+ # Tamper tripwire: any installed client hook config must still reference the
378
+ # DevCouncil gate. A present-but-unreferenced file means the pre-action gate was
379
+ # disarmed (by an agent or by hand) and is reported as a failure.
380
+ for label, hook_path in _hook_config_tamper_targets(root):
381
+ references = _hook_config_references_devcouncil(hook_path)
382
+ if references is None:
383
+ continue
384
+ add(
385
+ references,
386
+ f"{label} hook integrity",
387
+ str(hook_path) if references else f"{hook_path} no longer references devcouncil (tampered/disarmed).",
388
+ )
389
+
390
+ recommended = resolve_automated_executor(root, None) if detected else None
391
+ return IntegrationCheckReport(tuple(rows), recommended, failures)
392
+
393
+
394
+ def integration_status_summary(project_root: Path) -> dict[str, Any]:
395
+ from devcouncil.app.config import load_config
396
+
397
+ probe_order = resolve_coding_cli_probe_order(project_root)
398
+ on_path = coding_clis_on_path(project_root)
399
+ detected = on_path[0] if on_path else None
400
+ try:
401
+ execution = load_config(project_root).execution
402
+ default_executor = execution.default_executor
403
+ stream_cli_output = execution.stream_cli_output
404
+ cursor_resume_mode = execution.cursor_resume_mode
405
+ custom_probe_order = list(execution.coding_cli_probe_order)
406
+ except Exception:
407
+ default_executor = "manual"
408
+ stream_cli_output = False
409
+ cursor_resume_mode = "off"
410
+ custom_probe_order = []
411
+
412
+ resolved = resolve_automated_executor(project_root, None) if detected or default_executor != "manual" else "manual"
413
+ config_path = project_root / ".devcouncil" / "config.yaml"
414
+ return {
415
+ "project_initialized": (project_root / ".devcouncil").is_dir(),
416
+ "config_path": str(config_path) if config_path.exists() else None,
417
+ "default_executor": default_executor,
418
+ "resolved_executor": resolved,
419
+ "detected_executor": detected,
420
+ "coding_clis_on_path": on_path,
421
+ "probe_order": list(probe_order),
422
+ "custom_probe_order": custom_probe_order,
423
+ "stream_cli_output": stream_cli_output,
424
+ "cursor_resume_mode": cursor_resume_mode,
425
+ "capabilities": integration_capability_rows(project_root),
426
+ }