code-context-control 2.37.0__tar.gz → 2.39.0__tar.gz

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 (221) hide show
  1. {code_context_control-2.37.0/code_context_control.egg-info → code_context_control-2.39.0}/PKG-INFO +5 -1
  2. {code_context_control-2.37.0 → code_context_control-2.39.0}/README.md +4 -0
  3. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/_hook_utils.py +39 -2
  4. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/c3.py +60 -35
  5. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/guide/oracle.html +1 -0
  6. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/hook_edit_ledger.py +9 -3
  7. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/hook_edit_unlock.py +9 -1
  8. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/hook_pretool_enforce.py +23 -7
  9. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/mcp_server.py +23 -8
  10. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/delegate.py +26 -20
  11. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/edit.py +65 -18
  12. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/memory.py +4 -0
  13. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/read.py +27 -5
  14. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/validate.py +43 -15
  15. {code_context_control-2.37.0 → code_context_control-2.39.0/code_context_control.egg-info}/PKG-INFO +5 -1
  16. {code_context_control-2.37.0 → code_context_control-2.39.0}/code_context_control.egg-info/SOURCES.txt +6 -0
  17. {code_context_control-2.37.0 → code_context_control-2.39.0}/core/mcp_toml.py +29 -2
  18. {code_context_control-2.37.0 → code_context_control-2.39.0}/core/web_security.py +6 -0
  19. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/mcp_oracle.py +18 -3
  20. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/oracle.html +108 -0
  21. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/oracle_server.py +90 -15
  22. code_context_control-2.39.0/oracle/services/activity_reporter.py +281 -0
  23. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/c3_bridge.py +29 -2
  24. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/chat_engine.py +24 -0
  25. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/tool_registry.py +22 -0
  26. {code_context_control-2.37.0 → code_context_control-2.39.0}/pyproject.toml +1 -1
  27. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/claude_md.py +26 -11
  28. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/compressor.py +5 -1
  29. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/context_snapshot.py +39 -9
  30. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/conversation_store.py +99 -48
  31. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/edit_ledger.py +58 -27
  32. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/embedding_index.py +26 -2
  33. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/file_memory.py +77 -6
  34. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/parser.py +32 -6
  35. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/vector_store.py +28 -2
  36. code_context_control-2.39.0/tests/test_activity_reporter.py +145 -0
  37. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_claude_md_merge.py +39 -0
  38. code_context_control-2.39.0/tests/test_edit_ledger_hook.py +88 -0
  39. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_edit_normalization.py +121 -0
  40. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_enforcement_flip.py +67 -2
  41. code_context_control-2.39.0/tests/test_lazy_store_init.py +77 -0
  42. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_mcp_toml.py +39 -0
  43. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_oracle_discovery_api.py +11 -0
  44. code_context_control-2.39.0/tests/test_oracle_security_fixes.py +159 -0
  45. code_context_control-2.39.0/tests/test_service_durability.py +186 -0
  46. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_tool_registry.py +9 -0
  47. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_web_security.py +31 -0
  48. {code_context_control-2.37.0 → code_context_control-2.39.0}/LICENSE +0 -0
  49. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/__init__.py +0 -0
  50. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/commands/__init__.py +0 -0
  51. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/commands/common.py +0 -0
  52. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/commands/parser.py +0 -0
  53. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/docs.html +0 -0
  54. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/edits.html +0 -0
  55. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/guide/bitbucket.html +0 -0
  56. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/guide/getting-started.html +0 -0
  57. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/guide/index.html +0 -0
  58. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/guide/shared.css +0 -0
  59. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/guide/tools.html +0 -0
  60. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/guide/workflow.html +0 -0
  61. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/hook_auto_snapshot.py +0 -0
  62. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/hook_c3_signal.py +0 -0
  63. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/hook_c3read.py +0 -0
  64. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/hook_filter.py +0 -0
  65. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/hook_ghost_files.py +0 -0
  66. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/hook_read.py +0 -0
  67. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/hook_session_stats.py +0 -0
  68. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/hook_terse_advisor.py +0 -0
  69. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/hub.html +0 -0
  70. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/hub_server.py +0 -0
  71. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/mcp_proxy.py +0 -0
  72. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/server.py +0 -0
  73. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/__init__.py +0 -0
  74. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/_helpers.py +0 -0
  75. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/agent.py +0 -0
  76. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/bitbucket.py +0 -0
  77. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/compress.py +0 -0
  78. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/edits.py +0 -0
  79. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/filter.py +0 -0
  80. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/impact.py +0 -0
  81. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/project.py +0 -0
  82. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/search.py +0 -0
  83. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/session.py +0 -0
  84. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/shell.py +0 -0
  85. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/tools/status.py +0 -0
  86. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui/api.js +0 -0
  87. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui/app.js +0 -0
  88. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui/components/bitbucket.js +0 -0
  89. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui/components/chat.js +0 -0
  90. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui/components/dashboard.js +0 -0
  91. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui/components/edits.js +0 -0
  92. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui/components/instructions.js +0 -0
  93. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui/components/memory.js +0 -0
  94. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui/components/sessions.js +0 -0
  95. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui/components/settings.js +0 -0
  96. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui/components/sidebar.js +0 -0
  97. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui/icons.js +0 -0
  98. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui/shared.js +0 -0
  99. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui/theme.js +0 -0
  100. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui.html +0 -0
  101. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui_legacy.html +0 -0
  102. {code_context_control-2.37.0 → code_context_control-2.39.0}/cli/ui_nano.html +0 -0
  103. {code_context_control-2.37.0 → code_context_control-2.39.0}/code_context_control.egg-info/dependency_links.txt +0 -0
  104. {code_context_control-2.37.0 → code_context_control-2.39.0}/code_context_control.egg-info/entry_points.txt +0 -0
  105. {code_context_control-2.37.0 → code_context_control-2.39.0}/code_context_control.egg-info/requires.txt +0 -0
  106. {code_context_control-2.37.0 → code_context_control-2.39.0}/code_context_control.egg-info/top_level.txt +0 -0
  107. {code_context_control-2.37.0 → code_context_control-2.39.0}/core/__init__.py +0 -0
  108. {code_context_control-2.37.0 → code_context_control-2.39.0}/core/config.py +0 -0
  109. {code_context_control-2.37.0 → code_context_control-2.39.0}/core/ide.py +0 -0
  110. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/__init__.py +0 -0
  111. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/config.py +0 -0
  112. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/__init__.py +0 -0
  113. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/api_auth.py +0 -0
  114. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/chat_store.py +0 -0
  115. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/cross_memory.py +0 -0
  116. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/federated_graph.py +0 -0
  117. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/health_checker.py +0 -0
  118. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/insight_engine.py +0 -0
  119. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/memory_reader.py +0 -0
  120. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/memory_writer.py +0 -0
  121. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/ollama_bridge.py +0 -0
  122. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/project_scanner.py +0 -0
  123. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/review_agent.py +0 -0
  124. {code_context_control-2.37.0 → code_context_control-2.39.0}/oracle/services/tool_executor.py +0 -0
  125. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/__init__.py +0 -0
  126. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/activity_log.py +0 -0
  127. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/agent_base.py +0 -0
  128. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/agents.py +0 -0
  129. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/auto_memory.py +0 -0
  130. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/bench/__init__.py +0 -0
  131. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/bench/external/__init__.py +0 -0
  132. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/bench/external/aider_polyglot.py +0 -0
  133. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/bench/external/swe_bench.py +0 -0
  134. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/benchmark_dashboard.py +0 -0
  135. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/bitbucket_client.py +0 -0
  136. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/bitbucket_credentials.py +0 -0
  137. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/doc_index.py +0 -0
  138. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/e2e_benchmark.py +0 -0
  139. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/e2e_evaluator.py +0 -0
  140. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/e2e_tasks.py +0 -0
  141. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/error_reporting.py +0 -0
  142. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/git_context.py +0 -0
  143. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/hub_service.py +0 -0
  144. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/indexer.py +0 -0
  145. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/memory.py +0 -0
  146. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/memory_consolidator.py +0 -0
  147. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/memory_graph.py +0 -0
  148. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/memory_grounder.py +0 -0
  149. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/memory_scorer.py +0 -0
  150. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/metrics.py +0 -0
  151. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/notifications.py +0 -0
  152. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/ollama_client.py +0 -0
  153. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/output_filter.py +0 -0
  154. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/project_manager.py +0 -0
  155. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/project_runtime.py +0 -0
  156. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/protocol.py +0 -0
  157. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/proxy_state.py +0 -0
  158. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/retrieval_broker.py +0 -0
  159. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/router.py +0 -0
  160. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/runtime.py +0 -0
  161. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/session_benchmark.py +0 -0
  162. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/session_manager.py +0 -0
  163. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/session_preloader.py +0 -0
  164. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/text_index.py +0 -0
  165. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/tool_classifier.py +0 -0
  166. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/transcript_index.py +0 -0
  167. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/validation_cache.py +0 -0
  168. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/version_tracker.py +0 -0
  169. {code_context_control-2.37.0 → code_context_control-2.39.0}/services/watcher.py +0 -0
  170. {code_context_control-2.37.0 → code_context_control-2.39.0}/setup.cfg +0 -0
  171. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_aider_polyglot.py +0 -0
  172. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_bitbucket_cli_smoke.py +0 -0
  173. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_bitbucket_client.py +0 -0
  174. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_bitbucket_credentials.py +0 -0
  175. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_bitbucket_tool.py +0 -0
  176. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_c3_shell.py +0 -0
  177. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_cli_smoke.py +0 -0
  178. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_e2e_benchmark.py +0 -0
  179. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_federated_graph.py +0 -0
  180. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_ghost_files.py +0 -0
  181. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_git_branch_awareness.py +0 -0
  182. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_hub_server_smoke.py +0 -0
  183. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_install_mcp_entrypoint.py +0 -0
  184. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_mcp_host_guard.py +0 -0
  185. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_mcp_server_smoke.py +0 -0
  186. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_memory_graph_api.py +0 -0
  187. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_memory_system.py +0 -0
  188. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_notification_discipline.py +0 -0
  189. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_oracle_api_auth.py +0 -0
  190. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_oracle_apikey_api.py +0 -0
  191. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_output_filter.py +0 -0
  192. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_permissions.py +0 -0
  193. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_project_manager.py +0 -0
  194. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_project_manager_merge.py +0 -0
  195. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_project_tool.py +0 -0
  196. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_read_coercion.py +0 -0
  197. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_session_benchmark.py +0 -0
  198. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_session_budget.py +0 -0
  199. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_shell_robustness.py +0 -0
  200. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_swe_bench.py +0 -0
  201. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_upgrade_and_version.py +0 -0
  202. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_validate.py +0 -0
  203. {code_context_control-2.37.0 → code_context_control-2.39.0}/tests/test_windows_reliability.py +0 -0
  204. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/__init__.py +0 -0
  205. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/backend.py +0 -0
  206. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/main.py +0 -0
  207. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/screens/__init__.py +0 -0
  208. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/screens/benchmark_view.py +0 -0
  209. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/screens/claudemd_view.py +0 -0
  210. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/screens/compress_view.py +0 -0
  211. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/screens/index_view.py +0 -0
  212. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/screens/init_view.py +0 -0
  213. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/screens/mcp_view.py +0 -0
  214. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/screens/optimize_view.py +0 -0
  215. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/screens/pipe_view.py +0 -0
  216. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/screens/projects_view.py +0 -0
  217. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/screens/search_view.py +0 -0
  218. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/screens/session_view.py +0 -0
  219. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/screens/stats.py +0 -0
  220. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/screens/ui_view.py +0 -0
  221. {code_context_control-2.37.0 → code_context_control-2.39.0}/tui/theme.tcss +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.37.0
