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,236 @@
1
+ """Registry-backed MCP debugger and runtime-trace tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Awaitable, Callable
7
+
8
+ from mcp.types import TextContent, Tool
9
+
10
+ from devcouncil.codeintel.debug.consent import require_debug_consent, set_debug_consent
11
+ from devcouncil.codeintel.debug.discovery import adapter_by_id, discover_adapters
12
+ from devcouncil.codeintel.debug.session import get_debug_manager
13
+ from devcouncil.codeintel.debug.tracing import NodeCpuProfileProvider, PythonTraceProvider, import_runtime_trace
14
+ from devcouncil.codeintel.service import canonical_project_root
15
+ from devcouncil.integrations.mcp.util import error_text, json_text
16
+
17
+ Handler = Callable[[Path, dict], Awaitable[list[TextContent]]]
18
+
19
+
20
+ def _schema(properties: dict, required: list[str] | None = None) -> dict:
21
+ schema: dict = {"type": "object", "properties": {
22
+ "projectPath": {"type": "string", "description": "Explicit repository path."},
23
+ **properties,
24
+ }}
25
+ if required:
26
+ schema["required"] = required
27
+ return schema
28
+
29
+
30
+ def tools() -> list[Tool]:
31
+ return [
32
+ Tool(
33
+ name="devcouncil_debug_discover",
34
+ description=(
35
+ "Discover installed DAP adapters after explicit one-time consent; "
36
+ "returns paths, versions, hashes, and launch/attach support."
37
+ ),
38
+ inputSchema=_schema({"consent": {"type": "boolean", "default": False}}),
39
+ ),
40
+ Tool(
41
+ name="devcouncil_debug_start",
42
+ description="Launch or attach a capability-negotiated DAP session.",
43
+ inputSchema=_schema({
44
+ "adapterId": {"type": "string"},
45
+ "adapterCommand": {"type": "array", "items": {"type": "string"}},
46
+ "request": {"type": "string", "enum": ["launch", "attach"], "default": "launch"},
47
+ "configuration": {"type": "object"},
48
+ "initialBreakpoints": {
49
+ "type": "object",
50
+ "additionalProperties": {"type": "array", "items": {"type": "integer"}},
51
+ },
52
+ "timeout": {"type": "number", "minimum": 1, "maximum": 120, "default": 30},
53
+ }),
54
+ ),
55
+ Tool(
56
+ name="devcouncil_debug_breakpoints",
57
+ description="Replace all breakpoints for one source in a DAP session.",
58
+ inputSchema=_schema({
59
+ "sessionId": {"type": "string"},
60
+ "source": {"type": "string"},
61
+ "lines": {"type": "array", "items": {"type": "integer"}},
62
+ }, ["sessionId", "source", "lines"]),
63
+ ),
64
+ Tool(
65
+ name="devcouncil_debug_control",
66
+ description="Continue, pause, or step a DAP session.",
67
+ inputSchema=_schema({
68
+ "sessionId": {"type": "string"},
69
+ "action": {"type": "string", "enum": ["continue", "pause", "next", "stepIn", "stepOut"]},
70
+ "threadId": {"type": "integer"},
71
+ }, ["sessionId", "action"]),
72
+ ),
73
+ Tool(
74
+ name="devcouncil_debug_inspect",
75
+ description="Inspect threads, stack frames, scopes, variables, source, or disassembly.",
76
+ inputSchema=_schema({
77
+ "sessionId": {"type": "string"},
78
+ "operation": {"type": "string", "enum": ["threads", "stackTrace", "scopes", "variables", "source", "disassemble"]},
79
+ "arguments": {"type": "object"},
80
+ }, ["sessionId", "operation"]),
81
+ ),
82
+ Tool(
83
+ name="devcouncil_debug_evaluate",
84
+ description="Side-effectful DAP evaluate; allowSideEffects must be explicitly true.",
85
+ inputSchema=_schema({
86
+ "sessionId": {"type": "string"},
87
+ "expression": {"type": "string"},
88
+ "frameId": {"type": "integer"},
89
+ "allowSideEffects": {"type": "boolean", "const": True},
90
+ }, ["sessionId", "expression", "allowSideEffects"]),
91
+ ),
92
+ Tool(
93
+ name="devcouncil_debug_trace",
94
+ description="Capture a DAP stack, run exact Python tracing, or import JSONL/Node CPU profile evidence.",
95
+ inputSchema=_schema({
96
+ "provider": {"type": "string", "enum": ["dap-stack", "python", "node", "import"]},
97
+ "sessionId": {"type": "string"},
98
+ "threadId": {"type": "integer"},
99
+ "script": {"type": "string"},
100
+ "args": {"type": "array", "items": {"type": "string"}},
101
+ "path": {"type": "string"},
102
+ }, ["provider"]),
103
+ ),
104
+ Tool(
105
+ name="devcouncil_debug_stop",
106
+ description="Disconnect a DAP session and optionally leave the debuggee running.",
107
+ inputSchema=_schema({
108
+ "sessionId": {"type": "string"},
109
+ "terminateDebuggee": {"type": "boolean", "default": True},
110
+ }, ["sessionId"]),
111
+ ),
112
+ ]
113
+
114
+
115
+ async def _discover(root: Path, arguments: dict) -> list[TextContent]:
116
+ if arguments.get("consent") is True:
117
+ set_debug_consent(root, True)
118
+ require_debug_consent(root)
119
+ return json_text({"consent": True, "adapters": [adapter.as_dict() for adapter in discover_adapters()]})
120
+
121
+
122
+ def _command(arguments: dict) -> list[str]:
123
+ supplied = arguments.get("adapterCommand")
124
+ if isinstance(supplied, list) and supplied:
125
+ return [str(value) for value in supplied]
126
+ adapter_id = str(arguments.get("adapterId") or "debugpy")
127
+ adapter = adapter_by_id(adapter_id)
128
+ if adapter is None:
129
+ raise ValueError(f"adapter {adapter_id!r} was not discovered")
130
+ return list(adapter.command)
131
+
132
+
133
+ async def _start(root: Path, arguments: dict) -> list[TextContent]:
134
+ require_debug_consent(root)
135
+ session = get_debug_manager().start(
136
+ root,
137
+ _command(arguments),
138
+ request=str(arguments.get("request", "launch")),
139
+ arguments=dict(arguments.get("configuration") or {}),
140
+ initial_breakpoints={
141
+ str(source): [int(line) for line in lines]
142
+ for source, lines in dict(arguments.get("initialBreakpoints") or {}).items()
143
+ },
144
+ timeout=float(arguments.get("timeout", 30.0)),
145
+ )
146
+ return json_text(session.as_dict())
147
+
148
+
149
+ async def _breakpoints(root: Path, arguments: dict) -> list[TextContent]:
150
+ require_debug_consent(root)
151
+ return json_text(get_debug_manager().set_breakpoints(
152
+ str(arguments["sessionId"]), str(arguments["source"]), [int(value) for value in arguments["lines"]]
153
+ ))
154
+
155
+
156
+ async def _control(root: Path, arguments: dict) -> list[TextContent]:
157
+ require_debug_consent(root)
158
+ return json_text(get_debug_manager().control(
159
+ str(arguments["sessionId"]),
160
+ str(arguments["action"]),
161
+ thread_id=int(arguments["threadId"]) if arguments.get("threadId") is not None else None,
162
+ ))
163
+
164
+
165
+ async def _inspect(root: Path, arguments: dict) -> list[TextContent]:
166
+ require_debug_consent(root)
167
+ return json_text(get_debug_manager().inspect(
168
+ str(arguments["sessionId"]), str(arguments["operation"]), dict(arguments.get("arguments") or {})
169
+ ))
170
+
171
+
172
+ async def _evaluate(root: Path, arguments: dict) -> list[TextContent]:
173
+ require_debug_consent(root)
174
+ if arguments.get("allowSideEffects") is not True:
175
+ raise PermissionError("debug evaluate requires allowSideEffects=true")
176
+ return json_text(get_debug_manager().evaluate(
177
+ str(arguments["sessionId"]),
178
+ str(arguments["expression"]),
179
+ frame_id=int(arguments["frameId"]) if arguments.get("frameId") is not None else None,
180
+ allow_side_effects=True,
181
+ ))
182
+
183
+
184
+ async def _trace(root: Path, arguments: dict) -> list[TextContent]:
185
+ require_debug_consent(root)
186
+ provider = str(arguments["provider"])
187
+ if provider == "dap-stack":
188
+ return json_text(get_debug_manager().capture_stack(
189
+ str(arguments["sessionId"]), thread_id=int(arguments["threadId"])
190
+ ))
191
+ if provider == "python":
192
+ return json_text(PythonTraceProvider(root).run(
193
+ Path(str(arguments["script"])), [str(value) for value in arguments.get("args") or []]
194
+ ))
195
+ if provider == "node":
196
+ return json_text(NodeCpuProfileProvider(root).run(
197
+ Path(str(arguments["script"])), [str(value) for value in arguments.get("args") or []]
198
+ ))
199
+ if provider == "import":
200
+ return json_text(import_runtime_trace(root, Path(str(arguments["path"]))))
201
+ raise ValueError(f"unsupported trace provider: {provider}")
202
+
203
+
204
+ async def _stop(root: Path, arguments: dict) -> list[TextContent]:
205
+ require_debug_consent(root)
206
+ session_id = str(arguments["sessionId"])
207
+ get_debug_manager().stop(session_id, terminate_debuggee=bool(arguments.get("terminateDebuggee", True)))
208
+ return json_text({"stopped": session_id})
209
+
210
+
211
+ REGISTRY: dict[str, Handler] = {
212
+ "devcouncil_debug_discover": _discover,
213
+ "devcouncil_debug_start": _start,
214
+ "devcouncil_debug_breakpoints": _breakpoints,
215
+ "devcouncil_debug_control": _control,
216
+ "devcouncil_debug_inspect": _inspect,
217
+ "devcouncil_debug_evaluate": _evaluate,
218
+ "devcouncil_debug_trace": _trace,
219
+ "devcouncil_debug_stop": _stop,
220
+ }
221
+
222
+
223
+ async def dispatch(name: str, default_root: Path, arguments: dict) -> list[TextContent] | None:
224
+ handler = REGISTRY.get(name)
225
+ if handler is None:
226
+ return None
227
+ explicit = arguments.get("projectPath")
228
+ root = canonical_project_root(Path(explicit) if isinstance(explicit, str) and explicit else default_root)
229
+ try:
230
+ return await handler(root, arguments)
231
+ except PermissionError as exc:
232
+ return error_text(str(exc), code="debug_consent_required", tool=name)
233
+ except FileNotFoundError as exc:
234
+ return error_text(str(exc), code="not_found", tool=name)
235
+ except (KeyError, TypeError, ValueError, RuntimeError, TimeoutError) as exc:
236
+ return error_text(str(exc), code="debug_error", tool=name)
@@ -0,0 +1,70 @@
1
+ """Evidence read/write MCP tool handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from mcp.types import TextContent
8
+
9
+ from devcouncil.integrations.mcp.util import (
10
+ error_text,
11
+ int_argument,
12
+ json_text,
13
+ optional_string_argument,
14
+ parse_cli_json,
15
+ required_string_argument,
16
+ run_cli_command,
17
+ )
18
+
19
+
20
+ async def handle_append_evidence(root: Path, db: object, arguments: dict) -> list[TextContent]:
21
+ del db # routed through CLI service layer
22
+ task_id, arg_error = required_string_argument(arguments, "task_id")
23
+ if arg_error:
24
+ return arg_error
25
+ lease_token, arg_error = required_string_argument(arguments, "lease_token")
26
+ if arg_error:
27
+ return arg_error
28
+ command, arg_error = required_string_argument(arguments, "command")
29
+ if arg_error:
30
+ return arg_error
31
+ summary_text, arg_error = required_string_argument(arguments, "summary")
32
+ if arg_error:
33
+ return arg_error
34
+ assert task_id is not None and lease_token is not None
35
+ exit_code = arguments.get("exit_code", 0)
36
+ if not isinstance(exit_code, int) or isinstance(exit_code, bool):
37
+ return error_text("exit_code must be an integer", code="invalid_arguments")
38
+ cli_args = [
39
+ "evidence-append", task_id,
40
+ "--lease-token", lease_token,
41
+ "--command", command or "",
42
+ "--summary", summary_text or "",
43
+ "--exit-code", str(exit_code),
44
+ "--json",
45
+ ]
46
+ payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
47
+ if cli_error:
48
+ return cli_error
49
+ assert payload is not None
50
+ return json_text(payload)
51
+
52
+
53
+ async def handle_get_evidence(root: Path, db: object, arguments: dict) -> list[TextContent]:
54
+ del db # routed through CLI service layer
55
+ task_id, arg_error = required_string_argument(arguments, "task_id")
56
+ if arg_error:
57
+ return arg_error
58
+ assert task_id is not None
59
+ command_filter = optional_string_argument(arguments, "command")
60
+ if command_filter == "":
61
+ return error_text("command must be a string", code="invalid_arguments", argument="command")
62
+ limit = int_argument(arguments, "limit", 20, minimum=1, maximum=100)
63
+ cli_args = ["evidence-list", task_id, "--limit", str(limit), "--json"]
64
+ if command_filter:
65
+ cli_args.extend(["--command", command_filter])
66
+ payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
67
+ if cli_error:
68
+ return cli_error
69
+ assert payload is not None
70
+ return json_text(payload)
@@ -0,0 +1,99 @@
1
+ """Git diff MCP tool handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ from mcp.types import TextContent
10
+
11
+ from devcouncil.integrations.mcp.util import (
12
+ CLI_TIMEOUT_SECONDS,
13
+ error_text,
14
+ is_git_repo,
15
+ json_text,
16
+ optional_string_argument,
17
+ optional_string_list_argument,
18
+ truncate_text,
19
+ )
20
+ from devcouncil.storage.db import Database
21
+ from devcouncil.storage.repositories import TaskRepository
22
+
23
+
24
+ async def git_diff(root: Path, paths: list[str], staged: bool) -> dict[str, object]:
25
+ """Compute a (optionally path-scoped, optionally staged) git diff."""
26
+ diff_args = ["git", "diff"]
27
+ numstat_args = ["git", "diff", "--numstat"]
28
+ namestatus_args = ["git", "diff", "--name-status"]
29
+ if staged:
30
+ for args in (diff_args, numstat_args, namestatus_args):
31
+ args.append("--cached")
32
+ if paths:
33
+ for args in (diff_args, numstat_args, namestatus_args):
34
+ args.append("--")
35
+ args.extend(paths)
36
+
37
+ def _run(args: list[str]) -> subprocess.CompletedProcess[str]:
38
+ return subprocess.run(
39
+ args, cwd=root, capture_output=True, text=True,
40
+ encoding="utf-8", errors="replace", timeout=CLI_TIMEOUT_SECONDS,
41
+ )
42
+
43
+ try:
44
+ loop = asyncio.get_event_loop()
45
+ diff_proc, numstat_proc, namestatus_proc = await asyncio.gather(
46
+ loop.run_in_executor(None, _run, diff_args),
47
+ loop.run_in_executor(None, _run, numstat_args),
48
+ loop.run_in_executor(None, _run, namestatus_args),
49
+ )
50
+ except (OSError, subprocess.TimeoutExpired) as exc:
51
+ return {"ok": False, "files": [], "unified_diff": "", "truncated": False, "error": str(exc)}
52
+
53
+ status_by_path: dict[str, str] = {}
54
+ for line in namestatus_proc.stdout.splitlines():
55
+ parts = line.split("\t")
56
+ if len(parts) >= 2:
57
+ status_by_path[parts[-1].replace("\\", "/")] = parts[0]
58
+
59
+ files: list[dict[str, object]] = []
60
+ for line in numstat_proc.stdout.splitlines():
61
+ parts = line.split("\t")
62
+ if len(parts) < 3:
63
+ continue
64
+ added_str, deleted_str, file_path = parts[0], parts[1], parts[-1]
65
+ file_path = file_path.replace("\\", "/")
66
+ files.append({
67
+ "path": file_path,
68
+ "status": status_by_path.get(file_path, "M"),
69
+ "additions": int(added_str) if added_str.isdigit() else 0,
70
+ "deletions": int(deleted_str) if deleted_str.isdigit() else 0,
71
+ })
72
+
73
+ unified_diff, truncated = truncate_text(diff_proc.stdout)
74
+ return {"ok": True, "files": files, "unified_diff": unified_diff, "truncated": truncated, "staged": staged}
75
+
76
+
77
+ async def handle_get_diff(root: Path, db: Database | None, arguments: dict) -> list[TextContent]:
78
+ if not is_git_repo(root):
79
+ return error_text("get_diff requires a git repository.", code="not_a_git_repo")
80
+ task_id = optional_string_argument(arguments, "task_id")
81
+ if task_id == "":
82
+ return error_text("task_id must be a string", code="invalid_arguments", argument="task_id")
83
+ explicit_paths, arg_error = optional_string_list_argument(arguments, "paths")
84
+ if arg_error:
85
+ return arg_error
86
+ staged_value = arguments.get("staged", False)
87
+ if not isinstance(staged_value, bool):
88
+ return error_text("staged must be a boolean", code="invalid_arguments", argument="staged")
89
+ scope_paths: list[str] = list(explicit_paths)
90
+ if task_id and db:
91
+ with db.get_session() as session:
92
+ task = TaskRepository(session).get_by_id(task_id)
93
+ if task is None:
94
+ return error_text(f"Task {task_id} not found.", code="not_found", task_id=task_id)
95
+ for planned in task.planned_files:
96
+ p = planned.path.replace("\\", "/")
97
+ if p not in scope_paths:
98
+ scope_paths.append(p)
99
+ return json_text(await git_diff(root, scope_paths, staged_value))
@@ -0,0 +1,36 @@
1
+ """Code-review graph context 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
+ json_text,
12
+ run_cli_command,
13
+ )
14
+
15
+
16
+ async def handle_graph_context(root: Path, arguments: dict) -> list[TextContent]:
17
+ files = arguments.get("files", [])
18
+ if not isinstance(files, list):
19
+ files = []
20
+ file_args: list[str] = []
21
+ for item in files:
22
+ if isinstance(item, str) and item:
23
+ file_args.extend(["--file", item])
24
+ result = run_cli_command(["graph-context", "--json", *file_args], root)
25
+ if result.get("ok"):
26
+ try:
27
+ payload = json.loads(str(result.get("stdout") or "{}"))
28
+ return json_text(payload)
29
+ except json.JSONDecodeError:
30
+ pass
31
+ from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
32
+
33
+ context = CodeReviewGraphAdapter(root).get_context(
34
+ [file for file in files if isinstance(file, str)]
35
+ )
36
+ return [TextContent(type="text", text=context.model_dump_json(indent=2))]
@@ -0,0 +1,53 @@
1
+ """Agent handoff MCP tool handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from mcp.types import TextContent
8
+
9
+ from devcouncil.integrations.mcp.util import (
10
+ json_text,
11
+ parse_cli_json,
12
+ required_string_argument,
13
+ run_cli_command,
14
+ )
15
+
16
+
17
+ async def handle_handoff_agent(root: Path, db: object, arguments: dict) -> list[TextContent]:
18
+ del db # routed through CLI service layer
19
+ task_id, arg_error = required_string_argument(arguments, "task_id")
20
+ if arg_error:
21
+ return arg_error
22
+ lease_token, arg_error = required_string_argument(arguments, "lease_token")
23
+ if arg_error:
24
+ return arg_error
25
+ from_agent, arg_error = required_string_argument(arguments, "from_agent")
26
+ if arg_error:
27
+ return arg_error
28
+ to_agent, arg_error = required_string_argument(arguments, "to_agent")
29
+ if arg_error:
30
+ return arg_error
31
+ assert task_id is not None and lease_token is not None
32
+ cli_args = [
33
+ "handoff-leased", task_id,
34
+ "--lease-token", lease_token,
35
+ "--from", from_agent or "",
36
+ "--to", to_agent or "",
37
+ "--json",
38
+ ]
39
+ instruction = str(arguments.get("instruction") or "")
40
+ if instruction:
41
+ cli_args.extend(["--instruction", instruction])
42
+ payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
43
+ if cli_error:
44
+ return cli_error
45
+ assert payload is not None
46
+ if not payload.get("ok"):
47
+ from devcouncil.integrations.mcp.util import error_text
48
+ return error_text(
49
+ str(payload.get("error") or "handoff failed"),
50
+ code=str(payload.get("code") or "handoff_failed"),
51
+ task_id=task_id,
52
+ )
53
+ return json_text(payload)
@@ -0,0 +1,30 @@
1
+ """Knowledge selection 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
+ json_text,
12
+ required_string_argument,
13
+ run_cli_command,
14
+ )
15
+ from devcouncil.knowledge.knowledge_select import select_knowledge_payload
16
+
17
+
18
+ async def handle_select_knowledge(root: Path, arguments: dict) -> list[TextContent]:
19
+ goal, arg_error = required_string_argument(arguments, "goal")
20
+ if arg_error:
21
+ return arg_error
22
+ assert goal is not None
23
+ result = run_cli_command(["okf", "select", "--json", "--goal", goal], root)
24
+ if result.get("ok"):
25
+ try:
26
+ payload = json.loads(str(result.get("stdout") or "{}"))
27
+ return json_text(payload)
28
+ except json.JSONDecodeError:
29
+ pass
30
+ return json_text(select_knowledge_payload(root, goal))
@@ -0,0 +1,70 @@
1
+ """Task lease MCP tool handlers (release, renew, list)."""
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_release_task(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
+ assert task_id is not None and lease_token is not None
27
+ payload, cli_error = parse_cli_json(
28
+ run_cli_command(["release", task_id, "--lease-token", lease_token, "--json"], root),
29
+ )
30
+ if cli_error:
31
+ return cli_error
32
+ assert payload is not None
33
+ return json_text(payload)
34
+
35
+
36
+ async def handle_renew_lease(root: Path, db: object, arguments: dict) -> list[TextContent]:
37
+ del db # routed through CLI service layer
38
+ task_id, arg_error = required_string_argument(arguments, "task_id")
39
+ if arg_error:
40
+ return arg_error
41
+ lease_token, arg_error = required_string_argument(arguments, "lease_token")
42
+ if arg_error:
43
+ return arg_error
44
+ assert task_id is not None and lease_token is not None
45
+ ttl_value = arguments.get("ttl_seconds")
46
+ if ttl_value is not None and (not isinstance(ttl_value, int) or isinstance(ttl_value, bool)):
47
+ return error_text("ttl_seconds must be an integer", code="invalid_arguments", argument="ttl_seconds")
48
+ cli_args = ["lease", "renew", task_id, "--lease-token", lease_token, "--json"]
49
+ if isinstance(ttl_value, int) and not isinstance(ttl_value, bool):
50
+ cli_args.extend(["--ttl-seconds", str(ttl_value)])
51
+ payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
52
+ if cli_error:
53
+ return cli_error
54
+ assert payload is not None
55
+ return json_text(payload)
56
+
57
+
58
+ async def handle_list_leases(root: Path, db: object, arguments: dict) -> list[TextContent]:
59
+ del db # routed through CLI service layer
60
+ active_only = arguments.get("active_only", True)
61
+ if not isinstance(active_only, bool):
62
+ return error_text("active_only must be a boolean", code="invalid_arguments", argument="active_only")
63
+ cli_args = ["lease", "list", "--json"]
64
+ if not active_only:
65
+ cli_args.append("--all")
66
+ payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
67
+ if cli_error:
68
+ return cli_error
69
+ assert payload is not None
70
+ return json_text(payload)