devcouncil 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/README.md +46 -30
  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 +49 -30
  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 +960 -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 +99 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -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 +101 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +74 -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 +84 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -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 +60 -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 +303 -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/tasks.py +12 -3
  255. package/src/devcouncil/live/transcripts.py +69 -2
  256. package/src/devcouncil/llm/cache.py +5 -6
  257. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  258. package/src/devcouncil/llm/provider.py +647 -73
  259. package/src/devcouncil/llm/router.py +271 -46
  260. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  261. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  262. package/src/devcouncil/optimization/skillopt.py +9 -5
  263. package/src/devcouncil/planning/arbiter_service.py +12 -3
  264. package/src/devcouncil/planning/correction_manifest.py +107 -10
  265. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  266. package/src/devcouncil/planning/plan_service.py +5 -2
  267. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  268. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  269. package/src/devcouncil/planning/question_conversion.py +56 -0
  270. package/src/devcouncil/planning/spec_service.py +9 -3
  271. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  272. package/src/devcouncil/repo/gitignore.py +1 -2
  273. package/src/devcouncil/reporting/evidence_export.py +124 -0
  274. package/src/devcouncil/reporting/evidence_html.py +210 -0
  275. package/src/devcouncil/reporting/json_report.py +16 -12
  276. package/src/devcouncil/reporting/markdown_report.py +38 -9
  277. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  278. package/src/devcouncil/reporting/report_builder.py +40 -4
  279. package/src/devcouncil/reporting/task_provenance.py +42 -0
  280. package/src/devcouncil/reporting/verdict.py +75 -0
  281. package/src/devcouncil/skills/library/README.md +1 -0
  282. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  283. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  285. package/src/devcouncil/skills/registry.py +43 -12
  286. package/src/devcouncil/storage/db.py +57 -11
  287. package/src/devcouncil/storage/models.py +6 -0
  288. package/src/devcouncil/storage/native.py +5 -3
  289. package/src/devcouncil/storage/repositories.py +50 -18
  290. package/src/devcouncil/telemetry/context.py +28 -0
  291. package/src/devcouncil/telemetry/cost.py +4 -5
  292. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  293. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  294. package/src/devcouncil/telemetry/stages.py +27 -2
  295. package/src/devcouncil/telemetry/tracker.py +50 -13
  296. package/src/devcouncil/ui/dashboard.py +120 -8
  297. package/src/devcouncil/utils/fsio.py +58 -0
  298. package/src/devcouncil/utils/git_snapshot.py +112 -0
  299. package/src/devcouncil/utils/json_persist.py +53 -0
  300. package/src/devcouncil/utils/proc.py +89 -0
  301. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  302. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  303. package/src/devcouncil/verification/checks/__init__.py +41 -0
  304. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  305. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  306. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  307. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  308. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  309. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  310. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  311. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  312. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  313. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  314. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  315. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  316. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  317. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  318. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  319. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  320. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  321. package/src/devcouncil/verification/checks/wiring.py +216 -0
  322. package/src/devcouncil/verification/claims/__init__.py +23 -0
  323. package/src/devcouncil/verification/claims/checks.py +395 -0
  324. package/src/devcouncil/verification/claims/mapper.py +168 -0
  325. package/src/devcouncil/verification/claims/models.py +39 -0
  326. package/src/devcouncil/verification/claims/transcript.py +92 -0
  327. package/src/devcouncil/verification/claims/verdict.py +88 -0
  328. package/src/devcouncil/verification/command_evidence.py +170 -0
  329. package/src/devcouncil/verification/command_malformation.py +147 -0
  330. package/src/devcouncil/verification/command_runner.py +164 -0
  331. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  332. package/src/devcouncil/verification/diff_coverage.py +151 -0
  333. package/src/devcouncil/verification/difficulty.py +296 -0
  334. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  335. package/src/devcouncil/verification/gap_ids.py +63 -0
  336. package/src/devcouncil/verification/gate_cache.py +194 -0
  337. package/src/devcouncil/verification/gate_selector.py +344 -0
  338. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  339. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  340. package/src/devcouncil/verification/incremental_check.py +241 -0
  341. package/src/devcouncil/verification/next_actions.py +60 -1
  342. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  343. package/src/devcouncil/verification/sandbox.py +38 -11
  344. package/src/devcouncil/verification/stub_detector.py +369 -0
  345. package/src/devcouncil/verification/test_resolver.py +67 -1
  346. package/src/devcouncil/verification/verifier.py +137 -1666
  347. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  348. package/src/devcouncil/verification/verify_setup.py +176 -0
  349. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  350. package/src/semantic_layer/__init__.py +58 -0
  351. package/src/semantic_layer/benchmark.py +75 -0
  352. package/src/semantic_layer/cache.py +290 -0
  353. package/src/semantic_layer/compressor.py +137 -0
  354. package/src/semantic_layer/config.py +75 -0
  355. package/src/semantic_layer/embeddings.py +69 -0
  356. package/src/semantic_layer/llm_backends.py +99 -0
  357. package/src/semantic_layer/pipeline.py +111 -0
  358. package/src/semantic_layer/router.py +128 -0
  359. package/src/semantic_layer/tuner.py +72 -0
  360. package/uv.lock +973 -9
  361. package/src/devcouncil/artifacts/migrations.py +0 -20
  362. package/src/devcouncil/artifacts/schemas.py +0 -23
  363. package/src/devcouncil/artifacts/serializer.py +0 -21
  364. package/src/devcouncil/integrations/gitnexus.py +0 -70
  365. 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,101 @@
