devcouncil 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/README.md +92 -33
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +56 -40
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +964 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +281 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +87 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +103 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +69 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +53 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +325 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/summary.py +21 -3
  255. package/src/devcouncil/live/tasks.py +12 -3
  256. package/src/devcouncil/live/transcripts.py +69 -2
  257. package/src/devcouncil/llm/cache.py +5 -6
  258. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  259. package/src/devcouncil/llm/provider.py +647 -73
  260. package/src/devcouncil/llm/router.py +271 -46
  261. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  262. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  263. package/src/devcouncil/optimization/skillopt.py +9 -5
  264. package/src/devcouncil/planning/arbiter_service.py +12 -3
  265. package/src/devcouncil/planning/correction_manifest.py +107 -10
  266. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  267. package/src/devcouncil/planning/plan_service.py +5 -2
  268. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  269. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  270. package/src/devcouncil/planning/question_conversion.py +56 -0
  271. package/src/devcouncil/planning/spec_service.py +9 -3
  272. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  273. package/src/devcouncil/repo/gitignore.py +1 -2
  274. package/src/devcouncil/reporting/evidence_export.py +124 -0
  275. package/src/devcouncil/reporting/evidence_html.py +210 -0
  276. package/src/devcouncil/reporting/json_report.py +16 -12
  277. package/src/devcouncil/reporting/markdown_report.py +38 -9
  278. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  279. package/src/devcouncil/reporting/report_builder.py +40 -4
  280. package/src/devcouncil/reporting/task_provenance.py +42 -0
  281. package/src/devcouncil/reporting/verdict.py +75 -0
  282. package/src/devcouncil/skills/library/README.md +1 -0
  283. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  285. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  286. package/src/devcouncil/skills/registry.py +43 -12
  287. package/src/devcouncil/storage/db.py +57 -11
  288. package/src/devcouncil/storage/models.py +6 -0
  289. package/src/devcouncil/storage/native.py +5 -3
  290. package/src/devcouncil/storage/repositories.py +50 -18
  291. package/src/devcouncil/telemetry/context.py +28 -0
  292. package/src/devcouncil/telemetry/cost.py +4 -5
  293. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  294. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  295. package/src/devcouncil/telemetry/stages.py +27 -2
  296. package/src/devcouncil/telemetry/tracker.py +50 -13
  297. package/src/devcouncil/ui/dashboard.py +120 -8
  298. package/src/devcouncil/utils/fsio.py +58 -0
  299. package/src/devcouncil/utils/git_snapshot.py +112 -0
  300. package/src/devcouncil/utils/json_persist.py +53 -0
  301. package/src/devcouncil/utils/proc.py +89 -0
  302. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  303. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  304. package/src/devcouncil/verification/checks/__init__.py +41 -0
  305. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  306. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  307. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  308. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  309. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  310. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  311. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  312. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  313. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  314. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  315. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  316. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  317. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  318. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  319. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  320. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  321. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  322. package/src/devcouncil/verification/checks/wiring.py +216 -0
  323. package/src/devcouncil/verification/claims/__init__.py +23 -0
  324. package/src/devcouncil/verification/claims/checks.py +395 -0
  325. package/src/devcouncil/verification/claims/mapper.py +168 -0
  326. package/src/devcouncil/verification/claims/models.py +39 -0
  327. package/src/devcouncil/verification/claims/transcript.py +92 -0
  328. package/src/devcouncil/verification/claims/verdict.py +88 -0
  329. package/src/devcouncil/verification/command_evidence.py +170 -0
  330. package/src/devcouncil/verification/command_malformation.py +147 -0
  331. package/src/devcouncil/verification/command_runner.py +164 -0
  332. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  333. package/src/devcouncil/verification/diff_coverage.py +151 -0
  334. package/src/devcouncil/verification/difficulty.py +296 -0
  335. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  336. package/src/devcouncil/verification/gap_ids.py +63 -0
  337. package/src/devcouncil/verification/gate_cache.py +194 -0
  338. package/src/devcouncil/verification/gate_selector.py +344 -0
  339. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  340. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  341. package/src/devcouncil/verification/incremental_check.py +241 -0
  342. package/src/devcouncil/verification/next_actions.py +60 -1
  343. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  344. package/src/devcouncil/verification/sandbox.py +38 -11
  345. package/src/devcouncil/verification/stub_detector.py +369 -0
  346. package/src/devcouncil/verification/test_resolver.py +67 -1
  347. package/src/devcouncil/verification/verifier.py +137 -1666
  348. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  349. package/src/devcouncil/verification/verify_setup.py +176 -0
  350. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  351. package/src/semantic_layer/__init__.py +58 -0
  352. package/src/semantic_layer/benchmark.py +75 -0
  353. package/src/semantic_layer/cache.py +290 -0
  354. package/src/semantic_layer/compressor.py +137 -0
  355. package/src/semantic_layer/config.py +75 -0
  356. package/src/semantic_layer/embeddings.py +69 -0
  357. package/src/semantic_layer/llm_backends.py +99 -0
  358. package/src/semantic_layer/pipeline.py +111 -0
  359. package/src/semantic_layer/router.py +128 -0
  360. package/src/semantic_layer/tuner.py +72 -0
  361. package/uv.lock +973 -9
  362. package/src/devcouncil/artifacts/migrations.py +0 -20
  363. package/src/devcouncil/artifacts/schemas.py +0 -23
  364. package/src/devcouncil/artifacts/serializer.py +0 -21
  365. package/src/devcouncil/integrations/gitnexus.py +0 -70
  366. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -0,0 +1,203 @@
