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,356 @@
1
+ """Shepherd-style reversible run traces: one view over a run's events + checkpoints.
2
+
3
+ DevCouncil already records the three ingredients of a reversible agent run:
4
+
5
+ * **manifests** — ``.devcouncil/runs/<run-id>/agent-run.json`` (executor, status, task);
6
+ * **trace events** — ``.devcouncil/logs/traces.jsonl`` (:mod:`devcouncil.telemetry.traces`);
7
+ * **git checkpoints** — before/after/attempt refs + patches
8
+ (:class:`devcouncil.execution.checkpoints.CheckpointService`).
9
+
10
+ What was missing — and what Shepherd (shepherd-agents/shepherd) argues for — is treating
11
+ the run itself as an object another agent can operate on: inspect the full timeline,
12
+ see exactly what the run changed, and revert it, behind one programming model. This
13
+ module joins the three stores into a :class:`RunTimeline` and provides the meta-agent
14
+ operations on top: :func:`revert_run` (reverse the run's workspace effects via its git
15
+ checkpoints) and :func:`supervise_run` (an LLM meta-agent that reviews the timeline +
16
+ diff and issues a keep/revert/repair verdict, degrading to deterministic heuristics
17
+ when no model is configured).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import logging
24
+ from pathlib import Path
25
+ from typing import TYPE_CHECKING, Literal, Optional
26
+
27
+ from pydantic import BaseModel, Field
28
+
29
+ from devcouncil.execution.checkpoints import CheckpointResult, CheckpointService
30
+ from devcouncil.telemetry.traces import TraceEvent, TraceLogger, read_trace_events
31
+ from devcouncil.utils.json_persist import read_json
32
+
33
+ if TYPE_CHECKING:
34
+ from devcouncil.llm.router import ModelRouter
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ # Event types that indicate the run itself went wrong (not merely that work remains).
39
+ _FAILURE_EVENT_TYPES = frozenset({
40
+ "llm_structured_parse_repair_failed",
41
+ "llm_provider_request_failed_fallback",
42
+ "task_failed",
43
+ "verification_failed",
44
+ })
45
+
46
+ _SUPERVISOR_SYSTEM = (
47
+ "You are a supervisor meta-agent reviewing one recorded run of a coding agent. "
48
+ "You are given the run manifest, its trace-event timeline, and the workspace diff "
49
+ "the run produced. Decide whether its changes should be kept, reverted, or "
50
+ "repaired (kept but with follow-up fixes). Judge only from the evidence given; "
51
+ "an exit code of 0 with a plausible diff is normally 'keep'. Recommend 'revert' "
52
+ "only when the evidence shows the run failed, was cut off, or made changes that "
53
+ "contradict its task."
54
+ )
55
+
56
+
57
+ class RunCheckpoint(BaseModel):
58
+ """One git checkpoint ref recorded for the run's task."""
59
+
60
+ stage: str # before | after | attempt-N
61
+ ref: str
62
+ sha: str = ""
63
+ patch_path: str = ""
64
+
65
+
66
+ class RunTimeline(BaseModel):
67
+ """A run as an inspectable object: manifest + events + checkpoints + diff."""
68
+
69
+ run_id: str = ""
70
+ task_id: str = ""
71
+ manifest: dict = Field(default_factory=dict)
72
+ events: list[TraceEvent] = Field(default_factory=list)
73
+ checkpoints: list[RunCheckpoint] = Field(default_factory=list)
74
+ diff_stat: str = ""
75
+ reversible: bool = False
76
+
77
+ @property
78
+ def status(self) -> str:
79
+ return str(self.manifest.get("status") or "")
80
+
81
+ @property
82
+ def returncode(self) -> Optional[int]:
83
+ rc = self.manifest.get("returncode")
84
+ return rc if isinstance(rc, int) else None
85
+
86
+
87
+ class SupervisorVerdict(BaseModel):
88
+ """The supervisor meta-agent's decision about a run."""
89
+
90
+ verdict: Literal["keep", "revert", "repair"] = "keep"
91
+ confidence: float = Field(0.5, ge=0.0, le=1.0)
92
+ rationale: str = ""
93
+ findings: list[str] = Field(default_factory=list)
94
+ source: str = "heuristic" # heuristic | model
95
+
96
+
97
+ # --- Resolution -------------------------------------------------------------------
98
+
99
+
100
+ def _runs_dir(project_root: Path) -> Path:
101
+ return project_root / ".devcouncil" / "runs"
102
+
103
+
104
+ def _load_manifest(project_root: Path, run_id: str) -> dict:
105
+ path = _runs_dir(project_root) / run_id / "agent-run.json"
106
+ try:
107
+ data = read_json(path)
108
+ return data if isinstance(data, dict) else {}
109
+ except (OSError, json.JSONDecodeError):
110
+ return {}
111
+
112
+
113
+ def resolve_run(project_root: Path, ref: str) -> tuple[str, str]:
114
+ """Resolve ``ref`` (a run id or a task id) to ``(run_id, task_id)``.
115
+
116
+ A run id is matched by manifest directory; a task id is matched against the
117
+ newest run manifest recorded for that task (empty run_id when the task only has
118
+ checkpoints and no recorded run). Either element may be empty, but not both.
119
+ """
120
+ ref = ref.strip()
121
+ if not ref:
122
+ raise ValueError("Empty run/task reference.")
123
+
124
+ manifest = _load_manifest(project_root, ref)
125
+ if manifest:
126
+ return ref, str(manifest.get("task_id") or "")
127
+
128
+ # Treat as a task id: newest manifest that claims it.
129
+ runs_dir = _runs_dir(project_root)
130
+ candidates: list[tuple[float, str]] = []
131
+ if runs_dir.is_dir():
132
+ for manifest_path in runs_dir.glob("*/agent-run.json"):
133
+ data = _load_manifest(project_root, manifest_path.parent.name)
134
+ if str(data.get("task_id") or "") == ref:
135
+ try:
136
+ mtime = manifest_path.stat().st_mtime
137
+ except OSError:
138
+ mtime = 0.0
139
+ candidates.append((mtime, manifest_path.parent.name))
140
+ if candidates:
141
+ candidates.sort(reverse=True)
142
+ return candidates[0][1], ref
143
+
144
+ # No manifest at all — accept a bare task id if it has checkpoints, so runs made
145
+ # before manifests existed (or by native executors) are still addressable.
146
+ service = CheckpointService(project_root)
147
+ if service._ref_exists(CheckpointService.REF_BEFORE.format(task_id=ref)) or (
148
+ service.checkpoint_dir / f"{ref}-after.patch"
149
+ ).exists():
150
+ return "", ref
151
+ raise ValueError(f"No run manifest or checkpoints found for {ref!r}.")
152
+
153
+
154
+ # --- Timeline assembly --------------------------------------------------------------
155
+
156
+
157
+ def _git_lines(project_root: Path, *args: str) -> str:
158
+ from devcouncil.utils.proc import git_output
159
+
160
+ return git_output(list(args), cwd=project_root, default="").strip()
161
+
162
+
163
+ def _collect_checkpoints(project_root: Path, task_id: str) -> list[RunCheckpoint]:
164
+ if not task_id:
165
+ return []
166
+ service = CheckpointService(project_root)
167
+ checkpoints: list[RunCheckpoint] = []
168
+
169
+ def add(stage: str, ref: str, patch_name: str = "") -> None:
170
+ sha = _git_lines(project_root, "rev-parse", "--verify", ref)
171
+ patch = service.checkpoint_dir / patch_name if patch_name else None
172
+ if sha or (patch is not None and patch.exists()):
173
+ checkpoints.append(RunCheckpoint(
174
+ stage=stage,
175
+ ref=ref,
176
+ sha=sha,
177
+ patch_path=str(patch) if patch is not None and patch.exists() else "",
178
+ ))
179
+
180
+ add("before", CheckpointService.REF_BEFORE.format(task_id=task_id), f"{task_id}-before.patch")
181
+ add("after", CheckpointService.REF_AFTER.format(task_id=task_id), f"{task_id}-after.patch")
182
+
183
+ # Attempt refs live under refs/devcouncil/tasks/<task>/attempts/<n>.
184
+ prefix = f"refs/devcouncil/tasks/{task_id}/attempts/"
185
+ listing = _git_lines(project_root, "for-each-ref", "--format=%(refname) %(objectname)", prefix)
186
+ for line in listing.splitlines():
187
+ parts = line.split()
188
+ if len(parts) == 2 and parts[0].startswith(prefix):
189
+ checkpoints.append(RunCheckpoint(
190
+ stage=f"attempt-{parts[0][len(prefix):]}", ref=parts[0], sha=parts[1]
191
+ ))
192
+ return checkpoints
193
+
194
+
195
+ def diff_run(project_root: Path, task_id: str, *, stat_only: bool = False) -> str:
196
+ """The workspace changes a run produced: ``git diff before..after`` when both refs
197
+ exist, falling back to the recorded after-patch."""
198
+ if not task_id:
199
+ return ""
200
+ before = CheckpointService.REF_BEFORE.format(task_id=task_id)
201
+ after = CheckpointService.REF_AFTER.format(task_id=task_id)
202
+ if _git_lines(project_root, "rev-parse", "--verify", before) and _git_lines(
203
+ project_root, "rev-parse", "--verify", after
204
+ ):
205
+ args = ["diff", "--stat", before, after] if stat_only else ["diff", before, after]
206
+ return _git_lines(project_root, *args)
207
+ patch = project_root / ".devcouncil" / "checkpoints" / f"{task_id}-after.patch"
208
+ try:
209
+ content = patch.read_text(encoding="utf-8")
210
+ except OSError:
211
+ return ""
212
+ if not stat_only:
213
+ return content
214
+ # Cheap stat for a raw patch: count per-file hunks.
215
+ files = [line.split(" b/", 1)[-1] for line in content.splitlines() if line.startswith("diff --git ")]
216
+ return "\n".join(files)
217
+
218
+
219
+ def load_timeline(project_root: Path, ref: str, *, event_limit: int = 200) -> RunTimeline:
220
+ """Assemble the full reversible-trace view for a run (or bare task) reference."""
221
+ run_id, task_id = resolve_run(project_root, ref)
222
+ manifest = _load_manifest(project_root, run_id) if run_id else {}
223
+ if not task_id:
224
+ task_id = str(manifest.get("task_id") or "")
225
+
226
+ events = [
227
+ e for e in read_trace_events(project_root)
228
+ if (run_id and e.run_id == run_id) or (task_id and e.task_id == task_id)
229
+ ]
230
+ if event_limit > 0:
231
+ events = events[-event_limit:]
232
+
233
+ checkpoints = _collect_checkpoints(project_root, task_id)
234
+ reversible = any(c.stage in ("before", "after") for c in checkpoints)
235
+ return RunTimeline(
236
+ run_id=run_id,
237
+ task_id=task_id,
238
+ manifest=manifest,
239
+ events=events,
240
+ checkpoints=checkpoints,
241
+ diff_stat=diff_run(project_root, task_id, stat_only=True),
242
+ reversible=reversible,
243
+ )
244
+
245
+
246
+ # --- Meta-agent operations -----------------------------------------------------------
247
+
248
+
249
+ def revert_run(project_root: Path, ref: str) -> CheckpointResult:
250
+ """Reverse a run's workspace effects using its task's git checkpoints.
251
+
252
+ The revert itself is recorded as a trace event, so the trace stays a faithful,
253
+ append-only history of everything that happened — including supervision.
254
+ """
255
+ run_id, task_id = resolve_run(project_root, ref)
256
+ if not task_id:
257
+ raise ValueError(f"Run {ref!r} has no task id; nothing to revert.")
258
+ result = CheckpointService(project_root).rollback(task_id)
259
+ reverted = result.git_ref_created or bool(result.patch_path and "Rolled back" in result.message)
260
+ TraceLogger(project_root).log_event(
261
+ "run_reverted" if reverted else "run_revert_failed",
262
+ {"reference": ref, "message": result.message},
263
+ run_id=run_id or None,
264
+ task_id=task_id,
265
+ summary=f"Supervisor revert of {task_id}: {result.message}",
266
+ )
267
+ return result
268
+
269
+
270
+ def heuristic_verdict(timeline: RunTimeline) -> SupervisorVerdict:
271
+ """Deterministic verdict from the recorded evidence (no model required)."""
272
+ findings: list[str] = []
273
+ rc = timeline.returncode
274
+ if rc not in (None, 0):
275
+ findings.append(f"Executor exited with returncode {rc}.")
276
+ if timeline.status == "running":
277
+ findings.append("Run is still marked 'running' (possibly orphaned).")
278
+ failures = [e for e in timeline.events if e.type in _FAILURE_EVENT_TYPES]
279
+ for event in failures[:5]:
280
+ findings.append(f"Failure event: {event.type} — {event.summary or '(no summary)'}")
281
+ if not timeline.diff_stat:
282
+ findings.append("No workspace diff recorded for this run.")
283
+
284
+ if rc not in (None, 0) and timeline.diff_stat:
285
+ verdict: Literal["keep", "revert", "repair"] = "revert"
286
+ rationale = "The executor failed after modifying the workspace; its changes are unverified."
287
+ elif failures:
288
+ verdict = "repair"
289
+ rationale = "The run completed but recorded failure events; keep the changes and fix forward."
290
+ else:
291
+ verdict = "keep"
292
+ rationale = "No failure evidence recorded for this run."
293
+ return SupervisorVerdict(
294
+ verdict=verdict,
295
+ confidence=0.4 if findings else 0.6,
296
+ rationale=rationale,
297
+ findings=findings,
298
+ source="heuristic",
299
+ )
300
+
301
+
302
+ async def supervise_run(
303
+ project_root: Path,
304
+ timeline: RunTimeline,
305
+ router: "Optional[ModelRouter]" = None,
306
+ *,
307
+ diff_chars: int = 12_000,
308
+ ) -> SupervisorVerdict:
309
+ """Review a run and produce a keep/revert/repair verdict.
310
+
311
+ With a router, the ``run_supervisor`` role reviews the manifest, a bounded event
312
+ timeline, and the run's diff; the deterministic heuristic verdict is both the
313
+ degradation fallback and part of the prompt (so the model starts from the
314
+ recorded evidence rather than re-deriving it).
315
+ """
316
+ fallback = heuristic_verdict(timeline)
317
+ if router is None:
318
+ return fallback
319
+
320
+ events_payload = [
321
+ {"type": e.type, "summary": e.summary, "timestamp": e.timestamp}
322
+ for e in timeline.events[-60:]
323
+ ]
324
+ diff = diff_run(project_root, timeline.task_id)[:diff_chars]
325
+ payload = {
326
+ "manifest": timeline.manifest,
327
+ "heuristic_findings": fallback.findings,
328
+ "events": events_payload,
329
+ "diff_stat": timeline.diff_stat,
330
+ "diff_excerpt": diff,
331
+ "reversible": timeline.reversible,
332
+ }
333
+ messages = [
334
+ {"role": "system", "content": _SUPERVISOR_SYSTEM},
335
+ {"role": "user", "content": f"Run evidence (JSON):\n{json.dumps(payload, indent=2, default=str)}"},
336
+ ]
337
+ try:
338
+ verdict = await router.complete_structured(
339
+ "run_supervisor", messages, SupervisorVerdict, fallback=fallback,
340
+ run_id=timeline.run_id or None,
341
+ )
342
+ except Exception as exc:
343
+ # A router without a run_supervisor role (or any other model failure) must
344
+ # degrade to the heuristic verdict, never break supervision.
345
+ logger.warning("Model supervision unavailable; using heuristic verdict: %s", exc)
346
+ verdict = fallback
347
+ if verdict is not fallback:
348
+ verdict.source = "model"
349
+ TraceLogger(project_root).log_event(
350
+ "run_supervised",
351
+ {"verdict": verdict.verdict, "confidence": verdict.confidence, "source": verdict.source},
352
+ run_id=timeline.run_id or None,
353
+ task_id=timeline.task_id or None,
354
+ summary=f"Supervisor verdict for {timeline.task_id or timeline.run_id}: {verdict.verdict}",
355
+ )
356
+ return verdict
@@ -12,6 +12,7 @@ from pathlib import Path
12
12
 
