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,590 @@
1
+ """Task gate operations shared by CLI and MCP (verify, scope, evidence, policy, run)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import shlex
8
+ import subprocess
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from devcouncil.domain.evidence import CommandResult, DiffCoverageEvidence, DiffEvidence, TestEvidence
13
+ from devcouncil.execution.hook_policy import HookPolicy
14
+ from devcouncil.execution.lease_validation import require_valid_lease
15
+ from devcouncil.integrations.mcp.util import allowed_next_tools, read_log_file, truncate_text
16
+ from devcouncil.storage.native import ShellCommandRepository, TaskLeaseRepository
17
+ from devcouncil.storage.repositories import (
18
+ EvidenceRepository,
19
+ GapRepository,
20
+ RequirementRepository,
21
+ TaskRepository,
22
+ )
23
+ from devcouncil.verification.command_evidence import (
24
+ command_has_acceptance_evidence,
25
+ command_is_trivial_evidence,
26
+ )
27
+ from devcouncil.verification.next_actions import split_next_actions
28
+
29
+ RECORD_COMMAND_STATUSES = frozenset({"started", "finished", "failed", "blocked"})
30
+ CLI_TIMEOUT_SECONDS = 120
31
+
32
+
33
+ def _db(project_root: Path):
34
+ from devcouncil.cli.commands.init import initialize_project
35
+ from devcouncil.storage.db import get_db
36
+
37
+ if not (project_root / ".devcouncil" / "state.sqlite").is_file():
38
+ initialize_project(project_root, quiet=True)
39
+ return get_db(project_root)
40
+
41
+
42
+ def _build_router(project_root: Path):
43
+ try:
44
+ from devcouncil.app.config import get_api_key, load_config
45
+ from devcouncil.llm.provider import create_provider, validate_model_provider
46
+ from devcouncil.llm.router import ModelRouter
47
+
48
+ config = load_config(project_root)
49
+ validate_model_provider(config.models.provider)
50
+ api_key = get_api_key(config.models.provider, project_root)
51
+ provider = create_provider(
52
+ config.models.provider, api_key, project_root=project_root, provider_prefs=config.provider,
53
+ )
54
+ role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
55
+ return ModelRouter(provider, role_config, project_root=project_root)
56
+ except Exception:
57
+ return None
58
+
59
+
60
+ def verify_task_payload(
61
+ project_root: Path,
62
+ *,
63
+ task_id: str,
64
+ lease_token: str,
65
+ sandbox: str = "local",
66
+ ) -> dict[str, Any]:
67
+ if sandbox in {"docker", "nix"}:
68
+ return {
69
+ "ok": False,
70
+ "code": "unsupported_sandbox",
71
+ "reason": f"Sandbox {sandbox} is not available in this build.",
72
+ "sandbox": sandbox,
73
+ }
74
+ db = _db(project_root)
75
+ if not db:
76
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
77
+
78
+ with db.get_session() as session:
79
+ lease_error = require_valid_lease(session, task_id, lease_token)
80
+ if lease_error:
81
+ return lease_error
82
+ task_repo = TaskRepository(session)
83
+ task = task_repo.get_by_id(task_id)
84
+ if not task:
85
+ return {"ok": False, "error": f"Task {task_id} not found.", "code": "not_found", "task_id": task_id}
86
+ from devcouncil.execution.stop_gate_verify_cache import record_verify_cache
87
+ from devcouncil.verification.verifier import Verifier
88
+
89
+ GapRepository(session).delete_for_task(task_id)
90
+ EvidenceRepository(session).delete_for_task(task_id)
91
+ verifier = Verifier(project_root, router=_build_router(project_root))
92
+ evidence_gaps, evidence = asyncio.run(
93
+ verifier.verify_task(task, RequirementRepository(session).get_all()),
94
+ )
95
+ gaps = evidence_gaps
96
+ for gap in gaps:
97
+ GapRepository(session).save(gap)
98
+ for ev in evidence:
99
+ if isinstance(ev, CommandResult):
100
+ EvidenceRepository(session).save_command_result(task_id, ev)
101
+ elif isinstance(ev, DiffCoverageEvidence):
102
+ EvidenceRepository(session).save_diff_coverage_evidence(ev)
103
+ elif isinstance(ev, DiffEvidence):
104
+ EvidenceRepository(session).save_diff_evidence(ev)
105
+ elif isinstance(ev, TestEvidence):
106
+ EvidenceRepository(session).save_test_evidence(ev, task_id)
107
+ task.status = "blocked" if any(g.blocking for g in gaps) else "verified"
108
+ task_repo.save(task)
109
+ blocking = [g.model_dump() for g in gaps if g.blocking]
110
+ blocking_actions, advisory_actions = split_next_actions(gaps)
111
+ record_verify_cache(
112
+ project_root,
113
+ task_id=task_id,
114
+ status=task.status,
115
+ blocking_gaps=len(blocking),
116
+ next_actions=[a.model_dump() for a in blocking_actions[:20]],
117
+ passed=len(blocking) == 0,
118
+ )
119
+ outcome = verifier.last_outcome
120
+ return {
121
+ "ok": True,
122
+ "task_id": task_id,
123
+ "status": task.status,
124
+ "sandbox": sandbox,
125
+ "blocking_gaps": blocking,
126
+ "next_actions": [a.model_dump() for a in blocking_actions],
127
+ "advisory_actions": [a.model_dump() for a in advisory_actions],
128
+ "allowed_next_tools": allowed_next_tools(task.status, len(blocking) > 0),
129
+ "passed": len(blocking) == 0,
130
+ "verification_mode": outcome.mode if outcome else "unknown",
131
+ "compiler_active": outcome.compiler_active if outcome else False,
132
+ "diff_empty": outcome.diff_empty if outcome else False,
133
+ "coverage_measured": outcome.coverage_measured if outcome else False,
134
+ "coverage_skipped_reason": outcome.coverage_skipped_reason if outcome else None,
135
+ "difficulty": outcome.difficulty if outcome else None,
136
+ "rigor_applied": list(outcome.rigor_applied) if outcome else [],
137
+ }
138
+
139
+
140
+ def attach_committed_range_payload(project_root: Path, *, task_id: str, lease_token: str, base: str, head: str = "HEAD") -> dict[str, Any]:
141
+ """Attach an existing committed range to a leased task's checkpoint refs."""
142
+ db = _db(project_root)
143
+ if not db:
144
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
145
+
146
+ with db.get_session() as session:
147
+ lease_error = require_valid_lease(session, task_id, lease_token)
148
+ if lease_error:
149
+ return lease_error
150
+ if not TaskRepository(session).get_by_id(task_id):
151
+ return {"ok": False, "error": f"Task {task_id} not found.", "code": "not_found", "task_id": task_id}
152
+
153
+ try:
154
+ base_sha = subprocess.check_output(["git", "rev-parse", "--verify", f"{base}^{{commit}}"], cwd=project_root, text=True, timeout=CLI_TIMEOUT_SECONDS).strip()
155
+ head_sha = subprocess.check_output(["git", "rev-parse", "--verify", f"{head}^{{commit}}"], cwd=project_root, text=True, timeout=CLI_TIMEOUT_SECONDS).strip()
156
+ subprocess.run(["git", "merge-base", "--is-ancestor", base_sha, head_sha], cwd=project_root, check=True, timeout=CLI_TIMEOUT_SECONDS)
157
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
158
+ return {"ok": False, "code": "invalid_commit_range", "error": "--base and --head must resolve to an ancestor commit range.", "task_id": task_id}
159
+ if base_sha == head_sha:
160
+ return {"ok": False, "code": "invalid_commit_range", "error": "--base must be a strict ancestor of --head.", "task_id": task_id}
161
+
162
+ refs = {"before": f"refs/devcouncil/tasks/{task_id}/before", "after": f"refs/devcouncil/tasks/{task_id}/after"}
163
+ subprocess.run(["git", "update-ref", refs["before"], base_sha], cwd=project_root, check=True, timeout=CLI_TIMEOUT_SECONDS)
164
+ subprocess.run(["git", "update-ref", refs["after"], head_sha], cwd=project_root, check=True, timeout=CLI_TIMEOUT_SECONDS)
165
+ return {"ok": True, "task_id": task_id, "base": base_sha, "head": head_sha, "range": f"{base_sha}..{head_sha}", "refs": refs}
166
+
167
+
168
+ def update_task_scope_payload(
169
+ project_root: Path,
170
+ *,
171
+ task_id: str,
172
+ lease_token: str,
173
+ expected_tests: list[str] | None = None,
174
+ allowed_commands: list[str] | None = None,
175
+ planned_files: list[str] | None = None,
176
+ create_planned_files: list[str] | None = None,
177
+ ) -> dict[str, Any]:
178
+ db = _db(project_root)
179
+ if not db:
180
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
181
+
182
+ expected_tests = expected_tests or []
183
+ allowed_commands = allowed_commands or []
184
+ planned_files = planned_files or []
185
+ create_planned_files = create_planned_files or []
186
+ with db.get_session() as session:
187
+ lease_error = require_valid_lease(session, task_id, lease_token)
188
+ if lease_error:
189
+ return lease_error
190
+ task_repo = TaskRepository(session)
191
+ task = task_repo.get_by_id(task_id)
192
+ if not task:
193
+ return {"ok": False, "error": f"Task {task_id} not found.", "code": "not_found", "task_id": task_id}
194
+ rejected_commands: list[str] = []
195
+ for cmd in allowed_commands:
196
+ if cmd in task.allowed_commands:
197
+ continue
198
+ if command_is_trivial_evidence(cmd):
199
+ rejected_commands.append(cmd)
200
+ continue
201
+ task.allowed_commands.append(cmd)
202
+ if cmd not in task.agent_appended_allowed_commands:
203
+ task.agent_appended_allowed_commands.append(cmd)
204
+ rejected_tests: list[str] = []
205
+ for test in expected_tests:
206
+ if test in task.expected_tests:
207
+ continue
208
+ if not command_has_acceptance_evidence(test):
209
+ rejected_tests.append(test)
210
+ continue
211
+ task.expected_tests.append(test)
212
+ if test not in task.agent_appended_expected_tests:
213
+ task.agent_appended_expected_tests.append(test)
214
+ rejected_planned_files: list[str] = []
215
+ if planned_files:
216
+ from fnmatch import fnmatch
217
+
218
+ from devcouncil.domain.task import PlannedFile
219
+ from devcouncil.execution.policy_engine import (
220
+ SECRET_PATH_PATTERNS,
221
+ TaskPolicyEngine,
222
+ )
223
+
224
+ engine = TaskPolicyEngine(project_root)
225
+ existing = {pf.path.replace("\\", "/") for pf in task.planned_files}
226
+ for raw in planned_files:
227
+ path = raw.replace("\\", "/")
228
+ if path.startswith("./"):
229
+ path = path[2:]
230
+ if not path or path in existing:
231
+ continue
232
+ normalized = engine._normalize_path(path)
233
+ if any(fnmatch(normalized, pat) for pat in SECRET_PATH_PATTERNS):
234
+ rejected_planned_files.append(raw)
235
+ continue
236
+ if engine._matches_restricted(normalized):
237
+ rejected_planned_files.append(raw)
238
+ continue
239
+ # Modify-op only: agents may append an existing caller to wire a new
240
+ # file, not authorize creates/deletes through this surface.
241
+ if not (project_root / normalized).is_file():
242
+ rejected_planned_files.append(raw)
243
+ continue
244
+ task.planned_files.append(PlannedFile(
245
+ path=normalized,
246
+ reason="agent-appended scope (wire caller)",
247
+ allowed_change="modify",
248
+ ))
249
+ existing.add(normalized)
250
+ if normalized not in task.agent_appended_planned_files:
251
+ task.agent_appended_planned_files.append(normalized)
252
+ if create_planned_files:
253
+ from fnmatch import fnmatch
254
+
255
+ from devcouncil.domain.task import PlannedFile
256
+ from devcouncil.execution.policy_engine import (
257
+ SECRET_PATH_PATTERNS,
258
+ TaskPolicyEngine,
259
+ )
260
+
261
+ engine = TaskPolicyEngine(project_root)
262
+ existing = {pf.path.replace("\\", "/") for pf in task.planned_files}
263
+ for raw in create_planned_files:
264
+ path = raw.replace("\\", "/")
265
+ if path.startswith("./"):
266
+ path = path[2:]
267
+ if not path or path in existing:
268
+ continue
269
+ normalized = engine._normalize_path(path)
270
+ if any(fnmatch(normalized, pat) for pat in SECRET_PATH_PATTERNS):
271
+ rejected_planned_files.append(raw)
272
+ continue
273
+ if engine._matches_restricted(normalized):
274
+ rejected_planned_files.append(raw)
275
+ continue
276
+ task.planned_files.append(PlannedFile(
277
+ path=normalized,
278
+ reason="agent-appended scope (create)",
279
+ allowed_change="create",
280
+ ))
281
+ existing.add(normalized)
282
+ if normalized not in task.agent_appended_planned_files:
283
+ task.agent_appended_planned_files.append(normalized)
284
+ task_repo.save(task)
285
+ return {
286
+ "ok": True,
287
+ "task_id": task_id,
288
+ "allowed_commands": task.allowed_commands,
289
+ "expected_tests": task.expected_tests,
290
+ "planned_files": [pf.model_dump() for pf in task.planned_files],
291
+ "agent_appended_planned_files": list(task.agent_appended_planned_files),
292
+ "rejected_expected_tests": rejected_tests,
293
+ "rejected_allowed_commands": rejected_commands,
294
+ "rejected_planned_files": rejected_planned_files,
295
+ }
296
+
297
+
298
+ def append_evidence_payload(
299
+ project_root: Path,
300
+ *,
301
+ task_id: str,
302
+ lease_token: str,
303
+ command: str,
304
+ summary: str,
305
+ exit_code: int = 0,
306
+ ) -> dict[str, Any]:
307
+ db = _db(project_root)
308
+ if not db:
309
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
310
+
311
+ with db.get_session() as session:
312
+ lease_error = require_valid_lease(session, task_id, lease_token)
313
+ if lease_error:
314
+ return lease_error
315
+ EvidenceRepository(session).save_command_result(
316
+ task_id,
317
+ CommandResult(
318
+ command=command,
319
+ exit_code=exit_code,
320
+ stdout_path="",
321
+ stderr_path="",
322
+ summary=summary,
323
+ ),
324
+ )
325
+ return {"ok": True, "task_id": task_id, "recorded": True}
326
+
327
+
328
+ def get_evidence_payload(
329
+ project_root: Path,
330
+ *,
331
+ task_id: str,
332
+ command_filter: str | None = None,
333
+ limit: int = 20,
334
+ ) -> dict[str, Any]:
335
+ db = _db(project_root)
336
+ if not db:
337
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
338
+
339
+ with db.get_session() as session:
340
+ results = EvidenceRepository(session).get_command_results_for_task(task_id)
341
+ evidence_rows: list[dict[str, object]] = []
342
+ for result in results:
343
+ if command_filter and command_filter not in result.command:
344
+ continue
345
+ stdout, stdout_truncated = truncate_text(read_log_file(result.stdout_path))
346
+ stderr, stderr_truncated = truncate_text(read_log_file(result.stderr_path))
347
+ evidence_rows.append({
348
+ "command": result.command,
349
+ "exit_code": result.exit_code,
350
+ "summary": result.summary,
351
+ "stdout": stdout,
352
+ "stderr": stderr,
353
+ "truncated": stdout_truncated or stderr_truncated,
354
+ })
355
+ if len(evidence_rows) >= limit:
356
+ break
357
+ return {"ok": True, "task_id": task_id, "evidence": evidence_rows}
358
+
359
+
360
+ def policy_check_write_payload(
361
+ project_root: Path,
362
+ *,
363
+ path: str,
364
+ task_id: str | None = None,
365
+ ) -> dict[str, Any]:
366
+ db = _db(project_root)
367
+ if not db:
368
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
369
+
370
+ with db.get_session() as session:
371
+ task_repo = TaskRepository(session)
372
+ if task_id:
373
+ task = task_repo.get_by_id(task_id)
374
+ else:
375
+ running = [task for task in task_repo.get_all() if task.status == "running"]
376
+ task = running[0] if running else None
377
+ decision = HookPolicy(project_root=project_root).evaluate_file_write(path, task)
378
+ return {
379
+ "action": decision.action,
380
+ "allowed": decision.allowed,
381
+ "reason": decision.reason,
382
+ "target": decision.target,
383
+ "task_id": task.id if task else None,
384
+ }
385
+
386
+
387
+ def record_command_payload(
388
+ project_root: Path,
389
+ *,
390
+ task_id: str,
391
+ lease_token: str,
392
+ command: str,
393
+ status: str,
394
+ exit_code: int | None = None,
395
+ reason: str = "",
396
+ ) -> dict[str, Any]:
397
+ if status not in RECORD_COMMAND_STATUSES:
398
+ return {
399
+ "ok": False,
400
+ "error": f"status must be one of {sorted(RECORD_COMMAND_STATUSES)}",
401
+ "code": "invalid_arguments",
402
+ "argument": "status",
403
+ }
404
+ db = _db(project_root)
405
+ if not db:
406
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
407
+
408
+ with db.get_session() as session:
409
+ lease_error = require_valid_lease(session, task_id, lease_token)
410
+ if lease_error:
411
+ return lease_error
412
+ ShellCommandRepository(session).record(
413
+ task_id,
414
+ command,
415
+ status,
416
+ exit_code=exit_code if isinstance(exit_code, int) else None,
417
+ reason=reason,
418
+ )
419
+ return {"ok": True, "task_id": task_id, "recorded": True}
420
+
421
+
422
+ def next_task_payload(
423
+ project_root: Path,
424
+ *,
425
+ status_filter: str | None = None,
426
+ client_id: str | None = None,
427
+ ) -> dict[str, Any]:
428
+ del client_id # reserved for future fleet routing
429
+ db = _db(project_root)
430
+ if not db:
431
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
432
+
433
+ with db.get_session() as session:
434
+ tasks = TaskRepository(session).get_all()
435
+ leased_task_ids = {
436
+ lease.task_id
437
+ for lease, expired in TaskLeaseRepository(session).list_leases(active_only=True)
438
+ if not expired
439
+ }
440
+ blocking_by_task: dict[str, int] = {}
441
+ for gap in GapRepository(session).get_all():
442
+ if gap.blocking and gap.task_id:
443
+ blocking_by_task[gap.task_id] = blocking_by_task.get(gap.task_id, 0) + 1
444
+ done_ids = {t.id for t in tasks if t.status in {"verified", "done"}}
445
+ wanted_statuses = {status_filter} if status_filter else {"planned", "ready"}
446
+ candidates = []
447
+ for task in tasks:
448
+ if task.status not in wanted_statuses:
449
+ continue
450
+ if task.id in leased_task_ids:
451
+ continue
452
+ if any(dep not in done_ids for dep in task.depends_on):
453
+ continue
454
+ candidates.append(task)
455
+ if not candidates:
456
+ return {
457
+ "ok": True,
458
+ "task": None,
459
+ "reason": "No unblocked, unleased task is available.",
460
+ }
461
+ candidates.sort(key=lambda t: (len(t.depends_on), t.id))
462
+ chosen = candidates[0]
463
+ blocking_count = blocking_by_task.get(chosen.id, 0)
464
+ return {
465
+ "ok": True,
466
+ "task": chosen.model_dump(),
467
+ "blocking_gap_count": blocking_count,
468
+ "ready_to_checkout": blocking_count == 0,
469
+ "allowed_next_tools": allowed_next_tools(chosen.status, blocking_count > 0),
470
+ }
471
+
472
+
473
+ def run_command_payload(
474
+ project_root: Path,
475
+ *,
476
+ task_id: str,
477
+ lease_token: str,
478
+ command: str,
479
+ ) -> dict[str, Any]:
480
+ from devcouncil.utils.subprocess_env import clean_subprocess_env
481
+
482
+ normalized = " ".join(command.split())
483
+ db = _db(project_root)
484
+ if not db:
485
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
486
+
487
+ with db.get_session() as session:
488
+ lease_error = require_valid_lease(session, task_id, lease_token)
489
+ if lease_error:
490
+ return lease_error
491
+ task = TaskRepository(session).get_by_id(task_id)
492
+ if not task:
493
+ return {"ok": False, "error": f"Task {task_id} not found.", "code": "not_found", "task_id": task_id}
494
+ from devcouncil.execution.policy_engine import TaskPolicyEngine
495
+
496
+ policy_decision = TaskPolicyEngine(project_root).evaluate_command(normalized, task)
497
+ if policy_decision.action == "deny":
498
+ ShellCommandRepository(session).record(
499
+ task_id, normalized, "blocked", reason=policy_decision.reason,
500
+ )
501
+ return {
502
+ "ok": False,
503
+ "error": policy_decision.reason or "Command is not in the task allowlist.",
504
+ "code": "command_not_allowed",
505
+ "task_id": task_id,
506
+ "command": normalized,
507
+ }
508
+ try:
509
+ args = shlex.split(normalized, posix=(os.name != "nt"))
510
+ completed = subprocess.run(
511
+ args,
512
+ cwd=project_root,
513
+ capture_output=True,
514
+ text=True,
515
+ encoding="utf-8",
516
+ errors="replace",
517
+ env=clean_subprocess_env(),
518
+ timeout=CLI_TIMEOUT_SECONDS,
519
+ )
520
+ exit_code = completed.returncode
521
+ stdout, stdout_truncated = truncate_text(completed.stdout)
522
+ stderr, stderr_truncated = truncate_text(completed.stderr)
523
+ timed_out = False
524
+ except subprocess.TimeoutExpired as exc:
525
+ exit_code = None
526
+ stdout, stdout_truncated = truncate_text(exc.output)
527
+ stderr, stderr_truncated = truncate_text(exc.stderr)
528
+ timed_out = True
529
+ except (FileNotFoundError, OSError, ValueError) as exc:
530
+ ShellCommandRepository(session).record(
531
+ task_id, normalized, "failed", reason=str(exc),
532
+ )
533
+ return {
534
+ "ok": False,
535
+ "error": f"Could not run command: {exc}",
536
+ "code": "run_failed",
537
+ "task_id": task_id,
538
+ }
539
+ ShellCommandRepository(session).record(
540
+ task_id,
541
+ normalized,
542
+ "finished" if exit_code == 0 else "failed",
543
+ exit_code=exit_code,
544
+ )
545
+ return {
546
+ "ok": exit_code == 0,
547
+ "task_id": task_id,
548
+ "exit_code": exit_code,
549
+ "stdout": stdout,
550
+ "stderr": stderr,
551
+ "truncated": stdout_truncated or stderr_truncated,
552
+ "timed_out": timed_out,
553
+ }
554
+
555
+
556
+ def handoff_agent_payload(
557
+ project_root: Path,
558
+ *,
559
+ task_id: str,
560
+ lease_token: str,
561
+ from_agent: str,
562
+ to_agent: str,
563
+ instruction: str = "",
564
+ ) -> dict[str, Any]:
565
+ db = _db(project_root)
566
+ if not db:
567
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
568
+
569
+ with db.get_session() as session:
570
+ lease_error = require_valid_lease(session, task_id, lease_token)
571
+ if lease_error:
572
+ return lease_error
573
+ try:
574
+ from devcouncil.execution.handoff import HandoffService
575
+
576
+ manifest, handoff_path, run_id = HandoffService(project_root).create(
577
+ task_id,
578
+ from_agent,
579
+ to_agent,
580
+ instruction=instruction,
581
+ )
582
+ return {
583
+ "ok": True,
584
+ "task_id": task_id,
585
+ "manifest_path": str(handoff_path),
586
+ "run_id": run_id,
587
+ "manifest": manifest.model_dump(),
588
+ }
589
+ except ValueError as exc:
590
+ return {"ok": False, "error": str(exc), "code": "handoff_failed", "task_id": task_id}
@@ -17,6 +17,7 @@ from devcouncil.app.errors import ExecutionError
17
17
  from devcouncil.execution.patch import PatchEngine
