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