devcouncil 0.3.1 → 0.4.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 (366) hide show
  1. package/README.md +92 -33
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +56 -40
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +964 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +281 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +87 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +103 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +69 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +53 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +325 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/summary.py +21 -3
  255. package/src/devcouncil/live/tasks.py +12 -3
  256. package/src/devcouncil/live/transcripts.py +69 -2
  257. package/src/devcouncil/llm/cache.py +5 -6
  258. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  259. package/src/devcouncil/llm/provider.py +647 -73
  260. package/src/devcouncil/llm/router.py +271 -46
  261. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  262. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  263. package/src/devcouncil/optimization/skillopt.py +9 -5
  264. package/src/devcouncil/planning/arbiter_service.py +12 -3
  265. package/src/devcouncil/planning/correction_manifest.py +107 -10
  266. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  267. package/src/devcouncil/planning/plan_service.py +5 -2
  268. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  269. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  270. package/src/devcouncil/planning/question_conversion.py +56 -0
  271. package/src/devcouncil/planning/spec_service.py +9 -3
  272. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  273. package/src/devcouncil/repo/gitignore.py +1 -2
  274. package/src/devcouncil/reporting/evidence_export.py +124 -0
  275. package/src/devcouncil/reporting/evidence_html.py +210 -0
  276. package/src/devcouncil/reporting/json_report.py +16 -12
  277. package/src/devcouncil/reporting/markdown_report.py +38 -9
  278. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  279. package/src/devcouncil/reporting/report_builder.py +40 -4
  280. package/src/devcouncil/reporting/task_provenance.py +42 -0
  281. package/src/devcouncil/reporting/verdict.py +75 -0
  282. package/src/devcouncil/skills/library/README.md +1 -0
  283. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  285. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  286. package/src/devcouncil/skills/registry.py +43 -12
  287. package/src/devcouncil/storage/db.py +57 -11
  288. package/src/devcouncil/storage/models.py +6 -0
  289. package/src/devcouncil/storage/native.py +5 -3
  290. package/src/devcouncil/storage/repositories.py +50 -18
  291. package/src/devcouncil/telemetry/context.py +28 -0
  292. package/src/devcouncil/telemetry/cost.py +4 -5
  293. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  294. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  295. package/src/devcouncil/telemetry/stages.py +27 -2
  296. package/src/devcouncil/telemetry/tracker.py +50 -13
  297. package/src/devcouncil/ui/dashboard.py +120 -8
  298. package/src/devcouncil/utils/fsio.py +58 -0
  299. package/src/devcouncil/utils/git_snapshot.py +112 -0
  300. package/src/devcouncil/utils/json_persist.py +53 -0
  301. package/src/devcouncil/utils/proc.py +89 -0
  302. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  303. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  304. package/src/devcouncil/verification/checks/__init__.py +41 -0
  305. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  306. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  307. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  308. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  309. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  310. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  311. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  312. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  313. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  314. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  315. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  316. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  317. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  318. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  319. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  320. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  321. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  322. package/src/devcouncil/verification/checks/wiring.py +216 -0
  323. package/src/devcouncil/verification/claims/__init__.py +23 -0
  324. package/src/devcouncil/verification/claims/checks.py +395 -0
  325. package/src/devcouncil/verification/claims/mapper.py +168 -0
  326. package/src/devcouncil/verification/claims/models.py +39 -0
  327. package/src/devcouncil/verification/claims/transcript.py +92 -0
  328. package/src/devcouncil/verification/claims/verdict.py +88 -0
  329. package/src/devcouncil/verification/command_evidence.py +170 -0
  330. package/src/devcouncil/verification/command_malformation.py +147 -0
  331. package/src/devcouncil/verification/command_runner.py +164 -0
  332. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  333. package/src/devcouncil/verification/diff_coverage.py +151 -0
  334. package/src/devcouncil/verification/difficulty.py +296 -0
  335. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  336. package/src/devcouncil/verification/gap_ids.py +63 -0
  337. package/src/devcouncil/verification/gate_cache.py +194 -0
  338. package/src/devcouncil/verification/gate_selector.py +344 -0
  339. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  340. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  341. package/src/devcouncil/verification/incremental_check.py +241 -0
  342. package/src/devcouncil/verification/next_actions.py +60 -1
  343. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  344. package/src/devcouncil/verification/sandbox.py +38 -11
  345. package/src/devcouncil/verification/stub_detector.py +369 -0
  346. package/src/devcouncil/verification/test_resolver.py +67 -1
  347. package/src/devcouncil/verification/verifier.py +137 -1666
  348. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  349. package/src/devcouncil/verification/verify_setup.py +176 -0
  350. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  351. package/src/semantic_layer/__init__.py +58 -0
  352. package/src/semantic_layer/benchmark.py +75 -0
  353. package/src/semantic_layer/cache.py +290 -0
  354. package/src/semantic_layer/compressor.py +137 -0
  355. package/src/semantic_layer/config.py +75 -0
  356. package/src/semantic_layer/embeddings.py +69 -0
  357. package/src/semantic_layer/llm_backends.py +99 -0
  358. package/src/semantic_layer/pipeline.py +111 -0
  359. package/src/semantic_layer/router.py +128 -0
  360. package/src/semantic_layer/tuner.py +72 -0
  361. package/uv.lock +973 -9
  362. package/src/devcouncil/artifacts/migrations.py +0 -20
  363. package/src/devcouncil/artifacts/schemas.py +0 -23
  364. package/src/devcouncil/artifacts/serializer.py +0 -21
  365. package/src/devcouncil/integrations/gitnexus.py +0 -70
  366. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -1,1089 +1,107 @@
1
- import json
2
- import shlex
1
+ from devcouncil.utils.json_persist import dump_json
2
+ import logging
3
3
  import shutil
4
- import subprocess
5
- import sys
6
- from contextlib import contextmanager
7
4
  from pathlib import Path
8
5
 
9
6
  import typer
10
- import yaml # type: ignore[import-untyped]
11
7
  from rich.console import Console
12
8
  from rich.table import Table
9
+ from devcouncil.telemetry.stages import log_stage, log_step
13
10
 
14
11
  from devcouncil.executors.agent_registry import (
15
- BUILTIN_CODING_EXECUTOR_NAMES,
16
- CODING_CLI_INTEGRATION_INFO,
12
+ GEMINI_DEPRECATION_MESSAGE,
17
13
  VALID_INPUT_MODES,
18
14
  agent_config_entry,
19
- detect_available_coding_cli,
20
- integration_tier_label,
21
15
  is_reserved_agent_name,
22
16
  load_agent_profiles,
23
- load_cli_agent_specs,
24
17
  normalize_agent_name,
25
- resolve_automated_executor,
26
- resolve_coding_cli_executable,
27
- resolve_coding_cli_probe_order,
28
18
  )
29
19
  from devcouncil.integrations.actions import apply_integration_target