3
+ Version: 2.39.0
4
4
  Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer.
5
5
  Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -334,6 +334,10 @@ Only **read** and **safe-action** tools are exposed (no code editing); requests
334
334
  default. Generate, rotate, and copy the token from the dashboard's **Settings →
335
335
  Discovery API** tab. See the [Oracle Discovery API guide](oracle-guide/discovery-api.md).
336
336
 
337
+ As of v2.38.0 the Oracle also reports a **cross-project activity digest** — sessions,
338
+ tool calls, edits, git mutations, and token/cost for a day — via the `activity_report`
339
+ discovery tool, the `GET /api/activity/digest` endpoint, and the dashboard's **Activity** tab.
340
+
337
341
  ---
338
342
 
339
343
  ## Tiered local AI (optional)
@@ -272,6 +272,10 @@ Only **read** and **safe-action** tools are exposed (no code editing); requests
272
272
  default. Generate, rotate, and copy the token from the dashboard's **Settings →
273
273
  Discovery API** tab. See the [Oracle Discovery API guide](oracle-guide/discovery-api.md).
274
274
 
275
+ As of v2.38.0 the Oracle also reports a **cross-project activity digest** — sessions,
276
+ tool calls, edits, git mutations, and token/cost for a day — via the `activity_report`
277
+ discovery tool, the `GET /api/activity/digest` endpoint, and the dashboard's **Activity** tab.
278
+
275
279
  ---
