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,272 @@
1
+ """Git diff fallbacks when no batched snapshot is available.
2
+
3
+ Extracted from ``verifier.py`` so the verifier orchestrator stays focused on gate
4
+ logic while pre-head / untracked / walk-the-tree paths live in one place.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import fnmatch
10
+ import logging
11
+ import subprocess
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING, List, Optional
15
+
16
+ from devcouncil.domain.checkpoint_refs import task_before_ref
17
+ from devcouncil.utils.proc import GIT_TIMEOUT
18
+
19
+ if TYPE_CHECKING:
20
+ from devcouncil.utils.git_snapshot import GitWorktreeSnapshot
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ IGNORED_CHANGE_PATTERNS = (
25
+ "__pycache__/*",
26
+ "*/__pycache__/*",
27
+ "*.pyc",
28
+ "*.pyo",
29
+ ".pytest_cache/*",
30
+ ".mypy_cache/*",
31
+ ".ruff_cache/*",
32
+ ".devcouncil/*",
33
+ "AGENTS.md",
34
+ "CLAUDE.md",
35
+ ".gitignore",
36
+ )
37
+
38
+ MAX_UNTRACKED_DIFF_BYTES = 256_000
39
+
40
+
41
+ @dataclass
42
+ class GitDiffFallback:
43
+ """Pre-snapshot git diff helpers used by :class:`~devcouncil.verification.verifier.Verifier`."""
44
+
45
+ project_root: Path
46
+ git_snapshot: Optional["GitWorktreeSnapshot"] = None
47
+ untracked_cache: Optional[List[str]] = field(default=None, repr=False)
48
+ # When set, diff/changed-files use ``git diff <ref>`` (PR/base scope) instead of HEAD/worktree.
49
+ diff_base: Optional[str] = None
50
+
51
+ def has_head(self) -> bool:
52
+ if self.git_snapshot is not None:
53
+ return self.git_snapshot.has_head
54
+ try:
55
+ return subprocess.run(
56
+ ["git", "rev-parse", "--verify", "HEAD"],
57
+ cwd=self.project_root,
58
+ stdout=subprocess.DEVNULL,
59
+ stderr=subprocess.DEVNULL,
60
+ timeout=GIT_TIMEOUT,
61
+ ).returncode == 0
62
+ except (subprocess.TimeoutExpired, OSError) as exc:
63
+ logger.warning("git rev-parse HEAD failed: %s", exc)
64
+ return False
65
+
66
+ def get_initial_repo_diff(self) -> str:
67
+ parts: List[str] = []
68
+ for cmd in (["git", "diff", "--cached"], ["git", "diff"]):
69
+ result = subprocess.run(
70
+ cmd,
71
+ cwd=self.project_root,
72
+ capture_output=True,
73
+ text=True,
74
+ encoding="utf-8",
75
+ errors="replace",
76
+ timeout=GIT_TIMEOUT,
77
+ )
78
+ if result.returncode == 0 and result.stdout:
79
+ parts.append(result.stdout)
80
+ untracked_diff = self.get_untracked_files_diff()
81
+ if untracked_diff:
82
+ parts.append(untracked_diff)
83
+ return "\n".join(parts)
84
+
85
+ def get_status_files(self) -> List[str]:
86
+ files: set[str] = set()
87
+ commands = (
88
+ ["git", "diff", "--cached", "--name-only"],
89
+ ["git", "diff", "--name-only"],
90
+ ["git", "ls-files", "--others", "--exclude-standard"],
91
+ )
92
+ for cmd in commands:
93
+ try:
94
+ output = subprocess.check_output(
95
+ cmd,
96
+ cwd=self.project_root,
97
+ stderr=subprocess.DEVNULL,
98
+ timeout=GIT_TIMEOUT,
99
+ ).decode("utf-8", errors="replace").splitlines()
100
+ files.update(path.replace("\\", "/") for path in output if path.strip())
101
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
102
+ continue
103
+ if not files:
104
+ files.update(self.walk_project_files())
105
+ return self.filter_change_paths(sorted(files))
106
+
107
+ def get_untracked_files(self) -> List[str]:
108
+ if self.git_snapshot is not None:
109
+ return self.filter_change_paths(self.git_snapshot.untracked_files)
110
+ if self.untracked_cache is not None:
111
+ return self.untracked_cache
112
+ try:
113
+ output = subprocess.check_output(
114
+ ["git", "ls-files", "--others", "--exclude-standard"],
115
+ cwd=self.project_root,
116
+ stderr=subprocess.DEVNULL,
117
+ timeout=GIT_TIMEOUT,
118
+ ).decode("utf-8", errors="replace").splitlines()
119
+ return self.filter_change_paths(output)
120
+ except Exception as exc:
121
+ logger.debug("Failed to list untracked files: %s", exc)
122
+ return []
123
+
124
+ def get_untracked_files_diff(self) -> str:
125
+ parts: List[str] = []
126
+ for rel_path in self.get_untracked_files():
127
+ full_path = self.project_root / rel_path
128
+ if not full_path.is_file():
129
+ continue
130
+ parts.append(self.format_new_file_diff(rel_path, full_path))
131
+ return "\n".join(part for part in parts if part)
132
+
133
+ def format_new_file_diff(self, rel_path: str, full_path: Path) -> str:
134
+ try:
135
+ raw = full_path.read_bytes()
136
+ except Exception as exc:
137
+ logger.debug("Failed to read untracked file %s: %s", rel_path, exc)
138
+ return ""
139
+
140
+ header = [
141
+ f"diff --git a/{rel_path} b/{rel_path}",
142
+ "new file mode 100644",
143
+ "--- /dev/null",
144
+ f"+++ b/{rel_path}",
145
+ ]
146
+ if b"\0" in raw[:8192]:
147
+ return "\n".join([*header, f"Binary files /dev/null and b/{rel_path} differ"])
148
+
149
+ truncated = len(raw) > MAX_UNTRACKED_DIFF_BYTES
150
+ if truncated:
151
+ raw = raw[:MAX_UNTRACKED_DIFF_BYTES]
152
+ text = raw.decode("utf-8", errors="replace")
153
+ lines = text.splitlines()
154
+ if text.endswith(("\n", "\r")):
155
+ line_count = len(lines)
156
+ else:
157
+ line_count = max(len(lines), 1 if text else 0)
158
+
159
+ diff_lines = [*header, f"@@ -0,0 +1,{line_count} @@"]
160
+ if not text:
161
+ return "\n".join(header) + "\n"
162
+
163
+ diff_lines.extend(f"+{line}" for line in lines)
164
+ if truncated:
165
+ diff_lines.append("+[devcouncil: untracked file diff truncated]")
166
+ return "\n".join(diff_lines)
167
+
168
+ def walk_project_files(self) -> List[str]:
169
+ files: List[str] = []
170
+ for path in self.project_root.rglob("*"):
171
+ if not path.is_file():
172
+ continue
173
+ rel = path.relative_to(self.project_root).as_posix()
174
+ if rel.startswith(".git/"):
175
+ continue
176
+ files.append(rel)
177
+ return files
178
+
179
+ def filter_change_paths(self, paths: List[str]) -> List[str]:
180
+ return [
181
+ path
182
+ for path in (p.strip().replace("\\", "/") for p in paths)
183
+ if path and not self.is_ignored_change(path)
184
+ ]
185
+
186
+ def is_ignored_change(self, path: str) -> bool:
187
+ return any(fnmatch.fnmatch(path, pattern) for pattern in IGNORED_CHANGE_PATTERNS)
188
+
189
+ def get_diff(self) -> str:
190
+ if self.diff_base:
191
+ try:
192
+ return subprocess.check_output(
193
+ ["git", "diff", self.diff_base],
194
+ cwd=self.project_root,
195
+ timeout=GIT_TIMEOUT,
196
+ ).decode("utf-8", errors="replace")
197
+ except Exception as e:
198
+ logger.warning("Failed to get git diff against %s: %s", self.diff_base, e)
199
+ return ""
200
+ snap = self.git_snapshot
201
+ try:
202
+ if snap is not None:
203
+ if not snap.has_head:
204
+ parts = [snap.cached_diff, snap.working_diff]
205
+ untracked_diff = self.get_untracked_files_diff()
206
+ return "\n".join(part for part in [*parts, untracked_diff] if part)
207
+ untracked_diff = self.get_untracked_files_diff()
208
+ return "\n".join(part for part in [snap.tracked_diff, untracked_diff] if part)
209
+ if not self.has_head():
210
+ return self.get_initial_repo_diff()
211
+ tracked_diff = subprocess.check_output(
212
+ ["git", "diff", "HEAD"], cwd=self.project_root, timeout=GIT_TIMEOUT
213
+ ).decode("utf-8", errors="replace")
214
+ untracked_diff = self.get_untracked_files_diff()
215
+ return "\n".join(part for part in [tracked_diff, untracked_diff] if part)
216
+ except Exception as e:
217
+ logger.warning("Failed to get git diff: %s", e)
218
+ return ""
219
+
220
+ def get_changed_files(self) -> List[str]:
221
+ if self.diff_base:
222
+ try:
223
+ output = subprocess.check_output(
224
+ ["git", "diff", self.diff_base, "--name-only"],
225
+ cwd=self.project_root,
226
+ timeout=GIT_TIMEOUT,
227
+ ).decode("utf-8", errors="replace").splitlines()
228
+ return self.filter_change_paths(output)
229
+ except Exception as e:
230
+ logger.warning("Failed to get changed files against %s: %s", self.diff_base, e)
231
+ return []
232
+ snap = self.git_snapshot
233
+ try:
234
+ if snap is not None:
235
+ if not snap.has_head:
236
+ return self.filter_change_paths(snap.status_paths or snap.untracked_files)
237
+ files = set(snap.tracked_changed_files)
238
+ files.update(snap.untracked_files)
239
+ return self.filter_change_paths(sorted(files))
240
+ if not self.has_head():
241
+ return self.get_status_files()
242
+ output = subprocess.check_output(
243
+ ["git", "diff", "HEAD", "--name-only"], cwd=self.project_root, timeout=GIT_TIMEOUT
244
+ ).decode("utf-8", errors="replace").splitlines()
245
+ files = set(output)
246
+ files.update(self.get_untracked_files())
247
+ return self.filter_change_paths(sorted(files))
248
+ except Exception as e:
249
+ logger.warning("Failed to get changed files: %s", e)
250
+ return []
251
+
252
+ def committed_task_diff(self, task_id: str) -> str:
253
+ """Diff of work committed since the task's ``before`` checkpoint, or ""."""
254
+ before_ref = task_before_ref(task_id)
255
+ try:
256
+ has_ref = subprocess.run(
257
+ ["git", "rev-parse", "--verify", before_ref],
258
+ cwd=self.project_root,
259
+ stdout=subprocess.DEVNULL,
260
+ stderr=subprocess.DEVNULL,
261
+ timeout=GIT_TIMEOUT,
262
+ ).returncode == 0
263
+ if has_ref:
264
+ return subprocess.check_output(
265
+ ["git", "diff", before_ref],
266
+ cwd=self.project_root,
267
+ stderr=subprocess.DEVNULL,
268
+ timeout=GIT_TIMEOUT,
269
+ ).decode("utf-8", errors="replace")
270
+ except Exception as e:
271
+ logger.debug("Failed to read committed task diff for %s: %s", task_id, e)
272
+ return ""
@@ -47,6 +47,19 @@ Your task is to identify if the implementation is complete, correct, and follows
47
47
  - Identify architectural drift.