30
- from devcouncil.utils.subprocess_env import clean_subprocess_env
31
- from devcouncil.integrations.check import (
32
- build_integration_check_report,
33
- integration_status_summary,
20
+ from devcouncil.integrations.integration_cli import (
21
+ print_integration_matrix,
22
+ print_integration_status,
23
+ print_recommendations,
24
+ run_integration_check,
25
+ )
26
+ from devcouncil.integrations.setup import (
27
+ apply_agent_flow_setup,
28
+ apply_code_review_graph_setup,
29
+ build_integrations_doctor_table,
30
+ )
31
+ from devcouncil.integrations.clients import (
32
+ antigravity as antigravity_client,
33
+ aider as aider_client,
34
+ claude as claude_client,
35
+ codex as codex_client,
36
+ common,
37
+ cursor as cursor_client,
38
+ gemini as gemini_client,
39
+ grok as grok_client,
40
+ hooks as hooks_client,
41
+ opencode as opencode_client,
42
+ warp as warp_client,
34
43
  )
35
44
 
36
45
  app = typer.Typer(help="Set up DevCouncil integrations with coding CLIs.")
37
46
  setup_app = typer.Typer(help="Set up optional external companion integrations.")
38
47
  app.add_typer(setup_app, name="setup")
39
48
  console = Console()
49
+ logger = logging.getLogger(__name__)
40
50
 
41
- SUPPORTED_TOOLS = ("codex", "gemini", "claude", "cursor", "opencode", "antigravity", "warp", "aider")
42
- SUPPORTED_HOOK_TOOLS = ("codex", "gemini", "claude", "cursor")
43
- OPENCODE_HOOK_PLUGIN_NAME = "opencode_devcouncil_plugin.mjs"
51
+ SUPPORTED_TOOLS = ("claude", "codex", "cursor", "grok", "opencode", "antigravity", "warp", "aider", "gemini")
52
+ SUPPORTED_HOOK_TOOLS = common.SUPPORTED_HOOK_TOOLS
53
+ OPENCODE_HOOK_PLUGIN_NAME = common.OPENCODE_HOOK_PLUGIN_NAME
44
54
  PREFERRED_COMMAND = "dev integrate"
45
55
  LEGACY_COMMAND = "dev setup --integrate"
46
56
 
47
-
48
- def _project_root(path: str | Path | None) -> Path:
49
- return Path(path or ".").expanduser().resolve()
50
-
51
-
52
- def _warn_if_verify_only(client: str) -> None:
53
- """Print a prominent containment warning when wiring a verify-only client.
54
-
55
- Verify-only clients have no native pre-tool-use hook, so DevCouncil cannot block a
56
- forbidden write or command before it happens — it is only caught post-hoc at verify
57
- time. Surface this loudly so users don't assume hard containment."""
58
- info = CODING_CLI_INTEGRATION_INFO.get(normalize_agent_name(client))
59
- if info is not None and not info.hooks:
60
- console.print(
61
- f"[bold yellow]Warning ({info.label}): No pre-action containment — "
62
- "forbidden writes/commands are caught only at verify time.[/bold yellow]"
63
- )
64
-
65
-
66
- def _server_args(project_root: Path) -> list[str]:
67
- return ["devcouncil", "mcp-server"]
68
-
69
-
70
- def _codex_command(project_root: Path) -> list[str]:
71
- return [
72
- "codex",
73
- "mcp",
74
- "add",
75
- "devcouncil",
76
- "--env",
77
- f"DEVCOUNCIL_PROJECT_ROOT={project_root}",
78
- "--",
79
- *_server_args(project_root),
80
- ]
81
-
82
-
83
- def _gemini_command(project_root: Path, scope: str) -> list[str]:
84
- return [
85
- "gemini",
86
- "mcp",
87
- "add",
88
- "--scope",
89
- scope,
90
- "--env",
91
- f"DEVCOUNCIL_PROJECT_ROOT={project_root}",
92
- "devcouncil",
93
- *_server_args(project_root),
94
- ]
95
-
96
-
97
- def _claude_command(project_root: Path, scope: str) -> list[str]:
98
- # The server name must come BEFORE --env: the current Claude CLI treats --env
99
- # as variadic, so `--env KEY=VALUE devcouncil` swallows the name `devcouncil`
100
- # as a second (invalid) env var. Putting the name first — matching the working
101
- # codex form — and terminating options with `--` avoids that.
102
- return [
103
- "claude",
104
- "mcp",
105
- "add",
106
- "--scope",
107
- scope,
108
- "devcouncil",
109
- "--env",
110
- f"DEVCOUNCIL_PROJECT_ROOT={project_root}",
111
- "--",
112
- *_server_args(project_root),
113
- ]
114
-
115
-
116
- def _cursor_config_path(project_root: Path) -> Path:
117
- return project_root / ".cursor" / "mcp.json"
118
-
119
-
120
- def _cursor_mcp_config(project_root: Path) -> dict:
121
- return {
122
- "mcpServers": {
123
- "devcouncil": {
124
- "type": "stdio",
125
- "command": "devcouncil",
126
- "args": ["mcp-server"],
127
- "env": {"DEVCOUNCIL_PROJECT_ROOT": str(project_root)},
128
- }
129
- }
130
- }
131
-
132
-
133
- def _warp_mcp_config(project_root: Path) -> dict:
134
- return {
135
- "devcouncil": {
136
- "command": "devcouncil",
137
- "args": ["mcp-server"],
138
- "env": {"DEVCOUNCIL_PROJECT_ROOT": str(project_root)},
139
- }
140
- }
141
-
142
-
143
- def _warp_mcp_path(project_root: Path) -> Path:
144
- return project_root / ".devcouncil" / "integrations" / "warp-mcp.json"
145
-
146
-
147
- def _opencode_config_path(project_root: Path) -> Path:
148
- return project_root / "opencode.json"
149
-
150
-
151
- def _opencode_mcp_entry(project_root: Path) -> dict:
152
- return {
153
- "type": "local",
154
- "command": ["devcouncil", "mcp-server"],
155
- "environment": {"DEVCOUNCIL_PROJECT_ROOT": str(project_root)},
156
- "enabled": True,
157
- "timeout": 10000,
158
- }
159
-
160
-
161
- def _antigravity_mcp_path(project_root: Path) -> Path:
162
- return project_root / ".agents" / "mcp_config.json"
163
-
164
-
165
- def _antigravity_mcp_config(project_root: Path) -> dict:
166
- return {
167
- "mcpServers": {
168
- "devcouncil": {
169
- "command": "devcouncil",
170
- "args": ["mcp-server"],
171
- "env": {"DEVCOUNCIL_PROJECT_ROOT": str(project_root)},
172
- "cwd": str(project_root),
173
- }
174
- }
175
- }
176
-
177
-
178
- def _write_warp_mcp_config(project_root: Path) -> Path:
179
- path = _warp_mcp_path(project_root)
180
- _save_json(path, _warp_mcp_config(project_root))
181
- return path
182
-
183
-
184
- def _write_cursor_config(project_root: Path) -> Path:
185
- path = _cursor_config_path(project_root)
186
- data = _load_json_strict(path, "Cursor")
187
- mcp_servers = data.setdefault("mcpServers", {})
188
- mcp_servers["devcouncil"] = _cursor_mcp_config(project_root)["mcpServers"]["devcouncil"]
189
- _save_json(path, data)
190
- return path
191
-
192
-
193
- # When set, _mutate_raw_config applies record mutations in memory and
194
- # _batched_raw_config saves config.yaml once at the end (used by
195
- # `dev integrate all --apply`, which otherwise re-parses YAML per tool).
196
- _PENDING_RAW_CONFIG: dict | None = None
197
-
198
-
199
- @contextmanager
200
- def _batched_raw_config(project_root: Path):
201
- global _PENDING_RAW_CONFIG
202
- # Re-entrant: if a batch is already active, participate in it instead of
203
- # starting a nested load/save (which would otherwise reset the shared
204
- # buffer to None on inner exit and drop the outer batch's mutations).
205
- if _PENDING_RAW_CONFIG is not None:
206
- yield
207
- return
208
- _PENDING_RAW_CONFIG = _load_raw_config(project_root)
209
- try:
210
- yield
211
- finally:
212
- # Persist whatever mutations accumulated, even if an inner installer raised
213
- # partway through — matching the old per-installer save, which committed each
214
- # installer's change immediately rather than dropping the whole batch on a
215
- # mid-loop failure.
216
- pending = _PENDING_RAW_CONFIG
217
- _PENDING_RAW_CONFIG = None
218
- if pending is not None:
219
- _save_raw_config(project_root, pending)
220
-
221
-
222
- def _mutate_raw_config(project_root: Path, mutate) -> None:
223
- if _PENDING_RAW_CONFIG is not None:
224
- mutate(_PENDING_RAW_CONFIG)
225
- return
226
- config = _load_raw_config(project_root)
227
- mutate(config)
228
- _save_raw_config(project_root, config)
229
-
230
-
231
- def _record_cursor_config(project_root: Path) -> None:
232
- def mutate(config: dict) -> None:
233
- cursor = config.setdefault("integrations", {}).setdefault("cursor", {})
234
- cursor.update({
235
- "enabled": True,
236
- "config_path": str(_cursor_config_path(project_root).relative_to(project_root)),
237
- })
238
-
239
- _mutate_raw_config(project_root, mutate)
240
-
241
-
242
- def _record_warp_config(project_root: Path) -> None:
243
- def mutate(config: dict) -> None:
244
- warp = config.setdefault("integrations", {}).setdefault("warp", {})
245
- warp.update({
246
- "enabled": True,
247
- "command": warp.get("command", "oz"),
248
- "run_mode": warp.get("run_mode", "local"),
249
- "mcp_config_path": str(_warp_mcp_path(project_root).relative_to(project_root)),
250
- })
251
-
252
- _mutate_raw_config(project_root, mutate)
253
-
254
-
255
- def _record_opencode_config(project_root: Path) -> None:
256
- def mutate(config: dict) -> None:
257
- opencode = config.setdefault("integrations", {}).setdefault("opencode", {})
258
- opencode.update({
259
- "enabled": True,
260
- "config_path": str(_opencode_config_path(project_root).relative_to(project_root)),
261
- })
262
-
263
- _mutate_raw_config(project_root, mutate)
264
-
265
-
266
- def _record_antigravity_config(project_root: Path) -> None:
267
- def mutate(config: dict) -> None:
268
- antigravity = config.setdefault("integrations", {}).setdefault("antigravity", {})
269
- antigravity.update({
270
- "enabled": True,
271
- "mcp_config_path": str(_antigravity_mcp_path(project_root).relative_to(project_root)),
272
- })
273
-
274
- _mutate_raw_config(project_root, mutate)
275
-
276
-
277
- def _load_json_strict(path: Path, label: str = "JSON") -> dict:
278
- if not path.exists():
279
- return {}
280
- try:
281
- return json.loads(path.read_text(encoding="utf-8")) or {}
282
- except json.JSONDecodeError as exc:
283
- raise ValueError(f"{path} is not valid JSON. Fix the {label} config before rerunning integration setup.") from exc
284
-
285
-
286
- def _write_opencode_config(project_root: Path) -> Path:
287
- path = _opencode_config_path(project_root)
288
- data = _load_json_strict(path, "OpenCode")
289
- data.setdefault("$schema", "https://opencode.ai/config.json")
290
- mcp = data.setdefault("mcp", {})
291
- mcp["devcouncil"] = _opencode_mcp_entry(project_root)
292
- _save_json(path, data)
293
- return path
294
-
295
-
296
- def _write_antigravity_mcp_config(project_root: Path) -> Path:
297
- path = _antigravity_mcp_path(project_root)
298
- data = _load_json_strict(path, "Antigravity")
299
- mcp_servers = data.setdefault("mcpServers", {})
300
- mcp_servers["devcouncil"] = _antigravity_mcp_config(project_root)["mcpServers"]["devcouncil"]
301
- _save_json(path, data)
302
- return path
303
-
304
-
305
- def _configure_cursor(project_root: Path, apply: bool) -> bool:
306
- path = _cursor_config_path(project_root)
307
- config = _cursor_mcp_config(project_root)
308
- if not apply:
309
- console.print("[bold]Cursor[/bold]")
310
- console.print(f"Project MCP config file: [dim]{path}[/dim]")
311
- console.print(json.dumps(config, separators=(",", ":")), soft_wrap=True)
312
- console.print("Verify in Cursor CLI with: [dim]cursor-agent mcp list[/dim]")
313
- return True
314
-
315
- if not shutil.which("cursor") and not shutil.which("cursor-agent"):
316
- console.print("[yellow]Cursor CLI not found on PATH. Project MCP config will still be available to Cursor.[/yellow]")
317
- try:
318
- written = _write_cursor_config(project_root)
319
- except ValueError as exc:
320
- console.print(f"[red]{exc}[/red]")
321
- return False
322
- _record_cursor_config(project_root)
323
- console.print(f"[green]Cursor MCP config written:[/green] {written}")
324
- return True
325
-
326
-
327
- def _configure_opencode(project_root: Path, apply: bool) -> bool:
328
- path = _opencode_config_path(project_root)
329
- config = {
330
- "$schema": "https://opencode.ai/config.json",
331
- "mcp": {"devcouncil": _opencode_mcp_entry(project_root)},
332
- }
333
- if not apply:
334
- console.print("[bold]OpenCode[/bold]")
335
- console.print(f"Project config file: [dim]{path}[/dim]")
336
- console.print(json.dumps(config, separators=(",", ":")), soft_wrap=True)
337
- console.print(
338
- "Direct executor command: "
339
- "[dim]opencode run --file .devcouncil/TASK-001-opencode-task.md "
340
- '"Execute the DevCouncil task described in the attached prompt file."[/dim]'
341
- )
342
- return True
343
-
344
- if not shutil.which("opencode"):
345
- console.print("[yellow]OpenCode CLI not found on PATH. Install it before using `dev run --executor opencode`.[/yellow]")
346
- try:
347
- written = _write_opencode_config(project_root)
348
- except ValueError as exc:
349
- console.print(f"[red]{exc}[/red]")
350
- return False
351
- _record_opencode_config(project_root)
352
- console.print(f"[green]OpenCode MCP config written:[/green] {written}")
353
- return True
354
-
355
-
356
- def _configure_antigravity(project_root: Path, apply: bool) -> bool:
357
- path = _antigravity_mcp_path(project_root)
358
- config = _antigravity_mcp_config(project_root)
359
- if not apply:
360
- console.print("[bold]Google Antigravity CLI[/bold]")
361
- console.print(f"Project MCP config file: [dim]{path}[/dim]")
362
- console.print(json.dumps(config, separators=(",", ":")), soft_wrap=True)
363
- console.print(
364
- "Direct executor command: "
365
- "[dim]agy --print --print-timeout 30m "
366
- '"Read and execute the DevCouncil task prompt at .devcouncil/TASK-001-antigravity-task.md."[/dim]'
367
- )
368
- return True
369
-
370
- if not shutil.which("agy"):
371
- console.print("[yellow]Antigravity CLI (`agy`) not found on PATH. Install it before using `dev run --executor antigravity`.[/yellow]")
372
- try:
373
- written = _write_antigravity_mcp_config(project_root)
374
- except ValueError as exc:
375
- console.print(f"[red]{exc}[/red]")
376
- return False
377
- _record_antigravity_config(project_root)
378
- console.print(f"[green]Antigravity MCP config written:[/green] {written}")
379
- return True
380
-
381
-
382
- def _configure_warp(project_root: Path, apply: bool) -> bool:
383
- path = _warp_mcp_path(project_root)
384
- config = _warp_mcp_config(project_root)
385
- if not apply:
386
- console.print("[bold]Warp / Oz[/bold]")
387
- console.print(f"MCP config file: [dim]{path}[/dim]")
388
- console.print(json.dumps(config, separators=(",", ":")), soft_wrap=True)
389
- console.print(f"Direct executor command: [dim]oz agent run --cwd {project_root} --mcp {path} --prompt <task prompt>[/dim]")
390
- return True
391
-
392
- written = _write_warp_mcp_config(project_root)
393
- _record_warp_config(project_root)
394
- console.print(f"[green]Warp MCP config written:[/green] {written}")
395
- if not shutil.which("oz"):
396
- console.print("[yellow]oz CLI not found on PATH. Install Warp/Oz before using `dev run --executor warp`.[/yellow]")
397
- return True
398
-
399
-
400
- def _format_command(command: list[str]) -> str:
401
- if sys.platform == "win32":
402
- return " ".join(_quote_powershell_arg(arg) for arg in command)
403
- return shlex.join(command)
404
-
405
-
406
- def _quote_powershell_arg(arg: str) -> str:
407
- if arg == "":
408
- return "''"
409
- special_chars = set(" \t\r\n'\"{}[](),;|&<>")
410
- if not any(char in special_chars for char in arg):
411
- return arg
412
- return "'" + arg.replace("'", "''") + "'"
413
-
414
-
415
- def _opencode_plugin_source() -> Path:
416
- return Path(__file__).resolve().parents[2] / "integrations" / OPENCODE_HOOK_PLUGIN_NAME
417
-
418
-
419
- def _opencode_plugin_path(project_root: Path) -> Path:
420
- return project_root / ".devcouncil" / "integrations" / OPENCODE_HOOK_PLUGIN_NAME
421
-
422
-
423
- def _hook_command(project_root: Path, client: str, event: str) -> str:
424
- return _format_command([
425
- "devcouncil",
426
- "hook",
427
- event,
428
- "--client",
429
- client,
430
- "--project-root",
431
- str(project_root),
432
- ])
433
-
434
-
435
- def _probe_mcp_tools(root: Path, *, timeout_seconds: float = 30.0) -> list[str]:
436
- from mcp import ClientSession, StdioServerParameters
437
- from mcp.client.stdio import stdio_client
438
- import asyncio
439
- import os
440
-
441
- async def _list_tools() -> list[str]:
442
- env = os.environ.copy()
443
- env["DEVCOUNCIL_PROJECT_ROOT"] = str(root)
444
- params = StdioServerParameters(
445
- command=sys.executable,
446
- args=["-m", "devcouncil", "mcp-server"],
447
- cwd=str(root),
448
- env=env,
449
- )
450
- async with stdio_client(params) as (read, write):
451
- async with ClientSession(read, write) as session:
452
- await session.initialize()
453
- tools = await session.list_tools()
454
- return [tool.name for tool in tools.tools]
455
-
456
- async def _list_tools_with_deadline() -> list[str]:
457
- # A wedged server process would otherwise block `dev integrate check`
458
- # indefinitely; the caller treats TimeoutError as a failed probe.
459
- return await asyncio.wait_for(_list_tools(), timeout=timeout_seconds)
460
-
461
- return asyncio.run(_list_tools_with_deadline())
462
-
463
-
464
- def _run(command: list[str]) -> int:
465
- executable = shutil.which(command[0])
466
- if not executable:
467
- return 127
468
- resolved = [executable, *command[1:]]
469
- use_shell = sys.platform == "win32" and Path(executable).suffix.lower() in {".bat", ".cmd", ".ps1"}
470
- invocation = subprocess.list2cmdline(resolved) if use_shell else resolved
471
- try:
472
- result = subprocess.run(invocation, text=True, shell=use_shell)
473
- except (FileNotFoundError, OSError):
474
- return 127
475
- return result.returncode
476
-
477
-
478
- def _run_capture(command: list[str], timeout: int = 10) -> tuple[int, str]:
479
- executable = shutil.which(command[0])
480
- if not executable:
481
- return 127, f"{command[0]} not found on PATH"
482
-
483
- resolved = [executable, *command[1:]]
484
- use_shell = sys.platform == "win32" and Path(executable).suffix.lower() in {".bat", ".cmd", ".ps1"}
485
- invocation = subprocess.list2cmdline(resolved) if use_shell else resolved
486
- try:
487
- result = subprocess.run(
488
- invocation,
489
- capture_output=True,
490
- text=True,
491
- encoding="utf-8",
492
- errors="replace",
493
- shell=use_shell,
494
- timeout=timeout,
495
- env=clean_subprocess_env(),
496
- )
497
- except subprocess.TimeoutExpired:
498
- return 124, "timed out"
499
- except (FileNotFoundError, OSError) as exc:
500
- return 127, f"{command[0]} could not be executed: {exc}"
501
- return result.returncode, (result.stdout + result.stderr).strip()
502
-
503
-
504
- def _config_path(project_root: Path) -> Path:
505
- return project_root / ".devcouncil" / "config.yaml"
506
-
507
-
508
- def _load_raw_config(project_root: Path) -> dict:
509
- path = _config_path(project_root)
510
- if not path.exists():
511
- return {}
512
- return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
513
-
514
-
515
- def _save_raw_config(project_root: Path, config: dict) -> None:
516
- path = _config_path(project_root)
517
- path.parent.mkdir(parents=True, exist_ok=True)
518
- path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8")
519
-
520
-
521
- def _load_json(path: Path) -> dict:
522
- if not path.exists():
523
- return {}
524
- try:
525
- return json.loads(path.read_text(encoding="utf-8")) or {}
526
- except json.JSONDecodeError:
527
- return {}
528
-
529
-
530
- def _save_json(path: Path, data: dict) -> None:
531
- path.parent.mkdir(parents=True, exist_ok=True)
532
- path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
533
-
534
-
535
- def _upsert_hook(settings: dict, event: str, matcher: str, command: str, name: str) -> None:
536
- hooks = settings.setdefault("hooks", {})
537
- groups = hooks.setdefault(event, [])
538
- for group in groups:
539
- if group.get("matcher") == matcher:
540
- group_hooks = group.setdefault("hooks", [])
541
- if not any(hook.get("command") == command for hook in group_hooks):
542
- group_hooks.append({
543
- "type": "command",
544
- "name": name,
545
- "command": command,
546
- "timeout": 10000,
547
- })
548
- return
549
- groups.append({
550
- "matcher": matcher,
551
- "hooks": [{
552
- "type": "command",
553
- "name": name,
554
- "command": command,
555
- "timeout": 10000,
556
- }],
557
- })
558
-
559
-
560
- def _ensure_codex_hooks_enabled(project_root: Path) -> Path:
561
- config_path = project_root / ".codex" / "config.toml"
562
- config_path.parent.mkdir(parents=True, exist_ok=True)
563
- existing = config_path.read_text(encoding="utf-8") if config_path.exists() else ""
564
- if "codex_hooks" not in existing:
565
- if "[features]" in existing:
566
- lines = existing.splitlines()
567
- updated: list[str] = []
568
- in_features = False
569
- inserted = False
570
- for line in lines:
571
- stripped = line.strip()
572
- if stripped == "[features]":
573
- in_features = True
574
- updated.append(line)
575
- continue
576
- if in_features and stripped.startswith("[") and stripped.endswith("]"):
577
- updated.append("codex_hooks = true")
578
- inserted = True
579
- in_features = False
580
- updated.append(line)
581
- if in_features and not inserted:
582
- updated.append("codex_hooks = true")
583
- config_path.write_text("\n".join(updated) + "\n", encoding="utf-8")
584
- else:
585
- separator = "\n" if existing and not existing.endswith("\n") else ""
586
- config_path.write_text(f"{existing}{separator}\n[features]\ncodex_hooks = true\n", encoding="utf-8")
587
- return config_path
588
-
589
-
590
- def _install_codex_hooks(project_root: Path) -> list[Path]:
591
- path = project_root / ".codex" / "hooks.json"
592
- settings = _load_json(path)
593
- matcher = "Bash|shell_command|exec_command|local_shell|Write|Edit|MultiEdit|write_file|edit_file|apply_patch"
594
- _upsert_hook(
595
- settings,
596
- "PreToolUse",
597
- matcher,
598
- _hook_command(project_root, "codex", "pre-tool-use"),
599
- "devcouncil-pre-tool-use",
600
- )
601
- _upsert_hook(
602
- settings,
603
- "PostToolUse",
604
- matcher,
605
- _hook_command(project_root, "codex", "post-tool-use"),
606
- "devcouncil-post-tool-use",
607
- )
608
- _save_json(path, settings)
609
- return [path, _ensure_codex_hooks_enabled(project_root)]
610
-
611
-
612
- def _install_gemini_hooks(project_root: Path) -> list[Path]:
613
- path = project_root / ".gemini" / "settings.json"
614
- settings = _load_json(path)
615
- matcher = "run_shell_command|shell_command|write_file|edit_file|replace|apply_patch"
616
- _upsert_hook(
617
- settings,
618
- "BeforeTool",
619
- matcher,
620
- _hook_command(project_root, "gemini", "pre-tool-use"),
621
- "devcouncil-pre-tool-use",
622
- )
623
- _upsert_hook(
624
- settings,
625
- "AfterTool",
626
- matcher,
627
- _hook_command(project_root, "gemini", "post-tool-use"),
628
- "devcouncil-post-tool-use",
629
- )
630
- _save_json(path, settings)
631
- return [path]
632
-
633
-
634
- def _upsert_cursor_hook(settings: dict, event: str, matcher: str, command: str) -> None:
635
- hooks = settings.setdefault("hooks", {})
636
- entries = hooks.setdefault(event, [])
637
- for entry in entries:
638
- if entry.get("command") == command:
639
- return
640
- payload: dict = {"command": command}
641
- if matcher:
642
- payload["matcher"] = matcher
643
- entries.append(payload)
644
-
645
-
646
- def _install_cursor_hooks(project_root: Path) -> list[Path]:
647
- path = project_root / ".cursor" / "hooks.json"
648
- settings = _load_json(path)
649
- settings.setdefault("version", 1)
650
- matcher = "Shell|Write|Edit|MultiEdit|Read|Task"
651
- _upsert_cursor_hook(
652
- settings,
653
- "preToolUse",
654
- matcher,
655
- _hook_command(project_root, "cursor", "pre-tool-use"),
656
- )
657
- _upsert_cursor_hook(
658
- settings,
659
- "postToolUse",
660
- matcher,
661
- _hook_command(project_root, "cursor", "post-tool-use"),
662
- )
663
- _save_json(path, settings)
664
-
665
- def mutate(config: dict) -> None:
666
- cursor = config.setdefault("integrations", {}).setdefault("cursor", {})
667
- cursor.update({
668
- "hooks_path": str(path.relative_to(project_root)),
669
- })
670
-
671
- _mutate_raw_config(project_root, mutate)
672
- return [path]
673
-
674
-
675
- def _install_opencode_hooks(project_root: Path) -> list[Path]:
676
- source = _opencode_plugin_source()
677
- if not source.exists():
678
- raise FileNotFoundError(f"Missing bundled OpenCode hook plugin: {source}")
679
- destination = _opencode_plugin_path(project_root)
680
- destination.parent.mkdir(parents=True, exist_ok=True)
681
- destination.write_text(source.read_text(encoding="utf-8"), encoding="utf-8")
682
-
683
- path = _opencode_config_path(project_root)
684
- data = _load_json_strict(path, "OpenCode") if path.exists() else {"$schema": "https://opencode.ai/config.json"}
685
- data.setdefault("$schema", "https://opencode.ai/config.json")
686
- plugins_raw = data.setdefault("plugin", [])
687
- if not isinstance(plugins_raw, list):
688
- plugins_raw = []
689
- data["plugin"] = plugins_raw
690
- plugins: list[str] = [str(item) for item in plugins_raw]
691
- data["plugin"] = plugins
692
- plugin_ref = f"./.devcouncil/integrations/{OPENCODE_HOOK_PLUGIN_NAME}"
693
- if plugin_ref not in plugins:
694
- plugins.append(plugin_ref)
695
- _save_json(path, data)
696
- _record_opencode_config(project_root)
697
- return [destination, path]
698
-
699
-
700
- def _install_claude_hooks(project_root: Path, *, write_gate: bool = False) -> list[Path]:
701
- """Install DevCouncil's Claude Code hooks into .claude/settings.local.json.
702
-
703
- By default this installs only the *assistive* lifecycle hooks (status injection on
704
- SessionStart/UserPromptSubmit, the live-review Stop signal, and the SessionEnd/
705
- PreCompact/SubagentStop/Notification trace hooks). These never block a tool call.
706
-
707
- The blocking pre-action **write-gate** (PreToolUse/PostToolUse, which denies any
708
- Bash/Write/Edit not authorized by an active task lease) is installed ONLY when
709
- ``write_gate`` is True. It is meant for autonomous executor runs, not interactive
710
- human sessions — in an interactive session there is no task lease, so the gate would
711
- fail-closed and deny every command. (``dev run --executor claude`` does its own
712
- post-hoc scope enforcement and does not depend on this hook, so leaving it off by
713
- default loses no containment.)"""
714
- path = project_root / ".claude" / "settings.local.json"
715
- settings = _load_json(path)
716
- matcher = "Bash|Write|Edit|MultiEdit"
717
- if write_gate:
718
- _upsert_hook(
719
- settings,
720
- "PreToolUse",
721
- matcher,
722
- _hook_command(project_root, "claude", "pre-tool-use"),
723
- "devcouncil-pre-tool-use",
724
- )
725
- _upsert_hook(
726
- settings,
727
- "PostToolUse",
728
- matcher,
729
- _hook_command(project_root, "claude", "post-tool-use"),
730
- "devcouncil-post-tool-use",
731
- )
732
- _upsert_hook(
733
- settings,
734
- "Stop",
735
- "",
736
- _hook_command(project_root, "claude", "agent-response"),
737
- "devcouncil-agent-response-ready",
738
- )
739
- # Lifecycle events: status-on-start/prompt, teardown, compaction, subagent finish,
740
- # and notifications. These complete DevCouncil's coverage of the documented Claude
741
- # Code hook surface beyond the pre/post/stop gate.
742
- _upsert_hook(
743
- settings,
744
- "SessionStart",
745
- "startup|resume",
746
- _hook_command(project_root, "claude", "session-start"),
747
- "devcouncil-session-start",
748
- )
749
- _upsert_hook(
750
- settings,
751
- "UserPromptSubmit",
752
- "",
753
- _hook_command(project_root, "claude", "user-prompt-submit"),
754
- "devcouncil-user-prompt-submit",
755
- )
756
- _upsert_hook(
757
- settings,
758
- "SessionEnd",
759
- "",
760
- _hook_command(project_root, "claude", "session-end"),
761
- "devcouncil-session-end",
762
- )
763
- _upsert_hook(
764
- settings,
765
- "PreCompact",
766
- "",
767
- _hook_command(project_root, "claude", "pre-compact"),
768
- "devcouncil-pre-compact",
769
- )
770
- _upsert_hook(
771
- settings,
772
- "SubagentStop",
773
- "",
774
- _hook_command(project_root, "claude", "subagent-stop"),
775
- "devcouncil-subagent-stop",
776
- )
777
- _upsert_hook(
778
- settings,
779
- "Notification",
780
- "",
781
- _hook_command(project_root, "claude", "notification"),
782
- "devcouncil-notification",
783
- )
784
- _save_json(path, settings)
785
- return [path]
786
-
787
-
788
- def _devcouncil_version() -> str:
789
- """Package version for plugin manifests, or a stable placeholder when uninstalled."""
790
- import importlib.metadata
791
-
792
- try:
793
- return importlib.metadata.version("devcouncil")
794
- except importlib.metadata.PackageNotFoundError:
795
- return "0.0.0"
796
-
797
-
798
- # Read-only DevCouncil commands the generated slash commands / hooks shell out to. Adding
799
- # them to the Claude permissions allow-list keeps the integration from prompting on every
800
- # `dev status`/`dev report` the slash commands run.
801
- _CLAUDE_PERMISSION_ALLOW = [
802
- "Bash(dev status:*)",
803
- "Bash(dev report:*)",
804
- "Bash(dev tasks:*)",
805
- "Bash(dev verify:*)",
806
- "Bash(dev repair:*)",
807
- "Bash(dev plan:*)",
808
- "Bash(dev watch:*)",
809
- "Bash(devcouncil mcp-server)",
810
- ]
811
-
812
-
813
- def _install_claude_settings(project_root: Path) -> tuple[Path, bool]:
814
- """Write the statusLine, MCP enablement, and permission allow-list into Claude settings.
815
-
816
- Merges into .claude/settings.local.json without clobbering existing user entries.
817
- Returns (path, changed); only rewrites the file when the merge changes something so
818
- re-running integration is a true no-op."""
819
- path = project_root / ".claude" / "settings.local.json"
820
- settings = _load_json(path)
821
- before = json.dumps(settings, sort_keys=True)
822
-
823
- settings["statusLine"] = {
824
- "type": "command",
825
- "command": "devcouncil hook claude-statusline",
826
- }
827
- # Auto-enable the project-scoped DevCouncil MCP server so a teammate cloning the repo
828
- # doesn't have to approve it interactively.
829
- enabled = settings.setdefault("enabledMcpjsonServers", [])
830
- if isinstance(enabled, list) and "devcouncil" not in enabled:
831
- enabled.append("devcouncil")
832
-
833
- permissions = settings.setdefault("permissions", {})
834
- if isinstance(permissions, dict):
835
- allow = permissions.setdefault("allow", [])
836
- if isinstance(allow, list):
837
- for rule in _CLAUDE_PERMISSION_ALLOW:
838
- if rule not in allow:
839
- allow.append(rule)
840
-
841
- changed = json.dumps(settings, sort_keys=True) != before
842
- if changed:
843
- _save_json(path, settings)
844
- return path, changed
845
-
846
-
847
- def _selected_skill_assets(project_root: Path):
848
- """Scaffold the applicable skills and return them as GeneratedAsset-like records.
849
-
850
- Returns (written_paths, skill_assets) where skill_assets carry (path, content) for the
851
- plugin bundler so the plugin ships the same skill bodies that land in .claude/skills/."""
852
- from devcouncil.integrations.claude_assets import GeneratedAsset
853
- from devcouncil.skills.registry import scaffold_skills, select_skills
854
-
855
- skills = select_skills("", project_root)
856
- written = scaffold_skills(project_root, skills)
857
- assets: list[GeneratedAsset] = []
858
- skills_root = project_root / ".claude" / "skills"
859
- for skill in skills:
860
- target = skills_root / skill.name / "SKILL.md"
861
- if target.exists():
862
- assets.append(GeneratedAsset(target, target.read_text(encoding="utf-8")))
863
- return written, assets
864
-
865
-
866
- def _install_claude_assets(project_root: Path) -> list[Path]:
867
- """Generate the static Claude Code asset surface (commands, agents, output style,
868
- statusline, permissions) and scaffold the applicable skills. Idempotent."""
869
- from devcouncil.integrations import claude_assets
870
-
871
- written: list[Path] = []
872
- assets: list[claude_assets.GeneratedAsset] = []
873
- assets += claude_assets.build_slash_commands(project_root)
874
- assets += claude_assets.build_subagents(project_root)
875
- assets += claude_assets.build_output_style(project_root)
876
- for asset in assets:
877
- if asset.write_if_changed():
878
- written.append(asset.path)
879
-
880
- skills_written, _ = _selected_skill_assets(project_root)
881
- written.extend(skills_written)
882
- settings_path, settings_changed = _install_claude_settings(project_root)
883
- if settings_changed:
884
- written.append(settings_path)
885
- return written
886
-
887
-
888
- def _install_claude_plugin(project_root: Path, *, write_gate: bool = False) -> list[Path]:
889
- """Build the self-contained Claude Code plugin + single-repo marketplace bundle.
890
-
891
- Bundles the commands, agents, applicable skills, hooks, and MCP config so the entire
892
- DevCouncil integration installs with one `/plugin install`. Assist-mode hooks by
893
- default; pass write_gate=True to bundle the blocking containment gate."""
894
- from devcouncil.integrations import claude_assets
895
-
896
- _, skill_assets = _selected_skill_assets(project_root)
897
- bundle = claude_assets.build_plugin_bundle(
898
- project_root, version=_devcouncil_version(), skill_assets=skill_assets, write_gate=write_gate
899
- )
900
- return [asset.path for asset in bundle if asset.write_if_changed()]
901
-
902
-
903
- def _uninstall_claude(project_root: Path) -> list[str]:
904
- """Remove everything DevCouncil installed into a Claude Code project. Idempotent.
905
-
906
- Strips DevCouncil's hooks (every event), the DevCouncil statusLine, the MCP enablement
907
- and permission rules from .claude/settings.local.json (leaving any user-authored
908
- entries untouched), deletes the generated commands/subagents/output-style files, and
909
- best-effort de-registers the MCP server via `claude mcp remove`. Returns a list of the
910
- changes made. The recoverable, in-band counterpart to a fail-closed write-gate."""
911
- removed: list[str] = []
912
- path = project_root / ".claude" / "settings.local.json"
913
- settings = _load_json(path)
914
- before = json.dumps(settings, sort_keys=True)
915
-
916
- # Hooks: drop any entry whose command invokes `devcouncil hook`, then prune empties.
917
- hooks = settings.get("hooks")
918
- if isinstance(hooks, dict):
919
- for event in list(hooks):
920
- groups = hooks.get(event)
921
- if not isinstance(groups, list):
922
- continue
923
- kept_groups = []
924
- for group in groups:
925
- inner = group.get("hooks", []) if isinstance(group, dict) else []
926
- inner_kept = [
927
- h for h in inner
928
- if "devcouncil hook" not in str(h.get("command", ""))
929
- ]
930
- if inner_kept:
931
- group["hooks"] = inner_kept
932
- kept_groups.append(group)
933
- if kept_groups:
934
- hooks[event] = kept_groups
935
- else:
936
- hooks.pop(event)
937
- if not hooks:
938
- settings.pop("hooks")
939
- removed.append(f"hooks in {path.name}")
940
-
941
- # statusLine: only remove ours.
942
- status = settings.get("statusLine")
943
- if isinstance(status, dict) and "devcouncil" in str(status.get("command", "")):
944
- settings.pop("statusLine")
945
- removed.append("statusLine")
946
-
947
- enabled = settings.get("enabledMcpjsonServers")
948
- if isinstance(enabled, list) and "devcouncil" in enabled:
949
- enabled.remove("devcouncil")
950
- if not enabled:
951
- settings.pop("enabledMcpjsonServers")
952
- removed.append("enabledMcpjsonServers entry")
953
-
954
- permissions = settings.get("permissions")
955
- if isinstance(permissions, dict) and isinstance(permissions.get("allow"), list):
956
- kept = [r for r in permissions["allow"] if r not in _CLAUDE_PERMISSION_ALLOW]
957
- if len(kept) != len(permissions["allow"]):
958
- permissions["allow"] = kept
959
- removed.append("permission allow-rules")
960
- if not permissions.get("allow"):
961
- permissions.pop("allow", None)
962
- if not permissions:
963
- settings.pop("permissions")
964
-
965
- if json.dumps(settings, sort_keys=True) != before:
966
- if settings:
967
- _save_json(path, settings)
968
- elif path.exists():
969
- path.unlink()
970
- removed.append(f"deleted empty {path.name}")
971
-
972
- # Generated asset files.
973
- targets = [
974
- project_root / ".claude" / "commands" / "devcouncil",
975
- project_root / ".claude" / "output-styles" / "devcouncil.md",
976
- ]
977
- targets += [
978
- project_root / ".claude" / "agents" / f"{name}.md"
979
- for name in ("devcouncil-implementer", "devcouncil-verifier", "devcouncil-reviewer")
980
- ]
981
- for target in targets:
982
- if target.is_dir():
983
- shutil.rmtree(target)
984
- removed.append(str(target.relative_to(project_root)))
985
- elif target.exists():
986
- target.unlink()
987
- removed.append(str(target.relative_to(project_root)))
988
-
989
- # De-register the MCP server (best-effort; only if the claude CLI is present).
990
- if shutil.which("claude"):
991
- code = _run(["claude", "mcp", "remove", "devcouncil"])
992
- if code == 0:
993
- removed.append("claude mcp server registration")
994
-
995
- return removed
996
-
997
-
998
- def _preview_hook_paths(project_root: Path, tool: str) -> list[tuple[str, Path]]:
999
- paths = {
1000
- "codex": [project_root / ".codex" / "hooks.json", project_root / ".codex" / "config.toml"],
1001
- "gemini": [project_root / ".gemini" / "settings.json"],
1002
- "claude": [project_root / ".claude" / "settings.local.json"],
1003
- "cursor": [project_root / ".cursor" / "hooks.json"],
1004
- "opencode": [_opencode_plugin_path(project_root), _opencode_config_path(project_root)],
1005
- }
1006
- selected: tuple[str, ...]
1007
- if tool == "all":
1008
- selected = (*SUPPORTED_HOOK_TOOLS, "opencode")
1009
- elif tool == "opencode":
1010
- selected = ("opencode",)
1011
- else:
1012
- selected = (tool,)
1013
- return [(client, path) for client in selected for path in paths.get(client, [])]
1014
-
1015
-
1016
- def _configure_native_hooks(
1017
- project_root: Path, tool: str = "all", apply: bool = False, *, claude_write_gate: bool = False
1018
- ) -> None:
1019
- allowed = {"all", *SUPPORTED_HOOK_TOOLS, "opencode"}
1020
- if tool not in allowed:
1021
- console.print("[red]--tool must be one of: all, codex, gemini, claude, cursor, opencode.[/red]")
1022
- raise typer.Exit(code=2)
1023
-
1024
- if not apply:
1025
- console.print("[bold]Native hook config preview[/bold]")
1026
- for client, path in _preview_hook_paths(project_root, tool):
1027
- console.print(f"{client}: {path}", soft_wrap=True)
1028
- console.print("[yellow]Preview only. Rerun with --apply to write hook config files.[/yellow]")
1029
- return
1030
-
1031
- selected: tuple[str, ...]
1032
- if tool == "all":
1033
- selected = (*SUPPORTED_HOOK_TOOLS, "opencode")
1034
- elif tool == "opencode":
1035
- selected = ("opencode",)
1036
- else:
1037
- selected = (tool,)
1038
- installers = {
1039
- "codex": _install_codex_hooks,
1040
- "gemini": _install_gemini_hooks,
1041
- # Claude's blocking write-gate is opt-in (assist-mode default); the other clients
1042
- # install their native pre/post hooks unconditionally as before.
1043
- "claude": lambda root: _install_claude_hooks(root, write_gate=claude_write_gate),
1044
- "cursor": _install_cursor_hooks,
1045
- "opencode": _install_opencode_hooks,
1046
- }
1047
- # Batch the per-installer config.yaml record updates (cursor/opencode)
1048
- # into one load/save instead of re-parsing YAML per tool.
1049
- with _batched_raw_config(project_root):
1050
- for client in selected:
1051
- try:
1052
- written = installers[client](project_root)
1053
- except (ValueError, FileNotFoundError) as exc:
1054
- console.print(f"[red]{client} hook setup failed: {exc}[/red]")
1055
- raise typer.Exit(code=1) from exc
1056
- console.print(f"[green]{client} native hooks configured:[/green] {', '.join(str(path) for path in written)}")
1057
-
1058
-
1059
- def _print_command(tool: str, command: list[str], apply: bool):
1060
- if apply:
1061
- console.print(f"[cyan]Configuring {tool} MCP integration...[/cyan]")
1062
- else:
1063
- console.print(f"[bold]{tool}[/bold]")
1064
- console.print(_format_command(command), soft_wrap=True)
1065
-
1066
-
1067
- def _configure(tool: str, command: list[str], apply: bool) -> bool:
1068
- executable = command[0]
1069
- if not shutil.which(executable):
1070
- console.print(f"[yellow]{tool} CLI not found on PATH. Install it first, then rerun this command.[/yellow]")
1071
- console.print(_format_command(command), soft_wrap=True)
1072
- return False
1073
-
1074
- _print_command(tool, command, apply)
1075
- if not apply:
1076
- return True
1077
-
1078
- code = _run(command)
1079
- if code == 0:
1080
- console.print(f"[green]{tool} integration configured.[/green]")
1081
- return True
1082
-
1083
- console.print(f"[red]{tool} integration command failed with exit code {code}.[/red]")
1084
- console.print("You can rerun it manually:")
1085
- console.print(_format_command(command), soft_wrap=True)
1086
- return False
57
+ # Back-compat re-exports for tests and apply_integration_target
58
+ _project_root = common._project_root
59
+ _warn_if_verify_only = common._warn_if_verify_only
60
+ _server_args = common._server_args
61
+ _codex_command = codex_client._codex_command
62
+ _gemini_command = gemini_client._gemini_command
63
+ _claude_command = claude_client._claude_command
64
+ _cursor_config_path = cursor_client._cursor_config_path
65
+ _configure_cursor = cursor_client._configure_cursor
66
+ _configure_grok = grok_client._configure_grok
67
+ _configure_opencode = opencode_client._configure_opencode
68
+ _configure_antigravity = antigravity_client._configure_antigravity
69
+ _configure_warp = warp_client._configure_warp
70
+ _configure_aider = aider_client._configure_aider
71
+ _write_cursor_config = cursor_client._write_cursor_config
72
+ _grok_config_path = grok_client._grok_config_path
73
+ _write_opencode_config = opencode_client._write_opencode_config
74
+ _write_antigravity_mcp_config = antigravity_client._write_antigravity_mcp_config
75
+ _write_warp_mcp_config = warp_client._write_warp_mcp_config
76
+ _record_cursor_config = cursor_client._record_cursor_config
77
+ _record_grok_config = grok_client._record_grok_config
78
+ _record_claude_config = claude_client._record_claude_config
79
+ _record_opencode_config = opencode_client._record_opencode_config
80
+ _record_antigravity_config = antigravity_client._record_antigravity_config
81
+ _record_warp_config = warp_client._record_warp_config
82
+ _record_aider_config = aider_client._record_aider_config
83
+ _batched_raw_config = common._batched_raw_config
84
+ _mutate_raw_config = common._mutate_raw_config
85
+ _load_raw_config = common._load_raw_config
86
+ _save_raw_config = common._save_raw_config
87
+ _config_path = common._config_path
88
+ _load_json = common._load_json
89
+ _save_json = common._save_json
90
+ _format_command = common._format_command
91
+ _configure = common._configure
92
+ _run = common._run
93
+ _run_capture = common._run_capture
94
+ _probe_mcp_tools = common._probe_mcp_tools
95
+ _cursor_mcp_config = cursor_client._cursor_mcp_config
96
+ _opencode_plugin_source = opencode_client._opencode_plugin_source
97
+ _install_claude_hooks = hooks_client._install_claude_hooks
98
+ _install_codex_hooks = hooks_client._install_codex_hooks
99
+ _install_claude_assets = claude_client._install_claude_assets
100
+ _install_claude_plugin = claude_client._install_claude_plugin
101
+ _uninstall_claude = claude_client._uninstall_claude
102
+ _configure_native_hooks = hooks_client._configure_native_hooks
103
+ _opencode_config_path = opencode_client._opencode_config_path
104
+ _opencode_plugin_path = opencode_client._opencode_plugin_path
1087
105
 
1088
106
 
1089
107
  @app.callback(invoke_without_command=True)
@@ -1094,31 +112,40 @@ def overview(ctx: typer.Context):
1094
112
  if ctx.invoked_subcommand is not None:
1095
113
  return
1096
114
 
1097
- table = Table(title="DevCouncil Coding CLI Integrations")
1098
- table.add_column("Tool", style="cyan")
1099
- table.add_column("Setup command", style="green")
1100
- table.add_column("Notes")
1101
- table.add_row("Codex CLI", f"{PREFERRED_COMMAND} codex --apply", "Adds DevCouncil as a stdio MCP server.")
1102
- table.add_row("Gemini CLI", f"{PREFERRED_COMMAND} gemini --apply", "Adds DevCouncil as a project-scoped stdio MCP server.")
1103
- table.add_row("Claude Code", f"{PREFERRED_COMMAND} claude --apply", "MCP + assistive hooks + slash commands, subagents, output style, skills, statusline. Add --write-gate for blocking containment.")
1104
- table.add_row("Claude assets", f"{PREFERRED_COMMAND} claude-assets --apply", "Slash commands, subagents, output style, statusline, permissions, skills (no MCP/hooks).")
1105
- table.add_row("Claude plugin", f"{PREFERRED_COMMAND} claude-plugin --apply", "Self-contained Claude Code plugin + marketplace bundling everything for /plugin install.")
1106
- table.add_row("Claude uninstall", f"{PREFERRED_COMMAND} claude --uninstall", "Remove DevCouncil hooks, statusline, MCP enablement, and generated assets from .claude/.")
1107
- table.add_row("Cursor", f"{PREFERRED_COMMAND} cursor --apply", "Writes project .cursor/mcp.json for Cursor editor and cursor-agent.")
1108
- table.add_row("OpenCode", f"{PREFERRED_COMMAND} opencode --apply", "Adds DevCouncil as a project-scoped OpenCode MCP server and executor.")
1109
- table.add_row("Google Antigravity CLI", f"{PREFERRED_COMMAND} antigravity --apply", "Writes project .agents/mcp_config.json and enables the agy executor.")
1110
- table.add_row("Warp / Oz", f"{PREFERRED_COMMAND} warp --apply", "Writes a Warp-compatible MCP JSON file for local agents and Oz CLI.")
1111
- table.add_row("Aider", f"{PREFERRED_COMMAND} aider --apply", "Enables the built-in Aider headless executor (no MCP).")
1112
- table.add_row("Bring your own CLI", f"{PREFERRED_COMMAND} cli-agent NAME --command TOOL --apply", "Registers any prompt-taking CLI as a DevCouncil executor.")
1113
- table.add_row("All", f"{PREFERRED_COMMAND} all --apply", "Runs MCP setup and installs native hooks.")
1114
- table.add_row("Native hooks", f"{PREFERRED_COMMAND} hooks --apply", "Installs Codex, Gemini, Claude, Cursor, and OpenCode hook files.")
1115
- table.add_row("Recommend", f"{PREFERRED_COMMAND} recommend", "Show the best executor for this machine and project.")
1116
- table.add_row("Status", f"{PREFERRED_COMMAND} status", "Compact PATH + config summary (no MCP probe).")
1117
- table.add_row("Matrix", f"{PREFERRED_COMMAND} matrix", "Print built-in coding CLI integration tiers.")
1118
- table.add_row("Check", f"{PREFERRED_COMMAND} check", "Verify MCP, hooks, and optional CLIs (--strict, --json for CI).")
1119
- console.print(table)
1120
- console.print(f"\nIf your install exposes only the setup flow, use: {LEGACY_COMMAND} --apply")
1121
- console.print("\nRun without [bold]--apply[/bold] to preview the exact commands first.")
115
+ logger.info("dev integrate: overview")
116
+ with log_stage("integrate", subcommand="overview"):
117
+ log_step("integrate/1: listing integration options", trace=True)
118
+ table = Table(title="DevCouncil Coding CLI Integrations")
119
+ table.add_column("Tool", style="cyan")
120
+ table.add_column("Setup command", style="green")
121
+ table.add_column("Notes")
122
+ table.add_row("Claude Code", f"{PREFERRED_COMMAND} claude --apply", "MCP + assistive hooks + slash commands, subagents, output style, skills, statusline. Add --write-gate for blocking containment.")
123
+ table.add_row("Codex CLI", f"{PREFERRED_COMMAND} codex --apply", "MCP + native lifecycle hooks. Review project hooks with /hooks after setup.")
124
+ table.add_row("Gemini CLI (deprecated)", f"{PREFERRED_COMMAND} gemini --apply", "Deprecated use dev integrate antigravity --apply instead.")
125
+ table.add_row("Claude assets", f"{PREFERRED_COMMAND} claude-assets --apply", "Slash commands, subagents, output style, statusline, permissions, skills (no MCP/hooks).")
126
+ table.add_row("Claude plugin", f"{PREFERRED_COMMAND} claude-plugin --apply", "Self-contained Claude Code plugin + marketplace bundling everything for /plugin install.")
127
+ table.add_row("Claude uninstall", f"{PREFERRED_COMMAND} claude --uninstall", "Remove DevCouncil hooks, statusline, MCP enablement, and generated assets from .claude/.")
128
+ table.add_row("Cursor", f"{PREFERRED_COMMAND} cursor --apply", "Writes project .cursor/mcp.json for Cursor editor and agent/cursor-agent.")
129
+ table.add_row("Grok Build", f"{PREFERRED_COMMAND} grok --apply", "Registers DevCouncil MCP via grok mcp add or .grok/config.toml fallback.")
130
+ table.add_row("OpenCode", f"{PREFERRED_COMMAND} opencode --apply", "Adds DevCouncil as a project-scoped OpenCode MCP server and executor.")
131
+ table.add_row("Google Antigravity CLI", f"{PREFERRED_COMMAND} antigravity --apply", "Writes project .agents/mcp_config.json and enables the agy executor.")
132
+ table.add_row("Warp / Oz", f"{PREFERRED_COMMAND} warp --apply", "Writes a Warp-compatible MCP JSON file for local agents and Oz CLI.")
133
+ table.add_row("Aider", f"{PREFERRED_COMMAND} aider --apply", "Enables the built-in Aider headless executor (no MCP).")
134
+ table.add_row("Bring your own CLI", f"{PREFERRED_COMMAND} cli-agent NAME --command TOOL --apply", "Registers any prompt-taking CLI as a DevCouncil executor.")
135
+ table.add_row("All", f"{PREFERRED_COMMAND} all --apply", "Runs MCP setup and installs native hooks.")
136
+ table.add_row("Native hooks", f"{PREFERRED_COMMAND} hooks --apply", "Installs Codex, Claude, Cursor, Grok, and OpenCode hook files.")
137
+ table.add_row("Recommend", f"{PREFERRED_COMMAND} recommend", "Show the best executor for this machine and project.")
138
+ table.add_row("Status", f"{PREFERRED_COMMAND} status", "Compact PATH + config summary (no MCP probe).")
139
+ table.add_row("Matrix", f"{PREFERRED_COMMAND} matrix", "Print built-in coding CLI integration tiers.")
140
+ table.add_row(
141
+ "Check",
142
+ f"{PREFERRED_COMMAND} check",
143
+ "Verify MCP, hooks, and integrations (--strict fails on real defects; optional coding CLIs stay warnings).",
144
+ )
145
+ console.print(table)
146
+ console.print(f"\nIf your install exposes only the setup flow, use: {LEGACY_COMMAND} --apply")
147
+ console.print("\nRun without [bold]--apply[/bold] to preview the exact commands first.")
148
+ log_step("integrate/complete", trace=True)
1122
149
 
1123
150
 
1124
151
  @app.command("doctor")
@@ -1127,51 +154,7 @@ def integrations_doctor(
1127
154
  ):
1128
155
  """Check optional integration tools and local client wiring prerequisites."""
1129
156
  root = _project_root(project_root)
1130
- table = Table(title="DevCouncil Integration Doctor")
1131
- table.add_column("Integration", style="cyan", no_wrap=True)
1132
- table.add_column("Status")
1133
- table.add_column("Notes", overflow="fold")
1134
-
1135
- checks = [
1136
- ("Agent Flow", "agent-flow-app", "Optional live/replay visualizer for trace JSONL."),
1137
- ("code-review-graph", "code-review-graph", "Optional structural graph context adapter."),
1138
- ("Claude Code", "claude", "Optional MCP client and native hook runtime for pre-tool-use enforcement."),
1139
- ("Codex CLI", "codex", "Optional MCP client, headless executor companion, and native hook runtime."),
1140
- ("Gemini CLI", "gemini", "Optional MCP client companion and native hook runtime."),
1141
- ("Cursor", "cursor-agent", "Optional MCP client, cursor-agent executor, and native hooks."),
1142
- ("OpenCode", "opencode", "Optional MCP client and headless coding-agent executor."),
1143
- ("Google Antigravity CLI", "agy", "Optional Antigravity CLI companion and headless coding-agent executor."),
1144
- ("Warp / Oz", "oz", "Optional Warp/Oz CLI companion and agent executor."),
1145
- ("Aider", "aider", "Optional headless executor via `dev run --executor aider` (no MCP)."),
1146
- ]
1147
- for label, executable, notes in checks:
1148
- found = shutil.which(executable)
1149
- table.add_row(label, "[green]OK[/green]" if found else "[yellow]Missing[/yellow]", found or notes)
1150
-
1151
- profiles = load_agent_profiles(root)
1152
- for name, spec in load_cli_agent_specs(root).items():
1153
- if spec.built_in:
1154
- continue
1155
- found = shutil.which(spec.executable)
1156
- mode_ok = spec.input_mode in VALID_INPUT_MODES
1157
- profile_ok = spec.default_profile in profiles
1158
- status = "[green]OK[/green]" if found and mode_ok and profile_ok else "[red]Invalid[/red]"
1159
- if not found:
1160
- status = "[yellow]Missing[/yellow]"
1161
- details = found or f"{spec.executable} not found on PATH"
1162
- if not mode_ok:
1163
- details = f"invalid input_mode={spec.input_mode}"
1164
- if not profile_ok:
1165
- details = f"{details}; missing profile={spec.default_profile}"
1166
- table.add_row(f"CLI agent: {name}", status, details)
1167
-
1168
- config = _config_path(root)
1169
- table.add_row(
1170
- "DevCouncil config",
1171
- "[green]OK[/green]" if config.exists() else "[red]Missing[/red]",
1172
- str(config) if config.exists() else "Run dev init first.",
1173
- )
1174
- console.print(table)
157
+ console.print(build_integrations_doctor_table(root))
1175
158
 
1176
159
 
1177
160
  @app.command("codex")
@@ -1180,11 +163,22 @@ def codex(
1180
163
  project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
1181
164
  ):
1182
165
  """
1183
- Set up DevCouncil MCP tools for Codex CLI.
166
+ Set up DevCouncil MCP tools and native lifecycle hooks for Codex CLI.
1184
167
  """
1185
168
  root = _project_root(project_root)
1186
169
  command = _codex_command(root)
1187
170
  ok = _configure("Codex CLI", command, apply)
171
+ if apply and ok:
172
+ try:
173
+ written = _install_codex_hooks(root)
174
+ common.record_hook_dev_executable(root)
175
+ except (ValueError, FileNotFoundError, OSError) as exc:
176
+ console.print(f"[red]Codex hook setup failed: {exc}[/red]")
177
+ raise typer.Exit(code=1) from exc
178
+ console.print(
179
+ f"[green]Codex integration installed[/green] ({len(written)} hook/config file(s)): MCP + native hooks."
180
+ )
181
+ console.print("[yellow]Open Codex in this project and run /hooks to review and trust the generated hooks.[/yellow]")
1188
182
  if not ok and apply:
1189
183
  raise typer.Exit(code=1)
1190
184
 
@@ -1203,8 +197,9 @@ def gemini(
1203
197
  raise typer.Exit(code=2)
1204
198
 
1205
199
  root = _project_root(project_root)
200
+ console.print(f"[yellow]{GEMINI_DEPRECATION_MESSAGE}[/yellow]")
1206
201
  command = _gemini_command(root, scope)
1207
- ok = _configure("Gemini CLI", command, apply)
202
+ ok = _configure("Gemini CLI (deprecated)", command, apply)
1208
203
  if not ok and apply:
1209
204
  raise typer.Exit(code=1)
1210
205
 
@@ -1256,6 +251,7 @@ def claude(
1256
251
  try:
1257
252
  written = _install_claude_hooks(root, write_gate=write_gate)
1258
253
  written += _install_claude_assets(root)
254
+ _record_claude_config(root, scope=scope, write_gate=write_gate)
1259
255
  except (ValueError, FileNotFoundError, OSError) as exc:
1260
256
  console.print(f"[red]Claude asset setup failed: {exc}[/red]")
1261
257
  raise typer.Exit(code=1) from exc
@@ -1356,6 +352,37 @@ def claude_plugin_cmd(
1356
352
  console.print(" [dim]/plugin install devcouncil@devcouncil-local[/dim]")
1357
353
 
1358
354
 
355
+ @app.command("claude-github")
356
+ def claude_github(
357
+ apply: bool = typer.Option(False, "--apply", help="Write the workflow file instead of previewing it."),
358
+ project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
359
+ ):
360
+ """
361
+ Generate a GitHub Actions workflow that runs DevCouncil + Claude Code on repo events.
362
+
363
+ Read-only DevCouncil verification on pull requests, and gated autonomous task pickup via
364
+ headless Claude Code on manual dispatch or a nightly schedule. This is the stable-primitive
365
+ alternative to Claude Code's experimental cloud Routines; the autonomous job needs an
366
+ ANTHROPIC_API_KEY repository secret.
367
+ """
368
+ from devcouncil.integrations.claude_assets import build_github_workflow
369
+
370
+ root = _project_root(project_root)
371
+ asset = build_github_workflow(root)
372
+ if not apply:
373
+ console.print("[bold]DevCouncil GitHub Actions workflow (preview)[/bold]")
374
+ console.print(f"Would write: [dim]{asset.path}[/dim]")
375
+ console.print("[yellow]Preview only. Rerun with --apply to write the workflow.[/yellow]")
376
+ return
377
+ changed = asset.write_if_changed()
378
+ rel = asset.path.relative_to(root)
379
+ if changed:
380
+ console.print(f"[green]Wrote GitHub Actions workflow[/green] at {rel}")
381
+ else:
382
+ console.print(f"[dim]GitHub Actions workflow already up to date at {rel}[/dim]")
383
+ console.print("Add an [bold]ANTHROPIC_API_KEY[/bold] repository secret for the autonomous runs.")
384
+
385
+
1359
386
  @app.command("cursor")
1360
387
  def cursor(
1361
388
  apply: bool = typer.Option(False, "--apply", help="Write project Cursor MCP config instead of printing it."),
@@ -1377,6 +404,27 @@ def cursor(
1377
404
  raise typer.Exit(code=1)
1378
405
 
1379
406
 
407
+ @app.command("grok")
408
+ def grok(
409
+ apply: bool = typer.Option(False, "--apply", help="Register Grok MCP config instead of printing it."),
410
+ project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
411
+ ):
412
+ """
413
+ Set up DevCouncil MCP tools for Grok Build.
414
+ """
415
+ root = _project_root(project_root)
416
+ if apply:
417
+ report = apply_integration_target(root, "grok")
418
+ if not report.ok:
419
+ console.print(report.to_json())
420
+ raise typer.Exit(code=1)
421
+ console.print("[green]Grok integration configured.[/green]")
422
+ return
423
+ ok = _configure_grok(root, apply)
424
+ if not ok and apply:
425
+ raise typer.Exit(code=1)
426
+
427
+
1380
428
  @app.command("opencode")
1381
429
  def opencode(
1382
430
  apply: bool = typer.Option(False, "--apply", help="Write project OpenCode config instead of printing it."),
@@ -1441,29 +489,6 @@ def warp(
1441
489
  _configure_warp(root, apply)
1442
490
 
1443
491
 
1444
- def _record_aider_config(project_root: Path) -> None:
1445
- def mutate(config: dict) -> None:
1446
- config.setdefault("integrations", {}).setdefault("aider", {}).update({"enabled": True})
1447
-
1448
- _mutate_raw_config(project_root, mutate)
1449
-
1450
-
1451
- def _configure_aider(project_root: Path, apply: bool) -> bool:
1452
- command = ["aider", "--yes", "--no-show-model-warnings", "--message", "<task prompt>"]
1453
- if not apply:
1454
- console.print("[bold]Aider[/bold]")
1455
- console.print("Built-in executor: [dim]dev run TASK-001 --executor aider[/dim]")
1456
- console.print("Launch command: [dim]" + _format_command(command) + "[/dim]")
1457
- console.print("Aider does not expose a first-party DevCouncil MCP server.")
1458
- return True
1459
-
1460
- if not shutil.which("aider"):
1461
- console.print("[yellow]Aider CLI not found on PATH. Install it before using `dev run --executor aider`.[/yellow]")
1462
- _record_aider_config(project_root)
1463
- console.print("[green]Aider executor enabled in .devcouncil/config.yaml.[/green]")
1464
- return True
1465
-
1466
-
1467
492
  @app.command("aider")
1468
493
  def aider(
1469
494
  apply: bool = typer.Option(False, "--apply", help="Record the built-in Aider executor in DevCouncil config."),
@@ -1542,7 +567,7 @@ def cli_agent(
1542
567
  if not apply:
1543
568
  console.print("[bold]Bring your own CLI executor preview[/bold]")
1544
569
  console.print(f"Executor: [cyan]{normalized}[/cyan]")
1545
- console.print(json.dumps(entry, indent=2), soft_wrap=True)
570
+ console.print(dump_json(entry, indent=2), soft_wrap=True)
1546
571
  console.print(f"Run with: [dim]dev run TASK-001 --executor {normalized}[/dim]")
1547
572
  console.print("[yellow]Preview only. Rerun with --apply to update .devcouncil/config.yaml.[/yellow]")
1548
573
  return
@@ -1570,7 +595,7 @@ def all_tools(
1570
595
  strict: bool = typer.Option(
1571
596
  False,
1572
597
  "--strict",
1573
- help="After --apply, run dev integrate check --strict and fail on missing optional CLIs.",
598
+ help="After --apply, run dev integrate check --strict (fails on real integration defects; missing optional coding CLIs stay warnings).",
1574
599
  ),
1575
600
  ):
1576
601
  """
@@ -1601,13 +626,13 @@ def all_tools(
1601
626
  return
1602
627
 
1603
628
  commands = [
1604
- ("Codex CLI", _codex_command(root)),
1605
- ("Gemini CLI", _gemini_command(root, gemini_scope)),
1606
629
  ("Claude Code", _claude_command(root, claude_scope)),
630
+ ("Codex CLI", _codex_command(root)),
1607
631
  ]
1608
632
  for tool, command in commands:
1609
633
  _configure(tool, command, apply)
1610
634
  _configure_cursor(root, apply)
635
+ _configure_grok(root, apply)
1611
636
  _configure_opencode(root, apply)
1612
637
  _configure_antigravity(root, apply)
1613
638
  _configure_warp(root, apply)
@@ -1621,47 +646,7 @@ def recommend(
1621
646
  project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
1622
647
  ):
1623
648
  """Recommend a coding CLI executor for this machine and project."""
1624
- root = _project_root(project_root)
1625
- probe_order = resolve_coding_cli_probe_order(root)
1626
- detected = detect_available_coding_cli(root, probe_order=probe_order)
1627
- resolved = resolve_automated_executor(root, None)
1628
-
1629
- table = Table(title="DevCouncil Integration Recommendations")
1630
- table.add_column("Client", style="cyan")
1631
- table.add_column("PATH")
1632
- table.add_column("Tier")
1633
- table.add_column("MCP")
1634
- table.add_column("Hooks")
1635
-
1636
- for client in probe_order:
1637
- info = CODING_CLI_INTEGRATION_INFO.get(client)
1638
- on_path = resolve_coding_cli_executable(root, client)
1639
- table.add_row(
1640
- client,
1641
- "[green]yes[/green]" if on_path else "[dim]no[/dim]",
1642
- integration_tier_label(client),
1643
- "yes" if info and info.mcp else "no",
1644
- "yes" if info and info.hooks else "no",
1645
- )
1646
-
1647
- console.print(table)
1648
- if summary := integration_status_summary(root):
1649
- if summary.get("custom_probe_order"):
1650
- console.print(
1651
- f"\n[dim]Probe order:[/dim] {', '.join(summary['probe_order'])} "
1652
- f"(from execution.coding_cli_probe_order)"
1653
- )
1654
- else:
1655
- console.print(f"\n[dim]Probe order:[/dim] {', '.join(summary['probe_order'])} (default)")
1656
- if detected:
1657
- console.print(f"\n[bold]Recommended executor:[/bold] [cyan]{resolved}[/cyan]")
1658
- console.print(f"Run: [dim]dev run TASK-001 --executor {resolved}[/dim]")
1659
- console.print(f"Or: [dim]dev go \"Your goal\" --executor {resolved}[/dim]")
1660
- console.print(f"Setup: [dim]{PREFERRED_COMMAND} {resolved} --apply[/dim]")
1661
- else:
1662
- console.print("\n[yellow]No built-in coding CLI was found on PATH.[/yellow]")
1663
- console.print("Install Codex, Gemini, Claude Code, Cursor Agent, OpenCode, or register a custom CLI:")
1664
- console.print(f"[dim]{PREFERRED_COMMAND} cli-agent NAME --command TOOL --apply[/dim]")
649
+ print_recommendations(_project_root(project_root), console)
1665
650
 
1666
651
 
1667
652
  @app.command("status")
@@ -1670,46 +655,7 @@ def status(
1670
655
  as_json: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
1671
656
  ):
1672
657
  """Show a compact integration summary without running the MCP server probe."""
1673
- root = _project_root(project_root)
1674
- summary = integration_status_summary(root)
1675
- raw_config = _load_raw_config(root) if (root / ".devcouncil").exists() else {}
1676
- integrations = raw_config.get("integrations", {})
1677
-
1678
- if as_json:
1679
- payload = {
1680
- **summary,
1681
- "integrations_enabled": {
1682
- name: bool(integrations.get(name, {}).get("enabled"))
1683
- for name in ("cursor", "opencode", "antigravity", "warp", "aider")
1684
- },
1685
- }
1686
- typer.echo(json.dumps(payload, indent=2))
1687
- return
1688
-
1689
- table = Table(title="DevCouncil Integration Status")
1690
- table.add_column("Setting", style="cyan")
1691
- table.add_column("Value")
1692
-
1693
- table.add_row("Project", "[green]initialized[/green]" if summary["project_initialized"] else "[yellow]not initialized[/yellow]")
1694
- table.add_row("Default executor", summary["default_executor"])
1695
- table.add_row("Resolved executor", summary["resolved_executor"])
1696
- table.add_row("CLIs on PATH", ", ".join(summary["coding_clis_on_path"]) or "[dim]none[/dim]")
1697
- table.add_row("Probe order", ", ".join(summary["probe_order"]))
1698
- table.add_row("Stream CLI output", "yes" if summary["stream_cli_output"] else "no")
1699
- table.add_row("Cursor resume mode", summary["cursor_resume_mode"])
1700
-
1701
- for name in ("cursor", "opencode", "antigravity", "warp", "aider"):
1702
- enabled = bool(integrations.get(name, {}).get("enabled"))
1703
- table.add_row(f"{name} integration", "[green]enabled[/green]" if enabled else "[dim]off[/dim]")
1704
-
1705
- console.print(table)
1706
- if summary["resolved_executor"] not in {"", "manual"}:
1707
- console.print(
1708
- f"\n[dim]Next:[/dim] dev run TASK-001 --executor {summary['resolved_executor']} "
1709
- f"| {PREFERRED_COMMAND} check for full readiness"
1710
- )
1711
- else:
1712
- console.print(f"\n[dim]Next:[/dim] {PREFERRED_COMMAND} recommend | {PREFERRED_COMMAND} check")
658
+ print_integration_status(_project_root(project_root), console, as_json=as_json)
1713
659
 
1714
660
 
1715
661
  @app.command("matrix")
@@ -1717,36 +663,8 @@ def matrix(
1717
663
  project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
1718
664
  ):
1719
665
  """Print built-in coding CLI integration tiers and capabilities."""
1720
- root = _project_root(project_root)
1721
- _ = root
1722
- table = Table(title="DevCouncil Coding CLI Integration Matrix")
1723
- table.add_column("Client", style="cyan")
1724
- table.add_column("Tier")
1725
- table.add_column("Headless")
1726
- table.add_column("MCP setup")
1727
- table.add_column("Native hooks")
1728
- table.add_column("Enforcement")
1729
- table.add_column("Notes")
1730
-
1731
- for client in sorted(BUILTIN_CODING_EXECUTOR_NAMES):
1732
- info = CODING_CLI_INTEGRATION_INFO.get(client)
1733
- posture = info.enforcement if info else "verify-only"
1734
- posture_render = "[green]pre-action[/green]" if posture == "pre-action" else "[yellow]verify-only[/yellow]"
1735
- table.add_row(
1736
- client,
1737
- integration_tier_label(client),
1738
- "yes" if info and info.tier == 1 else "no",
1739
- "yes" if info and info.mcp else "no",
1740
- "yes" if info and info.hooks else "verify only",
1741
- posture_render,
1742
- info.notes if info else "",
1743
- )
1744
- console.print(table)
1745
- console.print(
1746
- "\n[dim]Enforcement:[/dim] [green]pre-action[/green] blocks forbidden writes/commands "
1747
- "before they happen; [yellow]verify-only[/yellow] catches them only at verify time."
1748
- )
1749
- console.print("\nSee [dim]docs/integration-tiers.md[/dim] for workflow guidance.")
666
+ _ = _project_root(project_root)
667
+ print_integration_matrix(console)
1750
668
 
1751
669
 
1752
670
  @app.command("hooks")
@@ -1758,27 +676,117 @@ def hooks(
1758
676
  False,
1759
677
  "--write-gate/--no-write-gate",
1760
678
  "--contain/--no-contain",
1761
- help="Install Claude's blocking PreToolUse/PostToolUse write-gate too (off by default; "
679
+ help="Install Claude's blocking PreToolUse write-gate too (off by default; "
1762
680
  "fail-closes an interactive session without a task lease).",
1763
681
  ),
682
+ git: bool = typer.Option(
683
+ True,
684
+ "--git/--no-git",
685
+ help="Install git post-commit/post-merge/post-checkout hooks that run "
686
+ "`dev map --if-stale` (on by default with --apply).",
687
+ ),
688
+ check: bool = typer.Option(
689
+ False,
690
+ "--check",
691
+ help="Report whether installed hook commands still point at the resolved project `dev` executable.",
692
+ ),
1764
693
  ):
1765
694
  """
1766
695
  Install DevCouncil hook configuration for Codex, Gemini, Claude, Cursor, and OpenCode.
1767
696
 
1768
- Claude installs only assistive hooks by default; add --write-gate for pre-action
1769
- containment (intended for autonomous executor runs).
697
+ Claude installs assistive hooks plus refresh-only PostToolUse by default; add
698
+ --write-gate for pre-action PreToolUse containment (autonomous executor runs).
699
+ Git map-refresh hooks install by default with --apply (use --no-git to skip).
1770
700
  """
1771
701
  root = _project_root(project_root)
702
+ if check:
703
+ ok, details = common.check_hook_dev_executable(root)
704
+ current = common.resolve_dev_executable(root)
705
+ recorded = common.recorded_hook_dev_executable(root)
706
+ console.print(f"Resolved `dev`: [cyan]{current}[/cyan]")
707
+ console.print(f"Recorded for hooks: [cyan]{recorded or '(none)'}[/cyan]")
708
+ if ok:
709
+ console.print(f"[green]OK[/green] — {details}")
710
+ raise typer.Exit(code=0)
711
+ console.print(f"[red]MISMATCH[/red] — {details}")
712
+ console.print(
713
+ f"[dim]Re-run `{PREFERRED_COMMAND} hooks --apply` to retarget hook commands.[/dim]"
714
+ )
715
+ raise typer.Exit(code=1)
1772
716
  if apply and tool == "all":
1773
717
  report = apply_integration_target(root, "hooks", claude_write_gate=write_gate)
1774
718
  if not report.ok:
1775
719
  console.print(report.to_json())
1776
720
  raise typer.Exit(code=1)
1777
721
  console.print("[green]Native hooks configured.[/green]")
1778
- return
1779
- _configure_native_hooks(root, tool, apply, claude_write_gate=write_gate)
722
+ else:
723
+ _configure_native_hooks(root, tool, apply, claude_write_gate=write_gate)
724
+ if git:
725
+ written = _install_git_map_hooks(root, apply=apply)
726
+ for path in written:
727
+ console.print(f"[green]{'Wrote' if apply else 'Would write'} git hook:[/green] {path}")
728
+
729
+
730
+ def _install_git_map_hooks(root: Path, *, apply: bool) -> list[str]:
731
+ """Install post-commit/post-merge/post-checkout hooks that refresh the map when stale."""
732
+ hooks_dir = root / ".git" / "hooks"
733
+ if not (root / ".git").exists():
734
+ console.print("[yellow]No .git directory; skipping git map hooks.[/yellow]")
735
+ return []
736
+ executable = common.resolve_dev_executable(root)
737
+ # Quote for POSIX shell; absolute path avoids PATH shadowing by a stale global install.
738
+ quoted = common._format_command([executable])
739
+ script = f"""#!/bin/sh
740
+ # DevCouncil: refresh repo map after git operations (best-effort).
741
+ {quoted} map --if-stale --no-wiki --project-root "$(git rev-parse --show-toplevel)" >/dev/null 2>&1 || true
742
+ """
743
+ written: list[str] = []
744
+ for name in ("post-commit", "post-merge", "post-checkout"):
745
+ path = hooks_dir / name
746
+ rel = str(path.relative_to(root)) if path.is_relative_to(root) else str(path)
747
+ written.append(rel)
748
+ if not apply:
749
+ continue
750
+ hooks_dir.mkdir(parents=True, exist_ok=True)
751
+ existing = path.read_text(encoding="utf-8") if path.exists() else ""
752
+ marker = "# DevCouncil: refresh repo map"
753
+ if marker in existing:
754
+ # Retarget the executable if a previous install used a different path.
755
+ if quoted not in existing:
756
+ updated = _retarget_git_hook_script(existing, quoted)
757
+ path.write_text(updated, encoding="utf-8")
758
+ continue
759
+ if existing and not existing.endswith("\n"):
760
+ existing += "\n"
761
+ path.write_text(existing + script if existing else script, encoding="utf-8")
762
+ path.chmod(path.stat().st_mode | 0o111)
763
+ if apply:
764
+ common.record_hook_dev_executable(root, executable)
765
+ return written
1780
766
 
1781
767
 
768
+ def _retarget_git_hook_script(existing: str, quoted_executable: str) -> str:
769
+ """Replace the DevCouncil map-refresh command line with the current executable."""
770
+ import re
771
+
772
+ pattern = re.compile(
773
+ r"(?m)^.*\bmap --if-stale\b.*$|^.*\bmap --no-wiki\b.*$",
774
+ )
775
+ replacement = (
776
+ f'{quoted_executable} map --if-stale --no-wiki '
777
+ f'--project-root "$(git rev-parse --show-toplevel)" >/dev/null 2>&1 || true'
778
+ )
779
+ if pattern.search(existing):
780
+ return pattern.sub(replacement, existing, count=1)
781
+ # Marker present but no recognizable command line — append a fresh one.
782
+ suffix = existing if existing.endswith("\n") else existing + "\n"
783
+ return (
784
+ suffix
785
+ + "# DevCouncil: refresh repo map after git operations (best-effort).\n"
786
+ + replacement
787
+ + "\n"
788
+ )
789
+
1782
790
  @app.command("uninstall")
1783
791
  def uninstall(
1784
792
  project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
@@ -1807,7 +815,7 @@ def check(
1807
815
  strict: bool = typer.Option(
1808
816
  False,
1809
817
  "--strict",
1810
- help="Treat missing optional coding CLIs as failures instead of warnings.",
818
+ help="Fail on real project/integration defects (broken state, hooks, MCP, configured-client auth). Missing optional coding CLIs stay warnings.",
1811
819
  ),
1812
820
  as_json: bool = typer.Option(False, "--json", help="Emit machine-readable JSON for CI."),
1813
821
  report_file: Path | None = typer.Option(
@@ -1821,48 +829,14 @@ def check(
1821
829
  """
1822
830
  Check whether DevCouncil is ready to integrate with coding CLIs.
1823
831
  """
1824
- root = _project_root(project_root)
1825
- report = build_integration_check_report(root, strict=strict)
1826
- table = Table(title="DevCouncil Integration Check")
1827
- table.add_column("Check", style="cyan")
1828
- table.add_column("Status", style="magenta")
1829
- table.add_column("Details")
1830
-
1831
- for row in report.checks:
1832
- if row.status == "ok":
1833
- rendered = "[green]OK[/green]"
1834
- elif row.status == "skip":
1835
- rendered = "[dim]SKIP[/dim]"
1836
- elif row.status == "missing":
1837
- rendered = "[yellow]Missing[/yellow]"
1838
- else:
1839
- rendered = "[red]FAIL[/red]"
1840
- table.add_row(row.name, rendered, row.details)
1841
-
1842
- write_json = as_json or report_file is not None
1843
- if write_json:
1844
- json_text = report.to_json()
1845
- if report_file is not None:
1846
- report_path = Path(report_file).expanduser().resolve()
1847
- report_path.parent.mkdir(parents=True, exist_ok=True)
1848
- report_path.write_text(json_text + "\n", encoding="utf-8")
1849
- if not as_json:
1850
- console.print(f"[dim]Wrote integration report to[/dim] {report_path}")
1851
- if as_json:
1852
- typer.echo(json_text)
1853
- if not write_json or not as_json:
1854
- console.print(table)
1855
-
1856
- if report.failures:
1857
- if not as_json:
1858
- console.print(
1859
- f"\n[yellow]Fix failed checks, then run:[/yellow] {PREFERRED_COMMAND} all --apply "
1860
- f"(or {LEGACY_COMMAND} --apply)."
1861
- )
1862
- raise typer.Exit(code=1)
1863
-
1864
- if not as_json:
1865
- console.print(f"\n[green]Ready.[/green] Run: {PREFERRED_COMMAND} all --apply (or {LEGACY_COMMAND} --apply).")
832
+ run_integration_check(
833
+ _project_root(project_root),
834
+ console,
835
+ strict=strict,
836
+ as_json=as_json,
837
+ report_file=report_file,
838
+ legacy_command=LEGACY_COMMAND,
839
+ )
1866
840
 
1867
841
 
1868
842
  @setup_app.command("agent-flow")
@@ -1882,33 +856,7 @@ def setup_agent_flow(
1882
856
  console.print("[yellow]Preview only. Rerun with --apply to record this integration in config.[/yellow]")
1883
857
  return
1884
858
 
1885
- config = _load_raw_config(root)
1886
- integrations = config.setdefault("integrations", {})
1887
- integrations["agent_flow"] = {
1888
- "enabled": True,
1889
- "trace_path": str(trace_path),
1890
- "mode": "jsonl",
1891
- }
1892
- _save_raw_config(root, config)
1893
- docs_dir = root / ".devcouncil" / "integrations"
1894
- docs_dir.mkdir(parents=True, exist_ok=True)
1895
- (docs_dir / "agent-flow.md").write_text(
1896
- "\n".join([
1897
- "# Agent Flow",
1898
- "",
1899
- f"DevCouncil writes trace events to `{trace_path}`.",
1900
- "",
1901
- "Local replay:",
1902
- "",
1903
- "```bash",
1904
- "dev trace tail --follow",
1905
- "```",
1906
- "",
1907
- "External visualizers can watch the JSONL file directly. DevCouncil does not modify global editor or Claude Code settings from this setup command.",
1908
- "",
1909
- ]),
1910
- encoding="utf-8",
1911
- )
859
+ trace_path = apply_agent_flow_setup(root)
1912
860
  console.print("[green]Agent Flow trace integration recorded in .devcouncil/config.yaml.[/green]")
1913
861
 
1914
862
 
@@ -1930,44 +878,7 @@ def setup_code_review_graph(
1930
878
  console.print("[yellow]Preview only. Rerun with --apply to record this integration.[/yellow]")
1931
879
  return
1932
880
 
1933
- if not ignore_path.exists():
1934
- ignore_path.write_text(
1935
- "\n".join([
1936
- ".devcouncil/**",
1937
- ".git/**",
1938
- ".venv/**",
1939
- "dist/**",
1940
- "node_modules/**",
1941
- "",
1942
- ]),
1943
- encoding="utf-8",
1944
- )
881
+ _executable, ignore_path, created = apply_code_review_graph_setup(root)
882
+ if created:
1945
883
  console.print(f"[green]Created {ignore_path}.[/green]")
1946
-
1947
- config = _load_raw_config(root)
1948
- integrations = config.setdefault("integrations", {})
1949
- integrations["code_review_graph"] = {
1950
- "enabled": True,
1951
- "command": "code-review-graph",
1952
- "optional": True,
1953
- }
1954
- _save_raw_config(root, config)
1955
- docs_dir = root / ".devcouncil" / "integrations"
1956
- docs_dir.mkdir(parents=True, exist_ok=True)
1957
- (docs_dir / "code-review-graph.md").write_text(
1958
- "\n".join([
1959
- "# code-review-graph",
1960
- "",
1961
- "Install and build the graph outside DevCouncil:",
1962
- "",
1963
- "```bash",
1964
- "pipx install code-review-graph",
1965
- "code-review-graph build",
1966
- "```",
1967
- "",
1968
- "DevCouncil uses this as an optional context adapter for mapping, prompts, verification traces, and MCP graph context.",
1969
- "",
1970
- ]),
1971
- encoding="utf-8",
1972
- )
1973
884
  console.print("[green]code-review-graph adapter recorded in .devcouncil/config.yaml.[/green]")