276
280
 
277
281
  ## Tiered local AI (optional)
@@ -74,9 +74,46 @@ def get_tool_output(data: dict) -> tuple:
74
74
 
75
75
 
76
76
  def get_tool_input_path(data: dict) -> str:
77
- """Extract file path from tool_input, handling both Claude (file_path) and Gemini (path)."""
77
+ """Extract file path from tool_input, handling Claude (file_path),
78
+ Gemini (path), and NotebookEdit (notebook_path)."""
78
79
  tool_input = data.get("tool_input", {})
79
- return tool_input.get("file_path", "") or tool_input.get("path", "")
80
+ return (
81
+ tool_input.get("file_path", "")
82
+ or tool_input.get("path", "")
83
+ or tool_input.get("notebook_path", "")
84
+ )
85
+
86
+
87
+ def record_json_unlocks(editable: list, project_path: Path | None = None) -> None:
88
+ """Record file paths as read+edit unlocked in .c3/unlocked_files.json.
89
+
90
+ This is the map that hook_pretool_enforce.py actually reads (the plain
91
+ .txt unlock list is not consumed by any hook). Mirrors the behaviour of
92
+ cli/hook_c3read._record_json_unlocks so c3_compress/c3_agent sticky
93
+ unlocks reach the enforcer. Fails silently on I/O errors.
94
+ """
95
+ base = project_path if project_path is not None else Path.cwd()
96
+ json_path = base / ".c3" / "unlocked_files.json"
97
+ try:
98
+ existing: dict = {}
99
+ if json_path.exists():
100
+ try:
101
+ existing = json.loads(json_path.read_text(encoding="utf-8"))
102
+ if not isinstance(existing, dict):
103
+ existing = {}
104
+ except Exception:
105
+ existing = {}
106
+ for fp in editable:
107
+ if not fp:
108
+ continue
109
+ normalized = str(Path(fp).resolve())
110
+ cats = set(existing.get(normalized, []))
111
+ cats.update({"read", "edit"})
112
+ existing[normalized] = sorted(cats)
113
+ json_path.parent.mkdir(parents=True, exist_ok=True)
114
+ json_path.write_text(json.dumps(existing), encoding="utf-8")
115
+ except Exception:
116
+ pass
80
117
 
