code-context-control 2.118.0__tar.gz → 2.118.2__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 (498) hide show
  1. {code_context_control-2.118.0 → code_context_control-2.118.2}/PKG-INFO +1 -1
  2. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/c3.py +1 -1
  3. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_server.py +5 -3
  4. {code_context_control-2.118.0 → code_context_control-2.118.2}/code_context_control.egg-info/PKG-INFO +1 -1
  5. {code_context_control-2.118.0 → code_context_control-2.118.2}/code_context_control.egg-info/SOURCES.txt +2 -0
  6. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/mobile_api.py +6 -4
  7. {code_context_control-2.118.0 → code_context_control-2.118.2}/pyproject.toml +1 -1
  8. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/access_guard.py +13 -5
  9. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/agent_locks.py +10 -4
  10. code_context_control-2.118.2/services/atomic_json.py +118 -0
  11. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/shell_output.py +17 -3
  12. code_context_control-2.118.2/tests/test_atomic_json.py +240 -0
  13. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_shell_output.py +98 -0
  14. {code_context_control-2.118.0 → code_context_control-2.118.2}/LICENSE +0 -0
  15. {code_context_control-2.118.0 → code_context_control-2.118.2}/README.md +0 -0
  16. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/__init__.py +0 -0
  17. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/_hook_utils.py +0 -0
  18. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/_shell_writes.py +0 -0
  19. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/commands/__init__.py +0 -0
  20. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/commands/common.py +0 -0
  21. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/commands/parser.py +0 -0
  22. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/docs.html +0 -0
  23. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/edits.html +0 -0
  24. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/guide/access.html +0 -0
  25. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/guide/bitbucket.html +0 -0
  26. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/guide/credentials.html +0 -0
  27. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/guide/getting-started.html +0 -0
  28. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/guide/index.html +0 -0
  29. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/guide/masking.html +0 -0
  30. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/guide/oracle.html +0 -0
  31. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/guide/shared.css +0 -0
  32. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/guide/tools.html +0 -0
  33. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/guide/workflow.html +0 -0
  34. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_access_guard.py +0 -0
  35. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_artifact.py +0 -0
  36. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_auto_snapshot.py +0 -0
  37. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_c3_signal.py +0 -0
  38. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_c3read.py +0 -0
  39. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_dispatch.py +0 -0
  40. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_edit_ledger.py +0 -0
  41. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_edit_unlock.py +0 -0
  42. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_filter.py +0 -0
  43. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_ghost_files.py +0 -0
  44. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_pretool_enforce.py +0 -0
  45. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_prompt_recall.py +0 -0
  46. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_read.py +0 -0
  47. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_session_stats.py +0 -0
  48. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hook_terse_advisor.py +0 -0
  49. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/app.js +0 -0
  50. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/add_project.js +0 -0
  51. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/config_editor.js +0 -0
  52. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/drill_artifacts.js +0 -0
  53. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/drill_health.js +0 -0
  54. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/drill_panel.js +0 -0
  55. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/drill_subprojects.js +0 -0
  56. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/drill_tasks.js +0 -0
  57. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/drill_views.js +0 -0
  58. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/global_search.js +0 -0
  59. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/hub_access.js +0 -0
  60. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/hub_ci.js +0 -0
  61. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/hub_cred_audit.js +0 -0
  62. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/hub_credentials.js +0 -0
  63. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/hub_enforcement.js +0 -0
  64. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/hub_locks.js +0 -0
  65. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/hub_tokens.js +0 -0
  66. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/mcp_manager.js +0 -0
  67. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/modals.js +0 -0
  68. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/project_card.js +0 -0
  69. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/project_tree.js +0 -0
  70. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/session_drawer.js +0 -0
  71. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/settings_modal.js +0 -0
  72. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/sidebar.js +0 -0
  73. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/summary_bar.js +0 -0
  74. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/task_board.js +0 -0
  75. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/toasts.js +0 -0
  76. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/components/topbar.js +0 -0
  77. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui/state.js +0 -0
  78. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/hub_ui.html +0 -0
  79. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/mcp_proxy.py +0 -0
  80. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/mcp_server.py +0 -0
  81. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/progress.py +0 -0
  82. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/server.py +0 -0
  83. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/__init__.py +0 -0
  84. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/_grants.py +0 -0
  85. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/_helpers.py +0 -0
  86. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/agent.py +0 -0
  87. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/artifacts.py +0 -0
  88. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/bitbucket.py +0 -0
  89. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/ci.py +0 -0
  90. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/compress.py +0 -0
  91. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/credentials.py +0 -0
  92. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/delegate.py +0 -0
  93. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/edit.py +0 -0
  94. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/edit_verify.py +0 -0
  95. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/edits.py +0 -0
  96. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/federate.py +0 -0
  97. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/filter.py +0 -0
  98. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/impact.py +0 -0
  99. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/jira.py +0 -0
  100. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/locks.py +0 -0
  101. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/memory.py +0 -0
  102. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/override.py +0 -0
  103. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/project.py +0 -0
  104. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/read.py +0 -0
  105. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/search.py +0 -0
  106. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/session.py +0 -0
  107. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/shell.py +0 -0
  108. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/shell_job.py +0 -0
  109. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/shell_nudge.py +0 -0
  110. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/shell_parsers.py +0 -0
  111. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/shell_render.py +0 -0
  112. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/status.py +0 -0
  113. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/tasks.py +0 -0
  114. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/tools/validate.py +0 -0
  115. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/api.js +0 -0
  116. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/app.js +0 -0
  117. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/components/access.js +0 -0
  118. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/components/bitbucket.js +0 -0
  119. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/components/chat.js +0 -0
  120. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/components/credentials.js +0 -0
  121. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/components/dashboard.js +0 -0
  122. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/components/edits.js +0 -0
  123. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/components/enforcement.js +0 -0
  124. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/components/instructions.js +0 -0
  125. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/components/jira.js +0 -0
  126. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/components/memory.js +0 -0
  127. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/components/sessions.js +0 -0
  128. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/components/settings.js +0 -0
  129. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/components/sidebar.js +0 -0
  130. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/components/tasks.js +0 -0
  131. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/icons.js +0 -0
  132. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/pm_shared.js +0 -0
  133. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/shared.js +0 -0
  134. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui/theme.js +0 -0
  135. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui.html +0 -0
  136. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui_legacy.html +0 -0
  137. {code_context_control-2.118.0 → code_context_control-2.118.2}/cli/ui_nano.html +0 -0
  138. {code_context_control-2.118.0 → code_context_control-2.118.2}/code_context_control.egg-info/dependency_links.txt +0 -0
  139. {code_context_control-2.118.0 → code_context_control-2.118.2}/code_context_control.egg-info/entry_points.txt +0 -0
  140. {code_context_control-2.118.0 → code_context_control-2.118.2}/code_context_control.egg-info/requires.txt +0 -0
  141. {code_context_control-2.118.0 → code_context_control-2.118.2}/code_context_control.egg-info/top_level.txt +0 -0
  142. {code_context_control-2.118.0 → code_context_control-2.118.2}/core/__init__.py +0 -0
  143. {code_context_control-2.118.0 → code_context_control-2.118.2}/core/config.py +0 -0
  144. {code_context_control-2.118.0 → code_context_control-2.118.2}/core/ide.py +0 -0
  145. {code_context_control-2.118.0 → code_context_control-2.118.2}/core/mcp_toml.py +0 -0
  146. {code_context_control-2.118.0 → code_context_control-2.118.2}/core/web_security.py +0 -0
  147. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/__init__.py +0 -0
  148. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/config.py +0 -0
  149. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/mcp_oracle.py +0 -0
  150. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/oracle_server.py +0 -0
  151. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/oracle_ui.html +0 -0
  152. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/__init__.py +0 -0
  153. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/activity_reporter.py +0 -0
  154. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/api_auth.py +0 -0
  155. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/c3_bridge.py +0 -0
  156. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/chat_engine.py +0 -0
  157. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/chat_poll.py +0 -0
  158. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/chat_store.py +0 -0
  159. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/cross_memory.py +0 -0
  160. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/discovery_audit.py +0 -0
  161. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/federated_graph.py +0 -0
  162. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/health_checker.py +0 -0
  163. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/insight_engine.py +0 -0
  164. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/local_session.py +0 -0
  165. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/memory_reader.py +0 -0
  166. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/memory_writer.py +0 -0
  167. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/project_scanner.py +0 -0
  168. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/review_agent.py +0 -0
  169. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/tool_executor.py +0 -0
  170. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/services/tool_registry.py +0 -0
  171. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/activity.js +0 -0
  172. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/agents.js +0 -0
  173. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/app.js +0 -0
  174. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/busy.js +0 -0
  175. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/chat/conversations.js +0 -0
  176. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/chat/input.js +0 -0
  177. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/chat/markdown.js +0 -0
  178. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/chat/send.js +0 -0
  179. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/chat/stream_renderer.js +0 -0
  180. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/chat/toolbar.js +0 -0
  181. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/core.js +0 -0
  182. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/crossgraph.js +0 -0
  183. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/header.js +0 -0
  184. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/insights.js +0 -0
  185. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/projects.js +0 -0
  186. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/qrcode.js +0 -0
  187. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/settings.js +0 -0
  188. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/suggestions.js +0 -0
  189. {code_context_control-2.118.0 → code_context_control-2.118.2}/oracle/ui/theme_tabs.js +0 -0
  190. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/__init__.py +0 -0
  191. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/access_telemetry.py +0 -0
  192. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/activity_log.py +0 -0
  193. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/agent_base.py +0 -0
  194. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/agents.py +0 -0
  195. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/artifact_defs.py +0 -0
  196. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/artifact_store.py +0 -0
  197. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/auto_memory.py +0 -0
  198. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/background_service.py +0 -0
  199. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/bench/__init__.py +0 -0
  200. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/bench/external/__init__.py +0 -0
  201. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/bench/external/aider_polyglot.py +0 -0
  202. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/bench/external/swe_bench.py +0 -0
  203. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/bench/search_eval.py +0 -0
  204. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/bench/shell_eval.py +0 -0
  205. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/benchmark_dashboard.py +0 -0
  206. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/bitbucket_client.py +0 -0
  207. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/bitbucket_credentials.py +0 -0
  208. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/ci_act.py +0 -0
  209. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/ci_cache.py +0 -0
  210. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/ci_expr.py +0 -0
  211. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/ci_failures.py +0 -0
  212. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/ci_github.py +0 -0
  213. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/ci_impact.py +0 -0
  214. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/ci_intel.py +0 -0
  215. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/ci_runner.py +0 -0
  216. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/ci_workflow.py +0 -0
  217. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/circuit_breaker.py +0 -0
  218. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/claude_md.py +0 -0
  219. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/compressor.py +0 -0
  220. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/context_snapshot.py +0 -0
  221. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/conversation_store.py +0 -0
  222. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/cred_audit.py +0 -0
  223. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/cred_telemetry.py +0 -0
  224. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/credential_store.py +0 -0
  225. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/doc_index.py +0 -0
  226. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/e2e_benchmark.py +0 -0
  227. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/e2e_evaluator.py +0 -0
  228. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/e2e_tasks.py +0 -0
  229. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/edit_ledger.py +0 -0
  230. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/embedding_index.py +0 -0
  231. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/enforcement_policy.py +0 -0
  232. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/error_reporting.py +0 -0
  233. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/file_memory.py +0 -0
  234. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/git_context.py +0 -0
  235. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/hub_service.py +0 -0
  236. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/index_store.py +0 -0
  237. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/indexer.py +0 -0
  238. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/jira_client.py +0 -0
  239. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/jira_cloud.py +0 -0
  240. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/jira_credentials.py +0 -0
  241. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/jira_data_center.py +0 -0
  242. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/jira_links.py +0 -0
  243. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/lexical_index.py +0 -0
  244. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/mask_activation.py +0 -0
  245. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/mask_mirror.py +0 -0
  246. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/mask_presets.py +0 -0
  247. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/memory.py +0 -0
  248. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/memory_consolidator.py +0 -0
  249. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/memory_distiller.py +0 -0
  250. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/memory_graph.py +0 -0
  251. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/memory_grounder.py +0 -0
  252. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/memory_queue.py +0 -0
  253. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/memory_scorer.py +0 -0
  254. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/metrics.py +0 -0
  255. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/notifications.py +0 -0
  256. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/ollama_bridge.py +0 -0
  257. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/ollama_client.py +0 -0
  258. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/ollama_credentials.py +0 -0
  259. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/oracle_service.py +0 -0
  260. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/output_filter.py +0 -0
  261. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/override_grants.py +0 -0
  262. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/override_policy.py +0 -0
  263. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/override_requests.py +0 -0
  264. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/override_wake.py +0 -0
  265. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/parser.py +0 -0
  266. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/project_manager.py +0 -0
  267. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/project_runtime.py +0 -0
  268. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/protocol.py +0 -0
  269. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/proxy_state.py +0 -0
  270. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/repo_map.py +0 -0
  271. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/repo_shape.py +0 -0
  272. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/reranker.py +0 -0
  273. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/retention.py +0 -0
  274. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/retrieval.py +0 -0
  275. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/retrieval_broker.py +0 -0
  276. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/router.py +0 -0
  277. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/runtime.py +0 -0
  278. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/scanner.py +0 -0
  279. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/session_benchmark.py +0 -0
  280. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/session_manager.py +0 -0
  281. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/session_preloader.py +0 -0
  282. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/shell_jobs.py +0 -0
  283. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/subprojects.py +0 -0
  284. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/task_store.py +0 -0
  285. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/telemetry.py +0 -0
  286. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/text_index.py +0 -0
  287. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/time_tracker.py +0 -0
  288. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/tool_classifier.py +0 -0
  289. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/transcript_index.py +0 -0
  290. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/validation_cache.py +0 -0
  291. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/vector_store.py +0 -0
  292. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/watcher.py +0 -0
  293. {code_context_control-2.118.0 → code_context_control-2.118.2}/services/win_subprocess.py +0 -0
  294. {code_context_control-2.118.0 → code_context_control-2.118.2}/setup.cfg +0 -0
  295. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_access_builtin_optout.py +0 -0
  296. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_access_guard.py +0 -0
  297. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_access_guard_meta.py +0 -0
  298. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_access_guard_shell_project.py +0 -0
  299. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_access_guard_surfaces.py +0 -0
  300. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_access_guard_wiring.py +0 -0
  301. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_activity_reporter.py +0 -0
  302. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_agent_locks.py +0 -0
  303. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_aider_polyglot.py +0 -0
  304. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_antigravity_transition.py +0 -0
  305. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_artifact_store.py +0 -0
  306. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_artifact_tool.py +0 -0
  307. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_background_service.py +0 -0
  308. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_bitbucket_cli_smoke.py +0 -0
  309. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_bitbucket_client.py +0 -0
  310. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_bitbucket_config_fallback.py +0 -0
  311. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_bitbucket_credentials.py +0 -0
  312. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_bitbucket_tool.py +0 -0
  313. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_builtin_modes.py +0 -0
  314. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_c3_bridge_project_artifacts.py +0 -0
  315. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_c3_shell.py +0 -0
  316. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_chat_poll.py +0 -0
  317. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_ci_act.py +0 -0
  318. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_ci_cache.py +0 -0
  319. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_ci_expr.py +0 -0
  320. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_ci_impact.py +0 -0
  321. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_ci_intel.py +0 -0
  322. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_ci_runner.py +0 -0
  323. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_ci_workflow.py +0 -0
  324. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_circuit_breaker.py +0 -0
  325. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_claude_md_merge.py +0 -0
  326. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_claude_md_updater.py +0 -0
  327. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_cli_smoke.py +0 -0
  328. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_compressor_large_file.py +0 -0
  329. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_confirm_guard.py +0 -0
  330. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_confirm_guard_wiring.py +0 -0
  331. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_cooccurrence_bounds.py +0 -0
  332. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_cred_audit.py +0 -0
  333. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_cred_telemetry.py +0 -0
  334. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_credential_login_kind.py +0 -0
  335. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_credential_store.py +0 -0
  336. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_credential_ui_parity.py +0 -0
  337. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_credentials_routes.py +0 -0
  338. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_credentials_tool.py +0 -0
  339. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_creds_cli_smoke.py +0 -0
  340. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_delegate_cascade.py +0 -0
  341. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_delegate_version_probe.py +0 -0
  342. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_e2e_benchmark.py +0 -0
  343. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_edit_ledger_deadline.py +0 -0
  344. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_edit_ledger_hook.py +0 -0
  345. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_edit_locking.py +0 -0
  346. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_edit_normalization.py +0 -0
  347. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_edit_verify.py +0 -0
  348. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_embedding_index_deadlock.py +0 -0
  349. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_enforcement_flip.py +0 -0
  350. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_enforcement_policy.py +0 -0
  351. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_enforcement_routes.py +0 -0
  352. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_env_parser.py +0 -0
  353. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_failed_calls_do_not_unlock.py +0 -0
  354. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_federated_graph.py +0 -0
  355. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_filter_backoff.py +0 -0
  356. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_ghost_files.py +0 -0
  357. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_ghost_generation.py +0 -0
  358. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_git_branch_awareness.py +0 -0
  359. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hook_access_guard.py +0 -0
  360. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hook_codex_compat.py +0 -0
  361. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hook_dispatch.py +0 -0
  362. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hook_pretool_enforce.py +0 -0
  363. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hook_prompt_recall.py +0 -0
  364. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hook_smoke.py +0 -0
  365. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hook_state.py +0 -0
  366. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hook_utf8_stdio.py +0 -0
  367. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hub_credentials_route.py +0 -0
  368. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hub_credentials_routes_write.py +0 -0
  369. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hub_enforcement_routes.py +0 -0
  370. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hub_inspect_api.py +0 -0
  371. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hub_locks_routes.py +0 -0
  372. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hub_oracle_service_routes.py +0 -0
  373. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hub_override_routes.py +0 -0
  374. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hub_server_smoke.py +0 -0
  375. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_hub_subproject_links.py +0 -0
  376. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_ide_workflow_adaptation.py +0 -0
  377. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_index_cap_not_hardcoded.py +0 -0
  378. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_init_clear_preserves_user_content.py +0 -0
  379. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_init_scan.py +0 -0
  380. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_install_mcp_entrypoint.py +0 -0
  381. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_jira_cli_smoke.py +0 -0
  382. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_jira_client.py +0 -0
  383. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_jira_config_fallback.py +0 -0
  384. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_jira_credentials.py +0 -0
  385. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_jira_links.py +0 -0
  386. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_jira_routes.py +0 -0
  387. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_jira_tool.py +0 -0
  388. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_lazy_store_init.py +0 -0
  389. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_mask_guard.py +0 -0
  390. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_mask_routes.py +0 -0
  391. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_mask_surfaces.py +0 -0
  392. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_mcp_host_guard.py +0 -0
  393. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_mcp_memory_timeout.py +0 -0
  394. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_mcp_server_smoke.py +0 -0
  395. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_mcp_toml.py +0 -0
  396. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_memory_distiller.py +0 -0
  397. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_memory_graph_api.py +0 -0
  398. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_memory_queue.py +0 -0
  399. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_memory_system.py +0 -0
  400. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_mobile_api.py +0 -0
  401. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_mobile_extras.py +0 -0
  402. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_mobile_override_routes.py +0 -0
  403. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_mobile_security_api.py +0 -0
  404. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_no_stale_model_pins.py +0 -0
  405. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_notification_dedup.py +0 -0
  406. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_notification_discipline.py +0 -0
  407. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_ollama_credentials.py +0 -0
  408. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_api_auth.py +0 -0
  409. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_apikey_api.py +0 -0
  410. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_c3_bridge.py +0 -0
  411. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_chat_engine.py +0 -0
  412. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_chat_store.py +0 -0
  413. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_discovery_api.py +0 -0
  414. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_discovery_audit.py +0 -0
  415. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_health_probe.py +0 -0
  416. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_local_auth.py +0 -0
  417. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_ollama_bridge.py +0 -0
  418. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_scanner_cache.py +0 -0
  419. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_security_fixes.py +0 -0
  420. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_session_bootstrap.py +0 -0
  421. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_subproject_awareness.py +0 -0
  422. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_oracle_ui_bundle.py +0 -0
  423. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_output_filter.py +0 -0
  424. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_override_grants.py +0 -0
  425. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_override_grants_mcp.py +0 -0
  426. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_override_requests.py +0 -0
  427. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_override_wake.py +0 -0
  428. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_permissions.py +0 -0
  429. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_pm_api.py +0 -0
  430. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_project_manager.py +0 -0
  431. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_project_manager_merge.py +0 -0
  432. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_project_tool.py +0 -0
  433. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_read_coercion.py +0 -0
  434. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_read_edit_parity.py +0 -0
  435. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_related_facts.py +0 -0
  436. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_repo_map.py +0 -0
  437. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_repo_shape.py +0 -0
  438. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_response_boilerplate.py +0 -0
  439. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_retention.py +0 -0
  440. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_review_digest.py +0 -0
  441. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_search_eval.py +0 -0
  442. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_search_p1.py +0 -0
  443. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_search_p2.py +0 -0
  444. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_search_p2b.py +0 -0
  445. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_search_p3.py +0 -0
  446. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_search_p4.py +0 -0
  447. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_search_p5.py +0 -0
  448. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_service_durability.py +0 -0
  449. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_session_benchmark.py +0 -0
  450. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_session_budget.py +0 -0
  451. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_shell_budget_s1.py +0 -0
  452. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_shell_creds.py +0 -0
  453. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_shell_eval.py +0 -0
  454. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_shell_jobs_s3.py +0 -0
  455. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_shell_nudge_s4.py +0 -0
  456. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_shell_parsers_s2.py +0 -0
  457. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_shell_robustness.py +0 -0
  458. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_shell_telemetry_s0.py +0 -0
  459. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_shell_write_ledger.py +0 -0
  460. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_shell_writes.py +0 -0
  461. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_subproject_clear_scope.py +0 -0
  462. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_subproject_depth_consumers.py +0 -0
  463. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_subproject_exclusion.py +0 -0
  464. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_subproject_federation.py +0 -0
  465. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_subproject_linkage_api.py +0 -0
  466. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_subprojects.py +0 -0
  467. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_swe_bench.py +0 -0
  468. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_task_store.py +0 -0
  469. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_task_tool.py +0 -0
  470. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_time_tracker.py +0 -0
  471. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_token_telemetry.py +0 -0
  472. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_token_tracking.py +0 -0
  473. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_tool_registry.py +0 -0
  474. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_ui_jsx_syntax.py +0 -0
  475. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_upgrade_and_version.py +0 -0
  476. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_validate.py +0 -0
  477. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_vault_write_guard.py +0 -0
  478. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_version_sync.py +0 -0
  479. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_web_security.py +0 -0
  480. {code_context_control-2.118.0 → code_context_control-2.118.2}/tests/test_windows_reliability.py +0 -0
  481. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/__init__.py +0 -0
  482. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/backend.py +0 -0
  483. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/main.py +0 -0
  484. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/screens/__init__.py +0 -0
  485. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/screens/benchmark_view.py +0 -0
  486. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/screens/claudemd_view.py +0 -0
  487. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/screens/compress_view.py +0 -0
  488. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/screens/index_view.py +0 -0
  489. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/screens/init_view.py +0 -0
  490. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/screens/mcp_view.py +0 -0
  491. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/screens/optimize_view.py +0 -0
  492. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/screens/pipe_view.py +0 -0
  493. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/screens/projects_view.py +0 -0
  494. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/screens/search_view.py +0 -0
  495. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/screens/session_view.py +0 -0
  496. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/screens/stats.py +0 -0
  497. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/screens/ui_view.py +0 -0
  498. {code_context_control-2.118.0 → code_context_control-2.118.2}/tui/theme.tcss +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.118.0
