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,35 @@
1
+ """Next-task selection 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
+ error_text,
11
+ json_text,
12
+ optional_string_argument,
13
+ parse_cli_json,
14
+ run_cli_command,
15
+ )
16
+
17
+
18
+ async def handle_next_task(root: Path, db: object, arguments: dict) -> list[TextContent]:
19
+ del db # routed through CLI service layer
20
+ status_filter = optional_string_argument(arguments, "status")
21
+ if status_filter == "":
22
+ return error_text("status must be a string", code="invalid_arguments", argument="status")
23
+ client_id = optional_string_argument(arguments, "client_id")
24
+ if client_id == "":
25
+ return error_text("client_id must be a string", code="invalid_arguments", argument="client_id")
26
+ cli_args = ["next-task", "--json"]
27
+ if status_filter:
28
+ cli_args.extend(["--status", status_filter])
29
+ if client_id:
30
+ cli_args.extend(["--client-id", client_id])
31
+ payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
32
+ if cli_error:
33
+ return cli_error
34
+ assert payload is not None
35
+ return json_text(payload)
@@ -0,0 +1,80 @@
1
+ """Policy and command-recording 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
+ json_text,
12
+ optional_string_argument,
13
+ parse_cli_json,
14
+ required_string_argument,
15
+ run_cli_command,
16
+ )
17
+
18
+ RECORD_COMMAND_STATUSES = frozenset({"started", "finished", "failed", "blocked"})
19
+
20
+
21
+ async def handle_policy_check_write(root: Path, db: object, arguments: dict) -> list[TextContent]:
22
+ del db # routed through CLI service layer
23
+ path, arg_error = required_string_argument(arguments, "path")
24
+ if arg_error:
25
+ return arg_error
26
+ assert path is not None
27
+ task_id = optional_string_argument(arguments, "task_id")
28
+ if task_id == "":
29
+ return error_text("task_id must be a string", code="invalid_arguments", argument="task_id")
30
+ cli_args = ["policy-check", path, "--json"]
31
+ if task_id:
32
+ cli_args.extend(["--task-id", task_id])
33
+ payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
34
+ if cli_error:
35
+ return cli_error
36
+ assert payload is not None
37
+ return json_text(payload)
38
+
39
+
40
+ async def handle_record_command(root: Path, db: object, arguments: dict) -> list[TextContent]:
41
+ del db # routed through CLI service layer
42
+ task_id, arg_error = required_string_argument(arguments, "task_id")
43
+ if arg_error:
44
+ return arg_error
45
+ lease_token, arg_error = required_string_argument(arguments, "lease_token")
46
+ if arg_error:
47
+ return arg_error
48
+ command, arg_error = required_string_argument(arguments, "command")
49
+ if arg_error:
50
+ return arg_error
51
+ status, arg_error = required_string_argument(arguments, "status")
52
+ if arg_error:
53
+ return arg_error
54
+ assert task_id is not None and lease_token is not None
55
+ if status not in RECORD_COMMAND_STATUSES:
56
+ return error_text(
57
+ f"status must be one of {sorted(RECORD_COMMAND_STATUSES)}",
58
+ code="invalid_arguments",
59
+ argument="status",
60
+ )
61
+ exit_code = arguments.get("exit_code")
62
+ if exit_code is not None and (not isinstance(exit_code, int) or isinstance(exit_code, bool)):
63
+ return error_text("exit_code must be an integer", code="invalid_arguments")
64
+ cli_args = [
65
+ "record-command", task_id,
66
+ "--lease-token", lease_token,
67
+ "--command", command or "",
68
+ "--status", status or "finished",
69
+ "--json",
70
+ ]
71
+ if isinstance(exit_code, int):
72
+ cli_args.extend(["--exit-code", str(exit_code)])
73
+ reason = str(arguments.get("reason") or "")
74
+ if reason:
75
+ cli_args.extend(["--reason", reason])
76
+ payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
77
+ if cli_error:
78
+ return cli_error
79
+ assert payload is not None
80
+ return json_text(payload)
@@ -0,0 +1,168 @@
1
+ """MCP prompt templates and live status snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import NamedTuple
7
+
8
+ from mcp.types import GetPromptResult, Prompt, PromptArgument, PromptMessage, TextContent
9
+
10
+ from devcouncil.app.project_status import compute_phase
11
+ from devcouncil.executors.advisor_tool import ADVISOR_STEERING_NUDGE
12
+ from devcouncil.integrations.mcp.util import normalize_arguments
13
+ from devcouncil.storage.db import get_db
14
+ from devcouncil.storage.repositories import ArtifactGraphRepository, StateRepository
15
+
16
+
17
+ class PromptSpec(NamedTuple):
18
+ name: str
19
+ description: str
20
+ arguments: list[PromptArgument]
21
+
22
+
23
+ PROMPT_SPECS: list[PromptSpec] = [
24
+ PromptSpec(
25
+ name="devcouncil_implement_next_task",
26
+ description="Pick up the next unblocked DevCouncil task and implement it through the policy-gated MCP loop.",
27
+ arguments=[
28
+ PromptArgument(name="client_id", description="Optional stable client id used for the task lease.", required=False),
29
+ ],
30
+ ),
31
+ PromptSpec(
32
+ name="devcouncil_repair_task",
33
+ description="Repair the blocking verification gaps for a task (defaults to the active running task).",
34
+ arguments=[
35
+ PromptArgument(name="task_id", description="Task id, e.g. TASK-001. Defaults to the active task.", required=False),
36
+ ],
37
+ ),
38
+ PromptSpec(
39
+ name="devcouncil_verify_task",
40
+ description="Run DevCouncil verification for a task and report blocking gaps.",
41
+ arguments=[
42
+ PromptArgument(name="task_id", description="Task id, e.g. TASK-001. Defaults to the active task.", required=False),
43
+ ],
44
+ ),
45
+ PromptSpec(
46
+ name="devcouncil_review_live",
47
+ description="Review pending live-review critique cards and resolve the blocking ones.",
48
+ arguments=[
49
+ PromptArgument(name="task_id", description="Optional task scope for the live-review cards.", required=False),
50
+ ],
51
+ ),
52
+ PromptSpec(
53
+ name="devcouncil_project_status",
54
+ description="Summarize the current DevCouncil project phase, tasks, and blocking gaps.",
55
+ arguments=[],
56
+ ),
57
+ PromptSpec(
58
+ name="devcouncil_apply_knowledge",
59
+ description="Select the ingested project knowledge (OKF + design) that applies to a goal and inject it.",
60
+ arguments=[
61
+ PromptArgument(name="goal", description="The task or goal to find applicable project knowledge for.", required=True),
62
+ ],
63
+ ),
64
+ ]
65
+
66
+
67
+ def status_snapshot(root: Path) -> str:
68
+ """A short live status block for prompt bodies, or an init hint when uninitialized."""
69
+ db = get_db(root)
70
+ if not db:
71
+ return (
72
+ "DevCouncil is not initialized here yet — run `dev status` or `dev map` "
73
+ "(both auto-bootstrap a minimal project) or `dev init` for full setup."
74
+ )
75
+ try:
76
+ with db.get_session() as session:
77
+ graph = ArtifactGraphRepository(session).load_graph()
78
+ summary = graph.coverage_summary()
79
+ state = StateRepository(session).get_state()
80
+ phase = compute_phase(graph, state.current_phase if state else None)
81
+ return (
82
+ f"Phase: {phase} | "
83
+ f"tasks: {summary['total_tasks']} | "
84
+ f"gaps: {summary['total_gaps']} ({summary['blocking_gaps']} blocking)"
85
+ )
86
+ except Exception:
87
+ return "DevCouncil status unavailable."
88
+
89
+
90
+ def render_prompt_text(name: str, arguments: dict, root: Path) -> str:
91
+ snapshot = status_snapshot(root)
92
+ if name == "devcouncil_implement_next_task":
93
+ client_id = arguments.get("client_id") or "claude-code"
94
+ return (
95
+ "You are implementing the next DevCouncil task under policy enforcement.\n\n"
96
+ f"Project status: {snapshot}\n\n"
97
+ "Do exactly this:\n"
98
+ "1. Call `devcouncil_next_task` to get the highest-priority unblocked task.\n"
99
+ f"2. Call `devcouncil_checkout_task` with that task_id and client_id='{client_id}' to acquire a lease.\n"
100
+ "3. Read the task scope with `devcouncil_get_task` and `devcouncil_get_prompt`; inspect files with `devcouncil_read_file` and `devcouncil_get_diff`.\n"
101
+ "4. Make changes ONLY through `devcouncil_write_file` / `devcouncil_apply_patch` (the policy gate rejects out-of-scope or protected paths) and run tests with `devcouncil_run_command`.\n"
102
+ f"5. {ADVISOR_STEERING_NUDGE}\n"
103
+ "6. Call `devcouncil_verify_task`; if it reports blocking gaps, fix them and re-verify.\n"
104
+ "7. When verified, call `devcouncil_release_task` with the lease token.\n\n"
105
+ "Never edit files outside the task scope. If a write is rejected, call `devcouncil_update_task_scope` only when the change is legitimately in-scope."
106
+ )
107
+ if name == "devcouncil_repair_task":
108
+ task_id = arguments.get("task_id") or "(the active task)"
109
+ return (
110
+ f"Repair the blocking verification gaps for {task_id}.\n\n"
111
+ f"Project status: {snapshot}\n\n"
112
+ "1. Call `devcouncil_get_gaps` (blocking_only=true) and `devcouncil_get_next_actions` for the task.\n"
113
+ "2. Inspect the relevant files and evidence with `devcouncil_read_file` and `devcouncil_get_evidence`.\n"
114
+ "3. Apply minimal fixes via `devcouncil_apply_patch` / `devcouncil_write_file`.\n"
115
+ f"4. {ADVISOR_STEERING_NUDGE}\n"
116
+ "5. Re-run `devcouncil_verify_task` until no blocking gaps remain, then `devcouncil_release_task`."
117
+ )
118
+ if name == "devcouncil_verify_task":
119
+ task_id = arguments.get("task_id") or "(the active task)"
120
+ return (
121
+ f"Run DevCouncil verification for {task_id} and report the result.\n\n"
122
+ f"Project status: {snapshot}\n\n"
123
+ "Call `devcouncil_verify_task` (you must hold the task lease via `devcouncil_checkout_task`). "
124
+ "Summarize the blocking gaps and proposed next actions; do not mark work complete while blocking gaps remain."
125
+ )
126
+ if name == "devcouncil_review_live":
127
+ scope = arguments.get("task_id")
128
+ scope_line = f" scoped to {scope}" if scope else ""
129
+ return (
130
+ f"Review the pending live-review critique cards{scope_line}.\n\n"
131
+ "1. Call `devcouncil_live_review` for the blocker count and `devcouncil_live_cards` (status='open') for the cards.\n"
132
+ "2. For each blocking card, call `devcouncil_live_repair_prompt` (or `devcouncil_live_repair_all`) to get a ready-to-apply repair.\n"
133
+ "3. Apply the fixes through the policy-gated write tools and re-verify."
134
+ )
135
+ if name == "devcouncil_project_status":
136
+ return (
137
+ "Summarize the DevCouncil project state for the user.\n\n"
138
+ f"Live snapshot: {snapshot}\n\n"
139
+ "Call `devcouncil_status` and `devcouncil_report` for the full coverage report, then give a concise "
140
+ "phase / tasks / blocking-gaps summary and recommend the next action."
141
+ )
142
+ if name == "devcouncil_apply_knowledge":
143
+ goal = arguments.get("goal") or ""
144
+ return (
145
+ f"Find and apply the project knowledge that applies to this goal: {goal!r}.\n\n"
146
+ "Call `devcouncil_select_knowledge` with the goal, then treat the returned preamble as authoritative "
147
+ "project context (design system + OKF docs) for any code you write toward this goal."
148
+ )
149
+ return f"Unknown DevCouncil prompt: {name}"
150
+
151
+
152
+ def list_prompts() -> list[Prompt]:
153
+ return [
154
+ Prompt(name=spec.name, description=spec.description, arguments=list(spec.arguments))
155
+ for spec in PROMPT_SPECS
156
+ ]
157
+
158
+
159
+ def get_prompt(name: str, arguments: dict | None, root: Path) -> GetPromptResult:
160
+ spec = next((spec for spec in PROMPT_SPECS if spec.name == name), None)
161
+ if spec is None:
162
+ raise ValueError(f"Unknown prompt: {name}")
163
+ args = normalize_arguments(arguments)
164
+ text = render_prompt_text(name, args, root)
165
+ return GetPromptResult(
166
+ description=spec.description,
167
+ messages=[PromptMessage(role="user", content=TextContent(type="text", text=text))],
168
+ )
@@ -0,0 +1,87 @@
1
+ """Task provenance and MCP corpus resource handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from mcp.types import Resource, TextContent
8
+ from pydantic import AnyUrl
9
+
10
+ from devcouncil.integrations.mcp.util import (
11
+ json_text,
12
+ parse_cli_json,
13
+ required_string_argument,
14
+ run_cli_command,
15
+ run_cli_json,
16
+ )
17
+ from devcouncil.knowledge.resource_discovery import (
18
+ discover_knowledge_sources,
19
+ knowledge_source_uri,
20
+ )
21
+
22
+ __all__ = [
23
+ "discover_knowledge_sources",
24
+ "handle_get_task_provenance",
25
+ "knowledge_settings",
26
+ "knowledge_source_uri",
27
+ "list_resources",
28
+ "read_resource",
29
+ ]
30
+
31
+ # Re-export for callers that imported from this module.
32
+ from devcouncil.knowledge.resource_discovery import knowledge_settings # noqa: E402
33
+
34
+
35
+ async def handle_get_task_provenance(root: Path, db: object, arguments: dict) -> list[TextContent]:
36
+ task_id, arg_error = required_string_argument(arguments, "task_id")
37
+ if arg_error:
38
+ return arg_error
39
+ assert task_id is not None
40
+ payload, cli_error = run_cli_json(["provenance", task_id, "--json"], root)
41
+ if cli_error:
42
+ return cli_error
43
+ assert payload is not None
44
+ return json_text(payload)
45
+
46
+
47
+ async def list_resources(root: Path) -> list[Resource]:
48
+ """Expose the DevCouncil corpus as browsable MCP resources."""
49
+ payload, _cli_error = parse_cli_json(
50
+ run_cli_command(["resource", "list", "--json"], root, truncate=False),
51
+ )
52
+ if payload is not None:
53
+ descriptors = payload.get("resources") or []
54
+ if isinstance(descriptors, list):
55
+ return [
56
+ Resource(
57
+ uri=AnyUrl(item["uri"]),
58
+ name=item["name"],
59
+ description=item["description"],
60
+ mimeType=item["mimeType"],
61
+ )
62
+ for item in descriptors
63
+ if isinstance(item, dict) and item.get("uri")
64
+ ]
65
+ # Fallback when CLI is unavailable (e.g. during early init).
66
+ from devcouncil.reporting.mcp_resources import list_mcp_resource_uris
67
+
68
+ return [
69
+ Resource(
70
+ uri=AnyUrl(item["uri"]),
71
+ name=item["name"],
72
+ description=item["description"],
73
+ mimeType=item["mimeType"],
74
+ )
75
+ for item in list_mcp_resource_uris(root)
76
+ ]
77
+
78
+
79
+ async def read_resource(root: Path, uri: AnyUrl) -> str:
80
+ key = str(uri).rstrip("/")
81
+ result = run_cli_command(["resource", "read", key], root, truncate=False)
82
+ stdout = result.get("stdout")
83
+ if result.get("ok") and stdout is not None:
84
+ return str(stdout)
85
+ from devcouncil.reporting.mcp_resources import read_mcp_resource
86
+
87
+ return read_mcp_resource(root, key)
@@ -0,0 +1,103 @@
1
+ """Read-file MCP tool handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from pathlib import Path
7
+
8
+ from mcp.types import TextContent
9
+
10
+ from devcouncil.integrations.mcp.util import (
11
+ error_text,
12
+ int_argument,
13
+ is_secret_path,
14
+ json_text,
15
+ optional_string_argument,
16
+ required_string_argument,
17
+ truncate_text,
18
+ within_root,
19
+ )
20
+ from devcouncil.storage.db import Database
21
+ from devcouncil.storage.repositories import TaskRepository
22
+
23
+
24
+ async def handle_read_file(
25
+ root: Path,
26
+ arguments: dict,
27
+ db: Database | None = None,
28
+ ) -> list[TextContent]:
29
+ rel_path, arg_error = required_string_argument(arguments, "path")
30
+ if arg_error:
31
+ return arg_error
32
+ assert rel_path is not None
33
+ task_id = optional_string_argument(arguments, "task_id")
34
+ if task_id == "":
35
+ return error_text("task_id must be a string", code="invalid_arguments", argument="task_id")
36
+ if task_id:
37
+ if db is None:
38
+ return error_text(
39
+ "DevCouncil not initialized in this directory.",
40
+ code="not_initialized",
41
+ )
42
+ with db.get_session() as session:
43
+ task = TaskRepository(session).get_by_id(task_id)
44
+ if task is None:
45
+ return error_text(f"Task {task_id} not found.", code="not_found", task_id=task_id)
46
+ planned = {pf.path.replace("\\", "/") for pf in task.planned_files}
47
+ normalized = rel_path.replace("\\", "/")
48
+ if normalized not in planned:
49
+ # Fail closed: never broaden task scope to arbitrary repo paths.
50
+ return error_text(
51
+ f"Path {normalized} is outside task {task_id} planned-file scope.",
52
+ code="out_of_scope",
53
+ path=normalized,
54
+ task_id=task_id,
55
+ )
56
+ if is_secret_path(root, rel_path):
57
+ return error_text(
58
+ "Refusing to read a secret/credential path.",
59
+ code="secret_path", path=rel_path,
60
+ )
61
+ target = within_root(root, rel_path)
62
+ if target is None:
63
+ return error_text("path escapes the project root", code="path_escape", path=rel_path)
64
+ if not target.exists() or not target.is_file():
65
+ return error_text(f"File not found: {rel_path}", code="not_found", path=rel_path)
66
+ try:
67
+ raw = target.read_bytes()
68
+ except OSError as exc:
69
+ return error_text(f"Read failed: {exc}", code="read_failed", path=rel_path)
70
+ sha256 = hashlib.sha256(raw).hexdigest()
71
+ text = raw.decode("utf-8", errors="replace")
72
+ all_lines = text.splitlines()
73
+ line_count = len(all_lines)
74
+ line_range = optional_string_argument(arguments, "line_range")
75
+ if line_range == "":
76
+ return error_text("line_range must be a string", code="invalid_arguments", argument="line_range")
77
+ selected = all_lines
78
+ if line_range:
79
+ try:
80
+ start_str, _, end_str = line_range.partition("-")
81
+ start = max(1, int(start_str))
82
+ end = int(end_str) if end_str else line_count
83
+ except ValueError:
84
+ return error_text("line_range must look like '10-40'", code="invalid_arguments", argument="line_range")
85
+ selected = all_lines[start - 1:end]
86
+ else:
87
+ offset = int_argument(arguments, "offset", 0, minimum=0, maximum=10_000_000)
88
+ limit_value = arguments.get("limit")
89
+ if isinstance(limit_value, int) and not isinstance(limit_value, bool):
90
+ limit = max(1, limit_value)
91
+ selected = all_lines[offset:offset + limit]
92
+ elif offset:
93
+ selected = all_lines[offset:]
94
+ windowed = "\n".join(selected)
95
+ content, truncated = truncate_text(windowed)
96
+ return json_text({
97
+ "ok": True,
98
+ "path": rel_path.replace("\\", "/"),
99
+ "content": content,
100
+ "sha256": sha256,
101
+ "line_count": line_count,
102
+ "truncated": truncated,
103
+ })
@@ -0,0 +1,53 @@
1
+ """Per-root ModelRouter cache for MCP verify/supervise handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from devcouncil.integrations.mcp.handlers import ast_lsp as ast_lsp_handlers
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ _ROUTER_CACHE: dict[str, tuple[Any, Any]] = {}
14
+
15
+
16
+ def reset_caches() -> None:
17
+ """Drop all per-root MCP caches (router/ast/lsp). For test isolation."""
18
+ _ROUTER_CACHE.clear()
19
+ ast_lsp_handlers.reset_caches()
20
+
21
+
22
+ def load_router(root: Path):
23
+ """Build a ModelRouter from project config, or return None when no provider key
24
+ is configured. Cached per resolved project root with config stat invalidation."""
25
+ key = str(root.resolve())
26
+ try:
27
+ cfg_stat = (root / ".devcouncil" / "config.yaml").stat()
28
+ signature: object = (cfg_stat.st_mtime_ns, cfg_stat.st_size, cfg_stat.st_ino)
29
+ except OSError:
30
+ signature = None
31
+ cached = _ROUTER_CACHE.get(key)
32
+ if cached is not None and cached[0] == signature:
33
+ return cached[1]
34
+ router = _build_router(root)
35
+ _ROUTER_CACHE[key] = (signature, router)
36
+ return router
37
+
38
+
39
+ def _build_router(root: Path):
40
+ try:
41
+ from devcouncil.app.config import get_api_key, load_config
42
+ from devcouncil.llm.provider import create_provider, validate_model_provider
43
+ from devcouncil.llm.router import ModelRouter
44
+
45
+ config = load_config(root)
46
+ validate_model_provider(config.models.provider)
47
+ api_key = get_api_key(config.models.provider, root)
48
+ provider = create_provider(config.models.provider, api_key, project_root=root, provider_prefs=config.provider)
49
+ role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
50
+ return ModelRouter(provider, role_config, project_root=root)
51
+ except Exception:
52
+ logger.warning("Failed to build MCP model router for %s", root, exc_info=True)
53
+ return None
@@ -0,0 +1,53 @@
1
+ """Run-command 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
+ error_text,
11
+ json_text,
12
+ parse_cli_json,
13
+ required_string_argument,
14
+ run_cli_command,
15
+ )
16
+
17
+
18
+ async def handle_run_command(root: Path, db: object, arguments: dict) -> list[TextContent]:
19
+ del db # routed through CLI service layer
20
+ task_id, arg_error = required_string_argument(arguments, "task_id")
21
+ if arg_error:
22
+ return arg_error
23
+ lease_token, arg_error = required_string_argument(arguments, "lease_token")
24
+ if arg_error:
25
+ return arg_error
26
+ command, arg_error = required_string_argument(arguments, "command")
27
+ if arg_error:
28
+ return arg_error
29
+ assert task_id is not None and lease_token is not None and command is not None
30
+ cli_args = [
31
+ "run-cmd", task_id,
32
+ "--lease-token", lease_token,
33
+ "--command", command,
34
+ "--json",
35
+ ]
36
+ payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
37
+ if cli_error:
38
+ return cli_error
39
+ assert payload is not None
40
+ if payload.get("code") == "command_not_allowed":
41
+ return error_text(
42
+ str(payload.get("error") or "Command is not in the task allowlist."),
43
+ code="command_not_allowed",
44
+ task_id=task_id,
45
+ command=str(payload.get("command") or command),
46
+ )
47
+ if payload.get("code") == "run_failed":
48
+ return error_text(
49
+ str(payload.get("error") or "Could not run command"),
50
+ code="run_failed",
51
+ task_id=task_id,
52
+ )
53
+ return json_text(payload)
@@ -0,0 +1,69 @@
1
+ """Agent run listing and inspection 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
+ required_string_argument,
15
+ run_cli_json,
16
+ truncate_text,
17
+ )
18
+
19
+
20
+ async def handle_list_agent_runs(root: Path, arguments: dict) -> list[TextContent]:
21
+ status_filter = optional_string_argument(arguments, "status")
22
+ if status_filter == "":
23
+ return error_text("status must be a string", code="invalid_arguments", argument="status")
24
+ limit = int_argument(arguments, "limit", 20, minimum=1, maximum=500)
25
+ cli_args = ["runs", "list", "--json", "--limit", str(limit)]
26
+ if status_filter:
27
+ cli_args.extend(["--status", status_filter])
28
+ payload, cli_error = run_cli_json(cli_args, root)
29
+ if cli_error:
30
+ return cli_error
31
+ assert payload is not None
32
+ runs = payload.get("runs") or []
33
+ if not isinstance(runs, list):
34
+ runs = []
35
+ total = payload.get("total", payload.get("count", len(runs)))
36
+ return json_text({
37
+ "ok": True,
38
+ "runs": runs,
39
+ "total": total,
40
+ "returned": len(runs),
41
+ })
42
+
43
+
44
+ async def handle_get_run(root: Path, arguments: dict) -> list[TextContent]:
45
+ run_id, arg_error = required_string_argument(arguments, "run_id")
46
+ if arg_error:
47
+ return arg_error
48
+ assert run_id is not None
49
+ payload, cli_error = run_cli_json(["runs", "show", run_id, "--json"], root)
50
+ if cli_error:
51
+ return cli_error
52
+ assert payload is not None
53
+ if not payload.get("ok", True):
54
+ return error_text(
55
+ str(payload.get("error") or f"Run {run_id} not found."),
56
+ code=str(payload.get("code") or "not_found"),
57
+ run_id=run_id,
58
+ )
59
+ tail = str(payload.get("transcript_tail") or "")
60
+ truncated_tail, truncated = truncate_text(tail)
61
+ return json_text({
62
+ "ok": True,
63
+ "run_id": payload.get("run_id", run_id),
64
+ "manifest": payload.get("manifest"),
65
+ "orphaned": payload.get("orphaned", False),
66
+ "transcript_path": payload.get("transcript_path"),
67
+ "transcript_tail": truncated_tail,
68
+ "transcript_truncated": truncated,
69
+ })