81
118
 
82
119
  def emit_additional_context(text: str, is_gemini: bool) -> None:
@@ -85,7 +85,7 @@ console = Console() if HAS_RICH else None
85
85
  # Config
86
86
  CONFIG_DIR = ".c3"
87
87
  CONFIG_FILE = ".c3/config.json"
88
- __version__ = "2.37.0"
88
+ __version__ = "2.39.0"
89
89
 
90
90
 
91
91
  def _command_deps() -> CommandDeps:
@@ -4084,7 +4084,11 @@ def _upsert_toml_section(toml_path: Path, section: str, entries: dict) -> None:
4084
4084
  content = toml_path.read_text(encoding="utf-8") if toml_path.exists() else ""
4085
4085
  header = f"[{section}]"
4086
4086
 
4087
- # Strip existing section (header + its key=value lines)
4087
+ # Strip existing section (header + its key=value lines). Also strip any
4088
+ # dotted child subtables (e.g. "[mcp_servers.c3.env]" under
4089
+ # "[mcp_servers.c3]") so they are not orphaned beneath the re-appended
4090
+ # section, which would corrupt the file on re-run.
4091
+ child_prefix = f"{header[:-1]}." # "[mcp_servers.c3]" -> "[mcp_servers.c3."
4088
4092
  lines = content.splitlines()
4089
4093
  new_lines: list[str] = []
4090
4094
  skip = False
@@ -4094,7 +4098,8 @@ def _upsert_toml_section(toml_path: Path, section: str, entries: dict) -> None:
4094
4098
  skip = True
4095
4099
  continue
4096
4100
  if skip and stripped.startswith("["):
4097
- skip = False
4101
+ if not stripped.startswith(child_prefix):
4102
+ skip = False
4098
4103
  if not skip:
4099
4104
  new_lines.append(line)
4100
4105
 
@@ -4577,41 +4582,48 @@ def _ensure_global_claude_md() -> None:
4577
4582
 
4578
4583
  existing = global_md.read_text(encoding="utf-8")
4579
4584
 
4580
- if _GLOBAL_CLAUDE_MD_MARKER not in existing:
4581
- # User has their own CLAUDE.md append C3 section
4582
- merged = existing.rstrip() + "\n\n" + _GLOBAL_CLAUDE_MD_CONTENT
4583
- global_md.write_text(merged, encoding="utf-8")
4584
- print(f"Updated {global_md} (appended C3 enforcement)")
4585
- return
4585
+ # The C3-managed region is delimited by explicit BEGIN/END sentinels (the
4586
+ # same ones used for project instruction docs). This is unambiguous, so
4587
+ # user-written content outside the markers — including H1 headings that
4588
+ # happen to mention "C3" or "Tool Discipline" — is never swallowed.
4589
+ from services.claude_md import C3_BLOCK_BEGIN, C3_BLOCK_END, merge_c3_block
4586
4590
 
4587
- # C3 section exists — replace it with the latest version
4588
- # Find the C3 section boundaries: starts at the marker, ends at next # heading or EOF
4589
- start = existing.index(_GLOBAL_CLAUDE_MD_MARKER)
4590
- # Find the next top-level heading after the C3 section
4591
- rest = existing[start + len(_GLOBAL_CLAUDE_MD_MARKER):]
4592
- lines_after = rest.split("\n")
4593
- end_offset = len(rest) # default: to EOF
4594
- running = 0
4595
- for line in lines_after:
4596
- running += len(line) + 1
4597
- # A top-level heading that's NOT part of C3's sub-headings
4598
- if line.startswith("# ") and "C3" not in line and "Tool Discipline" not in line:
4599
- end_offset = running - len(line) - 1
4600
- break
4601
-
4602
- end = start + len(_GLOBAL_CLAUDE_MD_MARKER) + end_offset
4603
- before = existing[:start].rstrip()
4604
- after = existing[end:].lstrip()
4591
+ wrapped = f"{C3_BLOCK_BEGIN}\n{_GLOBAL_CLAUDE_MD_CONTENT.strip()}\n{C3_BLOCK_END}"
4605
4592
 
4606
- parts = []
4607
- if before:
4608
- parts.append(before)
4609
- parts.append(_GLOBAL_CLAUDE_MD_CONTENT.strip())
4610
- if after:
4611
- parts.append(after)
4593
+ # Markers already present → surgical, marker-bounded replacement.
4594
+ if C3_BLOCK_BEGIN in existing:
4595
+ global_md.write_text(merge_c3_block(existing, wrapped), encoding="utf-8")
4596
+ print(f"Updated {global_md} (refreshed C3 enforcement)")
4597
+ return
4612
4598
 