3
+ Version: 2.118.2
4
4
  Summary: Local MCP code-intelligence for AI coding tools: surgical search/read/edit, agent-config version history, path-level access + masking guards, and a multi-project hub.
5
5
  Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -92,7 +92,7 @@ console = Console() if HAS_RICH else None
92
92
  # Config
93
93
  CONFIG_DIR = ".c3"
94
94
  CONFIG_FILE = ".c3/config.json"
95
- __version__ = "2.118.0"
95
+ __version__ = "2.118.2"
96
96
 
97
97
 
98
98
  def _compress_file_cli(compressor, path, mode="smart", **kw):
@@ -32,6 +32,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
32
32
  from core.ide import PROFILES, detect_ide, get_profile, load_ide_config, normalize_ide_name
33
33
  from services import access_guard
34
34
  from services.activity_log import ActivityLog
35
+ from services.atomic_json import write_json_atomic
35
36
  from services.project_manager import ProjectManager
36
37
  from services.tool_classifier import CATEGORIES
37
38
 
@@ -3078,9 +3079,10 @@ def api_projects_config_put():
3078
3079
  else:
3079
3080
  merged[key] = val
3080
3081
  cfg[section] = merged
3081
- tmp = cfg_file.with_name("config.json.tmp")
3082
- tmp.write_text(json.dumps(cfg, indent=2), encoding="utf-8")
3083
- os.replace(tmp, cfg_file)
3082
+ # Not `config.json.tmp`: the CLI (services.access_guard) and the mobile
3083
+ # API write this same file, and the hub itself is threaded — one shared
3084
+ # temp name is a name they all pick at once. See services.atomic_json.
3085
+ write_json_atomic(cfg_file, cfg, trailing_newline=False)
3084
3086
  try:
3085
3087
  ActivityLog(str(resolved)).log("hub_config_write", {
3086
3088
  "section": section, "keys": sorted(values.keys()), "source": "hub"})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.118.0
3
+ Version: 2.118.2
4
4
  Summary: Local MCP code-intelligence for AI coding tools: surgical search/read/edit, agent-config version history, path-level access + masking guards, and a multi-project hub.
5
5
  Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -189,6 +189,7 @@ services/agent_locks.py
189
189
  services/agents.py
190
190
  services/artifact_defs.py
191
191
  services/artifact_store.py
192
+ services/atomic_json.py
192
193
  services/auto_memory.py
193
194
  services/background_service.py
194
195
  services/benchmark_dashboard.py
@@ -299,6 +300,7 @@ tests/test_aider_polyglot.py
299
300
  tests/test_antigravity_transition.py
300
301
  tests/test_artifact_store.py
301
302
  tests/test_artifact_tool.py
303
+ tests/test_atomic_json.py
302
304
  tests/test_background_service.py
303
305
  tests/test_bitbucket_cli_smoke.py
304
306
  tests/test_bitbucket_client.py
@@ -56,6 +56,7 @@ from oracle.config import ORACLE_DIR, load_config
56
56
  from oracle.services import api_auth, discovery_audit
57
57
  from oracle.services.api_auth import extract_bearer
