devcouncil 0.3.0 → 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,168 @@
1
+ """Claim-to-check mapper: free-text completion claims → structured assertions.
2
+
3
+ Port of Oversight semantics: sentence-scoped negation/hedging; specific
4
+ assertions suppress GENERIC_DONE; capped to MAX_ASSERTIONS.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+
11
+ from devcouncil.verification.claims.models import Assertion, Kind
12
+
13
+ MAX_ASSERTIONS = 5
14
+
15
+ _SENTENCE_SPLIT = re.compile(r"(?<=[.!?])\s+|\n+")
16
+
17
+ _BENIGN_NEGATIVE = re.compile(
18
+ r"\b(?:0|no|zero|without)\s+(?:\w+\s+)?(?:failures?|errors?|warnings?|regressions?|issues?)\b"
19
+ r"|\bnone\s+failed\b",
20
+ re.I,
21
+ )
22
+
23
+ _HEDGE = re.compile(
24
+ r"\b(?:not|never|no|fail(?:s|ed|ing|ure)?|should|would|could|might|"
25
+ r"will|going\s+to|plan(?:s|ning)?\s+to|need(?:s)?\s+to|todo|"
26
+ r"haven'?t|hasn'?t|didn'?t|don'?t|doesn'?t|won'?t|can'?t|isn'?t|aren'?t|"
27
+ r"still|yet|broken|remaining\s+work)\b",
28
+ re.I,
29
+ )
30
+
31
+ _TESTS_PASS = re.compile(
32
+ r"\btests?\s+(?:are\s+|all\s+|now\s+)*pass(?:es|ing)?\b"
33
+ r"|\btest\s+suite\s+passes\b"
34
+ r"|\b(?:pytest|jest|vitest|unittest)\b.*\b(?:green|passed|passing)\b"
35
+ r"|\b\d+\s+(?:tests?\s+)?(?:passed|passing)\b"
36
+ r"|\b(?:test\s+)?suite\s+is\s+green\b"
37
+ r"|\b(?:all\s+)?checks\s+(?:are\s+)?green\b"
38
+ r"|\b(?:unit\s+)?tests\s+succeed(?:ed)?\b"
39
+ r"|\bci\s+is\s+(?:happy|green|passing)\b"
40
+ r"|\beverything\s+passed\b",
41
+ re.I,
42
+ )
43
+ _BUILD = re.compile(
44
+ r"\bbuild\s+(?:succeeds|succeeded|passes|passed|completed\s+successfully|is\s+(?:now\s+)?(?:passing|clean|green))\b"
45
+ r"|\bcompiles?(?:\s+cleanly)?\b"
46
+ r"|\bbuilds\s+cleanly\b"
47
+ r"|\bbuild\s+completed\b",
48
+ re.I,
49
+ )
50
+ _LINT = re.compile(
51
+ r"\blint(?:ing|er)?\s+(?:passes|passed|is\s+clean|clean)\b"
52
+ r"|\bno\s+(?:type|lint(?:ing)?)\s+errors\b"
53
+ r"|\btype-?check(?:ing)?\s+passes\b"
54
+ r"|\bno\s+lint\s+(?:errors|warnings)\b"
55
+ r"|\b(?:mypy|ruff|eslint|flake8)\b.*\b(?:passes|passed|clean)\b",
56
+ re.I,
57
+ )
58
+ _GENERIC_DONE = re.compile(
59
+ r"\b(?:done|completed?|finished|everything\s+works|all\s+set|task\s+is\s+complete|implementation\s+is\s+complete)\b",
60
+ re.I,
61
+ )
62
+ _CREATE_VERB = re.compile(r"\b(?:created|added|wrote|generated|new\s+file)\b", re.I)
63
+ _MODIFY_VERB = re.compile(r"\b(?:updated|edited|modified|changed|fixed|refactored|patched|rewrote)\b", re.I)
64
+ _RAN_COMMAND = re.compile(r"\b(?:ran|executed|invoked)\s+[`\"']([^`\"'\n]+)[`\"'].*?\bsuccess", re.I)
65
+
66
+ _QUOTED_SPAN = re.compile(r"[`\"']([^`\"'\n]+)[`\"']")
67
+ _BARE_PATH = re.compile(r"(?<![\w./\\-])(?:[A-Za-z]:[/\\])?[\w.-]+(?:[/\\][\w.-]+)+(?![\w/\\])")
68
+ _KNOWN_EXTENSIONS = {
69
+ "py", "js", "ts", "tsx", "jsx", "json", "md", "txt", "toml", "yaml", "yml",
70
+ "html", "css", "scss", "sql", "sh", "ps1", "psm1", "bat", "cs", "java",
71
+ "go", "rs", "c", "cpp", "h", "hpp", "rb", "php", "xml", "csv", "ini",
72
+ "cfg", "lock", "env",
73
+ }
74
+
75
+
76
+ def _has_known_extension(token: str) -> bool:
77
+ name = token.replace("\\", "/").rsplit("/", 1)[-1]
78
+ if "." not in name:
79
+ return False
80
+ return name.rsplit(".", 1)[-1].lower() in _KNOWN_EXTENSIONS
81
+
82
+
83
+ def _looks_like_path(token: str, bare: bool = False) -> bool:
84
+ token = token.strip()
85
+ if not token or " " in token:
86
+ return False
87
+ if bare:
88
+ return _has_known_extension(token)
89
+ if "/" in token or "\\" in token:
90
+ return True
91
+ return _has_known_extension(token)
92
+
93
+
94
+ def _extract_paths(sentence: str) -> list[str]:
95
+ paths: list[str] = []
96
+ seen: set[str] = set()
97
+ for match in _QUOTED_SPAN.finditer(sentence):
98
+ token = match.group(1).strip().rstrip(".,;:!?")
99
+ if _looks_like_path(token) and token not in seen:
100
+ seen.add(token)
101
+ paths.append(token)
102
+ without_quotes = _QUOTED_SPAN.sub(" ", sentence)
103
+ for match in _BARE_PATH.finditer(without_quotes):
104
+ token = match.group(0).rstrip(".,;:!?")
105
+ if _looks_like_path(token, bare=True) and token not in seen:
106
+ seen.add(token)
107
+ paths.append(token)
108
+ return paths
109
+
110
+
111
+ def map_claims(text: str) -> list[Assertion]:
112
+ """Parse a completion message into a deduplicated, capped assertion list."""
113
+ if not text or not text.strip():
114
+ return []
115
+
116
+ specific: list[Assertion] = []
117
+ generic: list[Assertion] = []
118
+ seen: set[tuple[Kind, str | None]] = set()
119
+
120
+ def add(bucket: list[Assertion], kind: Kind, target: str | None, source: str) -> None:
121
+ key = (kind, target)
122
+ if key in seen:
123
+ return
124
+ seen.add(key)
125
+ bucket.append(Assertion(kind=kind, target=target, source_text=source.strip()))
126
+
127
+ for sentence in _SENTENCE_SPLIT.split(text):
128
+ sentence = sentence.strip()
129
+ if not sentence:
130
+ continue
131
+ sanitized = _BENIGN_NEGATIVE.sub(" ", sentence)
132
+ if _HEDGE.search(sanitized):
133
+ continue
134
+
135
+ if _TESTS_PASS.search(sentence):
136
+ add(specific, Kind.TESTS_PASS, None, sentence)
137
+ if _BUILD.search(sentence):
138
+ add(specific, Kind.BUILD_SUCCEEDS, None, sentence)
139
+ if _LINT.search(sentence):
140
+ add(specific, Kind.LINT_CLEAN, None, sentence)
141
+
142
+ ran = _RAN_COMMAND.search(sentence)
143
+ if ran:
144
+ add(specific, Kind.COMMAND_SUCCEEDED, ran.group(1).strip(), sentence)
145
+
146
+ paths = _extract_paths(sentence)
147
+ if paths:
148
+ if _CREATE_VERB.search(sentence):
149
+ for path in paths:
150
+ add(specific, Kind.FILE_CREATED, path, sentence)
151
+ elif _MODIFY_VERB.search(sentence):
152
+ for path in paths:
153
+ add(specific, Kind.FILE_UPDATED, path, sentence)
154
+
155
+ if _GENERIC_DONE.search(sentence):
156
+ add(generic, Kind.GENERIC_DONE, None, sentence)
157
+
158
+ priority = {
159
+ Kind.TESTS_PASS: 0,
160
+ Kind.BUILD_SUCCEEDS: 1,
161
+ Kind.LINT_CLEAN: 2,
162
+ Kind.COMMAND_SUCCEEDED: 3,
163
+ Kind.FILE_CREATED: 4,
164
+ Kind.FILE_UPDATED: 5,
165
+ }
166
+ specific.sort(key=lambda a: priority.get(a.kind, 9))
167
+ result = specific if specific else generic
168
+ return result[:MAX_ASSERTIONS]
@@ -0,0 +1,39 @@
1
+ """Core data types for claim verification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+
8
+
9
+ class Kind(str, Enum):
10
+ TESTS_PASS = "tests_pass"
11
+ BUILD_SUCCEEDS = "build_succeeds"
12
+ LINT_CLEAN = "lint_clean"
13
+ FILE_CREATED = "file_created"
14
+ FILE_UPDATED = "file_updated"
15
+ COMMAND_SUCCEEDED = "command_succeeded"
16
+ GENERIC_DONE = "generic_done"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class Assertion:
21
+ """A checkable claim extracted from the agent's completion message."""
22
+
23
+ kind: Kind
24
+ target: str | None = None
25
+ source_text: str = ""
26
+
27
+
28
+ class Status(str, Enum):
29
+ PASS = "pass"
30
+ FAIL = "fail"
31
+ UNVERIFIABLE = "unverifiable"
32
+ SKIPPED = "skipped"
33
+
34
+
35
+ @dataclass
36
+ class CheckResult:
37
+ assertion: Assertion
38
+ status: Status
39
+ detail: str = ""
@@ -0,0 +1,92 @@
1
+ """Extract the final assistant message from a Claude transcript JSONL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+
9
+ _SENTENCE_SPLIT = re.compile(r"(?<=[.!?])\s+")
10
+
11
+
12
+ def _text_of(entry: dict) -> str | None:
13
+ message = entry.get("message")
14
+ if not isinstance(message, dict):
15
+ return None
16
+ content = message.get("content")
17
+ if isinstance(content, str):
18
+ return content.strip() or None
19
+ if isinstance(content, list):
20
+ texts = [
21
+ block.get("text", "")
22
+ for block in content
23
+ if isinstance(block, dict) and block.get("type") == "text"
24
+ ]
25
+ joined = "\n".join(t for t in texts if t.strip())
26
+ return joined.strip() or None
27
+ return None
28
+
29
+
30
+ def last_assistant_text(path: Path) -> str | None:
31
+ """Last assistant message that contains text (tool-use-only turns skipped)."""
32
+ try:
33
+ lines = path.read_text(encoding="utf-8-sig", errors="replace").splitlines()
34
+ except OSError:
35
+ return None
36
+ for line in reversed(lines):
37
+ line = line.strip()
38
+ if not line:
39
+ continue
40
+ try:
41
+ entry = json.loads(line)
42
+ except json.JSONDecodeError:
43
+ continue
44
+ if not isinstance(entry, dict) or entry.get("type") != "assistant":
45
+ continue
46
+ text = _text_of(entry)
47
+ if text:
48
+ return text
49
+ return None
50
+
51
+
52
+ def last_assistant_sentence(path: Path) -> str | None:
53
+ """Last sentence of the last assistant text, or None."""
54
+ text = last_assistant_text(path)
55
+ if not text:
56
+ return None
57
+ parts = [p.strip() for p in _SENTENCE_SPLIT.split(text) if p.strip()]
58
+ return parts[-1] if parts else text.strip()
59
+
60
+
61
+ def ends_on_open_question(path: Path) -> bool:
62
+ """Heuristic: last assistant turn ends with '?' and no later user turn."""
63
+ try:
64
+ lines = path.read_text(encoding="utf-8-sig", errors="replace").splitlines()
65
+ except OSError:
66
+ return False
67
+ last_assistant_idx = -1
68
+ last_user_idx = -1
69
+ last_assistant_msg: str | None = None
70
+ for i, line in enumerate(lines):
71
+ line = line.strip()
72
+ if not line:
73
+ continue
74
+ try:
75
+ entry = json.loads(line)
76
+ except json.JSONDecodeError:
77
+ continue
78
+ if not isinstance(entry, dict):
79
+ continue
80
+ etype = entry.get("type")
81
+ if etype == "assistant":
82
+ text = _text_of(entry)
83
+ if text:
84
+ last_assistant_idx = i
85
+ last_assistant_msg = text
86
+ elif etype == "user":
87
+ last_user_idx = i
88
+ if last_assistant_idx < 0 or not last_assistant_msg:
89
+ return False
90
+ if last_user_idx > last_assistant_idx:
91
+ return False
92
+ return last_assistant_msg.rstrip().endswith("?")
@@ -0,0 +1,88 @@
1
+ """Merge claim check results into a corrective message / report."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from devcouncil.verification.claims.models import CheckResult, Status
8
+
9
+
10
+ @dataclass
11
+ class ClaimVerdict:
12
+ block: bool
13
+ reason: str
14
+ report: str
15
+
16
+
17
+ def render_report(results: list[CheckResult]) -> str:
18
+ if not results:
19
+ return ""
20
+ lines = [
21
+ "# DevCouncil stop-gate claim report",
22
+ "",
23
+ "| Claim | Check | Status |",
24
+ "|---|---|---|",
25
+ ]
26
+ for r in results:
27
+ claim = (r.assertion.source_text or r.assertion.kind.value).replace("|", "\\|")
28
+ target = f" `{r.assertion.target}`" if r.assertion.target else ""
29
+ lines.append(f"| {claim} | {r.assertion.kind.value}{target} | {r.status.name} |")
30
+ lines.append("")
31
+ for r in results:
32
+ if r.detail:
33
+ lines.append(f"## {r.assertion.kind.value} — {r.status.name}")
34
+ lines.append(r.detail)
35
+ lines.append("")
36
+ return "\n".join(lines)
37
+
38
+
39
+ _STATUS_ICONS = {
40
+ Status.PASS: "✓",
41
+ Status.FAIL: "✗",
42
+ Status.UNVERIFIABLE: "?",
43
+ Status.SKIPPED: "–",
44
+ }
45
+
46
+
47
+ def summary_line(results: list[CheckResult], *, prefix: str = "🛡 devcouncil") -> str:
48
+ """One-line verdict summary for Claude systemMessage."""
49
+ if not results:
50
+ return f"{prefix}: (no claims)"
51
+ parts = []
52
+ for r in results:
53
+ icon = _STATUS_ICONS.get(r.status, "?")
54
+ target = f" {r.assertion.target}" if r.assertion.target else ""
55
+ parts.append(f"{r.assertion.kind.value}{target} {icon}")
56
+ return f"{prefix}: " + " | ".join(parts)
57
+
58
+
59
+ def _corrective_message(failures: list[CheckResult]) -> str:
60
+ bullets = []
61
+ for r in failures:
62
+ claim = r.assertion.source_text or r.assertion.kind.value
63
+ bullets.append(f'- You claimed: "{claim}" — but verification found: {r.detail}')
64
+ joined = "\n".join(bullets)
65
+ return (
66
+ "CLAIM VERIFICATION FAILED. DevCouncil re-checked your completion claims "
67
+ "against the real environment and found discrepancies:\n\n"
68
+ f"{joined}\n\n"
69
+ "Do not repeat the claim. Investigate the filesystem, git, and command exit "
70
+ "codes, fix the discrepancy, and only finish when these verifications would succeed."
71
+ )
72
+
73
+
74
+ def decide_claims(results: list[CheckResult], blocks_so_far: int, max_blocks: int) -> ClaimVerdict:
75
+ failures = [r for r in results if r.status is Status.FAIL]
76
+ report = render_report(results)
77
+
78
+ if not failures:
79
+ return ClaimVerdict(block=False, reason="", report=report)
80
+
81
+ if blocks_so_far >= max_blocks:
82
+ report += (
83
+ f"\n> Block cap reached ({blocks_so_far}/{max_blocks}): failures above were "
84
+ "NOT re-injected into the agent. Review them yourself.\n"
85
+ )
86
+ return ClaimVerdict(block=False, reason="", report=report)
87
+
88
+ return ClaimVerdict(block=True, reason=_corrective_message(failures), report=report)
@@ -0,0 +1,170 @@
1
+ """Shared helpers for deciding whether a command qualifies as acceptance evidence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import shlex
7
+
8
+ # Executables (basename of argv[0]) that ARE test/check runners.
9
+ _RUNNER_EXECUTABLES = frozenset({
10
+ "pytest", "py.test", "vitest", "jest", "mocha", "ava", "tap",
11
+ "unittest", "tox", "nox", "behave", "rspec", "minitest", "phpunit",
12
+ "mypy", "pyright", "tsc",
13
+ })
14
+
15
+ # (argv0 basename, first subcommand) pairs that are runners.
16
+ _RUNNER_SUBCOMMANDS = frozenset({
17
+ ("cargo", "test"), ("go", "test"), ("mvn", "test"), ("gradle", "test"),
18
+ ("gradlew", "test"), ("make", "test"), ("make", "check"), ("ruff", "check"),
19
+ ("swift", "test"), ("dotnet", "test"), ("mix", "test"), ("stack", "test"),
20
+ })
21
+
22
+ # Package managers whose ``test`` / ``run <script>`` invocations count.
23
+ _PKG_MANAGERS = frozenset({"npm", "yarn", "pnpm", "bun"})
24
+ _SCRIPT_KEYWORDS = ("test", "typecheck", "type-check", "vitest", "jest", "check")
25
+
26
+ # Interpreters whose inline payloads (-c / -e / --eval) can carry assertions.
27
+ _INLINE_INTERPRETERS = frozenset({"python", "python3", "python2", "py", "node", "deno", "bun"})
28
+ _INLINE_FLAGS = frozenset({"-c", "-e", "--eval"})
29
+ # Assertion-ish content inside an inline payload. ``print(...)`` alone is not
30
+ # evidence; an assert / raise / expect() is.
31
+ _INLINE_EVIDENCE_RE = re.compile(
32
+ r"(\bassert\b|\braise\s+\w+|\bexpect\s*\(|\.to(?:Be|Equal|Throw)\b|\bthrow\s)",
33
+ re.IGNORECASE,
34
+ )
35
+
36
+ # A single argument token that references a test file/directory. Path-like — a
37
+ # quoted sentence containing the word "test" has spaces and cannot match.
38
+ _TEST_PATH_RE = re.compile(
39
+ r"(^|/)(tests?(/|$)|test_[^/\s]*$|[^/\s]*(?:_tests?|\.test|\.spec)\.[a-z]+$)",
40
+ re.IGNORECASE,
41
+ )
42
+
43
+
44
+ def _split(command: str) -> list[str]:
45
+ text = (command or "").strip()
46
+ if not text:
47
+ return []
48
+ try:
49
+ return shlex.split(text)
50
+ except ValueError:
51
+ return text.split()
52
+
53
+
54
+ def command_has_acceptance_evidence(command: str) -> bool:
55
+ """True when ``command``'s actual INVOCATION can produce behavioral evidence.
56
+
57
+ STRICT, structure-aware allowlist — use for UNTRUSTED input (an agent appending
58
+ expected_tests via ``update_task_scope``). The runner must be the executable, a
59
+ subcommand, a package-manager script, an inline interpreter assertion, or a
60
+ test-file path argument — never just a keyword substring anywhere in the line
61
+ (``echo "tests assert ok"`` must not qualify). For trusted planner/config
62
+ commands at verify time use :func:`command_is_trivial_evidence` instead, whose
63
+ deny-list does not punish legitimate behavioral commands that lack a keyword
64
+ (``make check``, ``./run_smoke.sh``, ``python scripts/validate.py``)."""
65
+ tokens = _split(command)
66
+ if not tokens:
67
+ return False
68
+ base = tokens[0].rsplit("/", 1)[-1].lower()
69
+ if base in _TRIVIAL_EXECUTABLES:
70
+ return False
71
+ if base in _RUNNER_EXECUTABLES:
72
+ return True
73
+
74
+ args = tokens[1:]
75
+ lowered_args = [a.lower() for a in args]
76
+ first_arg = lowered_args[0] if lowered_args else ""
77
+
78
+ # Subcommand runners: `cargo test`, `go test ./...`, `ruff check .`, ...
79
+ if (base, first_arg) in _RUNNER_SUBCOMMANDS:
80
+ return True
81
+
82
+ # Package-manager scripts: `npm test`, `npm run typecheck`, `yarn vitest`.
83
+ if base in _PKG_MANAGERS and lowered_args:
84
+ script = lowered_args[1] if first_arg == "run" and len(lowered_args) > 1 else first_arg
85
+ if any(keyword in script for keyword in _SCRIPT_KEYWORDS):
86
+ return True
87
+
88
+ # Interpreters: `python -m pytest`, `python -c "assert ..."`, `node -e "expect(...)"`.
89
+ if base in _INLINE_INTERPRETERS:
90
+ for i, arg in enumerate(lowered_args):
91
+ if arg == "-m" and i + 1 < len(lowered_args):
92
+ module = lowered_args[i + 1]
93
+ if module in _RUNNER_EXECUTABLES or module == "unittest":
94
+ return True
95
+ if arg in _INLINE_FLAGS and i + 1 < len(args) and _INLINE_EVIDENCE_RE.search(args[i + 1]):
96
+ return True
97
+
98
+ # A test-file/dir reference as the executable or an argument: `./run_tests.sh`,
99
+ # `python tests/test_x.py`, `bash tests/run.sh`.
100
+ for token in tokens:
101
+ if " " not in token and _TEST_PATH_RE.search(token):
102
+ return True
103
+
104
+ return False
105
+
106
+
107
+ # Executables whose success proves nothing about the code under review: pure
108
+ # printing, filesystem/system queries, and no-ops. First-token match (after
109
+ # common wrappers) so e.g. ``echo done`` or ``date`` can never coarse-prove an
110
+ # acceptance criterion, while ``make check`` / ``./run_smoke.sh`` still can.
111
+ _TRIVIAL_EXECUTABLES = frozenset({
112
+ "echo", "printf", "true", "false", ":", "exit", "sleep", "ls", "dir", "cat",
113
+ "head", "tail", "wc", "pwd", "cd", "which", "whereis", "whoami", "date",
114
+ "env", "printenv", "type", "touch", "mkdir", "cp", "mv", "hostname", "uname",
115
+ })
116
+
117
+ # Wrapper tokens to skip before resolving the effective executable.
118
+ _WRAPPER_TOKENS = frozenset({"sudo", "time", "nice", "nohup", "uv", "poetry", "pdm", "hatch", "rye", "npx"})
119
+
120
+ # Version/help style flags: a command that only reports its own identity.
121
+ _IDENTITY_FLAGS = frozenset({"--version", "-v", "-version", "--help", "-h", "-help", "version", "--about"})
122
+
123
+
124
+ def command_is_trivial_evidence(command: str) -> bool:
125
+ """True when ``command`` cannot possibly prove behavior — deny-list, not allowlist.
126
+
127
+ Use at VERIFY time to filter which passing planner/config commands may
128
+ coarse-prove an acceptance criterion. Deliberately permissive: a behavioral
129
+ command without a test keyword (``make check``, ``python scripts/validate.py``,
130
+ ``./run_smoke.sh``) is NOT trivial and keeps its evidential value; only commands
131
+ that provably exercise nothing are rejected:
132
+
133
+ * empty commands;
134
+ * pure printing / system queries (``echo``, ``ls``, ``date``, ...);
135
+ * identity checks (``python --version``, ``node --help``);
136
+ * ``git`` state queries (repo state is not behavior);
137
+ * interpreter one-liners that neither import nor assert nor raise
138
+ (``python -c "print('ok')"``).
139
+ """
140
+ lowered = (command or "").strip().lower()
141
+ if not lowered:
142
+ return True
143
+ if command_has_acceptance_evidence(lowered):
144
+ return False
145
+
146
+ tokens = lowered.split()
147
+ index = 0
148
+ while index < len(tokens) and tokens[index] in _WRAPPER_TOKENS:
149
+ index += 1
150
+ if index < len(tokens) and tokens[index] == "run":
151
+ index += 1
152
+ if index >= len(tokens):
153
+ return True
154
+ executable = tokens[index].rsplit("/", 1)[-1]
155
+
156
+ if executable in _TRIVIAL_EXECUTABLES:
157
+ return True
158
+ # `git status` / `git diff` / `git log` etc.: repository state, not behavior.
159
+ if executable == "git":
160
+ return True
161
+ # Bare identity check: every remaining argument is a version/help flag.
162
+ rest = tokens[index + 1:]
163
+ if rest and all(tok in _IDENTITY_FLAGS for tok in rest):
164
+ return True
165
+ # Interpreter one-liner that neither imports the code, asserts, raises, nor
166
+ # calls anything that could fail meaningfully: `python -c "print('ok')"`.
167
+ if executable.startswith(("python", "node", "ruby", "perl")) and (" -c " in f" {lowered} " or " -e " in f" {lowered} "):
168
+ if not any(marker in lowered for marker in ("import", "require", "assert", "raise", "exit(", "sys.exit")):
169
+ return True
170
+ return False