4613
- global_md.write_text("\n\n".join(parts) + "\n", encoding="utf-8")
4614
- print(f"Updated {global_md} (refreshed C3 enforcement)")
4599
+ # Legacy marker-less C3 region → one-time migration into the marked block.
4600
+ # Bound the region from the legacy heading to the NEXT top-level (``# ``)
4601
+ # heading. C3's own content has exactly one H1 (the legacy heading itself),
4602
+ # so the next H1 reliably marks where user content resumes; we deliberately
4603
+ # do NOT skip H1s containing "C3"/"Tool Discipline" (the old heuristic did,
4604
+ # which is what swallowed user headings).
4605
+ if _GLOBAL_CLAUDE_MD_MARKER in existing:
4606
+ start = existing.index(_GLOBAL_CLAUDE_MD_MARKER)
4607
+ rest = existing[start + len(_GLOBAL_CLAUDE_MD_MARKER):]
4608
+ end_offset = len(rest) # default: to EOF
4609
+ running = 0
4610
+ for line in rest.split("\n"):
4611
+ running += len(line) + 1
4612
+ if line.startswith("# "):
4613
+ end_offset = running - len(line) - 1
4614
+ break
4615
+ end = start + len(_GLOBAL_CLAUDE_MD_MARKER) + end_offset
4616
+ before = existing[:start].rstrip()
4617
+ after = existing[end:].lstrip()
4618
+ parts = [p for p in (before, wrapped, after) if p]
4619
+ global_md.write_text("\n\n".join(parts) + "\n", encoding="utf-8")
4620
+ print(f"Updated {global_md} (migrated C3 enforcement to markers)")
4621
+ return
4622
+
4623
+ # User has their own CLAUDE.md with no C3 content — append the marked block.
4624
+ merged = existing.rstrip() + "\n\n" + wrapped + "\n"
4625
+ global_md.write_text(merged, encoding="utf-8")
4626
+ print(f"Updated {global_md} (appended C3 enforcement)")
4615
4627
 
4616
4628
 
4617
4629
  def _instruction_documents_for_project() -> list[tuple[str, str]]:
@@ -5019,6 +5031,8 @@ def cmd_install_mcp(args):
5019
5031
  glob_matcher = "find_files"
5020
5032
  edit_matcher = "edit_file"
5021
5033
  write_matcher = "write_file"
5034
+ # Gemini has no MultiEdit / NotebookEdit equivalents.
5035
+ extra_edit_matchers = []
5022
5036
  else:
5023
5037
  shell_matcher = "Bash"
5024
5038
  read_matcher = "Read"
@@ -5026,6 +5040,9 @@ def cmd_install_mcp(args):
5026
5040
  glob_matcher = "Glob"
5027
5041
  edit_matcher = "Edit"
5028
5042
  write_matcher = "Write"
5043
+ # Claude Code also exposes MultiEdit (batch edits) and NotebookEdit;
5044
+ # both bypass enforcement/logging unless their matchers are registered.
5045
+ extra_edit_matchers = ["MultiEdit", "NotebookEdit"]
5029
5046
 
5030
5047
  # ── PostToolUse hooks ──
5031
5048
  desired_post_hooks = [
@@ -5120,6 +5137,10 @@ def cmd_install_mcp(args):
5120
5137
  "matcher": write_matcher,
5121
5138
  "hooks": [{"type": "command", "command": hook_edit_ledger_cmd}]
5122
5139
  },
5140
+ *[
5141
+ {"matcher": m, "hooks": [{"type": "command", "command": hook_edit_ledger_cmd}]}
5142
+ for m in extra_edit_matchers
5143
+ ],
5123
5144
  ]
5124
5145
 
5125
5146
  # ── PreToolUse hooks (enforcement — blocks native tools without prior c3_*) ──
@@ -5144,6 +5165,10 @@ def cmd_install_mcp(args):
5144
5165
  "matcher": write_matcher,
5145
5166
  "hooks": [{"type": "command", "command": hook_enforce_cmd}]
5146
5167
  },
5168
+ *[
5169
+ {"matcher": m, "hooks": [{"type": "command", "command": hook_enforce_cmd}]}
5170
+ for m in extra_edit_matchers
5171
+ ],
5147
5172
  ]
5148
5173
 
5149
5174
  # Merge: replace existing C3 hooks (so re-running install-mcp updates commands),
@@ -269,6 +269,7 @@ curl -X POST \
269
269
  <tr><td class="grp-read"><code>c3_status</code></td> <td>Project health / budget / sessions overview</td></tr>
270
270
  <tr><td class="grp-read"><code>c3_memory_query</code></td> <td>Read-only memory query (recall/list/score/graph/trends)</td></tr>
271
271
  <tr><td class="grp-read"><code>c3_edits</code> / <code>c3_edits_cross</code></td><td>Query the edit ledger (one / all projects)</td></tr>
272
+ <tr><td class="grp-read"><code>activity_report</code></td><td>Cross-project daily digest: sessions, tool calls, edits, git mutations, token/cost (optional LLM narration)</td></tr>
272
273
  </tbody>
273
274
  </table>
274
275
 
@@ -10,6 +10,7 @@ EditLedgerEnricherAgent running in the MCP server background.
10
10
 
11
11
  import json
12
12
  import sys
13
+ import uuid
13
14
  from datetime import datetime, timezone
14
15
  from pathlib import Path
15
16
 
