code-context-control 2.74.0__tar.gz → 2.76.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {code_context_control-2.74.0/code_context_control.egg-info → code_context_control-2.76.2}/PKG-INFO +2 -2
- {code_context_control-2.74.0 → code_context_control-2.76.2}/README.md +1 -1
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/c3.py +1 -1
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_access_guard.py +108 -5
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/mcp_server.py +13 -5
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/edit.py +74 -9
- code_context_control-2.76.2/cli/tools/edit_verify.py +267 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/edits.py +18 -2
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/project.py +5 -1
- {code_context_control-2.74.0 → code_context_control-2.76.2/code_context_control.egg-info}/PKG-INFO +2 -2
- {code_context_control-2.74.0 → code_context_control-2.76.2}/code_context_control.egg-info/SOURCES.txt +6 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/oracle_server.py +7 -1
- code_context_control-2.76.2/oracle/services/chat_poll.py +397 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/mobile_api.py +597 -1
- {code_context_control-2.74.0 → code_context_control-2.76.2}/pyproject.toml +1 -1
- code_context_control-2.76.2/tests/test_chat_poll.py +570 -0
- code_context_control-2.76.2/tests/test_edit_ledger_deadline.py +166 -0
- code_context_control-2.76.2/tests/test_edit_verify.py +342 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_hook_access_guard.py +130 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_mobile_api.py +3 -1
- code_context_control-2.76.2/tests/test_mobile_extras.py +736 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/LICENSE +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/__init__.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/_hook_utils.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/commands/__init__.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/commands/common.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/commands/parser.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/docs.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/edits.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/guide/access.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/guide/bitbucket.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/guide/credentials.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/guide/getting-started.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/guide/index.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/guide/masking.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/guide/oracle.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/guide/shared.css +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/guide/tools.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/guide/workflow.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_artifact.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_auto_snapshot.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_c3_signal.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_c3read.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_dispatch.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_edit_ledger.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_edit_unlock.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_filter.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_ghost_files.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_pretool_enforce.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_prompt_recall.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_read.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_session_stats.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hook_terse_advisor.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_server.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/app.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/add_project.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/config_editor.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/drill_artifacts.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/drill_health.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/drill_panel.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/drill_subprojects.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/drill_tasks.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/drill_views.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/global_search.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/hub_credentials.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/hub_enforcement.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/hub_locks.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/mcp_manager.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/modals.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/project_card.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/project_tree.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/session_drawer.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/settings_modal.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/sidebar.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/summary_bar.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/task_board.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/toasts.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/components/topbar.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui/state.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/hub_ui.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/mcp_proxy.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/progress.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/server.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/__init__.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/_grants.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/_helpers.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/agent.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/artifacts.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/bitbucket.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/compress.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/credentials.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/delegate.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/federate.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/filter.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/impact.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/jira.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/locks.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/memory.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/override.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/read.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/search.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/session.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/shell.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/status.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/tasks.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/tools/validate.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/api.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/app.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/components/access.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/components/bitbucket.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/components/chat.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/components/credentials.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/components/dashboard.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/components/edits.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/components/enforcement.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/components/instructions.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/components/jira.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/components/memory.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/components/sessions.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/components/settings.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/components/sidebar.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/components/tasks.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/icons.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/pm_shared.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/shared.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui/theme.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui_legacy.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/cli/ui_nano.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/code_context_control.egg-info/dependency_links.txt +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/code_context_control.egg-info/entry_points.txt +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/code_context_control.egg-info/requires.txt +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/code_context_control.egg-info/top_level.txt +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/core/__init__.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/core/config.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/core/ide.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/core/mcp_toml.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/core/web_security.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/__init__.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/config.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/mcp_oracle.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/oracle_ui.html +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/__init__.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/activity_reporter.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/api_auth.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/c3_bridge.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/chat_engine.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/chat_store.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/cross_memory.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/discovery_audit.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/federated_graph.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/health_checker.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/insight_engine.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/local_session.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/memory_reader.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/memory_writer.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/project_scanner.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/review_agent.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/tool_executor.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/services/tool_registry.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/activity.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/agents.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/app.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/busy.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/chat/conversations.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/chat/input.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/chat/markdown.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/chat/send.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/chat/stream_renderer.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/chat/toolbar.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/core.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/crossgraph.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/header.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/insights.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/projects.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/qrcode.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/settings.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/suggestions.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/oracle/ui/theme_tabs.js +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/__init__.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/access_guard.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/access_telemetry.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/activity_log.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/agent_base.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/agent_locks.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/agents.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/artifact_defs.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/artifact_store.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/auto_memory.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/bench/__init__.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/bench/external/__init__.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/bench/external/aider_polyglot.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/bench/external/swe_bench.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/benchmark_dashboard.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/bitbucket_client.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/bitbucket_credentials.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/circuit_breaker.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/claude_md.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/compressor.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/context_snapshot.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/conversation_store.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/credential_store.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/doc_index.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/e2e_benchmark.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/e2e_evaluator.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/e2e_tasks.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/edit_ledger.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/embedding_index.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/enforcement_policy.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/error_reporting.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/file_memory.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/git_context.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/hub_service.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/indexer.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/jira_client.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/jira_cloud.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/jira_credentials.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/jira_data_center.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/jira_links.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/mask_activation.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/mask_mirror.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/mask_presets.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/memory.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/memory_consolidator.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/memory_distiller.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/memory_graph.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/memory_grounder.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/memory_queue.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/memory_scorer.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/metrics.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/notifications.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/ollama_bridge.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/ollama_client.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/ollama_credentials.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/output_filter.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/override_grants.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/override_policy.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/override_requests.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/override_wake.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/parser.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/project_manager.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/project_runtime.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/protocol.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/proxy_state.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/repo_map.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/retention.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/retrieval_broker.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/router.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/runtime.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/scanner.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/session_benchmark.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/session_manager.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/session_preloader.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/subprojects.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/task_store.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/telemetry.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/text_index.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/time_tracker.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/tool_classifier.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/transcript_index.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/validation_cache.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/vector_store.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/watcher.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/services/win_subprocess.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/setup.cfg +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_access_builtin_optout.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_access_guard.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_access_guard_meta.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_access_guard_shell_project.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_access_guard_surfaces.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_access_guard_wiring.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_activity_reporter.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_agent_locks.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_aider_polyglot.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_antigravity_transition.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_artifact_store.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_artifact_tool.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_bitbucket_cli_smoke.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_bitbucket_client.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_bitbucket_config_fallback.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_bitbucket_credentials.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_bitbucket_tool.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_c3_bridge_project_artifacts.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_c3_shell.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_circuit_breaker.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_claude_md_merge.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_cli_smoke.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_compressor_large_file.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_credential_store.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_credentials_routes.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_credentials_tool.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_creds_cli_smoke.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_delegate_cascade.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_delegate_version_probe.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_e2e_benchmark.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_edit_ledger_hook.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_edit_locking.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_edit_normalization.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_embedding_index_deadlock.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_enforcement_flip.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_enforcement_policy.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_enforcement_routes.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_federated_graph.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_filter_backoff.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_ghost_files.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_ghost_generation.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_git_branch_awareness.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_hook_dispatch.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_hook_pretool_enforce.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_hook_prompt_recall.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_hook_smoke.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_hook_state.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_hook_utf8_stdio.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_hub_credentials_route.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_hub_credentials_routes_write.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_hub_enforcement_routes.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_hub_inspect_api.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_hub_locks_routes.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_hub_server_smoke.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_hub_subproject_links.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_ide_workflow_adaptation.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_init_scan.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_install_mcp_entrypoint.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_jira_cli_smoke.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_jira_client.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_jira_config_fallback.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_jira_credentials.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_jira_links.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_jira_routes.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_jira_tool.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_lazy_store_init.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_mask_guard.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_mask_routes.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_mask_surfaces.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_mcp_host_guard.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_mcp_memory_timeout.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_mcp_server_smoke.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_mcp_toml.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_memory_distiller.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_memory_graph_api.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_memory_queue.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_memory_system.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_mobile_override_routes.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_mobile_security_api.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_no_stale_model_pins.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_notification_dedup.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_notification_discipline.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_ollama_credentials.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_oracle_api_auth.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_oracle_apikey_api.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_oracle_c3_bridge.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_oracle_chat_engine.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_oracle_chat_store.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_oracle_discovery_api.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_oracle_discovery_audit.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_oracle_local_auth.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_oracle_ollama_bridge.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_oracle_scanner_cache.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_oracle_security_fixes.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_oracle_session_bootstrap.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_oracle_subproject_awareness.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_oracle_ui_bundle.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_output_filter.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_override_grants.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_override_grants_mcp.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_override_requests.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_override_wake.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_permissions.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_pm_api.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_project_manager.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_project_manager_merge.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_project_tool.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_read_coercion.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_read_edit_parity.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_related_facts.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_repo_map.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_response_boilerplate.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_retention.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_review_digest.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_service_durability.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_session_benchmark.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_session_budget.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_shell_creds.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_shell_robustness.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_subproject_exclusion.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_subproject_federation.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_subprojects.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_swe_bench.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_task_store.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_task_tool.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_time_tracker.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_token_telemetry.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_tool_registry.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_upgrade_and_version.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_validate.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_vault_write_guard.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_version_sync.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_web_security.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tests/test_windows_reliability.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/__init__.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/backend.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/main.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/screens/__init__.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/screens/benchmark_view.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/screens/claudemd_view.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/screens/compress_view.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/screens/index_view.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/screens/init_view.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/screens/mcp_view.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/screens/optimize_view.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/screens/pipe_view.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/screens/projects_view.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/screens/search_view.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/screens/session_view.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/screens/stats.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/screens/ui_view.py +0 -0
- {code_context_control-2.74.0 → code_context_control-2.76.2}/tui/theme.tcss +0 -0
{code_context_control-2.74.0/code_context_control.egg-info → code_context_control-2.76.2}/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.76.2
|
|
4
4
|
Summary: Local MCP code-intelligence for AI coding tools: surgical search/read/edit, agent-config version history, path-level access + masking guards, and a multi-project hub.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -204,7 +204,7 @@ C3 exposes **21 tools** as a native MCP server. Your IDE calls them directly:
|
|
|
204
204
|
| `c3_locks` | Agent leases — who is working on which file, so two agents don't collide (v2.65.0) |
|
|
205
205
|
| `c3_delegate` | Offload heavy work to local Ollama / Codex / Gemini |
|
|
206
206
|
| `c3_agent` | Workflows: `review_changes`, `investigate`, `preflight`, `prepare_context`, `validate_compress` |
|
|
207
|
-
| `c3_edits` | Edit-ledger queries, version diffs, restore points, per-branch filter |
|
|
207
|
+
| `c3_edits` | Edit-ledger queries, version diffs, restore points, per-branch filter, `verify` (did a failed `c3_edit` still land?) |
|
|
208
208
|
| `c3_task` | Per-project PM — tasks, dependencies, milestones, time tracking (v2.53.0) |
|
|
209
209
|
| `c3_artifacts` | Agent-config version history, diff & restore (v2.46.0) |
|
|
210
210
|
| `c3_credentials` | Named-secret vault; values never enter model context (v2.58.0) |
|
|
@@ -141,7 +141,7 @@ C3 exposes **21 tools** as a native MCP server. Your IDE calls them directly:
|
|
|
141
141
|
| `c3_locks` | Agent leases — who is working on which file, so two agents don't collide (v2.65.0) |
|
|
142
142
|
| `c3_delegate` | Offload heavy work to local Ollama / Codex / Gemini |
|
|
143
143
|
| `c3_agent` | Workflows: `review_changes`, `investigate`, `preflight`, `prepare_context`, `validate_compress` |
|
|
144
|
-
| `c3_edits` | Edit-ledger queries, version diffs, restore points, per-branch filter |
|
|
144
|
+
| `c3_edits` | Edit-ledger queries, version diffs, restore points, per-branch filter, `verify` (did a failed `c3_edit` still land?) |
|
|
145
145
|
| `c3_task` | Per-project PM — tasks, dependencies, milestones, time tracking (v2.53.0) |
|
|
146
146
|
| `c3_artifacts` | Agent-config version history, diff & restore (v2.46.0) |
|
|
147
147
|
| `c3_credentials` | Named-secret vault; values never enter model context (v2.58.0) |
|
|
@@ -48,12 +48,96 @@ _IPV6_RE = re.compile(
|
|
|
48
48
|
re.IGNORECASE,
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
+
# Characters that make a token a compound expression rather than a path
|
|
52
|
+
# argument: what Windows forbids in a filename outright (< > " | ? *) plus the
|
|
53
|
+
# bracketing and quoting that marks structure ({} [] () ' ` &).
|
|
54
|
+
#
|
|
55
|
+
# NOT in this set, deliberately: ':' is the thing under test; '$' and '=' are
|
|
56
|
+
# legal in a real path AND in a real ADS spelling — './notes.txt:$DATA' is the
|
|
57
|
+
# canonical default-stream syntax, and dropping it here silently deleted an
|
|
58
|
+
# existing negative control the first time this was written. Every genuinely
|
|
59
|
+
# dangerous shell use of '$' arrives with '(' or '{' anyway, which are covered.
|
|
60
|
+
#
|
|
61
|
+
# This is the general form of the fix #50 attempted with _is_network_token, and
|
|
62
|
+
# the reason that one was not enough: it whitelisted two *shapes* — a token
|
|
63
|
+
# STARTING with a scheme, or a bare IPv6 — so a URL that was not the whole token
|
|
64
|
+
# still tripped, and syntax carrying no URL at all was never covered:
|
|
65
|
+
#
|
|
66
|
+
# echo "see [#1](https://example.com/a/b)" -> token '[#1](https://…' , scheme not at position 0
|
|
67
|
+
# echo '{a:.x,b:("/"+.y)}' -> a jq filter, no URL anywhere
|
|
68
|
+
# python -c "… ['src/a.py','src/b.py'] …" -> a list literal
|
|
69
|
+
#
|
|
70
|
+
# All three contain '/', so the token gate admitted them; all three canonicalize
|
|
71
|
+
# to something with a residual colon, and <ads> is exempt from existence-gating,
|
|
72
|
+
# so each became a hard deny on text that names no file at all.
|
|
73
|
+
_SYNTAX_CHARS = frozenset("{}[]()<>\"'`|&*?")
|
|
74
|
+
|
|
75
|
+
# `pkg/mod.py::TestThing`, `ns::Type` — a pytest node id or a C++/Rust scope.
|
|
76
|
+
# NTFS spells a stream `file:name:$TYPE`, and the doubled-colon default-stream
|
|
77
|
+
# form `file::$DATA` is real, so "contains ::" is NOT safe on its own. What is
|
|
78
|
+
# safe is the type slot: stream types are `$`-prefixed system constants, so a
|
|
79
|
+
# `::` NOT followed by `$` cannot be a stream spelling.
|
|
80
|
+
_DOUBLE_COLON_RE = re.compile(r"::(?!\$)")
|
|
81
|
+
|
|
82
|
+
# `<rev>:<path>` — how git names a blob: `git show HEAD:setup.py`,
|
|
83
|
+
# `git cat-file -p origin/main:src/a.py`.
|
|
84
|
+
_GIT_REVSPEC_RE = re.compile(r"^(?P<rev>[^:\s]+):(?P<path>[^:\s]+)$")
|
|
85
|
+
_GIT_CMD_RE = re.compile(r"^\s*(?:[\w.\-]*[/\\])?git(?:\.exe)?\b", re.IGNORECASE)
|
|
86
|
+
|
|
51
87
|
|
|
52
88
|
def _is_network_token(tok: str) -> bool:
|
|
53
89
|
"""True for URLs and IPv6 literals/CIDRs — never for an ADS spelling."""
|
|
54
90
|
return bool(_SCHEME_RE.match(tok) or _IPV6_RE.match(tok))
|
|
55
91
|
|
|
56
92
|
|
|
93
|
+
def _is_scope_token(tok: str) -> bool:
|
|
94
|
+
"""True for `a/b.py::Thing` and `ns::Type` — never an ADS spelling."""
|
|
95
|
+
return bool(_DOUBLE_COLON_RE.search(tok))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _git_revspec_path(cmd: str, tok: str):
|
|
99
|
+
"""The path half of a git ``<rev>:<path>`` token, or None.
|
|
100
|
+
|
|
101
|
+
Returned so the caller can check **the path** rather than the whole token,
|
|
102
|
+
which is the fix and a tightening at once. Today every git revspec is denied
|
|
103
|
+
by accident: ``<rev>:<path>`` canonicalizes to something with a residual
|
|
104
|
+
colon and trips ``<ads>``, so ``git show origin/main:pyproject.toml`` — an
|
|
105
|
+
everyday command naming nothing sensitive — is refused.
|
|
106
|
+
|
|
107
|
+
Whitelisting the shape outright would go too far the other way, because
|
|
108
|
+
``git show HEAD:.env`` really does print a denied file's contents, and right
|
|
109
|
+
now the only thing stopping it is the false positive. Checking the path half
|
|
110
|
+
gets both right: `.env` is denied on its own rule, `pyproject.toml` is
|
|
111
|
+
allowed on its own merits, and neither answer depends on a rule misfiring.
|
|
112
|
+
|
|
113
|
+
Gated on the command actually being ``git``, since ``<word>:<word>`` means
|
|
114
|
+
something else almost everywhere else.
|
|
115
|
+
"""
|
|
116
|
+
if not _GIT_CMD_RE.match(cmd or ""):
|
|
117
|
+
return None
|
|
118
|
+
if len(tok) > 1 and tok[1] == ":":
|
|
119
|
+
return None # a drive letter is not a revspec separator
|
|
120
|
+
m = _GIT_REVSPEC_RE.match(tok)
|
|
121
|
+
if not m:
|
|
122
|
+
return None
|
|
123
|
+
path = m.group("path")
|
|
124
|
+
# A `$`-prefixed suffix is a stream type, not a path. Do not reinterpret it.
|
|
125
|
+
return None if path.startswith("$") else path
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _looks_like_a_path(tok: str) -> bool:
|
|
129
|
+
"""False for tokens that are shell syntax rather than a path argument.
|
|
130
|
+
|
|
131
|
+
Deliberately asymmetric, because the two errors are not equal. Skipping a
|
|
132
|
+
token that WAS a path costs a missed advisory flag in a best-effort scanner
|
|
133
|
+
— the real gates (the Read/Write/Edit path check above, and every c3 tool)
|
|
134
|
+
still refuse it. Flagging a token that was NOT a path costs a hard,
|
|
135
|
+
unappealable deny on a command that touches nothing, which is what #50 was.
|
|
136
|
+
So this errs toward skipping.
|
|
137
|
+
"""
|
|
138
|
+
return not (_SYNTAX_CHARS & set(tok))
|
|
139
|
+
|
|
140
|
+
|
|
57
141
|
def _deny(reason: str) -> dict:
|
|
58
142
|
return {
|
|
59
143
|
"hookSpecificOutput": {
|
|
@@ -113,9 +197,10 @@ def _scan_shell(cmd: str, base: str):
|
|
|
113
197
|
covers (existence-gating keeps regex/pattern arguments like '\\.env'
|
|
114
198
|
from false-denying), or the evaluator flags the spelling itself.
|
|
115
199
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
200
|
+
Tokens that are shell syntax rather than path arguments are skipped
|
|
201
|
+
outright, as are URLs and IPv6 literals: all of them trip the ADS spelling
|
|
202
|
+
check, which is exempt from existence-gating, so a token naming nothing on
|
|
203
|
+
disk would otherwise hard-deny (#50).
|
|
119
204
|
"""
|
|
120
205
|
for raw in _TOKEN_SPLIT.split(cmd)[:_MAX_TOKENS]:
|
|
121
206
|
tok = raw.strip("\"'`;,()")
|
|
@@ -123,8 +208,22 @@ def _scan_shell(cmd: str, base: str):
|
|
|
123
208
|
continue
|
|
124
209
|
if "/" not in tok and "\\" not in tok and not tok.startswith("."):
|
|
125
210
|
continue
|
|
126
|
-
|
|
211
|
+
# Order matters only for readability: a URL is also syntax-free, so
|
|
212
|
+
# either check alone would skip 'https://x'. Both are kept because they
|
|
213
|
+
# answer different questions — "is this a network literal" and "is this
|
|
214
|
+
# a path at all" — and the second is the one that generalizes.
|
|
215
|
+
if not _looks_like_a_path(tok) or _is_network_token(tok):
|
|
216
|
+
continue
|
|
217
|
+
# `tests/x.py::TestThing` is a node id, not a stream spelling.
|
|
218
|
+
if _is_scope_token(tok):
|
|
127
219
|
continue
|
|
220
|
+
# `git show <rev>:<path>` — judge the PATH, not the whole token. This
|
|
221
|
+
# is the one place the scan rewrites what it checks rather than skipping
|
|
222
|
+
# it, because skipping would let `git show HEAD:.env` through.
|
|
223
|
+
revspec_path = _git_revspec_path(cmd, tok)
|
|
224
|
+
from_revspec = revspec_path is not None
|
|
225
|
+
if from_revspec:
|
|
226
|
+
tok = revspec_path
|
|
128
227
|
if re.match(r"^/[a-z]/", tok): # MSYS /c/foo → C:/foo
|
|
129
228
|
tok = f"{tok[1]}:{tok[2:]}"
|
|
130
229
|
denial = ag.check(tok, "read", base)
|
|
@@ -134,7 +233,11 @@ def _scan_shell(cmd: str, base: str):
|
|
|
134
233
|
exists = (p if p.is_absolute() else Path(base) / p).exists()
|
|
135
234
|
except OSError:
|
|
136
235
|
exists = False
|
|
137
|
-
|
|
236
|
+
# A revspec names a path in HISTORY, so the working tree is the
|
|
237
|
+
# wrong place to ask whether it is real. `git show HEAD~5:.env`
|
|
238
|
+
# reads a denied file whether or not that file exists today, and
|
|
239
|
+
# existence-gating it would be a hole this rewrite opened.
|
|
240
|
+
if exists or from_revspec or denial.rule.startswith("<"):
|
|
138
241
|
return denial, tok
|
|
139
242
|
return None, ""
|
|
140
243
|
|
|
@@ -708,7 +708,9 @@ async def c3_edit(file_path: str, old_string: str = "", new_string: str = "",
|
|
|
708
708
|
"""EDIT — read+patch+write+log in one step. Primary code-change tool; always prefer over native Edit.
|
|
709
709
|
old_string: text to replace. new_string: replacement. summary: ledger description.
|
|
710
710
|
edits: JSON list of {old_string, new_string, summary?} for multi-hunk batch on one file.
|
|
711
|
-
Parallel across files. Create new file: non-existent file_path + old_string='' + new_string=<content>.
|
|
711
|
+
Parallel across files. Create new file: non-existent file_path + old_string='' + new_string=<content>.
|
|
712
|
+
If this call ERRORS OR TIMES OUT, do not retry blind — a failed c3_edit may still have
|
|
713
|
+
written the file. Re-send the same args to c3_edits(action='verify') for a verdict."""
|
|
712
714
|
path_err = validate_file_path(file_path)
|
|
713
715
|
if path_err:
|
|
714
716
|
return f"[c3_edit:error] {path_err}"
|
|
@@ -725,10 +727,15 @@ async def c3_edit(file_path: str, old_string: str = "", new_string: str = "",
|
|
|
725
727
|
async def c3_edits(action: str, file: str = "", change_type: str = "modified",
|
|
726
728
|
summary: str = "", lines_changed: str = "", tags: str = "",
|
|
727
729
|
limit: int = 50, since: str = "", edit_id: str = "",
|
|
728
|
-
tag: str = "", branch: str = "",
|
|
730
|
+
tag: str = "", branch: str = "", old_string: str = "",
|
|
731
|
+
new_string: str = "", edits: str = "", ctx: Context = None) -> str:
|
|
729
732
|
"""EDIT HISTORY — inspect the ledger. Different from c3_edit (which writes); this one reads.
|
|
730
|
-
actions: log (append entry), history (recent edits), versions (per-file), stats, tag (mark edit_id)
|
|
731
|
-
|
|
733
|
+
actions: log (append entry), history (recent edits), versions (per-file), stats, tag (mark edit_id),
|
|
734
|
+
verify (did an edit land?).
|
|
735
|
+
branch: filter history to edits stamped with a given git branch.
|
|
736
|
+
verify: pass the SAME file/old_string/new_string (or edits) you gave the c3_edit that errored or
|
|
737
|
+
timed out. Answers APPLIED (do not retry) / NOT_APPLIED (safe to retry) / INCONCLUSIVE (read the
|
|
738
|
+
file) — a failed c3_edit may still have written, so retrying blind can apply an edit twice."""
|
|
732
739
|
svc = _svc(ctx)
|
|
733
740
|
|
|
734
741
|
def finalize(name, args, resp, summ, **kw):
|
|
@@ -737,7 +744,8 @@ async def c3_edits(action: str, file: str = "", change_type: str = "modified",
|
|
|
737
744
|
from cli.tools.edits import handle_edits
|
|
738
745
|
return await asyncio.to_thread(handle_edits, action, file, change_type, summary,
|
|
739
746
|
lines_changed, tags, limit, since, edit_id, tag,
|
|
740
|
-
svc, finalize, branch
|
|
747
|
+
svc, finalize, branch, old_string, new_string,
|
|
748
|
+
edits)
|
|
741
749
|
|
|
742
750
|
|
|
743
751
|
@mcp.tool()
|
|
@@ -387,10 +387,12 @@ def _edit_locked(path: Path, rel: str, file_path: str, old_string: str,
|
|
|
387
387
|
tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else None
|
|
388
388
|
n_new = new_string.count("\n") + 1 if new_string else 0
|
|
389
389
|
create_summary = summary or f"Created {rel} ({n_new}L)"
|
|
390
|
-
_log_to_ledger(
|
|
391
|
-
|
|
390
|
+
deferred = _log_to_ledger(
|
|
391
|
+
rel, create_summary, tag_list, svc,
|
|
392
|
+
detail={"old_string": "", "new_string": new_string[:_DETAIL_CAP], "created": True})
|
|
392
393
|
short = f"✓ {rel} [created, +{n_new}L]" + (f" — {summary}" if summary else "")
|
|
393
|
-
return finalize("c3_edit", {"file": file_path}, short
|
|
394
|
+
return finalize("c3_edit", {"file": file_path}, short + deferred,
|
|
395
|
+
f"{rel} created")
|
|
394
396
|
|
|
395
397
|
# Parse tag list once
|
|
396
398
|
tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else None
|
|
@@ -477,6 +479,7 @@ def _edit_locked(path: Path, rel: str, file_path: str, old_string: str,
|
|
|
477
479
|
f"Write error: {e}", "write error")
|
|
478
480
|
|
|
479
481
|
# Log batch to ledger as one entry (store each patch's old/new for diff view)
|
|
482
|
+
deferred = ""
|
|
480
483
|
if any_applied:
|
|
481
484
|
batch_detail = {"patches": [
|
|
482
485
|
{
|
|
@@ -486,7 +489,9 @@ def _edit_locked(path: Path, rel: str, file_path: str, old_string: str,
|
|
|
486
489
|
}
|
|
487
490
|
for p in edit_list if p.get("old_string") is not None
|
|
488
491
|
]}
|
|
489
|
-
|
|
492
|
+
deferred = _log_to_ledger(
|
|
493
|
+
rel, summary or f"Batch edit: {len(edit_list)} patches",
|
|
494
|
+
tag_list, svc, detail=batch_detail)
|
|
490
495
|
|
|
491
496
|
# Classify from the structured status list, not by substring-scanning
|
|
492
497
|
# the human-readable result lines — a patch *summary* containing words
|
|
@@ -499,7 +504,7 @@ def _edit_locked(path: Path, rel: str, file_path: str, old_string: str,
|
|
|
499
504
|
short += "\n" + "\n".join(failed)
|
|
500
505
|
if first_miss:
|
|
501
506
|
short += first_miss
|
|
502
|
-
return finalize("c3_edit", {"file": file_path}, short,
|
|
507
|
+
return finalize("c3_edit", {"file": file_path}, short + deferred,
|
|
503
508
|
f"{rel} patched ({len(edit_list)} patches)")
|
|
504
509
|
|
|
505
510
|
# ── Single-edit mode ──────────────────────────────────────────────────────
|
|
@@ -548,7 +553,8 @@ def _edit_locked(path: Path, rel: str, file_path: str, old_string: str,
|
|
|
548
553
|
}
|
|
549
554
|
if used_fallback:
|
|
550
555
|
single_detail["unicode_normalized"] = True
|
|
551
|
-
_log_to_ledger(rel, auto_summary, tag_list, svc,
|
|
556
|
+
deferred = _log_to_ledger(rel, auto_summary, tag_list, svc,
|
|
557
|
+
detail=single_detail)
|
|
552
558
|
|
|
553
559
|
n_old = old_string.count("\n") + 1
|
|
554
560
|
n_new = new_string.count("\n") + 1
|
|
@@ -556,14 +562,73 @@ def _edit_locked(path: Path, rel: str, file_path: str, old_string: str,
|
|
|
556
562
|
occ = f" ({occurrences}x)" if occurrences > 1 else ""
|
|
557
563
|
norm_tag = " [unicode-normalized]" if used_fallback else ""
|
|
558
564
|
short = f"✓ {rel} [{delta}]{occ}{norm_tag}" + (f" — {summary}" if summary else "")
|
|
559
|
-
return finalize("c3_edit", {"file": file_path}, short
|
|
565
|
+
return finalize("c3_edit", {"file": file_path}, short + deferred,
|
|
566
|
+
f"{rel} patched")
|
|
560
567
|
|
|
561
568
|
|
|
562
569
|
_DETAIL_CAP = 2000 # chars stored per old/new string in the ledger
|
|
563
570
|
|
|
571
|
+
#: How long the post-write bookkeeping gets before the caller stops waiting on
|
|
572
|
+
#: it. Normal is milliseconds — a JSONL append and a git call that already caps
|
|
573
|
+
#: itself at 4s. Ten seconds is far outside normal and far inside the harness's
|
|
574
|
+
#: idle timeout, which is the number this exists to stay away from.
|
|
575
|
+
_LEDGER_DEADLINE_S = 10.0
|
|
576
|
+
|
|
577
|
+
_DEFERRED_NOTE = (
|
|
578
|
+
"\n [c3:ledger-deferred] The FILE WRITE SUCCEEDED. Recording it in the "
|
|
579
|
+
"ledger outran {sec:.0f}s and was left running in the background, so the "
|
|
580
|
+
"edit may be missing from c3_edits history and from "
|
|
581
|
+
"c3_edits(action='verify') corroboration.\n"
|
|
582
|
+
" Do not re-apply this edit on the strength of that absence — the file is "
|
|
583
|
+
"the evidence, and it already has the change."
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _log_to_ledger(rel: str, summary: str, tag_list, svc,
|
|
588
|
+
detail: dict = None) -> str:
|
|
589
|
+
"""Record an edit in the ledger, activity log, and session manager.
|
|
590
|
+
|
|
591
|
+
Never raises, and — since #74 — never blocks the caller indefinitely.
|
|
592
|
+
|
|
593
|
+
The write has already happened by the time this runs. That ordering is what
|
|
594
|
+
makes a stall here so expensive: one logged `c3_edit` hung until the harness
|
|
595
|
+
aborted it at 1800s while the file on disk was already correct, so half an
|
|
596
|
+
hour bought a report of work that had finished in milliseconds.
|
|
597
|
+
|
|
598
|
+
The host-side cause of that stall is not C3's (see #74 — the box hits
|
|
599
|
+
commit-charge exhaustion in short unpredictable windows), and nothing here
|
|
600
|
+
prevents it. What this does prevent is *paying 1800s to find out*. The
|
|
601
|
+
bookkeeping runs on a daemon thread; if it outlives the deadline the caller
|
|
602
|
+
returns anyway, with the response saying plainly that the write landed and
|
|
603
|
+
the record may not have. The thread is left alone rather than killed — it
|
|
604
|
+
may well finish a moment later, and a half-written ledger entry would be
|
|
605
|
+
worse than a late one.
|
|
606
|
+
|
|
607
|
+
Returns "" normally, or a note to append to the response when the deadline
|
|
608
|
+
passed. Reporting a degraded record is the point; a silent one would make
|
|
609
|
+
this indistinguishable from a clean run, which is the failure class this
|
|
610
|
+
whole subsystem exists to remove.
|
|
611
|
+
"""
|
|
612
|
+
if not svc.edit_ledger:
|
|
613
|
+
return ""
|
|
614
|
+
done = threading.Event()
|
|
615
|
+
|
|
616
|
+
def _record() -> None:
|
|
617
|
+
try:
|
|
618
|
+
_log_to_ledger_blocking(rel, summary, tag_list, svc, detail)
|
|
619
|
+
finally:
|
|
620
|
+
done.set()
|
|
621
|
+
|
|
622
|
+
threading.Thread(target=_record, name="c3-edit-ledger",
|
|
623
|
+
daemon=True).start()
|
|
624
|
+
if done.wait(_LEDGER_DEADLINE_S):
|
|
625
|
+
return ""
|
|
626
|
+
return _DEFERRED_NOTE.format(sec=_LEDGER_DEADLINE_S)
|
|
627
|
+
|
|
564
628
|
|
|
565
|
-
def
|
|
566
|
-
|
|
629
|
+
def _log_to_ledger_blocking(rel: str, summary: str, tag_list, svc,
|
|
630
|
+
detail: dict = None) -> None:
|
|
631
|
+
"""The bookkeeping itself. Never raises; may block. Always run on a thread."""
|
|
567
632
|
if not svc.edit_ledger:
|
|
568
633
|
return
|
|
569
634
|
try:
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""`c3_edits(action='verify')` — did that edit land?
|
|
2
|
+
|
|
3
|
+
Written for #74. A `c3_edit` call can fail *after* doing its work: one call in a
|
|
4
|
+
logged session hung until the harness aborted it at the 1800s MCP idle timeout,
|
|
5
|
+
and the write had fully landed; another dropped the MCP connection and had
|
|
6
|
+
written nothing. Both reached the caller as an error, and nothing distinguished
|
|
7
|
+
them.
|
|
8
|
+
|
|
9
|
+
That ambiguity is the defect, separately from whatever causes the stall. The
|
|
10
|
+
retry that is safe after the second incident is a double-apply after the first,
|
|
11
|
+
and the case where that actually corrupts a file is not exotic — any edit whose
|
|
12
|
+
``new_string`` contains its ``old_string`` (appending a line, wrapping a call)
|
|
13
|
+
matches again on a retry and applies twice. `c3_edit`'s own not-found error is
|
|
14
|
+
what saves the ordinary case, which is luck rather than a property.
|
|
15
|
+
|
|
16
|
+
**The file is the primary evidence; the ledger corroborates.** In that order,
|
|
17
|
+
because they answer different questions: the file says whether the intended text
|
|
18
|
+
is there now, the ledger says whether *this* edit is what put it there. Neither
|
|
19
|
+
alone is enough —
|
|
20
|
+
|
|
21
|
+
- text present, no ledger entry → it may have been there all along;
|
|
22
|
+
- ledger entry, text absent → something wrote and something else overwrote it.
|
|
23
|
+
|
|
24
|
+
So there are three verdicts, and the third one is the honest half:
|
|
25
|
+
|
|
26
|
+
- ``NOT_APPLIED`` — ``new_string`` is absent from the file. Definitive: had the
|
|
27
|
+
edit landed, its own replacement text would be there. Safe to retry.
|
|
28
|
+
- ``APPLIED`` — ``new_string`` is present *and* a ledger entry for this file
|
|
29
|
+
records this exact old/new pair. Definitive: do not retry.
|
|
30
|
+
- ``INCONCLUSIVE`` — everything else, with the specific check that failed named
|
|
31
|
+
in the output. Never collapsed into either of the above; a verifier that
|
|
32
|
+
guesses is worth less than one that says it cannot tell, because the caller
|
|
33
|
+
can always fall back to reading the file.
|
|
34
|
+
|
|
35
|
+
Batch (`edits=`) verification reports per patch, since `c3_edit`'s batch mode
|
|
36
|
+
writes the file once after applying every hunk in memory — so "half written" is
|
|
37
|
+
not a state it can leave behind, but "patch 2 missed while 1 and 3 applied" very
|
|
38
|
+
much is, and that is what the caller needs itemized.
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import json
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
|
|
45
|
+
from cli.tools import _grants
|
|
46
|
+
from services import access_guard
|
|
47
|
+
|
|
48
|
+
VERDICT_APPLIED = "APPLIED"
|
|
49
|
+
VERDICT_NOT_APPLIED = "NOT_APPLIED"
|
|
50
|
+
VERDICT_INCONCLUSIVE = "INCONCLUSIVE"
|
|
51
|
+
|
|
52
|
+
#: How far back to look for a corroborating ledger entry. Generous on purpose:
|
|
53
|
+
#: the failure this exists for is a call that hung for half an hour, so a window
|
|
54
|
+
#: measured in a handful of entries would miss the case it was built for.
|
|
55
|
+
_DEFAULT_LOOKBACK = 200
|
|
56
|
+
|
|
57
|
+
#: The ledger caps stored old/new strings (``_DETAIL_CAP`` in cli/tools/edit.py).
|
|
58
|
+
#: A long ``new_string`` is therefore recorded truncated, and comparing it whole
|
|
59
|
+
#: would report INCONCLUSIVE for every large edit — which is to say, for exactly
|
|
60
|
+
#: the edits most likely to have timed out. Compare on the stored prefix.
|
|
61
|
+
_DETAIL_CAP = 2000
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _rel(file_path: str, project_path: str) -> str:
|
|
65
|
+
"""The ledger's spelling of ``file_path``.
|
|
66
|
+
|
|
67
|
+
The caller should be able to paste the same ``file_path`` they gave
|
|
68
|
+
``c3_edit`` — which is frequently absolute, and which the ledger stores
|
|
69
|
+
project-relative with forward slashes. Making them convert by hand is how a
|
|
70
|
+
verification tool goes unused at the moment it is needed.
|
|
71
|
+
"""
|
|
72
|
+
p = Path(file_path)
|
|
73
|
+
if not p.is_absolute():
|
|
74
|
+
p = Path(project_path) / p
|
|
75
|
+
try:
|
|
76
|
+
p = p.resolve()
|
|
77
|
+
except OSError:
|
|
78
|
+
pass
|
|
79
|
+
try:
|
|
80
|
+
return str(p.relative_to(Path(project_path).resolve())).replace("\\", "/")
|
|
81
|
+
except (ValueError, OSError):
|
|
82
|
+
return str(file_path).replace("\\", "/")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _read(path: Path):
|
|
86
|
+
"""File text with EOLs normalized the way ``c3_edit`` normalizes them.
|
|
87
|
+
|
|
88
|
+
Without this, an edit to a CRLF file verifies as NOT_APPLIED: c3_edit
|
|
89
|
+
matches against LF-normalized content, so a multi-line ``new_string`` the
|
|
90
|
+
caller passed never appears verbatim in the bytes on disk.
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
raw = path.read_bytes()
|
|
94
|
+
except OSError:
|
|
95
|
+
return None
|
|
96
|
+
text = raw.decode("utf-8", errors="surrogateescape")
|
|
97
|
+
return text.replace("\r\n", "\n").replace("\r", "\n")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _pairs_from_ledger(entry: dict) -> list:
|
|
101
|
+
"""Every (old, new) pair an entry records — single edits and batches alike."""
|
|
102
|
+
detail = entry.get("detail") or {}
|
|
103
|
+
if not isinstance(detail, dict):
|
|
104
|
+
return []
|
|
105
|
+
patches = detail.get("patches")
|
|
106
|
+
if isinstance(patches, list):
|
|
107
|
+
return [
|
|
108
|
+
(str(p.get("old_string", "")), str(p.get("new_string", "")))
|
|
109
|
+
for p in patches if isinstance(p, dict)
|
|
110
|
+
]
|
|
111
|
+
if "old_string" in detail or "new_string" in detail:
|
|
112
|
+
return [(str(detail.get("old_string", "")),
|
|
113
|
+
str(detail.get("new_string", "")))]
|
|
114
|
+
return []
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _ledger_match(ledger, rel: str, old: str, new: str, limit: int):
|
|
118
|
+
"""The most recent ledger entry recording this exact old/new pair, or None.
|
|
119
|
+
|
|
120
|
+
Compared against the ledger's truncated prefixes, not the full strings — see
|
|
121
|
+
``_DETAIL_CAP``. Truncation means a *prefix* match is the strongest claim
|
|
122
|
+
available here, so two edits sharing a 2000-character prefix would be
|
|
123
|
+
indistinguishable. That is a real limit, and it is why the file check leads
|
|
124
|
+
and this one only corroborates.
|
|
125
|
+
"""
|
|
126
|
+
if ledger is None:
|
|
127
|
+
return None
|
|
128
|
+
try:
|
|
129
|
+
entries = ledger.get_history(file=rel, limit=limit)
|
|
130
|
+
except Exception:
|
|
131
|
+
return None
|
|
132
|
+
want = (old[:_DETAIL_CAP], new[:_DETAIL_CAP])
|
|
133
|
+
for entry in reversed(entries or []):
|
|
134
|
+
for pair in _pairs_from_ledger(entry):
|
|
135
|
+
if pair == want:
|
|
136
|
+
return entry
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _verify_one(content, old: str, new: str, ledger, rel: str,
|
|
141
|
+
limit: int) -> dict:
|
|
142
|
+
"""Verdict for one (old, new) pair. Pure apart from the ledger read."""
|
|
143
|
+
if content is None:
|
|
144
|
+
return {
|
|
145
|
+
"verdict": VERDICT_NOT_APPLIED,
|
|
146
|
+
"why": "the file does not exist or could not be read, so nothing "
|
|
147
|
+
"was written to it",
|
|
148
|
+
"entry": None,
|
|
149
|
+
}
|
|
150
|
+
if not new:
|
|
151
|
+
# A deletion: new_string is empty. "Is the replacement present" has no
|
|
152
|
+
# answer, so the ledger is the only positive evidence available.
|
|
153
|
+
entry = _ledger_match(ledger, rel, old, new, limit)
|
|
154
|
+
if old and old in content:
|
|
155
|
+
return {"verdict": VERDICT_NOT_APPLIED,
|
|
156
|
+
"why": "the text this edit removes is still present",
|
|
157
|
+
"entry": None}
|
|
158
|
+
if entry is not None:
|
|
159
|
+
return {"verdict": VERDICT_APPLIED,
|
|
160
|
+
"why": f"the removed text is gone and ledger entry "
|
|
161
|
+
f"{entry.get('id', '')} records this deletion",
|
|
162
|
+
"entry": entry}
|
|
163
|
+
return {"verdict": VERDICT_INCONCLUSIVE,
|
|
164
|
+
"why": "the text is absent but no ledger entry records this "
|
|
165
|
+
"deletion — it may never have been there",
|
|
166
|
+
"entry": None}
|
|
167
|
+
|
|
168
|
+
if new not in content:
|
|
169
|
+
return {
|
|
170
|
+
"verdict": VERDICT_NOT_APPLIED,
|
|
171
|
+
"why": "new_string is not in the file; had this edit landed, its "
|
|
172
|
+
"own replacement text would be there",
|
|
173
|
+
"entry": None,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
entry = _ledger_match(ledger, rel, old, new, limit)
|
|
177
|
+
if entry is not None:
|
|
178
|
+
return {
|
|
179
|
+
"verdict": VERDICT_APPLIED,
|
|
180
|
+
"why": f"new_string is present and ledger entry "
|
|
181
|
+
f"{entry.get('id', '')} ({entry.get('timestamp', '')[:19]}) "
|
|
182
|
+
f"records this exact old/new pair",
|
|
183
|
+
"entry": entry,
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
"verdict": VERDICT_INCONCLUSIVE,
|
|
187
|
+
"why": "new_string is present but no ledger entry records this edit — "
|
|
188
|
+
"the text may have been there already, or been written by "
|
|
189
|
+
"something other than c3_edit. Read the file before retrying",
|
|
190
|
+
"entry": None,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def verify(file_path: str, old_string: str, new_string: str, edits: str,
|
|
195
|
+
svc, limit: int = _DEFAULT_LOOKBACK):
|
|
196
|
+
"""Verify one edit or a batch. Returns (body, summary) for ``finalize``.
|
|
197
|
+
|
|
198
|
+
Takes the same arguments the original ``c3_edit`` call took, so recovering
|
|
199
|
+
from an aborted edit means re-sending it to a different action rather than
|
|
200
|
+
reconstructing anything.
|
|
201
|
+
"""
|
|
202
|
+
if not file_path:
|
|
203
|
+
return "file is required", "missing file"
|
|
204
|
+
|
|
205
|
+
project_path = str(getattr(svc, "project_path", "") or "")
|
|
206
|
+
rel = _rel(file_path, project_path)
|
|
207
|
+
path = Path(file_path)
|
|
208
|
+
if not path.is_absolute():
|
|
209
|
+
path = Path(project_path) / path
|
|
210
|
+
|
|
211
|
+
# Access Guard, read verdict, before a single byte is read. Caught by
|
|
212
|
+
# tests/test_access_guard_meta.py rather than by review — and it was right
|
|
213
|
+
# to catch it. This tool returns a verdict rather than content, which is
|
|
214
|
+
# exactly why it is worth guarding: "does .env contain X" answered one
|
|
215
|
+
# substring at a time is a content read with extra steps.
|
|
216
|
+
denial = access_guard.check(str(path), "read", project_path)
|
|
217
|
+
if denial and not _grants.allow(svc, denial, tool="c3_edits", op="read",
|
|
218
|
+
path=str(path)):
|
|
219
|
+
return (access_guard.refusal(denial, file_path, "read"),
|
|
220
|
+
"access-denied")
|
|
221
|
+
|
|
222
|
+
content = _read(path)
|
|
223
|
+
ledger = getattr(svc, "edit_ledger", None)
|
|
224
|
+
|
|
225
|
+
if edits:
|
|
226
|
+
try:
|
|
227
|
+
edit_list = json.loads(edits) if isinstance(edits, str) else edits
|
|
228
|
+
except json.JSONDecodeError as exc:
|
|
229
|
+
return f"edits must be a valid JSON list: {exc}", "bad edits param"
|
|
230
|
+
if not isinstance(edit_list, list) or not edit_list:
|
|
231
|
+
return "edits must be a non-empty JSON list", "bad edits param"
|
|
232
|
+
|
|
233
|
+
lines = [f"[edits:verify] {rel} — {len(edit_list)} patch(es)"]
|
|
234
|
+
verdicts = []
|
|
235
|
+
for i, patch in enumerate(edit_list):
|
|
236
|
+
if not isinstance(patch, dict):
|
|
237
|
+
lines.append(f" patch[{i}]: INCONCLUSIVE — not an object")
|
|
238
|
+
verdicts.append(VERDICT_INCONCLUSIVE)
|
|
239
|
+
continue
|
|
240
|
+
r = _verify_one(content, str(patch.get("old_string", "")),
|
|
241
|
+
str(patch.get("new_string", "")), ledger, rel, limit)
|
|
242
|
+
verdicts.append(r["verdict"])
|
|
243
|
+
lines.append(f" patch[{i}]: {r['verdict']} — {r['why']}")
|
|
244
|
+
# A batch writes once, so a mixed result means individual hunks did not
|
|
245
|
+
# match — not that the file was left half-written. Say so, rather than
|
|
246
|
+
# letting the caller infer a torn write that cannot happen.
|
|
247
|
+
lines.append(" note: c3_edit's batch mode writes the file once after "
|
|
248
|
+
"applying every hunk in memory, so a mixed result means "
|
|
249
|
+
"some hunks did not match — not a partial write.")
|
|
250
|
+
counts = {v: verdicts.count(v) for v in sorted(set(verdicts))}
|
|
251
|
+
summary = ", ".join(f"{n} {v.lower()}" for v, n in counts.items())
|
|
252
|
+
return "\n".join(lines), f"{rel}: {summary}"
|
|
253
|
+
|
|
254
|
+
if not old_string and not new_string:
|
|
255
|
+
return ("old_string and/or new_string are required — verify answers "
|
|
256
|
+
"'did THIS edit land', which needs the edit"), "missing params"
|
|
257
|
+
|
|
258
|
+
r = _verify_one(content, old_string, new_string, ledger, rel, limit)
|
|
259
|
+
lines = [f"[edits:verify] {rel} — {r['verdict']}", f" {r['why']}"]
|
|
260
|
+
if r["verdict"] == VERDICT_NOT_APPLIED:
|
|
261
|
+
lines.append(" → safe to re-send the original c3_edit call.")
|
|
262
|
+
elif r["verdict"] == VERDICT_APPLIED:
|
|
263
|
+
lines.append(" → do NOT retry; re-sending would either fail to match "
|
|
264
|
+
"or apply a second time.")
|
|
265
|
+
else:
|
|
266
|
+
lines.append(" → read the file before deciding; do not retry blind.")
|
|
267
|
+
return "\n".join(lines), f"{rel}: {r['verdict']}"
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
"""c3_edits — AI-tracked edit ledger: log, query, and version file changes."""
|
|
2
2
|
|
|
3
|
+
from cli.tools.edit_verify import verify as _verify
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
def handle_edits(action: str, file: str, change_type: str, summary: str,
|
|
5
7
|
lines_changed: str, tags: str, limit: int, since: str,
|
|
6
|
-
edit_id: str, tag: str, svc, finalize, branch: str = ""
|
|
8
|
+
edit_id: str, tag: str, svc, finalize, branch: str = "",
|
|
9
|
+
old_string: str = "", new_string: str = "",
|
|
10
|
+
edits: str = "") -> str:
|
|
7
11
|
"""Route c3_edits actions."""
|
|
8
12
|
ledger = svc.edit_ledger
|
|
13
|
+
|
|
14
|
+
# verify runs BEFORE the ledger-availability gate. It leads on the file, and
|
|
15
|
+
# the file is readable whether or not the ledger is: answering "did my edit
|
|
16
|
+
# land" with "ledger disabled" would withhold the evidence that matters most
|
|
17
|
+
# at the one moment a caller has no other way to find out (#74).
|
|
18
|
+
if action == "verify":
|
|
19
|
+
body, summ = _verify(file, old_string, new_string, edits, svc,
|
|
20
|
+
limit=limit or 200)
|
|
21
|
+
return finalize("c3_edits", {"action": "verify", "file": file},
|
|
22
|
+
body, summ)
|
|
23
|
+
|
|
9
24
|
if ledger is None:
|
|
10
25
|
return finalize("c3_edits", {"action": action}, "Edit ledger not available", "ledger disabled")
|
|
11
26
|
|
|
@@ -119,5 +134,6 @@ def handle_edits(action: str, file: str, change_type: str, summary: str,
|
|
|
119
134
|
|
|
120
135
|
else:
|
|
121
136
|
return finalize("c3_edits", {"action": action},
|
|
122
|
-
f"Unknown action: {action}.
|
|
137
|
+
f"Unknown action: {action}. "
|
|
138
|
+
"Use: log, history, versions, stats, tag, verify",
|
|
123
139
|
"unknown action")
|