58
58
  from oracle.services.tool_registry import _c3_version
59
+ from services.atomic_json import write_json_atomic
59
60
 
60
61
  # 2: /info reports EFFECTIVE capabilities (filtered by the config switches)
61
62
  # rather than a static list, so a client can gate its UI instead of probing.
@@ -2090,10 +2091,11 @@ def _write_override_section(project: Path, section: dict) -> dict:
2090
2091
  else:
2091
2092
  block[key] = value
2092
2093
  cfg["override"] = block
2093
- tmp = cfg_file.with_name(f"{cfg_file.name}.tmp")
2094
- tmp.write_text(json.dumps(cfg, indent=2, ensure_ascii=False),
2095
- encoding="utf-8")
2096
- tmp.replace(cfg_file)
2094
+ # Shared with the CLI and the hub, and served from a threaded API, so the
2095
+ # publish goes through the one race-safe writer rather than a temp name
2096
+ # every writer of this file would pick. See services.atomic_json.
2097
+ write_json_atomic(cfg_file, cfg, ensure_ascii=False,
2098
+ trailing_newline=False)
2097
2099
  return block
2098
2100
 
2099
2101
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-context-control"
7
- version = "2.118.0"
7
+ version = "2.118.2"
8
8
  description = "Local MCP code-intelligence for AI coding tools: surgical search/read/edit, agent-config version history, path-level access + masking guards, and a multi-project hub."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -20,6 +20,8 @@ import re
