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
@@ -0,0 +1,376 @@
1
+ """In-process Claude executor backed by the Claude Agent SDK.
2
+
3
+ Unlike :class:`CodingCliExecutor`, which shells out to ``claude -p`` and can only enforce
4
+ scope *after* the subprocess has already written to disk, this executor arbitrates every
5
+ tool call live through the SDK's ``can_use_tool`` callback. The callback is backed by the
6
+ same :class:`HookPolicy` the shell write-gate uses, so an out-of-scope write or command is
7
+ denied *before it happens* — real containment that, unlike the shell PreToolUse gate, does
8
+ not fail-closed on an interactive session (the decision is evaluated against the run's own
9
+ task, not a globally-resolved lease).
10
+
11
+ The Claude Agent SDK is an OPTIONAL dependency: it is imported lazily so nothing else in
12
+ DevCouncil requires it, and a missing install produces an actionable error rather than an
13
+ import-time crash. Wire it up with ``pip install claude-agent-sdk``.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import importlib
19
+ import logging
20
+ from pathlib import Path
21
+ from typing import Any, Coroutine, List, Optional, TypeVar
22
+
23
+ from devcouncil.domain.requirement import Requirement
24
+ from devcouncil.domain.task import Task
25
+ from devcouncil.execution.executor import ExecutionResult, Executor
26
+ from devcouncil.execution.hook_policy import HookDecision, HookPolicy
27
+ from devcouncil.execution.prompt_builder import PromptBuilder
28
+ from devcouncil.executors.transient_retry import transient_error_in_text
29
+ from devcouncil.telemetry.stages import log_step
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ _T = TypeVar("_T")
34
+
35
+
36
+ class ClaudeSdkExecutor(Executor):
37
+ """Run a DevCouncil task through the Claude Agent SDK with lease-aware gating."""
38
+
39
+ def __init__(
40
+ self,
41
+ project_root: Path | str,
42
+ *,
43
+ active_task: Task | None = None,
44
+ model: str | None = None,
45
+ advisor_model: str | None = None,
46
+ permission_mode: str = "default",
47
+ env: dict[str, str] | None = None,
48
+ ):
49
+ # NOTE: default (not acceptEdits). acceptEdits auto-approves file edits, which would
50
+ # skip the can_use_tool callback entirely and defeat the lease gate; "default" routes
51
+ # every write/command through our callback so scope is actually enforced.
52
+ self.project_root = Path(project_root)
53
+ # The task whose scope authorizes writes/commands. Passed explicitly so the gate is
54
+ # evaluated against THIS run's task rather than a globally-resolved lease (which is
55
+ # why this can enforce interactively where the shell PreToolUse gate cannot).
56
+ self.active_task = active_task
57
+ self.model = model
58
+ self.advisor_model = (advisor_model or "").strip() or None
59
+ self.permission_mode = permission_mode
60
+ # Extra environment for the SDK's underlying Claude Code process. Lets a run
61
+ # target an alternative Anthropic-compatible endpoint (ANTHROPIC_BASE_URL /
62
+ # ANTHROPIC_AUTH_TOKEN, e.g. a local proxy) — same knob the subprocess
63
+ # executor exposes via per-profile ``env`` overrides.
64
+ self.env = dict(env) if env else {}
65
+ self.policy = HookPolicy(project_root=self.project_root)
66
+ self.last_agent_session_id: str | None = None
67
+ # (tool_name, reason) for every call the gate denied — surfaced in the result.
68
+ self.denials: list[tuple[str, str]] = []
69
+
70
+ # -- policy ---------------------------------------------------------------------
71
+
72
+ def permission_decision(self, tool_name: str, tool_input: dict[str, Any]) -> HookDecision:
73
+ """Lease-aware allow/deny for a single tool call, reusing DevCouncil's HookPolicy.
74
+
75
+ Pure and side-effect free (bar reading repo policy) so it is unit-testable on its
76
+ own: the same call the SDK would make, evaluated against this run's task."""
77
+ call_data = {"tool_name": tool_name, "tool_input": tool_input or {}}
78
+ return self.policy.evaluate(call_data, self.active_task)
79
+
80
+ # -- SDK loading ----------------------------------------------------------------
81
+
82
+ @staticmethod
83
+ def _load_sdk():
84
+ try:
85
+ return importlib.import_module("claude_agent_sdk")
86
+ except ModuleNotFoundError as exc:
87
+ raise RuntimeError(
88
+ "The Claude Agent SDK is not installed. Install it with "
89
+ "`pip install claude-agent-sdk` to use the claude-sdk executor."
90
+ ) from exc
91
+
92
+ @staticmethod
93
+ def _allow(sdk, tool_input: dict[str, Any]):
94
+ """Build the SDK's 'allow' permission result, preferring its typed class."""
95
+ allow_cls = getattr(sdk, "PermissionResultAllow", None)
96
+ if allow_cls is not None:
97
+ try:
98
+ return allow_cls(updated_input=tool_input)
99
+ except TypeError:
100
+ return allow_cls()
101
+ # Documented plain-object contract as a fallback.
102
+ return {"behavior": "allow", "updatedInput": tool_input}
103
+
104
+ @staticmethod
105
+ def _deny(sdk, reason: str):
106
+ deny_cls = getattr(sdk, "PermissionResultDeny", None)
107
+ if deny_cls is not None:
108
+ try:
109
+ return deny_cls(message=reason)
110
+ except TypeError:
111
+ return deny_cls(reason)
112
+ return {"behavior": "deny", "message": reason}
113
+
114
+ # -- run ------------------------------------------------------------------------
115
+
116
+ def run_task(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
117
+ # A task can be passed at construction (for the gate) or here; the run argument wins.
118
+ if task is not None:
119
+ self.active_task = task
120
+ log_step(
121
+ f"executor/claude-sdk: starting task {task.id}",
122
+ project_root=self.project_root,
123
+ task_id=task.id,
124
+ )
125
+ try:
126
+ result = self._run_coroutine_from_sync(self._run_async(task, requirements))
127
+ log_step(
128
+ f"executor/claude-sdk: finished task {task.id}",
129
+ project_root=self.project_root,
130
+ task_id=task.id,
131
+ success=result.success,
132
+ )
133
+ return result
134
+ except RuntimeError as exc:
135
+ # Missing SDK or a nested event loop — report cleanly, never crash the caller.
136
+ logger.error("claude-sdk executor could not run %s: %s", getattr(task, "id", "?"), exc)
137
+ log_step(
138
+ f"executor/claude-sdk: finished task {getattr(task, 'id', '?')}",
139
+ project_root=self.project_root,
140
+ task_id=getattr(task, "id", None),
141
+ success=False,
142
+ error=str(exc),
143
+ )
144
+ return ExecutionResult(success=False, message=str(exc))
145
+
146
+ @staticmethod
147
+ def _run_coroutine_from_sync(coro: Coroutine[Any, Any, _T]) -> _T:
148
+ """Run an async coroutine from sync code, including nested-loop contexts.
149
+
150
+ ``asyncio.run()`` raises when a loop is already running (pytest-asyncio,
151
+ Jupyter). With no running loop we use ``asyncio.run()``; otherwise we run
152
+ the coroutine on a fresh loop in a worker thread.
153
+ """
154
+ import asyncio
155
+ import concurrent.futures
156
+
157
+ try:
158
+ asyncio.get_running_loop()
159
+ except RuntimeError:
160
+ return asyncio.run(coro)
161
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
162
+ return executor.submit(asyncio.run, coro).result()
163
+
164
+ async def _run_async(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
165
+ sdk = self._load_sdk()
166
+ prompt = PromptBuilder(self.project_root).build_task_prompt(task, requirements)
167
+ # Parity with the subprocess executor: a repair run must carry the correction
168
+ # manifest so the model knows what the prior attempt got wrong.
169
+ from devcouncil.planning.correction_manifest import repair_prompt_prefix
170
+
171
+ prefix = repair_prompt_prefix(self.project_root, task.id)
172
+ if prefix:
173
+ prompt = f"{prefix}{prompt}"
174
+
175
+ # Claude-only advisor: resolve attach, build options (old SDKs may drop
176
+ # extra_args), then steer only when the advisor flag actually landed.
177
+ advisor_extra = self._resolved_advisor_extra_args()
178
+
179
+ async def can_use_tool(tool_name: str, tool_input: dict[str, Any], *args: Any, **kwargs: Any):
180
+ decision = self.permission_decision(tool_name, tool_input or {})
181
+ if decision.allowed:
182
+ return self._allow(sdk, tool_input or {})
183
+ self.denials.append((tool_name, decision.reason))
184
+ logger.info("claude-sdk gate denied %s: %s", tool_name, decision.reason)
185
+ return self._deny(sdk, decision.reason)
186
+
187
+ options = self._build_options(sdk, can_use_tool, advisor_extra=advisor_extra)
188
+ if self._options_have_advisor(options, advisor_extra):
189
+ from devcouncil.executors.advisor_tool import (
190
+ advisor_steering_text,
191
+ advisor_user_cost_trim,
192
+ warn_advisor_preflight,
193
+ )
194
+
195
+ for warning in warn_advisor_preflight(
196
+ env=self.env or None,
197
+ main_model=self.model,
198
+ advisor_model=self.advisor_model,
199
+ ):
200
+ logger.warning("%s", warning)
201
+ nudge = advisor_steering_text(repair=bool(prefix))
202
+ cost_trim = advisor_user_cost_trim()
203
+ prompt = f"{nudge}\n\n{cost_trim}\n\n{prompt}"
204
+ try:
205
+ return await self._run_async_once(sdk, prompt, options, task)
206
+ except Exception as exc: # a mid-run SDK failure is a failed execution, not a crash
207
+ logger.exception(
208
+ "claude-sdk executor raised mid-run for %s: %s",
209
+ getattr(task, "id", "?"),
210
+ exc,
211
+ )
212
+ reason = transient_error_in_text(str(exc))
213
+ if reason is not None:
214
+ import time
215
+
216
+ logger.warning(
217
+ "claude-sdk transient failure for %s (%s); retrying once in 5s",
218
+ getattr(task, "id", "?"),
219
+ reason,
220
+ )
221
+ time.sleep(5.0)
222
+ try:
223
+ return await self._run_async_once(sdk, prompt, options, task)
224
+ except Exception as retry_exc:
225
+ logger.exception(
226
+ "claude-sdk retry failed for %s: %s",
227
+ getattr(task, "id", "?"),
228
+ retry_exc,
229
+ )
230
+ return ExecutionResult(
231
+ success=False,
232
+ message=f"Claude Agent SDK run failed after retry: {retry_exc}",
233
+ )
234
+ return ExecutionResult(success=False, message=f"Claude Agent SDK run failed: {exc}")
235
+
236
+ async def _run_async_once(self, sdk, prompt: str, options, task: Task) -> ExecutionResult:
237
+ result_text: Optional[str] = None
238
+ is_error = False
239
+ async for message in sdk.query(prompt=prompt, options=options):
240
+ session_id = self._message_session_id(message)
241
+ if session_id:
242
+ self.last_agent_session_id = session_id
243
+ text, error = self._message_result(message)
244
+ if text is not None:
245
+ result_text = text
246
+ if error:
247
+ is_error = True
248
+ if self.denials:
249
+ denied = "; ".join(f"{name} ({reason})" for name, reason in self.denials)
250
+ suffix = f" Gate denied {len(self.denials)} out-of-scope call(s): {denied}."
251
+ else:
252
+ suffix = ""
253
+ message = (result_text or "Claude Agent SDK run finished.") + suffix
254
+ return ExecutionResult(success=not is_error, message=message)
255
+
256
+ def _resolved_advisor_extra_args(self) -> dict[str, str] | None:
257
+ """Build SDK ``extra_args={"advisor": ...}`` when pairing looks safe."""
258
+ if not self.advisor_model:
259
+ return None
260
+ from devcouncil.executors.advisor_tool import decide_advisor_attach
261
+
262
+ decision, resolved, reason = decide_advisor_attach(
263
+ main_model=self.model,
264
+ advisor_model=self.advisor_model,
265
+ env=self.env,
266
+ )
267
+ if decision != "attach" or not resolved:
268
+ if reason:
269
+ logger.warning(
270
+ "Skipping SDK advisor %s: %s",
271
+ self.advisor_model,
272
+ reason,
273
+ )
274
+ return None
275
+ return {"advisor": resolved}
276
+
277
+ @staticmethod
278
+ def _options_have_advisor(options: Any, advisor_extra: dict[str, str] | None) -> bool:
279
+ """True when advisor was requested and survived into the SDK options object."""
280
+ if not advisor_extra:
281
+ return False
282
+ if isinstance(options, dict):
283
+ extra = options.get("extra_args") or {}
284
+ else:
285
+ extra = getattr(options, "extra_args", None) or {}
286
+ if not isinstance(extra, dict):
287
+ return False
288
+ return bool(extra.get("advisor"))
289
+
290
+ def _build_options(self, sdk, can_use_tool, *, advisor_extra: dict[str, str] | None = None):
291
+ options_cls = getattr(sdk, "ClaudeAgentOptions", None)
292
+ kwargs: dict[str, Any] = {
293
+ "cwd": str(self.project_root),
294
+ "permission_mode": self.permission_mode,
295
+ "can_use_tool": can_use_tool,
296
+ }
297
+ if self.model:
298
+ kwargs["model"] = self.model
299
+ if self.env:
300
+ kwargs["env"] = dict(self.env)
301
+ if advisor_extra:
302
+ kwargs["extra_args"] = dict(advisor_extra)
303
+ if options_cls is None:
304
+ return kwargs
305
+ # Only pass kwargs the SDK's options object actually accepts, so a version skew in
306
+ # field names degrades gracefully instead of raising TypeError.
307
+ try:
308
+ return options_cls(**kwargs)
309
+ except TypeError:
310
+ # Progressively narrower kwarg sets: an SDK too old for ``env`` / ``extra_args``
311
+ # still gets cwd/permission_mode/model rather than losing all options.
312
+ obj = None
313
+ dropped_extra = bool(advisor_extra)
314
+ for keys in (
315
+ {"cwd", "permission_mode", "model", "env", "extra_args"},
316
+ {"cwd", "permission_mode", "model", "env"},
317
+ {"cwd", "permission_mode", "model"},
318
+ ):
319
+ safe = {k: v for k, v in kwargs.items() if k in keys}
320
+ try:
321
+ obj = options_cls(**safe)
322
+ if dropped_extra and "extra_args" not in safe:
323
+ logger.warning(
324
+ "claude-agent-sdk ClaudeAgentOptions rejected extra_args; "
325
+ "advisor_model=%r will not be applied. Upgrade claude-agent-sdk.",
326
+ self.advisor_model,
327
+ )
328
+ break
329
+ except TypeError:
330
+ continue
331
+ if obj is None:
332
+ obj = options_cls()
333
+ if dropped_extra:
334
+ logger.warning(
335
+ "claude-agent-sdk ClaudeAgentOptions could not accept advisor "
336
+ "extra_args; advisor_model=%r dropped.",
337
+ self.advisor_model,
338
+ )
339
+ if hasattr(obj, "can_use_tool"):
340
+ obj.can_use_tool = can_use_tool
341
+ return obj
342
+
343
+ @staticmethod
344
+ def _message_session_id(message: Any) -> str | None:
345
+ for getter in (lambda: getattr(message, "session_id", None),
346
+ lambda: message.get("session_id") if isinstance(message, dict) else None,
347
+ lambda: (getattr(message, "data", {}) or {}).get("session_id")):
348
+ try:
349
+ value = getter()
350
+ except Exception:
351
+ value = None
352
+ if value:
353
+ return str(value)
354
+ return None
355
+
356
+ @staticmethod
357
+ def _message_result(message: Any) -> tuple[str | None, bool]:
358
+ """Extract final result text and error flag from a terminal result message."""
359
+ mtype = getattr(message, "type", None)
360
+ if mtype is None and isinstance(message, dict):
361
+ mtype = message.get("type")
362
+ subtype = getattr(message, "subtype", None)
363
+ if subtype is None and isinstance(message, dict):
364
+ subtype = message.get("subtype")
365
+ is_result = mtype == "result" or type(message).__name__ == "ResultMessage"
366
+ if not is_result:
367
+ return None, False
368
+ result = getattr(message, "result", None)
369
+ if result is None and isinstance(message, dict):
370
+ result = message.get("result")
371
+ is_error = getattr(message, "is_error", None)
372
+ if is_error is None and isinstance(message, dict):
373
+ is_error = message.get("is_error")
374
+ if is_error is None:
375
+ is_error = subtype not in (None, "success")
376
+ return (str(result) if result is not None else None), bool(is_error)