devcouncil 0.3.1 → 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
@@ -2,11 +2,14 @@ import logging
2
2
  import subprocess
3
3
  import sys
4
4
  from pathlib import Path
5
+
6
+ from devcouncil.executors.transient_retry import run_subprocess_with_transient_retry
5
7
  from rich.console import Console
6
8
  from devcouncil.domain.task import Task
7
9
  from devcouncil.domain.requirement import Requirement
8
10
  from devcouncil.execution.executor import Executor, ExecutionResult
9
11
  from devcouncil.execution.prompt_builder import PromptBuilder
12
+ from devcouncil.telemetry.stages import log_step
10
13
 
11
14
  console = Console()
12
15
  logger = logging.getLogger(__name__)
@@ -17,7 +20,12 @@ class MiniSWEExecutor(Executor):
17
20
 
18
21
  def run_task(self, task: Task, requirements: list[Requirement]) -> ExecutionResult:
19
22
  builder = PromptBuilder(self.project_root)
23
+ from devcouncil.planning.correction_manifest import repair_prompt_prefix
24
+
20
25
  task_prompt = builder.build_task_prompt(task, requirements)
26
+ prefix = repair_prompt_prefix(self.project_root, task.id)
27
+ if prefix:
28
+ task_prompt = f"{prefix}{task_prompt}"
21
29
 
22
30
  # Write temporary instruction file for mini-SWE-agent
23
31
  instruction_file = self.project_root / ".devcouncil" / f"{task.id}-mini-swe-task.md"
@@ -25,6 +33,11 @@ class MiniSWEExecutor(Executor):
25
33
  instruction_file.write_text(task_prompt, encoding="utf-8")
26
34
 
27
35
  logger.info("mini-SWE-agent starting for %s", task.id)
36
+ log_step(
37
+ f"executor/mini-swe: starting task {task.id}",
38
+ project_root=self.project_root,
39
+ task_id=task.id,
40
+ )
28
41
  console.print(f"Starting [bold]mini-SWE-agent[/bold] for task {task.id}...")
29
42
 
30
43
  # In a real implementation, we'd invoke the agent CLI
@@ -42,24 +55,53 @@ class MiniSWEExecutor(Executor):
42
55
  console.print("[yellow]Note: mini_swe_agent must be installed in the environment.[/yellow]")
43
56
 
44
57
  try:
45
- result = subprocess.run(
46
- cmd,
47
- capture_output=True,
48
- text=True,
49
- encoding="utf-8",
50
- errors="replace",
51
- cwd=self.project_root,
52
- timeout=1800,
58
+ def _run_once():
59
+ return subprocess.run(
60
+ cmd,
61
+ capture_output=True,
62
+ text=True,
63
+ encoding="utf-8",
64
+ errors="replace",
65
+ cwd=self.project_root,
66
+ timeout=1800,
67
+ )
68
+
69
+ result = run_subprocess_with_transient_retry(
70
+ self.project_root,
71
+ label="mini-SWE-agent",
72
+ task_id=task.id,
73
+ run_once=_run_once,
53
74
  )
54
75
  self._write_log(task.id, result)
55
76
  if result.returncode != 0:
56
77
  logger.error("mini-SWE-agent exited %s for %s", result.returncode, task.id)
78
+ log_step(
79
+ f"executor/mini-swe: finished task {task.id}",
80
+ project_root=self.project_root,
81
+ task_id=task.id,
82
+ returncode=result.returncode,
83
+ success=False,
84
+ )
57
85
  console.print(f"[red]mini-SWE-agent exited with {result.returncode}.[/red]")
58
86
  return ExecutionResult(success=False, message='Execution failed')
59
87
  logger.info("mini-SWE-agent finished for %s", task.id)
88
+ log_step(
89
+ f"executor/mini-swe: finished task {task.id}",
90
+ project_root=self.project_root,
91
+ task_id=task.id,
92
+ returncode=0,
93
+ success=True,
94
+ )
60
95
  return ExecutionResult(success=True, message='Execution successful')
61
96
  except Exception as e:
62
97
  logger.exception("mini-SWE-agent error for %s: %s", task.id, e)
98
+ log_step(
99
+ f"executor/mini-swe: finished task {task.id}",
100
+ project_root=self.project_root,
101
+ task_id=task.id,
102
+ success=False,
103
+ error=str(e),
104
+ )
63
105
  console.print(f"[red]Error running mini-SWE-agent: {e}[/red]")
64
106
  return ExecutionResult(success=False, message='Execution failed')
65
107
 
@@ -1,4 +1,4 @@
1
- from typing import List, Dict, Any
1
+ from typing import Any, Coroutine, Dict, List, Optional, Tuple, TypeVar
2
2
  import asyncio
3
3
  import logging
4
4
  from rich.console import Console
@@ -12,6 +12,7 @@ from devcouncil.execution.context_builder import ContextBuilder
12
12
  from devcouncil.execution.prompt_builder import PromptBuilder
13
13
  from devcouncil.execution.paths import resolve_project_path
14
14
  from devcouncil.app.errors import ExecutionError
15
+ from devcouncil.telemetry.stages import log_step
15
16
 
16
17
  console = Console()
17
18
  logger = logging.getLogger(__name__)
@@ -20,6 +21,12 @@ logger = logging.getLogger(__name__)
20
21
  MAX_AGENT_STEPS = 10
21
22
  MAX_STRUCTURED_FAILURES = 2 # model can't produce a valid action -> give up cleanly
22
23
  MAX_CONSECUTIVE_PATCH_FAILURES = 3 # stop spinning on a patch the model can't fix
24
+ # How many times the agent may signal "finish", get BLOCKED by verification, and be
25
+ # handed the shared next-actions repair contract before we stop the closed loop. Mirrors
26
+ # the bounded self-repair budget the MCP/dev-go loops apply so native cannot spin forever.
27
+ MAX_VERIFY_ROUNDS = 3
28
+
29
+ _T = TypeVar("_T")
23
30
 
24
31
  class ToolCall(BaseModel):
25
32
  tool: str
@@ -31,33 +38,206 @@ class AgentAction(BaseModel):
31
38
  finish: bool = False
32
39
 
33
40
  class NativeAgent(Executor):
34
- def __init__(self, router: ModelRouter, task_runner: TaskRunner):
41
+ def __init__(self, router: ModelRouter, task_runner: TaskRunner, *, sandbox: str = "local"):
35
42
  self.router = router
36
43
  self.task_runner = task_runner
44
+ # Writes and verification are now routed through the SAME lease-gated policy path
45
+ # the MCP surface uses (execution/gated_write.py + HookPolicy, verification via the
46
+ # shared verify payload), so the native executor no longer bypasses the lease/scope
47
+ # gate with direct TaskRunner writes. task_runner is retained for run_command (which
48
+ # already honors execution.command_timeout).
49
+ self.project_root = task_runner.project_root
50
+ self.sandbox = sandbox
37
51
  # ContextBuilder is retained only for the cheap list_files file listing; the
38
52
  # implementation context itself uses the budgeted PromptBuilder so the native
39
53
  # executor gets the same repo-map orientation, symbol outlines, dependents and
40
54
  # context-window budgeting as the CLI executors (rather than a flat JSON dump).
41
55
  self.context_builder = ContextBuilder(task_runner.project_root)
42
56
  self.prompt_builder = PromptBuilder(task_runner.project_root)
57
+ self._lease_token: Optional[str] = None
43
58
 
44
59
  def run_task(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
45
60
  """Run the preview native executor behind the normal synchronous executor contract."""
46
- return asyncio.run(self._run_task_async(task, requirements))
61
+ root = self.task_runner.project_root
62
+ log_step(
63
+ f"executor/native: starting task {task.id}",
64
+ project_root=root,
65
+ task_id=task.id,
66
+ )
67
+ result = self._run_coroutine_from_sync(self._run_task_async(task, requirements))
68
+ log_step(
69
+ f"executor/native: finished task {task.id}",
70
+ project_root=root,
71
+ task_id=task.id,
72
+ success=result.success,
73
+ )
74
+ return result
75
+
76
+ @staticmethod
77
+ def _run_coroutine_from_sync(coro: Coroutine[Any, Any, _T]) -> _T:
78
+ """Run async work from sync code without breaking nested event loops.
79
+
80
+ ``asyncio.run()`` fails when a loop is already running (pytest-asyncio,
81
+ Jupyter). With no running loop we use ``asyncio.run()``; otherwise we run
82
+ the coroutine on a fresh loop in a worker thread.
83
+ """
84
+ import concurrent.futures
85
+
86
+ try:
87
+ asyncio.get_running_loop()
88
+ except RuntimeError:
89
+ return asyncio.run(coro)
90
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
91
+ return executor.submit(asyncio.run, coro).result()
92
+
93
+ # ------------------------------------------------------------------ lease-gated I/O
94
+ def _acquire_lease(self, task: Task) -> dict:
95
+ """Acquire a task lease so writes/verify run through the shared gated path."""
96
+ from devcouncil.execution.lease_ops import checkout_task_payload
97
+
98
+ return checkout_task_payload(self.project_root, task_id=task.id, client_id="native")
99
+
100
+ def _release_lease(self, task: Task, lease_token: str) -> None:
101
+ from devcouncil.execution.lease_ops import release_task_payload
102
+
103
+ try:
104
+ release_task_payload(self.project_root, task_id=task.id, lease_token=lease_token)
105
+ except Exception as exc: # a failed release only costs a TTL wait; never fail the run
106
+ logger.debug("Native lease release failed for %s: %s", task.id, exc)
107
+
108
+ def _gated_apply_patch(self, task: Task, patch: str) -> None:
109
+ """Apply a unified diff through execution/gated_write.py (lease + scope + HookPolicy).
110
+
111
+ Raises ExecutionError on rejection so the loop's existing patch-failure handling
112
+ (retry guidance, consecutive-failure abort) applies unchanged.
113
+ """
114
+ from devcouncil.execution.gated_write import apply_patch_payload
115
+
116
+ payload = apply_patch_payload(
117
+ self.project_root,
118
+ task_id=task.id,
119
+ lease_token=self._lease_token or "",
120
+ unified_diff=patch,
121
+ )
122
+ if not payload.get("ok"):
123
+ raise ExecutionError(self._write_rejection_reason(payload))
124
+
125
+ def _gated_write_file(self, task: Task, path: str, content: str) -> None:
126
+ """Write a whole file through execution/gated_write.py (lease + scope + HookPolicy)."""
127
+ from devcouncil.execution.gated_write import write_file_payload
128
+
129
+ payload = write_file_payload(
130
+ self.project_root,
131
+ task_id=task.id,
132
+ lease_token=self._lease_token or "",
133
+ rel_path=path,
134
+ content=content,
135
+ )
136
+ if not payload.get("ok"):
137
+ raise ExecutionError(self._write_rejection_reason(payload))
138
+
139
+ @staticmethod
140
+ def _write_rejection_reason(payload: dict) -> str:
141
+ rejected = payload.get("rejected_files") or []
142
+ if rejected:
143
+ first = rejected[0]
144
+ return f"Write to {first.get('path')} rejected: {first.get('reason')}"
145
+ return payload.get("error") or "Write rejected by the lease/scope gate."
146
+
147
+ def _verify_task(
148
+ self, task: Task, requirements: List[Requirement]
149
+ ) -> Tuple[bool, List[dict], List[dict]]:
150
+ """Verify through the shared surface and return (passed, blocking, advisory).
151
+
152
+ Local sandbox uses the same ``verify_task_payload`` the MCP verify loop uses, so the
153
+ ``split_next_actions`` repair contract is byte-for-byte identical across surfaces.
154
+ docker/nix run through ``verification/sandbox.py`` for coding-CLI parity.
155
+ """
156
+ if self.sandbox in {"docker", "nix"}:
157
+ return self._verify_via_sandbox(task, requirements)
158
+ from devcouncil.execution.task_gate_ops import verify_task_payload
159
+
160
+ payload = verify_task_payload(
161
+ self.project_root,
162
+ task_id=task.id,
163
+ lease_token=self._lease_token or "",
164
+ sandbox="local",
165
+ )
166
+ if not payload.get("ok"):
167
+ # A verification setup error (e.g. lease lost) is treated as "not passed" with the
168
+ # error surfaced as the single next action, so the loop can react rather than crash.
169
+ return False, [{"action": payload.get("error", "Verification failed to run.")}], []
170
+ return (
171
+ bool(payload.get("passed")),
172
+ list(payload.get("next_actions") or []),
173
+ list(payload.get("advisory_actions") or []),
174
+ )
175
+
176
+ def _verify_via_sandbox(
177
+ self, task: Task, requirements: List[Requirement]
178
+ ) -> Tuple[bool, List[dict], List[dict]]:
179
+ """Run the task's expected commands in a docker/nix sandbox (command_timeout-bounded)."""
180
+ from devcouncil.verification.sandbox import get_sandbox
181
+
182
+ commands = task.expected_tests or task.allowed_commands
183
+ result = get_sandbox(self.sandbox, self.project_root).run(task, commands, requirements)
184
+ if result.status == "unsupported":
185
+ reason = f"Sandbox '{self.sandbox}' is unavailable in this environment."
186
+ return False, [{"action": reason}], []
187
+ return result.status == "passed", [], []
188
+
189
+ @staticmethod
190
+ def _format_next_actions(blocking: List[dict], advisory: List[dict]) -> str:
191
+ """Render the shared next-actions contract into the repair message fed back to the
192
+ model — the same guidance an MCP agent reads from devcouncil_verify_task."""
193
+ lines = ["[Verification BLOCKED] The change did not pass. Address these before finishing:"]
194
+ for i, action in enumerate(blocking, 1):
195
+ text = action.get("action") or action.get("gap_type") or "Resolve the blocking gap."
196
+ loc = action.get("file")
197
+ if loc and action.get("line"):
198
+ loc = f"{loc}:{action['line']}"
199
+ suffix = f" ({loc})" if loc else ""
200
+ cmd = action.get("suggested_command")
201
+ cmd_txt = f" Suggested command: {cmd}" if cmd else ""
202
+ lines.append(f"{i}. {text}{suffix}{cmd_txt}")
203
+ if advisory:
204
+ lines.append("Advisory (non-blocking) signals worth addressing:")
205
+ for action in advisory:
206
+ lines.append(f"- {action.get('action') or action.get('gap_type')}")
207
+ lines.append(
208
+ "Apply the fix through apply_patch, then set finish=true again to re-verify."
209
+ )
210
+ return "\n".join(lines)
47
211
 
48
212
  async def _run_task_async(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
49
213
  logger.info("Native agent starting for %s (max_steps=%d)", task.id, MAX_AGENT_STEPS)
50
214
  console.print(f"Starting [bold]Native Executor[/bold] for task {task.id}...")
51
215
  console.print("[yellow]Native executor is preview quality; DevCouncil verification remains the completion gate.[/yellow]")
52
-
216
+
217
+ # Acquire a lease so every write and verify runs through the same lease/scope gate
218
+ # as the MCP surface. Without a lease the gated write path refuses the write.
219
+ lease = self._acquire_lease(task)
220
+ if not lease.get("ok"):
221
+ message = lease.get("error", "Could not acquire a task lease.")
222
+ logger.error("Native agent could not lease %s: %s", task.id, message)
223
+ console.print(f"[red]Native agent could not acquire a lease for {task.id}: {message}[/red]")
224
+ return ExecutionResult(success=False, message=f"Lease unavailable: {message}")
225
+ self._lease_token = lease["lease_token"]
226
+ try:
227
+ return await self._run_agent_loop(task, requirements)
228
+ finally:
229
+ token = self._lease_token
230
+ self._lease_token = None
231
+ if token:
232
+ self._release_lease(task, token)
233
+
234
+ async def _run_agent_loop(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
53
235
  # 1. Gather rich context (budgeted; includes repo-map orientation + symbol outlines)
54
236
  context_block = self.prompt_builder.build_task_prompt(task, requirements)
55
- from devcouncil.planning.correction_manifest import load_latest_correction_manifest
237
+ from devcouncil.planning.correction_manifest import repair_prompt_prefix
56
238
 
57
- correction = load_latest_correction_manifest(self.task_runner.project_root, task.id)
58
- correction_block = ""
59
- if correction is not None:
60
- correction_block = f"\nCorrection Manifest:\n{correction.model_dump_json(indent=2)}\n"
239
+ prefix = repair_prompt_prefix(self.task_runner.project_root, task.id)
240
+ correction_block = f"\n{prefix}" if prefix else ""
61
241
 
62
242
  system_prompt = f"""
63
243
  You are the DevCouncil Native Agent. Your goal is to implement the provided task.
@@ -78,7 +258,7 @@ Rules:
78
258
  4. Set 'finish' to true when you believe the task is complete and verified.
79
259
  """
80
260
  messages = [{"role": "system", "content": system_prompt}]
81
-
261
+
82
262
  # Initial task prompt
83
263
  messages.append({"role": "user", "content": f"Begin implementing task {task.id} based on the context provided."})
84
264
 
@@ -87,6 +267,7 @@ Rules:
87
267
  # unfixable patch.
88
268
  structured_failures = 0
89
269
  consecutive_patch_failures = 0
270
+ verify_rounds = 0
90
271
  for step in range(MAX_AGENT_STEPS):
91
272
  try:
92
273
  action = await self.router.complete_structured(
@@ -131,9 +312,33 @@ Rules:
131
312
  messages.append({"role": "assistant", "content": action.model_dump_json()})
132
313
 
133
314
  if action.finish:
134
- logger.info("Native agent signaled completion for %s at step %d", task.id, step + 1)
135
- console.print("[green]Native agent signaled completion.[/green]")
136
- return ExecutionResult(success=True, message="Agent signaled completion; pending DevCouncil verification")
315
+ # Closed loop: instead of trusting the agent's self-report, verify through the
316
+ # same gate as MCP and, when BLOCKED, feed the shared next-actions repair
317
+ # contract back so the agent can self-repair and re-verify.
318
+ logger.info("Native agent signaled completion for %s at step %d; verifying", task.id, step + 1)
319
+ console.print("[green]Native agent signaled completion; verifying...[/green]")
320
+ # verify_task_payload runs the verifier via asyncio.run(); run it in a worker
321
+ # thread so it gets its own event loop instead of nesting inside this one.
322
+ passed, blocking, advisory = await asyncio.to_thread(
323
+ self._verify_task, task, requirements
324
+ )
325
+ if passed:
326
+ logger.info("Native agent %s verified after %d repair round(s)", task.id, verify_rounds)
327
+ console.print("[green]Verification passed.[/green]")
328
+ return ExecutionResult(success=True, message="Native agent completed and verification passed.")
329
+ verify_rounds += 1
330
+ if verify_rounds >= MAX_VERIFY_ROUNDS:
331
+ logger.error("Native agent %s still blocked after %d verify round(s)", task.id, verify_rounds)
332
+ return ExecutionResult(
333
+ success=False,
334
+ message=(
335
+ f"Verification still blocked after {verify_rounds} repair round(s): "
336
+ f"{len(blocking)} blocking gap(s)."
337
+ ),
338
+ )
339
+ console.print(f"[yellow]Verification blocked ({len(blocking)} gap(s)); handing back repair guidance.[/yellow]")
340
+ messages.append({"role": "user", "content": self._format_next_actions(blocking, advisory)})
341
+ continue
137
342
 
138
343
  if not action.tool_calls:
139
344
  # No action and not finished — nudge instead of silently burning a step.
@@ -169,10 +374,10 @@ Rules:
169
374
  elif tool_call.tool == "apply_patch":
170
375
  if "path" in tool_call.args and "content" in tool_call.args:
171
376
  # Fallback for when the model can't produce a valid unified
172
- # diff. Routes through write_file, which enforces the same
173
- # planned-files permission check — no widening of scope.
174
- self.task_runner.write_file(
175
- tool_call.args["path"], tool_call.args["content"], task
377
+ # diff. Routes through the lease-gated write path, which enforces
378
+ # the same planned-files/scope policy — no widening of scope.
379
+ self._gated_write_file(
380
+ task, tool_call.args["path"], tool_call.args["content"]
176
381
  )
177
382
  consecutive_patch_failures = 0
178
383
  result_summary = f"Wrote {tool_call.args['path']} via path+content fallback."
@@ -184,7 +389,7 @@ Rules:
184
389
  "'diff --git a/<path> b/<path>', then '--- a/<path>' (or "
185
390
  "'--- /dev/null' for a new file), '+++ b/<path>', and '@@' hunks."
186
391
  )
187
- self.task_runner.apply_patch(patch, task)
392
+ self._gated_apply_patch(task, patch)
188
393
  consecutive_patch_failures = 0
189
394
  result_summary = "Successfully applied patch."
190
395
  elif tool_call.tool == "run_command":
@@ -192,7 +397,7 @@ Rules:
192
397
  result_summary = f"Command finished with exit code {cmd_result.exit_code}."
193
398
  else:
194
399
  raise ValueError(f"Unknown tool: {tool_call.tool}")
195
-
400
+
196
401
  messages.append({"role": "user", "content": f"[Tool Result] '{tool_call.tool}': {result_summary}"})
197
402
  except Exception as e:
198
403
  logger.warning("Native agent tool %s failed for %s: %s", tool_call.tool, task.id, e)
@@ -1,11 +1,14 @@
1
1
  import logging
2
2
  import subprocess
3
3
  from pathlib import Path
4
+
5
+ from devcouncil.executors.transient_retry import run_subprocess_with_transient_retry
4
6
  from rich.console import Console
5
7
  from devcouncil.domain.task import Task
6
8
  from devcouncil.domain.requirement import Requirement
7
9
  from devcouncil.execution.executor import Executor, ExecutionResult
8
10
  from devcouncil.execution.prompt_builder import PromptBuilder
11
+ from devcouncil.telemetry.stages import log_step
9
12
 
10
13
  console = Console()
11
14
  logger = logging.getLogger(__name__)
@@ -16,9 +19,19 @@ class OpenHandsExecutor(Executor):
16
19
 
17
20
  def run_task(self, task: Task, requirements: list[Requirement]) -> ExecutionResult:
18
21
  builder = PromptBuilder(self.project_root)
22
+ from devcouncil.planning.correction_manifest import repair_prompt_prefix
23
+
19
24
  task_prompt = builder.build_task_prompt(task, requirements)
25
+ prefix = repair_prompt_prefix(self.project_root, task.id)
26
+ if prefix:
27
+ task_prompt = f"{prefix}{task_prompt}"
20
28
 
21
29
  logger.info("OpenHands starting for %s", task.id)
30
+ log_step(
31
+ "executor/openhands: starting task %s" % task.id,
32
+ project_root=self.project_root,
33
+ task_id=task.id,
34
+ )
22
35
  console.print(f"Starting [bold]OpenHands[/bold] for task {task.id}...")
23
36
 
24
37
  # OpenHands often expects a workspace mount and an instruction.
@@ -40,24 +53,53 @@ class OpenHandsExecutor(Executor):
40
53
  console.print("[yellow]Note: OpenHands must be installed and configured in the environment.[/yellow]")
41
54
 
42
55
  try:
43
- result = subprocess.run(
44
- cmd,
45
- capture_output=True,
46
- text=True,
47
- encoding="utf-8",
48
- errors="replace",
49
- cwd=self.project_root,
50
- timeout=1800,
56
+ def _run_once():
57
+ return subprocess.run(
58
+ cmd,
59
+ capture_output=True,
60
+ text=True,
61
+ encoding="utf-8",
62
+ errors="replace",
63
+ cwd=self.project_root,
64
+ timeout=1800,
65
+ )
66
+
67
+ result = run_subprocess_with_transient_retry(
68
+ self.project_root,
69
+ label="OpenHands",
70
+ task_id=task.id,
71
+ run_once=_run_once,
51
72
  )
52
73
  self._write_log(task.id, result)
53
74
  if result.returncode != 0:
54
75
  logger.error("OpenHands exited %s for %s", result.returncode, task.id)
76
+ log_step(
77
+ "executor/openhands: finished task %s" % task.id,
78
+ project_root=self.project_root,
79
+ task_id=task.id,
80
+ returncode=result.returncode,
81
+ success=False,
82
+ )
55
83
  console.print(f"[red]OpenHands exited with {result.returncode}.[/red]")
56
84
  return ExecutionResult(success=False, message=f"Exited with code {result.returncode}")
57
85
  logger.info("OpenHands finished for %s", task.id)
86
+ log_step(
87
+ "executor/openhands: finished task %s" % task.id,
88
+ project_root=self.project_root,
89
+ task_id=task.id,
90
+ returncode=0,
91
+ success=True,
92
+ )
58
93
  return ExecutionResult(success=True, message="Completed successfully")
59
94
  except Exception as e:
60
95
  logger.exception("OpenHands error for %s: %s", task.id, e)
96
+ log_step(
97
+ "executor/openhands: finished task %s" % task.id,
98
+ project_root=self.project_root,
99
+ task_id=task.id,
100
+ success=False,
101
+ error=str(e),
102
+ )
61
103
  console.print(f"[red]Error running OpenHands: {e}[/red]")
62
104
  return ExecutionResult(success=False, message=str(e))
63
105
 
@@ -0,0 +1,99 @@
1
+ """Shared transient-failure detection and retry for subprocess executors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import subprocess
7
+ import time
8
+ from collections.abc import Callable
9
+ from pathlib import Path
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Output signatures of failures caused by the NETWORK/PROVIDER, not the task.
14
+ # Matched case-insensitively against stderr plus the tail of stdout.
15
+ TRANSIENT_FAILURE_MARKERS = (
16
+ "connection closed",
17
+ "connection reset",
18
+ "connection refused",
19
+ "connection error",
20
+ "connection aborted",
21
+ "econnreset",
22
+ "econnrefused",
23
+ "etimedout",
24
+ "socket hang up",
25
+ "mid-response",
26
+ "network error",
27
+ "fetch failed",
28
+ "temporarily unavailable",
29
+ "service unavailable",
30
+ "internal server error",
31
+ "bad gateway",
32
+ "gateway timeout",
33
+ "overloaded",
34
+ "rate limit",
35
+ "too many requests",
36
+ "request timed out",
37
+ "timeout awaiting",
38
+ "tls handshake",
39
+ "dns",
40
+ )
41
+
42
+
43
+ def transient_failure_reason(result: subprocess.CompletedProcess[str]) -> str | None:
44
+ """Return the matched transient marker when a failed run looks provider-caused."""
45
+ haystack = f"{result.stderr or ''}\n{(result.stdout or '')[-4000:]}".lower()
46
+ for marker in TRANSIENT_FAILURE_MARKERS:
47
+ if marker in haystack:
48
+ return marker
49
+ return None
50
+
51
+
52
+ def transient_error_in_text(text: str) -> str | None:
53
+ """Return a transient marker found in an exception message, else None."""
54
+ haystack = (text or "").lower()
55
+ for marker in TRANSIENT_FAILURE_MARKERS:
56
+ if marker in haystack:
57
+ return marker
58
+ return None
59
+
60
+
61
+ def transient_retry_limit(project_root: Path, *, default: int = 2) -> int:
62
+ """Max transient-failure retries from ``execution.transient_retry_attempts``."""
63
+ try:
64
+ from devcouncil.app.config import load_config
65
+
66
+ return max(0, int(load_config(project_root).execution.transient_retry_attempts))
67
+ except Exception:
68
+ return default
69
+
70
+
71
+ def run_subprocess_with_transient_retry(
72
+ project_root: Path,
73
+ *,
74
+ label: str,
75
+ task_id: str,
76
+ run_once: Callable[[], subprocess.CompletedProcess[str]],
77
+ ) -> subprocess.CompletedProcess[str]:
78
+ """Run a subprocess callable, retrying on known transient failure signatures."""
79
+ result = run_once()
80
+ retry_limit = transient_retry_limit(project_root)
81
+ retries = 0
82
+ while result.returncode != 0 and retries < retry_limit:
83
+ reason = transient_failure_reason(result)
84
+ if reason is None:
85
+ break
86
+ retries += 1
87
+ delay = min(30.0, 5.0 * retries)
88
+ logger.warning(
89
+ "%s failed with a transient error for %s (%s); retrying %d/%d in %.0fs",
90
+ label,
91
+ task_id,
92
+ reason,
93
+ retries,
94
+ retry_limit,
95
+ delay,
96
+ )
97
+ time.sleep(delay)
98
+ result = run_once()
99
+ return result
@@ -1,6 +1,7 @@
1
1
  import subprocess
2
2
  import logging
3
3
  from devcouncil.domain.gap import Gap
4
+ from devcouncil.utils.proc import GIT_TIMEOUT
4
5
 
5
6
  logger = logging.getLogger(__name__)
6
7
 
@@ -15,7 +16,9 @@ class CleanGitCheck:
15
16
 
16
17
  def check(self, project_root, task_id: str) -> list[Gap]:
17
18
  try:
18
- status = subprocess.check_output(["git", "status", "--porcelain"], cwd=project_root).decode()
19
+ status = subprocess.check_output(
20
+ ["git", "status", "--porcelain"], cwd=project_root, timeout=GIT_TIMEOUT
21
+ ).decode()
19
22
  dirty_lines = [line for line in status.splitlines() if line.strip() and not self._is_runtime_state(line)]
20
23
  if dirty_lines:
21
24
  return [Gap(
@@ -38,7 +41,7 @@ class CleanGitCheck:
38
41
  recommended_fix="Install git and ensure it is in your PATH.",
39
42
  blocking=True
40
43
  )]
41
- except subprocess.CalledProcessError as e:
44
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
42
45
  logger.warning("Git status check failed: %s", e)
43
46
  return [Gap(
44
47
  id=f"GAP-{task_id}-GIT-ERROR",