devcouncil 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/README.md +92 -33
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +56 -40
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +964 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +281 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +87 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +103 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +69 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +53 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +325 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/summary.py +21 -3
  255. package/src/devcouncil/live/tasks.py +12 -3
  256. package/src/devcouncil/live/transcripts.py +69 -2
  257. package/src/devcouncil/llm/cache.py +5 -6
  258. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  259. package/src/devcouncil/llm/provider.py +647 -73
  260. package/src/devcouncil/llm/router.py +271 -46
  261. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  262. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  263. package/src/devcouncil/optimization/skillopt.py +9 -5
  264. package/src/devcouncil/planning/arbiter_service.py +12 -3
  265. package/src/devcouncil/planning/correction_manifest.py +107 -10
  266. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  267. package/src/devcouncil/planning/plan_service.py +5 -2
  268. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  269. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  270. package/src/devcouncil/planning/question_conversion.py +56 -0
  271. package/src/devcouncil/planning/spec_service.py +9 -3
  272. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  273. package/src/devcouncil/repo/gitignore.py +1 -2
  274. package/src/devcouncil/reporting/evidence_export.py +124 -0
  275. package/src/devcouncil/reporting/evidence_html.py +210 -0
  276. package/src/devcouncil/reporting/json_report.py +16 -12
  277. package/src/devcouncil/reporting/markdown_report.py +38 -9
  278. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  279. package/src/devcouncil/reporting/report_builder.py +40 -4
  280. package/src/devcouncil/reporting/task_provenance.py +42 -0
  281. package/src/devcouncil/reporting/verdict.py +75 -0
  282. package/src/devcouncil/skills/library/README.md +1 -0
  283. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  285. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  286. package/src/devcouncil/skills/registry.py +43 -12
  287. package/src/devcouncil/storage/db.py +57 -11
  288. package/src/devcouncil/storage/models.py +6 -0
  289. package/src/devcouncil/storage/native.py +5 -3
  290. package/src/devcouncil/storage/repositories.py +50 -18
  291. package/src/devcouncil/telemetry/context.py +28 -0
  292. package/src/devcouncil/telemetry/cost.py +4 -5
  293. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  294. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  295. package/src/devcouncil/telemetry/stages.py +27 -2
  296. package/src/devcouncil/telemetry/tracker.py +50 -13
  297. package/src/devcouncil/ui/dashboard.py +120 -8
  298. package/src/devcouncil/utils/fsio.py +58 -0
  299. package/src/devcouncil/utils/git_snapshot.py +112 -0
  300. package/src/devcouncil/utils/json_persist.py +53 -0
  301. package/src/devcouncil/utils/proc.py +89 -0
  302. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  303. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  304. package/src/devcouncil/verification/checks/__init__.py +41 -0
  305. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  306. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  307. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  308. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  309. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  310. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  311. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  312. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  313. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  314. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  315. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  316. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  317. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  318. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  319. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  320. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  321. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  322. package/src/devcouncil/verification/checks/wiring.py +216 -0
  323. package/src/devcouncil/verification/claims/__init__.py +23 -0
  324. package/src/devcouncil/verification/claims/checks.py +395 -0
  325. package/src/devcouncil/verification/claims/mapper.py +168 -0
  326. package/src/devcouncil/verification/claims/models.py +39 -0
  327. package/src/devcouncil/verification/claims/transcript.py +92 -0
  328. package/src/devcouncil/verification/claims/verdict.py +88 -0
  329. package/src/devcouncil/verification/command_evidence.py +170 -0
  330. package/src/devcouncil/verification/command_malformation.py +147 -0
  331. package/src/devcouncil/verification/command_runner.py +164 -0
  332. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  333. package/src/devcouncil/verification/diff_coverage.py +151 -0
  334. package/src/devcouncil/verification/difficulty.py +296 -0
  335. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  336. package/src/devcouncil/verification/gap_ids.py +63 -0
  337. package/src/devcouncil/verification/gate_cache.py +194 -0
  338. package/src/devcouncil/verification/gate_selector.py +344 -0
  339. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  340. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  341. package/src/devcouncil/verification/incremental_check.py +241 -0
  342. package/src/devcouncil/verification/next_actions.py +60 -1
  343. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  344. package/src/devcouncil/verification/sandbox.py +38 -11
  345. package/src/devcouncil/verification/stub_detector.py +369 -0
  346. package/src/devcouncil/verification/test_resolver.py +67 -1
  347. package/src/devcouncil/verification/verifier.py +137 -1666
  348. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  349. package/src/devcouncil/verification/verify_setup.py +176 -0
  350. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  351. package/src/semantic_layer/__init__.py +58 -0
  352. package/src/semantic_layer/benchmark.py +75 -0
  353. package/src/semantic_layer/cache.py +290 -0
  354. package/src/semantic_layer/compressor.py +137 -0
  355. package/src/semantic_layer/config.py +75 -0
  356. package/src/semantic_layer/embeddings.py +69 -0
  357. package/src/semantic_layer/llm_backends.py +99 -0
  358. package/src/semantic_layer/pipeline.py +111 -0
  359. package/src/semantic_layer/router.py +128 -0
  360. package/src/semantic_layer/tuner.py +72 -0
  361. package/uv.lock +973 -9
  362. package/src/devcouncil/artifacts/migrations.py +0 -20
  363. package/src/devcouncil/artifacts/schemas.py +0 -23
  364. package/src/devcouncil/artifacts/serializer.py +0 -21
  365. package/src/devcouncil/integrations/gitnexus.py +0 -70
  366. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -0,0 +1,194 @@