48
48
  - Identify security risks not caught by static scans.
49
49
 
50
+ Review rules — follow them exactly:
51
+ - The diff above IS the complete, authoritative record of the change. Review the CODE in it.
52
+ Do not ask for proof that the change was made, and do not report "no evidence of
53
+ implementation" — you are looking at the implementation.
54
+ - Every finding must cite the specific file/hunk in the diff that demonstrates the problem
55
+ (in its 'evidence'). A concern you cannot tie to a concrete line in the diff is not a finding.
56
+ - Do not review prose, commit-message style, response formatting, or your estimate of effort.
57
+ - Severity rubric: 'critical' is reserved for a demonstrable correctness or security defect
58
+ visible in the diff (wrong behavior, data loss, injection, broken requirement). Missing
59
+ edge cases are 'high' at most; style and structure are 'low'.
60
+ - If the diff implements the requirements correctly, return is_satisfactory=true and an
61
+ empty findings list. An empty review of correct code is a correct review.
62
+
50
63
  Return a JSON object with 'is_satisfactory' and a list of 'findings' (as Gap objects).
51
64
  """
52
65
  messages = [{"role": "user", "content": prompt}]
@@ -0,0 +1,241 @@
1
+ """Incremental gate runner: the sub-second sidecar behind ``dev check --watch``.
2
+
3
+ Ties :mod:`devcouncil.verification.gate_selector` (path → relevant/narrowed gates) to
4
+ :mod:`devcouncil.verification.gate_cache` (content-hash skip of previously-green gates).
5
+ On an iterative edit only the gates whose inputs actually changed are executed; the rest
6
+ are served from cache. This is deliberately separate from the full ``verify_task`` path,
7
+ which release / ``dev go`` still run in full and which never consults this cache.
8
+
9
+ Given the working-tree changed files and the project's configured commands, it:
10
+ 1. selects the gates relevant to the changed stacks (and narrows linters to the touched
11
+ files),
12
+ 2. skips any gate that passed last time with byte-identical inputs,
13
+ 3. runs the remainder and records their pass/fail in the cache,
14
+ 4. returns a compact, render-friendly result.
15
+
16
+ Best-effort and side-effect-light: it shells out only to run the gates themselves and
17
+ persists a single small JSON cache file.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ import time
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+ from typing import Callable, List, Mapping, Optional, Sequence
27
+
28
+ from devcouncil.utils.json_persist import read_json
29
+ from devcouncil.verification.gate_cache import GateResultCache
30
+ from devcouncil.verification.gate_selector import GateSpec, select_gates
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ @dataclass
36
+ class GateOutcome:
37
+ name: str
38
+ kind: str
39
+ command: str
40
+ passed: bool
41
+ cached: bool
42
+ summary: str = ""
43
+ duration_s: float = 0.0
44
+ narrowed: bool = False
45
+
46
+
47
+ @dataclass
48
+ class IncrementalResult:
49
+ changed_files: List[str] = field(default_factory=list)
50
+ outcomes: List[GateOutcome] = field(default_factory=list)
51
+ skipped: List[tuple[str, str]] = field(default_factory=list)
52
+ duration_s: float = 0.0
53
+ no_changes: bool = False
54
+ no_gates: bool = False
55
+ narrowed: bool = False
56
+
57
+ @property
58
+ def ran(self) -> List[GateOutcome]:
59
+ return [o for o in self.outcomes if not o.cached]
60
+
61
+ @property
62
+ def cached(self) -> List[GateOutcome]:
63
+ return [o for o in self.outcomes if o.cached]
64
+
65
+ @property
66
+ def passed(self) -> bool:
67
+ return all(o.passed for o in self.outcomes)
68
+
69
+ def to_dict(self) -> dict:
70
+ return {
71
+ "passed": self.passed,
72
+ "no_changes": self.no_changes,
73
+ "no_gates": self.no_gates,
74
+ "narrowed": self.narrowed,
75
+ "changed_files": self.changed_files,
76
+ "duration_s": round(self.duration_s, 4),
77
+ "gates": [
78
+ {
79
+ "name": o.name,
80
+ "kind": o.kind,
81
+ "command": o.command,
82
+ "passed": o.passed,
83
+ "cached": o.cached,
84
+ "narrowed": o.narrowed,
85
+ "duration_s": round(o.duration_s, 4),
86
+ }
87
+ for o in self.outcomes
88
+ ],
89
+ "ran": len(self.ran),
90
+ "cached_hits": len(self.cached),
91
+ "skipped": [{"command": c, "reason": r} for c, r in self.skipped],
92
+ }
93
+
94
+
95
+ def _default_commands(project_root: Path) -> dict[str, list[str]]:
96
+ """Configured test/lint/typecheck commands, or ``{}`` on any config failure."""
97
+ try:
98
+ from devcouncil.app.config import load_config
99
+
100
+ cfg = load_config(project_root)
101
+ return {
102
+ "test": list(cfg.commands.test),
103
+ "lint": list(cfg.commands.lint),
104
+ "typecheck": list(cfg.commands.typecheck),
105
+ }
106
+ except Exception as exc:
107
+ logger.debug("incremental gates: config commands unavailable: %s", exc)
108
+ return {}
109
+
110
+
111
+ def _load_repo_map(project_root: Path) -> dict | None:
112
+ """Best-effort load of ``.devcouncil/repo_map.json`` for dependent expansion."""
113
+ map_path = project_root / ".devcouncil" / "repo_map.json"
114
+ if not map_path.is_file():
115
+ return None
116
+ try:
117
+ data = read_json(map_path)
118
+ return data if isinstance(data, dict) else None
119
+ except Exception as exc:
120
+ logger.debug("incremental gates: repo map unavailable: %s", exc)
121
+ return None
122
+
123
+
124
+ def _default_changed_files(project_root: Path) -> list[str]:
125
+ from devcouncil.verification.verifier import Verifier
126
+
127
+ return Verifier(project_root).get_changed_files()
128
+
129
+
130
+ def _default_runner(project_root: Path, timeout: int) -> Callable[[str], tuple[bool, str]]:
131
+ from devcouncil.verification.command_runner import run_verification_command
132
+
133
+ def _run(command: str) -> tuple[bool, str]:
134
+ result = run_verification_command(
135
+ project_root, command, task_id="check-watch", timeout=timeout
136
+ )
137
+ return result.exit_code == 0, result.summary
138
+
139
+ return _run
140
+
141
+
142
+ def run_incremental_gates(
143
+ project_root: Path,
144
+ *,
145
+ commands: Optional[Mapping[str, Sequence[str]]] = None,
146
+ extra_test_commands: Optional[Sequence[str]] = None,
147
+ changed_files: Optional[Sequence[str]] = None,
148
+ cache: Optional[GateResultCache] = None,
149
+ use_cache: bool = True,
150
+ narrow: bool = True,
151
+ runner: Optional[Callable[[str], tuple[bool, str]]] = None,
152
+ timeout: int = 300,
153
+ ) -> IncrementalResult:
154
+ """Run only the gates affected by ``changed_files``, skipping cached-green ones.
155
+
156
+ ``commands`` defaults to the project's configured test/lint/typecheck commands;
157
+ ``extra_test_commands`` (e.g. ``dev check --test``) are appended to the test kind.
158
+ ``runner`` is a ``command -> (passed, summary)`` callable (defaults to the real
159
+ verification command runner) — the seam that keeps this unit-testable without
160
+ subprocesses.
161
+ """
162
+ started = time.perf_counter()
163
+ root = Path(project_root)
164
+
165
+ if changed_files is None:
166
+ changed_files = _default_changed_files(root)
167
+ changed = [p for p in changed_files if p and p.strip()]
168
+
169
+ resolved_commands: dict[str, list[str]] = {
170
+ k: list(v) for k, v in (commands or _default_commands(root)).items()
171
+ }
172
+ if extra_test_commands:
173
+ resolved_commands.setdefault("test", [])
174
+ resolved_commands["test"] = list(resolved_commands["test"]) + [
175
+ c for c in extra_test_commands if c and c.strip()
176
+ ]
177
+
178
+ if not changed:
179
+ return IncrementalResult(no_changes=True, duration_s=time.perf_counter() - started)
180
+
181
+ repo_map = _load_repo_map(root)
182
+ selection = select_gates(changed, resolved_commands, narrow=narrow, repo_map=repo_map)
183
+ any_narrowed = any(g.narrowed for g in selection.gates)
184
+ if not selection.gates:
185
+ return IncrementalResult(
186
+ changed_files=changed,
187
+ skipped=selection.skipped,
188
+ no_gates=True,
189
+ narrowed=any_narrowed,
190
+ duration_s=time.perf_counter() - started,
191
+ )
192
+
193
+ if cache is None:
194
+ cache = GateResultCache(root)
195
+ cache.load()
196
+ run = runner or _default_runner(root, timeout)
197
+
198
+ outcomes: List[GateOutcome] = []
199
+ for gate in selection.gates:
200
+ if use_cache and cache.is_green(gate):
201
+ outcomes.append(GateOutcome(
202
+ name=gate.name, kind=gate.kind, command=gate.command,
203
+ passed=True, cached=True,
204
+ summary=cache.cached_summary(gate) or "", narrowed=gate.narrowed,
205
+ ))
206
+ continue
207
+ gate_started = time.perf_counter()
208
+ try:
209
+ passed, summary = run(gate.command)
210
+ except Exception as exc: # a gate crash must not kill the sidecar loop
211
+ logger.debug("incremental gate %s crashed: %s", gate.name, exc)
212
+ passed, summary = False, f"gate crashed: {exc}"
213
+ gate_dt = time.perf_counter() - gate_started
214
+ cache.record(gate, passed=passed, summary=summary)
215
+ outcomes.append(GateOutcome(
216
+ name=gate.name, kind=gate.kind, command=gate.command,
217
+ passed=passed, cached=False, summary=summary,
218
+ duration_s=gate_dt, narrowed=gate.narrowed,
219
+ ))
220
+
221
+ cache.save()
222
+ return IncrementalResult(
223
+ changed_files=changed,
224
+ outcomes=outcomes,
225
+ skipped=selection.skipped,
226
+ narrowed=any_narrowed,
227
+ duration_s=time.perf_counter() - started,
228
+ )
229
+
230
+
231
+ def selected_gate_specs(
232
+ project_root: Path,
233
+ changed_files: Sequence[str],
234
+ *,
235
+ commands: Optional[Mapping[str, Sequence[str]]] = None,
236
+ narrow: bool = True,
237
+ ) -> list[GateSpec]:
238
+ """Convenience: the gates that *would* run for these changed files (no execution)."""
239
+ resolved = commands or _default_commands(Path(project_root))
240
+ repo_map = _load_repo_map(Path(project_root))
241
+ return select_gates(changed_files, resolved, narrow=narrow, repo_map=repo_map).gates
@@ -40,6 +40,14 @@ _CATEGORY_BY_GAP_TYPE = {
40
40
  "migration_gap": "fix_code",
41
41
  "requirement_not_planned": "plan",
42
42
  "task_not_implemented": "plan",
43
+ "stub_detected": "fix_code",
44
+ "stub_declared": "review",
45
+ "suspicious_effort": "review",
46
+ "coarse_acceptance_proof": "add_test",
47
+ "unwired_file": "fix_code",
48
+ "dead_symbol": "fix_code",
49
+ "stranded_code": "fix_code",
50
+ "stale_map": "refresh_map",
43
51
  }