18
18
  from devcouncil.execution.paths import resolve_project_path
19
19
  from devcouncil.telemetry.traces import TraceLogger
20
+ from devcouncil.telemetry.stages import log_step
20
21
  from devcouncil.utils.redaction import redact_string
21
22
 
22
23
  logger = logging.getLogger(__name__)
@@ -50,6 +51,12 @@ class TaskRunner:
50
51
  self._validate_path_within_root(path)
51
52
  self.permissions.validate_action("file_write", path, task, operation=operation)
52
53
  applied = self.patch_engine.apply_patch(patch)
54
+ log_step(
55
+ f"task_runner/apply_patch: {'applied' if applied else 'failed'}",
56
+ project_root=self.project_root,
57
+ task_id=task.id,
58
+ paths=sorted(changes),
59
+ )
53
60
  self.tracer.log_event(
54
61
  "tool_patch_applied",
55
62
  {"paths": sorted(changes), "success": applied},
@@ -144,6 +151,12 @@ class TaskRunner:
144
151
  stderr_path = self._save_command_log(task.id, command, "stderr", stderr)
145
152
  stdout_summary = redact_string(stdout[-500:])
146
153
  stderr_summary = redact_string(stderr[-500:])
154
+ log_step(
155
+ f"task_runner/run_command: exit {result.returncode}",
156
+ project_root=self.project_root,
157
+ task_id=task.id,
158
+ command=command,
159
+ )
147
160
  self.tracer.log_event(
148
161
  "command_executed",
149
162
  {"command": command, "exit_code": result.returncode},
@@ -171,6 +184,12 @@ class TaskRunner:
171
184
  try:
172
185
  full_path.parent.mkdir(parents=True, exist_ok=True)
173
186
  full_path.write_text(content, encoding="utf-8")
187
+ log_step(
188
+ f"task_runner/write_file: {path}",
189
+ project_root=self.project_root,
190
+ task_id=task.id,
191
+ operation=operation,
192
+ )
174
193
  self.tracer.log_event(
175
194
  "file_written",
176
195
  {"path": path},