1
+ """Policy-gated file writes shared by CLI and MCP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fnmatch
6
+ import os
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+ from devcouncil.execution.hook_policy import HookPolicy
11
+ from devcouncil.execution.lease_validation import require_valid_lease
12
+ from devcouncil.integrations.mcp.util import (
13
+ GIT_APPLY_TIMEOUT_SECONDS,
14
+ diff_target_paths,
15
+ is_git_repo,
16
+ within_root,
17
+ )
18
+ from devcouncil.storage.native import FileChangeRepository, TaskLeaseRepository
19
+ from devcouncil.storage.repositories import TaskRepository
20
+
21
+
22
+ def _explicitly_planned(path: str, task) -> bool: # noqa: ANN001
23
+ normalized = path.replace("\\", "/")
24
+ while normalized.startswith("./"):
25
+ normalized = normalized[2:]
26
+ return any(
27
+ normalized == planned.path.replace("\\", "/")
28
+ or fnmatch.fnmatch(normalized, planned.path.replace("\\", "/"))
29
+ for planned in task.planned_files
30
+ )
31
+
32
+
33
+ def write_file_payload(
34
+ project_root: Path,
35
+ *,
36
+ task_id: str,
37
+ lease_token: str,
38
+ rel_path: str,
39
+ content: str,
40
+ ) -> dict:
41
+ from devcouncil.cli.commands.init import initialize_project
42
+ from devcouncil.storage.db import get_db
43
+
44
+ initialize_project(project_root, quiet=True)
45
+ db = get_db(project_root)
46
+ if not db:
47
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
48
+
49
+ with db.get_session() as session:
50
+ lease_error = require_valid_lease(session, task_id, lease_token)
51
+ if lease_error:
52
+ return lease_error
53
+ lease_record = TaskLeaseRepository(session).active_for_task(task_id)
54
+ assert lease_record is not None
55
+ task = TaskRepository(session).get_by_id(task_id)
56
+ if not task:
57
+ return {"ok": False, "error": f"Task {task_id} not found.", "code": "not_found", "task_id": task_id}
58
+
59
+ target = within_root(project_root, rel_path)
60
+ if target is None:
61
+ reason = "path escapes the project root"
62
+ FileChangeRepository(session).record(
63
+ rel_path, "write", False, task_id=task_id, lease_id=lease_record.id, reason=reason,
64
+ )
65
+ return {
66
+ "ok": False, "task_id": task_id, "applied_files": [],
67
+ "rejected_files": [{"path": rel_path, "reason": reason}],
68
+ }
69
+
70
+ if not _explicitly_planned(rel_path, task):
71
+ reason = f"Task {task_id} does not explicitly authorize changes to {rel_path}."
72
+ FileChangeRepository(session).record(
73
+ rel_path, "write", False, task_id=task_id, lease_id=lease_record.id, reason=reason,
74
+ )
75
+ return {
76
+ "ok": False, "task_id": task_id, "applied_files": [],
77
+ "rejected_files": [{"path": rel_path, "reason": reason}],
78
+ }
79
+
80
+ decision = HookPolicy(project_root=project_root).evaluate_file_write(rel_path, task, content=content)
81
+ if not decision.allowed:
82
+ FileChangeRepository(session).record(
83
+ rel_path, "write", False, task_id=task_id, lease_id=lease_record.id, reason=decision.reason,
84
+ )
85
+ return {
86
+ "ok": False, "task_id": task_id, "applied_files": [],
87
+ "rejected_files": [{"path": rel_path, "reason": decision.reason}],
88
+ }
89
+ try:
90
+ target.parent.mkdir(parents=True, exist_ok=True)
91
+ tmp = target.with_name(target.name + ".devcouncil-tmp")
92
+ tmp.write_text(content, encoding="utf-8")
93
+ os.replace(tmp, target)
94
+ except OSError as exc:
95
+ return {"ok": False, "error": f"Write failed: {exc}", "code": "write_failed", "task_id": task_id}
96
+ FileChangeRepository(session).record(
97
+ rel_path, "write", True, task_id=task_id, lease_id=lease_record.id, reason=decision.reason,
98
+ )
99
+ return {
100
+ "ok": True, "task_id": task_id, "applied_files": [rel_path], "rejected_files": [],
101
+ }
102
+
103
+
104
+ def apply_patch_payload(
105
+ project_root: Path,
106
+ *,
107
+ task_id: str,
108
+ lease_token: str,
109
+ unified_diff: str,
110
+ ) -> dict:
111
+ from devcouncil.cli.commands.init import initialize_project
112
+ from devcouncil.storage.db import get_db
113
+
114
+ initialize_project(project_root, quiet=True)
115
+ db = get_db(project_root)
116
+ if not db:
117
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
118
+
119
+ if not is_git_repo(project_root):
120
+ return {
121
+ "ok": False,
122
+ "error": "apply_patch requires a git repository. Use write_file instead.",
123
+ "code": "not_a_git_repo",
124
+ "task_id": task_id,
125
+ }
126
+ targets = diff_target_paths(unified_diff)
127
+ if not targets:
128
+ return {"ok": False, "error": "No target files found in the diff.", "code": "empty_patch", "task_id": task_id}
129
+
130
+ with db.get_session() as session:
131
+ lease_error = require_valid_lease(session, task_id, lease_token)
132
+ if lease_error:
133
+ return lease_error
134
+ lease_record = TaskLeaseRepository(session).active_for_task(task_id)
135
+ assert lease_record is not None
136
+ task = TaskRepository(session).get_by_id(task_id)
137
+ if not task:
138
+ return {"ok": False, "error": f"Task {task_id} not found.", "code": "not_found", "task_id": task_id}
139
+
140
+ policy = HookPolicy(project_root=project_root)
141
+ rejected: list[dict[str, str]] = []
142
+ for path in targets:
143
+ if within_root(project_root, path) is None:
144
+ rejected.append({"path": path, "reason": "path escapes the project root"})
145
+ continue
146
+ if not _explicitly_planned(path, task):
147
+ rejected.append({
148
+ "path": path,
149
+ "reason": f"Task {task_id} does not explicitly authorize changes to {path}.",
150
+ })
151
+ continue
152
+ decision = policy.evaluate_file_write(path, task)
153
+ if not decision.allowed:
154
+ rejected.append({"path": path, "reason": decision.reason})
155
+ if rejected:
156
+ for item in rejected:
157
+ FileChangeRepository(session).record(
158
+ item["path"], "apply_patch", False, task_id=task_id, lease_id=lease_record.id, reason=item["reason"],
159
+ )
160
+ return {
161
+ "ok": False, "task_id": task_id, "applied_files": [], "rejected_files": rejected,
162
+ }
163
+
164
+ patch_path = project_root / ".devcouncil" / f"mcp-apply-{lease_record.id}.patch"
165
+ patch_path.parent.mkdir(parents=True, exist_ok=True)
166
+ patch_path.write_text(unified_diff, encoding="utf-8")
167
+ try:
168
+ check = subprocess.run(
169
+ ["git", "apply", "--check", "--ignore-whitespace", str(patch_path)],
170
+ cwd=project_root, capture_output=True, text=True,
171
+ timeout=GIT_APPLY_TIMEOUT_SECONDS,
172
+ )
173
+ if check.returncode != 0:
174
+ return {
175
+ "ok": False,
176
+ "error": f"Patch does not apply cleanly: {check.stderr.strip()}",
177
+ "code": "patch_rejected",
178
+ "task_id": task_id,
179
+ }
180
+ applied = subprocess.run(
181
+ ["git", "apply", "--ignore-whitespace", str(patch_path)],
182
+ cwd=project_root, capture_output=True, text=True,
183
+ timeout=GIT_APPLY_TIMEOUT_SECONDS,
184
+ )
185
+ if applied.returncode != 0:
186
+ return {
187
+ "ok": False,
188
+ "error": f"Patch apply failed: {applied.stderr.strip()}",
189
+ "code": "patch_failed",
190
+ "task_id": task_id,
191
+ }
192
+ finally:
193
+ try:
194
+ patch_path.unlink()
195
+ except OSError:
196
+ pass
197
+ for path in targets:
198
+ FileChangeRepository(session).record(
199
+ path, "apply_patch", True, task_id=task_id, lease_id=lease_record.id, reason="policy allowed",
200
+ )
201
+ return {
202
+ "ok": True, "task_id": task_id, "applied_files": targets, "rejected_files": [],
203
+ }
@@ -12,6 +12,7 @@ from devcouncil.execution.checkpoints import CheckpointService
12
12
  from devcouncil.storage.db import get_db
13
13
  from devcouncil.storage.native import AgentHandoffRepository, SemanticDiffRepository, TaskLeaseRepository
14
14
  from devcouncil.storage.repositories import EvidenceRepository, GapRepository, RequirementRepository, TaskRepository
15
+ from devcouncil.utils.json_persist import write_model_json
15
16
  from devcouncil.verification.verifier import Verifier
16
17
 
17
18
 
@@ -78,7 +79,7 @@ class HandoffService:
78
79
  instruction=instruction,
79
80
  created_at=datetime.now(timezone.utc).isoformat(),
80
81
  )
81
- manifest_path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8")
82
+ write_model_json(manifest_path, manifest)
82
83
 
83
84
  status = "manifest_only"
84
85
  with db.get_session() as session:
@@ -1,4 +1,5 @@
1
1
  import fnmatch
2
+ import logging
2
3
  import re
3
4
  from dataclasses import dataclass
4
5
  from pathlib import Path
@@ -13,6 +14,8 @@ from devcouncil.execution.policy_engine import (
13
14
  )
14
15
  from devcouncil.utils.redaction import SECRET_PATTERNS
15
16
 
17
+ logger = logging.getLogger(__name__)
18
+
16
19
  # Splits a shell command into the segments a shell would execute independently, on
17
20
  # the chaining/pipe/sequence operators. We deliberately do NOT try to parse quoting
18
21
  # perfectly — any operator we miss only makes us evaluate a *larger* segment as a
@@ -56,12 +59,23 @@ class HookPolicy:
56
59
  try:
57
60
  from devcouncil.app.config import load_config
58
61
 
59
- execution = load_config(self.project_root).execution
60
- configured = getattr(execution, "global_allowed_commands", None)
62
+ config = load_config(self.project_root)
63
+ # Same derivation as the run command's PermissionPolicy: the project's
64
+ # configured test/lint/typecheck commands are repo-wide allowed.
65
+ allowed = [
66
+ str(item)
67
+ for item in (config.commands.test + config.commands.lint + config.commands.typecheck)
68
+ ]
69
+ # Forward-compat: honor an explicit execution.global_allowed_commands
70
+ # if the config schema ever grows one.
71
+ configured = getattr(config.execution, "global_allowed_commands", None)
61
72
  if isinstance(configured, (list, tuple)):
62
- return [str(item) for item in configured]
63
- except Exception:
64
- pass
73
+ allowed.extend(str(item) for item in configured)
74
+ return allowed
75
+ except Exception as e:
76
+ # Fail-closed is safe, but a silently dropped allowlist makes every
77
+ # configured command deny with no visible cause.
78
+ logger.warning("Failed to load global allowed commands from config, gate uses empty allowlist: %s", e)
65
79
  return []
66
80
 
67
81
  secret_path_patterns = SECRET_PATH_PATTERNS
@@ -0,0 +1,177 @@
1
+ """Task lease operations shared by CLI and MCP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ from devcouncil.execution.prompt_builder import PromptBuilder
9
+ from devcouncil.execution.lease_validation import diagnose_lease, lease_error_payload, LeaseCode
10
+ from devcouncil.integrations.mcp.util import allowed_next_tools, lease_ttl_seconds
11
+ from devcouncil.storage.native import TaskLeaseRepository
12
+ from devcouncil.storage.repositories import GapRepository, RequirementRepository, TaskRepository
13
+ from devcouncil.utils.json_persist import read_json
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def _refresh_stale_map_if_needed(project_root: Path) -> bool:
19
+ """Regenerate ``repo_map.json`` when stale, before prompt/baseline."""
20
+ from devcouncil.indexing.map_refresh import refresh_stale_map_if_needed
21
+
22
+ return refresh_stale_map_if_needed(project_root, on_checkout=True, on_verify=False)
23
+
24
+
25
+ def checkout_task_payload(
26
+ project_root: Path,
27
+ *,
28
+ task_id: str,
29
+ client_id: str,
30
+ agent: str | None = None,
31
+ force: bool = False,
32
+ ) -> dict:
33
+ """Acquire a task lease and return scope for gated write tools."""
34
+ from devcouncil.cli.commands.init import initialize_project
35
+ from devcouncil.storage.db import get_db
36
+
37
+ initialize_project(project_root, quiet=True)
38
+ db = get_db(project_root)
39
+ if not db:
40
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
41
+
42
+ with db.get_session() as session:
43
+ task_repo = TaskRepository(session)
44
+ task = task_repo.get_by_id(task_id)
45
+ if not task:
46
+ return {"ok": False, "error": f"Task {task_id} not found.", "code": "not_found", "task_id": task_id}
47
+ lease_repo = TaskLeaseRepository(session)
48
+ try:
49
+ lease = lease_repo.acquire(
50
+ task_id,
51
+ owner=f"mcp:{client_id}",
52
+ agent=agent,
53
+ client_id=client_id,
54
+ ttl_seconds=lease_ttl_seconds(project_root),
55
+ force=force,
56
+ )
57
+ except ValueError as exc:
58
+ return {"ok": False, "error": str(exc), "code": "lease_conflict", "task_id": task_id}
59
+ # Refresh stale map BEFORE prompt + liveness baseline so both see fresh
60
+ # fingerprints. Write-once baseline semantics are unchanged (snapshot still
61
+ # skips when a complete baseline already exists).
62
+ _refresh_stale_map_if_needed(project_root)
63
+ prompt = PromptBuilder(project_root).build_task_prompt(task, RequirementRepository(session).get_all())
64
+ semantic = None
65
+ semantic_path = project_root / ".devcouncil" / "semantic" / task_id / "before.json"
66
+ if semantic_path.exists():
67
+ semantic = read_json(semantic_path)
68
+ # Snapshot liveness at checkout so verify can ratchet against stranded
69
+ # pre-existing code. Write-once (skip if complete baseline exists).
70
+ # Best-effort; never blocks checkout.
71
+ try:
72
+ from devcouncil.verification.checks.liveness_ratchet import (
73
+ snapshot_liveness_baseline,
74
+ )
75
+
76
+ snapshot_liveness_baseline(project_root, task_id, reset=False)
77
+ except Exception:
78
+ pass
79
+ return {
80
+ "ok": True,
81
+ "lease_token": lease.lease_token,
82
+ "task_id": task.id,
83
+ "status": task.status,
84
+ "expires_at": lease.expires_at,
85
+ "prompt": prompt,
86
+ "planned_files": [f.model_dump() for f in task.planned_files],
87
+ "allowed_commands": task.allowed_commands,
88
+ "expected_tests": task.expected_tests,
89
+ "semantic_context": semantic,
90
+ "allowed_next_tools": allowed_next_tools(
91
+ "running",
92
+ bool(GapRepository(session).get_blocking_for_task(task_id)),
93
+ ),
94
+ }
95
+
96
+
97
+ def release_task_payload(project_root: Path, *, task_id: str, lease_token: str) -> dict:
98
+ from devcouncil.cli.commands.init import initialize_project
99
+ from devcouncil.storage.db import get_db
100
+
101
+ initialize_project(project_root, quiet=True)
102
+ db = get_db(project_root)
103
+ if not db:
104
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
105
+
106
+ with db.get_session() as session:
107
+ released = TaskLeaseRepository(session).release(task_id, lease_token)
108
+ if not released:
109
+ code, message = diagnose_lease(session, task_id, lease_token)
110
+ if code is LeaseCode.VALID:
111
+ return {"ok": False, "error": "Lease already released.", "code": "invalid_lease", "task_id": task_id}
112
+ return lease_error_payload(code, message, task_id)
113
+ try:
114
+ from devcouncil.verification.checks.liveness_ratchet import (
115
+ delete_liveness_baseline,
116
+ )
117
+
118
+ delete_liveness_baseline(project_root, task_id)
119
+ except Exception:
120
+ pass
121
+ return {"ok": True, "task_id": task_id, "released": True}
122
+
123
+
124
+ def renew_lease_payload(
125
+ project_root: Path,
126
+ *,
127
+ task_id: str,
128
+ lease_token: str,
129
+ ttl_seconds: int | None = None,
130
+ ) -> dict:
131
+ from devcouncil.cli.commands.init import initialize_project
132
+ from devcouncil.storage.db import get_db
133
+
134
+ initialize_project(project_root, quiet=True)
135
+ db = get_db(project_root)
136
+ if not db:
137
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
138
+
139
+ ttl = ttl_seconds if ttl_seconds is not None else lease_ttl_seconds(project_root)
140
+ with db.get_session() as session:
141
+ renewed_lease = TaskLeaseRepository(session).renew(task_id, lease_token, ttl)
142
+ if renewed_lease is None:
143
+ code, message = diagnose_lease(session, task_id, lease_token)
144
+ if code is LeaseCode.VALID:
145
+ return {"ok": False, "error": "Lease renew failed.", "code": "invalid_lease", "task_id": task_id}
146
+ return lease_error_payload(code, message, task_id)
147
+ return {
148
+ "ok": True,
149
+ "task_id": task_id,
150
+ "expires_at": renewed_lease.expires_at,
151
+ "ttl_seconds": ttl,
152
+ }
153
+
154
+
155
+ def list_leases_payload(project_root: Path, *, active_only: bool = True) -> dict:
156
+ from devcouncil.cli.commands.init import initialize_project
157
+ from devcouncil.storage.db import get_db
158
+
159
+ initialize_project(project_root, quiet=True)
160
+ db = get_db(project_root)
161
+ if not db:
162
+ return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
163
+
164
+ with db.get_session() as session:
165
+ pairs = TaskLeaseRepository(session).list_leases(active_only=active_only)
166
+ leases = [
167
+ {
168
+ "task_id": lease.task_id,
169
+ "owner": lease.owner,
170
+ "agent": lease.agent,
171
+ "status": lease.status,
172
+ "expires_at": lease.expires_at,
173
+ "expired": expired,
174
+ }
175
+ for lease, expired in pairs
176
+ ]
177
+ return {"ok": True, "leases": leases, "count": len(leases)}
@@ -0,0 +1,71 @@
1
+ """Lease diagnosis for MCP/CLI gates — distinguish expired vs invalid vs conflict."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+ from typing import Any
7
+
8
+ from sqlmodel import Session, col, select
9
+
10
+ from devcouncil.storage.models import TaskLeaseModel
11
+ from devcouncil.storage.native import TaskLeaseRepository
12
+
13
+
14
+ class LeaseCode(str, Enum):
15
+ VALID = "valid"
16
+ LEASE_EXPIRED = "lease_expired"
17
+ INVALID_LEASE = "invalid_lease"
18
+ LEASE_HELD_BY_OTHER = "lease_held_by_other"
19
+
20
+
21
+ def diagnose_lease(session: Session, task_id: str, lease_token: str) -> tuple[LeaseCode, str]:
22
+ """Classify a lease token for actionable agent recovery."""
23
+ repo = TaskLeaseRepository(session)
24
+ active = repo.active_for_task(task_id)
25
+ if active is not None:
26
+ if active.lease_token == lease_token:
27
+ return LeaseCode.VALID, ""
28
+ holder = active.owner or active.client_id or "another agent"
29
+ return (
30
+ LeaseCode.LEASE_HELD_BY_OTHER,
31
+ f"Task {task_id} is held by {holder}. Pick another task or wait for release.",
32
+ )
33
+
34
+ stmt = (
35
+ select(TaskLeaseModel)
36
+ .where(TaskLeaseModel.task_id == task_id)
37
+ .where(TaskLeaseModel.lease_token == lease_token)
38
+ .order_by(col(TaskLeaseModel.created_at).desc())
39
+ )
40
+ prior = session.exec(stmt).first()
41
+ if prior is not None and prior.status in {"stale", "released"}:
42
+ return (
43
+ LeaseCode.LEASE_EXPIRED,
44
+ "Lease TTL expired. Check out the task again with devcouncil_checkout_task.",
45
+ )
46
+ return LeaseCode.INVALID_LEASE, "Invalid lease token."
47
+
48
+
49
+ def lease_error_payload(code: LeaseCode, message: str, task_id: str) -> dict[str, Any]:
50
+ payload: dict[str, Any] = {
51
+ "ok": False,
52
+ "error": message,
53
+ "code": code.value,
54
+ "task_id": task_id,
55
+ }
56
+ if code is LeaseCode.LEASE_EXPIRED:
57
+ payload["suggested_action"] = "checkout_again"
58
+ payload["suggested_tool"] = "devcouncil_checkout_task"
59
+ payload["hint"] = "Renew only works before TTL expiry; call checkout again after expiry."
60
+ elif code is LeaseCode.LEASE_HELD_BY_OTHER:
61
+ payload["suggested_action"] = "pick_other_task"
62
+ payload["suggested_tool"] = "devcouncil_next_task"
63
+ return payload
64
+
65
+
66
+ def require_valid_lease(session: Session, task_id: str, lease_token: str) -> dict[str, Any] | None:
67
+ """Return an error payload when the lease is not valid, else None."""
68
+ code, message = diagnose_lease(session, task_id, lease_token)
69
+ if code is LeaseCode.VALID:
70
+ return None
71
+ return lease_error_payload(code, message, task_id)
@@ -6,6 +6,8 @@ from devcouncil.app.errors import ExecutionError
6
6
 
7
7
  logger = logging.getLogger(__name__)
8
8
 
9
+ _GIT_APPLY_TIMEOUT_SECONDS = 120
10
+
9
11
 
10
12
  class PatchEngine:
11
13
  """Handles applying unified diff patches to the codebase."""
@@ -67,6 +69,7 @@ class PatchEngine:
67
69
  text=True,
68
70
  encoding="utf-8",
69
71
  errors="replace",
72
+ timeout=_GIT_APPLY_TIMEOUT_SECONDS,
70
73
  )
71
74
  if proc.returncode == 0:
72
75
  logger.info("Patch applied (git apply %s)", " ".join(extra_args) or "strict")
@@ -34,6 +34,7 @@ class PermissionManager:
34
34
  lines = ignore_file.read_text().splitlines()
35
35
  return [line.strip() for line in lines if line.strip() and not line.startswith("#")]
36
36
  except Exception:
37
+ logger.debug("Failed to read .devcouncilignore", exc_info=True)
37
38
  pass
38
39
  return []
39
40