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
@@ -0,0 +1,236 @@
1
+ """Registry-backed MCP debugger and runtime-trace tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Awaitable, Callable
7
+
8
+ from mcp.types import TextContent, Tool
9
+
10
+ from devcouncil.codeintel.debug.consent import require_debug_consent, set_debug_consent
11
+ from devcouncil.codeintel.debug.discovery import adapter_by_id, discover_adapters
12
+ from devcouncil.codeintel.debug.session import get_debug_manager
13
+ from devcouncil.codeintel.debug.tracing import NodeCpuProfileProvider, PythonTraceProvider, import_runtime_trace
14
+ from devcouncil.codeintel.service import canonical_project_root
15
+ from devcouncil.integrations.mcp.util import error_text, json_text
16
+
17
+ Handler = Callable[[Path, dict], Awaitable[list[TextContent]]]
18
+
19
+
20
+ def _schema(properties: dict, required: list[str] | None = None) -> dict:
21
+ schema: dict = {"type": "object", "properties": {
22
+ "projectPath": {"type": "string", "description": "Explicit repository path."},
23
+ **properties,
24
+ }}
25
+ if required:
26
+ schema["required"] = required
27
+ return schema
28
+
29
+
30
+ def tools() -> list[Tool]:
31
+ return [
32
+ Tool(
33
+ name="devcouncil_debug_discover",
34
+ description=(
35
+ "Discover installed DAP adapters after explicit one-time consent; "
36
+ "returns paths, versions, hashes, and launch/attach support."
37
+ ),
38
+ inputSchema=_schema({"consent": {"type": "boolean", "default": False}}),
39
+ ),
40
+ Tool(
41
+ name="devcouncil_debug_start",
42
+ description="Launch or attach a capability-negotiated DAP session.",
43
+ inputSchema=_schema({
44
+ "adapterId": {"type": "string"},
45
+ "adapterCommand": {"type": "array", "items": {"type": "string"}},
46
+ "request": {"type": "string", "enum": ["launch", "attach"], "default": "launch"},
47
+ "configuration": {"type": "object"},
48
+ "initialBreakpoints": {
49
+ "type": "object",
50
+ "additionalProperties": {"type": "array", "items": {"type": "integer"}},
51
+ },
52
+ "timeout": {"type": "number", "minimum": 1, "maximum": 120, "default": 30},
53
+ }),
54
+ ),
55
+ Tool(
56
+ name="devcouncil_debug_breakpoints",
57
+ description="Replace all breakpoints for one source in a DAP session.",
58
+ inputSchema=_schema({
59
+ "sessionId": {"type": "string"},
60
+ "source": {"type": "string"},
61
+ "lines": {"type": "array", "items": {"type": "integer"}},
62
+ }, ["sessionId", "source", "lines"]),
63
+ ),
64
+ Tool(
65
+ name="devcouncil_debug_control",
66
+ description="Continue, pause, or step a DAP session.",
67
+ inputSchema=_schema({
68
+ "sessionId": {"type": "string"},
69
+ "action": {"type": "string", "enum": ["continue", "pause", "next", "stepIn", "stepOut"]},
70
+ "threadId": {"type": "integer"},
71
+ }, ["sessionId", "action"]),
72
+ ),
73
+ Tool(
74
+ name="devcouncil_debug_inspect",
75
+ description="Inspect threads, stack frames, scopes, variables, source, or disassembly.",
76
+ inputSchema=_schema({
77
+ "sessionId": {"type": "string"},
78
+ "operation": {"type": "string", "enum": ["threads", "stackTrace", "scopes", "variables", "source", "disassemble"]},
79
+ "arguments": {"type": "object"},
80
+ }, ["sessionId", "operation"]),
81
+ ),
82
+ Tool(
83
+ name="devcouncil_debug_evaluate",
84
+ description="Side-effectful DAP evaluate; allowSideEffects must be explicitly true.",
85
+ inputSchema=_schema({
86
+ "sessionId": {"type": "string"},
87
+ "expression": {"type": "string"},
88
+ "frameId": {"type": "integer"},
89
+ "allowSideEffects": {"type": "boolean", "const": True},
90
+ }, ["sessionId", "expression", "allowSideEffects"]),
91
+ ),
92
+ Tool(
93
+ name="devcouncil_debug_trace",
94
+ description="Capture a DAP stack, run exact Python tracing, or import JSONL/Node CPU profile evidence.",
95
+ inputSchema=_schema({
96
+ "provider": {"type": "string", "enum": ["dap-stack", "python", "node", "import"]},
97
+ "sessionId": {"type": "string"},
98
+ "threadId": {"type": "integer"},
99
+ "script": {"type": "string"},
100
+ "args": {"type": "array", "items": {"type": "string"}},
101
+ "path": {"type": "string"},
102
+ }, ["provider"]),
103
+ ),
104
+ Tool(
105
+ name="devcouncil_debug_stop",
106
+ description="Disconnect a DAP session and optionally leave the debuggee running.",
107
+ inputSchema=_schema({
108
+ "sessionId": {"type": "string"},
109
+ "terminateDebuggee": {"type": "boolean", "default": True},
110
+ }, ["sessionId"]),
111
+ ),
112
+ ]
113
+
114
+
115
+ async def _discover(root: Path, arguments: dict) -> list[TextContent]:
116
+ if arguments.get("consent") is True:
117
+ set_debug_consent(root, True)
118
+ require_debug_consent(root)
119
+ return json_text({"consent": True, "adapters": [adapter.as_dict() for adapter in discover_adapters()]})
120
+
121
+
122
+ def _command(arguments: dict) -> list[str]:
123
+ supplied = arguments.get("adapterCommand")
124
+ if isinstance(supplied, list) and supplied:
125
+ return [str(value) for value in supplied]
126
+ adapter_id = str(arguments.get("adapterId") or "debugpy")
127
+ adapter = adapter_by_id(adapter_id)
128
+ if adapter is None:
129
+ raise ValueError(f"adapter {adapter_id!r} was not discovered")
130
+ return list(adapter.command)
131
+
132
+
133
+ async def _start(root: Path, arguments: dict) -> list[TextContent]:
134
+ require_debug_consent(root)
135
+ session = get_debug_manager().start(
136
+ root,
137
+ _command(arguments),
138
+ request=str(arguments.get("request", "launch")),
139
+ arguments=dict(arguments.get("configuration") or {}),
140
+ initial_breakpoints={
141
+ str(source): [int(line) for line in lines]
142
+ for source, lines in dict(arguments.get("initialBreakpoints") or {}).items()
143
+ },
144
+ timeout=float(arguments.get("timeout", 30.0)),
145
+ )
146
+ return json_text(session.as_dict())
147
+
148
+
149
+ async def _breakpoints(root: Path, arguments: dict) -> list[TextContent]:
150
+ require_debug_consent(root)
151
+ return json_text(get_debug_manager().set_breakpoints(
152
+ str(arguments["sessionId"]), str(arguments["source"]), [int(value) for value in arguments["lines"]]
153
+ ))
154
+
155
+
156
+ async def _control(root: Path, arguments: dict) -> list[TextContent]:
157
+ require_debug_consent(root)
158
+ return json_text(get_debug_manager().control(
159
+ str(arguments["sessionId"]),
160
+ str(arguments["action"]),
161
+ thread_id=int(arguments["threadId"]) if arguments.get("threadId") is not None else None,
162
+ ))
163
+
164
+
165
+ async def _inspect(root: Path, arguments: dict) -> list[TextContent]:
166
+ require_debug_consent(root)
167
+ return json_text(get_debug_manager().inspect(
168
+ str(arguments["sessionId"]), str(arguments["operation"]), dict(arguments.get("arguments") or {})
169
+ ))
170
+
171
+
172
+ async def _evaluate(root: Path, arguments: dict) -> list[TextContent]:
173
+ require_debug_consent(root)
174
+ if arguments.get("allowSideEffects") is not True:
175
+ raise PermissionError("debug evaluate requires allowSideEffects=true")
176
+ return json_text(get_debug_manager().evaluate(
177
+ str(arguments["sessionId"]),
178
+ str(arguments["expression"]),
179
+ frame_id=int(arguments["frameId"]) if arguments.get("frameId") is not None else None,
180
+ allow_side_effects=True,
181
+ ))
182
+
183
+
184
+ async def _trace(root: Path, arguments: dict) -> list[TextContent]:
185
+ require_debug_consent(root)
186
+ provider = str(arguments["provider"])
187
+ if provider == "dap-stack":
188
+ return json_text(get_debug_manager().capture_stack(
189
+ str(arguments["sessionId"]), thread_id=int(arguments["threadId"])
190
+ ))
191
+ if provider == "python":
192
+ return json_text(PythonTraceProvider(root).run(
193
+ Path(str(arguments["script"])), [str(value) for value in arguments.get("args") or []]
194
+ ))
195
+ if provider == "node":
196
+ return json_text(NodeCpuProfileProvider(root).run(
197
+ Path(str(arguments["script"])), [str(value) for value in arguments.get("args") or []]
198
+ ))
199
+ if provider == "import":
200
+ return json_text(import_runtime_trace(root, Path(str(arguments["path"]))))
201
+ raise ValueError(f"unsupported trace provider: {provider}")
202
+
203
+
204
+ async def _stop(root: Path, arguments: dict) -> list[TextContent]:
205
+ require_debug_consent(root)
206
+ session_id = str(arguments["sessionId"])
207
+ get_debug_manager().stop(session_id, terminate_debuggee=bool(arguments.get("terminateDebuggee", True)))
208
+ return json_text({"stopped": session_id})
209
+
210
+
211
+ REGISTRY: dict[str, Handler] = {
212
+ "devcouncil_debug_discover": _discover,
213
+ "devcouncil_debug_start": _start,
214
+ "devcouncil_debug_breakpoints": _breakpoints,
215
+ "devcouncil_debug_control": _control,
216
+ "devcouncil_debug_inspect": _inspect,
217
+ "devcouncil_debug_evaluate": _evaluate,
218
+ "devcouncil_debug_trace": _trace,
219
+ "devcouncil_debug_stop": _stop,
220
+ }
221
+
222
+
223
+ async def dispatch(name: str, default_root: Path, arguments: dict) -> list[TextContent] | None:
224
+ handler = REGISTRY.get(name)
225
+ if handler is None:
226
+ return None
227
+ explicit = arguments.get("projectPath")
228
+ root = canonical_project_root(Path(explicit) if isinstance(explicit, str) and explicit else default_root)
229
+ try:
230
+ return await handler(root, arguments)
231
+ except PermissionError as exc:
232
+ return error_text(str(exc), code="debug_consent_required", tool=name)
233
+ except FileNotFoundError as exc:
234
+ return error_text(str(exc), code="not_found", tool=name)
235
+ except (KeyError, TypeError, ValueError, RuntimeError, TimeoutError) as exc:
236
+ return error_text(str(exc), code="debug_error", tool=name)
@@ -0,0 +1,70 @@
1
+ """Evidence read/write MCP tool handlers."""
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 (
10
+ error_text,
11
+ int_argument,
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_append_evidence(root: Path, db: object, arguments: dict) -> list[TextContent]:
21
+ del db # routed through CLI service layer
22
+ task_id, arg_error = required_string_argument(arguments, "task_id")
23
+ if arg_error:
24
+ return arg_error
25
+ lease_token, arg_error = required_string_argument(arguments, "lease_token")
26
+ if arg_error:
27
+ return arg_error
28
+ command, arg_error = required_string_argument(arguments, "command")
29
+ if arg_error:
30
+ return arg_error
31
+ summary_text, arg_error = required_string_argument(arguments, "summary")
32
+ if arg_error:
33
+ return arg_error
34
+ assert task_id is not None and lease_token is not None
35
+ exit_code = arguments.get("exit_code", 0)
36
+ if not isinstance(exit_code, int) or isinstance(exit_code, bool):
37
+ return error_text("exit_code must be an integer", code="invalid_arguments")
38
+ cli_args = [
39
+ "evidence-append", task_id,
40
+ "--lease-token", lease_token,
41
+ "--command", command or "",
42
+ "--summary", summary_text or "",
43
+ "--exit-code", str(exit_code),
44
+ "--json",
45
+ ]
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)
51
+
52
+
53
+ async def handle_get_evidence(root: Path, db: object, arguments: dict) -> list[TextContent]:
54
+ del db # routed through CLI service layer
55
+ task_id, arg_error = required_string_argument(arguments, "task_id")
56
+ if arg_error:
57
+ return arg_error
58
+ assert task_id is not None
59
+ command_filter = optional_string_argument(arguments, "command")
60
+ if command_filter == "":
61
+ return error_text("command must be a string", code="invalid_arguments", argument="command")
62
+ limit = int_argument(arguments, "limit", 20, minimum=1, maximum=100)
63
+ cli_args = ["evidence-list", task_id, "--limit", str(limit), "--json"]
64
+ if command_filter:
65
+ cli_args.extend(["--command", command_filter])
66
+ payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
67
+ if cli_error:
68
+ return cli_error
69
+ assert payload is not None
70
+ return json_text(payload)
@@ -0,0 +1,281 @@
1
+ """Git diff MCP tool handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ from mcp.types import TextContent
10
+
11
+ from devcouncil.integrations.mcp.util import (
12
+ CLI_TIMEOUT_SECONDS,
13
+ error_text,
14
+ is_git_repo,
15
+ json_text,
16
+ optional_string_argument,
17
+ optional_string_list_argument,
18
+ truncate_text,
19
+ )
20
+ from devcouncil.storage.db import Database
21
+ from devcouncil.storage.repositories import TaskRepository
22
+
23
+ _BINARY_PROBE_BYTES = 8192
24
+
25
+
26
+ def _run_git(root: Path, args: list[str]) -> subprocess.CompletedProcess[str]:
27
+ return subprocess.run(
28
+ args,
29
+ cwd=root,
30
+ capture_output=True,
31
+ text=True,
32
+ encoding="utf-8",
33
+ errors="replace",
34
+ timeout=CLI_TIMEOUT_SECONDS,
35
+ )
36
+
37
+
38
+ def _parse_name_status_z(data: str) -> dict[str, str]:
39
+ """Parse ``git diff --name-status -z`` into ``{path: status}``.
40
+
41
+ Rename/copy records are ``STATUS\\0OLD\\0NEW\\0``; other records are
42
+ ``STATUS\\0PATH\\0``. The new path is the authoritative key for renames.
43
+ """
44
+ status_by_path: dict[str, str] = {}
45
+ parts = data.split("\0")
46
+ i = 0
47
+ while i < len(parts):
48
+ status = parts[i]
49
+ if not status:
50
+ i += 1
51
+ continue
52
+ kind = status[0]
53
+ if kind in "RC" and i + 2 < len(parts):
54
+ new_path = parts[i + 2].replace("\\", "/")
55
+ if new_path:
56
+ status_by_path[new_path] = status
57
+ i += 3
58
+ continue
59
+ if i + 1 < len(parts):
60
+ path = parts[i + 1].replace("\\", "/")
61
+ if path:
62
+ status_by_path[path] = status
63
+ i += 2
64
+ continue
65
+ break
66
+ return status_by_path
67
+
68
+
69
+ def _format_untracked_file_diff(rel_path: str, full_path: Path) -> tuple[str, int]:
70
+ """Return ``(unified_diff_fragment, addition_count)`` for a new untracked file."""
71
+ try:
72
+ raw = full_path.read_bytes()
73
+ except OSError:
74
+ return "", 0
75
+
76
+ header = [
77
+ f"diff --git a/{rel_path} b/{rel_path}",
78
+ "new file mode 100644",
79
+ "--- /dev/null",
80
+ f"+++ b/{rel_path}",
81
+ ]
82
+ if b"\0" in raw[:_BINARY_PROBE_BYTES]:
83
+ return "\n".join([*header, f"Binary files /dev/null and b/{rel_path} differ"]), 0
84
+
85
+ text = raw.decode("utf-8", errors="replace")
86
+ if not text:
87
+ return "\n".join(header) + "\n", 0
88
+
89
+ lines = text.splitlines()
90
+ if text.endswith(("\n", "\r")):
91
+ line_count = len(lines)
92
+ else:
93
+ line_count = max(len(lines), 1)
94
+
95
+ diff_lines = [*header, f"@@ -0,0 +1,{line_count} @@"]
96
+ diff_lines.extend(f"+{line}" for line in lines)
97
+ return "\n".join(diff_lines), line_count
98
+
99
+
100
+ def _list_untracked(root: Path, paths: list[str]) -> tuple[list[str], str | None]:
101
+ """List untracked paths; return ``(paths, error)`` when the Git call fails."""
102
+ args = ["git", "ls-files", "--others", "--exclude-standard", "-z"]
103
+ if paths:
104
+ args.append("--")
105
+ args.extend(paths)
106
+ try:
107
+ proc = _run_git(root, args)
108
+ except (OSError, subprocess.TimeoutExpired) as exc:
109
+ return [], str(exc)
110
+ if proc.returncode != 0:
111
+ detail = (proc.stderr or proc.stdout or f"git ls-files exited {proc.returncode}").strip()
112
+ return [], detail or f"git ls-files exited {proc.returncode}"
113
+ return [p.replace("\\", "/") for p in proc.stdout.split("\0") if p.strip()], None
114
+
115
+
116
+ def _collect_untracked(
117
+ root: Path,
118
+ paths: list[str],
119
+ *,
120
+ known_paths: set[str],
121
+ ) -> tuple[list[dict[str, object]], str, str | None]:
122
+ """Build file entries and unified diff for untracked files in scope."""
123
+ untracked, err = _list_untracked(root, paths)
124
+ if err is not None:
125
+ return [], "", err
126
+
127
+ files: list[dict[str, object]] = []
128
+ fragments: list[str] = []
129
+ for rel in untracked:
130
+ if rel in known_paths:
131
+ continue
132
+ full = root / rel
133
+ if not full.is_file():
134
+ continue
135
+ fragment, additions = _format_untracked_file_diff(rel, full)
136
+ if not fragment:
137
+ continue
138
+ files.append({
139
+ "path": rel,
140
+ "status": "A",
141
+ "additions": additions,
142
+ "deletions": 0,
143
+ })
144
+ fragments.append(fragment.rstrip("\n"))
145
+ unified = "\n".join(fragments)
146
+ if unified:
147
+ unified += "\n"
148
+ return files, unified, None
149
+
150
+
151
+ async def git_diff(root: Path, paths: list[str], staged: bool) -> dict[str, object]:
152
+ """Compute a (optionally path-scoped, optionally staged) git diff."""
153
+ diff_args = ["git", "diff"]
154
+ numstat_args = ["git", "diff", "--numstat"]
155
+ namestatus_args = ["git", "diff", "--name-status", "-z"]
156
+ if staged:
157
+ for args in (diff_args, numstat_args, namestatus_args):
158
+ args.append("--cached")
159
+ if paths:
160
+ for args in (diff_args, numstat_args, namestatus_args):
161
+ args.append("--")
162
+ args.extend(paths)
163
+
164
+ def _run(args: list[str]) -> subprocess.CompletedProcess[str]:
165
+ return _run_git(root, args)
166
+
167
+ try:
168
+ loop = asyncio.get_event_loop()
169
+ diff_proc, numstat_proc, namestatus_proc = await asyncio.gather(
170
+ loop.run_in_executor(None, _run, diff_args),
171
+ loop.run_in_executor(None, _run, numstat_args),
172
+ loop.run_in_executor(None, _run, namestatus_args),
173
+ )
174
+ except (OSError, subprocess.TimeoutExpired) as exc:
175
+ return {"ok": False, "files": [], "unified_diff": "", "truncated": False, "error": str(exc)}
176
+
177
+ for proc, label in (
178
+ (diff_proc, "git diff"),
179
+ (numstat_proc, "git diff --numstat"),
180
+ (namestatus_proc, "git diff --name-status"),
181
+ ):
182
+ if proc.returncode != 0:
183
+ detail = (proc.stderr or proc.stdout or f"{label} exited {proc.returncode}").strip()
184
+ return {
185
+ "ok": False,
186
+ "files": [],
187
+ "unified_diff": "",
188
+ "truncated": False,
189
+ "error": detail or f"{label} exited {proc.returncode}",
190
+ "staged": staged,
191
+ }
192
+
193
+ status_by_path = _parse_name_status_z(namestatus_proc.stdout)
194
+
195
+ files: list[dict[str, object]] = []
196
+ for line in numstat_proc.stdout.splitlines():
197
+ parts = line.split("\t")
198
+ if len(parts) < 3:
199
+ continue
200
+ added_str, deleted_str, file_path = parts[0], parts[1], parts[-1]
201
+ # Rename numstat without -z may use "old => new"; prefer the new side.
202
+ if " => " in file_path:
203
+ file_path = file_path.split(" => ", 1)[-1]
204
+ file_path = file_path.replace("\\", "/")
205
+ files.append({
206
+ "path": file_path,
207
+ "status": status_by_path.get(file_path, "M"),
208
+ "additions": int(added_str) if added_str.isdigit() else 0,
209
+ "deletions": int(deleted_str) if deleted_str.isdigit() else 0,
210
+ })
211
+
212
+ unified_parts = [diff_proc.stdout.rstrip("\n")] if diff_proc.stdout else []
213
+ if not staged:
214
+ known = {str(entry["path"]) for entry in files}
215
+ untracked_files, untracked_diff, untracked_err = await asyncio.get_event_loop().run_in_executor(
216
+ None,
217
+ lambda: _collect_untracked(root, paths, known_paths=known),
218
+ )
219
+ if untracked_err is not None:
220
+ return {
221
+ "ok": False,
222
+ "files": [],
223
+ "unified_diff": "",
224
+ "truncated": False,
225
+ "error": untracked_err,
226
+ "staged": staged,
227
+ }
228
+ files.extend(untracked_files)
229
+ if untracked_diff:
230
+ unified_parts.append(untracked_diff.rstrip("\n"))
231
+
232
+ combined = "\n".join(part for part in unified_parts if part)
233
+ if combined:
234
+ combined += "\n"
235
+ unified_diff, truncated = truncate_text(combined)
236
+ return {"ok": True, "files": files, "unified_diff": unified_diff, "truncated": truncated, "staged": staged}
237
+
238
+
239
+ def _empty_diff_payload(staged: bool) -> dict[str, object]:
240
+ return {"ok": True, "files": [], "unified_diff": "", "truncated": False, "staged": staged}
241
+
242
+
243
+ async def handle_get_diff(root: Path, db: Database | None, arguments: dict) -> list[TextContent]:
244
+ if not is_git_repo(root):
245
+ return error_text("get_diff requires a git repository.", code="not_a_git_repo")
246
+ task_id = optional_string_argument(arguments, "task_id")
247
+ if task_id == "":
248
+ return error_text("task_id must be a string", code="invalid_arguments", argument="task_id")
249
+ explicit_paths, arg_error = optional_string_list_argument(arguments, "paths")
250
+ if arg_error:
251
+ return arg_error
252
+ staged_value = arguments.get("staged", False)
253
+ if not isinstance(staged_value, bool):
254
+ return error_text("staged must be a boolean", code="invalid_arguments", argument="staged")
255
+
256
+ scope_paths: list[str] = [p.replace("\\", "/") for p in explicit_paths]
257
+ task_scoped = False
258
+ if task_id:
259
+ if db is None:
260
+ return error_text(
261
+ "DevCouncil not initialized in this directory.",
262
+ code="not_initialized",
263
+ )
264
+ with db.get_session() as session:
265
+ task = TaskRepository(session).get_by_id(task_id)
266
+ if task is None:
267
+ return error_text(f"Task {task_id} not found.", code="not_found", task_id=task_id)
268
+ planned = [pf.path.replace("\\", "/") for pf in task.planned_files]
269
+ planned_set = set(planned)
270
+ task_scoped = True
271
+ if explicit_paths:
272
+ # Intersect only — explicit paths must never broaden task scope.
273
+ scope_paths = [p for p in scope_paths if p in planned_set]
274
+ else:
275
+ scope_paths = planned
276
+
277
+ if task_scoped and not scope_paths:
278
+ # Empty planned scope or empty intersection: fail closed to empty, never full repo.
279
+ return json_text(_empty_diff_payload(staged_value))
280
+
281
+ return json_text(await git_diff(root, scope_paths, staged_value))
@@ -0,0 +1,34 @@
1
+ """Code-review graph context MCP tool handlers."""
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 (
10
+ json_text,
11
+ parse_cli_json,
12
+ run_cli_command,
13
+ )
14
+
15
+
16
+ async def handle_graph_context(root: Path, arguments: dict) -> list[TextContent]:
17
+ files = arguments.get("files", [])
18
+ if not isinstance(files, list):
19
+ files = []
20
+ file_args: list[str] = []
21
+ for item in files:
22
+ if isinstance(item, str) and item:
23
+ file_args.extend(["--file", item])
24
+ payload, _cli_error = parse_cli_json(
25
+ run_cli_command(["graph-context", "--json", *file_args], root, truncate=False),
26
+ )
27
+ if payload is not None:
28
+ return json_text(payload)
29
+ from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
30
+
31
+ context = CodeReviewGraphAdapter(root).get_context(
32
+ [file for file in files if isinstance(file, str)]
33
+ )
34
+ return [TextContent(type="text", text=context.model_dump_json(indent=2))]
@@ -0,0 +1,53 @@
1
+ """Agent handoff 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 (
10
+ json_text,
11
+ parse_cli_json,
12
+ required_string_argument,
13
+ run_cli_command,
14
+ )
15
+
16
+
17
+ async def handle_handoff_agent(root: Path, db: object, arguments: dict) -> list[TextContent]:
18
+ del db # routed through CLI service layer
19
+ task_id, arg_error = required_string_argument(arguments, "task_id")
20
+ if arg_error:
21
+ return arg_error
22
+ lease_token, arg_error = required_string_argument(arguments, "lease_token")
23
+ if arg_error:
24
+ return arg_error
25
+ from_agent, arg_error = required_string_argument(arguments, "from_agent")
26
+ if arg_error:
27
+ return arg_error
28
+ to_agent, arg_error = required_string_argument(arguments, "to_agent")
29
+ if arg_error:
30
+ return arg_error
31
+ assert task_id is not None and lease_token is not None
32
+ cli_args = [
33
+ "handoff-leased", task_id,
34
+ "--lease-token", lease_token,
35
+ "--from", from_agent or "",
36
+ "--to", to_agent or "",
37
+ "--json",
38
+ ]
39
+ instruction = str(arguments.get("instruction") or "")
40
+ if instruction:
41
+ cli_args.extend(["--instruction", instruction])
42
+ payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
43
+ if cli_error:
44
+ return cli_error
45
+ assert payload is not None
46
+ if not payload.get("ok"):
47
+ from devcouncil.integrations.mcp.util import error_text
48
+ return error_text(
49
+ str(payload.get("error") or "handoff failed"),
50
+ code=str(payload.get("code") or "handoff_failed"),
51
+ task_id=task_id,
52
+ )
53
+ return json_text(payload)