13
13
  from rich.console import Console
14
14
 
15
+ from devcouncil.app.config import load_config
15
16
  from devcouncil.domain.evidence import CommandResult
16
17
  from devcouncil.domain.task import Task
17
18
  from devcouncil.execution.checkpoints import CheckpointService
@@ -25,12 +26,26 @@ logger = logging.getLogger(__name__)
25
26
 
26
27
 
27
28
  class ShellBackend:
28
- def run_command(self, command: str, cwd: Path, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
29
+ def run_command(
30
+ self,
31
+ command: str,
32
+ cwd: Path,
33
+ env: dict[str, str] | None = None,
34
+ *,
35
+ timeout: int | None = None,
36
+ ) -> subprocess.CompletedProcess[str]:
29
37
  raise NotImplementedError
30
38
 
31
39
 
32
40
  class CommandLoopBackend(ShellBackend):
33
- def run_command(self, command: str, cwd: Path, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
41
+ def run_command(
42
+ self,
43
+ command: str,
44
+ cwd: Path,
45
+ env: dict[str, str] | None = None,
46
+ *,
47
+ timeout: int | None = None,
48
+ ) -> subprocess.CompletedProcess[str]:
34
49
  args = shlex.split(command, posix=(os.name != "nt"))
35
50
  return subprocess.run(
36
51
  args,
@@ -40,6 +55,7 @@ class CommandLoopBackend(ShellBackend):
40
55
  encoding="utf-8",
41
56
  errors="replace",
42
57
  env=env,
58
+ timeout=timeout,
43
59
  )
44
60
 
45
61
 
@@ -63,7 +79,14 @@ class ShellWrappedBackend(ShellBackend):
63
79
  raise ValueError(f"Shell '{launcher[0]}' is not installed or not on PATH.")
64
80
  self.launcher = launcher
65
81
 
66
- def run_command(self, command: str, cwd: Path, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
82
+ def run_command(
83
+ self,
84
+ command: str,
85
+ cwd: Path,
86
+ env: dict[str, str] | None = None,
87
+ *,
88
+ timeout: int | None = None,
89
+ ) -> subprocess.CompletedProcess[str]:
67
90
  return subprocess.run(
68
91
  [*self.launcher, command],
69
92
  cwd=cwd,
@@ -72,6 +95,7 @@ class ShellWrappedBackend(ShellBackend):
72
95
  encoding="utf-8",
73
96
  errors="replace",
74
97
  env=env,
98
+ timeout=timeout,
75
99
  )
76
100
 
77
101
 
@@ -81,10 +105,16 @@ console = Console()
81
105
 
82
106
 
83
107
  class GuardedShellSession:
84
- def __init__(self, project_root: Path, task: Task, *, shell: str = "auto"):
108
+ def __init__(self, project_root: Path, task: Task, *, shell: str = "auto", command_timeout: int | None = None):
85
109
  self.project_root = project_root.resolve()
86
110
  self.task = task
87
111
  self.shell = shell
112
+ if command_timeout is None:
113
+ try:
114
+ command_timeout = load_config(self.project_root).execution.command_timeout
115
+ except Exception:
116
+ command_timeout = 300
117
+ self.command_timeout = max(1, int(command_timeout))
88
118
  # HookPolicy (not the bare TaskPolicyEngine) so a chained command is split into
89
119
  # its segments and EACH is allowlisted, plus git-safety denies (force-push,
90
120
  # --no-verify, protected-branch reset). Critical for the `--shell bash/zsh`
@@ -158,7 +188,18 @@ class GuardedShellSession:
158
188
 
159
189
  logger.info("Shell command for %s: %s", self.task.id, normalized)
160
190
  try:
161
- result = self.backend.run_command(normalized, self.project_root)
191
+ result = self.backend.run_command(
192
+ normalized, self.project_root, timeout=self.command_timeout,
193
+ )
194
+ except subprocess.TimeoutExpired:
195
+ reason = f"Command timed out after {self.command_timeout}s"
196
+ logger.warning("Shell command timed out for %s: %s", self.task.id, normalized)
197
+ self._record_command(normalized, "failed", reason=reason)
198
+ console.print(f"[red]Command timed out after {self.command_timeout}s:[/red] {normalized}")
199
+ console.print(
200
+ "[dim]Increase execution.command_timeout in .devcouncil/config.yaml or split the command.[/dim]"
201
+ )
202
+ return 1
162
203
  except (NotImplementedError, FileNotFoundError, OSError) as exc:
163
204
  logger.warning("Shell command could not run for %s: %s (%s)", self.task.id, normalized, exc)
164
205
  self._record_command(normalized, "denied", reason=str(exc))