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,216 @@
1
+ """Unwired-file gate: flag newly added modules nothing (non-test) imports.
2
+
3
+ Diff-scoped — only files this task *added*. Requires an importer outside the
4
+ task's added set that is not a test file. Shares exemptions/entry-roots with
5
+ ``indexing/wiring.py``. Never raises.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from pathlib import Path
12
+ from typing import Callable, List, Optional, Set
13
+
14
+ from devcouncil.domain.gap import Gap
15
+ from devcouncil.domain.task import Task
16
+ from devcouncil.indexing.wiring import (
17
+ ALLOW_UNWIRED,
18
+ build_dynamic_import_index,
19
+ entry_roots,
20
+ has_allow_unwired,
21
+ is_liveness_code_file,
22
+ is_test_path,
23
+ reference_cleared,
24
+ structural_exemptions,
25
+ )
26
+ from devcouncil.verification.checks.orphan_diff import classify_change_paths
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ def _norm(path: str) -> str:
32
+ s = path.replace("\\", "/")
33
+ while s.startswith("./"):
34
+ s = s[2:]
35
+ return s
36
+
37
+
38
+ def added_files_from_diff(diff_content: str) -> Set[str]:
39
+ """Paths introduced via ``--- /dev/null`` / ``+++ b/...`` headers."""
40
+ added: Set[str] = set()
41
+ pending_new = False
42
+ for raw in (diff_content or "").splitlines():
43
+ if raw.startswith("--- "):
44
+ target = raw[4:].strip()
45
+ pending_new = target == "/dev/null"
46
+ continue
47
+ if raw.startswith("+++ ") and pending_new:
48
+ target = raw[4:].strip()
49
+ pending_new = False
50
+ if target == "/dev/null":
51
+ continue
52
+ path = target[2:] if target.startswith(("a/", "b/")) else target
53
+ added.add(_norm(path))
54
+ return added
55
+
56
+
57
+ def collect_added_files(
58
+ *,
59
+ project_root: Path,
60
+ changed_files: List[str],
61
+ diff_content: str,
62
+ get_untracked_files: Callable[[], List[str]],
63
+ classify_fn: Callable[[List[str]], tuple[List[str], List[str]]] | None = None,
64
+ ) -> Set[str]:
65
+ """Union of classify_change_paths added set and committed-diff ``/dev/null`` headers.
66
+
67
+ Restricted to paths that appear in the change set or are currently untracked
68
+ (new) so stray diff headers outside the task's change set are dropped.
69
+ """
70
+ if classify_fn is not None:
71
+ classified, _ = classify_fn(changed_files)
72
+ added = set(classified)
73
+ else:
74
+ classified, _ = classify_change_paths(project_root, changed_files, get_untracked_files)
75
+ added = set(classified)
76
+ added |= added_files_from_diff(diff_content)
77
+ changed_norm = {_norm(p) for p in changed_files}
78
+ try:
79
+ untracked_norm = {_norm(p) for p in get_untracked_files()}
80
+ except Exception:
81
+ untracked_norm = set()
82
+ return {
83
+ _norm(p) for p in added
84
+ if _norm(p) in changed_norm or _norm(p) in untracked_norm
85
+ }
86
+
87
+
88
+ def detect_unwired_file_gaps(
89
+ *,
90
+ task: Task,
91
+ project_root: Path,
92
+ changed_files: List[str],
93
+ diff_content: str,
94
+ get_untracked_files: Callable[[], List[str]],
95
+ next_gap_id: Callable[[str, str], str],
96
+ unwired_enabled: bool = True,
97
+ unwired_blocking: bool = False,
98
+ classify_fn: Callable[[List[str]], tuple[List[str], List[str]]] | None = None,
99
+ git_files: Optional[List[str]] = None,
100
+ ) -> List[Gap]:
101
+ """Flag added Python/JS/TS files with no external non-test importer."""
102
+ gaps: List[Gap] = []
103
+ if not unwired_enabled:
104
+ return gaps
105
+ try:
106
+ added = collect_added_files(
107
+ project_root=project_root,
108
+ changed_files=changed_files,
109
+ diff_content=diff_content,
110
+ get_untracked_files=get_untracked_files,
111
+ classify_fn=classify_fn,
112
+ )
113
+ candidates = sorted(p for p in added if is_liveness_code_file(p))
114
+ if not candidates:
115
+ return gaps
116
+
117
+ from devcouncil.indexing.repo_mapper import RepoMapper
118
+
119
+ mapper = RepoMapper(project_root)
120
+ # Include untracked/new files in the edge graph so same-task imports resolve.
121
+ if git_files is None:
122
+ try:
123
+ tracked = mapper.get_git_files()
124
+ except Exception:
125
+ tracked = []
126
+ else:
127
+ tracked = list(git_files)
128
+ all_files = sorted(set(tracked) | added | {_norm(p) for p in changed_files})
129
+ dependents = mapper.dependents_for(all_files)
130
+ roots = set(entry_roots(project_root, all_files))
131
+ added_set = set(candidates)
132
+ dyn_index = build_dynamic_import_index(project_root, all_files)
133
+
134
+ for path in candidates:
135
+ if path in roots or structural_exemptions(path):
136
+ continue
137
+
138
+ if has_allow_unwired(project_root, path):
139
+ # Marker always clears the blocking gap; still emit an advisory so
140
+ # intentional unwired files stay visible in the repair loop.
141
+ gaps.append(Gap(
142
+ id=next_gap_id(task.id, f"UNWIREDDECL-{path}"),
143
+ severity="medium",
144
+ gap_type="unwired_file",
145
+ task_id=task.id,
146
+ description=(
147
+ f"Intentional unwired file declared at {path} "
148
+ f"({ALLOW_UNWIRED})."
149
+ ),
150
+ evidence=[path, ALLOW_UNWIRED],
151
+ recommended_fix=(
152
+ "Review declared unwired files before marking done; wire each "
153
+ "into its intended caller when scaffolding is complete."
154
+ ),
155
+ blocking=False,
156
+ file=path,
157
+ ))
158
+ continue
159
+
160
+ importers = dependents.get(path, set())
161
+ external_non_test = {
162
+ i for i in importers
163
+ if i not in added_set and not is_test_path(i)
164
+ }
165
+ if external_non_test:
166
+ continue
167
+
168
+ if reference_cleared(
169
+ project_root,
170
+ path,
171
+ skip_files=added_set,
172
+ git_files=all_files,
173
+ dynamic_index=dyn_index,
174
+ ):
175
+ continue
176
+
177
+ only_test = {i for i in importers if is_test_path(i) and i not in added_set}
178
+ only_same_task = {i for i in importers if i in added_set}
179
+ if only_test and not only_same_task:
180
+ desc = (
181
+ f"New file {path} is only imported by test files "
182
+ f"({', '.join(sorted(only_test)[:3])}); production code must import it."
183
+ )
184
+ elif only_same_task and not only_test:
185
+ desc = (
186
+ f"New file {path} is only imported by other files added in this task "
187
+ f"({', '.join(sorted(only_same_task)[:3])}); a pre-existing non-test "
188
+ "caller must import it (same-task island rule)."
189
+ )
190
+ elif importers:
191
+ desc = (
192
+ f"New file {path} has no external non-test importer "
193
+ f"(importers: {', '.join(sorted(importers)[:5])})."
194
+ )
195
+ else:
196
+ desc = f"New file {path} is never imported by any other module."
197
+
198
+ gaps.append(Gap(
199
+ id=next_gap_id(task.id, f"UNWIRED-{path}"),
200
+ severity="high" if unwired_blocking else "medium",
201
+ gap_type="unwired_file",
202
+ task_id=task.id,
203
+ description=desc,
204
+ evidence=[path],
205
+ recommended_fix=(
206
+ f"Import or register `{path}` from its intended non-test caller "
207
+ f"(append the caller with `dev scope update {task.id} --lease-token "
208
+ f"<token> --planned-file <caller>` if needed), or delete the unused file."
209
+ ),
210
+ blocking=unwired_blocking,
211
+ file=path,
212
+ ))
213
+ except Exception:
214
+ logger.debug("detect_unwired_file_gaps failed; degrading to zero gaps", exc_info=True)
215
+ return []
216
+ return gaps
@@ -0,0 +1,23 @@
1
+ """Claim lie-detector: map completion text → assertions → independent checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from devcouncil.verification.claims.checks import execute_checks
6
+ from devcouncil.verification.claims.mapper import map_claims
7
+ from devcouncil.verification.claims.models import Assertion, CheckResult, Kind, Status
8
+ from devcouncil.verification.claims.transcript import last_assistant_text, last_assistant_sentence
9
+ from devcouncil.verification.claims.verdict import ClaimVerdict, decide_claims, summary_line
10
+
11
+ __all__ = [
12
+ "Assertion",
13
+ "CheckResult",
14
+ "ClaimVerdict",
15
+ "Kind",
16
+ "Status",
17
+ "decide_claims",
18
+ "execute_checks",
19
+ "last_assistant_sentence",
20
+ "last_assistant_text",
21
+ "map_claims",
22
+ "summary_line",
23
+ ]
@@ -0,0 +1,395 @@
1
+ """Independent re-execution of configured commands and git/fs cross-checks.
2
+
3
+ Commands come from DevCouncil ``config.commands`` (test/lint/typecheck), never
4
+ from claim text — except COMMAND_SUCCEEDED matching a configured command.
5
+ Timeouts yield UNVERIFIABLE (never block).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import shlex
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ import time
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ from devcouncil.verification.claims.models import Assertion, CheckResult, Kind, Status
19
+
20
+ TAIL_LINES = 50
21
+
22
+
23
+ @dataclass
24
+ class ClaimCheckBudget:
25
+ """Timeouts for claim command re-runs."""
26
+
27
+ per_command_timeout: int = 90
28
+ total_timeout: int = 120
29
+
30
+
31
+ @dataclass
32
+ class CommandOutcome:
33
+ exit_code: int | None
34
+ output_tail: str
35
+ duration: float
36
+ timed_out: bool
37
+
38
+
39
+ def _tail(*chunks: str | bytes | None) -> str:
40
+ parts: list[str] = []
41
+ for chunk in chunks:
42
+ if chunk is None:
43
+ continue
44
+ if isinstance(chunk, bytes):
45
+ chunk = chunk.decode("utf-8", errors="replace")
46
+ if chunk.strip():
47
+ parts.append(chunk)
48
+ combined = "\n".join(parts)
49
+ lines = combined.splitlines()
50
+ return "\n".join(lines[-TAIL_LINES:])
51
+
52
+
53
+ def _argv_to_shell(cmd: list[str] | str) -> str:
54
+ if isinstance(cmd, str):
55
+ return cmd
56
+ return shlex.join(cmd)
57
+
58
+
59
+ def run_command(cmd: str, cwd: Path, timeout: int) -> CommandOutcome:
60
+ try:
61
+ argv = shlex.split(cmd)
62
+ except ValueError:
63
+ argv = []
64
+ if argv and argv[0] in {"python", "python3"} and shutil.which(argv[0]) is None:
65
+ # Configs commonly use the portable `python` spelling even when macOS
66
+ # exposes only the interpreter running DevCouncil. Preserve the rest of
67
+ # the shell command while resolving that launcher deterministically.
68
+ cmd = f"{shlex.quote(sys.executable)}{cmd[len(argv[0]):]}"
69
+ start = time.monotonic()
70
+ try:
71
+ proc = subprocess.run(
72
+ cmd,
73
+ shell=True,
74
+ cwd=str(cwd),
75
+ capture_output=True,
76
+ text=True,
77
+ errors="replace",
78
+ timeout=timeout,
79
+ )
80
+ return CommandOutcome(
81
+ exit_code=proc.returncode,
82
+ output_tail=_tail(proc.stdout, proc.stderr),
83
+ duration=time.monotonic() - start,
84
+ timed_out=False,
85
+ )
86
+ except subprocess.TimeoutExpired as exc:
87
+ return CommandOutcome(
88
+ exit_code=None,
89
+ output_tail=_tail(exc.stdout, exc.stderr),
90
+ duration=time.monotonic() - start,
91
+ timed_out=True,
92
+ )
93
+ except OSError as exc:
94
+ return CommandOutcome(
95
+ exit_code=None,
96
+ output_tail=f"failed to launch command: {exc}",
97
+ duration=time.monotonic() - start,
98
+ timed_out=False,
99
+ )
100
+
101
+
102
+ def _git_output(cwd: Path, *args: str) -> str | None:
103
+ try:
104
+ proc = subprocess.run(
105
+ ["git", *args],
106
+ cwd=str(cwd),
107
+ capture_output=True,
108
+ text=True,
109
+ errors="replace",
110
+ timeout=15,
111
+ )
112
+ except (OSError, subprocess.TimeoutExpired):
113
+ return None
114
+ return proc.stdout if proc.returncode == 0 else None
115
+
116
+
117
+ def git_toplevel(cwd: Path) -> Path | None:
118
+ out = _git_output(cwd, "rev-parse", "--show-toplevel")
119
+ return Path(out.strip()) if out and out.strip() else None
120
+
121
+
122
+ def last_commit_paths(cwd: Path) -> set[str]:
123
+ out = _git_output(cwd, "log", "-1", "--name-only", "--pretty=format:")
124
+ if not out:
125
+ return set()
126
+ return {line.strip().strip('"') for line in out.splitlines() if line.strip()}
127
+
128
+
129
+ def git_changed_files(cwd: Path) -> dict[str, str] | None:
130
+ try:
131
+ proc = subprocess.run(
132
+ ["git", "status", "--porcelain", "-uall"],
133
+ cwd=str(cwd),
134
+ capture_output=True,
135
+ text=True,
136
+ errors="replace",
137
+ timeout=15,
138
+ )
139
+ except (OSError, subprocess.TimeoutExpired):
140
+ return None
141
+ if proc.returncode != 0:
142
+ return None
143
+ changed: dict[str, str] = {}
144
+ for line in proc.stdout.splitlines():
145
+ if len(line) < 4:
146
+ continue
147
+ status, path = line[:2], line[3:]
148
+ if " -> " in path:
149
+ path = path.split(" -> ", 1)[1]
150
+ changed[path.strip().strip('"')] = status
151
+ return changed
152
+
153
+
154
+ def _normalize(path: str) -> str:
155
+ return path.replace("\\", "/").strip("/").lower()
156
+
157
+
158
+ def _in_changed(rel_path: str, changed: dict[str, str]) -> bool:
159
+ target = _normalize(rel_path)
160
+ return any(_normalize(p) == target for p in changed)
161
+
162
+
163
+ class _Runner:
164
+ def __init__(self, budget: ClaimCheckBudget, cwd: Path):
165
+ self.budget = budget
166
+ self.cwd = cwd
167
+ self.deadline = time.monotonic() + budget.total_timeout
168
+ self.cache: dict[str, CommandOutcome] = {}
169
+
170
+ def run(self, cmd: str) -> CommandOutcome | None:
171
+ if cmd in self.cache:
172
+ return self.cache[cmd]
173
+ remaining = self.deadline - time.monotonic()
174
+ if remaining <= 0:
175
+ return None
176
+ outcome = run_command(
177
+ cmd, self.cwd, timeout=int(min(self.budget.per_command_timeout, max(remaining, 1)))
178
+ )
179
+ self.cache[cmd] = outcome
180
+ return outcome
181
+
182
+
183
+ _NOT_FOUND_MARKERS = (
184
+ "is not recognized as an internal or external command",
185
+ "is not recognized as a name of a cmdlet",
186
+ "command not found",
187
+ "commandnotfoundexception",
188
+ "no such file or directory",
189
+ "failed to launch command",
190
+ )
191
+
192
+
193
+ def _command_not_found(outcome: CommandOutcome) -> bool:
194
+ if outcome.exit_code in (127, 9009):
195
+ return True
196
+ tail = outcome.output_tail.lower()
197
+ return outcome.exit_code != 0 and any(marker in tail for marker in _NOT_FOUND_MARKERS)
198
+
199
+
200
+ def _command_result(assertion: Assertion, cmd: str | None, label: str, runner: _Runner) -> CheckResult:
201
+ if not cmd:
202
+ return CheckResult(assertion, Status.UNVERIFIABLE, f"no {label} command configured")
203
+ outcome = runner.run(cmd)
204
+ if outcome is None:
205
+ return CheckResult(assertion, Status.SKIPPED, "verification budget exhausted")
206
+ if outcome.timed_out:
207
+ return CheckResult(
208
+ assertion, Status.UNVERIFIABLE, f"`{cmd}` timed out after {outcome.duration:.0f}s"
209
+ )
210
+ if _command_not_found(outcome):
211
+ return CheckResult(
212
+ assertion,
213
+ Status.UNVERIFIABLE,
214
+ f"`{cmd}` could not be launched (command not found) — fix the configured {label} command",
215
+ )
216
+ if outcome.exit_code == 0:
217
+ return CheckResult(assertion, Status.PASS, f"`{cmd}` exited 0 in {outcome.duration:.1f}s")
218
+ return CheckResult(
219
+ assertion,
220
+ Status.FAIL,
221
+ f"`{cmd}` exited with exit code {outcome.exit_code}.\nOutput tail:\n{outcome.output_tail}",
222
+ )
223
+
224
+
225
+ def _resolve_in_repo(target: str, cwd: Path) -> Path | None:
226
+ candidate = Path(target)
227
+ if not candidate.is_absolute():
228
+ candidate = cwd / candidate
229
+ try:
230
+ resolved = candidate.resolve()
231
+ if not resolved.is_relative_to(cwd.resolve()):
232
+ return None
233
+ return resolved
234
+ except OSError:
235
+ return None
236
+
237
+
238
+ @dataclass
239
+ class _GitInfo:
240
+ toplevel: Path
241
+ changed: dict[str, str]
242
+ last_commit: set[str]
243
+
244
+
245
+ def _git_info(cwd: Path) -> _GitInfo | None:
246
+ toplevel = git_toplevel(cwd)
247
+ if toplevel is None:
248
+ return None
249
+ changed = git_changed_files(cwd) or {}
250
+ return _GitInfo(toplevel=toplevel, changed=changed, last_commit=last_commit_paths(cwd))
251
+
252
+
253
+ def _find_unique_basename(name: str, cwd: Path, git: _GitInfo | None) -> Path | None:
254
+ if git is None:
255
+ return None
256
+ tracked = _git_output(cwd, "ls-files")
257
+ pool = set(tracked.splitlines() if tracked else []) | set(git.changed) | git.last_commit
258
+ matches = {p for p in pool if p.strip() and _normalize(p).rsplit("/", 1)[-1] == _normalize(name)}
259
+ if len(matches) != 1:
260
+ return None
261
+ return git.toplevel / matches.pop()
262
+
263
+
264
+ def _changed_status(rel_path: str, changed: dict[str, str]) -> str:
265
+ target = _normalize(rel_path)
266
+ for path, status in changed.items():
267
+ if _normalize(path) == target:
268
+ return status.strip() or status
269
+ return "?"
270
+
271
+
272
+ def _file_result(assertion: Assertion, cwd: Path, git: _GitInfo | None) -> CheckResult:
273
+ target = assertion.target or ""
274
+ resolved = _resolve_in_repo(target, cwd)
275
+ if resolved is None:
276
+ return CheckResult(
277
+ assertion, Status.UNVERIFIABLE, f"claimed path `{target}` is outside the working directory"
278
+ )
279
+
280
+ if not resolved.exists() and "/" not in target and "\\" not in target:
281
+ alt = _find_unique_basename(target, cwd, git)
282
+ if alt is not None and alt.exists():
283
+ resolved = alt.resolve()
284
+
285
+ if not resolved.exists():
286
+ return CheckResult(assertion, Status.FAIL, f"claimed file `{target}` does not exist")
287
+
288
+ if git is None:
289
+ return CheckResult(assertion, Status.PASS, f"`{target}` exists")
290
+
291
+ try:
292
+ rel = resolved.relative_to(git.toplevel.resolve()).as_posix()
293
+ except ValueError:
294
+ return CheckResult(assertion, Status.PASS, f"`{target}` exists (outside the git repo)")
295
+
296
+ if _in_changed(rel, git.changed):
297
+ return CheckResult(
298
+ assertion,
299
+ Status.PASS,
300
+ f"`{target}` exists (git status: {_changed_status(rel, git.changed)})",
301
+ )
302
+ if any(_normalize(p) == _normalize(rel) for p in git.last_commit):
303
+ return CheckResult(
304
+ assertion, Status.PASS, f"`{target}` exists (touched by the most recent commit)"
305
+ )
306
+ return CheckResult(
307
+ assertion,
308
+ Status.UNVERIFIABLE,
309
+ f"`{target}` exists but git shows no recent change to it; cannot confirm it was "
310
+ "created/modified in this session",
311
+ )
312
+
313
+
314
+ @dataclass
315
+ class ResolvedCommands:
316
+ test: str | None = None
317
+ build: str | None = None # typecheck in DevCouncil
318
+ lint: str | None = None
319
+
320
+
321
+ def resolve_commands_from_config(commands_cfg: object) -> ResolvedCommands:
322
+ """Map DevCouncil CommandsConfig lists to shell command strings."""
323
+ def first(attr: str) -> str | None:
324
+ value = getattr(commands_cfg, attr, None) or []
325
+ if isinstance(value, str) and value.strip():
326
+ return value.strip()
327
+ if isinstance(value, list) and value:
328
+ first_cmd = value[0]
329
+ if isinstance(first_cmd, list):
330
+ return _argv_to_shell(first_cmd)
331
+ if isinstance(first_cmd, str) and first_cmd.strip():
332
+ return first_cmd.strip()
333
+ return None
334
+
335
+ return ResolvedCommands(test=first("test"), build=first("typecheck"), lint=first("lint"))
336
+
337
+
338
+ def _matching_config_command(target: str, resolved: ResolvedCommands) -> str | None:
339
+ wanted = " ".join(target.split()).lower()
340
+ for cmd in (resolved.test, resolved.build, resolved.lint):
341
+ if cmd and wanted == " ".join(cmd.split()).lower():
342
+ return cmd
343
+ return None
344
+
345
+
346
+ def execute_checks(
347
+ assertions: list[Assertion],
348
+ *,
349
+ cwd: Path,
350
+ commands: ResolvedCommands,
351
+ budget: ClaimCheckBudget | None = None,
352
+ ) -> list[CheckResult]:
353
+ """Independently verify each assertion against the real environment."""
354
+ runner = _Runner(budget or ClaimCheckBudget(), cwd)
355
+ git = _git_info(cwd)
356
+ results: list[CheckResult] = []
357
+
358
+ for assertion in assertions:
359
+ kind = assertion.kind
360
+ if kind is Kind.TESTS_PASS:
361
+ results.append(_command_result(assertion, commands.test, "test", runner))
362
+ elif kind is Kind.BUILD_SUCCEEDS:
363
+ results.append(_command_result(assertion, commands.build, "typecheck/build", runner))
364
+ elif kind is Kind.LINT_CLEAN:
365
+ results.append(_command_result(assertion, commands.lint, "lint", runner))
366
+ elif kind in (Kind.FILE_CREATED, Kind.FILE_UPDATED):
367
+ results.append(_file_result(assertion, cwd, git))
368
+ elif kind is Kind.COMMAND_SUCCEEDED:
369
+ cmd = _matching_config_command(assertion.target or "", commands)
370
+ if cmd is None:
371
+ results.append(
372
+ CheckResult(
373
+ assertion,
374
+ Status.UNVERIFIABLE,
375
+ f"claimed command `{assertion.target}` does not match any configured "
376
+ "command; not re-running arbitrary commands",
377
+ )
378
+ )
379
+ else:
380
+ results.append(_command_result(assertion, cmd, "matched", runner))
381
+ elif kind is Kind.GENERIC_DONE:
382
+ if commands.test:
383
+ results.append(_command_result(assertion, commands.test, "test", runner))
384
+ else:
385
+ results.append(
386
+ CheckResult(
387
+ assertion,
388
+ Status.UNVERIFIABLE,
389
+ "generic completion claim with no configured checks",
390
+ )
391
+ )
392
+ else:
393
+ results.append(CheckResult(assertion, Status.UNVERIFIABLE, f"no checker for kind {kind}"))
394
+
395
+ return results