44
52
 
45
53
 
@@ -85,7 +93,10 @@ def _derive_file(gap: Gap) -> Optional[str]:
85
93
  def _action_text(gap: Gap, file: Optional[str]) -> str:
86
94
  target = file or "the affected file"
87
95
  if gap.gap_type == "orphan_diff":
88
- return f"Revert changes to {target} or add it to the task's planned files."
96
+ return (
97
+ f"Revert changes to {target} or append it with "
98
+ f"`dev scope update <task_id> --lease-token <token> --planned-file {target}`."
99
+ )
89
100
  if gap.gap_type == "planned_file_not_changed":
90
101
  return f"Modify {target} as planned, or remove it from the task's planned files."
91
102
  if gap.gap_type == "dependency_risk":
@@ -104,6 +115,54 @@ def _action_text(gap: Gap, file: Optional[str]) -> str:
104
115
  return "Remove the detected secret/finding from the diff and rotate any exposed credential."
105
116
  if gap.gap_type == "architecture_drift":
106
117
  return "Address the flagged change or resolve the open critique card, then re-verify."
118
+ if gap.gap_type == "stub_detected":
119
+ loc = f"{target}{':' + str(gap.line) if gap.line else ''}"
120
+ return (
121
+ f"Replace the stub/placeholder at {loc} with a real implementation, then re-verify. "
122
+ "Do not mark work complete while placeholders remain."
123
+ )
124
+ if gap.gap_type == "stub_declared":
125
+ loc = f"{target}{':' + str(gap.line) if gap.line else ''}"
126
+ return (
127
+ f"Review the intentional stub declared at {loc}; replace it before marking done."
128
+ )
129
+ if gap.gap_type == "suspicious_effort":
130
+ return (
131
+ "The diff looks too small or superficial for the planned scope. Complete the "
132
+ "planned work (or restore removed tests), then re-verify."
133
+ )
134
+ if gap.gap_type == "coarse_acceptance_proof":
135
+ return (
136
+ "Acceptance criteria were proven only by a coarse passing command, not a "
137
+ "per-criterion check. Add a verification command or test that exercises each "
138
+ "listed criterion specifically, then re-verify."
139
+ )
140
+ if gap.gap_type == "unwired_file":
141
+ return (
142
+ f"Import or register `{target}` from its intended non-test caller "
143
+ f"(append the caller with `dev scope update <task_id> --lease-token <token> "
144
+ f"--planned-file <caller>` if needed), or delete the unused file."
145
+ )
146
+ if gap.gap_type == "dead_symbol":
147
+ loc = f"{target}{':' + str(gap.line) if gap.line else ''}"
148
+ name = ""
149
+ for item in gap.evidence:
150
+ if isinstance(item, str) and item.startswith("symbol:"):
151
+ name = item.split(":", 1)[1].strip()
152
+ break
153
+ symbol = name or "the new symbol"
154
+ return (
155
+ f"Call or register `{symbol}` at {loc} from the code that needs it "
156
+ f"(use `dev scope update ... --planned-file <caller>` if the caller is "
157
+ f"out of scope), or remove it."
158
+ )
159
+ if gap.gap_type == "stranded_code":
160
+ return (
161
+ f"Restore the import/call that kept `{target}` live, or delete the "
162
+ "stranded module if it is intentionally unused."
163
+ )
164
+ if gap.gap_type == "stale_map":
165
+ return "Run `dev map` to regenerate the repository map, then re-verify."
107
166
  # Fall back to the gap's own recommended fix for anything unmapped.
108
167
  return gap.recommended_fix
109
168