1
+ """Acceptance-criteria ↔ corpus alignment gate (soft by default)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Callable, List, Optional, Set
9
+
10
+ from devcouncil.domain.gap import Gap
11
+ from devcouncil.domain.requirement import Requirement
12
+ from devcouncil.domain.task import Task
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ _DOC_PATH_RE = re.compile(
17
+ r"(?<![`\w])(?:docs/[\w./-]+\.(?:md|rst|txt)|README(?:\.md)?)(?![`\w])",
18
+ re.IGNORECASE,
19
+ )
20
+ _DOC_CONCEPT_RE = re.compile(
21
+ r"\b(?:documentation|corpus|wiki|architecture doc(?:umentation)?|user guide|dev guide)\b",
22
+ re.IGNORECASE,
23
+ )
24
+ _EVIDENCE_PATH_RE = re.compile(
25
+ r"(?:evidence|see|per|from)\s*:?\s*[`']?((?:docs/|README)[\w./-]*(?:\.md)?)[`']?",
26
+ re.IGNORECASE,
27
+ )
28
+ _BACKTICK_TERM_RE = re.compile(r"`([^`]{3,80})`")
29
+
30
+
31
+ def _norm(path: str) -> str:
32
+ return path.replace("\\", "/").lstrip("./")
33
+
34
+
35
+ def _ac_by_id(requirements: List[Requirement], ac_id: str):
36
+ for req in requirements:
37
+ for ac in req.acceptance_criteria:
38
+ if ac.id == ac_id:
39
+ return ac, req.id
40
+ return None, None
41
+
42
+
43
+ def _doc_paths_in_text(text: str) -> List[str]:
44
+ paths: List[str] = []
45
+ for match in _DOC_PATH_RE.finditer(text):
46
+ paths.append(_norm(match.group(0)))
47
+ for match in _EVIDENCE_PATH_RE.finditer(text):
48
+ paths.append(_norm(match.group(1)))
49
+ return list(dict.fromkeys(paths))
50
+
51
+
52
+ def _cites_doc_concepts(description: str) -> bool:
53
+ if _DOC_PATH_RE.search(description):
54
+ return True
55
+ if _DOC_CONCEPT_RE.search(description):
56
+ return True
57
+ for match in _BACKTICK_TERM_RE.finditer(description):
58
+ term = match.group(1)
59
+ if term.startswith("docs/") or term.endswith(".md") or "documentation" in term.lower():
60
+ return True
61
+ return False
62
+
63
+
64
+ def _corpus_terms(description: str, doc_paths: List[str]) -> List[str]:
65
+ terms: List[str] = []
66
+ for path in doc_paths:
67
+ stem = Path(path).stem.replace("-", " ").replace("_", " ")
68
+ if stem and stem.lower() != "readme":
69
+ terms.append(stem)
70
+ terms.append(path)
71
+ for match in _BACKTICK_TERM_RE.finditer(description):
72
+ term = match.group(1).strip()
73
+ if len(term) >= 3 and not term.startswith("src/"):
74
+ terms.append(term)
75
+ if _DOC_CONCEPT_RE.search(description):
76
+ for word in re.findall(r"[A-Za-z]{4,}", description):
77
+ if word.lower() not in {"documentation", "acceptance", "criterion", "criteria", "verify", "behavior", "matches", "section"}:
78
+ terms.append(word)
79
+ # Residual phrase tokens from the description (e.g. "verification gates").
80
+ for chunk in re.findall(r"[A-Za-z][A-Za-z0-9 -]{2,40}", description):
81
+ stripped = chunk.strip()
82
+ if " " in stripped and len(stripped) >= 8:
83
+ terms.append(stripped)
84
+ return list(dict.fromkeys(t for t in terms if t.strip()))
85
+
86
+
87
+ def _corpus_has_hit(project_root: Path, terms: List[str], doc_paths: List[str]) -> bool:
88
+ if not terms and not doc_paths:
89
+ return False
90
+ try:
91
+ from devcouncil.indexing.wiring import load_corpus_graph, query_corpus
92
+
93
+ graph = load_corpus_graph(project_root)
94
+ if graph is not None:
95
+ doc_norm = {_norm(p) for p in doc_paths}
96
+ for node in graph.nodes:
97
+ node_path = _norm(node.path or "")
98
+ if node_path in doc_norm or any(
99
+ node_path.endswith(p) for p in doc_norm if p
100
+ ):
101
+ return True
102
+ for term in terms[:8]:
103
+ result = query_corpus(project_root, term, limit=3)
104
+ if result.get("matches"):
105
+ return True
106
+ except Exception:
107
+ logger.debug("corpus query failed during acceptance_corpus check", exc_info=True)
108
+ return False
109
+
110
+
111
+ def _explicit_evidence_path(
112
+ *,
113
+ project_root: Path,
114
+ doc_paths: List[str],
115
+ changed_files: List[str],
116
+ planned_paths: Set[str],
117
+ expected_tests: List[str],
118
+ diff_content: str,
119
+ ) -> Optional[str]:
120
+ changed_norm = {_norm(p) for p in changed_files}
121
+ tests_blob = "\n".join(expected_tests)
122
+ for path in doc_paths:
123
+ rel = _norm(path)
124
+ full = project_root / rel
125
+ if not full.is_file():
126
+ continue
127
+ if rel in changed_norm or rel in planned_paths:
128
+ return rel
129
+ if rel in diff_content or rel in tests_blob:
130
+ return rel
131
+ return None
132
+
133
+
134
+ def detect_acceptance_corpus_gaps(
135
+ *,
136
+ task: Task,
137
+ requirements: List[Requirement],
138
+ project_root: Path,
139
+ changed_files: List[str],
140
+ diff_content: str,
141
+ next_gap_id: Callable[[str, str], str],
142
+ acceptance_corpus_enabled: bool = True,
143
+ acceptance_corpus_blocking: bool = False,
144
+ ) -> List[Gap]:
145
+ if not acceptance_corpus_enabled or not task.acceptance_criterion_ids:
146
+ return []
147
+ gaps: List[Gap] = []
148
+ planned_paths = {_norm(pf.path) for pf in task.planned_files}
149
+ try:
150
+ for ac_id in task.acceptance_criterion_ids:
151
+ ac, req_id = _ac_by_id(requirements, ac_id)
152
+ if ac is None:
153
+ continue
154
+ description = ac.description or ""
155
+ if not _cites_doc_concepts(description):
156
+ continue
157
+ doc_paths = _doc_paths_in_text(description)
158
+ terms = _corpus_terms(description, doc_paths)
159
+ if _corpus_has_hit(project_root, terms, doc_paths):
160
+ continue
161
+ evidence_path = _explicit_evidence_path(
162
+ project_root=project_root,
163
+ doc_paths=doc_paths,
164
+ changed_files=changed_files,
165
+ planned_paths=planned_paths,
166
+ expected_tests=list(task.expected_tests or []),
167
+ diff_content=diff_content or "",
168
+ )
169
+ if evidence_path:
170
+ continue
171
+ gaps.append(Gap(
172
+ id=next_gap_id(task.id, f"ACCORPUS-{ac_id}"),
173
+ severity="high" if acceptance_corpus_blocking else "medium",
174
+ gap_type="acceptance_corpus",
175
+ task_id=task.id,
176
+ requirement_id=req_id,
177
+ acceptance_criterion_id=ac_id,
178
+ description=(
179
+ f"Acceptance criterion `{ac_id}` cites documentation concepts but "
180
+ "has no corpus match and no explicit evidence path in the task diff, "
181
+ "planned files, or expected tests."
182
+ ),
183
+ evidence=[ac_id, description[:200], *(doc_paths[:3])],
184
+ recommended_fix=(
185
+ "Add a corpus hit (run `dev corpus build` and align docs), cite an "
186
+ "existing doc path in planned/changed files, or add an explicit "
187
+ "`evidence: docs/...` path with supporting changes."
188
+ ),
189
+ blocking=acceptance_corpus_blocking,
190
+ suggested_command="dev corpus build",
191
+ ))
192
+ except Exception:
193
+ logger.debug("detect_acceptance_corpus_gaps failed", exc_info=True)
194
+ return gaps
@@ -0,0 +1,239 @@
1
+ """Acceptance-criteria evidence mapping extracted from Verifier.verify_task."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Callable, Dict, List, Literal, Optional, Set, Tuple
7
+
8
+ from devcouncil.domain.evidence import CommandResult, TestEvidence
9
+ from devcouncil.domain.gap import Gap
10
+ from devcouncil.domain.requirement import Requirement
11
+ from devcouncil.domain.task import Task
12
+ from devcouncil.verification.command_evidence import command_is_trivial_evidence
13
+ from devcouncil.verification.difficulty import RigorPolicy
14
+
15
+ _AD_HOC_TASK_ID = "CHECK"
16
+
17
+
18
+ @dataclass
19
+ class AcceptanceEvidenceResult:
20
+ gaps: List[Gap] = field(default_factory=list)
21
+ evidence: List[TestEvidence] = field(default_factory=list)
22
+
23
+
24
+ def requirement_id_for_ac(requirements: List[Requirement], ac_id: str) -> Optional[str]:
25
+ for req in requirements:
26
+ if any(ac.id == ac_id for ac in req.acceptance_criteria):
27
+ return req.id
28
+ return None
29
+
30
+
31
+ def _declared_test_proves_ac(
32
+ task: Task,
33
+ ac_id: str,
34
+ successful_commands: List[CommandResult],
35
+ ) -> bool:
36
+ """True when a user/planner declared test command proves this criterion.
37
+
38
+ For ad-hoc ``CHECK`` runs, ``--test`` commands are criterion-specific by
39
+ construction (one inline AC). They must not fall through to the coarse
40
+ "any passing command" bucket.
41
+ """
42
+ declared = list(task.expected_tests or [])
43
+ if not declared:
44
+ return False
45
+ passing_declared = [r.command for r in successful_commands if r.command in declared]
46
+ if not passing_declared:
47
+ return False
48
+ # Only ad-hoc CHECK treats inline --test commands as per-criterion proof.
49
+ # Workflow tasks with a single expected_test still use the coarse bucket so
50
+ # hard-rigor coarse_acceptance_proof blocking remains meaningful.
51
+ return task.id == _AD_HOC_TASK_ID
52
+
53
+
54
+ def map_acceptance_criteria_evidence(
55
+ *,
56
+ task: Task,
57
+ requirements: List[Requirement],
58
+ evidence_results: List[CommandResult],
59
+ compiled_pass: Dict[str, bool],
60
+ compiled_cmds_by_ac: Dict[str, List[str]],
61
+ failing_results_by_ac: Dict[str, List[CommandResult]],
62
+ compiled_vote: Dict[str, Tuple[int, int, bool]],
63
+ inconclusive_acs: Set[str],
64
+ work_present: bool,
65
+ genuine_failure: bool,
66
+ had_unrunnable: bool,
67
+ rigor: RigorPolicy,
68
+ is_quality_only_command: Callable[[str], bool],
69
+ next_gap_id: Callable[[str, str], str],
70
+ ) -> AcceptanceEvidenceResult:
71
+ """Map compiled + coarse verification evidence to gaps and TestEvidence records."""
72
+ result = AcceptanceEvidenceResult()
73
+ agent_appended = set(task.agent_appended_expected_tests or []) | set(
74
+ task.agent_appended_allowed_commands or []
75
+ )
76
+ successful_commands = [
77
+ item for item in evidence_results
78
+ if item.exit_code == 0
79
+ and not is_quality_only_command(item.command)
80
+ and item.command not in agent_appended
81
+ and not command_is_trivial_evidence(item.command)
82
+ ]
83
+ coarse_proof_available = work_present and bool(successful_commands)
84
+
85
+ if task.acceptance_criterion_ids:
86
+ req_by_ac = {ac.id: req.id for req in requirements for ac in req.acceptance_criteria}
87
+ unproven_acs: List[str] = []
88
+ coarse_proven_acs: List[str] = []
89
+ for ac_id in task.acceptance_criterion_ids:
90
+ proven: Optional[bool] = compiled_pass.get(ac_id)
91
+ coarse = False
92
+ if proven is None:
93
+ if _declared_test_proves_ac(task, ac_id, successful_commands):
94
+ proven = True
95
+ else:
96
+ proven = coarse_proof_available
97
+ coarse = proven
98
+ if proven:
99
+ if coarse:
100
+ coarse_proven_acs.append(ac_id)
101
+ if not (coarse and genuine_failure):
102
+ proof_mode: Literal["compiled", "vote", "coarse", ""]
103
+ if coarse:
104
+ proof_summary = (
105
+ "Acceptance criterion proven only by a COARSE signal (a passing "
106
+ "acceptance-capable command, not a per-criterion check); behavior "
107
+ "not precisely verified."
108
+ )
109
+ proof_mode = "coarse"
110
+ elif task.id == _AD_HOC_TASK_ID and task.expected_tests:
111
+ proof_mode = "compiled"
112
+ proof_summary = (
113
+ "Acceptance criterion proven by user-supplied verification "
114
+ "command(s) for this check."
115
+ )
116
+ else:
117
+ passes_n, decisive_n, was_repaired = compiled_vote.get(ac_id, (1, 1, False))
118
+ proof_mode = "vote" if decisive_n > 1 else "compiled"
119
+ how = (
120
+ f"a majority vote of independent compiled checks ({passes_n}/{decisive_n} passed)"
121
+ if decisive_n > 1 else
122
+ "a per-criterion compiled check"
123
+ )
124
+ repaired_note = (
125
+ " (one check was regenerated from its launcher error to run)"
126
+ if was_repaired else ""
127
+ )
128
+ proof_summary = f"Acceptance criterion proven by {how}.{repaired_note}"
129
+ result.evidence.append(TestEvidence(
130
+ requirement_id=req_by_ac.get(
131
+ ac_id, task.requirement_ids[0] if task.requirement_ids else ""
132
+ ),
133
+ acceptance_criterion_id=ac_id,
134
+ command="(devcouncil acceptance check)",
135
+ status="passed",
136
+ evidence_summary=proof_summary,
137
+ mode=proof_mode,
138
+ ))
139
+ else:
140
+ unproven_acs.append(ac_id)
141
+
142
+ if coarse_proven_acs and rigor.coarse_acceptance_enabled:
143
+ result.gaps.append(Gap(
144
+ id=next_gap_id(task.id, "COARSE"),
145
+ severity="high" if rigor.coarse_acceptance_blocking else "low",
146
+ gap_type="coarse_acceptance_proof",
147
+ task_id=task.id,
148
+ description=(
149
+ "Verification mode = COARSE for "
150
+ f"{', '.join(coarse_proven_acs)}: proven by a passing acceptance-capable "
151
+ "command, not a per-criterion check. Behavior is not precisely verified."
152
+ ),
153
+ evidence=[f"coarse-proven: {', '.join(coarse_proven_acs)}"],
154
+ recommended_fix=(
155
+ "Add a verification command (or test) that exercises each listed criterion "
156
+ "specifically, so DevCouncil can compile a per-criterion check instead of "
157
+ "relying on the coarse fallback."
158
+ ),
159
+ blocking=rigor.coarse_acceptance_blocking,
160
+ ))
161
+
162
+ if unproven_acs:
163
+ couldnt_verify = had_unrunnable and not genuine_failure and work_present
164
+ ac_by_id = {ac.id: ac for req in requirements for ac in req.acceptance_criteria}
165
+ automatable_methods = {"unit_test", "integration_test"}
166
+ for ac_id in unproven_acs:
167
+ ac = ac_by_id.get(ac_id)
168
+ method = ac.verification_method if ac else "unit_test"
169
+ is_automatable = (ac.required if ac else True) and method in automatable_methods
170
+ if not is_automatable:
171
+ blocks = False
172
+ optional = "" if (ac is None or ac.required) else " optional"
173
+ fix = (
174
+ f"This{optional} criterion's verification method is '{method}'; it cannot be "
175
+ "proven by running code. Review it manually (it does not block the gate)."
176
+ )
177
+ suffix = f" (non-blocking: {method})"
178
+ elif ac_id in inconclusive_acs:
179
+ blocks = False
180
+ fix = (
181
+ "Auto-generated acceptance checks disagreed on this criterion (some "
182
+ "passed, some failed). Add a precise verification command that "
183
+ "unambiguously proves it so the result is decisive."
184
+ )
185
+ suffix = " (auto-checks inconclusive)"
186
+ elif couldnt_verify:
187
+ blocks = False
188
+ fix = (
189
+ "Could not verify this criterion: the verification commands did not run "
190
+ "(missing tooling or tests). Regenerate them with 'dev repair' to confirm the work."
191
+ )
192
+ suffix = " (verification commands could not run)"
193
+ else:
194
+ blocks = True
195
+ fix = "Add or fix a verification command that proves this acceptance criterion."
196
+ suffix = ""
197
+ ac_compiled = compiled_cmds_by_ac.get(ac_id, [])
198
+ ac_failures = failing_results_by_ac.get(ac_id, [])
199
+ ac_evidence: List[str] = []
200
+ suggested_cmd: Optional[str] = None
201
+ if ac_compiled:
202
+ suggested_cmd = ac_compiled[0]
203
+ ac_evidence.extend(f"compiled check: {c}" for c in ac_compiled)
204
+ ac_evidence.extend(r.summary[:500] for r in ac_failures)
205
+ else:
206
+ ac_evidence.append(
207
+ f"no DevCouncil check compiled for {ac_id} "
208
+ f"(expected verification method: {method})"
209
+ )
210
+ result.gaps.append(Gap(
211
+ id=next_gap_id(task.id, f"AC-{ac_id}"),
212
+ severity="high" if blocks else "medium",
213
+ gap_type="acceptance_criteria_unproven",
214
+ requirement_id=requirement_id_for_ac(requirements, ac_id),
215
+ task_id=task.id,
216
+ description=(
217
+ f"Acceptance criterion {ac_id} has no passing verification evidence "
218
+ f"for task {task.id}.{suffix}"
219
+ ),
220
+ evidence=ac_evidence,
221
+ recommended_fix=fix,
222
+ blocking=blocks,
223
+ acceptance_criterion_id=ac_id,
224
+ expected_verification_method=method,
225
+ suggested_command=suggested_cmd,
226
+ ))
227
+ elif task.requirement_ids:
228
+ result.gaps.append(Gap(
229
+ id=next_gap_id(task.id, "NOAC"),
230
+ severity="high",
231
+ gap_type="acceptance_criteria_unproven",
232
+ requirement_id=task.requirement_ids[0],
233
+ task_id=task.id,
234
+ description=f"Task {task.id} is linked to requirements but no acceptance criteria.",
235
+ recommended_fix="Link the task to specific acceptance_criterion_ids before verification.",
236
+ blocking=True,
237
+ ))
238
+
239
+ return result
@@ -0,0 +1,148 @@
1
+ """Expected verification command execution extracted from Verifier."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Callable, Dict, List, Tuple
7
+
8
+ from devcouncil.domain.evidence import CommandResult
9
+ from devcouncil.domain.gap import Gap
10
+ from devcouncil.domain.task import Task
11
+
12
+
13
+ @dataclass
14
+ class CommandEvidenceRunResult:
15
+ gaps: List[Gap] = field(default_factory=list)
16
+ command_results: List[CommandResult] = field(default_factory=list)
17
+ evidence_results: List[CommandResult] = field(default_factory=list)
18
+ genuine_failure: bool = False
19
+ had_unrunnable: bool = False
20
+ demoted_failures: List[Gap] = field(default_factory=list)
21
+
22
+
23
+ def run_verification_commands(
24
+ *,
25
+ task: Task,
26
+ commands_for_task: Dict[str, List[str]],
27
+ compiler_active: bool,
28
+ command_applicable: Callable[[str], Tuple[bool, str]],
29
+ run_command: Callable[[str], CommandResult],
30
+ command_can_prove_acceptance: Callable[[str, str], bool],
31
+ command_is_malformed: Callable[[CommandResult], bool],
32
+ failure_location: Callable[[CommandResult], Tuple[str | None, int | None]],
33
+ is_quality_only_command: Callable[[str], bool],
34
+ next_gap_id: Callable[[str, str], str],
35
+ retry_flaky: bool = True,
36
+ ) -> CommandEvidenceRunResult:
37
+ """Run planner/config verification commands and collect gaps + evidence.
38
+
39
+ With ``retry_flaky`` (config ``verification.retry_flaky``, default on), a genuinely
40
+ failing acceptance-capable command gets ONE immediate re-run; a pass on the re-run
41
+ counts as passed with its summary tagged ``[flaky: passed on retry 2/2]`` so
42
+ next-actions/reports can distinguish a flaky pass from a stable one.
43
+ """
44
+ result = CommandEvidenceRunResult()
45
+ for cmd_type, cmds in commands_for_task.items():
46
+ for cmd in cmds:
47
+ applicable, skip_reason = command_applicable(cmd)
48
+ if not applicable:
49
+ result.gaps.append(Gap(
50
+ id=next_gap_id(task.id, f"SKIP-{cmd}"),
51
+ severity="low",
52
+ gap_type="skipped_verification_command",
53
+ task_id=task.id,
54
+ description=f"Skipped verification command '{cmd}': {skip_reason}.",
55
+ evidence=[skip_reason],
56
+ recommended_fix=(
57
+ "Replace it with a command for this repo's stack, or remove it "
58
+ "from .devcouncil/config.yaml / the task's expected_tests."
59
+ ),
60
+ blocking=False,
61
+ suggested_command=cmd,
62
+ ))
63
+ continue
64
+ cmd_result = run_command(cmd)
65
+ can_prove = command_can_prove_acceptance(cmd_type, cmd)
66
+ if (
67
+ retry_flaky
68
+ and can_prove
69
+ and cmd_result.exit_code > 0
70
+ and not cmd_result.timed_out
71
+ and not command_is_malformed(cmd_result)
72
+ ):
73
+ # Flaky-evidence retry: one immediate re-run of a genuinely failing
74
+ # acceptance-capable command. Only exit codes > 0 qualify — the
75
+ # synthetic -1 (unlaunchable / timed out) and malformed commands
76
+ # prove nothing and would fail identically or burn a second full
77
+ # timeout window. On a failed re-run the ORIGINAL result is kept.
78
+ retry_result = run_command(cmd)
79
+ if retry_result.exit_code == 0:
80
+ cmd_result = retry_result.model_copy(update={
81
+ "summary": f"[flaky: passed on retry 2/2] {retry_result.summary}",
82
+ })
83
+ result.command_results.append(cmd_result)
84
+ if can_prove:
85
+ result.evidence_results.append(cmd_result)
86
+ if cmd_result.exit_code != 0:
87
+ if command_is_malformed(cmd_result):
88
+ result.had_unrunnable = True
89
+ result.gaps.append(Gap(
90
+ id=next_gap_id(task.id, f"BADCMD-{cmd}"),
91
+ severity="medium",
92
+ gap_type="invalid_verification_command",
93
+ task_id=task.id,
94
+ description=(
95
+ f"Verification command could not run (not a code failure): '{cmd}'. "
96
+ "It appears malformed or its tooling is unavailable, so this command "
97
+ "proves nothing either way."
98
+ ),
99
+ evidence=[cmd_result.summary[:500]],
100
+ recommended_fix=(
101
+ "Regenerate the task's verification commands with 'dev repair', or edit "
102
+ "them to be a single runnable command (e.g. 'python -m pytest <file>')."
103
+ ),
104
+ blocking=False,
105
+ suggested_command=cmd,
106
+ stdout_path=cmd_result.stdout_path or None,
107
+ stderr_path=cmd_result.stderr_path or None,
108
+ ))
109
+ else:
110
+ is_quality_gate = cmd_type in {"lint", "typecheck"} or is_quality_only_command(cmd)
111
+ blocking = cmd_result.timed_out or (
112
+ (not compiler_active) and not is_quality_gate
113
+ )
114
+ if blocking:
115
+ result.genuine_failure = True
116
+ fail_file, fail_line = failure_location(cmd_result)
117
+ failure_label = (
118
+ "timed out after running for the configured limit"
119
+ if cmd_result.timed_out
120
+ else f"failed with exit code {cmd_result.exit_code}"
121
+ )
122
+ gap = Gap(
123
+ id=next_gap_id(task.id, f"{cmd_type.upper()}-{cmd}"),
124
+ severity="high" if blocking else "medium",
125
+ gap_type="quality_gate_failed" if is_quality_gate else "test_failed",
126
+ task_id=task.id,
127
+ description=(
128
+ f"{'Quality gate' if is_quality_gate else 'Command'} '{cmd}' "
129
+ f"{failure_label}"
130
+ + (
131
+ " (advisory: style/type, not a correctness gate)."
132
+ if is_quality_gate and not cmd_result.timed_out
133
+ else "."
134
+ )
135
+ ),
136
+ evidence=[cmd_result.summary[:500]],
137
+ recommended_fix=f"Fix the issues reported by '{cmd}'.",
138
+ blocking=blocking,
139
+ suggested_command=cmd,
140
+ file=fail_file,
141
+ line=fail_line,
142
+ stdout_path=cmd_result.stdout_path or None,
143
+ stderr_path=cmd_result.stderr_path or None,
144
+ )
145
+ result.gaps.append(gap)
146
+ if compiler_active and not is_quality_gate and not blocking:
147
+ result.demoted_failures.append(gap)
148
+ return result