code-context-control 2.65.0__tar.gz → 2.66.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 (394) hide show
  1. {code_context_control-2.65.0/code_context_control.egg-info → code_context_control-2.66.0}/PKG-INFO +1 -1
  2. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/_hook_utils.py +106 -3
  3. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/c3.py +271 -9
  4. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/commands/parser.py +39 -0
  5. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_access_guard.py +26 -0
  6. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_pretool_enforce.py +167 -45
  7. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_server.py +143 -0
  8. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/app.js +3 -1
  9. code_context_control-2.66.0/cli/hub_ui/components/hub_enforcement.js +342 -0
  10. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/topbar.js +1 -1
  11. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/server.py +97 -0
  12. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/app.js +4 -0
  13. code_context_control-2.66.0/cli/ui/components/enforcement.js +334 -0
  14. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/icons.js +1 -0
  15. {code_context_control-2.65.0 → code_context_control-2.66.0/code_context_control.egg-info}/PKG-INFO +1 -1
  16. {code_context_control-2.65.0 → code_context_control-2.66.0}/code_context_control.egg-info/SOURCES.txt +7 -0
  17. {code_context_control-2.65.0 → code_context_control-2.66.0}/pyproject.toml +1 -1
  18. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/access_guard.py +25 -0
  19. code_context_control-2.66.0/services/access_telemetry.py +214 -0
  20. code_context_control-2.66.0/services/enforcement_policy.py +309 -0
  21. code_context_control-2.66.0/tests/test_enforcement_policy.py +444 -0
  22. code_context_control-2.66.0/tests/test_enforcement_routes.py +163 -0
  23. code_context_control-2.66.0/tests/test_hub_enforcement_routes.py +234 -0
  24. {code_context_control-2.65.0 → code_context_control-2.66.0}/LICENSE +0 -0
  25. {code_context_control-2.65.0 → code_context_control-2.66.0}/README.md +0 -0
  26. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/__init__.py +0 -0
  27. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/commands/__init__.py +0 -0
  28. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/commands/common.py +0 -0
  29. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/docs.html +0 -0
  30. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/edits.html +0 -0
  31. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/access.html +0 -0
  32. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/bitbucket.html +0 -0
  33. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/credentials.html +0 -0
  34. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/getting-started.html +0 -0
  35. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/index.html +0 -0
  36. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/masking.html +0 -0
  37. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/oracle.html +0 -0
  38. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/shared.css +0 -0
  39. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/tools.html +0 -0
  40. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/workflow.html +0 -0
  41. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_artifact.py +0 -0
  42. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_auto_snapshot.py +0 -0
  43. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_c3_signal.py +0 -0
  44. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_c3read.py +0 -0
  45. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_dispatch.py +0 -0
  46. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_edit_ledger.py +0 -0
  47. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_edit_unlock.py +0 -0
  48. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_filter.py +0 -0
  49. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_ghost_files.py +0 -0
  50. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_prompt_recall.py +0 -0
  51. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_read.py +0 -0
  52. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_session_stats.py +0 -0
  53. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_terse_advisor.py +0 -0
  54. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/add_project.js +0 -0
  55. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/config_editor.js +0 -0
  56. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/drill_artifacts.js +0 -0
  57. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/drill_health.js +0 -0
  58. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/drill_panel.js +0 -0
  59. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/drill_subprojects.js +0 -0
  60. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/drill_tasks.js +0 -0
  61. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/drill_views.js +0 -0
  62. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/global_search.js +0 -0
  63. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/hub_credentials.js +0 -0
  64. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/hub_locks.js +0 -0
  65. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/mcp_manager.js +0 -0
  66. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/modals.js +0 -0
  67. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/project_card.js +0 -0
  68. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/project_tree.js +0 -0
  69. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/session_drawer.js +0 -0
  70. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/settings_modal.js +0 -0
  71. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/sidebar.js +0 -0
  72. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/summary_bar.js +0 -0
  73. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/task_board.js +0 -0
  74. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/toasts.js +0 -0
  75. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/state.js +0 -0
  76. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui.html +0 -0
  77. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/mcp_proxy.py +0 -0
  78. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/mcp_server.py +0 -0
  79. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/progress.py +0 -0
  80. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/__init__.py +0 -0
  81. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/_helpers.py +0 -0
  82. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/agent.py +0 -0
  83. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/artifacts.py +0 -0
  84. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/bitbucket.py +0 -0
  85. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/compress.py +0 -0
  86. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/credentials.py +0 -0
  87. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/delegate.py +0 -0
  88. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/edit.py +0 -0
  89. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/edits.py +0 -0
  90. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/federate.py +0 -0
  91. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/filter.py +0 -0
  92. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/impact.py +0 -0
  93. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/jira.py +0 -0
  94. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/locks.py +0 -0
  95. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/memory.py +0 -0
  96. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/project.py +0 -0
  97. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/read.py +0 -0
  98. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/search.py +0 -0
  99. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/session.py +0 -0
  100. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/shell.py +0 -0
  101. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/status.py +0 -0
  102. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/tasks.py +0 -0
  103. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/validate.py +0 -0
  104. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/api.js +0 -0
  105. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/access.js +0 -0
  106. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/bitbucket.js +0 -0
  107. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/chat.js +0 -0
  108. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/credentials.js +0 -0
  109. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/dashboard.js +0 -0
  110. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/edits.js +0 -0
  111. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/instructions.js +0 -0
  112. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/jira.js +0 -0
  113. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/memory.js +0 -0
  114. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/sessions.js +0 -0
  115. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/settings.js +0 -0
  116. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/sidebar.js +0 -0
  117. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/tasks.js +0 -0
  118. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/pm_shared.js +0 -0
  119. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/shared.js +0 -0
  120. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/theme.js +0 -0
  121. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui.html +0 -0
  122. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui_legacy.html +0 -0
  123. {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui_nano.html +0 -0
  124. {code_context_control-2.65.0 → code_context_control-2.66.0}/code_context_control.egg-info/dependency_links.txt +0 -0
  125. {code_context_control-2.65.0 → code_context_control-2.66.0}/code_context_control.egg-info/entry_points.txt +0 -0
  126. {code_context_control-2.65.0 → code_context_control-2.66.0}/code_context_control.egg-info/requires.txt +0 -0
  127. {code_context_control-2.65.0 → code_context_control-2.66.0}/code_context_control.egg-info/top_level.txt +0 -0
  128. {code_context_control-2.65.0 → code_context_control-2.66.0}/core/__init__.py +0 -0
  129. {code_context_control-2.65.0 → code_context_control-2.66.0}/core/config.py +0 -0
  130. {code_context_control-2.65.0 → code_context_control-2.66.0}/core/ide.py +0 -0
  131. {code_context_control-2.65.0 → code_context_control-2.66.0}/core/mcp_toml.py +0 -0
  132. {code_context_control-2.65.0 → code_context_control-2.66.0}/core/web_security.py +0 -0
  133. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/__init__.py +0 -0
  134. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/config.py +0 -0
  135. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/mcp_oracle.py +0 -0
  136. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/oracle_server.py +0 -0
  137. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/oracle_ui.html +0 -0
  138. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/__init__.py +0 -0
  139. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/activity_reporter.py +0 -0
  140. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/api_auth.py +0 -0
  141. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/c3_bridge.py +0 -0
  142. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/chat_engine.py +0 -0
  143. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/chat_store.py +0 -0
  144. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/cross_memory.py +0 -0
  145. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/discovery_audit.py +0 -0
  146. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/federated_graph.py +0 -0
  147. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/health_checker.py +0 -0
  148. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/insight_engine.py +0 -0
  149. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/local_session.py +0 -0
  150. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/memory_reader.py +0 -0
  151. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/memory_writer.py +0 -0
  152. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/project_scanner.py +0 -0
  153. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/review_agent.py +0 -0
  154. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/tool_executor.py +0 -0
  155. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/tool_registry.py +0 -0
  156. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/activity.js +0 -0
  157. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/agents.js +0 -0
  158. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/app.js +0 -0
  159. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/busy.js +0 -0
  160. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/chat/conversations.js +0 -0
  161. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/chat/input.js +0 -0
  162. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/chat/markdown.js +0 -0
  163. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/chat/send.js +0 -0
  164. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/chat/stream_renderer.js +0 -0
  165. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/chat/toolbar.js +0 -0
  166. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/core.js +0 -0
  167. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/crossgraph.js +0 -0
  168. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/header.js +0 -0
  169. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/insights.js +0 -0
  170. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/projects.js +0 -0
  171. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/settings.js +0 -0
  172. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/suggestions.js +0 -0
  173. {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/theme_tabs.js +0 -0
  174. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/__init__.py +0 -0
  175. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/activity_log.py +0 -0
  176. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/agent_base.py +0 -0
  177. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/agent_locks.py +0 -0
  178. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/agents.py +0 -0
  179. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/artifact_defs.py +0 -0
  180. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/artifact_store.py +0 -0
  181. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/auto_memory.py +0 -0
  182. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/bench/__init__.py +0 -0
  183. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/bench/external/__init__.py +0 -0
  184. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/bench/external/aider_polyglot.py +0 -0
  185. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/bench/external/swe_bench.py +0 -0
  186. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/benchmark_dashboard.py +0 -0
  187. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/bitbucket_client.py +0 -0
  188. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/bitbucket_credentials.py +0 -0
  189. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/circuit_breaker.py +0 -0
  190. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/claude_md.py +0 -0
  191. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/compressor.py +0 -0
  192. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/context_snapshot.py +0 -0
  193. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/conversation_store.py +0 -0
  194. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/credential_store.py +0 -0
  195. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/doc_index.py +0 -0
  196. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/e2e_benchmark.py +0 -0
  197. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/e2e_evaluator.py +0 -0
  198. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/e2e_tasks.py +0 -0
  199. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/edit_ledger.py +0 -0
  200. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/embedding_index.py +0 -0
  201. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/error_reporting.py +0 -0
  202. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/file_memory.py +0 -0
  203. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/git_context.py +0 -0
  204. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/hub_service.py +0 -0
  205. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/indexer.py +0 -0
  206. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/jira_client.py +0 -0
  207. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/jira_cloud.py +0 -0
  208. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/jira_credentials.py +0 -0
  209. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/jira_data_center.py +0 -0
  210. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/jira_links.py +0 -0
  211. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/mask_activation.py +0 -0
  212. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/mask_mirror.py +0 -0
  213. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/mask_presets.py +0 -0
  214. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/memory.py +0 -0
  215. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/memory_consolidator.py +0 -0
  216. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/memory_distiller.py +0 -0
  217. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/memory_graph.py +0 -0
  218. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/memory_grounder.py +0 -0
  219. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/memory_queue.py +0 -0
  220. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/memory_scorer.py +0 -0
  221. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/metrics.py +0 -0
  222. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/notifications.py +0 -0
  223. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/ollama_bridge.py +0 -0
  224. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/ollama_client.py +0 -0
  225. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/ollama_credentials.py +0 -0
  226. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/output_filter.py +0 -0
  227. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/parser.py +0 -0
  228. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/project_manager.py +0 -0
  229. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/project_runtime.py +0 -0
  230. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/protocol.py +0 -0
  231. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/proxy_state.py +0 -0
  232. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/repo_map.py +0 -0
  233. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/retention.py +0 -0
  234. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/retrieval_broker.py +0 -0
  235. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/router.py +0 -0
  236. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/runtime.py +0 -0
  237. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/scanner.py +0 -0
  238. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/session_benchmark.py +0 -0
  239. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/session_manager.py +0 -0
  240. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/session_preloader.py +0 -0
  241. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/subprojects.py +0 -0
  242. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/task_store.py +0 -0
  243. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/telemetry.py +0 -0
  244. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/text_index.py +0 -0
  245. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/time_tracker.py +0 -0
  246. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/tool_classifier.py +0 -0
  247. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/transcript_index.py +0 -0
  248. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/validation_cache.py +0 -0
  249. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/vector_store.py +0 -0
  250. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/watcher.py +0 -0
  251. {code_context_control-2.65.0 → code_context_control-2.66.0}/services/win_subprocess.py +0 -0
  252. {code_context_control-2.65.0 → code_context_control-2.66.0}/setup.cfg +0 -0
  253. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_access_builtin_optout.py +0 -0
  254. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_access_guard.py +0 -0
  255. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_access_guard_meta.py +0 -0
  256. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_access_guard_shell_project.py +0 -0
  257. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_access_guard_surfaces.py +0 -0
  258. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_access_guard_wiring.py +0 -0
  259. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_activity_reporter.py +0 -0
  260. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_agent_locks.py +0 -0
  261. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_aider_polyglot.py +0 -0
  262. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_antigravity_transition.py +0 -0
  263. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_artifact_store.py +0 -0
  264. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_artifact_tool.py +0 -0
  265. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_bitbucket_cli_smoke.py +0 -0
  266. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_bitbucket_client.py +0 -0
  267. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_bitbucket_config_fallback.py +0 -0
  268. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_bitbucket_credentials.py +0 -0
  269. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_bitbucket_tool.py +0 -0
  270. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_c3_bridge_project_artifacts.py +0 -0
  271. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_c3_shell.py +0 -0
  272. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_circuit_breaker.py +0 -0
  273. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_claude_md_merge.py +0 -0
  274. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_cli_smoke.py +0 -0
  275. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_compressor_large_file.py +0 -0
  276. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_credential_store.py +0 -0
  277. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_credentials_routes.py +0 -0
  278. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_credentials_tool.py +0 -0
  279. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_creds_cli_smoke.py +0 -0
  280. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_delegate_cascade.py +0 -0
  281. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_e2e_benchmark.py +0 -0
  282. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_edit_ledger_hook.py +0 -0
  283. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_edit_locking.py +0 -0
  284. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_edit_normalization.py +0 -0
  285. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_enforcement_flip.py +0 -0
  286. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_federated_graph.py +0 -0
  287. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_filter_backoff.py +0 -0
  288. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_ghost_files.py +0 -0
  289. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_ghost_generation.py +0 -0
  290. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_git_branch_awareness.py +0 -0
  291. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hook_access_guard.py +0 -0
  292. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hook_dispatch.py +0 -0
  293. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hook_pretool_enforce.py +0 -0
  294. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hook_prompt_recall.py +0 -0
  295. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hook_smoke.py +0 -0
  296. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hook_state.py +0 -0
  297. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hook_utf8_stdio.py +0 -0
  298. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hub_credentials_route.py +0 -0
  299. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hub_credentials_routes_write.py +0 -0
  300. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hub_inspect_api.py +0 -0
  301. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hub_locks_routes.py +0 -0
  302. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hub_server_smoke.py +0 -0
  303. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hub_subproject_links.py +0 -0
  304. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_ide_workflow_adaptation.py +0 -0
  305. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_init_scan.py +0 -0
  306. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_install_mcp_entrypoint.py +0 -0
  307. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_jira_cli_smoke.py +0 -0
  308. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_jira_client.py +0 -0
  309. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_jira_config_fallback.py +0 -0
  310. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_jira_credentials.py +0 -0
  311. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_jira_links.py +0 -0
  312. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_jira_routes.py +0 -0
  313. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_jira_tool.py +0 -0
  314. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_lazy_store_init.py +0 -0
  315. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_mask_guard.py +0 -0
  316. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_mask_routes.py +0 -0
  317. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_mask_surfaces.py +0 -0
  318. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_mcp_host_guard.py +0 -0
  319. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_mcp_memory_timeout.py +0 -0
  320. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_mcp_server_smoke.py +0 -0
  321. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_mcp_toml.py +0 -0
  322. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_memory_distiller.py +0 -0
  323. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_memory_graph_api.py +0 -0
  324. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_memory_queue.py +0 -0
  325. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_memory_system.py +0 -0
  326. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_no_stale_model_pins.py +0 -0
  327. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_notification_dedup.py +0 -0
  328. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_notification_discipline.py +0 -0
  329. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_ollama_credentials.py +0 -0
  330. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_api_auth.py +0 -0
  331. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_apikey_api.py +0 -0
  332. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_c3_bridge.py +0 -0
  333. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_chat_engine.py +0 -0
  334. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_chat_store.py +0 -0
  335. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_discovery_api.py +0 -0
  336. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_discovery_audit.py +0 -0
  337. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_local_auth.py +0 -0
  338. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_ollama_bridge.py +0 -0
  339. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_scanner_cache.py +0 -0
  340. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_security_fixes.py +0 -0
  341. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_session_bootstrap.py +0 -0
  342. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_subproject_awareness.py +0 -0
  343. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_ui_bundle.py +0 -0
  344. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_output_filter.py +0 -0
  345. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_permissions.py +0 -0
  346. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_pm_api.py +0 -0
  347. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_project_manager.py +0 -0
  348. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_project_manager_merge.py +0 -0
  349. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_project_tool.py +0 -0
  350. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_read_coercion.py +0 -0
  351. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_read_edit_parity.py +0 -0
  352. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_related_facts.py +0 -0
  353. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_repo_map.py +0 -0
  354. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_response_boilerplate.py +0 -0
  355. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_retention.py +0 -0
  356. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_review_digest.py +0 -0
  357. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_service_durability.py +0 -0
  358. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_session_benchmark.py +0 -0
  359. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_session_budget.py +0 -0
  360. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_shell_creds.py +0 -0
  361. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_shell_robustness.py +0 -0
  362. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_subproject_exclusion.py +0 -0
  363. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_subproject_federation.py +0 -0
  364. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_subprojects.py +0 -0
  365. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_swe_bench.py +0 -0
  366. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_task_store.py +0 -0
  367. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_task_tool.py +0 -0
  368. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_time_tracker.py +0 -0
  369. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_token_telemetry.py +0 -0
  370. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_tool_registry.py +0 -0
  371. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_upgrade_and_version.py +0 -0
  372. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_validate.py +0 -0
  373. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_vault_write_guard.py +0 -0
  374. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_version_sync.py +0 -0
  375. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_web_security.py +0 -0
  376. {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_windows_reliability.py +0 -0
  377. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/__init__.py +0 -0
  378. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/backend.py +0 -0
  379. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/main.py +0 -0
  380. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/__init__.py +0 -0
  381. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/benchmark_view.py +0 -0
  382. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/claudemd_view.py +0 -0
  383. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/compress_view.py +0 -0
  384. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/index_view.py +0 -0
  385. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/init_view.py +0 -0
  386. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/mcp_view.py +0 -0
  387. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/optimize_view.py +0 -0
  388. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/pipe_view.py +0 -0
  389. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/projects_view.py +0 -0
  390. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/search_view.py +0 -0
  391. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/session_view.py +0 -0
  392. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/stats.py +0 -0
  393. {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/ui_view.py +0 -0
  394. {code_context_control-2.65.0 → code_context_control-2.66.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.65.0
3
+ Version: 2.66.0
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
@@ -96,12 +96,111 @@ def _empty_state(session_id: str = "") -> dict:
96
96
  return {"session_id": session_id or "", "last_c3_call": None, "unlocked_files": {}}
97
97
 
98
98
 
99
+ #: os.replace retries for when Windows briefly holds a handle on the target —
100
+ #: AV scanner, Search indexer, or a concurrent hook reading the same state.
101
+ _REPLACE_ATTEMPTS = 4
102
+ _REPLACE_BACKOFF_S = 0.02
103
+
104
+
99
105
  def _atomic_write_json(path: Path, data: dict) -> None:
100
- """Write JSON atomically: temp file in the same directory + os.replace."""
106
+ """Write JSON durably: temp file in the same directory + os.replace.
107
+
108
+ Three failure modes this guards against, all observed in the wild
109
+ (10 orphaned .tmp<pid> files and a truncated enforcement_state.json on a
110
+ Windows box before v2.66):
111
+
112
+ 1. **Torn write.** Without fsync, os.replace can publish a file whose
113
+ bytes are still in the OS cache. A crash or power loss then leaves a
114
+ zero-length or half-written JSON that every later read quarantines —
115
+ which silently drops all sticky unlocks and makes enforcement *more*
116
+ aggressive, not less.
117
+ 2. **Orphaned temp files.** If os.replace raised, the temp file was
118
+ simply abandoned. They accumulate forever in .c3/ and are easy to
119
+ mistake for real state.
120
+ 3. **Transient Windows sharing violations.** Another process holding a
121
+ read handle on the target makes os.replace raise PermissionError; a
122
+ single attempt turned that into a lost write.
123
+ """
124
+ import time
125
+
101
126
  path.parent.mkdir(parents=True, exist_ok=True)
102
127
  tmp = path.with_name(f"{path.name}.tmp{os.getpid()}")
103
- tmp.write_text(json.dumps(data), encoding="utf-8")
104
- os.replace(tmp, path)
128
+ try:
129
+ with open(tmp, "w", encoding="utf-8") as fh:
130
+ json.dump(data, fh)
131
+ fh.flush()
132
+ os.fsync(fh.fileno()) # durability before publish (fixes #1)
133
+
134
+ last_exc = None
135
+ for attempt in range(_REPLACE_ATTEMPTS):
136
+ try:
137
+ os.replace(tmp, path)
138
+ return
139
+ except PermissionError as exc: # fixes #3
140
+ last_exc = exc
141
+ if attempt < _REPLACE_ATTEMPTS - 1:
142
+ time.sleep(_REPLACE_BACKOFF_S * (2 ** attempt))
143
+ if last_exc is not None:
144
+ raise last_exc
145
+ finally:
146
+ # fixes #2 — a successful replace already consumed tmp, so this only
147
+ # fires on the failure paths.
148
+ try:
149
+ if tmp.exists():
150
+ tmp.unlink()
151
+ except OSError:
152
+ pass
153
+
154
+
155
+ def sweep_stale_temps(project_path: Path | None = None) -> int:
156
+ """Remove orphaned *.tmp<pid> files left by older C3 builds.
157
+
158
+ Only touches temp files whose owning PID is gone, so a concurrent hook
159
+ mid-write is never disturbed. Returns how many were removed; never raises.
160
+ """
161
+ removed = 0
162
+ try:
163
+ base = project_path if project_path is not None else Path.cwd()
164
+ c3_dir = base / ".c3"
165
+ if not c3_dir.is_dir():
166
+ return 0
167
+ for tmp in c3_dir.glob("*.tmp*"):
168
+ suffix = tmp.name.rsplit(".tmp", 1)[-1]
169
+ if not suffix.isdigit():
170
+ continue
171
+ if _pid_alive(int(suffix)):
172
+ continue
173
+ try:
174
+ tmp.unlink()
175
+ removed += 1
176
+ except OSError:
177
+ pass
178
+ except Exception:
179
+ pass
180
+ return removed
181
+
182
+
183
+ def _pid_alive(pid: int) -> bool:
184
+ """Best-effort liveness check. On error assume ALIVE (don't delete)."""
185
+ if pid <= 0:
186
+ return False
187
+ try:
188
+ if os.name == "nt":
189
+ import ctypes
190
+ # PROCESS_QUERY_LIMITED_INFORMATION — works without elevation.
191
+ handle = ctypes.windll.kernel32.OpenProcess(0x1000, False, pid)
192
+ if not handle:
193
+ return False
194
+ ctypes.windll.kernel32.CloseHandle(handle)
195
+ return True
196
+ os.kill(pid, 0)
197
+ return True
198
+ except ProcessLookupError:
199
+ return False
200
+ except PermissionError:
201
+ return True # exists but not ours
202
+ except Exception:
203
+ return True # unknown → conservative
105
204
 
106
205
 
107
206
  def _read_legacy_state(base: Path) -> dict:
@@ -163,6 +262,10 @@ def load_enforcement_state(project_path: Path | None = None, session_id: str = "
163
262
  state_path.replace(state_path.with_name(state_path.name + ".corrupt"))
164
263
  except Exception:
165
264
  pass
265
+ # Corruption is the one reliable signal that a previous write went
266
+ # wrong, so it is also the moment to clear any temp files that
267
+ # write orphaned (pre-v2.66 _atomic_write_json had no cleanup).
268
+ sweep_stale_temps(base)
166
269
  STATE_WARNINGS.append(
167
270
  "[c3:hook-error] enforcement_state: corrupted "
168
271
  f"{ENFORCEMENT_STATE_FILE} quarantined ({type(exc).__name__}); "
@@ -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.65.0"
95
+ __version__ = "2.66.0"
96
96
 
97
97
 
98
98
  def _compress_file_cli(compressor, path, mode="smart", **kw):
@@ -509,8 +509,15 @@ def _clean_stale_tools(settings_path) -> int:
509
509
 
510
510
 
511
511
  def _apply_permission_tier(project_path: str, tier: str,
512
- include_mcp_wildcard: bool = False) -> None:
513
- """Write permission tier to .claude/settings.local.json, preserving existing keys."""
512
+ include_mcp_wildcard: bool = False,
513
+ enforcement: str | None = None) -> None:
514
+ """Write permission tier to .claude/settings.local.json, preserving existing keys.
515
+
516
+ Also derives the matching tool-discipline mode (v2.66+), so a tier means
517
+ what it says: `permissive` no longer leaves every native Edit hard-denied
518
+ by the PreToolUse hook. An explicit `c3 enforce` choice is never
519
+ overwritten here — enforcement_policy.set_mode defers to set_by='user'.
520
+ """
514
521
  tier = _TIER_ALIASES.get(tier, tier)
515
522
  settings_path = Path(project_path) / ".claude" / "settings.local.json"
516
523
  settings_path.parent.mkdir(parents=True, exist_ok=True)
@@ -536,6 +543,29 @@ def _apply_permission_tier(project_path: str, tier: str,
536
543
  suffix = " (+ mcp__* wildcard)" if include_mcp_wildcard else ""
537
544
  print(f" Permissions: {tier}{suffix} — {PERMISSION_TIERS[tier]}")
538
545
 
546
+ _apply_enforcement_for_tier(project_path, tier, enforcement)
547
+
548
+
549
+ def _apply_enforcement_for_tier(project_path: str, tier: str,
550
+ explicit: str | None = None) -> None:
551
+ """Persist the tool-discipline mode implied by a tier (or an explicit one)."""
552
+ try:
553
+ from services import enforcement_policy as ep
554
+ except Exception:
555
+ return
556
+ mode = explicit or ep.derive_from_tier(tier)
557
+ set_by = ep.SET_BY_USER if explicit else ep.SET_BY_TIER
558
+ try:
559
+ result = ep.set_mode(mode, project_path, set_by=set_by, scope="project")
560
+ except ValueError as exc:
561
+ print(f" [warn] Could not set tool discipline: {exc}")
562
+ return
563
+ if result.get("deferred"):
564
+ print(f" Discipline : {result['mode']} (kept — set explicitly via "
565
+ f"`c3 enforce`; tier would have used '{mode}')")
566
+ return
567
+ print(f" Discipline : {result['mode']} — {ep.MODE_HELP[result['mode']]}")
568
+
539
569
 
540
570
  def _check_c3_health(project_path: str) -> dict:
541
571
  """Inspect an existing .c3 installation and return a health report."""
@@ -821,7 +851,7 @@ def _prompt_init_steps(project_path: str, ide_name: str, default_mode: str = "di
821
851
 
822
852
  print()
823
853
  git_choice = _prompt_choice(
824
- "Step 2/3 — Initialize a local Git repository for this project?",
854
+ "Step 2/5 — Initialize a local Git repository for this project?",
825
855
  [
826
856
  "Yes — run local git init in this folder",
827
857
  "No — leave version control untouched",
@@ -836,7 +866,7 @@ def _prompt_init_steps(project_path: str, ide_name: str, default_mode: str = "di
836
866
 
837
867
  print()
838
868
  install_choice = _prompt_choice(
839
- "Step 3/3 — Install MCP tooling for this project?",
869
+ "Step 3/5 — Install MCP tooling for this project?",
840
870
  [
841
871
  "Yes — configure the IDE and wire up C3 MCP",
842
872
  "No — skip MCP install for now",
@@ -855,13 +885,13 @@ def _prompt_init_steps(project_path: str, ide_name: str, default_mode: str = "di
855
885
  )
856
886
  mcp_mode = "proxy" if mode_choice and mode_choice.startswith("Proxy") else default_mode
857
887
 
858
- # Step 4/4 — Permissions (Claude Code only)
888
+ # Step 4/5 — Permissions (Claude Code only)
859
889
  chosen_tier = None
860
890
  include_wildcard = False
861
891
  if chosen_ide == "claude-code":
862
892
  print()
863
893
  tier_choice = _prompt_choice(
864
- "Step 4/4 — Set a Claude Code permission tier for this project?",
894
+ "Step 4/5 — Set a Claude Code permission tier for this project?",
865
895
  [
866
896
  "standard — full editing + safe shell, block destructive ops (recommended)",
867
897
  "c3-strict — c3_* MCP tools only, deny native Read/Grep/Glob/Edit/Write",
@@ -885,11 +915,45 @@ def _prompt_init_steps(project_path: str, ide_name: str, default_mode: str = "di
885
915
  )
886
916
  include_wildcard = bool(wildcard_choice and wildcard_choice.startswith("Yes"))
887
917
 
918
+ chosen_enforcement = _prompt_enforcement(chosen_tier)
919
+
888
920
  _run_install_mcp(project_path, chosen_ide, mcp_mode=mcp_mode,
889
- permissions=chosen_tier, include_mcp_wildcard=include_wildcard)
921
+ permissions=chosen_tier, include_mcp_wildcard=include_wildcard,
922
+ enforcement=chosen_enforcement)
890
923
  return chosen_ide, True
891
924
 
892
925
 
926
+ def _prompt_enforcement(chosen_tier: str | None) -> str | None:
927
+ """Step 5/5 — tool discipline. Returns an explicit mode, or None to derive.
928
+
929
+ Asked separately from the permission tier because it answers a genuinely
930
+ different question: the tier controls what the IDE will let the agent call,
931
+ this controls how hard C3's own hook pushes it toward c3_* tools. Users hit
932
+ friction here, not in the tier, and previously had no way to change it.
933
+ """
934
+ try:
935
+ from services import enforcement_policy as ep
936
+ except Exception:
937
+ return None
938
+
939
+ default_mode = ep.derive_from_tier(chosen_tier) if chosen_tier else ep.MODE_ADVISORY
940
+ print()
941
+ print(f" (Your permission tier suggests: {default_mode})")
942
+ choice = _prompt_choice(
943
+ "Step 5/5 — Tool discipline: how hard should C3 push toward c3_* tools?",
944
+ [
945
+ "advisory — allow native Edit/Write with a nudge; ledger still logs (recommended)",
946
+ "strict — block native Edit/Write until a c3_* call runs first",
947
+ "off — no nudging (Access Guard + vault guard still enforce)",
948
+ f"Use tier default ({default_mode})",
949
+ ],
950
+ )
951
+ if not choice or choice.startswith("Use tier default"):
952
+ return None
953
+ mode = choice.split()[0]
954
+ return mode if mode in ep.MODES else None
955
+
956
+
893
957
  def _parse_cli_ide_arg(value: str) -> str:
894
958
  """Parse public CLI IDE names while still accepting legacy aliases."""
895
959
  raw = (value or "").strip().lower()
@@ -1033,6 +1097,7 @@ def cmd_init(args):
1033
1097
  mcp_mode=getattr(args, "mcp_mode", "direct"),
1034
1098
  permissions=getattr(args, "permissions", None),
1035
1099
  include_mcp_wildcard=bool(getattr(args, "include_mcp_wildcard", False)),
1100
+ enforcement=getattr(args, "enforcement", None),
1036
1101
  )
1037
1102
  else:
1038
1103
  _prompt_init_steps(project_path, requested_ide, default_mode=getattr(args, "mcp_mode", "direct"))
@@ -1112,6 +1177,35 @@ def cmd_init(args):
1112
1177
  except Exception:
1113
1178
  pass
1114
1179
 
1180
+ # Tool discipline (Layer C) — the knob users actually feel day to day.
1181
+ try:
1182
+ from services import enforcement_policy as _ep
1183
+ _pol = _ep.resolve(project_path)
1184
+ _bits = [_pol.mode]
1185
+ if _pol.scope == "default":
1186
+ _bits.append("default — never set")
1187
+ elif _pol.set_by:
1188
+ _bits.append(f"set by {_pol.set_by}")
1189
+ _stored_tier = _safe_read_json(
1190
+ c3_dir / "config.json", "config").get("permission_tier")
1191
+ if _stored_tier and _ep.derive_from_tier(_stored_tier) != _pol.mode:
1192
+ _bits.append(f"tier '{_stored_tier}' implies "
1193
+ f"'{_ep.derive_from_tier(_stored_tier)}'")
1194
+ print(f" Disc : {', '.join(_bits)} (change: c3 enforce <mode>)")
1195
+ for _w in _pol.warnings:
1196
+ print(f" [warn] {_w}")
1197
+ except Exception:
1198
+ pass
1199
+
1200
+ # Orphaned hook temp files (pre-v2.66 atomic-write bug).
1201
+ try:
1202
+ from cli._hook_utils import sweep_stale_temps as _sweep
1203
+ _swept = _sweep(Path(project_path))
1204
+ if _swept:
1205
+ print(f" Fixed : removed {_swept} orphaned .c3 temp file(s)")
1206
+ except Exception:
1207
+ pass
1208
+
1115
1209
  # Delegate backends
1116
1210
  try:
1117
1211
  from services.ollama_client import OllamaClient as _OC
@@ -4935,6 +5029,7 @@ def _cmd_permissions_clean(settings_path: Path) -> None:
4935
5029
  def _run_install_mcp(project_path: str, ide_name: str, mcp_mode: str = "direct",
4936
5030
  permissions: str | None = None,
4937
5031
  include_mcp_wildcard: bool = False,
5032
+ enforcement: str | None = None,
4938
5033
  banner: str = "Installing MCP tools...") -> None:
4939
5034
  """Run install-mcp programmatically with a consistent banner."""
4940
5035
  print(f"\n{banner}")
@@ -4942,6 +5037,7 @@ def _run_install_mcp(project_path: str, ide_name: str, mcp_mode: str = "direct",
4942
5037
  cmd_install_mcp(SimpleNamespace(
4943
5038
  project_path=project_path, ide=ide_name, mcp_mode=mcp_mode,
4944
5039
  permissions=permissions, include_mcp_wildcard=include_mcp_wildcard,
5040
+ enforcement=enforcement,
4945
5041
  ))
4946
5042
 
4947
5043
 
@@ -5346,6 +5442,14 @@ def cmd_install_mcp(args):
5346
5442
  with open(c3_config_path, "w", encoding="utf-8") as f:
5347
5443
  json.dump(_c3cfg, f, indent=2)
5348
5444
 
5445
+ # Tool discipline (Layer C) rides along with the tier so `permissive`
5446
+ # actually means permissive at the hook layer too. An explicit
5447
+ # --enforcement wins; otherwise the tier derives it.
5448
+ _enf = getattr(args, "enforcement", None)
5449
+ if (perm_tier and perm_tier in PERMISSION_TIERS) or _enf:
5450
+ _apply_enforcement_for_tier(
5451
+ str(target), perm_tier or "standard", _enf)
5452
+
5349
5453
  with open(settings_path, 'w', encoding="utf-8") as f:
5350
5454
  json.dump(settings, f, indent=2)
5351
5455
 
@@ -6002,13 +6106,117 @@ def _locks_audit(target: str, res: dict, project_path: str, note: str) -> None:
6002
6106
  pass
6003
6107
 
6004
6108
 
6109
+ def cmd_enforce(args):
6110
+ """`c3 enforce [strict|advisory|off]` — the Layer C knob.
6111
+
6112
+ Deliberately a separate command from `c3 access`: that one governs which
6113
+ PATHS the agent may touch (a security boundary), this one governs how hard
6114
+ C3 pushes it toward c3_* tools (a workflow preference). Conflating them is
6115
+ what made "the guard is slowing me down" unfixable without weakening
6116
+ something that should not be weakened.
6117
+ """
6118
+ from services import enforcement_policy as ep
6119
+
6120
+ project_path = str(Path(getattr(args, "project_path", ".") or ".").resolve())
6121
+ mode = getattr(args, "mode", None)
6122
+ scope = "global" if getattr(args, "use_global", False) else "project"
6123
+ ttl = getattr(args, "signal_ttl", None)
6124
+
6125
+ if not mode and ttl is None:
6126
+ _enforce_show(project_path, ep)
6127
+ return
6128
+
6129
+ current = ep.resolve(project_path)
6130
+ try:
6131
+ result = ep.set_mode(mode or current.mode, project_path,
6132
+ set_by=ep.SET_BY_USER, scope=scope,
6133
+ signal_ttl_s=ttl)
6134
+ except ValueError as exc:
6135
+ print(f"Error: {exc}")
6136
+ return
6137
+
6138
+ print_header("Tool discipline")
6139
+ print(f" Mode : {result['mode']} — {ep.MODE_HELP[result['mode']]}")
6140
+ print(f" Scope : {result['scope']} ({result['path']})")
6141
+ if ttl is not None:
6142
+ print(f" Signal TTL: {ttl}s")
6143
+ if not result["changed"] and mode:
6144
+ print(" (already set to this mode)")
6145
+
6146
+ if result["mode"] == ep.MODE_OFF:
6147
+ print("\n Still enforced regardless of this setting:")
6148
+ print(" - Access Guard path rules (c3 access list)")
6149
+ print(" - Credential vault guard (.c3/secrets.enc, cred_state.json)")
6150
+ print(" - Agent locks (c3 locks list)")
6151
+ if result["mode"] != ep.MODE_STRICT:
6152
+ print("\n The edit ledger still records native writes (PostToolUse).")
6153
+ print(" What you lose vs strict: the pre-edit snapshot c3_edit takes.")
6154
+
6155
+ _enforce_audit(result, project_path)
6156
+
6157
+
6158
+ def _enforce_show(project_path: str, ep) -> None:
6159
+ """Print the effective policy plus the tier it would be derived from."""
6160
+ policy = ep.resolve(project_path)
6161
+ print_header("Tool discipline")
6162
+ print(f" Mode : {policy.describe()}")
6163
+ print(f" Signal TTL: {policy.signal_ttl_s}s")
6164
+ blocked = ", ".join(sorted(policy.blocked_tools)) or "(none)"
6165
+ print(f" Blocks: {blocked}" if policy.blocks_writes
6166
+ else f" Blocks: nothing (would be {blocked} under strict)")
6167
+
6168
+ cfg = _safe_read_json(Path(project_path) / ".c3" / "config.json", "config")
6169
+ tier = cfg.get("permission_tier")
6170
+ if tier:
6171
+ derived = ep.derive_from_tier(tier)
6172
+ note = "" if derived == policy.mode else " <- differs from active mode"
6173
+ print(f" Tier : {tier} would derive '{derived}'{note}")
6174
+
6175
+ for warn in policy.warnings:
6176
+ print(f" [warn] {warn}")
6177
+
6178
+ print("\n Modes:")
6179
+ for name in ep.MODES:
6180
+ marker = "*" if name == policy.mode else " "
6181
+ print(f" {marker} {name:9s} {ep.MODE_HELP[name]}")
6182
+ print("\n Change: c3 enforce advisory | see cost: c3 access stats")
6183
+
6184
+
6185
+ def _enforce_audit(result: dict, project_path: str) -> None:
6186
+ """Ledger + activity entry — enforcement changes are security-adjacent."""
6187
+ try:
6188
+ from services.activity_log import ActivityLog
6189
+ ActivityLog(project_path).log("access_action", {
6190
+ "kind": "enforcement", "action": "set_mode",
6191
+ "mode": result["mode"], "previous": result.get("previous", ""),
6192
+ "scope": result["scope"], "set_by": result["set_by"],
6193
+ })
6194
+ except Exception:
6195
+ pass
6196
+ try:
6197
+ from services.edit_ledger import EditLedger
6198
+ EditLedger(project_path).log_edit(
6199
+ file=f"enforcement://{result['scope']}",
6200
+ change_type="enforcement_set_mode",
6201
+ summary=(f"tool discipline {result.get('previous') or 'default'} "
6202
+ f"-> {result['mode']} ({result['scope']}) via `c3 enforce`"),
6203
+ tags=["enforcement", "access"],
6204
+ detail={"kind": "enforcement", "mode": result["mode"],
6205
+ "previous": result.get("previous", ""),
6206
+ "scope": result["scope"], "set_by": result["set_by"]},
6207
+ )
6208
+ except Exception:
6209
+ pass
6210
+
6211
+
6005
6212
  def cmd_access(args):
6006
6213
  """Access Guard rule management — human-only mutation surface (spec §1)."""
6007
6214
  sub = getattr(args, "access_cmd", None)
6008
6215
  if not sub:
6009
- print("Usage: c3 access {list,add,remove,check,mask,builtin} [args]")
6216
+ print("Usage: c3 access {list,add,remove,check,stats,mask,builtin} [args]")
6010
6217
  print(" c3 access mask {add,rm,status,activate,preview} [args]")
6011
6218
  print(" c3 access builtin {disable,enable} <glob>")
6219
+ print(" c3 access stats [--session ID] [--clear] [--json]")
6012
6220
  return
6013
6221
 
6014
6222
  project_path = getattr(args, "project_path", ".") or "."
@@ -6021,6 +6229,8 @@ def cmd_access(args):
6021
6229
  _access_cmd_remove(args, project_path)
6022
6230
  elif sub == "check":
6023
6231
  _access_cmd_check(args, project_path)
6232
+ elif sub == "stats":
6233
+ _access_cmd_stats(args, project_path)
6024
6234
  elif sub == "mask":
6025
6235
  _access_cmd_mask(args, project_path)
6026
6236
  elif sub == "builtin":
@@ -6029,6 +6239,57 @@ def cmd_access(args):
6029
6239
  print(f"Unknown access subcommand: {sub}")
6030
6240
 
6031
6241
 
6242
+ def _access_cmd_stats(args, project_path: str) -> None:
6243
+ """`c3 access stats` — turn felt friction into a ranked, actionable list."""
6244
+ from services import access_telemetry as at
6245
+
6246
+ if getattr(args, "clear", False):
6247
+ removed = at.clear(project_path)
6248
+ print(f"[OK] Cleared denial log ({removed} file(s)).")
6249
+ return
6250
+
6251
+ agg = at.aggregate(project_path, session_id=getattr(args, "session", "") or "")
6252
+
6253
+ if getattr(args, "as_json", False):
6254
+ print(json.dumps(agg, indent=2))
6255
+ return
6256
+
6257
+ print_header("Denials")
6258
+ if not agg["total"]:
6259
+ print(" Nothing recorded. Either nothing was denied, or this C3 "
6260
+ "predates denial\n telemetry (v2.66.0) — it only logs from the "
6261
+ "first denial after upgrade.")
6262
+ return
6263
+
6264
+ by_layer = agg["by_layer"]
6265
+ print(f" {agg['total']} denial(s) across {agg['sessions']} session(s)")
6266
+ print(f" path policy (Access Guard) : {by_layer.get('access', 0)}")
6267
+ print(f" tool discipline (c3 enforce) : {by_layer.get('discipline', 0)}")
6268
+ print()
6269
+
6270
+ limit = max(1, int(getattr(args, "limit", 15) or 15))
6271
+ rows = agg["rows"][:limit]
6272
+ width = max((len(r["rule"]) for r in rows), default=8)
6273
+ print(f" {'HITS':>5} {'RULE'.ljust(width)} {'TOOL':<12} LAYER")
6274
+ for row in rows:
6275
+ print(f" {row['hits']:>5} {row['rule'].ljust(width)} "
6276
+ f"{row['tool']:<12} {row['layer']}")
6277
+
6278
+ print("\n How to unblock the top offenders:")
6279
+ seen: set = set()
6280
+ for row in rows[:5]:
6281
+ fix = at.suggest(row)
6282
+ if fix in seen:
6283
+ continue
6284
+ seen.add(fix)
6285
+ print(f" - {row['rule']}: {fix}")
6286
+
6287
+ remaining = len(agg["rows"]) - len(rows)
6288
+ if remaining > 0:
6289
+ print(f"\n ... {remaining} more (use --limit)")
6290
+ print("\n Reset counters: c3 access stats --clear")
6291
+
6292
+
6032
6293
  def _access_scope(args) -> str:
6033
6294
  return "global" if getattr(args, "use_global", False) else "project"
6034
6295
 
@@ -7732,6 +7993,7 @@ def main():
7732
7993
  "jira": cmd_jira,
7733
7994
  "creds": cmd_creds,
7734
7995
  "access": cmd_access,
7996
+ "enforce": cmd_enforce,
7735
7997
  "locks": cmd_locks,
7736
7998
  "oracle": cmd_oracle,
7737
7999
  "upgrade": cmd_upgrade,
@@ -26,6 +26,7 @@ def build_parser(version: str, parse_cli_ide_arg):
26
26
  p_init.add_argument("--git", action="store_true", help="Initialize a local Git repository during init/update")
27
27
  p_init.add_argument("--no-embed", action="store_true", help="Skip building the semantic embedding index during init")
28
28
  p_init.add_argument("--permissions", choices=["read-only", "c3-strict", "standard", "permissive"], default=None, help="Apply Claude Code permission tier (Claude Code only, used with --force)")
29
+ p_init.add_argument("--enforcement", choices=["strict", "advisory", "off"], default=None, help="Tool-discipline mode. Omit to derive from --permissions (standard->advisory, permissive->off, strict/read-only->strict)")
29
30
  p_init.add_argument("--include-mcp-wildcard", action="store_true", help="Add mcp__* wildcard so non-C3 MCP servers don't prompt per-call")
30
31
 
31
32
  p_upgrade = subparsers.add_parser("upgrade", help="Upgrade C3 to the latest PyPI release")
@@ -471,6 +472,28 @@ def build_parser(version: str, parse_cli_ide_arg):
471
472
  lk_sweep = locks_subs.add_parser("sweep", help="Drop expired leases now")
472
473
  lk_sweep.add_argument("--path", dest="project_path", default=".")
473
474
 
475
+ # ── Tool discipline / enforcement mode (v2.66.0) ────────────────────
476
+ # LAYER C: how hard C3 pushes the agent toward c3_* tools. Distinct from
477
+ # `c3 access` (path policy — a security boundary) and from
478
+ # `c3 permissions` (the IDE's own allow/deny lists).
479
+ p_enforce = subparsers.add_parser(
480
+ "enforce",
481
+ help="Tool discipline — strict | advisory | off (show current if omitted)",
482
+ )
483
+ p_enforce.add_argument(
484
+ "mode", nargs="?", default=None,
485
+ choices=["strict", "advisory", "off"],
486
+ help="strict = block native Edit/Write until a c3_* call; "
487
+ "advisory = allow with a nudge (ledger still logs); "
488
+ "off = no nudging. Omit to show the current mode.",
489
+ )
490
+ p_enforce.add_argument("--global", dest="use_global", action="store_true",
491
+ help="Write to the global scope (~/.c3) as the default for every project")
492
+ p_enforce.add_argument("--signal-ttl", type=int, default=None, dest="signal_ttl",
493
+ help="Seconds a c3_* call keeps native tools unlocked (default 600)")
494
+ p_enforce.add_argument("--path", dest="project_path", default=".",
495
+ help="Project directory (default: current)")
496
+
474
497
  # ── Access Guard (v2.62.0) ──────────────────────────────────────────
475
498
  # Human-only mutation surface (frozen spec docs/access-guard.md §1):
476
499
  # rule changes happen here or in the UI tab, never via an agent tool.
@@ -507,6 +530,22 @@ def build_parser(version: str, parse_cli_ide_arg):
507
530
  help="Operation to evaluate (default: read)")
508
531
  ac_check.add_argument("--path", dest="project_path", default=".", help="Project directory (default: current)")
509
532
 
533
+ # ── Denial telemetry (v2.66.0, docs/access-guard.md §3) ─────────────
534
+ # Answers "which rule is actually costing me time" with hit counts, and
535
+ # names the lever that would clear each one.
536
+ ac_stats = access_subs.add_parser(
537
+ "stats", help="What got denied, how often, and how to unblock it")
538
+ ac_stats.add_argument("--path", dest="project_path", default=".",
539
+ help="Project directory (default: current)")
540
+ ac_stats.add_argument("--session", default="",
541
+ help="Limit to one session id (default: all retained)")
542
+ ac_stats.add_argument("--limit", type=int, default=15,
543
+ help="Max rows to show (default: 15)")
544
+ ac_stats.add_argument("--clear", action="store_true",
545
+ help="Delete the retained denial log and exit")
546
+ ac_stats.add_argument("--json", dest="as_json", action="store_true",
547
+ help="Emit the aggregate as JSON")
548
+
510
549
  # ── Builtin opt-out (two-key) ───────────────────────────────────────
511
550
  # Builtins are on by default. Switching one off needs a config entry AND
512
551
  # a keyring attestation, so an agent that writes config.json alone still
@@ -23,6 +23,11 @@ from _hook_utils import normalize_tool_name # noqa: E402
23
23
 
24
24
  from services import access_guard as ag # noqa: E402
25
25
 
26
+ try:
27
+ from services import access_telemetry
28
+ except Exception: # pragma: no cover — telemetry is never load-bearing
29
+ access_telemetry = None
30
+
26
31
  _WRITE_TOOLS = {"Edit", "Write", "MultiEdit", "NotebookEdit"}
27
32
  _READ_TOOLS = {"Read", "SearchText"}
28
33
  _SEARCH_TOOLS = {"Grep", "Glob", "FindFiles"}
@@ -59,6 +64,23 @@ def _deny(reason: str) -> dict:
59
64
  }
60
65
 
61
66
 
67
+ def _record(denial, tool: str, operation: str, path: str,
68
+ base: str, session_id: str = "") -> None:
69
+ """Log a path-policy denial for `c3 access stats`. Best-effort."""
70
+ if access_telemetry is None or denial is None:
71
+ return
72
+ try:
73
+ access_telemetry.record(
74
+ layer=access_telemetry.LAYER_ACCESS,
75
+ rule=getattr(denial, "rule", ""),
76
+ scope=getattr(denial, "scope", ""),
77
+ tool=tool, operation=operation, path=path,
78
+ session_id=session_id, project_path=base,
79
+ )
80
+ except Exception:
81
+ pass
82
+
83
+
62
84
  def _target(tool_input: dict) -> str:
63
85
  return str(
64
86
  tool_input.get("file_path")
@@ -105,6 +127,7 @@ def run(payload: dict, project_path: Path | None = None) -> dict | None:
105
127
  tool = normalize_tool_name(payload.get("tool_name", ""))
106
128
  tool_input = payload.get("tool_input", {}) or {}
107
129
  base = str(project_path if project_path is not None else Path.cwd())
130
+ session_id = str(payload.get("session_id") or "")
108
131
 
109
132
  if tool in _WRITE_TOOLS or tool in _READ_TOOLS:
110
133
  fp = _target(tool_input)
@@ -113,6 +136,7 @@ def run(payload: dict, project_path: Path | None = None) -> dict | None:
113
136
  op = "write" if tool in _WRITE_TOOLS else "read"
114
137
  denial = ag.check(fp, op, base)
115
138
  if denial:
139
+ _record(denial, tool, op, fp, base, session_id)
116
140
  return _deny(ag.refusal(denial, fp, op, surface="hook", tool=tool))
117
141
  return None
118
142
 
@@ -123,6 +147,7 @@ def run(payload: dict, project_path: Path | None = None) -> dict | None:
123
147
  if root:
124
148
  denial = ag.check(root, "read", base)
125
149
  if denial and denial.kind == "deny":
150
+ _record(denial, tool, "read", root, base, session_id)
126
151
  return _deny(ag.refusal(denial, root, "read",
127
152
  surface="hook", tool=tool))
128
153
  footer = ag.search_footer(base)
@@ -134,6 +159,7 @@ def run(payload: dict, project_path: Path | None = None) -> dict | None:
134
159
  return None
135
160
  denial, tok = _scan_shell(cmd, base)
136
161
  if denial:
162
+ _record(denial, tool, "read", tok, base, session_id)
137
163
  return _deny(ag.refusal(denial, tok, "read",
138
164
  surface="hook", tool=tool)
139
165
  + " (best-effort shell scan)")