graphite-code 1.1.0__tar.gz → 1.1.1__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.
- {graphite_code-1.1.0 → graphite_code-1.1.1}/CHANGELOG.md +109 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/PKG-INFO +2 -2
- {graphite_code-1.1.0 → graphite_code-1.1.1}/README.md +1 -1
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/__init__.py +1 -1
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/channel.py +97 -10
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/daemon_health.py +4 -1
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/doctor.py +6 -22
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/doctor_probes.py +9 -1
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/git.py +8 -29
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/hookinstall.py +6 -1
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/init.py +4 -1
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/mcp_server.py +12 -4
- graphite_code-1.1.1/src/graphite/programs.py +190 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/process_runner.py +6 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/service.py +24 -12
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/ts_bridge.py +16 -1
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/windows_startup.py +6 -1
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/windows_task.py +12 -4
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_channel_core.py +224 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_channel_lock.py +55 -1
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_channel_log_integrity.py +44 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_channel_mcp.py +26 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_channel_register.py +26 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_daemon_health.py +31 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_doctor.py +72 -3
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_hookinstall.py +32 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_init.py +32 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_mutation_tests.py +116 -0
- graphite_code-1.1.1/tests/test_programs.py +403 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_process_runner.py +21 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_service.py +111 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_ts_bridge.py +63 -5
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_windows_startup.py +21 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_windows_task.py +42 -2
- {graphite_code-1.1.0 → graphite_code-1.1.1}/.gitignore +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/ARCHITECTURE.md +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/CONTRIBUTING.md +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/LICENSE +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/RELEASING.md +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/pyproject.toml +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/__main__.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/_cleanup_worker.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/_win32_ctypes.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/activation.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/agent_hooks.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/agent_settings.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/analyze.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/answer_contract.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/bootstrap.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/buildlock.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/cache.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/cli.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/cluster.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/config.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/context.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/daemon.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/daemon_launch.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/debt.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/dependency_install.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/detach.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/engine_identity.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/export/__init__.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/export/html.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/export/json.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/export/md.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/extract/__init__.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/extract/ast.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/freshness.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/graph.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/graph_io.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/health.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/hook_entry.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/hookshim.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/incident_ledger.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/ingest.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/io.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/launchd_agent.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/listing.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/llm.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/llm_probe.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/mcp.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/natural_query.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/overlays.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/probe_process.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/probe_workspace.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/process_contracts.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/provider_observer.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/py.typed +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/query.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/query_plan.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/replacement_audit.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/resolve.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/review.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/__init__.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/approval.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/classifier.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/claude_executor.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/claude_probe.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/cli_identity.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/codex_executor.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/codex_probe.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/context_builder.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/contracts.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/diff_policy.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/edit_apply.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/effort.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/lifecycle.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/lifecycle_operator.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/lifecycle_service.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/lifecycle_storage.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/ollama_executor.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/ollama_probe.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/openrouter_executor.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/openrouter_probe.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/policy.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/probe_runner.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/profiles.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/prompt.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/registry.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/route_pool.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/route_pool_execution.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/schema_validation.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/settings.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/shadow.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/storage.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/telemetry.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/worktree.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/zai_edit.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/zai_executor.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/routing/zai_probe.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/savings.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/systemd_unit.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/ts_resolver.mjs +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/typescript_activation.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/usage_ledger.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/validation.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/watch.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/src/graphite/windows_job.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/conftest.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/data/sqloracle/blindspots.py.txt +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/data/sqloracle/round69.py.txt +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/fake_clis/fake_cli.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/fixtures/provider_lifecycle_schema_v1.sql +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/fixtures/routing_schema_v2_ca77600.sql +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/fixtures/routing_schema_v3_94eb333.sql +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/fixtures/routing_schema_v4_lifecycle_migration.sql +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test__win32_ctypes.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_activation.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_activation_backstop.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_agent_hook_activation.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_agent_hooks.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_agent_settings.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_answer_contract.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_arrow_and_constructor_binding.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_atomic_write.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_bootstrap.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_build_detach.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_buildlock.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_cache_engine_identity.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_cache_entry_pruning.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_cache_file_set.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_cache_pruning.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_call_graph.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_channel_attribution.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_channel_cli.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_channel_report.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_channel_status.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_class_field_arrow_binding.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_cli_channel.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_cli_channel_attribution.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_cli_reference.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_cli_version.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_cluster.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_compare_dist.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_configuration_reference.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_context.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_context_builder.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_cwd_relative_config.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_cycle_search.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_daemon.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_daemon_activation.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_daemon_build_lock.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_daemon_child_lock.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_daemon_engine_staleness.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_daemon_health_activation.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_daemon_launch.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_daemon_service_cli.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_debt.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_dependency_install.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_destructured_binding.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_determinism.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_dispatch_evidence.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_doctor_agent_hooks.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_doctor_hooks.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_doctor_managed_docs.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_doctor_probes.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_documentation.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_edit_apply.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_engine_identity.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_exit_codes_reference.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_external_calls.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_git_security.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_go_rust.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_graph_io.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_graph_provider_isolation.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_hardening.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_health.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_hook_entry.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_hook_template.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_hookshim.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_html_security.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_incident_ledger.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_init_activation_doctrine.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_init_hooks.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_init_validation_exit.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_launchd_agent.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_lifecycle_operator.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_listing.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_listing_surfaces.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_llm.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_llm_probe.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_mcp.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_mcp_server.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_mcp_server_channel_attribution.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_method_dispatch_scope.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_monorepo.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_natural_query.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_node_identity.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_ollama_executor.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_overlays.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_path_leak_fixture.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_probe_diagnostics.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_probe_process.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_probe_workspace.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_process_contracts.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_provider_claude_probe.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_provider_codex_probe.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_provider_lifecycle.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_provider_lifecycle_service.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_provider_lifecycle_storage.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_provider_observer.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_provider_ollama_probe.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_provider_openrouter_probe.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_provider_probe_runner.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_public_surface.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_published_schemas.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_python_resolver.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_query_plan.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_reliability.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_replacement_audit.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_resolve.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_review.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_route_pool.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_route_pool_execution.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_approval.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_benchmark.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_classifier.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_claude_executor.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_cli.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_cli_contracts.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_cli_recovery.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_codex_executor.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_context.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_contracts.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_diff_policy.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_executor.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_openrouter_executor.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_policy.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_profiles.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_registry.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_schema_validation.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_security.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_shadow.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_storage.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_telemetry.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_worktree.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_zai_executor.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_routing_zai_probe.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_savings.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_search.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_smoke.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_sqloracle.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_synthetic_repo.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_systemd_unit.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_typescript_activation.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_typescript_resolver.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_usage_ledger.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_verify_artifact.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_verify_published_release.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_watch.py +0 -0
- {graphite_code-1.1.0 → graphite_code-1.1.1}/tests/test_zai_edit.py +0 -0
|
@@ -15,6 +15,115 @@ machine-checkable identity; the version is for humans.
|
|
|
15
15
|
|
|
16
16
|
## [Unreleased]
|
|
17
17
|
|
|
18
|
+
## [1.1.1] — 2026-10-03
|
|
19
|
+
|
|
20
|
+
A patch release. Security fixes ship in the next patch release of the current
|
|
21
|
+
minor (`docs/compatibility.md`, `SECURITY.md`). The graph itself does not
|
|
22
|
+
change: on this repository, measured against the deployed 1.1.0 engine,
|
|
23
|
+
7797 of 7797 node ids survive, 22154 of 22154 edge triples are
|
|
24
|
+
unchanged, every relation count is equal, and `analysis.cycles` is identical.
|
|
25
|
+
Only the engine fingerprint moves, because graphite's own bytes did.
|
|
26
|
+
|
|
27
|
+
### Security
|
|
28
|
+
|
|
29
|
+
**graphite no longer launches a program by bare name, so a program sitting in
|
|
30
|
+
a repository root cannot run in place of the real one.** On Windows a bare
|
|
31
|
+
name is looked up in the launching process's current directory before PATH.
|
|
32
|
+
That covers a list-form `subprocess.run(["git", ...])`, cmd.exe and
|
|
33
|
+
`shutil.which`, unless `NoDefaultCurrentDirectoryInExePath` is set. Claude
|
|
34
|
+
Code sets that variable, but the daemon at login and a commit from a terminal
|
|
35
|
+
do not.
|
|
36
|
+
|
|
37
|
+
graphite's builds, hooks and channel broker run with a repository root as
|
|
38
|
+
their current directory. A `node.exe` committed to a TypeScript repo's root
|
|
39
|
+
ran on every build, and the build still exited 0. This was measured against
|
|
40
|
+
the released 1.1.0 wheel; channel round 304 reported it.
|
|
41
|
+
|
|
42
|
+
Every launch now goes through `graphite.programs.resolve_program`, which:
|
|
43
|
+
|
|
44
|
+
- reads only absolute PATH entries;
|
|
45
|
+
- skips a PATH directory inside the target repository, or one that IS the
|
|
46
|
+
current directory, judged both as written and as resolved, so a committed
|
|
47
|
+
symlink cannot choose which outside binary runs (aramid llm-review
|
|
48
|
+
`1d8d8cf`);
|
|
49
|
+
- refuses any candidate that resolves into one of those;
|
|
50
|
+
- on Windows, tries `.exe` as CreateProcess does, unless the caller asks for
|
|
51
|
+
more.
|
|
52
|
+
|
|
53
|
+
Windows' own tools come from the system directory the OS reports. The
|
|
54
|
+
sites changed are:
|
|
55
|
+
|
|
56
|
+
- the TypeScript bridge's `node` (every build);
|
|
57
|
+
- the channel broker's `git`;
|
|
58
|
+
- the channel's commit-msg hook, which fails closed when no git is found;
|
|
59
|
+
- the `git` calls in `init` and `hookinstall`;
|
|
60
|
+
- `daemon-health`'s `powershell.exe`;
|
|
61
|
+
- the four `schtasks.exe` calls;
|
|
62
|
+
- the Startup launcher's `powershell.exe`;
|
|
63
|
+
- routing's `claude`/`codex` lookup, which keeps `.cmd` shims, excludes the
|
|
64
|
+
repository and the task worktree whatever the current directory (aramid
|
|
65
|
+
llm-review `30e2473`), and sets the CLI's environment (see below);
|
|
66
|
+
- `doctor`'s `graphite-mcp` check;
|
|
67
|
+
- the mutation-test launcher's `git`.
|
|
68
|
+
|
|
69
|
+
Resolving the first program is not enough when that program is a `.cmd` shim.
|
|
70
|
+
An npm-installed CLI with no `node.exe` beside it runs bare `node` through
|
|
71
|
+
cmd.exe, which searches the task worktree first. A `node.exe` committed to the
|
|
72
|
+
repo would then run before the CLI's own sandbox. Routing now sets
|
|
73
|
+
`NoDefaultCurrentDirectoryInExePath=1` in every CLI's environment, which turns
|
|
74
|
+
that search off for the whole chain. The commit-msg hook also has its own status
|
|
75
|
+
when it cannot find git. It used to fall through to "this commit names no agent".
|
|
76
|
+
|
|
77
|
+
Two older private copies of the same PATH walk, in `git` and `doctor`, now
|
|
78
|
+
delegate to the shared resolver. A test walks every launch call in `src/`,
|
|
79
|
+
`scripts/` and the hook's embedded program, and fails on a program named by a
|
|
80
|
+
literal or on `shell=True`.
|
|
81
|
+
|
|
82
|
+
The current directory is refused itself, not its subtree, because that is
|
|
83
|
+
what Windows searches. Refusing everything beneath it would refuse every tool
|
|
84
|
+
on drive C for a run started from `C:\`, and `~/.local/bin` or an
|
|
85
|
+
nvm-installed `node` for a run started from `$HOME`. A pre-release build
|
|
86
|
+
measured exactly that. The repository's own subtree is refused through the
|
|
87
|
+
repository each caller passes.
|
|
88
|
+
|
|
89
|
+
Artifacts already installed keep the old command until they are regenerated:
|
|
90
|
+
- the Startup `.vbs` (`graphite daemon-install-startup-windows`);
|
|
91
|
+
- the channel's commit-msg hook (rewritten by `graphite channel register`).
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
**`doctor --deep`'s LLM probe no longer spends the provider's timeout on
|
|
96
|
+
starting its worker.** One number was both the provider timeout and the worker
|
|
97
|
+
process's whole budget. The interpreter's start was therefore paid from the
|
|
98
|
+
provider's time, and a loaded machine reported `timeout` for a provider that
|
|
99
|
+
was only misconfigured: 2 s read `timeout` 7 times in 10 under 36 CPU burners.
|
|
100
|
+
|
|
101
|
+
The worker already enforces the provider timeout itself. Its process budget is
|
|
102
|
+
now that timeout plus `_LLM_WORKER_START_ALLOWANCE_SECONDS` (30 s), which only
|
|
103
|
+
bounds a worker that never answers. A 0.1 s provider timeout, below any
|
|
104
|
+
interpreter's start, now reaches `configuration`.
|
|
105
|
+
|
|
106
|
+
**The channel reports an abandoned lock on every path that can break one.**
|
|
107
|
+
These come from aramid llm-review findings:
|
|
108
|
+
- **`1810f9e`:** `inbox()`, the path every agent takes at session start, was
|
|
109
|
+
the one writer that dropped the recovery. `graphite_channel_inbox` now
|
|
110
|
+
carries `lock_recovered` when a delivery had to break a lock, and stays
|
|
111
|
+
silent otherwise.
|
|
112
|
+
- **`9751aab`:** a residue scan that failed read as `[]`, the same as a clean
|
|
113
|
+
one. It now reads `null`, meaning "we could not look". A wedged git, the
|
|
114
|
+
likeliest cause of an abandoned lock, is also what makes the scan fail.
|
|
115
|
+
- **`9a23b84`:** the recovery report no longer returns the dead holder's host
|
|
116
|
+
name. Nothing decides on it: identity is pid + start time, and staleness is
|
|
117
|
+
the clock.
|
|
118
|
+
|
|
119
|
+
**The channel's commit-msg hook is installed with exactly `0o755`, pinned on
|
|
120
|
+
every platform.** aramid's mutation drain found `0o755 -> 0o756`, a
|
|
121
|
+
world-writable audit hook on POSIX, surviving (`e37afd86`). On Windows,
|
|
122
|
+
`os.chmod` honours only the write bit, so no on-disk check there could see it.
|
|
123
|
+
A test now pins the argument, which kills the mutant on Windows too. A
|
|
124
|
+
POSIX-only test checks the on-disk property: executable, and not group- or
|
|
125
|
+
world-writable.
|
|
126
|
+
|
|
18
127
|
## [1.1.0] — 2026-09-27
|
|
19
128
|
|
|
20
129
|
A minor release. The agent channel's read surfaces gain fields, which is an
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: graphite-code
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: Local-first, zero-LLM knowledge graph extraction for codebases.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jared0565/graphite
|
|
6
6
|
Project-URL: Repository, https://github.com/jared0565/graphite
|
|
@@ -48,7 +48,7 @@ Description-Content-Type: text/markdown
|
|
|
48
48
|
|
|
49
49
|
Local-first, deterministic knowledge graph extraction for codebases — zero-LLM, daemon-maintained, agent-agnostic.
|
|
50
50
|
|
|
51
|
-
**Status: 1.1.
|
|
51
|
+
**Status: 1.1.1, production/stable.** Published on PyPI as
|
|
52
52
|
[`graphite-code`](https://pypi.org/project/graphite-code/) with PEP 740
|
|
53
53
|
attestations; supported on Windows, Linux and macOS with Python 3.11–3.14
|
|
54
54
|
(every cell gates CI). What 1.x promises — CLI, JSON outputs, `graph.json`,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Local-first, deterministic knowledge graph extraction for codebases — zero-LLM, daemon-maintained, agent-agnostic.
|
|
4
4
|
|
|
5
|
-
**Status: 1.1.
|
|
5
|
+
**Status: 1.1.1, production/stable.** Published on PyPI as
|
|
6
6
|
[`graphite-code`](https://pypi.org/project/graphite-code/) with PEP 740
|
|
7
7
|
attestations; supported on Windows, Linux and macOS with Python 3.11–3.14
|
|
8
8
|
(every cell gates CI). What 1.x promises — CLI, JSON outputs, `graph.json`,
|
|
@@ -38,4 +38,4 @@ __all__ = ["__version__"]
|
|
|
38
38
|
#: Coarse and hand-maintained by construction. It is a release label, not
|
|
39
39
|
#: evidence that a given fix is present -- to answer that, survey for the marker
|
|
40
40
|
#: the fix introduced, or compare `graphite --version` fingerprints.
|
|
41
|
-
__version__ = "1.1.
|
|
41
|
+
__version__ = "1.1.1"
|
|
@@ -34,6 +34,8 @@ from datetime import datetime, timedelta, timezone
|
|
|
34
34
|
from pathlib import Path
|
|
35
35
|
from typing import Callable
|
|
36
36
|
|
|
37
|
+
from .programs import resolve_program
|
|
38
|
+
|
|
37
39
|
CHANNEL_DIRNAME = ".agent-channel"
|
|
38
40
|
REGISTRY_FILENAME = "agents.json"
|
|
39
41
|
ROUNDS_DIRNAME = "rounds"
|
|
@@ -260,7 +262,7 @@ else
|
|
|
260
262
|
fi
|
|
261
263
|
|
|
262
264
|
"$PY" -I - "$MSG_FILE" "$ROOT" <<'PYEOF'
|
|
263
|
-
import json, re, subprocess, sys
|
|
265
|
+
import json, os, re, subprocess, sys
|
|
264
266
|
|
|
265
267
|
# Distinguishable so the shell can name the ACTUAL reason. Sentinel objects
|
|
266
268
|
# rather than strings: a registry whose contents happened to be `"absent"` would
|
|
@@ -269,6 +271,50 @@ ABSENT = object()
|
|
|
269
271
|
CORRUPT = object()
|
|
270
272
|
|
|
271
273
|
|
|
274
|
+
class GitUnavailable(Exception):
|
|
275
|
+
"""No usable git outside the channel. Its own exit status, so the shell can
|
|
276
|
+
name it: the catch-all's 1 means "names no agent", which is not the fault."""
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _git_executable(root):
|
|
280
|
+
"""git by absolute path, never from the channel or the current directory.
|
|
281
|
+
|
|
282
|
+
git starts this hook in the channel's root, and Windows looked a bare `git`
|
|
283
|
+
up in the current directory before PATH (channel round 304). Inlined from
|
|
284
|
+
`graphite.programs.resolve_program`: `-I` cannot import graphite. Raises
|
|
285
|
+
rather than returning nothing, so the gate fails CLOSED -- an unreadable
|
|
286
|
+
registry must never read as an absent one, which is the bootstrap branch.
|
|
287
|
+
"""
|
|
288
|
+
names = ("git.exe",) if os.name == "nt" else ("git",)
|
|
289
|
+
# The channel's whole subtree is refused; the cwd only itself, which is
|
|
290
|
+
# what Windows searches -- refusing everything beneath a cwd above git's
|
|
291
|
+
# install would refuse git.
|
|
292
|
+
channel = os.path.normcase(os.path.realpath(root))
|
|
293
|
+
cwd = os.path.normcase(os.path.realpath(os.getcwd()))
|
|
294
|
+
|
|
295
|
+
def refused(directory):
|
|
296
|
+
folded = os.path.normcase(directory)
|
|
297
|
+
return folded == cwd or folded == channel or folded.startswith(channel.rstrip(os.sep) + os.sep)
|
|
298
|
+
|
|
299
|
+
for raw in os.environ.get("PATH", "").split(os.pathsep):
|
|
300
|
+
if not raw or not os.path.isabs(raw):
|
|
301
|
+
continue
|
|
302
|
+
# The directory itself, as written and as resolved: a symlink inside
|
|
303
|
+
# the channel must not choose which outside git runs (1d8d8cf).
|
|
304
|
+
if refused(os.path.normpath(raw)) or refused(os.path.realpath(raw)):
|
|
305
|
+
continue
|
|
306
|
+
for name in names:
|
|
307
|
+
candidate = os.path.realpath(os.path.join(raw, name))
|
|
308
|
+
if not os.path.isfile(candidate):
|
|
309
|
+
continue
|
|
310
|
+
if os.name != "nt" and not os.access(candidate, os.X_OK):
|
|
311
|
+
continue
|
|
312
|
+
if refused(os.path.dirname(candidate)):
|
|
313
|
+
continue
|
|
314
|
+
return candidate
|
|
315
|
+
raise GitUnavailable("git was not found on PATH outside the channel")
|
|
316
|
+
|
|
317
|
+
|
|
272
318
|
def _registry_at(root, spec):
|
|
273
319
|
"""Parse `agents.json` at a git revision spec.
|
|
274
320
|
|
|
@@ -282,7 +328,7 @@ def _registry_at(root, spec):
|
|
|
282
328
|
is what the next commit will inherit as authority.
|
|
283
329
|
"""
|
|
284
330
|
result = subprocess.run(
|
|
285
|
-
[
|
|
331
|
+
[_git_executable(root), "-C", root, "show", spec],
|
|
286
332
|
capture_output=True,
|
|
287
333
|
text=True,
|
|
288
334
|
# Explicit, for the reason `_git` documents: `text=True` alone decodes
|
|
@@ -346,6 +392,8 @@ try:
|
|
|
346
392
|
# Parses, authorises nobody, and is not the empty object a fresh channel
|
|
347
393
|
# seeds. Nothing legitimate produces this, and the gate must not guess.
|
|
348
394
|
sys.exit(3)
|
|
395
|
+
except GitUnavailable:
|
|
396
|
+
sys.exit(4)
|
|
349
397
|
except Exception:
|
|
350
398
|
# `SystemExit` derives from BaseException, so the exits above pass through
|
|
351
399
|
# this handler untouched -- their codes survive.
|
|
@@ -405,6 +453,18 @@ EOF
|
|
|
405
453
|
exit 1
|
|
406
454
|
fi
|
|
407
455
|
|
|
456
|
+
if [ "$STATUS" -eq 4 ]; then
|
|
457
|
+
# `echo`, not `cat`, for the reason the missing-interpreter branch gives: an
|
|
458
|
+
# environment that has lost git may have lost the rest of its toolchain.
|
|
459
|
+
echo "[agent-channel] BLOCKED: git was not found on PATH outside the channel." >&2
|
|
460
|
+
echo "" >&2
|
|
461
|
+
echo "The audit gate reads the registry from HEAD with git, and will not run a" >&2
|
|
462
|
+
echo "git that sits inside the channel or the current directory. It cannot" >&2
|
|
463
|
+
echo "verify this commit, so it is refused rather than waved through. This is" >&2
|
|
464
|
+
echo "NOT a problem with your commit message. Put git on PATH, then commit again." >&2
|
|
465
|
+
exit 1
|
|
466
|
+
fi
|
|
467
|
+
|
|
408
468
|
# The banner heredocs stay QUOTED. An earlier revision unquoted them to
|
|
409
469
|
# interpolate `$PY`, which turned the audit gate's own output into a shell
|
|
410
470
|
# expansion surface: any `$`, backtick or `$(...)` that later reaches this text
|
|
@@ -721,10 +781,18 @@ def _git(root: Path, *args: str, check: bool = True) -> str:
|
|
|
721
781
|
refreshes stale stat data by REWRITING the index under `index.lock`, and a
|
|
722
782
|
writer's `git add` that meets that lock fails. Only optional locks are
|
|
723
783
|
skipped; `add` and `commit` still take the ones they need.
|
|
784
|
+
|
|
785
|
+
git is launched by ABSOLUTE path, from outside both the channel and the
|
|
786
|
+
directory this process stands in (channel round 304). The broker's current
|
|
787
|
+
directory is the agent's repository, and Windows looked a bare `git` up
|
|
788
|
+
there before PATH. See `graphite.programs`.
|
|
724
789
|
"""
|
|
790
|
+
git = resolve_program("git", exclude=(root,))
|
|
791
|
+
if git is None:
|
|
792
|
+
raise ChannelError("git_unavailable", "git was not found on PATH outside the repository")
|
|
725
793
|
try:
|
|
726
794
|
result = subprocess.run( # noqa: S603
|
|
727
|
-
[
|
|
795
|
+
[str(git), "-C", str(root), *args],
|
|
728
796
|
capture_output=True,
|
|
729
797
|
text=True,
|
|
730
798
|
encoding="utf-8",
|
|
@@ -766,8 +834,9 @@ def _commit(root: Path, paths: list[str], subject: str, agent: str) -> str:
|
|
|
766
834
|
return _git(root, "rev-parse", "--short", "HEAD").strip()
|
|
767
835
|
|
|
768
836
|
|
|
769
|
-
def _uncommitted_residue(root: Path) -> list[str]:
|
|
770
|
-
"""Channel files an abandoned holder left behind uncommitted
|
|
837
|
+
def _uncommitted_residue(root: Path) -> list[str] | None:
|
|
838
|
+
"""Channel files an abandoned holder left behind uncommitted, or None when
|
|
839
|
+
the scan itself failed.
|
|
771
840
|
|
|
772
841
|
Automatic recovery makes this MORE important, not less. `status_events`
|
|
773
842
|
reads from DISK, so an orphaned `status/NNN/*.json` written by a holder that
|
|
@@ -776,7 +845,10 @@ def _uncommitted_residue(root: Path) -> list[str]:
|
|
|
776
845
|
look; once the lock clears itself, nobody is looking unless we say so.
|
|
777
846
|
|
|
778
847
|
Best-effort by construction: this runs while we hold nothing, and a failure
|
|
779
|
-
to scan must never block a recovery that is otherwise safe.
|
|
848
|
+
to scan must never block a recovery that is otherwise safe. But it must not
|
|
849
|
+
read as a clean scan either (aramid llm-review 9751aab): `[]` says "the dead
|
|
850
|
+
holder left nothing", `None` says "we could not look". A wedged git -- the
|
|
851
|
+
likeliest cause of an abandoned lock -- is also what makes this fail.
|
|
780
852
|
"""
|
|
781
853
|
try:
|
|
782
854
|
# `--untracked-files=all`: bare `--porcelain` collapses a wholly
|
|
@@ -784,7 +856,7 @@ def _uncommitted_residue(root: Path) -> list[str]:
|
|
|
784
856
|
# operator needs in order to go and look at it.
|
|
785
857
|
porcelain = _git(root, "status", "--porcelain", "--untracked-files=all", check=False)
|
|
786
858
|
except ChannelError:
|
|
787
|
-
return
|
|
859
|
+
return None
|
|
788
860
|
residue = set()
|
|
789
861
|
for line in porcelain.splitlines():
|
|
790
862
|
path = line[3:].strip().strip('"')
|
|
@@ -893,9 +965,12 @@ class _Lock:
|
|
|
893
965
|
started = (first or {}).get("started_at")
|
|
894
966
|
if isinstance(started, (int, float)) and not isinstance(started, bool):
|
|
895
967
|
held_for = round(self._clock() - float(started), 1)
|
|
968
|
+
# No `host`: it is in the record for the operator reading the lock file,
|
|
969
|
+
# but nothing here decides on it -- identity is (pid, started_at) and
|
|
970
|
+
# staleness is the clock -- so it does not go to every caller (aramid
|
|
971
|
+
# llm-review 9a23b84).
|
|
896
972
|
self.recovered = {
|
|
897
973
|
"pid": (first or {}).get("pid"),
|
|
898
|
-
"host": (first or {}).get("host"),
|
|
899
974
|
"held_seconds": held_for,
|
|
900
975
|
"residue": _uncommitted_residue(self.root),
|
|
901
976
|
}
|
|
@@ -1194,13 +1269,23 @@ def record_status(
|
|
|
1194
1269
|
# --- inbox: notification and handover ---------------------------------------
|
|
1195
1270
|
|
|
1196
1271
|
|
|
1197
|
-
def inbox(
|
|
1272
|
+
def inbox(
|
|
1273
|
+
root: Path,
|
|
1274
|
+
project_root: Path,
|
|
1275
|
+
*,
|
|
1276
|
+
on_recovered: Callable[[dict], None] | None = None,
|
|
1277
|
+
) -> list[Round]:
|
|
1198
1278
|
"""Hand over rounds addressed to the caller that it has not been given yet.
|
|
1199
1279
|
|
|
1200
1280
|
The `delivered` event is written HERE, by the broker, as the message goes
|
|
1201
1281
|
out. The agent cannot assert it and cannot decline it, so an agent that
|
|
1202
1282
|
ignores its inbox leaves a visible `delivered` with no follow-up rather than
|
|
1203
1283
|
an absence indistinguishable from never having been told.
|
|
1284
|
+
|
|
1285
|
+
`on_recovered` receives each abandoned lock a delivery had to break. Every
|
|
1286
|
+
other writer returns that to its caller; this one returns rounds, and used
|
|
1287
|
+
to drop it -- on the busiest path, since every agent calls this at session
|
|
1288
|
+
start (aramid llm-review 1810f9e).
|
|
1204
1289
|
"""
|
|
1205
1290
|
root = require_channel(root)
|
|
1206
1291
|
agent = derive_identity(root, project_root)
|
|
@@ -1217,7 +1302,9 @@ def inbox(root: Path, project_root: Path) -> list[Round]:
|
|
|
1217
1302
|
pending.append((entry.number, entry))
|
|
1218
1303
|
|
|
1219
1304
|
for number, _entry in pending:
|
|
1220
|
-
_write_status_event(root, number, "delivered", agent, broker=True)
|
|
1305
|
+
event = _write_status_event(root, number, "delivered", agent, broker=True)
|
|
1306
|
+
if on_recovered is not None and "lock_recovered" in event:
|
|
1307
|
+
on_recovered(event["lock_recovered"])
|
|
1221
1308
|
return [entry for _number, entry in pending]
|
|
1222
1309
|
|
|
1223
1310
|
|
|
@@ -12,6 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from typing import Any, Callable, TypeGuard
|
|
13
13
|
|
|
14
14
|
from .daemon import DaemonStatusInvalidError, DaemonStatusTooLargeError, read_daemon_status
|
|
15
|
+
from .programs import system_program
|
|
15
16
|
from .routing.lifecycle import LifecycleReasonCode, ProviderLifecycleState
|
|
16
17
|
from .windows_task import DEFAULT_TASK_NAME
|
|
17
18
|
from .windows_startup import startup_status
|
|
@@ -285,7 +286,9 @@ def check_daemon_process(base: Path) -> dict[str, Any]:
|
|
|
285
286
|
)
|
|
286
287
|
try:
|
|
287
288
|
result = subprocess.run(
|
|
288
|
-
|
|
289
|
+
# By absolute path: a bare `powershell.exe` is looked up in the
|
|
290
|
+
# current directory before System32 (channel round 304).
|
|
291
|
+
[str(system_program("WindowsPowerShell", "v1.0", "powershell.exe")), "-NoProfile", "-Command", ps],
|
|
289
292
|
capture_output=True,
|
|
290
293
|
text=True,
|
|
291
294
|
# `errors` but deliberately NO `encoding`: Windows PowerShell 5.1
|
|
@@ -5,7 +5,6 @@ import importlib.util
|
|
|
5
5
|
import json
|
|
6
6
|
import math
|
|
7
7
|
import os
|
|
8
|
-
import shutil
|
|
9
8
|
import subprocess
|
|
10
9
|
import sys
|
|
11
10
|
import threading
|
|
@@ -25,6 +24,7 @@ from .hookinstall import DEFAULT_HOOKS_DIRNAME, hook_shim_present, hooks_dir
|
|
|
25
24
|
from .init import gitignored_managed_paths, managed_doc_paths
|
|
26
25
|
from .hookshim import TRIGGERS
|
|
27
26
|
from .llm import canonical_provider_name
|
|
27
|
+
from .programs import resolve_program
|
|
28
28
|
from .validation import validate_graph_bundle
|
|
29
29
|
|
|
30
30
|
DoctorStatus = Literal["ready", "optional", "degraded", "blocked"]
|
|
@@ -469,7 +469,10 @@ def check_daemon(root: Path, daemon_base: Path) -> DoctorCheck:
|
|
|
469
469
|
|
|
470
470
|
def check_mcp() -> DoctorCheck:
|
|
471
471
|
package = importlib.util.find_spec("mcp") is not None
|
|
472
|
-
|
|
472
|
+
# Not `shutil.which`: on Windows it answers from the current directory
|
|
473
|
+
# first, so a `graphite-mcp.exe` sitting in the repo read as installed
|
|
474
|
+
# (channel round 304).
|
|
475
|
+
command = resolve_program("graphite-mcp", extensions=(".exe", ".cmd", ".bat", ".com")) is not None
|
|
473
476
|
details = {"python_package": package, "command": command}
|
|
474
477
|
if package and command:
|
|
475
478
|
return DoctorCheck("mcp", "MCP", "ready", "MCP integration is available.", details)
|
|
@@ -498,26 +501,7 @@ def check_typescript(root: Path, *, timeout_seconds: float = 5.0) -> DoctorCheck
|
|
|
498
501
|
|
|
499
502
|
|
|
500
503
|
def _resolve_node_executable(root: Path, *, platform_name: str | None = None) -> Path | None:
|
|
501
|
-
|
|
502
|
-
name = "node.exe" if selected_platform == "nt" else "node"
|
|
503
|
-
resolved_root = root.resolve()
|
|
504
|
-
for raw_directory in os.environ.get("PATH", "").split(os.pathsep):
|
|
505
|
-
directory = Path(raw_directory)
|
|
506
|
-
if not raw_directory or not directory.is_absolute():
|
|
507
|
-
continue
|
|
508
|
-
try:
|
|
509
|
-
candidate = (directory / name).resolve(strict=True)
|
|
510
|
-
if not candidate.is_file():
|
|
511
|
-
continue
|
|
512
|
-
if selected_platform != "nt" and not os.access(candidate, os.X_OK):
|
|
513
|
-
continue
|
|
514
|
-
try:
|
|
515
|
-
candidate.relative_to(resolved_root)
|
|
516
|
-
except ValueError:
|
|
517
|
-
return candidate
|
|
518
|
-
except OSError:
|
|
519
|
-
continue
|
|
520
|
-
return None
|
|
504
|
+
return resolve_program("node", exclude=(root,), platform_name=platform_name)
|
|
521
505
|
|
|
522
506
|
|
|
523
507
|
def _node_environment() -> dict[str, str]:
|
|
@@ -53,6 +53,14 @@ _LLM_CATEGORIES = frozenset(
|
|
|
53
53
|
)
|
|
54
54
|
_LLM_OUTPUT_LIMIT_BYTES = 4096
|
|
55
55
|
_LLM_TIMEOUT_MAX_SECONDS = 60.0
|
|
56
|
+
#: Added to the provider timeout to make the WORKER's process budget. The two
|
|
57
|
+
#: used to be one number, so the worker's interpreter start was spent from the
|
|
58
|
+
#: provider's time, and a loaded machine read `timeout` for a provider that was
|
|
59
|
+
#: merely misconfigured (2 s read `timeout` 7 in 10 under 36 CPU burners). The
|
|
60
|
+
#: worker enforces the provider timeout itself and reports `timeout` when the
|
|
61
|
+
#: provider is slow, so this only bounds a worker that never answers. Generous
|
|
62
|
+
#: on purpose: it costs nothing unless the worker hangs.
|
|
63
|
+
_LLM_WORKER_START_ALLOWANCE_SECONDS = 30.0
|
|
56
64
|
_REQUIRED_MCP_TOOLS = frozenset(
|
|
57
65
|
{"graphite_query", "graphite_summary", "graphite_community", "graphite_refresh"}
|
|
58
66
|
)
|
|
@@ -2100,7 +2108,7 @@ def probe_llm(
|
|
|
2100
2108
|
[str(python), "-I", "-S", "-B", str(worker)],
|
|
2101
2109
|
cwd=python.parent,
|
|
2102
2110
|
stdin=stdin,
|
|
2103
|
-
timeout_seconds=timeout_seconds,
|
|
2111
|
+
timeout_seconds=timeout_seconds + _LLM_WORKER_START_ALLOWANCE_SECONDS,
|
|
2104
2112
|
max_output_bytes=_LLM_OUTPUT_LIMIT_BYTES,
|
|
2105
2113
|
check=False,
|
|
2106
2114
|
)
|
|
@@ -9,6 +9,8 @@ from dataclasses import dataclass
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Sequence
|
|
11
11
|
|
|
12
|
+
from .programs import resolve_program
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
DEFAULT_GIT_STDOUT_MAX_BYTES = 16 * 1024 * 1024
|
|
14
16
|
_MINIMUM_GIT_VERSION = (2, 38, 0)
|
|
@@ -376,35 +378,12 @@ def _close_stdout(process: subprocess.Popen[bytes]) -> bool:
|
|
|
376
378
|
def _resolve_git_executable(
|
|
377
379
|
resolved_project_root: Path, *, platform_name: str | None = None
|
|
378
380
|
) -> Path:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
directory = Path(raw_directory)
|
|
386
|
-
if not directory.is_absolute():
|
|
387
|
-
continue
|
|
388
|
-
candidate = directory / executable_name
|
|
389
|
-
try:
|
|
390
|
-
if not candidate.is_file():
|
|
391
|
-
continue
|
|
392
|
-
resolved_candidate = candidate.resolve(strict=True)
|
|
393
|
-
if not resolved_candidate.is_file():
|
|
394
|
-
continue
|
|
395
|
-
if selected_platform != "nt" and not os.access(resolved_candidate, os.X_OK):
|
|
396
|
-
continue
|
|
397
|
-
try:
|
|
398
|
-
resolved_candidate.relative_to(resolved_project_root)
|
|
399
|
-
except ValueError:
|
|
400
|
-
pass
|
|
401
|
-
else:
|
|
402
|
-
continue
|
|
403
|
-
except OSError:
|
|
404
|
-
continue
|
|
405
|
-
return resolved_candidate
|
|
406
|
-
|
|
407
|
-
raise GitUnavailableError("Git executable was not found")
|
|
381
|
+
resolved = resolve_program(
|
|
382
|
+
"git", exclude=(resolved_project_root,), platform_name=platform_name
|
|
383
|
+
)
|
|
384
|
+
if resolved is None:
|
|
385
|
+
raise GitUnavailableError("Git executable was not found")
|
|
386
|
+
return resolved
|
|
408
387
|
|
|
409
388
|
|
|
410
389
|
def _isolated_environment() -> dict[str, str]:
|
|
@@ -26,6 +26,7 @@ from pathlib import Path
|
|
|
26
26
|
|
|
27
27
|
from .config import default_projects_root
|
|
28
28
|
from .hookshim import CHAINED_SUFFIX, MARKER_START, TRIGGERS, render_trigger_shim
|
|
29
|
+
from .programs import require_program
|
|
29
30
|
|
|
30
31
|
DEFAULT_HOOKS_DIRNAME = ".githooks"
|
|
31
32
|
|
|
@@ -40,8 +41,12 @@ def _git(root: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
|
|
40
41
|
# locale codec, and git echoes repository paths and branch names that are
|
|
41
42
|
# not required to be Latin-1. A failure decodes on subprocess's reader
|
|
42
43
|
# thread, so it yields `stdout is None` rather than raising.
|
|
44
|
+
#
|
|
45
|
+
# By absolute path, from outside the repo (channel round 304): `init` runs
|
|
46
|
+
# from the root of the repo being onboarded, and Windows looked a bare `git`
|
|
47
|
+
# up there before PATH. A missing git still raises FileNotFoundError.
|
|
43
48
|
return subprocess.run(
|
|
44
|
-
["git", "-C", str(root), *args],
|
|
49
|
+
[str(require_program("git", exclude=(root,))), "-C", str(root), *args],
|
|
45
50
|
capture_output=True,
|
|
46
51
|
text=True,
|
|
47
52
|
encoding="utf-8",
|
|
@@ -19,6 +19,7 @@ from .git import GitError, GitRunner
|
|
|
19
19
|
from . import hookinstall
|
|
20
20
|
from .hookinstall import DEFAULT_HOOKS_DIRNAME
|
|
21
21
|
from .io import atomic_write_text
|
|
22
|
+
from .programs import require_program
|
|
22
23
|
|
|
23
24
|
# Bump whenever GRAPHITE_DOC, SHARED_POINTER, or CURSOR_POINTER changes;
|
|
24
25
|
# test_template_change_requires_doc_version_bump pins the pairing. Files
|
|
@@ -320,8 +321,10 @@ def _is_git_repo(root: Path) -> bool:
|
|
|
320
321
|
"""Ground truth, not a `.git`-exists heuristic: `--is-inside-work-tree`
|
|
321
322
|
also answers correctly for a repo `root` nested inside a larger one,
|
|
322
323
|
matching how `hookinstall`'s own `git -C root ...` calls resolve."""
|
|
324
|
+
# By absolute path, from outside the repo (channel round 304); see
|
|
325
|
+
# `hookinstall._git`.
|
|
323
326
|
result = subprocess.run(
|
|
324
|
-
["git", "-C", str(root), "rev-parse", "--is-inside-work-tree"],
|
|
327
|
+
[str(require_program("git", exclude=(root,))), "-C", str(root), "rev-parse", "--is-inside-work-tree"],
|
|
325
328
|
capture_output=True, text=True,
|
|
326
329
|
# See `channel._git`: `text=True` alone uses the locale codec, and a
|
|
327
330
|
# decode failure returns None instead of raising -- which here would
|
|
@@ -172,8 +172,10 @@ class GraphiteMCPServer:
|
|
|
172
172
|
def channel_inbox_tool(self) -> dict[str, Any]:
|
|
173
173
|
from .channel import inbox
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
def handover(root: Path) -> dict[str, Any]:
|
|
176
|
+
recovered: list[dict] = []
|
|
177
|
+
rounds = inbox(root, self.project_root, on_recovered=recovered.append)
|
|
178
|
+
result: dict[str, Any] = {
|
|
177
179
|
"ok": True,
|
|
178
180
|
"rounds": [
|
|
179
181
|
{
|
|
@@ -183,10 +185,16 @@ class GraphiteMCPServer:
|
|
|
183
185
|
"posted": entry.posted,
|
|
184
186
|
"body": entry.body,
|
|
185
187
|
}
|
|
186
|
-
for entry in
|
|
188
|
+
for entry in rounds
|
|
187
189
|
],
|
|
188
190
|
}
|
|
189
|
-
|
|
191
|
+
# Present only when a lock was broken, like `post` and `status`: a
|
|
192
|
+
# quiet key on every response is noise nobody reads.
|
|
193
|
+
if recovered:
|
|
194
|
+
result["lock_recovered"] = recovered
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
return self._channel_call(handover)
|
|
190
198
|
|
|
191
199
|
def channel_status_tool(
|
|
192
200
|
self, *, number: int, status: str, reason: str | None = None
|