code-context-control 2.40.0__tar.gz → 2.42.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 (240) hide show
  1. {code_context_control-2.40.0 → code_context_control-2.42.0}/PKG-INFO +3 -3
  2. {code_context_control-2.40.0 → code_context_control-2.42.0}/README.md +2 -2
  3. code_context_control-2.42.0/cli/_hook_utils.py +304 -0
  4. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/c3.py +57 -139
  5. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/hook_auto_snapshot.py +18 -10
  6. code_context_control-2.42.0/cli/hook_c3_signal.py +68 -0
  7. code_context_control-2.42.0/cli/hook_c3read.py +108 -0
  8. code_context_control-2.42.0/cli/hook_dispatch.py +265 -0
  9. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/hook_edit_ledger.py +91 -76
  10. code_context_control-2.42.0/cli/hook_edit_unlock.py +189 -0
  11. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/hook_filter.py +58 -43
  12. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/hook_ghost_files.py +33 -23
  13. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/hook_pretool_enforce.py +101 -106
  14. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/hook_read.py +110 -98
  15. code_context_control-2.42.0/cli/hook_session_stats.py +69 -0
  16. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/hook_terse_advisor.py +61 -49
  17. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/_helpers.py +40 -0
  18. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/delegate.py +90 -22
  19. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/read.py +22 -8
  20. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/shell.py +71 -1
  21. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/status.py +44 -3
  22. {code_context_control-2.40.0 → code_context_control-2.42.0}/code_context_control.egg-info/PKG-INFO +3 -3
  23. {code_context_control-2.40.0 → code_context_control-2.42.0}/code_context_control.egg-info/SOURCES.txt +11 -0
  24. {code_context_control-2.40.0 → code_context_control-2.42.0}/pyproject.toml +1 -1
  25. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/agents.py +18 -1
  26. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/compressor.py +166 -6
  27. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/notifications.py +101 -8
  28. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/output_filter.py +67 -8
  29. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/session_benchmark.py +195 -108
  30. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/session_manager.py +152 -10
  31. code_context_control-2.42.0/services/telemetry.py +236 -0
  32. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_c3_shell.py +75 -0
  33. code_context_control-2.42.0/tests/test_compressor_large_file.py +322 -0
  34. code_context_control-2.42.0/tests/test_delegate_cascade.py +246 -0
  35. code_context_control-2.42.0/tests/test_filter_backoff.py +156 -0
  36. code_context_control-2.42.0/tests/test_hook_dispatch.py +261 -0
  37. code_context_control-2.42.0/tests/test_hook_pretool_enforce.py +251 -0
  38. code_context_control-2.42.0/tests/test_hook_smoke.py +216 -0
  39. code_context_control-2.42.0/tests/test_hook_state.py +168 -0
  40. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_install_mcp_entrypoint.py +75 -1
  41. code_context_control-2.42.0/tests/test_notification_dedup.py +269 -0
  42. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_session_benchmark.py +30 -0
  43. code_context_control-2.42.0/tests/test_token_telemetry.py +332 -0
  44. code_context_control-2.40.0/cli/_hook_utils.py +0 -136
  45. code_context_control-2.40.0/cli/hook_c3_signal.py +0 -61
  46. code_context_control-2.40.0/cli/hook_c3read.py +0 -116
  47. code_context_control-2.40.0/cli/hook_edit_unlock.py +0 -178
  48. code_context_control-2.40.0/cli/hook_session_stats.py +0 -62
  49. {code_context_control-2.40.0 → code_context_control-2.42.0}/LICENSE +0 -0
  50. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/__init__.py +0 -0
  51. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/commands/__init__.py +0 -0
  52. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/commands/common.py +0 -0
  53. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/commands/parser.py +0 -0
  54. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/docs.html +0 -0
  55. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/edits.html +0 -0
  56. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/guide/bitbucket.html +0 -0
  57. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/guide/getting-started.html +0 -0
  58. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/guide/index.html +0 -0
  59. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/guide/oracle.html +0 -0
  60. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/guide/shared.css +0 -0
  61. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/guide/tools.html +0 -0
  62. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/guide/workflow.html +0 -0
  63. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/hub.html +0 -0
  64. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/hub_server.py +0 -0
  65. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/mcp_proxy.py +0 -0
  66. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/mcp_server.py +0 -0
  67. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/server.py +0 -0
  68. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/__init__.py +0 -0
  69. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/agent.py +0 -0
  70. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/bitbucket.py +0 -0
  71. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/compress.py +0 -0
  72. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/edit.py +0 -0
  73. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/edits.py +0 -0
  74. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/filter.py +0 -0
  75. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/impact.py +0 -0
  76. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/memory.py +0 -0
  77. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/project.py +0 -0
  78. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/search.py +0 -0
  79. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/session.py +0 -0
  80. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/tools/validate.py +0 -0
  81. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui/api.js +0 -0
  82. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui/app.js +0 -0
  83. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui/components/bitbucket.js +0 -0
  84. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui/components/chat.js +0 -0
  85. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui/components/dashboard.js +0 -0
  86. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui/components/edits.js +0 -0
  87. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui/components/instructions.js +0 -0
  88. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui/components/memory.js +0 -0
  89. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui/components/sessions.js +0 -0
  90. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui/components/settings.js +0 -0
  91. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui/components/sidebar.js +0 -0
  92. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui/icons.js +0 -0
  93. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui/shared.js +0 -0
  94. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui/theme.js +0 -0
  95. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui.html +0 -0
  96. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui_legacy.html +0 -0
  97. {code_context_control-2.40.0 → code_context_control-2.42.0}/cli/ui_nano.html +0 -0
  98. {code_context_control-2.40.0 → code_context_control-2.42.0}/code_context_control.egg-info/dependency_links.txt +0 -0
  99. {code_context_control-2.40.0 → code_context_control-2.42.0}/code_context_control.egg-info/entry_points.txt +0 -0
  100. {code_context_control-2.40.0 → code_context_control-2.42.0}/code_context_control.egg-info/requires.txt +0 -0
  101. {code_context_control-2.40.0 → code_context_control-2.42.0}/code_context_control.egg-info/top_level.txt +0 -0
  102. {code_context_control-2.40.0 → code_context_control-2.42.0}/core/__init__.py +0 -0
  103. {code_context_control-2.40.0 → code_context_control-2.42.0}/core/config.py +0 -0
  104. {code_context_control-2.40.0 → code_context_control-2.42.0}/core/ide.py +0 -0
  105. {code_context_control-2.40.0 → code_context_control-2.42.0}/core/mcp_toml.py +0 -0
  106. {code_context_control-2.40.0 → code_context_control-2.42.0}/core/web_security.py +0 -0
  107. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/__init__.py +0 -0
  108. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/config.py +0 -0
  109. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/mcp_oracle.py +0 -0
  110. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/oracle.html +0 -0
  111. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/oracle_server.py +0 -0
  112. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/__init__.py +0 -0
  113. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/activity_reporter.py +0 -0
  114. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/api_auth.py +0 -0
  115. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/c3_bridge.py +0 -0
  116. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/chat_engine.py +0 -0
  117. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/chat_store.py +0 -0
  118. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/cross_memory.py +0 -0
  119. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/federated_graph.py +0 -0
  120. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/health_checker.py +0 -0
  121. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/insight_engine.py +0 -0
  122. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/memory_reader.py +0 -0
  123. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/memory_writer.py +0 -0
  124. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/ollama_bridge.py +0 -0
  125. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/project_scanner.py +0 -0
  126. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/review_agent.py +0 -0
  127. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/tool_executor.py +0 -0
  128. {code_context_control-2.40.0 → code_context_control-2.42.0}/oracle/services/tool_registry.py +0 -0
  129. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/__init__.py +0 -0
  130. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/activity_log.py +0 -0
  131. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/agent_base.py +0 -0
  132. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/auto_memory.py +0 -0
  133. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/bench/__init__.py +0 -0
  134. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/bench/external/__init__.py +0 -0
  135. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/bench/external/aider_polyglot.py +0 -0
  136. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/bench/external/swe_bench.py +0 -0
  137. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/benchmark_dashboard.py +0 -0
  138. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/bitbucket_client.py +0 -0
  139. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/bitbucket_credentials.py +0 -0
  140. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/circuit_breaker.py +0 -0
  141. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/claude_md.py +0 -0
  142. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/context_snapshot.py +0 -0
  143. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/conversation_store.py +0 -0
  144. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/doc_index.py +0 -0
  145. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/e2e_benchmark.py +0 -0
  146. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/e2e_evaluator.py +0 -0
  147. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/e2e_tasks.py +0 -0
  148. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/edit_ledger.py +0 -0
  149. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/embedding_index.py +0 -0
  150. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/error_reporting.py +0 -0
  151. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/file_memory.py +0 -0
  152. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/git_context.py +0 -0
  153. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/hub_service.py +0 -0
  154. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/indexer.py +0 -0
  155. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/memory.py +0 -0
  156. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/memory_consolidator.py +0 -0
  157. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/memory_graph.py +0 -0
  158. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/memory_grounder.py +0 -0
  159. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/memory_scorer.py +0 -0
  160. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/metrics.py +0 -0
  161. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/ollama_client.py +0 -0
  162. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/parser.py +0 -0
  163. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/project_manager.py +0 -0
  164. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/project_runtime.py +0 -0
  165. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/protocol.py +0 -0
  166. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/proxy_state.py +0 -0
  167. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/retrieval_broker.py +0 -0
  168. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/router.py +0 -0
  169. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/runtime.py +0 -0
  170. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/session_preloader.py +0 -0
  171. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/text_index.py +0 -0
  172. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/tool_classifier.py +0 -0
  173. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/transcript_index.py +0 -0
  174. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/validation_cache.py +0 -0
  175. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/vector_store.py +0 -0
  176. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/version_tracker.py +0 -0
  177. {code_context_control-2.40.0 → code_context_control-2.42.0}/services/watcher.py +0 -0
  178. {code_context_control-2.40.0 → code_context_control-2.42.0}/setup.cfg +0 -0
  179. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_activity_reporter.py +0 -0
  180. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_aider_polyglot.py +0 -0
  181. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_bitbucket_cli_smoke.py +0 -0
  182. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_bitbucket_client.py +0 -0
  183. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_bitbucket_config_fallback.py +0 -0
  184. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_bitbucket_credentials.py +0 -0
  185. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_bitbucket_tool.py +0 -0
  186. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_circuit_breaker.py +0 -0
  187. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_claude_md_merge.py +0 -0
  188. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_cli_smoke.py +0 -0
  189. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_e2e_benchmark.py +0 -0
  190. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_edit_ledger_hook.py +0 -0
  191. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_edit_normalization.py +0 -0
  192. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_enforcement_flip.py +0 -0
  193. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_federated_graph.py +0 -0
  194. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_ghost_files.py +0 -0
  195. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_git_branch_awareness.py +0 -0
  196. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_hub_server_smoke.py +0 -0
  197. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_lazy_store_init.py +0 -0
  198. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_mcp_host_guard.py +0 -0
  199. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_mcp_server_smoke.py +0 -0
  200. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_mcp_toml.py +0 -0
  201. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_memory_graph_api.py +0 -0
  202. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_memory_system.py +0 -0
  203. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_notification_discipline.py +0 -0
  204. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_oracle_api_auth.py +0 -0
  205. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_oracle_apikey_api.py +0 -0
  206. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_oracle_discovery_api.py +0 -0
  207. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_oracle_security_fixes.py +0 -0
  208. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_output_filter.py +0 -0
  209. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_permissions.py +0 -0
  210. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_project_manager.py +0 -0
  211. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_project_manager_merge.py +0 -0
  212. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_project_tool.py +0 -0
  213. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_read_coercion.py +0 -0
  214. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_service_durability.py +0 -0
  215. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_session_budget.py +0 -0
  216. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_shell_robustness.py +0 -0
  217. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_swe_bench.py +0 -0
  218. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_tool_registry.py +0 -0
  219. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_upgrade_and_version.py +0 -0
  220. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_validate.py +0 -0
  221. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_web_security.py +0 -0
  222. {code_context_control-2.40.0 → code_context_control-2.42.0}/tests/test_windows_reliability.py +0 -0
  223. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/__init__.py +0 -0
  224. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/backend.py +0 -0
  225. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/main.py +0 -0
  226. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/screens/__init__.py +0 -0
  227. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/screens/benchmark_view.py +0 -0
  228. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/screens/claudemd_view.py +0 -0
  229. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/screens/compress_view.py +0 -0
  230. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/screens/index_view.py +0 -0
  231. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/screens/init_view.py +0 -0
  232. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/screens/mcp_view.py +0 -0
  233. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/screens/optimize_view.py +0 -0
  234. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/screens/pipe_view.py +0 -0
  235. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/screens/projects_view.py +0 -0
  236. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/screens/search_view.py +0 -0
  237. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/screens/session_view.py +0 -0
  238. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/screens/stats.py +0 -0
  239. {code_context_control-2.40.0 → code_context_control-2.42.0}/tui/screens/ui_view.py +0 -0
  240. {code_context_control-2.40.0 → code_context_control-2.42.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.40.0
3
+ Version: 2.42.0
4
4
  Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer.
5
5
  Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -92,7 +92,7 @@ A thin **local** layer that sits between your IDE and your repo. Every AI tool c
92
92
 
93
93
  | Without C3 | With C3 |
94
94
  |---|---|
95
- | `Read` the whole 2,000-line file | `c3_compress` returns a 70%-smaller structural map → `c3_read(symbols=...)` for the exact function |
95
+ | `Read` the whole 2,000-line file | `c3_compress` returns a structural map at 40-70% of the original token count (30-60% smaller) → `c3_read(symbols=...)` for the exact function |
96
96
  | `Grep` the whole repo blindly | `c3_search` returns ranked candidates with TF-IDF + symbol awareness |
97
97
  | Dump full `pytest` output into the prompt | `c3_filter` distills 500 lines → 30 actionable ones |
98
98
  | Edit, hope it compiled | `c3_edit` writes via a ledger + `c3_validate` runs `pyright`/`tsc` automatically |
@@ -197,7 +197,7 @@ c3 ui # opens http://127.0.0.1:3333
197
197
  <img src="https://raw.githubusercontent.com/drknowhow/code-context-control/main/docs/screenshots/ui_dashboard.png" alt="C3 per-project dashboard" width="900">
198
198
  </p>
199
199
 
200
- Real metrics from a real project: **448K tokens saved** (89.9% rate), 208 files indexed, 20 sessions, codebase breakdown by language, current-session live counters (in/out tokens, cache reads, services online), and a stream of recent tool calls and file changes.
200
+ Illustrative example from one project's dashboard (numbers vary by project): **448K tokens saved** (89.9% rate) — C3's estimate versus a full-file-read baseline — plus 208 files indexed, 20 sessions, codebase breakdown by language, current-session live counters (in/out tokens, cache reads, services online), and a stream of recent tool calls and file changes.
201
201
 
202
202
  ### 3. Edit Ledger — every AI-driven edit tracked
203
203
 
@@ -30,7 +30,7 @@ A thin **local** layer that sits between your IDE and your repo. Every AI tool c
30
30
 
31
31
  | Without C3 | With C3 |
32
32
  |---|---|
33
- | `Read` the whole 2,000-line file | `c3_compress` returns a 70%-smaller structural map → `c3_read(symbols=...)` for the exact function |
33
+ | `Read` the whole 2,000-line file | `c3_compress` returns a structural map at 40-70% of the original token count (30-60% smaller) → `c3_read(symbols=...)` for the exact function |
34
34
  | `Grep` the whole repo blindly | `c3_search` returns ranked candidates with TF-IDF + symbol awareness |
35
35
  | Dump full `pytest` output into the prompt | `c3_filter` distills 500 lines → 30 actionable ones |
36
36
  | Edit, hope it compiled | `c3_edit` writes via a ledger + `c3_validate` runs `pyright`/`tsc` automatically |
@@ -135,7 +135,7 @@ c3 ui # opens http://127.0.0.1:3333
135
135
  <img src="https://raw.githubusercontent.com/drknowhow/code-context-control/main/docs/screenshots/ui_dashboard.png" alt="C3 per-project dashboard" width="900">
136
136
  </p>
137
137
 
138
- Real metrics from a real project: **448K tokens saved** (89.9% rate), 208 files indexed, 20 sessions, codebase breakdown by language, current-session live counters (in/out tokens, cache reads, services online), and a stream of recent tool calls and file changes.
138
+ Illustrative example from one project's dashboard (numbers vary by project): **448K tokens saved** (89.9% rate) — C3's estimate versus a full-file-read baseline — plus 208 files indexed, 20 sessions, codebase breakdown by language, current-session live counters (in/out tokens, cache reads, services online), and a stream of recent tool calls and file changes.
139
139
 
140
140
  ### 3. Edit Ledger — every AI-driven edit tracked
141
141
 
@@ -0,0 +1,304 @@
1
+ """Shared utilities for C3 hook scripts — supports Claude Code and Gemini CLI.
2
+
3
+ Also owns the consolidated enforcement state (.c3/enforcement_state.json):
4
+ a single file replacing the previous trio of last_c3_call.json,
5
+ unlocked_files.json, and ad-hoc writers spread across four hook scripts.
6
+ All hook reads/writes of enforcement state MUST go through this module.
7
+ """
8
+ import json
9
+ import os
10
+ import sys
11
+ import traceback
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+
15
+ # Max size of hook_errors.log before it is rotated (50 KB)
16
+ _LOG_MAX_BYTES = 50 * 1024
17
+
18
+ # ── Consolidated enforcement state ───────────────────────────────────────────
19
+ # Canonical file (the ONLY file written from v2.42 on):
20
+ # {
21
+ # "session_id": "<claude session id or ''>",
22
+ # "last_c3_call": {"ts": "<ISO UTC>", "tool": "c3_search", "read_unlocked": true},
23
+ # "unlocked_files": {"<resolved path>": ["read", "edit"]}
24
+ # }
25
+ # Legacy files (READ as fallback for one release; never written anymore):
26
+ ENFORCEMENT_STATE_FILE = ".c3/enforcement_state.json"
27
+ LEGACY_SIGNAL_FILE = ".c3/last_c3_call.json"
28
+ LEGACY_UNLOCK_FILE = ".c3/unlocked_files.json"
29
+
30
+ # Critical state-layer warnings (e.g. corrupted state JSON) surfaced by the
31
+ # dispatcher as an additionalContext line so enforcement never silently stops
32
+ # enforcing. Drained via drain_state_warnings().
33
+ STATE_WARNINGS: list = []
34
+
35
+
36
+ def log_hook_error(hook_name: str, exc: BaseException) -> None:
37
+ """Append a timestamped error entry to .c3/hook_errors.log.
38
+
39
+ Never raises — hook scripts must not crash the IDE even in the error logger.
40
+ Rotates the log (renames to hook_errors.log.bak) when it exceeds 50 KB.
41
+ """
42
+ try:
43
+ c3_dir = Path.cwd() / ".c3"
44
+ if not c3_dir.exists():
45
+ return
46
+ log_file = c3_dir / "hook_errors.log"
47
+ # Rotate if too large
48
+ try:
49
+ if log_file.exists() and log_file.stat().st_size > _LOG_MAX_BYTES:
50
+ log_file.replace(c3_dir / "hook_errors.log.bak")
51
+ except Exception:
52
+ pass
53
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
54
+ tb = traceback.format_exc().strip()
55
+ line = f"[{ts}] [{hook_name}] {type(exc).__name__}: {exc}\n{tb}\n---\n"
56
+ with open(log_file, "a", encoding="utf-8") as f:
57
+ f.write(line)
58
+ except Exception:
59
+ pass # Absolutely must not propagate
60
+
61
+ def drain_state_warnings() -> list:
62
+ """Return and clear accumulated critical state warnings.
63
+
64
+ Called by the dispatcher after each sub-hook so corruption events become
65
+ a visible "[c3:hook-error] ..." additionalContext line instead of a
66
+ silent enforcement gap.
67
+ """
68
+ warnings = STATE_WARNINGS[:]
69
+ STATE_WARNINGS.clear()
70
+ return warnings
71
+
72
+
73
+ def _empty_state(session_id: str = "") -> dict:
74
+ return {"session_id": session_id or "", "last_c3_call": None, "unlocked_files": {}}
75
+
76
+
77
+ def _atomic_write_json(path: Path, data: dict) -> None:
78
+ """Write JSON atomically: temp file in the same directory + os.replace."""
79
+ path.parent.mkdir(parents=True, exist_ok=True)
80
+ tmp = path.with_name(f"{path.name}.tmp{os.getpid()}")
81
+ tmp.write_text(json.dumps(data), encoding="utf-8")
82
+ os.replace(tmp, path)
83
+
84
+
85
+ def _read_legacy_state(base: Path) -> dict:
86
+ """Build a state view from the pre-v2.42 files (read-only fallback)."""
87
+ state = _empty_state()
88
+ signal_path = base / LEGACY_SIGNAL_FILE
89
+ if signal_path.exists():
90
+ try:
91
+ data = json.loads(signal_path.read_text(encoding="utf-8"))
92
+ if isinstance(data, dict) and data.get("timestamp"):
93
+ state["last_c3_call"] = {
94
+ "ts": str(data.get("timestamp")),
95
+ "tool": str(data.get("tool", "")),
96
+ "read_unlocked": bool(data.get("read_unlocked", False)),
97
+ }
98
+ except Exception:
99
+ pass # Legacy file corruption is not critical — new file supersedes it
100
+ unlock_path = base / LEGACY_UNLOCK_FILE
101
+ if unlock_path.exists():
102
+ try:
103
+ data = json.loads(unlock_path.read_text(encoding="utf-8"))
104
+ if isinstance(data, dict):
105
+ state["unlocked_files"] = {
106
+ str(k): list(v) for k, v in data.items() if isinstance(v, list)
107
+ }
108
+ except Exception:
109
+ pass
110
+ return state
111
+
112
+
113
+ def load_enforcement_state(project_path: Path | None = None, session_id: str = "") -> dict:
114
+ """Load consolidated enforcement state with legacy fallback + session scoping.
115
+
116
+ - Missing new file → read legacy last_c3_call.json / unlocked_files.json
117
+ (one-release migration path; writes only ever go to the new file).
118
+ - Corrupted new file → quarantine to *.corrupt, log, push a critical
119
+ warning to STATE_WARNINGS, and return empty state (fail-open to the
120
+ advisory path, never a hard-deny surprise).
121
+ - session_id mismatch → state written by another session is STALE:
122
+ return empty state for the current session.
123
+ """
124
+ base = project_path if project_path is not None else Path.cwd()
125
+ state_path = base / ENFORCEMENT_STATE_FILE
126
+ state = None
127
+ if state_path.exists():
128
+ try:
129
+ data = json.loads(state_path.read_text(encoding="utf-8"))
130
+ if not isinstance(data, dict):
131
+ raise ValueError("enforcement_state.json root is not an object")
132
+ state = _empty_state()
133
+ state["session_id"] = str(data.get("session_id") or "")
134
+ last_call = data.get("last_c3_call")
135
+ state["last_c3_call"] = last_call if isinstance(last_call, dict) else None
136
+ unlocked = data.get("unlocked_files")
137
+ state["unlocked_files"] = unlocked if isinstance(unlocked, dict) else {}
138
+ except Exception as exc:
139
+ log_hook_error("enforcement_state", exc)
140
+ try:
141
+ state_path.replace(state_path.with_name(state_path.name + ".corrupt"))
142
+ except Exception:
143
+ pass
144
+ STATE_WARNINGS.append(
145
+ "[c3:hook-error] enforcement_state: corrupted "
146
+ f"{ENFORCEMENT_STATE_FILE} quarantined ({type(exc).__name__}); "
147
+ "see .c3/hook_errors.log"
148
+ )
149
+ return _empty_state(session_id)
150
+ if state is None:
151
+ state = _read_legacy_state(base)
152
+ # Session scoping: hook payloads carry session_id; state from a different
153
+ # session must not grant unlocks (signal files used to survive /clear).
154
+ if session_id and state.get("session_id") and state["session_id"] != session_id:
155
+ return _empty_state(session_id)
156
+ return state
157
+
158
+
159
+ def save_enforcement_state(state: dict, project_path: Path | None = None) -> None:
160
+ """Atomically persist the consolidated enforcement state."""
161
+ base = project_path if project_path is not None else Path.cwd()
162
+ try:
163
+ _atomic_write_json(base / ENFORCEMENT_STATE_FILE, state)
164
+ except Exception as exc:
165
+ log_hook_error("enforcement_state", exc)
166
+
167
+
168
+ def record_c3_signal(
169
+ tool: str,
170
+ read_unlocked: bool,
171
+ session_id: str = "",
172
+ project_path: Path | None = None,
173
+ ) -> None:
174
+ """Record 'a c3_* tool just completed' in the consolidated state."""
175
+ state = load_enforcement_state(project_path, session_id=session_id)
176
+ if session_id:
177
+ state["session_id"] = session_id
178
+ state["last_c3_call"] = {
179
+ "ts": datetime.now(timezone.utc).isoformat(),
180
+ "tool": tool,
181
+ "read_unlocked": bool(read_unlocked),
182
+ }
183
+ save_enforcement_state(state, project_path)
184
+
185
+
186
+ def record_unlocked_files(
187
+ paths,
188
+ categories,
189
+ session_id: str = "",
190
+ project_path: Path | None = None,
191
+ ) -> None:
192
+ """Merge sticky per-file unlock categories into the consolidated state."""
193
+ cats_to_add = {c for c in categories if c}
194
+ if not cats_to_add:
195
+ return
196
+ state = load_enforcement_state(project_path, session_id=session_id)
197
+ if session_id:
198
+ state["session_id"] = session_id
199
+ changed = False
200
+ for fp in paths:
201
+ if not fp:
202
+ continue
203
+ try:
204
+ normalized = str(Path(fp).resolve())
205
+ except OSError:
206
+ continue
207
+ cats = set(state["unlocked_files"].get(normalized, []))
208
+ merged = sorted(cats | cats_to_add)
209
+ if merged != state["unlocked_files"].get(normalized):
210
+ state["unlocked_files"][normalized] = merged
211
+ changed = True
212
+ if changed:
213
+ save_enforcement_state(state, project_path)
214
+
215
+
216
+ # Map Gemini CLI built-in tool names → canonical Claude Code equivalents
217
+ GEMINI_TOOL_MAP = {
218
+ "run_shell_command": "Bash",
219
+ "read_file": "Read",
220
+ "edit_file": "Edit",
221
+ "write_file": "Write",
222
+ "list_directory": "FindFiles",
223
+ "find_files": "FindFiles",
224
+ "grep": "SearchText",
225
+ "search_in_files_content": "SearchText",
226
+ "find_in_files": "SearchText",
227
+ }
228
+
229
+
230
+ def normalize_tool_name(tool_name: str) -> str:
231
+ """Normalize Gemini CLI tool names to their Claude Code equivalents."""
232
+ return GEMINI_TOOL_MAP.get(tool_name, tool_name)
233
+
234
+
235
+ def get_tool_output(data: dict) -> tuple:
236
+ """Extract the output text and detect IDE format from hook stdin data.
237
+
238
+ Returns (output_text: str, is_gemini: bool).
239
+ Claude passes tool_response as a plain string.
240
+ Gemini wraps it in {llmContent, returnDisplay}.
241
+ """
242
+ resp = data.get("tool_response", "")
243
+ if isinstance(resp, dict):
244
+ content = resp.get("llmContent", "") or resp.get("returnDisplay", "")
245
+ if isinstance(content, list):
246
+ # llmContent can be a list of content-part dicts like {text: "..."}
247
+ content = "\n".join(
248
+ p.get("text", str(p)) if isinstance(p, dict) else str(p)
249
+ for p in content
250
+ )
251
+ return str(content) if content is not None else "", True
252
+ return resp if isinstance(resp, str) else "", False
253
+
254
+
255
+ def get_tool_input_path(data: dict) -> str:
256
+ """Extract file path from tool_input, handling Claude (file_path),
257
+ Gemini (path), and NotebookEdit (notebook_path)."""
258
+ tool_input = data.get("tool_input", {})
259
+ return (
260
+ tool_input.get("file_path", "")
261
+ or tool_input.get("path", "")
262
+ or tool_input.get("notebook_path", "")
263
+ )
264
+
265
+
266
+ def record_json_unlocks(
267
+ editable: list,
268
+ project_path: Path | None = None,
269
+ session_id: str = "",
270
+ ) -> None:
271
+ """Record file paths as read+edit unlocked in the enforcement state.
272
+
273
+ Compatibility wrapper kept for existing callers (hook_edit_unlock,
274
+ hook_c3read): unlocks now land in .c3/enforcement_state.json via the
275
+ consolidated state layer instead of the legacy unlocked_files.json.
276
+ Fails silently on I/O errors.
277
+ """
278
+ try:
279
+ record_unlocked_files(
280
+ editable, {"read", "edit"},
281
+ session_id=session_id, project_path=project_path,
282
+ )
283
+ except Exception:
284
+ pass
285
+
286
+
287
+ def emit_additional_context(text: str, is_gemini: bool) -> None:
288
+ """Write additionalContext JSON to stdout in the correct format for the IDE."""
289
+ if is_gemini:
290
+ sys.stdout.write(json.dumps({"hookSpecificOutput": {"additionalContext": text}}))
291
+ else:
292
+ sys.stdout.write(json.dumps({"additionalContext": text}))
293
+
294
+
295
+ def emit_filtered_output(filtered: str, is_gemini: bool) -> None:
296
+ """Write filtered tool output to stdout.
297
+
298
+ Claude Code: replaces the tool result entirely via tool_result.
299
+ Gemini CLI: no direct replacement — appends as additionalContext instead.
300
+ """
301
+ if is_gemini:
302
+ sys.stdout.write(json.dumps({"hookSpecificOutput": {"additionalContext": filtered}}))
303
+ else:
304
+ sys.stdout.write(json.dumps({"tool_result": filtered}))
@@ -85,7 +85,7 @@ console = Console() if HAS_RICH else None
85
85
  # Config
86
86
  CONFIG_DIR = ".c3"
87
87
  CONFIG_FILE = ".c3/config.json"
88
- __version__ = "2.40.0"
88
+ __version__ = "2.42.0"
89
89
 
90
90
 
91
91
  def _command_deps() -> CommandDeps:
@@ -4448,7 +4448,7 @@ def _uninstall_mcp_all(project_path: str):
4448
4448
  # Remove hooks
4449
4449
  hooks = settings.get("hooks", {}).get("PostToolUse", [])
4450
4450
  new_hooks = []
4451
- c3_hook_files = {"hook_filter.py", "hook_read.py", "hook_c3read.py"}
4451
+ c3_hook_files = {"hook_filter.py", "hook_read.py", "hook_c3read.py", "hook_dispatch.py"}
4452
4452
  for h in hooks:
4453
4453
  if h.get("matcher") in ("Bash", "Read", "mcp__c3__c3_read"):
4454
4454
  h["hooks"] = [hook for hook in h.get("hooks", [])
@@ -5011,17 +5011,16 @@ def cmd_install_mcp(args):
5011
5011
  # file; "cmd /c …" returns "cmd: command not found"). The single-quoted paths are
5012
5012
  # correct — bash strips them and re-quotes for cmd.exe, preserving spaces/parens.
5013
5013
  _hook_prefix = "cmd.exe /c " if sys.platform == "win32" else ""
5014
- hook_filter_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_filter.py'))}"
5015
- hook_read_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_read.py'))}"
5016
- hook_c3read_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_c3read.py'))}"
5017
- hook_enforce_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_pretool_enforce.py'))}"
5018
- hook_edit_unlock_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_edit_unlock.py'))}"
5019
- hook_edit_ledger_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_edit_ledger.py'))}"
5020
- hook_ghost_files_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_ghost_files.py'))}"
5021
- hook_session_stats_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_session_stats.py'))}"
5022
- hook_auto_snapshot_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_auto_snapshot.py'))}"
5023
- hook_terse_advisor_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_terse_advisor.py'))}"
5024
- hook_c3_signal_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_c3_signal.py'))}"
5014
+ # v2.42: single dispatcher script per hook event instead of N separate
5015
+ # per-hook commands. One interpreter spawn per event; the dispatcher
5016
+ # (cli/hook_dispatch.py) runs all applicable sub-hooks in-process.
5017
+ _dispatch_base = (
5018
+ f"{_hook_prefix}{shlex.quote(sys.executable)} "
5019
+ f"{shlex.quote(str(cli_dir / 'hook_dispatch.py'))}"
5020
+ )
5021
+ hook_pretool_cmd = f"{_dispatch_base} pretool"
5022
+ hook_posttool_cmd = f"{_dispatch_base} posttool"
5023
+ hook_stop_cmd = f"{_dispatch_base} stop"
5025
5024
 
5026
5025
  # Tool matcher names differ by IDE: Gemini uses snake_case built-in names.
5027
5026
  if profile.name == "gemini":
@@ -5045,130 +5044,48 @@ def cmd_install_mcp(args):
5045
5044
  extra_edit_matchers = ["MultiEdit", "NotebookEdit"]
5046
5045
 
5047
5046
  # ── PostToolUse hooks ──
5047
+ # Matcher set is unchanged from pre-v2.42; every matcher now points at
5048
+ # the single posttool dispatcher (which sub-hooks run for which tool
5049
+ # moved into cli/hook_dispatch.py). One spawn per event instead of
5050
+ # up to three.
5051
+ _post_matcher_names = [
5052
+ shell_matcher,
5053
+ read_matcher,
5054
+ "mcp__c3__c3_read",
5055
+ "mcp__c3__c3_shell",
5056
+ "mcp__c3__c3_search",
5057
+ "mcp__c3__c3_compress",
5058
+ "mcp__c3__c3_filter",
5059
+ "mcp__c3__c3_memory",
5060
+ "mcp__c3__c3_validate",
5061
+ "mcp__c3__c3_edit",
5062
+ "mcp__c3__c3_edits",
5063
+ "mcp__c3__c3_impact",
5064
+ "mcp__c3__c3_status",
5065
+ "mcp__c3__c3_delegate",
5066
+ "mcp__c3__c3_session",
5067
+ "mcp__c3__c3_agent",
5068
+ edit_matcher,
5069
+ write_matcher,
5070
+ *extra_edit_matchers,
5071
+ ]
5048
5072
  desired_post_hooks = [
5049
- {
5050
- "matcher": shell_matcher,
5051
- "hooks": [
5052
- {"type": "command", "command": hook_filter_cmd},
5053
- {"type": "command", "command": hook_ghost_files_cmd},
5054
- ]
5055
- },
5056
- {
5057
- "matcher": read_matcher,
5058
- "hooks": [
5059
- {"type": "command", "command": hook_read_cmd},
5060
- {"type": "command", "command": hook_ghost_files_cmd},
5061
- ]
5062
- },
5063
- {
5064
- "matcher": "mcp__c3__c3_read",
5065
- "hooks": [
5066
- {"type": "command", "command": hook_c3read_cmd},
5067
- {"type": "command", "command": hook_c3_signal_cmd},
5068
- {"type": "command", "command": hook_ghost_files_cmd},
5069
- ]
5070
- },
5071
- {
5072
- "matcher": "mcp__c3__c3_shell",
5073
- "hooks": [
5074
- {"type": "command", "command": hook_c3_signal_cmd},
5075
- {"type": "command", "command": hook_ghost_files_cmd},
5076
- ]
5077
- },
5078
- {
5079
- "matcher": "mcp__c3__c3_search",
5080
- "hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
5081
- },
5082
- {
5083
- "matcher": "mcp__c3__c3_compress",
5084
- "hooks": [
5085
- {"type": "command", "command": hook_edit_unlock_cmd},
5086
- {"type": "command", "command": hook_c3_signal_cmd},
5087
- ]
5088
- },
5089
- {
5090
- "matcher": "mcp__c3__c3_filter",
5091
- "hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
5092
- },
5093
- {
5094
- "matcher": "mcp__c3__c3_memory",
5095
- "hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
5096
- },
5097
- {
5098
- "matcher": "mcp__c3__c3_validate",
5099
- "hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
5100
- },
5101
- {
5102
- "matcher": "mcp__c3__c3_edit",
5103
- "hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
5104
- },
5105
- {
5106
- "matcher": "mcp__c3__c3_edits",
5107
- "hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
5108
- },
5109
- {
5110
- "matcher": "mcp__c3__c3_impact",
5111
- "hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
5112
- },
5113
- {
5114
- "matcher": "mcp__c3__c3_status",
5115
- "hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
5116
- },
5117
- {
5118
- "matcher": "mcp__c3__c3_delegate",
5119
- "hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
5120
- },
5121
- {
5122
- "matcher": "mcp__c3__c3_session",
5123
- "hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
5124
- },
5125
- {
5126
- "matcher": "mcp__c3__c3_agent",
5127
- "hooks": [
5128
- {"type": "command", "command": hook_edit_unlock_cmd},
5129
- {"type": "command", "command": hook_c3_signal_cmd},
5130
- ]
5131
- },
5132
- {
5133
- "matcher": edit_matcher,
5134
- "hooks": [{"type": "command", "command": hook_edit_ledger_cmd}]
5135
- },
5136
- {
5137
- "matcher": write_matcher,
5138
- "hooks": [{"type": "command", "command": hook_edit_ledger_cmd}]
5139
- },
5140
- *[
5141
- {"matcher": m, "hooks": [{"type": "command", "command": hook_edit_ledger_cmd}]}
5142
- for m in extra_edit_matchers
5143
- ],
5073
+ {"matcher": m, "hooks": [{"type": "command", "command": hook_posttool_cmd}]}
5074
+ for m in _post_matcher_names
5144
5075
  ]
5145
5076
 
5146
5077
  # ── PreToolUse hooks (enforcement — blocks native tools without prior c3_*) ──
5078
+ _pre_matcher_names = [
5079
+ read_matcher,
5080
+ grep_matcher,
5081
+ glob_matcher,
5082
+ edit_matcher,
5083
+ write_matcher,
5084
+ *extra_edit_matchers,
5085
+ ]
5147
5086
  desired_pre_hooks = [
5148
- {
5149
- "matcher": read_matcher,
5150
- "hooks": [{"type": "command", "command": hook_enforce_cmd}]
5151
- },
5152
- {
5153
- "matcher": grep_matcher,
5154
- "hooks": [{"type": "command", "command": hook_enforce_cmd}]
5155
- },
5156
- {
5157
- "matcher": glob_matcher,
5158
- "hooks": [{"type": "command", "command": hook_enforce_cmd}]
5159
- },
5160
- {
5161
- "matcher": edit_matcher,
5162
- "hooks": [{"type": "command", "command": hook_enforce_cmd}]
5163
- },
5164
- {
5165
- "matcher": write_matcher,
5166
- "hooks": [{"type": "command", "command": hook_enforce_cmd}]
5167
- },
5168
- *[
5169
- {"matcher": m, "hooks": [{"type": "command", "command": hook_enforce_cmd}]}
5170
- for m in extra_edit_matchers
5171
- ],
5087
+ {"matcher": m, "hooks": [{"type": "command", "command": hook_pretool_cmd}]}
5088
+ for m in _pre_matcher_names
5172
5089
  ]
5173
5090
 
5174
5091
  # Merge: replace existing C3 hooks (so re-running install-mcp updates commands),
@@ -5198,18 +5115,19 @@ def cmd_install_mcp(args):
5198
5115
  {
5199
5116
  "matcher": "",
5200
5117
  "hooks": [
5201
- {"type": "command", "command": hook_session_stats_cmd},
5202
- {"type": "command", "command": hook_auto_snapshot_cmd},
5203
- {"type": "command", "command": hook_terse_advisor_cmd},
5118
+ {"type": "command", "command": hook_stop_cmd},
5204
5119
  ]
5205
5120
  },
5206
5121
  ]
5207
5122
  stop_event = "Stop"
5208
5123
  # Replace only C3's own stop hooks (identified by our hook scripts) and
5209
5124
  # keep every user-added stop hook — including matcher-less ones, which
5210
- # are the normal shape for Stop hooks.
5125
+ # are the normal shape for Stop hooks. The pre-v2.42 script names stay
5126
+ # in this tuple so re-running install-mcp migrates old per-hook
5127
+ # entries to the dispatcher.
5211
5128
  _c3_stop_scripts = (
5212
5129
  "hook_session_stats.py", "hook_auto_snapshot.py", "hook_terse_advisor.py",
5130
+ "hook_dispatch.py",
5213
5131
  )
5214
5132
 
5215
5133
  def _is_c3_stop_hook(entry: dict) -> bool:
@@ -5259,9 +5177,9 @@ def cmd_install_mcp(args):
5259
5177
  json.dump(settings, f, indent=2)
5260
5178
 
5261
5179
  print(f"Wrote {settings_path}")
5262
- print(f" Hooks ({hook_event}): {shell_matcher} (filter+ghost) + {read_matcher}/{edit_matcher}/{write_matcher} (ledger) + c3_read/c3_compress/c3_agent (unlock)")
5263
- print(f" Hooks ({pre_event}): {read_matcher}/{grep_matcher}/{glob_matcher}/{edit_matcher}/{write_matcher} (c3 enforcement)")
5264
- print(" Hooks (Stop): session_stats + auto_snapshot")
5180
+ print(f" Hooks ({hook_event}): dispatcher (1 spawn/event) filter/ghost/read-guard/ledger/unlock/signal via cli/hook_dispatch.py posttool")
5181
+ print(f" Hooks ({pre_event}): dispatcher — {read_matcher}/{grep_matcher}/{glob_matcher}/{edit_matcher}/{write_matcher} (c3 enforcement)")
5182
+ print(" Hooks (Stop): dispatcher — session_stats + auto_snapshot + terse_advisor")
5265
5183
  if profile.name == "claude-code":
5266
5184
  print(" Claude MCP prompt settings enabled for this project")
5267
5185
  if perm_tier and profile.name == "claude-code":
@@ -52,9 +52,11 @@ def _call_server(port: int, stop_hook_data: dict) -> bool:
52
52
  return False
53
53
 
54
54
 
55
- def _fallback_snapshot(stop_hook_data: dict) -> None:
55
+ def _fallback_snapshot(stop_hook_data: dict, base: Path | None = None) -> None:
56
56
  """Lightweight file-based snapshot when the UI server is not running."""
57
- c3_dir = Path(".c3")
57
+ if base is None:
58
+ base = Path.cwd()
59
+ c3_dir = base / ".c3"
58
60
  if not c3_dir.exists():
59
61
  return
60
62
 
@@ -116,6 +118,19 @@ def _fallback_snapshot(stop_hook_data: dict) -> None:
116
118
  json.dump(snapshot, f, indent=2)
117
119
 
118
120
 
121
+ def run(payload: dict, project_path: Path | None = None):
122
+ """Core logic — importable by the dispatcher and tests. Returns None."""
123
+ base = project_path if project_path is not None else Path.cwd()
124
+ port = _find_server_port(str(base))
125
+
126
+ if port and _call_server(port, payload):
127
+ return None
128
+
129
+ # Server not running or unreachable — fallback
130
+ _fallback_snapshot(payload, base)
131
+ return None
132
+
133
+
119
134
  def main() -> None:
120
135
  try:
121
136
  data = json.load(sys.stdin)
@@ -124,14 +139,7 @@ def main() -> None:
124
139
  sys.exit(0)
125
140
 
126
141
  try:
127
- project_path = str(Path.cwd())
128
- port = _find_server_port(project_path)
129
-
130
- if port and _call_server(port, data):
131
- sys.exit(0)
132
-
133
- # Server not running or unreachable — fallback
134
- _fallback_snapshot(data)
142
+ run(data)
135
143
  except Exception as exc:
136
144
  log_hook_error("hook_auto_snapshot", exc)
137
145