20
20
  from dataclasses import dataclass
21
21
  from pathlib import Path
22
22
 
23
+ from services.atomic_json import write_json_atomic
24
+
23
25
  # ── Rule model ──────────────────────────────────────────────────────────────
24
26
 
25
27
  _KIND_DENY = "deny"
@@ -1473,11 +1475,17 @@ def _load_config_for_write(cfg: Path) -> tuple:
1473
1475
 
1474
1476
 
1475
1477
  def _write_scope_config(cfg: Path, data: dict) -> None:
1476
- """Atomic same-directory replace; privileged internal write."""
1477
- cfg.parent.mkdir(parents=True, exist_ok=True)
1478
- tmp = cfg.with_name(cfg.name + ".tmp")
1479
- tmp.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
1480
- os.replace(tmp, cfg)
1478
+ """Atomic same-directory replace; privileged internal write.
1479
+
1480
+ The CLI, the hub and the mobile API all reach this for the same scope
1481
+ file, so the publish is via ``services.atomic_json`` — a shared
1482
+ ``config.json.tmp`` is a name they would collide on. The fsync matters
1483
+ more here than anywhere else in the codebase: a truncated ``config.json``
1484
+ comes back from ``load_all`` in its ``corrupt`` list, every path then
1485
+ fails closed with ``<corrupt-config>``, and the session can no longer run
1486
+ the command that would fix it.
1487
+ """
1488
+ write_json_atomic(cfg, data)
1481
1489
 
1482
1490
 
1483
1491
  def seed_default_global_rules() -> dict:
@@ -41,6 +41,7 @@ import time
41
41
  import uuid
42
42
  from pathlib import Path
43
43
 
44
+ from services.atomic_json import write_json_atomic
44
45
  from services.task_store import _FileLock
45
46
 
46
47
  SCHEMA_VERSION = 1
@@ -219,10 +220,15 @@ class LockStore:
219
220
  return doc
220
221
 
221
222
  def _save(self, doc: dict) -> None:
222
- self.data_dir.mkdir(parents=True, exist_ok=True)
223
- tmp = self.state_file.with_name(self.state_file.name + ".tmp")
224
- tmp.write_text(json.dumps(doc, indent=2) + "\n", encoding="utf-8")
225
- os.replace(tmp, self.state_file)
223
+ # The `_FileLock` guard serialises the read-modify-write, but only
224
+ # among callers that take it — and it is advisory, held per process.
225
+ # The publish itself has to be safe on its own: a shared
226
+ # `locks.json.tmp` is one name every writer on the box picks, and two
227
+ # of them landing together either publishes a half-written lease file
228
+ # or raises WinError 5 (see services.atomic_json). Losing this write
229
+ # means losing live leases, which is exactly what `_load` refuses to
230
+ # do when it quarantines a corrupt file instead of resetting it.
231
+ write_json_atomic(self.state_file, doc)
226
232
 
227
233
  def _guard(self):
228
234
  return _FileLock(self.lock_file)
