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,10 +1,16 @@
1
- import httpx
1
+ import asyncio
2
2
  import logging
3
+
4
+ import httpx
5
+
3
6
  from devcouncil.artifacts.graph import ArtifactGraph
4
7
  from devcouncil.reporting.github_check import GitHubCheckGenerator
5
8
 
6
9
  logger = logging.getLogger(__name__)
7
10
 
11
+ _RETRYABLE_STATUS = {429, 500, 502, 503, 504}
12
+
13
+
8
14
  class GitHubIntegration:
9
15
  """Manages interactions with GitHub API, specifically PR Checks."""
10
16
 
@@ -14,6 +20,66 @@ class GitHubIntegration:
14
20
  self.commit_sha = commit_sha
15
21
  self.base_url = f"https://api.github.com/repos/{repository}"
16
22
 
23
+ async def _post_with_retry(
24
+ self,
25
+ client: httpx.AsyncClient,
26
+ url: str,
27
+ *,
28
+ headers: dict,
29
+ json: dict,
30
+ max_attempts: int = 3,
31
+ ) -> httpx.Response:
32
+ last_exc: Exception | None = None
33
+ for attempt in range(1, max_attempts + 1):
34
+ try:
35
+ response = await client.post(url, headers=headers, json=json)
36
+ if response.status_code in _RETRYABLE_STATUS and attempt < max_attempts:
37
+ delay = min(30.0, 2.0 ** attempt)
38
+ logger.warning(
39
+ "GitHub POST %s returned %s; retrying %d/%d in %.0fs",
40
+ url,
41
+ response.status_code,
42
+ attempt,
43
+ max_attempts,
44
+ delay,
45
+ )
46
+ await asyncio.sleep(delay)
47
+ continue
48
+ response.raise_for_status()
49
+ return response
50
+ except httpx.TimeoutException as exc:
51
+ last_exc = exc
52
+ if attempt >= max_attempts:
53
+ raise
54
+ delay = min(30.0, 2.0 ** attempt)
55
+ logger.warning(
56
+ "GitHub POST %s timed out; retrying %d/%d in %.0fs",
57
+ url,
58
+ attempt,
59
+ max_attempts,
60
+ delay,
61
+ )
62
+ await asyncio.sleep(delay)
63
+ except httpx.HTTPStatusError:
64
+ raise
65
+ except httpx.HTTPError as exc:
66
+ last_exc = exc
67
+ if attempt >= max_attempts:
68
+ raise
69
+ delay = min(30.0, 2.0 ** attempt)
70
+ logger.warning(
71
+ "GitHub POST %s failed (%s); retrying %d/%d in %.0fs",
72
+ url,
73
+ exc,
74
+ attempt,
75
+ max_attempts,
76
+ delay,
77
+ )
78
+ await asyncio.sleep(delay)
79
+ if last_exc is not None:
80
+ raise last_exc
81
+ raise RuntimeError("GitHub POST failed without a response")
82
+
17
83
  async def report_verification(self, graph: ArtifactGraph):
18
84
  """Creates or updates a GitHub Check Run with the current verification status."""
19
85
  payload = GitHubCheckGenerator.generate(graph)
@@ -22,18 +88,18 @@ class GitHubIntegration:
22
88
  headers = {
23
89
  "Authorization": f"Bearer {self.github_token}",
24
90
  "Accept": "application/vnd.github.v3+json",
25
- "Content-Type": "application/json"
91
+ "Content-Type": "application/json",
26
92
  }
27
93
 
28
94
  async with httpx.AsyncClient() as client:
29
95
  try:
30
- response = await client.post(
96
+ await self._post_with_retry(
97
+ client,
31
98
  f"{self.base_url}/check-runs",
32
99
  headers=headers,
33
- json=payload
100
+ json=payload,
34
101
  )
35
- response.raise_for_status()
36
- logger.info(f"GitHub PR Check updated for {self.repository} at {self.commit_sha}")
102
+ logger.info("GitHub PR Check updated for %s at %s", self.repository, self.commit_sha)
37
103
  except Exception as e:
38
- logger.error(f"Failed to report to GitHub: {e}")
104
+ logger.error("Failed to report to GitHub: %s", e)
39
105
  raise
@@ -0,0 +1,197 @@
1
+ """Rich/JSON renderers for dev integrate subcommands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ from devcouncil.executors.agent_registry import (
13
+ BUILTIN_CODING_EXECUTOR_NAMES,
14
+ CODING_CLI_INTEGRATION_INFO,
15
+ detect_available_coding_cli,
16
+ integration_tier_label,
17
+ resolve_automated_executor,
18
+ resolve_coding_cli_executable,
19
+ resolve_coding_cli_probe_order,
20
+ )
21
+ from devcouncil.integrations.check import build_integration_check_report, integration_status_summary
22
+ from devcouncil.integrations.clients.common import _load_raw_config
23
+
24
+ PREFERRED_COMMAND = "dev integrate"
25
+
26
+
27
+ def print_recommendations(root: Path, console: Console) -> None:
28
+ probe_order = resolve_coding_cli_probe_order(root)
29
+ detected = detect_available_coding_cli(root, probe_order=probe_order)
30
+ resolved = resolve_automated_executor(root, None)
31
+
32
+ table = Table(title="DevCouncil Integration Recommendations")
33
+ table.add_column("Client", style="cyan")
34
+ table.add_column("PATH")
35
+ table.add_column("Tier")
36
+ table.add_column("MCP")
37
+ table.add_column("Hooks")
38
+
39
+ for client in probe_order:
40
+ info = CODING_CLI_INTEGRATION_INFO.get(client)
41
+ on_path = resolve_coding_cli_executable(root, client)
42
+ table.add_row(
43
+ client,
44
+ "[green]yes[/green]" if on_path else "[dim]no[/dim]",
45
+ integration_tier_label(client),
46
+ "yes" if info and info.mcp else "no",
47
+ "yes" if info and info.hooks else "no",
48
+ )
49
+
50
+ console.print(table)
51
+ if summary := integration_status_summary(root):
52
+ if summary.get("custom_probe_order"):
53
+ console.print(
54
+ f"\n[dim]Probe order:[/dim] {', '.join(summary['probe_order'])} "
55
+ f"(from execution.coding_cli_probe_order)"
56
+ )
57
+ else:
58
+ console.print(f"\n[dim]Probe order:[/dim] {', '.join(summary['probe_order'])} (default)")
59
+ if detected:
60
+ console.print(f"\n[bold]Recommended executor:[/bold] [cyan]{resolved}[/cyan]")
61
+ console.print(f"Run: [dim]dev run TASK-001 --executor {resolved}[/dim]")
62
+ console.print(f"Or: [dim]dev go \"Your goal\" --executor {resolved}[/dim]")
63
+ console.print(f"Setup: [dim]{PREFERRED_COMMAND} {resolved} --apply[/dim]")
64
+ else:
65
+ console.print("\n[yellow]No built-in coding CLI was found on PATH.[/yellow]")
66
+ console.print("Install Codex, Claude Code, Cursor Agent, OpenCode, Antigravity, or register a custom CLI:")
67
+ console.print(f"[dim]{PREFERRED_COMMAND} cli-agent NAME --command TOOL --apply[/dim]")
68
+
69
+
70
+ def print_integration_status(root: Path, console: Console, *, as_json: bool) -> None:
71
+ summary = integration_status_summary(root)
72
+ raw_config = _load_raw_config(root) if (root / ".devcouncil").exists() else {}
73
+ integrations = raw_config.get("integrations", {})
74
+
75
+ if as_json:
76
+ payload = {
77
+ **summary,
78
+ "integrations_enabled": {
79
+ name: bool(integrations.get(name, {}).get("enabled"))
80
+ for name in ("cursor", "opencode", "antigravity", "warp", "aider")
81
+ },
82
+ }
83
+ typer.echo(json.dumps(payload, indent=2))
84
+ return
85
+
86
+ table = Table(title="DevCouncil Integration Status")
87
+ table.add_column("Setting", style="cyan")
88
+ table.add_column("Value")
89
+
90
+ table.add_row("Project", "[green]initialized[/green]" if summary["project_initialized"] else "[yellow]not initialized[/yellow]")
91
+ table.add_row("Default executor", summary["default_executor"])
92
+ table.add_row("Resolved executor", summary["resolved_executor"])
93
+ table.add_row("CLIs on PATH", ", ".join(summary["coding_clis_on_path"]) or "[dim]none[/dim]")
94
+ table.add_row("Probe order", ", ".join(summary["probe_order"]))
95
+ table.add_row("Stream CLI output", "yes" if summary["stream_cli_output"] else "no")
96
+ table.add_row("Cursor resume mode", summary["cursor_resume_mode"])
97
+ table.add_row("Grok resume mode", summary.get("grok_resume_mode", "off"))
98
+
99
+ for name in ("cursor", "grok", "opencode", "antigravity", "warp", "aider"):
100
+ enabled = bool(integrations.get(name, {}).get("enabled"))
101
+ table.add_row(f"{name} integration", "[green]enabled[/green]" if enabled else "[dim]off[/dim]")
102
+
103
+ console.print(table)
104
+ if summary["resolved_executor"] not in {"", "manual"}:
105
+ console.print(
106
+ f"\n[dim]Next:[/dim] dev run TASK-001 --executor {summary['resolved_executor']} "
107
+ f"| {PREFERRED_COMMAND} check for full readiness"
108
+ )
109
+ else:
110
+ console.print(f"\n[dim]Next:[/dim] {PREFERRED_COMMAND} recommend | {PREFERRED_COMMAND} check")
111
+
112
+
113
+ def print_integration_matrix(console: Console) -> None:
114
+ table = Table(title="DevCouncil Coding CLI Integration Matrix")
115
+ table.add_column("Client", style="cyan")
116
+ table.add_column("Tier")
117
+ table.add_column("Headless")
118
+ table.add_column("MCP setup")
119
+ table.add_column("Native hooks")
120
+ table.add_column("Enforcement")
121
+ table.add_column("Notes")
122
+
123
+ for client in sorted(BUILTIN_CODING_EXECUTOR_NAMES):
124
+ info = CODING_CLI_INTEGRATION_INFO.get(client)
125
+ posture = info.enforcement if info else "verify-only"
126
+ posture_render = {
127
+ "pre-action": "[green]pre-action[/green]",
128
+ "advisory+verify": "[cyan]advisory+verify[/cyan]",
129
+ }.get(posture, "[yellow]verify-only[/yellow]")
130
+ table.add_row(
131
+ client,
132
+ integration_tier_label(client),
133
+ "yes" if info and info.tier == 1 else "no",
134
+ "yes" if info and info.mcp else "no",
135
+ "yes" if info and info.hooks else "verify only",
136
+ posture_render,
137
+ info.notes if info else "",
138
+ )
139
+ console.print(table)
140
+ console.print(
141
+ "\n[dim]Enforcement:[/dim] [green]pre-action[/green] blocks forbidden writes/commands "
142
+ "before they happen; [cyan]advisory+verify[/cyan] warns in native hooks and "
143
+ "contains through sandbox/verification; [yellow]verify-only[/yellow] catches them at verify time."
144
+ )
145
+ console.print("\nSee [dim]docs/coding-cli-integration.md[/dim] for workflow guidance.")
146
+
147
+
148
+ def run_integration_check(
149
+ root: Path,
150
+ console: Console,
151
+ *,
152
+ strict: bool,
153
+ as_json: bool,
154
+ report_file: Path | None,
155
+ legacy_command: str = "dev setup --integrate",
156
+ ) -> None:
157
+ report = build_integration_check_report(root, strict=strict)
158
+ table = Table(title="DevCouncil Integration Check")
159
+ table.add_column("Check", style="cyan")
160
+ table.add_column("Status", style="magenta")
161
+ table.add_column("Details")
162
+
163
+ for row in report.checks:
164
+ if row.status == "ok":
165
+ rendered = "[green]OK[/green]"
166
+ elif row.status == "skip":
167
+ rendered = "[dim]SKIP[/dim]"
168
+ elif row.status == "missing":
169
+ rendered = "[yellow]Missing[/yellow]"
170
+ else:
171
+ rendered = "[red]FAIL[/red]"
172
+ table.add_row(row.name, rendered, row.details)
173
+
174
+ write_json = as_json or report_file is not None
175
+ if write_json:
176
+ json_text = report.to_json()
177
+ if report_file is not None:
178
+ report_path = Path(report_file).expanduser().resolve()
179
+ report_path.parent.mkdir(parents=True, exist_ok=True)
180
+ report_path.write_text(json_text + "\n", encoding="utf-8")
181
+ if not as_json:
182
+ console.print(f"[dim]Wrote integration report to[/dim] {report_path}")
183
+ if as_json:
184
+ typer.echo(json_text)
185
+ if not write_json or not as_json:
186
+ console.print(table)
187
+
188
+ if report.failures:
189
+ if not as_json:
190
+ console.print(
191
+ f"\n[yellow]Fix failed checks, then run:[/yellow] {PREFERRED_COMMAND} all --apply "
192
+ f"(or {legacy_command} --apply)."
193
+ )
194
+ raise typer.Exit(code=1)
195
+
196
+ if not as_json:
197
+ console.print(f"\n[green]Ready.[/green] Run: {PREFERRED_COMMAND} all --apply (or {legacy_command} --apply).")
@@ -0,0 +1 @@
1
+ """MCP tool handler modules extracted from server.call_tool."""
@@ -0,0 +1,77 @@
1
+ """AST match and LSP status MCP tool handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from mcp.types import TextContent
9
+
10
+ from devcouncil.indexing.ast_matcher import AstMatcher
11
+ from devcouncil.indexing.lsp import LspInspector
12
+ from devcouncil.integrations.mcp.util import (
13
+ error_text,
14
+ int_argument,
15
+ optional_string_argument,
16
+ )
17
+
18
+ _AST_MATCHER_CACHE: dict[str, AstMatcher] = {}
19
+ _LSP_INSPECTOR_CACHE: dict[str, LspInspector] = {}
20
+
21
+
22
+ def reset_caches() -> None:
23
+ """Drop per-root AST/LSP caches (for test isolation)."""
24
+ _AST_MATCHER_CACHE.clear()
25
+ _LSP_INSPECTOR_CACHE.clear()
26
+
27
+
28
+ def get_ast_matcher(root: Path) -> AstMatcher:
29
+ key = str(root.resolve())
30
+ matcher = _AST_MATCHER_CACHE.get(key)
31
+ if matcher is None:
32
+ matcher = AstMatcher(root)
33
+ _AST_MATCHER_CACHE[key] = matcher
34
+ return matcher
35
+
36
+
37
+ def get_lsp_inspector(root: Path) -> LspInspector:
38
+ key = str(root.resolve())
39
+ inspector = _LSP_INSPECTOR_CACHE.get(key)
40
+ if inspector is None:
41
+ inspector = LspInspector(root)
42
+ _LSP_INSPECTOR_CACHE[key] = inspector
43
+ return inspector
44
+
45
+
46
+ async def handle_lsp_status(root: Path, arguments: dict) -> list[TextContent]:
47
+ _ = arguments
48
+ client_enabled = False
49
+ try:
50
+ from devcouncil.indexing.lsp_client import lsp_refs_enabled
51
+
52
+ client_enabled = lsp_refs_enabled(root)
53
+ except Exception:
54
+ client_enabled = False
55
+ return [
56
+ TextContent(
57
+ type="text",
58
+ text=get_lsp_inspector(root).summary_json(client_enabled=client_enabled),
59
+ )
60
+ ]
61
+
62
+
63
+ async def handle_ast_match(root: Path, arguments: dict) -> list[TextContent]:
64
+ query = optional_string_argument(arguments, "query")
65
+ language = optional_string_argument(arguments, "language")
66
+ kind = optional_string_argument(arguments, "kind")
67
+ for arg_name, value in [("query", query), ("language", language), ("kind", kind)]:
68
+ if value == "":
69
+ return error_text(f"{arg_name} must be a string", code="invalid_arguments", argument=arg_name)
70
+ limit = int_argument(arguments, "limit", 100, minimum=1, maximum=500)
71
+ matches = get_ast_matcher(root).match(
72
+ query=query or "",
73
+ language=language,
74
+ kind=kind,
75
+ limit=limit,
76
+ )
77
+ return [TextContent(type="text", text=json.dumps({"matches": [item.model_dump() for item in matches]}, indent=2))]
@@ -0,0 +1,50 @@
1
+ """Task checkout MCP tool handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Callable
7
+
8
+ from mcp.types import TextContent
9
+
10
+ from devcouncil.integrations.mcp.util import (
11
+ error_text,
12
+ json_text,
13
+ optional_string_argument,
14
+ parse_cli_json,
15
+ required_string_argument,
16
+ run_cli_command,
17
+ )
18
+
19
+
20
+ async def handle_checkout_task(
21
+ root: Path,
22
+ db: object,
23
+ arguments: dict,
24
+ *,
25
+ load_router: Callable[[Path], object | None],
26
+ ) -> list[TextContent]:
27
+ del db, load_router # routed through CLI service layer
28
+ task_id, arg_error = required_string_argument(arguments, "task_id")
29
+ if arg_error:
30
+ return arg_error
31
+ client_id, arg_error = required_string_argument(arguments, "client_id")
32
+ if arg_error:
33
+ return arg_error
34
+ assert task_id is not None and client_id is not None
35
+ agent = optional_string_argument(arguments, "agent")
36
+ if agent == "":
37
+ return error_text("agent must be a string", code="invalid_arguments", argument="agent")
38
+ force_value = arguments.get("force", False)
39
+ if not isinstance(force_value, bool):
40
+ return error_text("force must be a boolean", code="invalid_arguments", argument="force")
41
+ cli_args = ["checkout", task_id, "--client-id", client_id, "--json"]
42
+ if agent:
43
+ cli_args.extend(["--agent", agent])
44
+ if force_value:
45
+ cli_args.append("--force")
46
+ payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
47
+ if cli_error:
48
+ return cli_error
49
+ assert payload is not None
50
+ return json_text(payload)
@@ -0,0 +1,43 @@
1
+ """Gated devcouncil_cli MCP tool handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from mcp.types import TextContent
8
+
9
+ from devcouncil.integrations.mcp.util import error_text, json_text, run_cli_command
10
+
11
+ CLI_ALLOWED_ROOTS = frozenset({
12
+ "status", "tasks", "report", "map", "prompt", "show", "trace", "lsp", "ast",
13
+ "verify", "verify-leased", "rollback", "gaps", "doctor", "cost", "check", "export", "requirements",
14
+ "runs", "logs", "watch", "go", "wiki", "okf", "graph-context", "provenance", "resource",
15
+ "checkout", "release", "lease", "write", "apply-patch",
16
+ "next-task", "scope", "policy-check", "record-command", "run-cmd",
17
+ "evidence-append", "evidence-list", "handoff-leased",
18
+ })
19
+ CLI_FORBIDDEN_FLAGS = frozenset({"--project-root", "--github", "--github-pr-comment", "--gitlab-pr-comment"})
20
+
21
+
22
+ def forbidden_cli_flags(args: list[str]) -> list[str]:
23
+ forbidden: set[str] = set()
24
+ for arg in args:
25
+ for flag in CLI_FORBIDDEN_FLAGS:
26
+ if arg == flag or arg.startswith(f"{flag}="):
27
+ forbidden.add(flag)
28
+ return sorted(forbidden)
29
+
30
+
31
+ async def handle_cli(root: Path, arguments: dict) -> list[TextContent]:
32
+ args = arguments.get("args")
33
+ if not isinstance(args, list) or not all(isinstance(arg, str) for arg in args) or not args:
34
+ return error_text("args must be a non-empty string array", code="invalid_arguments")
35
+ if args[0] not in CLI_ALLOWED_ROOTS:
36
+ return error_text(f"command {args[0]} is not allowed through MCP", code="command_not_allowed", command=args[0])
37
+ forbidden = forbidden_cli_flags(args)
38
+ if forbidden:
39
+ return error_text("forbidden flag(s) through MCP: " + ", ".join(forbidden), code="forbidden_flags", flags=forbidden)
40
+ try:
41
+ return json_text(run_cli_command(args, root, truncate=True))
42
+ except Exception as exc:
43
+ return error_text(str(exc), code="cli_execution_error")
@@ -0,0 +1,182 @@
1
+ """Registry-backed MCP tools for transactional code intelligence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from pathlib import Path
7
+ from typing import Awaitable, Callable
8
+
9
+ from mcp.types import TextContent, Tool
10
+
11
+ from devcouncil.codeintel.query import CodeIntelQueryEngine
12
+ from devcouncil.codeintel.service import canonical_project_root, get_codeintel_service
13
+ from devcouncil.codeintel.sync import get_sync_coordinator
14
+ from devcouncil.integrations.mcp.util import error_text, json_text, with_codeintel_freshness
15
+
16
+ Handler = Callable[[Path, dict], Awaitable[list[TextContent]]]
17
+
18
+
19
+ def _schema(properties: dict, required: list[str] | None = None) -> dict:
20
+ schema: dict = {"type": "object", "properties": {
21
+ "projectPath": {
22
+ "type": "string",
23
+ "description": "Explicit repository path; defaults to the MCP server project.",
24
+ },
25
+ **properties,
26
+ }}
27
+ if required:
28
+ schema["required"] = required
29
+ return schema
30
+
31
+
32
+ def tools() -> list[Tool]:
33
+ return [
34
+ Tool(
35
+ name="devcouncil_code_explore",
36
+ description="Unified code exploration: source, callers/callees, semantic hops, and blast radius.",
37
+ inputSchema=_schema({
38
+ "query": {"type": "string"},
39
+ "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 20},
40
+ }, ["query"]),
41
+ ),
42
+ Tool(
43
+ name="devcouncil_code_search",
44
+ description="FTS5 symbol, qualified-name, and path search over the committed generation.",
45
+ inputSchema=_schema({
46
+ "query": {"type": "string"},
47
+ "limit": {"type": "integer", "minimum": 1, "maximum": 500, "default": 50},
48
+ }, ["query"]),
49
+ ),
50
+ Tool(
51
+ name="devcouncil_code_path",
52
+ description="Shortest call/import/framework path with confidence and provenance per hop.",
53
+ inputSchema=_schema({
54
+ "from": {"type": "string"},
55
+ "to": {"type": "string"},
56
+ "maxDepth": {"type": "integer", "minimum": 1, "maximum": 64, "default": 32},
57
+ }, ["from", "to"]),
58
+ ),
59
+ Tool(
60
+ name="devcouncil_code_impact",
61
+ description="Inbound symbol blast radius for one or more paths/symbols.",
62
+ inputSchema=_schema({
63
+ "targets": {"type": "array", "items": {"type": "string"}},
64
+ "maxDepth": {"type": "integer", "minimum": 1, "maximum": 8, "default": 3},
65
+ }, ["targets"]),
66
+ ),
67
+ Tool(
68
+ name="devcouncil_code_dead",
69
+ description="Confidence-tiered dead-code candidates; never deletes code.",
70
+ inputSchema=_schema({
71
+ "minimumConfidence": {
72
+ "type": "string",
73
+ "enum": ["extracted", "inferred", "ambiguous"],
74
+ "default": "inferred",
75
+ },
76
+ }),
77
+ ),
78
+ Tool(
79
+ name="devcouncil_code_affected_tests",
80
+ description="Tests reachable through the inbound blast radius of paths/symbols.",
81
+ inputSchema=_schema({
82
+ "targets": {"type": "array", "items": {"type": "string"}},
83
+ "maxDepth": {"type": "integer", "minimum": 1, "maximum": 8, "default": 3},
84
+ }, ["targets"]),
85
+ ),
86
+ Tool(
87
+ name="devcouncil_code_sync",
88
+ description="Reconcile and commit pending source changes to the canonical index.",
89
+ inputSchema=_schema({
90
+ "paths": {"type": "array", "items": {"type": "string"}},
91
+ }),
92
+ ),
93
+ Tool(
94
+ name="devcouncil_code_status",
95
+ description="Canonical generation, native watcher backend, pending files, and degraded state.",
96
+ inputSchema=_schema({}),
97
+ ),
98
+ ]
99
+
100
+
101
+ def resolve_root(default_root: Path, arguments: dict) -> Path:
102
+ explicit = arguments.get("projectPath")
103
+ return canonical_project_root(Path(explicit) if isinstance(explicit, str) and explicit else default_root)
104
+
105
+
106
+ async def _explore(root: Path, arguments: dict) -> list[TextContent]:
107
+ return json_text(CodeIntelQueryEngine(root).explore(str(arguments["query"]), limit=int(arguments.get("limit", 20))))
108
+
109
+
110
+ async def _search(root: Path, arguments: dict) -> list[TextContent]:
111
+ return json_text(CodeIntelQueryEngine(root).search(str(arguments["query"]), limit=int(arguments.get("limit", 50))))
112
+
113
+
114
+ async def _path(root: Path, arguments: dict) -> list[TextContent]:
115
+ return json_text(CodeIntelQueryEngine(root).path(
116
+ str(arguments["from"]), str(arguments["to"]), max_depth=int(arguments.get("maxDepth", 32))
117
+ ))
118
+
119
+
120
+ async def _impact(root: Path, arguments: dict) -> list[TextContent]:
121
+ return json_text(CodeIntelQueryEngine(root).impact(
122
+ [str(value) for value in arguments.get("targets") or []],
123
+ max_depth=int(arguments.get("maxDepth", 3)),
124
+ ))
125
+
126
+
127
+ async def _dead(root: Path, arguments: dict) -> list[TextContent]:
128
+ return json_text(CodeIntelQueryEngine(root).dead(
129
+ minimum_confidence=str(arguments.get("minimumConfidence", "inferred"))
130
+ ))
131
+
132
+
133
+ async def _affected(root: Path, arguments: dict) -> list[TextContent]:
134
+ return json_text(CodeIntelQueryEngine(root).affected_tests(
135
+ [str(value) for value in arguments.get("targets") or []],
136
+ max_depth=int(arguments.get("maxDepth", 3)),
137
+ ))
138
+
139
+
140
+ async def _sync(root: Path, arguments: dict) -> list[TextContent]:
141
+ coordinator = get_sync_coordinator(root)
142
+ supplied = [str(value) for value in arguments.get("paths") or []]
143
+ changed = supplied or await asyncio.to_thread(coordinator.reconcile)
144
+ ok = await asyncio.to_thread(coordinator.sync_now, changed)
145
+ payload = {"ok": ok, "reconciled": changed, **coordinator.status().as_dict()}
146
+ return json_text(payload) if ok else error_text(
147
+ coordinator.status().last_error or coordinator.status().degraded_reason or "sync failed",
148
+ code="codeintel_sync_failed",
149
+ **payload,
150
+ )
151
+
152
+
153
+ async def _status(root: Path, _arguments: dict) -> list[TextContent]:
154
+ service = get_codeintel_service(root)
155
+ return json_text({**service.status(), "sync": get_sync_coordinator(root).status().as_dict()})
156
+
157
+
158
+ REGISTRY: dict[str, Handler] = {
159
+ "devcouncil_code_explore": _explore,
160
+ "devcouncil_code_search": _search,
161
+ "devcouncil_code_path": _path,
162
+ "devcouncil_code_impact": _impact,
163
+ "devcouncil_code_dead": _dead,
164
+ "devcouncil_code_affected_tests": _affected,
165
+ "devcouncil_code_sync": _sync,
166
+ "devcouncil_code_status": _status,
167
+ }
168
+
169
+
170
+ async def dispatch(name: str, default_root: Path, arguments: dict) -> list[TextContent] | None:
171
+ handler = REGISTRY.get(name)
172
+ if handler is None:
173
+ return None
174
+ root = resolve_root(default_root, arguments)
175
+ try:
176
+ if name in {"devcouncil_code_sync", "devcouncil_code_status"}:
177
+ return await handler(root, arguments)
178
+ return await with_codeintel_freshness(root, lambda: handler(root, arguments))
179
+ except FileNotFoundError as exc:
180
+ return error_text(str(exc), code="codeintel_not_initialized", project_root=str(root))
181
+ except (KeyError, TypeError, ValueError) as exc:
182
+ return error_text(str(exc), code="invalid_arguments", tool=name)