@@ -25,7 +26,7 @@ EDITABLE_EXTS = {
25
26
  ".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs", ".java",
26
27
  ".rb", ".c", ".cpp", ".h", ".cs", ".html", ".css",
27
28
  ".json", ".yaml", ".yml", ".toml", ".sql", ".md", ".txt",
28
- ".sh", ".bat", ".ps1", ".r",
29
+ ".sh", ".bat", ".ps1", ".r", ".ipynb",
29
30
  }
30
31
 
31
32
  # How many tail lines to scan for version/seq (avoids full-file parse)
@@ -95,8 +96,11 @@ def _next_seq(ledger_file: Path, now: datetime) -> int:
95
96
  continue
96
97
  eid = entry.get("id", "")
97
98
  if eid.startswith(prefix):
99
+ # ids may carry a random suffix ("..._001_a1b2"); take the leading
100
+ # numeric run after the prefix so same-second seq counting survives.
101
+ seq_part = eid[len(prefix):].split("_", 1)[0]
98
102
  try:
99
- max_seq = max(max_seq, int(eid[len(prefix):]))
103
+ max_seq = max(max_seq, int(seq_part))
100
104
  except ValueError:
101
105
  pass
102
106
  return max_seq + 1
@@ -172,7 +176,9 @@ def main():
172
176
  git_pending = tracking_level != "minimal"
173
177
 