@@ -0,0 +1,118 @@
1
+ """One durable same-directory replace for the small JSON stores C3 writes.
2
+
3
+ Why this module exists
4
+ ----------------------
5
+ C3 keeps a handful of tiny JSON files that more than one writer touches: the
6
+ agent lock store, the per-scope ``config.json`` written by the CLI, the hub
7
+ and the mobile API, and the shell job store. Every one of them had grown the
8
+ same three-line publish step by copy — ``<name>.tmp`` then a single
9
+ ``os.replace`` — and on Windows that shape is a live bug rather than a style
10
+ nit:
11
+
12
+ 1. **A shared temp name races on the TEMP, not on the target.**
13
+ ``os.replace`` is atomic on the destination; nothing at all protects
14
+ ``<name>.tmp``. Two writers of one path both create it, so one publishes
15
+ a file the other is still writing, or the replace raises
16
+ ``PermissionError: [WinError 5] Access is denied`` because the loser still
17
+ holds a handle to it. Observed for real in the shell job store, where it
18
+ crashed a supervisor and reported "failed before running" for a command
19
+ that was fine (fixed there in 2.118.1, PR #157). The other copies had the
20
+ identical shape and no demonstrated failure only because they are written
21
+ far less often — the lock store especially is a coin that had not landed
22
+ on its edge yet.
23
+ 2. **A pid alone is not a unique suffix.** Threads share a pid, and the hub,
24
+ the Oracle and the lock store are all written from threaded servers. Six
25
+ threads are enough to drive a ``.tmp<pid>`` implementation into both
26
+ ``PermissionError`` and ``FileNotFoundError`` within a second, so the
27
+ suffix carries per-WRITE randomness as well.
28
+ 3. **One attempt turns a transient sharing violation into a lost write.** An
29
+ AV scanner or the Search indexer holding the target open for a few
30
+ milliseconds is ordinary on Windows and clears on its own; a single
31
+ ``os.replace`` turns it into an exception at the call site.
32
+
33
+ ``cli/_hook_utils._atomic_write_json`` deliberately does NOT import this. A
34
+ PreToolUse hook is a separate, short-lived, single-threaded process that
35
+ imports nothing from ``services`` on purpose, so pid-only is genuinely
36
+ enough there. That one duplication stays.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import json
42
+ import os
43
+ import secrets
44
+ import time
45
+ from pathlib import Path
46
+
47
+ # Four attempts across ~0.14s of backoff. Sized for a scanner or indexer
48
+ # holding a handle, not for a lock a peer holds for seconds — a failure that
49
+ # survives this is real and is re-raised.
50
+ REPLACE_ATTEMPTS = 4
51
+ REPLACE_BACKOFF_S = 0.02
52
+
53
+
54
+ def atomic_tmp_path(path: Path) -> Path:
55
+ """``<name>.tmp<pid>-<random>`` beside ``path`` — unique per write."""
56
+ return path.with_name(f"{path.name}.tmp{os.getpid()}-{secrets.token_hex(4)}")
57
+
58
+
59
+ def write_text_atomic(path, text: str, *, encoding: str = "utf-8",
60
+ fsync: bool = True) -> None:
61
+ """Publish ``text`` at ``path``: unique temp, then a retried ``os.replace``.
62
+
63
+ ``fsync`` flushes the temp's bytes before the replace, so a crash or power
64
+ loss between the two cannot publish a zero-length or half-written file.
65
+ Durable state wants that: a truncated ``config.json`` makes the Access
66
+ Guard read ``<corrupt-config>`` and fail closed on every subsequent call,
67
+ which wedges the whole session. Callers whose file is ephemeral spill
68
+ state pass ``fsync=False`` rather than pay a disk flush per update.
69
+
70
+ Newline translation matches ``Path.write_text`` (the default), so this is
71
+ a drop-in for the ``write_text`` + ``os.replace`` pairs it replaces and
72
+ does not churn line endings in files already on disk.
73
+ """
74
+ path = Path(path)
75
+ path.parent.mkdir(parents=True, exist_ok=True)
76
+ tmp = atomic_tmp_path(path)
77
+ try:
78
+ with open(tmp, "w", encoding=encoding) as fh:
79
+ fh.write(text)
80
+ if fsync:
81
+ fh.flush()
82
+ os.fsync(fh.fileno())
83
+ last_exc: OSError | None = None
84
+ for attempt in range(REPLACE_ATTEMPTS):
85
+ try:
86
+ os.replace(tmp, path)
87
+ return
88
+ except PermissionError as exc:
89
+ last_exc = exc
90
+ if attempt < REPLACE_ATTEMPTS - 1:
91
+ time.sleep(REPLACE_BACKOFF_S * (2 ** attempt))
92
+ if last_exc is not None:
93
+ raise last_exc
94
+ finally:
95
+ # A successful replace already consumed tmp, so this only fires on the
96
+ # failure paths — otherwise abandoned temps accumulate next to the
97
+ # file, and for `.c3/` that means they get indexed and committed.
98
+ try:
99
+ if tmp.exists():
100
+ tmp.unlink()
101
+ except OSError:
102
+ pass
103
+
104
+
105
+ def write_json_atomic(path, data, *, indent: int = 2,
106
+ ensure_ascii: bool = True,
107
+ trailing_newline: bool = True,
108
+ fsync: bool = True) -> None:
109
+ """``write_text_atomic`` over ``json.dumps``.
110
+
111
+ The serialisation knobs exist so each caller keeps the exact bytes it
112
+ wrote before this module: the point of the change is the publish step,
113
+ not a reformat of five JSON files that other tools and diffs already read.
114
+ """
115
+ text = json.dumps(data, indent=indent, ensure_ascii=ensure_ascii)
116
+ if trailing_newline:
117
+ text += "\n"
118
+ write_text_atomic(path, text, fsync=fsync)
@@ -75,6 +75,8 @@ from dataclasses import dataclass, field
75
75
  from datetime import datetime, timedelta, timezone
76
76
  from pathlib import Path
77
77
 
78
+ from services.atomic_json import write_json_atomic
79
+
78
80
  __all__ = [
79
81
  "STORE_ROOT", "RETENTION_DAYS", "RETENTION_BYTES", "HEAD_BYTES", "TAIL_BYTES",
80
82
  "TEXT_MAX_BYTES", "DEFAULT_READ_BYTES", "LINE_CLIP_CHARS",
@@ -961,9 +963,21 @@ def _load_meta(path: Path) -> OutputMeta | None:
961
963
 
962
964
 
963
965
  def _write_json_atomic(path: Path, data: dict) -> None:
964
- tmp = path.with_name(path.name + ".tmp")
965
- tmp.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding="utf-8")
966
- os.replace(tmp, path)
966
+ """Publish a job record durably — see ``services.atomic_json``.
967
+
968
+ This was the first copy of the unique-temp + retried-``os.replace`` shape
969
+ (2.118.1, PR #157, after a shared ``<name>.tmp`` crashed supervisors on
970
+ Windows CI with WinError 5 and reported "failed before running" for
971
+ commands that were fine). The implementation now lives in one place so the
972
+ lock store and the three ``config.json`` writers share it instead of
973
+ carrying four more copies of a bug.
974
+
975
+ ``fsync=False`` is the one thing kept local: a job record is ephemeral
976
+ spill state that a crash invalidates anyway, so a disk flush per status
977
+ update buys nothing. The durable stores take the default.
978
+ """
979
+ write_json_atomic(path, data, indent=1, ensure_ascii=False,
980
+ trailing_newline=False, fsync=False)
967
981
 
968
982
 
969
983
  def _move(src: Path, dst: Path) -> None:
@@ -0,0 +1,240 @@
1
+ """services.atomic_json — the one publish step for C3's small JSON stores.
2
+
3
+ Every test here fails against the shape these call sites carried before
4
+ (`<name>.tmp` + a single `os.replace`), which is the shape that crashed shell
5
+ job supervisors on Windows with `PermissionError: [WinError 5]`. The point of
6
+ the module is that the lock store and the three `config.json` writers stop
7
+ re-deriving it, so the last class checks each of those call sites actually
8
+ routes through it rather than only the helper being correct in isolation.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import sys
16
+ import tempfile
17
+ import threading
18
+ import unittest
19
+ from pathlib import Path
20
+ from unittest.mock import patch
21
+
22
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
23
+
24
+ from services import atomic_json as aj # noqa: E402
25
+
26
+
27
+ class TestTempNaming(unittest.TestCase):
28
+ """The temp must be unique per WRITE, not per path and not per process."""
29
+
30
+ def setUp(self):
31
+ self._tmp = tempfile.TemporaryDirectory()
32
+ self.path = Path(self._tmp.name) / "config.json"
33
+
34
+ def tearDown(self):
35
+ self._tmp.cleanup()
36
+
37
+ def _replace_spy(self):
38
+ seen: list[str] = []
39
+ real = os.replace
40
+
41
+ def spy(src, dst):
42
+ seen.append(Path(src).name)
43
+ return real(src, dst)
44
+
45
+ return seen, spy
46
+
47
+ def test_temp_is_not_the_shared_name(self):
48
+ seen, spy = self._replace_spy()
49
+ with patch.object(aj.os, "replace", spy):
50
+ aj.write_json_atomic(self.path, {"a": 1})
51
+ self.assertEqual(len(seen), 1)
52
+ self.assertNotEqual(seen[0], "config.json.tmp",
53
+ "shared temp name — concurrent writers race on it")
54
+ self.assertTrue(seen[0].startswith("config.json.tmp"))
55
+ self.assertIn(str(os.getpid()), seen[0])
56
+
57
+ def test_temp_differs_between_writes_in_one_process(self):
58
+ """Threads share a pid, so `.tmp<pid>` collides exactly as `.tmp` did."""
59
+ seen, spy = self._replace_spy()
60
+ with patch.object(aj.os, "replace", spy):
61
+ for i in range(5):
62
+ aj.write_json_atomic(self.path, {"a": i})
63
+ self.assertEqual(len(set(seen)), 5, f"temp name reused: {seen}")
64
+
65
+
66
+ class TestReplaceRetry(unittest.TestCase):
67
+ def setUp(self):
68
+ self._tmp = tempfile.TemporaryDirectory()
69
+ self.path = Path(self._tmp.name) / "config.json"
70
+
71
+ def tearDown(self):
72
+ self._tmp.cleanup()
73
+
74
+ def test_transient_permission_error_is_retried(self):
75
+ calls = {"n": 0}
76
+ real = os.replace
77
+
78
+ def flaky(src, dst):
79
+ calls["n"] += 1
80
+ if calls["n"] < 3: # two transient failures, then success
81
+ raise PermissionError(5, "Access is denied")
82
+ return real(src, dst)
83
+
84
+ with patch.object(aj.os, "replace", flaky):
85
+ aj.write_json_atomic(self.path, {"ok": True})
86
+ self.assertEqual(calls["n"], 3)
87
+ self.assertEqual(json.loads(self.path.read_text(encoding="utf-8")),
88
+ {"ok": True})
89
+
90
+ def test_persistent_permission_error_still_raises(self):
91
+ """Retrying must not turn a real failure into a silently lost write."""
92
+ def always(src, dst):
93
+ raise PermissionError(5, "Access is denied")
94
+
95
+ with patch.object(aj.os, "replace", always):
96
+ with self.assertRaises(PermissionError):
97
+ aj.write_json_atomic(self.path, {"a": 1})
98
+
99
+ def test_a_non_permission_oserror_is_not_retried(self):
100
+ """Only the Windows sharing violation is transient; the rest surface."""
101
+ calls = {"n": 0}
102
+
103
+ def broken(src, dst):
104
+ calls["n"] += 1
105
+ raise OSError(22, "Invalid argument")
106
+
107
+ with patch.object(aj.os, "replace", broken):
108
+ with self.assertRaises(OSError):
109
+ aj.write_json_atomic(self.path, {"a": 1})
110
+ self.assertEqual(calls["n"], 1)
111
+
112
+ def test_no_temp_is_left_behind_when_replace_fails(self):
113
+ def always(src, dst):
114
+ raise PermissionError(5, "Access is denied")
115
+
116
+ with patch.object(aj.os, "replace", always):
117
+ with self.assertRaises(PermissionError):
118
+ aj.write_json_atomic(self.path, {"a": 1})
119
+ leftovers = list(self.path.parent.glob("config.json.tmp*"))
120
+ self.assertEqual(leftovers, [], f"orphaned temps: {leftovers}")
121
+
122
+
123
+ class TestSerialisationIsPreserved(unittest.TestCase):
124
+ """Each call site keeps the exact bytes it wrote before the refactor."""
125
+
126
+ def setUp(self):
127
+ self._tmp = tempfile.TemporaryDirectory()
128
+ self.path = Path(self._tmp.name) / "f.json"
129
+
130
+ def tearDown(self):
131
+ self._tmp.cleanup()
132
+
133
+ def test_default_is_indent_2_with_trailing_newline(self):
134
+ aj.write_json_atomic(self.path, {"a": 1})
135
+ self.assertEqual(self.path.read_text(encoding="utf-8"),
136
+ '{\n "a": 1\n}\n')
137
+
138
+ def test_trailing_newline_can_be_suppressed(self):
139
+ aj.write_json_atomic(self.path, {"a": 1}, trailing_newline=False)
140
+ self.assertEqual(self.path.read_text(encoding="utf-8"),
141
+ '{\n "a": 1\n}')
142
+
143
+ def test_ensure_ascii_false_keeps_non_ascii_literal(self):
144
+ aj.write_json_atomic(self.path, {"k": "café"}, ensure_ascii=False)
145
+ self.assertIn("café", self.path.read_text(encoding="utf-8"))
146
+
147
+ def test_parent_directory_is_created(self):
148
+ deep = Path(self._tmp.name) / "a" / "b" / "c.json"
149
+ aj.write_json_atomic(deep, {"a": 1})
150
+ self.assertEqual(json.loads(deep.read_text(encoding="utf-8")), {"a": 1})
151
+
152
+
153
+ class TestFsync(unittest.TestCase):
154
+ def setUp(self):
155
+ self._tmp = tempfile.TemporaryDirectory()
156
+ self.path = Path(self._tmp.name) / "f.json"
157
+
158
+ def tearDown(self):
159
+ self._tmp.cleanup()
160
+
161
+ def test_durable_by_default(self):
162
+ """A truncated config.json fails the guard closed; flush before publish."""
163
+ with patch.object(aj.os, "fsync") as fs:
164
+ aj.write_json_atomic(self.path, {"a": 1})
165
+ self.assertEqual(fs.call_count, 1)
166
+
167
+ def test_opt_out_for_ephemeral_state(self):
168
+ with patch.object(aj.os, "fsync") as fs:
169
+ aj.write_json_atomic(self.path, {"a": 1}, fsync=False)
170
+ fs.assert_not_called()
171
+
172
+
173
+ class TestConcurrency(unittest.TestCase):
174
+ def test_threads_never_publish_a_truncated_file(self):
175
+ """The end-to-end property, and the one `.tmp<pid>` fails."""
176
+ with tempfile.TemporaryDirectory() as td:
177
+ path = Path(td) / "locks.json"
178
+ errors: list[Exception] = []
179
+
180
+ def writer(i):
181
+ try:
182
+ for _ in range(20):
183
+ aj.write_json_atomic(path, {"writer": i})
184
+ except Exception as exc: # noqa: BLE001 — surfaced below
185
+ errors.append(exc)
186
+
187
+ threads = [threading.Thread(target=writer, args=(i,))
188
+ for i in range(6)]
189
+ for t in threads:
190
+ t.start()
191
+ for t in threads:
192
+ t.join()
193
+
194
+ self.assertEqual(errors, [], f"concurrent writes failed: {errors}")
195
+ doc = json.loads(path.read_text(encoding="utf-8"))
196
+ self.assertIn(doc["writer"], range(6))
197
+ leftovers = list(path.parent.glob("locks.json.tmp*"))
198
+ self.assertEqual(leftovers, [], f"orphaned temps: {leftovers}")
199
+
200
+
201
+ class TestCallSitesUseIt(unittest.TestCase):
202
+ """The refactor's actual claim: no copy of the shape is left behind.
203
+
204
+ A source scan rather than a behavioural probe, because the four sites sit
205
+ behind a Flask route, an advisory file lock and a scope resolver — and
206
+ because the thing being asserted IS textual: nobody re-derives the publish
207
+ step. `cli/_hook_utils` is exempt on purpose (a PreToolUse subprocess
208
+ imports nothing from `services`).
209
+ """
210
+
211
+ ROOT = Path(__file__).resolve().parents[1]
212
+ SITES = (
213
+ "services/agent_locks.py",
214
+ "services/access_guard.py",
215
+ "services/shell_output.py",
216
+ "cli/hub_server.py",
217
+ "oracle/services/mobile_api.py",
218
+ )
219
+
220
+ def test_each_site_imports_the_helper(self):
221
+ for rel in self.SITES:
222
+ with self.subTest(file=rel):
223
+ src = (self.ROOT / rel).read_text(encoding="utf-8")
224
+ self.assertIn("from services.atomic_json import", src)
225
+
226
+ def test_no_site_still_writes_a_shared_temp(self):
227
+ for rel in self.SITES:
228
+ with self.subTest(file=rel):
229
+ src = (self.ROOT / rel).read_text(encoding="utf-8")
230
+ for line in src.splitlines():
231
+ stripped = line.strip()
232
+ if stripped.startswith("#") or '"""' in stripped:
233
+ continue # prose about the old shape is fine
234
+ self.assertNotIn('.name + ".tmp"', stripped)
235
+ self.assertNotIn('.name}.tmp"', stripped)
236
+ self.assertNotIn('"config.json.tmp"', stripped)
237
+
238
+
239
+ if __name__ == "__main__":
240
+ unittest.main()
@@ -723,5 +723,103 @@ class TestUnicodePaths(_StoreCase):
723
723
  self.assertEqual(self.store.list(project_path=project, session_id=session), [])
