code-context-control 2.39.0__tar.gz → 2.40.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 (224) hide show
  1. {code_context_control-2.39.0/code_context_control.egg-info → code_context_control-2.40.0}/PKG-INFO +16 -3
  2. {code_context_control-2.39.0 → code_context_control-2.40.0}/README.md +15 -2
  3. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/c3.py +36 -11
  4. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/commands/parser.py +1 -0
  5. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/bitbucket.py +25 -20
  6. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/delegate.py +80 -2
  7. {code_context_control-2.39.0 → code_context_control-2.40.0/code_context_control.egg-info}/PKG-INFO +16 -3
  8. {code_context_control-2.39.0 → code_context_control-2.40.0}/code_context_control.egg-info/SOURCES.txt +3 -0
  9. {code_context_control-2.39.0 → code_context_control-2.40.0}/core/config.py +34 -8
  10. {code_context_control-2.39.0 → code_context_control-2.40.0}/pyproject.toml +1 -1
  11. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/bitbucket_client.py +46 -11
  12. code_context_control-2.40.0/services/circuit_breaker.py +86 -0
  13. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/claude_md.py +1 -1
  14. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_bitbucket_client.py +63 -4
  15. code_context_control-2.40.0/tests/test_bitbucket_config_fallback.py +70 -0
  16. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_bitbucket_tool.py +10 -0
  17. code_context_control-2.40.0/tests/test_circuit_breaker.py +103 -0
  18. {code_context_control-2.39.0 → code_context_control-2.40.0}/LICENSE +0 -0
  19. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/__init__.py +0 -0
  20. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/_hook_utils.py +0 -0
  21. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/commands/__init__.py +0 -0
  22. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/commands/common.py +0 -0
  23. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/docs.html +0 -0
  24. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/edits.html +0 -0
  25. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/guide/bitbucket.html +0 -0
  26. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/guide/getting-started.html +0 -0
  27. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/guide/index.html +0 -0
  28. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/guide/oracle.html +0 -0
  29. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/guide/shared.css +0 -0
  30. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/guide/tools.html +0 -0
  31. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/guide/workflow.html +0 -0
  32. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_auto_snapshot.py +0 -0
  33. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_c3_signal.py +0 -0
  34. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_c3read.py +0 -0
  35. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_edit_ledger.py +0 -0
  36. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_edit_unlock.py +0 -0
  37. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_filter.py +0 -0
  38. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_ghost_files.py +0 -0
  39. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_pretool_enforce.py +0 -0
  40. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_read.py +0 -0
  41. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_session_stats.py +0 -0
  42. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_terse_advisor.py +0 -0
  43. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hub.html +0 -0
  44. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hub_server.py +0 -0
  45. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/mcp_proxy.py +0 -0
  46. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/mcp_server.py +0 -0
  47. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/server.py +0 -0
  48. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/__init__.py +0 -0
  49. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/_helpers.py +0 -0
  50. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/agent.py +0 -0
  51. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/compress.py +0 -0
  52. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/edit.py +0 -0
  53. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/edits.py +0 -0
  54. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/filter.py +0 -0
  55. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/impact.py +0 -0
  56. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/memory.py +0 -0
  57. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/project.py +0 -0
  58. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/read.py +0 -0
  59. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/search.py +0 -0
  60. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/session.py +0 -0
  61. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/shell.py +0 -0
  62. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/status.py +0 -0
  63. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/validate.py +0 -0
  64. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/api.js +0 -0
  65. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/app.js +0 -0
  66. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/bitbucket.js +0 -0
  67. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/chat.js +0 -0
  68. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/dashboard.js +0 -0
  69. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/edits.js +0 -0
  70. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/instructions.js +0 -0
  71. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/memory.js +0 -0
  72. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/sessions.js +0 -0
  73. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/settings.js +0 -0
  74. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/sidebar.js +0 -0
  75. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/icons.js +0 -0
  76. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/shared.js +0 -0
  77. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/theme.js +0 -0
  78. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui.html +0 -0
  79. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui_legacy.html +0 -0
  80. {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui_nano.html +0 -0
  81. {code_context_control-2.39.0 → code_context_control-2.40.0}/code_context_control.egg-info/dependency_links.txt +0 -0
  82. {code_context_control-2.39.0 → code_context_control-2.40.0}/code_context_control.egg-info/entry_points.txt +0 -0
  83. {code_context_control-2.39.0 → code_context_control-2.40.0}/code_context_control.egg-info/requires.txt +0 -0
  84. {code_context_control-2.39.0 → code_context_control-2.40.0}/code_context_control.egg-info/top_level.txt +0 -0
  85. {code_context_control-2.39.0 → code_context_control-2.40.0}/core/__init__.py +0 -0
  86. {code_context_control-2.39.0 → code_context_control-2.40.0}/core/ide.py +0 -0
  87. {code_context_control-2.39.0 → code_context_control-2.40.0}/core/mcp_toml.py +0 -0
  88. {code_context_control-2.39.0 → code_context_control-2.40.0}/core/web_security.py +0 -0
  89. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/__init__.py +0 -0
  90. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/config.py +0 -0
  91. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/mcp_oracle.py +0 -0
  92. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/oracle.html +0 -0
  93. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/oracle_server.py +0 -0
  94. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/__init__.py +0 -0
  95. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/activity_reporter.py +0 -0
  96. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/api_auth.py +0 -0
  97. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/c3_bridge.py +0 -0
  98. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/chat_engine.py +0 -0
  99. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/chat_store.py +0 -0
  100. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/cross_memory.py +0 -0
  101. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/federated_graph.py +0 -0
  102. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/health_checker.py +0 -0
  103. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/insight_engine.py +0 -0
  104. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/memory_reader.py +0 -0
  105. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/memory_writer.py +0 -0
  106. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/ollama_bridge.py +0 -0
  107. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/project_scanner.py +0 -0
  108. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/review_agent.py +0 -0
  109. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/tool_executor.py +0 -0
  110. {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/tool_registry.py +0 -0
  111. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/__init__.py +0 -0
  112. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/activity_log.py +0 -0
  113. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/agent_base.py +0 -0
  114. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/agents.py +0 -0
  115. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/auto_memory.py +0 -0
  116. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/bench/__init__.py +0 -0
  117. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/bench/external/__init__.py +0 -0
  118. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/bench/external/aider_polyglot.py +0 -0
  119. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/bench/external/swe_bench.py +0 -0
  120. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/benchmark_dashboard.py +0 -0
  121. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/bitbucket_credentials.py +0 -0
  122. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/compressor.py +0 -0
  123. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/context_snapshot.py +0 -0
  124. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/conversation_store.py +0 -0
  125. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/doc_index.py +0 -0
  126. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/e2e_benchmark.py +0 -0
  127. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/e2e_evaluator.py +0 -0
  128. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/e2e_tasks.py +0 -0
  129. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/edit_ledger.py +0 -0
  130. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/embedding_index.py +0 -0
  131. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/error_reporting.py +0 -0
  132. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/file_memory.py +0 -0
  133. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/git_context.py +0 -0
  134. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/hub_service.py +0 -0
  135. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/indexer.py +0 -0
  136. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/memory.py +0 -0
  137. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/memory_consolidator.py +0 -0
  138. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/memory_graph.py +0 -0
  139. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/memory_grounder.py +0 -0
  140. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/memory_scorer.py +0 -0
  141. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/metrics.py +0 -0
  142. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/notifications.py +0 -0
  143. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/ollama_client.py +0 -0
  144. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/output_filter.py +0 -0
  145. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/parser.py +0 -0
  146. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/project_manager.py +0 -0
  147. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/project_runtime.py +0 -0
  148. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/protocol.py +0 -0
  149. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/proxy_state.py +0 -0
  150. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/retrieval_broker.py +0 -0
  151. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/router.py +0 -0
  152. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/runtime.py +0 -0
  153. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/session_benchmark.py +0 -0
  154. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/session_manager.py +0 -0
  155. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/session_preloader.py +0 -0
  156. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/text_index.py +0 -0
  157. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/tool_classifier.py +0 -0
  158. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/transcript_index.py +0 -0
  159. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/validation_cache.py +0 -0
  160. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/vector_store.py +0 -0
  161. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/version_tracker.py +0 -0
  162. {code_context_control-2.39.0 → code_context_control-2.40.0}/services/watcher.py +0 -0
  163. {code_context_control-2.39.0 → code_context_control-2.40.0}/setup.cfg +0 -0
  164. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_activity_reporter.py +0 -0
  165. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_aider_polyglot.py +0 -0
  166. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_bitbucket_cli_smoke.py +0 -0
  167. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_bitbucket_credentials.py +0 -0
  168. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_c3_shell.py +0 -0
  169. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_claude_md_merge.py +0 -0
  170. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_cli_smoke.py +0 -0
  171. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_e2e_benchmark.py +0 -0
  172. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_edit_ledger_hook.py +0 -0
  173. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_edit_normalization.py +0 -0
  174. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_enforcement_flip.py +0 -0
  175. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_federated_graph.py +0 -0
  176. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_ghost_files.py +0 -0
  177. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_git_branch_awareness.py +0 -0
  178. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_hub_server_smoke.py +0 -0
  179. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_install_mcp_entrypoint.py +0 -0
  180. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_lazy_store_init.py +0 -0
  181. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_mcp_host_guard.py +0 -0
  182. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_mcp_server_smoke.py +0 -0
  183. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_mcp_toml.py +0 -0
  184. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_memory_graph_api.py +0 -0
  185. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_memory_system.py +0 -0
  186. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_notification_discipline.py +0 -0
  187. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_oracle_api_auth.py +0 -0
  188. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_oracle_apikey_api.py +0 -0
  189. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_oracle_discovery_api.py +0 -0
  190. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_oracle_security_fixes.py +0 -0
  191. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_output_filter.py +0 -0
  192. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_permissions.py +0 -0
  193. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_project_manager.py +0 -0
  194. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_project_manager_merge.py +0 -0
  195. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_project_tool.py +0 -0
  196. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_read_coercion.py +0 -0
  197. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_service_durability.py +0 -0
  198. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_session_benchmark.py +0 -0
  199. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_session_budget.py +0 -0
  200. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_shell_robustness.py +0 -0
  201. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_swe_bench.py +0 -0
  202. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_tool_registry.py +0 -0
  203. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_upgrade_and_version.py +0 -0
  204. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_validate.py +0 -0
  205. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_web_security.py +0 -0
  206. {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_windows_reliability.py +0 -0
  207. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/__init__.py +0 -0
  208. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/backend.py +0 -0
  209. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/main.py +0 -0
  210. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/__init__.py +0 -0
  211. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/benchmark_view.py +0 -0
  212. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/claudemd_view.py +0 -0
  213. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/compress_view.py +0 -0
  214. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/index_view.py +0 -0
  215. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/init_view.py +0 -0
  216. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/mcp_view.py +0 -0
  217. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/optimize_view.py +0 -0
  218. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/pipe_view.py +0 -0
  219. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/projects_view.py +0 -0
  220. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/search_view.py +0 -0
  221. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/session_view.py +0 -0
  222. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/stats.py +0 -0
  223. {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/ui_view.py +0 -0
  224. {code_context_control-2.39.0 → code_context_control-2.40.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.39.0
3
+ Version: 2.40.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
@@ -283,9 +283,12 @@ Access Token. Tokens live in the **OS keyring** (Windows Credential Manager,
283
283
  macOS Keychain, Linux Secret Service) — never in `.c3/config.json`.
284
284
 
285
285
  ```bash
286
- # One-time login per server
286
+ # One-time login per server (stored under this project's .c3/config.json)
287
287
  c3 bitbucket login --url https://bitbucket.example.com
288
- # prompts for username + PAT (masked)
288
+ # -> prompts for username + PAT (masked)
289
+
290
+ # ...or store it globally so every C3 project can use it
291
+ c3 bitbucket login --global --url https://bitbucket.example.com
289
292
 
290
293
  # Pin defaults so subsequent calls don't need project/repo
291
294
  c3 bitbucket set-default --project PROJ --repo my-service
@@ -294,6 +297,16 @@ c3 bitbucket set-default --project PROJ --repo my-service
294
297
  c3 bitbucket status
295
298
  ```
296
299
 
300
+ **Account resolution precedence:** the project's `.c3/config.json` wins, but when
301
+ it has no active account C3 falls back to the global `~/.c3/config.json`. So a
302
+ single `login --global` (or any login done from your home directory) is reusable
303
+ across every C3 project — the PAT always lives in the OS keyring, never on disk.
304
+
305
+ > **Upgrading:** stop the running `c3-mcp` server / CLI before `c3 upgrade`. A live
306
+ > process can hold package files open, leaving pip's `~`-prefixed backup dirs
307
+ > (`~ervices`, `~ools`, …) in `site-packages`; those are inert and safe to delete
308
+ > after the upgrade completes.
309
+
297
310
  The MCP tool dispatches by `action`. Read-only actions: `status`, `whoami`,
298
311
  `list_projects`, `list_repos`, `get_repo`, `list_prs`, `get_pr`, `get_pr_diff`,
299
312
  `get_pr_activities`, `list_branches`, `list_commits`, `list_activity`,
@@ -221,9 +221,12 @@ Access Token. Tokens live in the **OS keyring** (Windows Credential Manager,
221
221
  macOS Keychain, Linux Secret Service) — never in `.c3/config.json`.
222
222
 
223
223
  ```bash
224
- # One-time login per server
224
+ # One-time login per server (stored under this project's .c3/config.json)
225
225
  c3 bitbucket login --url https://bitbucket.example.com
226
- # prompts for username + PAT (masked)
226
+ # -> prompts for username + PAT (masked)
227
+
228
+ # ...or store it globally so every C3 project can use it
229
+ c3 bitbucket login --global --url https://bitbucket.example.com
227
230
 
228
231
  # Pin defaults so subsequent calls don't need project/repo
229
232
  c3 bitbucket set-default --project PROJ --repo my-service
@@ -232,6 +235,16 @@ c3 bitbucket set-default --project PROJ --repo my-service
232
235
  c3 bitbucket status
233
236
  ```
234
237
 
238
+ **Account resolution precedence:** the project's `.c3/config.json` wins, but when
239
+ it has no active account C3 falls back to the global `~/.c3/config.json`. So a
240
+ single `login --global` (or any login done from your home directory) is reusable
241
+ across every C3 project — the PAT always lives in the OS keyring, never on disk.
242
+
243
+ > **Upgrading:** stop the running `c3-mcp` server / CLI before `c3 upgrade`. A live
244
+ > process can hold package files open, leaving pip's `~`-prefixed backup dirs
245
+ > (`~ervices`, `~ools`, …) in `site-packages`; those are inert and safe to delete
246
+ > after the upgrade completes.
247
+
235
248
  The MCP tool dispatches by `action`. Read-only actions: `status`, `whoami`,
236
249
  `list_projects`, `list_repos`, `get_repo`, `list_prs`, `get_pr`, `get_pr_diff`,
237
250
  `get_pr_activities`, `list_branches`, `list_commits`, `list_activity`,
@@ -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.39.0"
88
+ __version__ = "2.40.0"
89
89
 
90
90
 
91
91
  def _command_deps() -> CommandDeps:
@@ -5577,14 +5577,19 @@ def _bb_cmd_login(args, project_path: str) -> None:
5577
5577
  from services import bitbucket_credentials as bb_creds
5578
5578
  from services.bitbucket_client import BitbucketDataCenterClient, BitbucketError
5579
5579
 
5580
+ # --global stores the account in ~/.c3/config.json so it is reusable in
5581
+ # every C3 project (load_bitbucket_config falls back to it automatically).
5582
+ if getattr(args, "use_global", False):
5583
+ project_path = str(Path.home())
5584
+
5580
5585
  base_url = (args.url or "").rstrip("/")
5581
5586
  username = args.username or input(f"Username for {base_url}: ").strip()
5582
5587
  if not username:
5583
- print("Login cancelled username required.")
5588
+ print("Login cancelled -- username required.")
5584
5589
  return
5585
5590
  token = args.token or getpass.getpass(f"Personal Access Token for {username}: ").strip()
5586
5591
  if not token:
5587
- print("Login cancelled token required.")
5592
+ print("Login cancelled -- token required.")
5588
5593
  return
5589
5594
 
5590
5595
  try:
@@ -5600,10 +5605,13 @@ def _bb_cmd_login(args, project_path: str) -> None:
5600
5605
  if getattr(args, "insecure", False):
5601
5606
  bb_creds.set_verify_tls(False, project_path=project_path)
5602
5607
 
5603
- print(f"[OK] Stored credentials for {username}@{base_url}")
5608
+ scope = "global (~/.c3)" if getattr(args, "use_global", False) else "project"
5609
+ print(f"[OK] Stored credentials for {username}@{base_url} [{scope}]")
5604
5610
 
5605
- # Connection probe non-fatal if it fails (token might be valid but
5606
- # network blocked at this moment).
5611
+ # Connection probe -- non-fatal if it fails (token might be valid but the
5612
+ # network is blocked right now). Gate success on application-properties
5613
+ # only; whoami enrichment is best-effort so a valid login never prints a
5614
+ # failure (Bitbucket DC has no /users/me).
5607
5615
  try:
5608
5616
  client = BitbucketDataCenterClient(
5609
5617
  base_url=base_url, token=token,
@@ -5611,12 +5619,20 @@ def _bb_cmd_login(args, project_path: str) -> None:
5611
5619
  )
5612
5620
  props = client.application_properties()
5613
5621
  version = props.get("version", "?")
5614
- user = client.whoami()
5615
5622
  print(f" Server: {version} ({base_url})")
5616
- print(f" Auth as: {user.get('displayName', username)} <{user.get('emailAddress', '?')}>")
5617
5623
  except BitbucketError as exc:
5618
5624
  print(f"[warn] Connection probe failed: {exc}")
5619
- print(" Token saved anyway re-test with `c3 bitbucket status`.")
5625
+ print(" Token saved anyway -- re-test with `c3 bitbucket status`.")
5626
+ return
5627
+ try:
5628
+ user = client.whoami()
5629
+ if user:
5630
+ print(
5631
+ f" Auth as: {user.get('displayName', username)} "
5632
+ f"<{user.get('emailAddress', '?')}>"
5633
+ )
5634
+ except BitbucketError:
5635
+ pass
5620
5636
 
5621
5637
 
5622
5638
  def _bb_cmd_logout(args, project_path: str) -> None:
@@ -5661,7 +5677,7 @@ def _bb_cmd_status(args, project_path: str) -> None:
5661
5677
  return
5662
5678
  token = bb_creds.load_token(active["base_url"], active["username"])
5663
5679
  if not token:
5664
- print(" Connection: FAIL no token in keyring")
5680
+ print(" Connection: FAIL -- no token in keyring")
5665
5681
  return
5666
5682
  try:
5667
5683
  client = BitbucketDataCenterClient(
@@ -5671,7 +5687,7 @@ def _bb_cmd_status(args, project_path: str) -> None:
5671
5687
  props = client.application_properties()
5672
5688
  print(f" Connection: OK (version {props.get('version','?')})")
5673
5689
  except BitbucketError as exc:
5674
- print(f" Connection: FAIL {exc}")
5690
+ print(f" Connection: FAIL -- {exc}")
5675
5691
 
5676
5692
 
5677
5693
  def _bb_cmd_use(args, project_path: str) -> None:
@@ -6571,6 +6587,15 @@ def _launch_tui() -> None:
6571
6587
 
6572
6588
 
6573
6589
  def main():
6590
+ # Force UTF-8 on the CLI streams so server-supplied text (PR titles, branch
6591
+ # names, diffs) and our own glyphs render cleanly on Windows cp1252 consoles
6592
+ # instead of raising UnicodeEncodeError or mojibaking.
6593
+ for _stream in (sys.stdout, sys.stderr):
6594
+ try:
6595
+ _stream.reconfigure(encoding="utf-8", errors="replace")
6596
+ except Exception:
6597
+ pass
6598
+
6574
6599
  try:
6575
6600
  from services import error_reporting
6576
6601
  error_reporting.init(component="c3-cli", version=__version__)
@@ -303,6 +303,7 @@ def build_parser(version: str, parse_cli_ide_arg):
303
303
  bb_login.add_argument("--token", help="Personal Access Token (prompted via getpass if omitted — preferred)")
304
304
  bb_login.add_argument("--no-set-active", action="store_true", help="Do not switch the active account to this one")
305
305
  bb_login.add_argument("--insecure", action="store_true", help="Disable TLS verification (self-signed certs)")
306
+ bb_login.add_argument("--global", dest="use_global", action="store_true", help="Store the account in the global ~/.c3/config.json so it is reusable in every C3 project")
306
307
  bb_login.add_argument("project_path", nargs="?", default=".")
307
308
 
308
309
  bb_logout = bb_subs.add_parser("logout", help="Remove a Bitbucket account from keyring + config")
@@ -50,7 +50,12 @@ def _cap(resp: str) -> str:
50
50
  candidate = "\n".join(lines) + "\n[truncated]"
51
51
  if count_tokens(candidate) <= _RESPONSE_TOKEN_CAP:
52
52
  return candidate
53
- return "\n".join(lines[:20]) + "\n[truncated]"
53
+ # A single over-long line never splits above; hard-clamp by characters as a
54
+ # final guard (~4 chars/token) so one huge line can't blow past the cap.
55
+ head = "\n".join(lines[:20])
56
+ if count_tokens(head) > _RESPONSE_TOKEN_CAP:
57
+ head = head[:_RESPONSE_TOKEN_CAP * 4]
58
+ return head + "\n[truncated]"
54
59
 
55
60
 
56
61
  def _build_client(svc) -> tuple[BitbucketDataCenterClient | None, str]:
@@ -111,20 +116,20 @@ def _format_pr(pr: dict) -> str:
111
116
  author = (pr.get("author") or {}).get("user", {}).get("name", "?")
112
117
  src = (pr.get("fromRef") or {}).get("displayId", "?")
113
118
  dst = (pr.get("toRef") or {}).get("displayId", "?")
114
- return f" #{pr_id} [{state:7}] {src} {dst} by {author}\n {title}"
119
+ return f" #{pr_id} [{state:7}] {src} -> {dst} by {author}\n {title}"
115
120
 
116
121
 
117
122
  def _format_pr_full(pr: dict) -> str:
118
123
  lines = [
119
- f"PR #{pr.get('id')} [{pr.get('state')}] {pr.get('title','')}",
120
- f" {(pr.get('fromRef') or {}).get('displayId')} {(pr.get('toRef') or {}).get('displayId')}",
124
+ f"PR #{pr.get('id')} [{pr.get('state')}] -- {pr.get('title','')}",
125
+ f" {(pr.get('fromRef') or {}).get('displayId')} -> {(pr.get('toRef') or {}).get('displayId')}",
121
126
  f" Author: {(pr.get('author') or {}).get('user',{}).get('displayName','?')}",
122
127
  f" Version: {pr.get('version')} | Open tasks: {pr.get('openTaskCount', 0)}",
123
128
  ]
124
129
  reviewers = pr.get("reviewers") or []
125
130
  if reviewers:
126
131
  rs = ", ".join(
127
- f"{r.get('user',{}).get('name','?')}({'' if r.get('approved') else '·'})"
132
+ f"{r.get('user',{}).get('name','?')}({'[x]' if r.get('approved') else '[ ]'})"
128
133
  for r in reviewers
129
134
  )
130
135
  lines.append(f" Reviewers: {rs}")
@@ -190,7 +195,7 @@ def _act_status(client: BitbucketDataCenterClient | None, err: str, svc) -> str:
190
195
  version = props.get("version", "?")
191
196
  out.append(f" Server : OK (version {version})")
192
197
  except BitbucketError as exc:
193
- out.append(f" Server : FAIL {exc}")
198
+ out.append(f" Server : FAIL -- {exc}")
194
199
  return "\n".join(out)
195
200
 
196
201
 
@@ -214,7 +219,7 @@ def _act_list_projects(client: BitbucketDataCenterClient, kwargs: dict) -> str:
214
219
  for p in projects[:50]:
215
220
  lines.append(f" {p.get('key','?'):16} {p.get('name','?')}")
216
221
  if len(projects) > 50:
217
- lines.append(f" +{len(projects) - 50} more")
222
+ lines.append(f" ... +{len(projects) - 50} more")
218
223
  return "\n".join(lines)
219
224
 
220
225
 
@@ -222,11 +227,11 @@ def _act_list_repos(client: BitbucketDataCenterClient, project: str) -> str:
222
227
  repos = client.list_repos(project)
223
228
  if not repos:
224
229
  return f"[bitbucket:repos] {project}: (none)"
225
- lines = [f"[bitbucket:repos] {project} {len(repos)} repo(s)"]
230
+ lines = [f"[bitbucket:repos] {project} - {len(repos)} repo(s)"]
226
231
  for r in repos[:80]:
227
232
  lines.append(f" {r.get('slug','?'):30} {r.get('name','?')}")
228
233
  if len(repos) > 80:
229
- lines.append(f" +{len(repos) - 80} more")
234
+ lines.append(f" ... +{len(repos) - 80} more")
230
235
  return "\n".join(lines)
231
236
 
232
237
 
@@ -252,7 +257,7 @@ def _act_list_prs(client, project: str, repo: str, kwargs: dict) -> str:
252
257
  )
253
258
  if not prs:
254
259
  return f"[bitbucket:prs] {project}/{repo} state={state}: (none)"
255
- out = [f"[bitbucket:prs] {project}/{repo} state={state} {len(prs)} PR(s)"]
260
+ out = [f"[bitbucket:prs] {project}/{repo} state={state} - {len(prs)} PR(s)"]
256
261
  for pr in prs:
257
262
  out.append(_format_pr(pr))
258
263
  return "\n".join(out)
@@ -266,7 +271,7 @@ def _act_get_pr(client, project: str, repo: str, pr_id: int) -> str:
266
271
  def _act_get_pr_diff(client, project: str, repo: str, pr_id: int, kwargs: dict) -> str:
267
272
  diff = client.get_pr_diff(project, repo, pr_id, context_lines=int(kwargs.get("context_lines", 3)))
268
273
  if len(diff) > _DIFF_PREVIEW_CHARS:
269
- diff = diff[:_DIFF_PREVIEW_CHARS] + "\n [diff truncated]"
274
+ diff = diff[:_DIFF_PREVIEW_CHARS] + "\n... [diff truncated]"
270
275
  return f"[bitbucket:diff] {project}/{repo}#{pr_id}\n{diff}"
271
276
 
272
277
 
@@ -274,7 +279,7 @@ def _act_get_pr_activities(client, project: str, repo: str, pr_id: int) -> str:
274
279
  acts = client.get_pr_activities(project, repo, pr_id)
275
280
  if not acts:
276
281
  return f"[bitbucket:pr-activity] {project}/{repo}#{pr_id}: (none)"
277
- out = [f"[bitbucket:pr-activity] {project}/{repo}#{pr_id} {len(acts)} event(s)"]
282
+ out = [f"[bitbucket:pr-activity] {project}/{repo}#{pr_id} - {len(acts)} event(s)"]
278
283
  for a in acts[:50]:
279
284
  out.append(_format_activity(a))
280
285
  return "\n".join(out)
@@ -341,11 +346,11 @@ def _act_list_branches(client, project: str, repo: str, kwargs: dict) -> str:
341
346
  branches = client.list_branches(project, repo, filter_text=kwargs.get("filter", ""))
342
347
  if not branches:
343
348
  return f"[bitbucket:branches] {project}/{repo}: (none)"
344
- out = [f"[bitbucket:branches] {project}/{repo} {len(branches)} branch(es)"]
349
+ out = [f"[bitbucket:branches] {project}/{repo} - {len(branches)} branch(es)"]
345
350
  for b in branches[:80]:
346
351
  out.append(_format_branch(b))
347
352
  if len(branches) > 80:
348
- out.append(f" +{len(branches) - 80} more")
353
+ out.append(f" ... +{len(branches) - 80} more")
349
354
  return "\n".join(out)
350
355
 
351
356
 
@@ -378,7 +383,7 @@ def _act_list_commits(client, project: str, repo: str, kwargs: dict) -> str:
378
383
  )
379
384
  if not commits:
380
385
  return f"[bitbucket:commits] {project}/{repo}: (none)"
381
- out = [f"[bitbucket:commits] {project}/{repo} {len(commits)} commit(s)"]
386
+ out = [f"[bitbucket:commits] {project}/{repo} - {len(commits)} commit(s)"]
382
387
  for c in commits:
383
388
  out.append(_format_commit(c))
384
389
  return "\n".join(out)
@@ -388,7 +393,7 @@ def _act_list_activity(client, project: str, repo: str, kwargs: dict) -> str:
388
393
  acts = client.list_repo_activities(project, repo, limit=int(kwargs.get("limit", 30)))
389
394
  if not acts:
390
395
  return f"[bitbucket:activity] {project}/{repo}: (none)"
391
- out = [f"[bitbucket:activity] {project}/{repo} {len(acts)} event(s)"]
396
+ out = [f"[bitbucket:activity] {project}/{repo} - {len(acts)} event(s)"]
392
397
  for a in acts:
393
398
  out.append(_format_commit(a))
394
399
  return "\n".join(out)
@@ -401,7 +406,7 @@ def _act_build_status(client, kwargs: dict) -> str:
401
406
  builds = client.get_build_status(commit)
402
407
  if not builds:
403
408
  return f"[bitbucket:build_status] {commit[:12]}: (none)"
404
- out = [f"[bitbucket:build_status] {commit[:12]} {len(builds)} build(s)"]
409
+ out = [f"[bitbucket:build_status] {commit[:12]} - {len(builds)} build(s)"]
405
410
  for b in builds:
406
411
  out.append(_format_build(b))
407
412
  return "\n".join(out)
@@ -429,10 +434,10 @@ def _act_list_webhooks(client, project: str, repo: str) -> str:
429
434
  hooks = client.list_webhooks(project, repo)
430
435
  if not hooks:
431
436
  return f"[bitbucket:webhooks] {project}/{repo}: (none)"
432
- out = [f"[bitbucket:webhooks] {project}/{repo} {len(hooks)} hook(s)"]
437
+ out = [f"[bitbucket:webhooks] {project}/{repo} - {len(hooks)} hook(s)"]
433
438
  for h in hooks:
434
439
  out.append(
435
- f" #{h.get('id')} [{'on' if h.get('active') else 'off'}] {h.get('name','?')} {h.get('url','?')}"
440
+ f" #{h.get('id')} [{'on' if h.get('active') else 'off'}] {h.get('name','?')} -> {h.get('url','?')}"
436
441
  )
437
442
  evs = h.get("events") or []
438
443
  if evs:
@@ -454,7 +459,7 @@ def _act_create_webhook(client, project: str, repo: str, kwargs: dict) -> str:
454
459
  active=bool(kwargs.get("active", True)),
455
460
  secret=kwargs.get("secret", ""),
456
461
  )
457
- return f"[bitbucket:webhook-created] {project}/{repo} #{res.get('id','?')} {name} {url}"
462
+ return f"[bitbucket:webhook-created] {project}/{repo} #{res.get('id','?')} {name} -> {url}"
458
463
 
459
464
 
460
465
  def _act_delete_webhook(client, project: str, repo: str, kwargs: dict) -> str:
@@ -11,10 +11,12 @@ import os
11
11
  import shutil
12
12
  import subprocess
13
13
  import sys
14
+ import threading
14
15
  import time
15
16
  from pathlib import Path
16
17
 
17
18
  from core import count_tokens
19
+ from services.circuit_breaker import CircuitBreaker
18
20
 
19
21
  log = logging.getLogger(__name__)
20
22
 
@@ -252,11 +254,20 @@ def _handle_claude_delegate(task: str, task_type: str, context: str,
252
254
  file_path: str, svc, dcfg: dict, finalize) -> str:
253
255
  """Handle delegation via Claude Code CLI."""
254
256
  timeout = int(dcfg.get("claude_timeout", 90))
257
+ breaker = _backend_breaker("claude", dcfg)
258
+ if not breaker.allow():
259
+ return finalize("c3_delegate", {"task_type": task_type, "backend": "claude"},
260
+ "[delegate:degraded] Claude skipped after repeated failures; retrying in "
261
+ f"~{breaker.cooldown_remaining()}s. Run 'claude --version' to diagnose.",
262
+ "degraded")
255
263
  _log_progress(svc, f"[delegate] Routing {task_type} → Claude CLI...")
256
264
  output, ok = _run_claude(task, context, cwd=str(svc.project_path), timeout=timeout)
257
265
  if not ok:
266
+ if breaker.record_failure():
267
+ _notify_backend_degraded(svc, "claude", breaker)
258
268
  return finalize("c3_delegate", {"task_type": task_type, "backend": "claude"},
259
269
  output, "error")
270
+ breaker.record_success()
260
271
  return finalize("c3_delegate", {"task_type": task_type, "backend": "claude"},
261
272
  output, "ok")
262
273
 
@@ -681,6 +692,51 @@ DELEGATE_TASKS = {
681
692
  _delegate_cache: dict[str, tuple[str, int]] = {}
682
693
  _delegate_metrics = {"total_calls": 0, "tokens_saved": 0}
683
694
 
695
+ # Per-backend runtime circuit breakers. Distinct from the install-status flags
696
+ # (_gemini_available etc., which only answer "is the CLI on PATH"): these track
697
+ # *runtime* health so a broken-but-installed backend (expired auth, repeated
698
+ # timeouts) stops re-spawning a 90-120s subprocess on every call. Keyed by
699
+ # backend name and intentionally process-global — backend health (auth, CLI
700
+ # version) is a property of the host, not of any single project.
701
+ _backend_breakers: dict[str, CircuitBreaker] = {}
702
+ _backend_breakers_lock = threading.Lock()
703
+
704
+
705
+ def _backend_breaker(name: str, dcfg: dict | None = None) -> CircuitBreaker:
706
+ """Return (creating on first use) the runtime circuit breaker for a backend."""
707
+ with _backend_breakers_lock:
708
+ breaker = _backend_breakers.get(name)
709
+ if breaker is None:
710
+ cfg = dcfg or {}
711
+ breaker = CircuitBreaker(
712
+ name,
713
+ failure_threshold=int(cfg.get("breaker_failure_threshold", 3) or 3),
714
+ cooldown_seconds=float(cfg.get("breaker_cooldown_seconds", 60) or 60),
715
+ )
716
+ _backend_breakers[name] = breaker
717
+ return breaker
718
+
719
+
720
+ def _notify_backend_degraded(svc, name: str, breaker: CircuitBreaker) -> None:
721
+ """Surface a backend trip via the NotificationStore (best-effort, never raises)."""
722
+ notifications = getattr(svc, "notifications", None)
723
+ if notifications is None:
724
+ return
725
+ try:
726
+ notifications.add(
727
+ agent="c3",
728
+ severity="warning",
729
+ title=f"Delegate backend degraded: {name}",
730
+ message=(
731
+ f"{name} failed {breaker.failure_threshold}x consecutively; c3_delegate "
732
+ f"will skip it for ~{int(breaker.cooldown_seconds)}s instead of re-spawning "
733
+ f"the CLI. Run '{name} --version' to diagnose."
734
+ ),
735
+ replace_if_unacked=True,
736
+ )
737
+ except Exception:
738
+ pass
739
+
684
740
 
685
741
  def get_delegate_metrics() -> dict:
686
742
  return dict(_delegate_metrics)
@@ -765,6 +821,13 @@ def _handle_codex_delegate(task: str, task_type: str, context: str,
765
821
  "[delegate:error] Codex CLI not available. Run 'codex --version' to diagnose.",
766
822
  "unavailable")
767
823
 
824
+ breaker = _backend_breaker("codex", dcfg)
825
+ if not breaker.allow():
826
+ return finalize("c3_delegate", {"task_type": task_type, "backend": "codex"},
827
+ "[delegate:degraded] Codex skipped after repeated failures; retrying in "
828
+ f"~{breaker.cooldown_remaining()}s. Run 'codex --version' to diagnose.",
829
+ "degraded")
830
+
768
831
  # Resolve model/sandbox/reasoning from config or defaults
769
832
  cdef = CODEX_MODELS.get(task_type, CODEX_MODELS.get("ask", {}))
770
833
  model = dcfg.get("codex_default_model") or cdef.get("model", "gpt-5.3-codex-spark")
@@ -807,10 +870,13 @@ def _handle_codex_delegate(task: str, task_type: str, context: str,
807
870
  elapsed = round(time.monotonic() - t0, 1)
808
871
 
809
872
  if not ok:
873
+ if breaker.record_failure():
874
+ _notify_backend_degraded(svc, "codex", breaker)
810
875
  return finalize("c3_delegate",
811
876
  {"task_type": task_type, "backend": "codex", "model": model, "elapsed": f"{elapsed}s"},
812
877
  output, "error")
813
878
 
879
+ breaker.record_success()
814
880
  _delegate_metrics["total_calls"] += 1
815
881
  _delegate_cache[ckey] = (output, count_tokens(output))
816
882
 
@@ -880,6 +946,13 @@ def _handle_gemini_delegate(task: str, task_type: str, context: str,
880
946
  "[delegate:error] Gemini CLI not available. Run 'gemini --version' to diagnose.",
881
947
  "unavailable")
882
948
 
949
+ breaker = _backend_breaker("gemini", dcfg)
950
+ if not breaker.allow():
951
+ return finalize("c3_delegate", {"task_type": task_type, "backend": "gemini"},
952
+ "[delegate:degraded] Gemini skipped after repeated failures; retrying in "
953
+ f"~{breaker.cooldown_remaining()}s. Run 'gemini --version' to diagnose.",
954
+ "degraded")
955
+
883
956
  # Resolve model from config or defaults
884
957
  gdef = GEMINI_MODELS.get(task_type, GEMINI_MODELS.get("ask", {}))
885
958
  model = dcfg.get("gemini_default_model") or gdef.get("model", "gemini-2.5-flash")
@@ -919,10 +992,13 @@ def _handle_gemini_delegate(task: str, task_type: str, context: str,
919
992
  elapsed = round(time.monotonic() - t0, 1)
920
993
 
921
994
  if not ok:
995
+ if breaker.record_failure():
996
+ _notify_backend_degraded(svc, "gemini", breaker)
922
997
  return finalize("c3_delegate",
923
998
  {"task_type": task_type, "backend": "gemini", "model": model, "elapsed": f"{elapsed}s"},
924
999
  output, "error")
925
1000
 
1001
+ breaker.record_success()
926
1002
  _delegate_metrics["total_calls"] += 1
927
1003
  _delegate_cache[ckey] = (output, count_tokens(output))
928
1004
 
@@ -1068,9 +1144,11 @@ def handle_delegate(task: str, task_type: str, context: str, file_path: str,
1068
1144
  _gemini_avail = (_gemini_available is True) or (
1069
1145
  _gemini_available is None and task_type not in _light_tasks and _is_gemini_on_path()
1070
1146
  )
1071
- if task_type in heavy_codex and _codex_avail and _codex_available is not False:
1147
+ if (task_type in heavy_codex and _codex_avail and _codex_available is not False
1148
+ and _backend_breaker("codex", dcfg).allow()):
1072
1149
  backend = "codex"
1073
- elif task_type in heavy_gemini and _gemini_avail and _gemini_available is not False:
1150
+ elif (task_type in heavy_gemini and _gemini_avail and _gemini_available is not False
1151
+ and _backend_breaker("gemini", dcfg).allow()):
1074
1152
  backend = "gemini"
1075
1153
  else:
1076
1154
  backend = "ollama"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.39.0
3
+ Version: 2.40.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
@@ -283,9 +283,12 @@ Access Token. Tokens live in the **OS keyring** (Windows Credential Manager,
283
283
  macOS Keychain, Linux Secret Service) — never in `.c3/config.json`.
284
284
 
285
285
  ```bash
286
- # One-time login per server
286
+ # One-time login per server (stored under this project's .c3/config.json)
287
287
  c3 bitbucket login --url https://bitbucket.example.com
288
- # prompts for username + PAT (masked)
288
+ # -> prompts for username + PAT (masked)
289
+
290
+ # ...or store it globally so every C3 project can use it
291
+ c3 bitbucket login --global --url https://bitbucket.example.com
289
292
 
290
293
  # Pin defaults so subsequent calls don't need project/repo
291
294
  c3 bitbucket set-default --project PROJ --repo my-service
@@ -294,6 +297,16 @@ c3 bitbucket set-default --project PROJ --repo my-service
294
297
  c3 bitbucket status
295
298
  ```
296
299
 
300
+ **Account resolution precedence:** the project's `.c3/config.json` wins, but when
301
+ it has no active account C3 falls back to the global `~/.c3/config.json`. So a
302
+ single `login --global` (or any login done from your home directory) is reusable
303
+ across every C3 project — the PAT always lives in the OS keyring, never on disk.
304
+
305
+ > **Upgrading:** stop the running `c3-mcp` server / CLI before `c3 upgrade`. A live
306
+ > process can hold package files open, leaving pip's `~`-prefixed backup dirs
307
+ > (`~ervices`, `~ools`, …) in `site-packages`; those are inert and safe to delete
308
+ > after the upgrade completes.
309
+
297
310
  The MCP tool dispatches by `action`. Read-only actions: `status`, `whoami`,
298
311
  `list_projects`, `list_repos`, `get_repo`, `list_prs`, `get_pr`, `get_pr_diff`,
299
312
  `get_pr_activities`, `list_branches`, `list_commits`, `list_activity`,
@@ -108,6 +108,7 @@ services/auto_memory.py
108
108
  services/benchmark_dashboard.py
109
109
  services/bitbucket_client.py
110
110
  services/bitbucket_credentials.py
111
+ services/circuit_breaker.py
111
112
  services/claude_md.py
112
113
  services/compressor.py
113
114
  services/context_snapshot.py
@@ -158,9 +159,11 @@ tests/test_activity_reporter.py
158
159
  tests/test_aider_polyglot.py
159
160
  tests/test_bitbucket_cli_smoke.py
160
161
  tests/test_bitbucket_client.py
162
+ tests/test_bitbucket_config_fallback.py
161
163
  tests/test_bitbucket_credentials.py
162
164
  tests/test_bitbucket_tool.py
163
165
  tests/test_c3_shell.py
166
+ tests/test_circuit_breaker.py
164
167
  tests/test_claude_md_merge.py
165
168
  tests/test_cli_smoke.py
166
169
  tests/test_e2e_benchmark.py
@@ -290,15 +290,41 @@ BITBUCKET_DEFAULTS = {
290
290
  }
291
291
 
292
292
 
293
+ def _read_bitbucket_section(config_file: Path) -> dict:
294
+ """Return the ``bitbucket`` section of a config file, or ``{}``."""
295
+ if not config_file.exists():
296
+ return {}
297
+ try:
298
+ with open(config_file, encoding="utf-8") as f:
299
+ data = json.load(f)
300
+ except Exception:
301
+ return {}
302
+ section = data.get("bitbucket", {})
303
+ return section if isinstance(section, dict) else {}
304
+
305
+
293
306
  def load_bitbucket_config(project_path: str) -> dict:
294
- """Load Bitbucket config from .c3/config.json, merged with defaults."""
295
- config_file = Path(project_path) / ".c3" / "config.json"
296
- overrides = {}
297
- if config_file.exists():
307
+ """Load Bitbucket config from .c3/config.json, merged with defaults.
308
+
309
+ Resolution precedence: the project ``<project>/.c3/config.json`` wins, but
310
+ when it has no active account we fall back to the global
311
+ ``~/.c3/config.json`` so a one-time ``c3 bitbucket login`` (or
312
+ ``login --global``) is reusable across every C3 project. The PAT itself
313
+ always lives in the OS keyring, never in these files.
314
+ """
315
+ project_file = Path(project_path) / ".c3" / "config.json"
316
+ overrides = _read_bitbucket_section(project_file)
317
+ if not (overrides.get("active") or {}).get("base_url"):
318
+ # Path.home() raises RuntimeError when no home dir is resolvable (e.g.
319
+ # a stripped subprocess env); treat that as "no global fallback".
298
320
  try:
299
- with open(config_file, encoding="utf-8") as f:
300
- data = json.load(f)
301
- overrides = data.get("bitbucket", {})
321
+ home_file = Path.home() / ".c3" / "config.json"
322
+ already_home = home_file.resolve() == project_file.resolve()
302
323
  except Exception:
303
- pass
324
+ home_file = None
325
+ already_home = True
326
+ if home_file is not None and not already_home:
327
+ home_overrides = _read_bitbucket_section(home_file)
328
+ if (home_overrides.get("active") or {}).get("base_url"):
329
+ overrides = home_overrides
304
330
  return {**BITBUCKET_DEFAULTS, **overrides}
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-context-control"
7
- version = "2.39.0"
7
+ version = "2.40.0"
8
8
  description = "Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"