1
+ """Task provenance and MCP corpus resource handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from mcp.types import Resource, TextContent
9
+ from pydantic import AnyUrl
10
+
11
+ from devcouncil.integrations.mcp.util import json_text, required_string_argument, run_cli_command
12
+ from devcouncil.knowledge.resource_discovery import (
13
+ discover_knowledge_sources,
14
+ knowledge_source_uri,
15
+ )
16
+
17
+ __all__ = [
18
+ "discover_knowledge_sources",
19
+ "handle_get_task_provenance",
20
+ "knowledge_settings",
21
+ "knowledge_source_uri",
22
+ "list_resources",
23
+ "read_resource",
24
+ ]
25
+
26
+ # Re-export for callers that imported from this module.
27
+ from devcouncil.knowledge.resource_discovery import knowledge_settings # noqa: E402
28
+
29
+
30
+ def _cli_json(root: Path, args: list[str]) -> tuple[dict | None, list[TextContent] | None]:
31
+ result = run_cli_command(args, root)
32
+ stdout = str(result.get("stdout") or "").strip()
33
+ if stdout:
34
+ try:
35
+ return json.loads(stdout), None
36
+ except json.JSONDecodeError:
37
+ pass
38
+ if not result.get("ok"):
39
+ stderr = str(result.get("stderr") or "CLI command failed")
40
+ from devcouncil.integrations.mcp.util import error_text
41
+ return None, error_text(stderr, code="cli_failed")
42
+ from devcouncil.integrations.mcp.util import error_text
43
+ return None, error_text("CLI command returned invalid JSON", code="cli_parse_error")
44
+
45
+
46
+ async def handle_get_task_provenance(root: Path, db: object, arguments: dict) -> list[TextContent]:
47
+ task_id, arg_error = required_string_argument(arguments, "task_id")
48
+ if arg_error:
49
+ return arg_error
50
+ assert task_id is not None
51
+ payload, cli_error = _cli_json(root, ["provenance", task_id, "--json"])
52
+ if cli_error:
53
+ return cli_error
54
+ assert payload is not None
55
+ return json_text(payload)
56
+
57
+
58
+ async def list_resources(root: Path) -> list[Resource]:
59
+ """Expose the DevCouncil corpus as browsable MCP resources."""
60
+ result = run_cli_command(["resource", "list", "--json"], root)
61
+ stdout = str(result.get("stdout") or "").strip()
62
+ if stdout:
63
+ try:
64
+ payload = json.loads(stdout)
65
+ descriptors = payload.get("resources") or []
66
+ if isinstance(descriptors, list):
67
+ return [
68
+ Resource(
69
+ uri=AnyUrl(item["uri"]),
70
+ name=item["name"],
71
+ description=item["description"],
72
+ mimeType=item["mimeType"],
73
+ )
74
+ for item in descriptors
75
+ if isinstance(item, dict) and item.get("uri")
76
+ ]
77
+ except json.JSONDecodeError:
78
+ pass
79
+ # Fallback when CLI is unavailable (e.g. during early init).
80
+ from devcouncil.reporting.mcp_resources import list_mcp_resource_uris
81
+
82
+ return [
83
+ Resource(
84
+ uri=AnyUrl(item["uri"]),
85
+ name=item["name"],
86
+ description=item["description"],
87
+ mimeType=item["mimeType"],
88
+ )
89
+ for item in list_mcp_resource_uris(root)
90
+ ]
91
+
92
+
93
+ async def read_resource(root: Path, uri: AnyUrl) -> str:
94
+ key = str(uri).rstrip("/")
95
+ result = run_cli_command(["resource", "read", key], root)
96
+ stdout = result.get("stdout")
97
+ if result.get("ok") and stdout is not None:
98
+ return str(stdout)
99
+ from devcouncil.reporting.mcp_resources import read_mcp_resource
100
+
101
+ return read_mcp_resource(root, key)
@@ -0,0 +1,74 @@
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
+
21
+
22
+ async def handle_read_file(root: Path, arguments: dict) -> list[TextContent]:
23
+ rel_path, arg_error = required_string_argument(arguments, "path")
24
+ if arg_error:
25
+ return arg_error
26
+ assert rel_path is not None
27
+ if is_secret_path(root, rel_path):
28
+ return error_text(
29
+ "Refusing to read a secret/credential path.",
30
+ code="secret_path", path=rel_path,
31
+ )
32
+ target = within_root(root, rel_path)
33
+ if target is None:
34
+ return error_text("path escapes the project root", code="path_escape", path=rel_path)
35
+ if not target.exists() or not target.is_file():
36
+ return error_text(f"File not found: {rel_path}", code="not_found", path=rel_path)
37
+ try:
38
+ raw = target.read_bytes()
39
+ except OSError as exc:
40
+ return error_text(f"Read failed: {exc}", code="read_failed", path=rel_path)
41
+ sha256 = hashlib.sha256(raw).hexdigest()
42
+ text = raw.decode("utf-8", errors="replace")
43
+ all_lines = text.splitlines()
44
+ line_count = len(all_lines)
45
+ line_range = optional_string_argument(arguments, "line_range")
46
+ if line_range == "":
47
+ return error_text("line_range must be a string", code="invalid_arguments", argument="line_range")
48
+ selected = all_lines
49
+ if line_range:
50
+ try:
51
+ start_str, _, end_str = line_range.partition("-")
52
+ start = max(1, int(start_str))
53
+ end = int(end_str) if end_str else line_count
54
+ except ValueError:
55
+ return error_text("line_range must look like '10-40'", code="invalid_arguments", argument="line_range")
56
+ selected = all_lines[start - 1:end]
57
+ else:
58
+ offset = int_argument(arguments, "offset", 0, minimum=0, maximum=10_000_000)
59
+ limit_value = arguments.get("limit")
60
+ if isinstance(limit_value, int) and not isinstance(limit_value, bool):
61
+ limit = max(1, limit_value)
62
+ selected = all_lines[offset:offset + limit]
63
+ elif offset:
64
+ selected = all_lines[offset:]
65
+ windowed = "\n".join(selected)
66
+ content, truncated = truncate_text(windowed)
67
+ return json_text({
68
+ "ok": True,
69
+ "path": rel_path.replace("\\", "/"),
70
+ "content": content,
71
+ "sha256": sha256,
72
+ "line_count": line_count,
73
+ "truncated": truncated,
74
+ })
@@ -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,84 @@
1
+ """Agent run listing and inspection MCP tool handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from mcp.types import TextContent
9
+
10
+ from devcouncil.integrations.mcp.util import (
11
+ error_text,
12
+ int_argument,
13
+ json_text,
14
+ optional_string_argument,
15
+ required_string_argument,
16
+ run_cli_command,
17
+ truncate_text,
18
+ )
19
+
20
+
21
+ def _cli_json(root: Path, args: list[str]) -> tuple[dict | None, list[TextContent] | None]:
22
+ result = run_cli_command(args, root)
23
+ stdout = str(result.get("stdout") or "").strip()
24
+ if stdout:
25
+ try:
26
+ return json.loads(stdout), None
27
+ except json.JSONDecodeError:
28
+ pass
29
+ if not result.get("ok"):
30
+ stderr = str(result.get("stderr") or "CLI command failed")
31
+ return None, error_text(stderr, code="cli_failed")
32
+ return None, error_text("CLI command returned invalid JSON", code="cli_parse_error")
33
+
34
+
35
+ async def handle_list_agent_runs(root: Path, arguments: dict) -> list[TextContent]:
36
+ status_filter = optional_string_argument(arguments, "status")
37
+ if status_filter == "":
38
+ return error_text("status must be a string", code="invalid_arguments", argument="status")
39
+ limit = int_argument(arguments, "limit", 20, minimum=1, maximum=500)
40
+ cli_args = ["runs", "list", "--json", "--limit", str(limit)]
41
+ if status_filter:
42
+ cli_args.extend(["--status", status_filter])
43
+ payload, cli_error = _cli_json(root, cli_args)
44
+ if cli_error:
45
+ return cli_error
46
+ assert payload is not None
47
+ runs = payload.get("runs") or []
48
+ if not isinstance(runs, list):
49
+ runs = []
50
+ total = payload.get("total", payload.get("count", len(runs)))
51
+ return json_text({
52
+ "ok": True,
53
+ "runs": runs,
54
+ "total": total,
55
+ "returned": len(runs),
56
+ })
57
+
58
+
59
+ async def handle_get_run(root: Path, arguments: dict) -> list[TextContent]:
60
+ run_id, arg_error = required_string_argument(arguments, "run_id")
61
+ if arg_error:
62
+ return arg_error
63
+ assert run_id is not None
64
+ payload, cli_error = _cli_json(root, ["runs", "show", run_id, "--json"])
65
+ if cli_error:
66
+ return cli_error
67
+ assert payload is not None
68
+ if not payload.get("ok", True):
69
+ return error_text(
70
+ str(payload.get("error") or f"Run {run_id} not found."),
71
+ code=str(payload.get("code") or "not_found"),
72
+ run_id=run_id,
73
+ )
74
+ tail = str(payload.get("transcript_tail") or "")
75
+ truncated_tail, truncated = truncate_text(tail)
76
+ return json_text({
77
+ "ok": True,
78
+ "run_id": payload.get("run_id", run_id),
79
+ "manifest": payload.get("manifest"),
80
+ "orphaned": payload.get("orphaned", False),
81
+ "transcript_path": payload.get("transcript_path"),
82
+ "transcript_tail": truncated_tail,
83
+ "transcript_truncated": truncated,
84
+ })