724
724
 
725
725
 
726
+ class TestAtomicWrite(unittest.TestCase):
727
+ """`_write_json_atomic` — the job store's publish step.
728
+
729
+ Both cases below fail against the pre-2.118.1 version, which used a single
730
+ shared `<name>.tmp` and attempted `os.replace` exactly once. That is what
731
+ crashed a supervisor on Windows CI with WinError 5 and reported "failed
732
+ before running" for a command that was fine.
733
+ """
734
+
735
+ def setUp(self):
736
+ self._tmp = tempfile.TemporaryDirectory()
737
+ self.path = Path(self._tmp.name) / "job.json"
738
+
739
+ def tearDown(self):
740
+ self._tmp.cleanup()
741
+
742
+ def test_temp_name_is_per_process(self):
743
+ """Two writers must not collide on the temp file itself."""
744
+ seen = []
745
+ real = os.replace
746
+
747
+ def spy(src, dst):
748
+ seen.append(Path(src).name)
749
+ return real(src, dst)
750
+
751
+ with patch.object(so.os, "replace", spy):
752
+ so._write_json_atomic(self.path, {"a": 1})
753
+ self.assertEqual(len(seen), 1)
754
+ self.assertNotEqual(seen[0], "job.json.tmp",
755
+ "shared temp name — concurrent writers will race")
756
+ self.assertTrue(seen[0].startswith("job.json.tmp"))
757
+ self.assertIn(str(os.getpid()), seen[0])
758
+ # …and unique per WRITE, not just per process: this store is written
759
+ # from a threaded server, and threads share a pid.
760
+ seen.clear()
761
+ with patch.object(so.os, "replace", spy):
762
+ so._write_json_atomic(self.path, {"a": 2})
763
+ so._write_json_atomic(self.path, {"a": 3})
764
+ self.assertEqual(len(set(seen)), 2, f"temp name reused: {seen}")
765
+
766
+ def test_transient_permission_error_is_retried(self):
767
+ calls = {"n": 0}
768
+ real = os.replace
769
+
770
+ def flaky(src, dst):
771
+ calls["n"] += 1
772
+ if calls["n"] < 3: # two transient failures, then success
773
+ raise PermissionError(5, "Access is denied")
774
+ return real(src, dst)
775
+
776
+ with patch.object(so.os, "replace", flaky):
777
+ so._write_json_atomic(self.path, {"ok": True})
778
+ self.assertEqual(calls["n"], 3)
779
+ self.assertEqual(json.loads(self.path.read_text(encoding="utf-8")),
780
+ {"ok": True})
781
+
782
+ def test_a_persistent_permission_error_still_raises(self):
783
+ """Retrying must not swallow a real, non-transient failure."""
784
+ def always(src, dst):
785
+ raise PermissionError(5, "Access is denied")
786
+
787
+ with patch.object(so.os, "replace", always):
788
+ with self.assertRaises(PermissionError):
789
+ so._write_json_atomic(self.path, {"a": 1})
790
+
791
+ def test_no_temp_file_is_left_behind_when_replace_fails(self):
792
+ def always(src, dst):
793
+ raise PermissionError(5, "Access is denied")
794
+
795
+ with patch.object(so.os, "replace", always):
796
+ with self.assertRaises(PermissionError):
797
+ so._write_json_atomic(self.path, {"a": 1})
798
+ leftovers = list(self.path.parent.glob("job.json.tmp*"))
799
+ self.assertEqual(leftovers, [], f"orphaned temp files: {leftovers}")
800
+
801
+ def test_concurrent_writers_all_publish_valid_json(self):
802
+ """The end-to-end property: N threads hammering one path never leave
803
+ the target truncated or half-written."""
804
+ import threading
805
+ errors = []
806
+
807
+ def writer(i):
808
+ try:
809
+ for _ in range(20):
810
+ so._write_json_atomic(self.path, {"writer": i})
811
+ except Exception as exc: # noqa: BLE001 — surfaced below
812
+ errors.append(exc)
813
+
814
+ threads = [threading.Thread(target=writer, args=(i,)) for i in range(6)]
815
+ for t in threads:
816
+ t.start()
817
+ for t in threads:
818
+ t.join()
819
+ self.assertEqual(errors, [])
820
+ self.assertIn("writer", json.loads(self.path.read_text(encoding="utf-8")))
821
+ self.assertEqual(list(self.path.parent.glob("job.json.tmp*")), [])
822
+
823
+
726
824
  if __name__ == "__main__":
727
825
  unittest.main()