graphite-code 1.2.0__tar.gz → 1.2.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.2.0 → graphite_code-1.2.1}/CHANGELOG.md +69 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/PKG-INFO +2 -2
- {graphite_code-1.2.0 → graphite_code-1.2.1}/README.md +1 -1
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/__init__.py +1 -1
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/agent_hooks.py +121 -20
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/ingest.py +16 -3
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_agent_hooks.py +319 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_hardening.py +17 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/.gitignore +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/ARCHITECTURE.md +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/CONTRIBUTING.md +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/LICENSE +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/RELEASING.md +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/pyproject.toml +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/__main__.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/_cleanup_worker.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/_win32_ctypes.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/activation.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/agent_settings.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/analyze.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/answer_contract.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/bootstrap.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/buildlock.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/cache.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/channel.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/cli.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/cluster.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/config.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/context.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/daemon.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/daemon_health.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/daemon_launch.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/debt.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/dependency_install.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/detach.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/doctor.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/doctor_probes.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/engine_identity.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/export/__init__.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/export/html.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/export/json.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/export/md.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/extract/__init__.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/extract/ast.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/extract/pyscope.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/freshness.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/git.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/graph.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/graph_io.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/health.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/hook_entry.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/hookinstall.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/hookshim.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/incident_ledger.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/init.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/io.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/launchd_agent.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/listing.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/llm.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/llm_probe.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/mcp.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/mcp_server.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/natural_query.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/overlays.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/probe_process.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/probe_workspace.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/process_contracts.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/programs.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/provider_observer.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/py.typed +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/query.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/query_plan.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/replacement_audit.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/resolve.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/review.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/__init__.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/approval.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/classifier.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/claude_executor.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/claude_probe.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/cli_identity.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/codex_executor.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/codex_probe.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/context_builder.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/contracts.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/diff_policy.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/edit_apply.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/effort.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/lifecycle.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/lifecycle_operator.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/lifecycle_service.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/lifecycle_storage.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/ollama_executor.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/ollama_probe.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/openrouter_executor.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/openrouter_probe.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/policy.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/probe_runner.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/process_runner.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/profiles.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/prompt.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/registry.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/route_pool.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/route_pool_execution.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/schema_validation.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/service.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/settings.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/shadow.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/storage.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/telemetry.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/worktree.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/zai_edit.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/zai_executor.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/routing/zai_probe.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/savings.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/systemd_unit.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/ts_bridge.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/ts_resolver.mjs +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/typescript_activation.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/usage_ledger.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/validation.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/watch.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/windows_job.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/windows_startup.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/src/graphite/windows_task.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/conftest.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/data/sqloracle/blindspots.py.txt +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/data/sqloracle/round69.py.txt +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/fake_clis/fake_cli.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/fixtures/provider_lifecycle_schema_v1.sql +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/fixtures/routing_schema_v2_ca77600.sql +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/fixtures/routing_schema_v3_94eb333.sql +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/fixtures/routing_schema_v4_lifecycle_migration.sql +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test__win32_ctypes.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_activation.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_activation_backstop.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_agent_hook_activation.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_agent_settings.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_answer_contract.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_arrow_and_constructor_binding.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_atomic_write.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_bootstrap.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_build_detach.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_buildlock.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_cache_engine_identity.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_cache_entry_pruning.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_cache_file_set.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_cache_pruning.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_call_graph.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_case_ambiguous_answer.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_channel_attribution.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_channel_cli.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_channel_core.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_channel_kind.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_channel_lock.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_channel_log_integrity.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_channel_mcp.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_channel_register.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_channel_report.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_channel_status.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_class_field_arrow_binding.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_cli_channel.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_cli_channel_attribution.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_cli_reference.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_cli_version.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_cluster.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_compare_dist.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_configuration_reference.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_context.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_context_builder.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_cwd_relative_config.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_cycle_search.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_daemon.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_daemon_activation.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_daemon_build_lock.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_daemon_child_lock.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_daemon_engine_staleness.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_daemon_health.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_daemon_health_activation.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_daemon_launch.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_daemon_service_cli.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_debt.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_dependency_install.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_destructured_binding.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_determinism.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_dispatch_evidence.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_doctor.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_doctor_agent_hooks.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_doctor_hooks.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_doctor_managed_docs.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_doctor_probes.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_documentation.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_edit_apply.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_engine_identity.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_exit_codes_reference.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_external_calls.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_git_security.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_go_rust.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_graph_io.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_graph_provider_isolation.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_health.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_hook_entry.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_hook_template.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_hookinstall.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_hookshim.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_html_security.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_incident_ledger.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_init.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_init_activation_doctrine.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_init_hooks.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_init_validation_exit.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_launchd_agent.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_lifecycle_operator.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_listing.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_listing_surfaces.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_llm.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_llm_probe.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_mcp.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_mcp_server.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_mcp_server_channel_attribution.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_method_dispatch_scope.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_monorepo.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_mutation_tests.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_natural_query.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_node_identity.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_ollama_executor.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_overlays.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_path_leak_fixture.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_probe_diagnostics.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_probe_process.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_probe_workspace.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_process_contracts.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_programs.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_provider_claude_probe.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_provider_codex_probe.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_provider_lifecycle.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_provider_lifecycle_service.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_provider_lifecycle_storage.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_provider_observer.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_provider_ollama_probe.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_provider_openrouter_probe.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_provider_probe_runner.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_public_surface.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_published_schemas.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_pyscope.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_pyscopeoracle.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_python_import_bindings.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_python_resolver.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_python_scope_identity.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_query_plan.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_reliability.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_replacement_audit.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_resolve.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_review.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_route_pool.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_route_pool_execution.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_approval.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_benchmark.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_classifier.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_claude_executor.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_cli.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_cli_contracts.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_cli_recovery.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_codex_executor.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_context.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_contracts.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_diff_policy.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_executor.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_openrouter_executor.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_policy.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_process_runner.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_profiles.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_registry.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_schema_validation.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_security.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_service.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_shadow.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_storage.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_telemetry.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_worktree.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_zai_executor.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_routing_zai_probe.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_savings.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_search.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_smoke.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_sqloracle.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_synthetic_repo.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_systemd_unit.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_ts_bridge.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_typescript_activation.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_typescript_resolver.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_usage_ledger.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_verify_artifact.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_verify_published_release.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_watch.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_windows_startup.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_windows_task.py +0 -0
- {graphite_code-1.2.0 → graphite_code-1.2.1}/tests/test_zai_edit.py +0 -0
|
@@ -15,6 +15,75 @@ machine-checkable identity; the version is for humans.
|
|
|
15
15
|
|
|
16
16
|
## [Unreleased]
|
|
17
17
|
|
|
18
|
+
## [1.2.1] — 2026-10-11
|
|
19
|
+
|
|
20
|
+
A patch release. It carries one fix, to the agent hooks (#80). No stable
|
|
21
|
+
surface in `docs/compatibility.md` changes: nothing under `docs/reference/` or
|
|
22
|
+
`docs/schemas/` moves, `graphite agent-hook` still always exits 0, and the
|
|
23
|
+
wording of a hook's message is not a stable surface. The graph itself does not
|
|
24
|
+
change: on this repository, measured against the deployed 1.2.0 engine,
|
|
25
|
+
8581 of 8581 node ids survive, 23612 of 23612 edge triples are unchanged,
|
|
26
|
+
every relation count is equal, and `analysis` is identical. Only the engine
|
|
27
|
+
fingerprint moves, because graphite's own bytes did.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
**The agent hooks find the repository from a subfolder (#80).** A hook payload
|
|
32
|
+
carries the session's directory, and in Claude Code that follows a `cd` in the
|
|
33
|
+
shell. All three hooks took it for the repository root. One directory below the
|
|
34
|
+
root, with the graph fresh at the root:
|
|
35
|
+
|
|
36
|
+
- `session-start` said `Graph status: missing`, the status that licenses a full
|
|
37
|
+
fallback from graph-first. The remedy it printed, `python -m graphite build .`,
|
|
38
|
+
is refused there ("nested Git roots are unsupported").
|
|
39
|
+
- `pre-tool-use` wrote nothing: no refusal in strict mode and no reminder. A
|
|
40
|
+
`cd` into any subfolder switched the gate off.
|
|
41
|
+
- `stop` created a `.graphite/local/` in the subfolder for its cursor and looked
|
|
42
|
+
for the turn's usage there.
|
|
43
|
+
- The subfolder was registered as an open project. The daemon cannot build
|
|
44
|
+
one, so it logged `project_snapshot_failed` for it at every discovery cycle,
|
|
45
|
+
and the repository's own marker was no longer renewed.
|
|
46
|
+
|
|
47
|
+
The repository is now the nearest directory upward that holds `.git`, the test
|
|
48
|
+
the build already uses to refuse a nested path (`ingest.enclosing_git_root`).
|
|
49
|
+
It is taken only where graphite is set up: a `graph-out/graph.json`, or the
|
|
50
|
+
`GRAPHITE.md` that `init` commits. In a subfolder of any other checkout, and in
|
|
51
|
+
a directory outside every checkout, the hooks read the session's directory as
|
|
52
|
+
they did. A repository inside the repository, with its own `.git`, stays its
|
|
53
|
+
own root.
|
|
54
|
+
|
|
55
|
+
One reading changes inside a checkout that is set up. A folder there that has
|
|
56
|
+
its own `GRAPHITE.md` or `graph-out/` but no `.git` was read as a project of
|
|
57
|
+
its own: `Graph status: missing`, or `unknown` over a leftover graph, with a
|
|
58
|
+
`build .` that is refused there. It is now read as part of the checkout.
|
|
59
|
+
|
|
60
|
+
Below the root:
|
|
61
|
+
|
|
62
|
+
- The graph, the freshness check, the usage ledger and the activation marker
|
|
63
|
+
are the repository's.
|
|
64
|
+
- A search's relative paths, and a search that names no path, belong to the
|
|
65
|
+
directory the shell is in. `grep <symbol> core.py` beside the file is one
|
|
66
|
+
file. A search with no path in a `docs/` that holds no code is text. `..` and
|
|
67
|
+
`../src` are judged as the directories they are.
|
|
68
|
+
- Once the command line has run a `cd` the hook does not follow, or any command
|
|
69
|
+
it does not model, the shell may be anywhere in the repository. A search then
|
|
70
|
+
keeps an absolute path, and a relative one that names a file in the session's
|
|
71
|
+
directory. With a directory, or no path, it is judged as a search of the
|
|
72
|
+
whole repository: from a `docs/` with no code, `cd ../src ; grep -rn <symbol> .`
|
|
73
|
+
is refused. At the root this case resolves paths against the root, as before.
|
|
74
|
+
- Every status, refusal and reminder ends by naming the root: "This session is
|
|
75
|
+
below the repository root: run graphite from `<root>`." The CLI still looks
|
|
76
|
+
for the graph in its own working directory (#81), so the commands those
|
|
77
|
+
messages print work from the root only.
|
|
78
|
+
|
|
79
|
+
The hooks no longer register a directory below a checkout's root with the
|
|
80
|
+
daemon, whether or not graphite is set up in that checkout.
|
|
81
|
+
|
|
82
|
+
Nothing changes for a session at the repository root. Measured on the 1.2.0
|
|
83
|
+
wheel and on this change: 32 strict-hook arms from the #72 work (25 refused, 7
|
|
84
|
+
allowed) and 12 hook arms on a checkout with `.git` and `GRAPHITE.md` read the
|
|
85
|
+
same on both.
|
|
86
|
+
|
|
18
87
|
## [1.2.0] — 2026-10-10
|
|
19
88
|
|
|
20
89
|
A minor release. Query and search results, channel rounds and Python
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: graphite-code
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.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.2.
|
|
51
|
+
**Status: 1.2.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.2.
|
|
5
|
+
**Status: 1.2.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.2.
|
|
41
|
+
__version__ = "1.2.1"
|
|
@@ -21,7 +21,9 @@ from .config import Config
|
|
|
21
21
|
from .freshness import check_graph_freshness
|
|
22
22
|
from .graph_io import load_validated_graph_bundle
|
|
23
23
|
from .health import _edge_language, is_placeholder, persisted_resolution
|
|
24
|
+
from .hook_entry import MARKER as _SETUP_MARKER
|
|
24
25
|
from .incident_ledger import record_incident, repo_ledger_dir
|
|
26
|
+
from .ingest import enclosing_git_root
|
|
25
27
|
|
|
26
28
|
SESSION_CONTRACT = (
|
|
27
29
|
"graphite-first: this repo uses a shared code graph. For cross-file questions "
|
|
@@ -34,22 +36,64 @@ SESSION_CONTRACT = (
|
|
|
34
36
|
_FRESHNESS_BUDGET_SECONDS = 2.0
|
|
35
37
|
|
|
36
38
|
|
|
37
|
-
def
|
|
39
|
+
def _payload_cwd(payload: dict[str, Any]) -> Path:
|
|
40
|
+
"""The directory the session is in. It follows a `cd` in the shell."""
|
|
38
41
|
cwd = payload.get("cwd")
|
|
39
42
|
if isinstance(cwd, str) and cwd:
|
|
40
43
|
return Path(cwd)
|
|
41
44
|
return Path.cwd()
|
|
42
45
|
|
|
43
46
|
|
|
47
|
+
def _repo_root(cwd: Path) -> Path:
|
|
48
|
+
"""The repository a session in `cwd` belongs to: where its graph is.
|
|
49
|
+
|
|
50
|
+
Taking the session's directory for the root made the graph "missing" one
|
|
51
|
+
`cd` below it, and switched the gate off there (#80). The repository is the
|
|
52
|
+
nearest directory upward that holds `.git`, the one place a build of it can
|
|
53
|
+
run (`ingest.enclosing_git_root`).
|
|
54
|
+
|
|
55
|
+
That directory is used only where graphite is already set up: it has a
|
|
56
|
+
graph, or the `GRAPHITE.md` that `init` commits. Anywhere else `cwd` stands,
|
|
57
|
+
as it did before, so a session that wanders into a subfolder of some other
|
|
58
|
+
checkout does not hand the daemon that whole project to build.
|
|
59
|
+
"""
|
|
60
|
+
top = enclosing_git_root(cwd)
|
|
61
|
+
if top is None or top == cwd:
|
|
62
|
+
return cwd
|
|
63
|
+
if (top / "graph-out" / "graph.json").is_file() or (top / _SETUP_MARKER).is_file():
|
|
64
|
+
return top
|
|
65
|
+
return cwd
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _where_to_run(root: Path, cwd: Path) -> str:
|
|
69
|
+
"""One sentence for a session below the repository root, else nothing.
|
|
70
|
+
|
|
71
|
+
The commands these hooks print are run where the agent is, and the CLI
|
|
72
|
+
looks for the graph in its own working directory. Below the root `query`,
|
|
73
|
+
`context` and `check .` report the graph missing and `build .` is refused,
|
|
74
|
+
so a status or a refusal that names no directory sends the agent to a
|
|
75
|
+
command that fails.
|
|
76
|
+
"""
|
|
77
|
+
if cwd == root:
|
|
78
|
+
return ""
|
|
79
|
+
return f" This session is below the repository root: run graphite from `{root}`."
|
|
80
|
+
|
|
81
|
+
|
|
44
82
|
def _activate(root: Path) -> None:
|
|
45
83
|
"""Register this repo as open, so the daemon supervises it and leaves the
|
|
46
84
|
rest of the machine alone.
|
|
47
85
|
|
|
86
|
+
A directory below a checkout's root is never registered. No build can run
|
|
87
|
+
there (`ingest` refuses it), so the daemon would fail on it at every
|
|
88
|
+
discovery cycle for as long as the marker lived.
|
|
89
|
+
|
|
48
90
|
Guarded separately from the caller's own try/except: activation is
|
|
49
91
|
bookkeeping, and this module is fail-open by contract -- a broken registry
|
|
50
92
|
must never cost the agent its session context or its graph-first reminder.
|
|
51
93
|
"""
|
|
52
94
|
try:
|
|
95
|
+
if enclosing_git_root(root) not in (None, root):
|
|
96
|
+
return
|
|
53
97
|
mark_active(root, "claude")
|
|
54
98
|
except Exception:
|
|
55
99
|
return
|
|
@@ -97,7 +141,8 @@ def _freshness_within_budget(root: Path) -> tuple[str, str | None]:
|
|
|
97
141
|
|
|
98
142
|
def handle_session_start(payload: dict[str, Any]) -> dict[str, Any] | None:
|
|
99
143
|
try:
|
|
100
|
-
|
|
144
|
+
cwd = _payload_cwd(payload)
|
|
145
|
+
root = _repo_root(cwd)
|
|
101
146
|
_activate(root)
|
|
102
147
|
if (root / "graph-out" / "graph.json").is_file():
|
|
103
148
|
state, reason = _freshness_within_budget(root)
|
|
@@ -121,7 +166,7 @@ def handle_session_start(payload: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
121
166
|
return {
|
|
122
167
|
"hookSpecificOutput": {
|
|
123
168
|
"hookEventName": "SessionStart",
|
|
124
|
-
"additionalContext": f"{SESSION_CONTRACT}\n{status}",
|
|
169
|
+
"additionalContext": f"{SESSION_CONTRACT}\n{status}{_where_to_run(root, cwd)}",
|
|
125
170
|
}
|
|
126
171
|
}
|
|
127
172
|
except Exception:
|
|
@@ -435,14 +480,22 @@ def _shell_path(text: str, msys: bool) -> Path:
|
|
|
435
480
|
return Path(text)
|
|
436
481
|
|
|
437
482
|
|
|
438
|
-
def _cd_target(start: Path, arguments: list[str], msys: bool) -> Path | None:
|
|
439
|
-
"""The directory `cd <arguments>` leaves the shell in, or None when unknown.
|
|
483
|
+
def _cd_target(start: Path | None, arguments: list[str], msys: bool) -> Path | None:
|
|
484
|
+
"""The directory `cd <arguments>` leaves the shell in, or None when unknown.
|
|
485
|
+
|
|
486
|
+
`start` is None when the hook does not know where the shell is. From there
|
|
487
|
+
only an absolute path leads somewhere known.
|
|
488
|
+
"""
|
|
440
489
|
if len(arguments) != 1 or os.environ.get("CDPATH"):
|
|
441
490
|
return None # with `CDPATH` set, `cd docs` may land somewhere other than `./docs`
|
|
442
491
|
text = arguments[0]
|
|
443
492
|
if text.startswith("-") or _SHELL_EXPANSION_CHARS.intersection(text):
|
|
444
493
|
return None
|
|
445
|
-
target =
|
|
494
|
+
target = _shell_path(text, msys)
|
|
495
|
+
if start is not None:
|
|
496
|
+
target = start / target # an absolute path replaces `start`
|
|
497
|
+
elif not target.is_absolute():
|
|
498
|
+
return None
|
|
446
499
|
try:
|
|
447
500
|
return target if target.is_dir() else None
|
|
448
501
|
except OSError: # pragma: no cover - a path the platform cannot stat
|
|
@@ -542,7 +595,29 @@ def _search_in(name: str, rest: list[str], base: Path | None, msys: bool) -> _Se
|
|
|
542
595
|
return _Search(positional[0], _located(positional[1:], base, msys), fold_case)
|
|
543
596
|
|
|
544
597
|
|
|
545
|
-
def
|
|
598
|
+
def _unplaced(search: _Search, cwd: Path) -> _Search:
|
|
599
|
+
"""`search` when the hook does not know which directory it runs in.
|
|
600
|
+
|
|
601
|
+
An absolute path says where it is. A relative one that names a file in the
|
|
602
|
+
session's directory is still one file's text, as it is at the root:
|
|
603
|
+
`ls && grep x notes.md`. A directory, a path that is not there, and a
|
|
604
|
+
search that names no path could be anywhere in the repository. The search
|
|
605
|
+
is then one of all of it, and no path is left to earn an exemption.
|
|
606
|
+
"""
|
|
607
|
+
placed: list[str] = []
|
|
608
|
+
for text in search.paths:
|
|
609
|
+
path = Path(text)
|
|
610
|
+
if not path.is_absolute():
|
|
611
|
+
path = cwd / path
|
|
612
|
+
if not path.is_file():
|
|
613
|
+
return search._replace(paths=[])
|
|
614
|
+
placed.append(str(path))
|
|
615
|
+
return search._replace(paths=placed)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _bash_searches(
|
|
619
|
+
command: str, root: Path, msys: bool = False, cwd: Path | None = None
|
|
620
|
+
) -> list[_Search]:
|
|
546
621
|
"""Every search `command` runs that is not downstream of a pipe.
|
|
547
622
|
|
|
548
623
|
All of them, not the first: `grep x notes.md ; grep -rn <symbol> src/` is
|
|
@@ -557,12 +632,23 @@ def _bash_searches(command: str, root: Path, msys: bool = False) -> list[_Search
|
|
|
557
632
|
`cd docs && ls && grep ...` is not followed. In every such case, and when
|
|
558
633
|
the directory is not one the hook can find, paths resolve against the
|
|
559
634
|
repository root as they did before `cd` was read at all.
|
|
635
|
+
|
|
636
|
+
`cwd` is the directory the shell starts in when that is below the root
|
|
637
|
+
(#80). The line's first command runs there, and so does a search reached
|
|
638
|
+
through searches alone. Once a `cd` or a command the hook does not model
|
|
639
|
+
has run, the shell may be anywhere. At the root that costs nothing, since
|
|
640
|
+
the root holds every directory it could be in. Below the root the session's
|
|
641
|
+
directory holds only itself: from a `docs/` with no code in it,
|
|
642
|
+
`cd ../src ; grep` would pass as a search of `docs/`. There the search
|
|
643
|
+
keeps only the paths that do not depend on where the shell is (`_unplaced`).
|
|
560
644
|
"""
|
|
561
645
|
searches: list[_Search] = []
|
|
562
|
-
base: Path | None = None
|
|
646
|
+
base: Path | None = cwd # None is the repository root
|
|
647
|
+
placed = True # `base` is where the shell is
|
|
648
|
+
moved = False # a `cd`, or a command the hook does not model, has run
|
|
563
649
|
for head in _bash_command_heads(command):
|
|
564
650
|
if head.before != "&&":
|
|
565
|
-
base = None
|
|
651
|
+
base, placed = (None, False) if moved else (cwd, True)
|
|
566
652
|
argv = head.argv
|
|
567
653
|
while argv and _ENV_ASSIGNMENT_RE.match(argv[0]):
|
|
568
654
|
argv = argv[1:]
|
|
@@ -574,16 +660,22 @@ def _bash_searches(command: str, root: Path, msys: bool = False) -> list[_Search
|
|
|
574
660
|
name = name[:-4]
|
|
575
661
|
rest = argv[1:]
|
|
576
662
|
if name in _CD_COMMANDS:
|
|
577
|
-
|
|
663
|
+
moved = True
|
|
664
|
+
# At the root an unplaced shell is read as being at the root. Below
|
|
665
|
+
# it, only an absolute path leads somewhere known.
|
|
666
|
+
start = base or (root if cwd is None else None)
|
|
667
|
+
base = _cd_target(start, rest, msys) if head.after == "&&" else None
|
|
668
|
+
placed = base is not None
|
|
578
669
|
continue
|
|
579
670
|
if name == "git" and rest and rest[0] == "grep":
|
|
580
671
|
name, rest = "grep", rest[1:]
|
|
581
672
|
if name not in _SEARCH_COMMANDS:
|
|
582
|
-
|
|
673
|
+
# `popd`, `builtin cd ..`, a function: the shell may have moved
|
|
674
|
+
base, placed, moved = None, False, True
|
|
583
675
|
continue
|
|
584
676
|
found = _search_in(name, rest, base, msys)
|
|
585
677
|
if found is not None:
|
|
586
|
-
searches.append(found)
|
|
678
|
+
searches.append(found if placed or cwd is None else _unplaced(found, cwd))
|
|
587
679
|
return searches
|
|
588
680
|
|
|
589
681
|
|
|
@@ -692,7 +784,7 @@ def _denial_for(root: Path, searches: list[_Search]) -> str | None:
|
|
|
692
784
|
return None
|
|
693
785
|
|
|
694
786
|
|
|
695
|
-
def _strict_denial(payload: dict[str, Any], root: Path) -> str | None:
|
|
787
|
+
def _strict_denial(payload: dict[str, Any], root: Path, cwd: Path | None = None) -> str | None:
|
|
696
788
|
try:
|
|
697
789
|
tool_input = payload.get("tool_input")
|
|
698
790
|
if not isinstance(tool_input, dict):
|
|
@@ -702,6 +794,10 @@ def _strict_denial(payload: dict[str, Any], root: Path) -> str | None:
|
|
|
702
794
|
return None
|
|
703
795
|
target = tool_input.get("path")
|
|
704
796
|
paths = [target] if isinstance(target, str) and target else []
|
|
797
|
+
if cwd is not None:
|
|
798
|
+
# Below the root: with no `path` the tool searches the session's
|
|
799
|
+
# directory, and a relative one starts there.
|
|
800
|
+
paths = _located(paths, cwd, False)
|
|
705
801
|
# The Grep tool matches case exactly unless `-i` is set. A value that
|
|
706
802
|
# is not plainly "off" is read as on: unsure folds.
|
|
707
803
|
# The tool is built on ripgrep, so it is read as `rg` with no flags of
|
|
@@ -747,7 +843,7 @@ def _cursor_number(value: Any, cast: type) -> Any:
|
|
|
747
843
|
def handle_stop(payload: dict[str, Any]) -> dict[str, Any] | None:
|
|
748
844
|
"""Turn-end savings summary; silent unless graphite was used this turn."""
|
|
749
845
|
try:
|
|
750
|
-
root =
|
|
846
|
+
root = _repo_root(_payload_cwd(payload))
|
|
751
847
|
# Stop fires once per assistant turn, which makes it the heartbeat that
|
|
752
848
|
# keeps a working session inside the activation TTL. It must run before
|
|
753
849
|
# the session_id guard below, which returns early on most turns.
|
|
@@ -821,9 +917,14 @@ def handle_pre_tool_use(payload: dict[str, Any], mode: str) -> dict[str, Any] |
|
|
|
821
917
|
tool_name = payload.get("tool_name")
|
|
822
918
|
if tool_name not in ("Grep", "Glob", "Bash", "PowerShell"):
|
|
823
919
|
return None
|
|
824
|
-
|
|
920
|
+
cwd = _payload_cwd(payload)
|
|
921
|
+
root = _repo_root(cwd)
|
|
825
922
|
if not (root / "graph-out" / "graph.json").is_file():
|
|
826
923
|
return None
|
|
924
|
+
# Two directories, not one: the graph is at `root`, and a search's
|
|
925
|
+
# relative paths start where the shell is.
|
|
926
|
+
below = None if cwd == root else cwd
|
|
927
|
+
where = _where_to_run(root, cwd)
|
|
827
928
|
denial: str | None = None
|
|
828
929
|
if tool_name in ("Bash", "PowerShell"):
|
|
829
930
|
# The matcher now routes EVERY Bash call here, so anything that is
|
|
@@ -835,13 +936,13 @@ def handle_pre_tool_use(payload: dict[str, Any], mode: str) -> dict[str, Any] |
|
|
|
835
936
|
command = tool_input.get("command") if isinstance(tool_input, dict) else None
|
|
836
937
|
# On Windows the Bash tool is Git Bash, which spells drives `/c/...`.
|
|
837
938
|
msys = tool_name == "Bash" and os.name == "nt"
|
|
838
|
-
searches = _bash_searches(command, root, msys) if isinstance(command, str) else []
|
|
939
|
+
searches = _bash_searches(command, root, msys, below) if isinstance(command, str) else []
|
|
839
940
|
if not searches:
|
|
840
941
|
return None
|
|
841
942
|
if mode == "strict":
|
|
842
943
|
denial = _denial_for(root, searches)
|
|
843
944
|
elif mode == "strict" and tool_name == "Grep":
|
|
844
|
-
denial = _strict_denial(payload, root)
|
|
945
|
+
denial = _strict_denial(payload, root, below)
|
|
845
946
|
if denial is not None:
|
|
846
947
|
health = persisted_resolution(
|
|
847
948
|
root,
|
|
@@ -858,19 +959,19 @@ def handle_pre_tool_use(payload: dict[str, Any], mode: str) -> dict[str, Any] |
|
|
|
858
959
|
"hookSpecificOutput": {
|
|
859
960
|
"hookEventName": "PreToolUse",
|
|
860
961
|
"permissionDecision": "deny",
|
|
861
|
-
"permissionDecisionReason": denial,
|
|
962
|
+
"permissionDecisionReason": denial + where,
|
|
862
963
|
}
|
|
863
964
|
}
|
|
864
965
|
return {
|
|
865
966
|
"hookSpecificOutput": {
|
|
866
967
|
"hookEventName": "PreToolUse",
|
|
867
|
-
"additionalContext": PRE_TOOL_REMINDER + STRICT_SUSPENSION_NOTE,
|
|
968
|
+
"additionalContext": PRE_TOOL_REMINDER + STRICT_SUSPENSION_NOTE + where,
|
|
868
969
|
}
|
|
869
970
|
}
|
|
870
971
|
return {
|
|
871
972
|
"hookSpecificOutput": {
|
|
872
973
|
"hookEventName": "PreToolUse",
|
|
873
|
-
"additionalContext": PRE_TOOL_REMINDER,
|
|
974
|
+
"additionalContext": PRE_TOOL_REMINDER + where,
|
|
874
975
|
}
|
|
875
976
|
}
|
|
876
977
|
except Exception:
|
|
@@ -157,6 +157,19 @@ def _normalize_git_path(root: Path, value: str) -> str | None:
|
|
|
157
157
|
return native_path.as_posix()
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
def enclosing_git_root(path: Path) -> Path | None:
|
|
161
|
+
"""The nearest directory, from `path` upward, that holds `.git`.
|
|
162
|
+
|
|
163
|
+
`.git` is a directory in a clone and a file in a linked worktree or a
|
|
164
|
+
submodule, so it is tested for existence only. None means `path` is in no
|
|
165
|
+
repository at all.
|
|
166
|
+
"""
|
|
167
|
+
for directory in (path, *path.parents):
|
|
168
|
+
if os.path.lexists(directory / ".git"):
|
|
169
|
+
return directory
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
160
173
|
def _git_ls_files(
|
|
161
174
|
root: Path,
|
|
162
175
|
*,
|
|
@@ -164,9 +177,9 @@ def _git_ls_files(
|
|
|
164
177
|
is_eligible: Callable[[str], bool] | None = None,
|
|
165
178
|
) -> list[str] | None:
|
|
166
179
|
"""Return bounded Git files, or None only when *root* is not a Git repository."""
|
|
167
|
-
|
|
168
|
-
if
|
|
169
|
-
if
|
|
180
|
+
enclosing = enclosing_git_root(root)
|
|
181
|
+
if enclosing != root:
|
|
182
|
+
if enclosing is not None:
|
|
170
183
|
raise IngestError("nested Git roots are unsupported")
|
|
171
184
|
return None
|
|
172
185
|
try:
|