174
178
  entry = {
175
- "id": f"edit_{now.strftime('%Y%m%d_%H%M%S')}_{_next_seq(ledger_file, now):03d}",
179
+ # Random suffix prevents id collisions when the hook process and the
180
+ # server process (services/edit_ledger.py) write within the same second.
181
+ "id": f"edit_{now.strftime('%Y%m%d_%H%M%S')}_{_next_seq(ledger_file, now):03d}_{uuid.uuid4().hex[:4]}",
176
182
  "timestamp": now.isoformat(),
177
183
  "session_id": "",
178
184
  "file": rel,
@@ -14,7 +14,11 @@ from pathlib import Path
14
14
 
15
15
  sys.path.insert(0, str(Path(__file__).parent.parent))
16
16
 
17
- from cli._hook_utils import emit_additional_context, log_hook_error # noqa: E402
17
+ from cli._hook_utils import ( # noqa: E402
18
+ emit_additional_context,
19
+ log_hook_error,
20
+ record_json_unlocks,
21
+ )
18
22
 
19
23
  EDITABLE_EXTS = {
20
24
  ".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs", ".java",
@@ -144,6 +148,10 @@ def main():
144
148
  except Exception:
145
149
  pass
146
150
 
151
+ # Fix 2: also write the .json unlock map — the .txt list above is read
152
+ # by NO hook; hook_pretool_enforce.py only consults unlocked_files.json.
153
+ record_json_unlocks(editable)
154
+
147
155
  # Emit batched nudge with all pending files
148
156
  # Prefer c3_edit (no unlock needed). Native Edit is also unlocked via sticky file set.
149
157
  if len(pending) == 1:
@@ -180,24 +180,30 @@ def _is_file_unlocked(project_path: Path, file_path: str, category: str) -> bool
180
180
  return category in cats or "both" in cats
181
181
 
182
182
 
183
- def _check_signal_file(project_path: Path) -> tuple[bool, bool]:
183
+ def _check_signal_file(project_path: Path) -> tuple[bool, bool, str]:
184
184
  """Read last_c3_call.json written by hook_c3_signal.py.
185
185
 
186
- Returns (recent, read_unlocked):
186
+ Returns (recent, read_unlocked, c3_tool):
187
187
  recent: True if a c3_* tool completed within _SIGNAL_MAX_AGE_SECS
188
188
  read_unlocked: True if that tool was c3_search/c3_compress/c3_filter
189
+ c3_tool: short name of the c3 tool that wrote the signal (e.g.
190
+ "c3_edit"), or "" if recent is False / unparseable.
191
+
192
+ Fails closed: on any parse error, returns (False, False, "").
189
193
  """
190
194
  signal_path = project_path / _SIGNAL_FILE
191
195
  if not signal_path.exists():
192
- return False, False
196
+ return False, False, ""
193
197
  try:
194
198
  data = json.loads(signal_path.read_text(encoding="utf-8"))
195
199
  ts = datetime.fromisoformat(data["timestamp"])
196
200
  age = (datetime.now(timezone.utc) - ts).total_seconds()
197
201
  recent = age <= _SIGNAL_MAX_AGE_SECS
198
- return recent, bool(data.get("read_unlocked", False)) and recent
202
+ if not recent:
203
+ return False, False, ""
204
+ return True, bool(data.get("read_unlocked", False)), str(data.get("tool", ""))
199
205
  except Exception:
200
- return False, False
206
+ return False, False, ""
201
207
 
202
208
 
203
209
  def _check_c3_used(project_path: Path, tool_name: str, tool_input: dict) -> tuple[bool, str]:
@@ -223,10 +229,20 @@ def _check_c3_used(project_path: Path, tool_name: str, tool_input: dict) -> tupl
223
229
  required_cat = _TOOL_CATEGORY.get(tool_name, "read")
224
230
 
225
231
  # ── Fix 4: signal file — primary, fast, reliable ─────────────────────────
226
- signal_recent, signal_read_unlocked = _check_signal_file(project_path)
232
+ signal_recent, signal_read_unlocked, signal_tool = _check_signal_file(project_path)
227
233
  if signal_recent:
234
+ # Bypass fix: for write-class tools (Edit/Write/MultiEdit), the signal
235
+ # may only unlock them when the c3 tool that wrote it actually satisfies
236
+ # this tool's prereqs (e.g. c3_edit/c3_edits/c3_agent). A read-class
237
+ # signal (c3_status, c3_search, …) must NOT unlock a native write.
238
+ if tool_name in _BLOCKED_TOOLS:
239
+ if signal_tool in allowed:
240
+ if native_target:
241
+ _record_unlock(project_path, native_target, required_cat)
242
+ return True, "signal"
243
+ # Fresh signal exists but it's not a write-prereq tool — fall through
228
244
  # Fix 5: Grep/Glob without file path needs a read-unlocking tool
229
- if not native_target and tool_name in ("Grep", "Glob", "FindFiles", "SearchText"):
245
+ elif not native_target and tool_name in ("Grep", "Glob", "FindFiles", "SearchText"):
230
246
  if signal_read_unlocked:
231
247
  return True, "signal"
232
248
  # Signal exists but not read-unlocking (e.g. c3_memory) — fall through
@@ -120,12 +120,19 @@ async def lifespan(server):
120
120
  services.indexer.build_index()
121
121
  except Exception:
122
122
  pass
123
- # After code index is built, build embedding index
124
- if services.embedding_index and services.embedding_index.ready:
123
+ # After code index is built, build embedding index. build() lazily
124
+ # inits its chromadb/ollama backends, kept off the handshake path.
125
+ if services.embedding_index:
125
126
  try:
126
127
  services.embedding_index.build(services.indexer)
127
128
  except Exception:
128
129
  pass
130
+ # Warm SLTM vector store so the first memory call isn't slow.
131
+ if services.vector_store:
132
+ try:
133
+ services.vector_store.warm()
134
+ except Exception:
135
+ pass
129
136
  # Build doc index for Local RAG Pipeline
130
137
  if services.doc_index:
131
138
  try:
@@ -136,15 +143,23 @@ async def lifespan(server):
136
143
  threading.Thread(target=_bg_build, daemon=True, name="c3-initial-index").start()
137
144
  else:
138
145
  services.indexer._load_index()
139
- # Build/update embedding index in background
140
- if services.embedding_index and services.embedding_index.ready:
146
+ # Build/update embedding index + warm SLTM in background. Deferred off
147
+ # the handshake path: build()/warm() lazily init the heavy backends, so
148
+ # this must NOT gate on .ready synchronously here.
149
+ if services.embedding_index or services.vector_store:
141
150
  import threading
142
151
 
143
152
  def _bg_embed():
144
- try:
145
- services.embedding_index.build(services.indexer)
146
- except Exception:
147
- pass
153
+ if services.embedding_index:
154
+ try:
155
+ services.embedding_index.build(services.indexer)
156
+ except Exception:
157
+ pass
158
+ if services.vector_store:
159
+ try:
160
+ services.vector_store.warm()
161
+ except Exception:
162
+ pass
148
163
 
149
164
  threading.Thread(target=_bg_embed, daemon=True, name="c3-embed-index").start()
150
165
 
@@ -40,6 +40,7 @@ def _kill_proc_tree(proc):
40
40
  subprocess.run(
41
41
  ["taskkill", "/F", "/T", "/PID", str(proc.pid)],
42
42
  capture_output=True, stdin=subprocess.DEVNULL,
43
+ creationflags=subprocess.CREATE_NO_WINDOW,
43
44
  )
44
45
  else:
45
46
  proc.kill()
@@ -51,8 +52,10 @@ def _kill_proc_tree(proc):
51
52
  def _communicate_with_heartbeat(proc, timeout=45, idle_timeout=15):
52
53
  """communicate() replacement with idle-activity watchdog.
53
54
 
54
- Monitors stderr for activity. If no stderr output for idle_timeout seconds,
55
- kills the process early (catches MCP startup hangs). Also enforces total timeout.
55
+ Monitors both stdout and stderr for activity. If neither stream produces
56
+ output for idle_timeout seconds, kills the process early (catches MCP startup
57
+ hangs) without killing a backend that streams its answer only on stdout.
58
+ Also enforces total timeout.
56
59
 
57
60
  Returns (stdout, stderr, status) where status is 'ok', 'timeout', or 'idle_timeout'.
58
61
  """
@@ -71,7 +74,7 @@ def _communicate_with_heartbeat(proc, timeout=45, idle_timeout=15):
71
74
  except (ValueError, OSError):
72
75
  pass
73
76
 
74
- t_out = threading.Thread(target=_read_stream, args=(proc.stdout, stdout_parts), daemon=True)
77
+ t_out = threading.Thread(target=_read_stream, args=(proc.stdout, stdout_parts, True), daemon=True)
75
78
  t_err = threading.Thread(target=_read_stream, args=(proc.stderr, stderr_parts, True), daemon=True)
76
79
  t_out.start()
77
80
  t_err.start()
@@ -218,10 +221,17 @@ def _run_claude(task: str, context: str, cwd: str | None = None,
218
221
  cmd,
219
222
  stdout=subprocess.PIPE, stderr=subprocess.PIPE,
220
223
  stdin=subprocess.DEVNULL,
221
- text=True, cwd=cwd,
224
+ text=True, encoding="utf-8", errors="replace", cwd=cwd,
222
225
  **_popen_kwargs(),
223
226
  )
224
- output, err = _communicate_with_heartbeat(proc, timeout=timeout, idle_timeout=idle_timeout)
227
+ output, err, status = _communicate_with_heartbeat(
228
+ proc, timeout=timeout, idle_timeout=idle_timeout,
229
+ )
230
+ if status == "idle_timeout":
231
+ return (f"[claude:idle_timeout] No stderr activity for {idle_timeout}s "
232
+ f"(likely MCP startup hang)"), False
233
+ if status == "timeout":
234
+ return f"[claude:timeout] No response after {timeout}s", False
225
235
  if proc.returncode == 0 and output.strip():
226
236
  return output.strip(), True
227
237
  return f"[claude:error] {(err or '').strip() or 'no output'}", False
@@ -307,7 +317,7 @@ def _start_gemini_early(model: str, timeout: int = 45, idle_timeout: int = 15,
307
317
  cmd,
308
318
  stdout=subprocess.PIPE, stderr=subprocess.PIPE,
309
319
  stdin=subprocess.PIPE,
310
- text=True,
320
+ text=True, encoding="utf-8", errors="replace",
311
321
  cwd=cwd,
312
322
  **_popen_kwargs(),
313
323
  )
@@ -344,7 +354,7 @@ def _finish_gemini_early(proc, task: str, context: str,
344
354
  except (ValueError, OSError):
345
355
  pass
346
356
 
347
- t_out = threading.Thread(target=_read_stream, args=(proc.stdout, stdout_parts), daemon=True)
357
+ t_out = threading.Thread(target=_read_stream, args=(proc.stdout, stdout_parts, True), daemon=True)
348
358
  t_err = threading.Thread(target=_read_stream, args=(proc.stderr, stderr_parts, True), daemon=True)
349
359
  t_out.start()
350
360
  t_err.start()
@@ -448,7 +458,7 @@ def _run_gemini(task: str, context: str, model: str,
448
458
  cmd,
449
459
  stdout=subprocess.PIPE, stderr=subprocess.PIPE,
450
460
  stdin=subprocess.DEVNULL,
451
- text=True,
461
+ text=True, encoding="utf-8", errors="replace",
452
462
  cwd=cwd,
453
463
  **_popen_kwargs(),
454
464
  )
@@ -563,7 +573,7 @@ def _run_codex(task: str, context: str, model: str, sandbox: str,
563
573
  cmd,
564
574
  stdout=subprocess.PIPE, stderr=subprocess.PIPE,
565
575
  stdin=subprocess.DEVNULL,
566
- text=True,
576
+ text=True, encoding="utf-8", errors="replace",
567
577
  cwd=cwd,
568
578
  **_popen_kwargs(),
569
579
  )
@@ -592,25 +602,18 @@ def _run_codex_resume(follow_up: str, timeout: int = 120,
592
602
  """Resume last Codex session with a follow-up prompt."""
593
603
  cmd = ["codex", "exec", "--skip-git-repo-check", "resume", "--last"]
594
604
  try:
595
- import sys
596
605
  proc = subprocess.Popen(
597
606
  cmd,
598
607
  stdout=subprocess.PIPE, stderr=subprocess.PIPE,
599
608
  stdin=subprocess.PIPE,
600
- text=True,
609
+ text=True, encoding="utf-8", errors="replace",
601
610
  cwd=cwd,
611
+ **_popen_kwargs(),
602
612
  )
603
613
  try:
604
614
  stdout, stderr = proc.communicate(input=follow_up, timeout=timeout)
605
615
  except subprocess.TimeoutExpired:
606
- if sys.platform == "win32":
607
- subprocess.run(
608
- ["taskkill", "/F", "/T", "/PID", str(proc.pid)],
609
- capture_output=True, stdin=subprocess.DEVNULL,
610
- )
611
- else:
612
- proc.kill()
613
- proc.wait(timeout=5)
616
+ _kill_proc_tree(proc)
614
617
  return f"[codex:timeout] Resume timed out after {timeout}s", False
615
618
 
616
619
  if proc.returncode != 0:
@@ -1166,7 +1169,10 @@ def handle_delegate(task: str, task_type: str, context: str, file_path: str,
1166
1169
  model=fallback, system=tdef["system"],
1167
1170
  temperature=tdef.get("temperature", 0.3),
1168
1171
  max_tokens=int(dcfg.get("max_tokens", 512) or 512),
1169
- )
1172
+ timeout=timeout_s)
1173
+ if retry_resp is None:
1174
+ # Timeout/failure on the fallback — not a valid empty answer.
1175
+ continue
1170
1176
  retry_conf = _estimate_confidence(task_type, retry_resp, count_tokens(retry_resp))
1171
1177
  if retry_conf != "low":
1172
1178
  resp = retry_resp