graphite-code 1.3.0__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (315) hide show
  1. {graphite_code-1.3.0 → graphite_code-1.4.0}/CHANGELOG.md +120 -0
  2. {graphite_code-1.3.0 → graphite_code-1.4.0}/PKG-INFO +2 -2
  3. {graphite_code-1.3.0 → graphite_code-1.4.0}/README.md +1 -1
  4. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/__init__.py +1 -1
  5. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/agent_hooks.py +61 -0
  6. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/cli.py +20 -3
  7. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/doctor.py +2 -2
  8. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/extract/ast.py +121 -29
  9. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/extract/pyscope.py +57 -7
  10. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/hookinstall.py +17 -0
  11. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/init.py +1 -0
  12. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/resolve.py +132 -1
  13. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_agent_hooks.py +68 -6
  14. graphite_code-1.4.0/tests/test_ast_make_id_length.py +33 -0
  15. graphite_code-1.4.0/tests/test_ast_source_lines.py +79 -0
  16. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_cache_file_set.py +6 -1
  17. graphite_code-1.4.0/tests/test_cli_build_missing_path.py +47 -0
  18. graphite_code-1.4.0/tests/test_cli_doctor_below_root.py +67 -0
  19. graphite_code-1.4.0/tests/test_cli_usage.py +36 -0
  20. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_doctor_hooks.py +57 -1
  21. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_documentation.py +23 -0
  22. graphite_code-1.4.0/tests/test_file_node_id_collision.py +187 -0
  23. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_hookinstall.py +35 -0
  24. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_init_hooks.py +36 -0
  25. graphite_code-1.4.0/tests/test_python_builtin_named_symbols.py +72 -0
  26. graphite_code-1.4.0/tests/test_python_inherits_edge.py +97 -0
  27. graphite_code-1.4.0/tests/test_python_local_alias_call.py +157 -0
  28. graphite_code-1.4.0/tests/test_python_reexport_follow.py +144 -0
  29. {graphite_code-1.3.0 → graphite_code-1.4.0}/.gitignore +0 -0
  30. {graphite_code-1.3.0 → graphite_code-1.4.0}/ARCHITECTURE.md +0 -0
  31. {graphite_code-1.3.0 → graphite_code-1.4.0}/CONTRIBUTING.md +0 -0
  32. {graphite_code-1.3.0 → graphite_code-1.4.0}/LICENSE +0 -0
  33. {graphite_code-1.3.0 → graphite_code-1.4.0}/RELEASING.md +0 -0
  34. {graphite_code-1.3.0 → graphite_code-1.4.0}/pyproject.toml +0 -0
  35. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/__main__.py +0 -0
  36. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/_cleanup_worker.py +0 -0
  37. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/_win32_ctypes.py +0 -0
  38. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/activation.py +0 -0
  39. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/agent_settings.py +0 -0
  40. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/analyze.py +0 -0
  41. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/answer_contract.py +0 -0
  42. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/bootstrap.py +0 -0
  43. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/buildlock.py +0 -0
  44. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/cache.py +0 -0
  45. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/channel.py +0 -0
  46. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/cluster.py +0 -0
  47. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/config.py +0 -0
  48. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/context.py +0 -0
  49. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/daemon.py +0 -0
  50. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/daemon_health.py +0 -0
  51. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/daemon_launch.py +0 -0
  52. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/debt.py +0 -0
  53. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/dependency_install.py +0 -0
  54. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/detach.py +0 -0
  55. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/doctor_probes.py +0 -0
  56. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/engine_identity.py +0 -0
  57. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/export/__init__.py +0 -0
  58. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/export/html.py +0 -0
  59. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/export/json.py +0 -0
  60. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/export/md.py +0 -0
  61. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/extract/__init__.py +0 -0
  62. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/freshness.py +0 -0
  63. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/git.py +0 -0
  64. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/graph.py +0 -0
  65. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/graph_io.py +0 -0
  66. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/health.py +0 -0
  67. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/hook_entry.py +0 -0
  68. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/hookshim.py +0 -0
  69. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/incident_ledger.py +0 -0
  70. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/ingest.py +0 -0
  71. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/io.py +0 -0
  72. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/launchd_agent.py +0 -0
  73. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/listing.py +0 -0
  74. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/llm.py +0 -0
  75. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/llm_probe.py +0 -0
  76. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/mcp.py +0 -0
  77. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/mcp_server.py +0 -0
  78. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/natural_query.py +0 -0
  79. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/overlays.py +0 -0
  80. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/ownership.py +0 -0
  81. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/probe_process.py +0 -0
  82. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/probe_workspace.py +0 -0
  83. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/process_contracts.py +0 -0
  84. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/programs.py +0 -0
  85. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/provider_observer.py +0 -0
  86. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/py.typed +0 -0
  87. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/query.py +0 -0
  88. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/query_plan.py +0 -0
  89. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/replacement_audit.py +0 -0
  90. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/review.py +0 -0
  91. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/__init__.py +0 -0
  92. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/approval.py +0 -0
  93. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/classifier.py +0 -0
  94. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/claude_executor.py +0 -0
  95. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/claude_probe.py +0 -0
  96. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/cli_identity.py +0 -0
  97. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/codex_executor.py +0 -0
  98. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/codex_probe.py +0 -0
  99. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/context_builder.py +0 -0
  100. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/contracts.py +0 -0
  101. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/diff_policy.py +0 -0
  102. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/edit_apply.py +0 -0
  103. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/effort.py +0 -0
  104. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/lifecycle.py +0 -0
  105. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/lifecycle_operator.py +0 -0
  106. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/lifecycle_service.py +0 -0
  107. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/lifecycle_storage.py +0 -0
  108. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/ollama_executor.py +0 -0
  109. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/ollama_probe.py +0 -0
  110. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/openrouter_executor.py +0 -0
  111. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/openrouter_probe.py +0 -0
  112. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/policy.py +0 -0
  113. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/probe_runner.py +0 -0
  114. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/process_runner.py +0 -0
  115. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/profiles.py +0 -0
  116. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/prompt.py +0 -0
  117. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/registry.py +0 -0
  118. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/route_pool.py +0 -0
  119. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/route_pool_execution.py +0 -0
  120. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/schema_validation.py +0 -0
  121. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/service.py +0 -0
  122. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/settings.py +0 -0
  123. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/shadow.py +0 -0
  124. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/storage.py +0 -0
  125. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/telemetry.py +0 -0
  126. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/worktree.py +0 -0
  127. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/zai_edit.py +0 -0
  128. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/zai_executor.py +0 -0
  129. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/routing/zai_probe.py +0 -0
  130. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/savings.py +0 -0
  131. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/systemd_unit.py +0 -0
  132. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/ts_bridge.py +0 -0
  133. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/ts_resolver.mjs +0 -0
  134. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/typescript_activation.py +0 -0
  135. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/usage_ledger.py +0 -0
  136. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/validation.py +0 -0
  137. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/watch.py +0 -0
  138. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/windows_job.py +0 -0
  139. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/windows_startup.py +0 -0
  140. {graphite_code-1.3.0 → graphite_code-1.4.0}/src/graphite/windows_task.py +0 -0
  141. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/conftest.py +0 -0
  142. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/data/sqloracle/blindspots.py.txt +0 -0
  143. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/data/sqloracle/round69.py.txt +0 -0
  144. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/fake_clis/fake_cli.py +0 -0
  145. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/fixtures/provider_lifecycle_schema_v1.sql +0 -0
  146. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/fixtures/routing_schema_v2_ca77600.sql +0 -0
  147. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/fixtures/routing_schema_v3_94eb333.sql +0 -0
  148. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/fixtures/routing_schema_v4_lifecycle_migration.sql +0 -0
  149. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test__win32_ctypes.py +0 -0
  150. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_activation.py +0 -0
  151. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_activation_backstop.py +0 -0
  152. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_agent_hook_activation.py +0 -0
  153. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_agent_settings.py +0 -0
  154. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_answer_contract.py +0 -0
  155. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_arrow_and_constructor_binding.py +0 -0
  156. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_atomic_write.py +0 -0
  157. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_bootstrap.py +0 -0
  158. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_build_detach.py +0 -0
  159. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_buildlock.py +0 -0
  160. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_cache_engine_identity.py +0 -0
  161. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_cache_entry_pruning.py +0 -0
  162. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_cache_file_identity.py +0 -0
  163. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_cache_pruning.py +0 -0
  164. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_call_graph.py +0 -0
  165. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_case_ambiguous_answer.py +0 -0
  166. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_channel_attribution.py +0 -0
  167. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_channel_cli.py +0 -0
  168. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_channel_core.py +0 -0
  169. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_channel_kind.py +0 -0
  170. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_channel_lock.py +0 -0
  171. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_channel_log_integrity.py +0 -0
  172. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_channel_mcp.py +0 -0
  173. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_channel_register.py +0 -0
  174. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_channel_report.py +0 -0
  175. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_channel_status.py +0 -0
  176. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_class_field_arrow_binding.py +0 -0
  177. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_cli_below_repository_root.py +0 -0
  178. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_cli_channel.py +0 -0
  179. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_cli_channel_attribution.py +0 -0
  180. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_cli_reference.py +0 -0
  181. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_cli_version.py +0 -0
  182. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_cluster.py +0 -0
  183. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_compare_dist.py +0 -0
  184. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_configuration_reference.py +0 -0
  185. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_context.py +0 -0
  186. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_context_builder.py +0 -0
  187. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_cwd_relative_config.py +0 -0
  188. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_cycle_search.py +0 -0
  189. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_daemon.py +0 -0
  190. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_daemon_activation.py +0 -0
  191. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_daemon_build_lock.py +0 -0
  192. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_daemon_child_lock.py +0 -0
  193. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_daemon_engine_staleness.py +0 -0
  194. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_daemon_health.py +0 -0
  195. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_daemon_health_activation.py +0 -0
  196. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_daemon_launch.py +0 -0
  197. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_daemon_service_cli.py +0 -0
  198. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_debt.py +0 -0
  199. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_dependency_install.py +0 -0
  200. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_destructured_binding.py +0 -0
  201. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_determinism.py +0 -0
  202. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_dispatch_evidence.py +0 -0
  203. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_doctor.py +0 -0
  204. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_doctor_agent_hooks.py +0 -0
  205. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_doctor_managed_docs.py +0 -0
  206. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_doctor_probes.py +0 -0
  207. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_edit_apply.py +0 -0
  208. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_engine_identity.py +0 -0
  209. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_exit_codes_reference.py +0 -0
  210. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_external_calls.py +0 -0
  211. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_git_security.py +0 -0
  212. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_go_rust.py +0 -0
  213. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_graph_io.py +0 -0
  214. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_graph_provider_isolation.py +0 -0
  215. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_hardening.py +0 -0
  216. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_health.py +0 -0
  217. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_hook_entry.py +0 -0
  218. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_hook_template.py +0 -0
  219. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_hookshim.py +0 -0
  220. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_html_security.py +0 -0
  221. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_incident_ledger.py +0 -0
  222. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_init.py +0 -0
  223. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_init_activation_doctrine.py +0 -0
  224. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_init_validation_exit.py +0 -0
  225. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_launchd_agent.py +0 -0
  226. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_lifecycle_operator.py +0 -0
  227. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_listing.py +0 -0
  228. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_listing_surfaces.py +0 -0
  229. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_llm.py +0 -0
  230. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_llm_probe.py +0 -0
  231. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_mcp.py +0 -0
  232. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_mcp_server.py +0 -0
  233. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_mcp_server_channel_attribution.py +0 -0
  234. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_member_call_receiver.py +0 -0
  235. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_method_dispatch_scope.py +0 -0
  236. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_monorepo.py +0 -0
  237. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_mutation_tests.py +0 -0
  238. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_natural_query.py +0 -0
  239. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_no_path_is_graded.py +0 -0
  240. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_node_identity.py +0 -0
  241. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_ollama_executor.py +0 -0
  242. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_overlays.py +0 -0
  243. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_path_leak_fixture.py +0 -0
  244. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_probe_diagnostics.py +0 -0
  245. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_probe_process.py +0 -0
  246. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_probe_workspace.py +0 -0
  247. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_process_contracts.py +0 -0
  248. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_programs.py +0 -0
  249. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_provider_claude_probe.py +0 -0
  250. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_provider_codex_probe.py +0 -0
  251. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_provider_lifecycle.py +0 -0
  252. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_provider_lifecycle_service.py +0 -0
  253. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_provider_lifecycle_storage.py +0 -0
  254. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_provider_observer.py +0 -0
  255. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_provider_ollama_probe.py +0 -0
  256. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_provider_openrouter_probe.py +0 -0
  257. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_provider_probe_runner.py +0 -0
  258. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_public_surface.py +0 -0
  259. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_published_schemas.py +0 -0
  260. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_pyscope.py +0 -0
  261. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_pyscopeoracle.py +0 -0
  262. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_python_import_bindings.py +0 -0
  263. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_python_resolver.py +0 -0
  264. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_python_scope_identity.py +0 -0
  265. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_query_plan.py +0 -0
  266. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_reliability.py +0 -0
  267. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_replacement_audit.py +0 -0
  268. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_repository_root_ownership.py +0 -0
  269. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_resolve.py +0 -0
  270. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_review.py +0 -0
  271. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_route_pool.py +0 -0
  272. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_route_pool_execution.py +0 -0
  273. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_approval.py +0 -0
  274. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_benchmark.py +0 -0
  275. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_classifier.py +0 -0
  276. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_claude_executor.py +0 -0
  277. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_cli.py +0 -0
  278. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_cli_contracts.py +0 -0
  279. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_cli_recovery.py +0 -0
  280. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_codex_executor.py +0 -0
  281. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_context.py +0 -0
  282. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_contracts.py +0 -0
  283. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_diff_policy.py +0 -0
  284. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_executor.py +0 -0
  285. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_openrouter_executor.py +0 -0
  286. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_policy.py +0 -0
  287. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_process_runner.py +0 -0
  288. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_profiles.py +0 -0
  289. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_registry.py +0 -0
  290. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_schema_validation.py +0 -0
  291. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_security.py +0 -0
  292. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_service.py +0 -0
  293. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_shadow.py +0 -0
  294. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_storage.py +0 -0
  295. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_telemetry.py +0 -0
  296. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_worktree.py +0 -0
  297. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_zai_executor.py +0 -0
  298. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_routing_zai_probe.py +0 -0
  299. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_savings.py +0 -0
  300. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_search.py +0 -0
  301. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_self_super_dispatch.py +0 -0
  302. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_smoke.py +0 -0
  303. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_sqloracle.py +0 -0
  304. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_synthetic_repo.py +0 -0
  305. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_systemd_unit.py +0 -0
  306. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_ts_bridge.py +0 -0
  307. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_typescript_activation.py +0 -0
  308. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_typescript_resolver.py +0 -0
  309. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_usage_ledger.py +0 -0
  310. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_verify_artifact.py +0 -0
  311. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_verify_published_release.py +0 -0
  312. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_watch.py +0 -0
  313. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_windows_startup.py +0 -0
  314. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_windows_task.py +0 -0
  315. {graphite_code-1.3.0 → graphite_code-1.4.0}/tests/test_zai_edit.py +0 -0
@@ -15,6 +15,126 @@ machine-checkable identity; the version is for humans.
15
15
 
16
16
  ## [Unreleased]
17
17
 
18
+ ## [1.4.0] — 2026-10-25
19
+
20
+ A minor release: eight fixes, to the Python graph, the build, the CLI and the
21
+ agent hooks, and one addition to the hook contract (#101).
22
+
23
+ The `hooks` check of `doctor --json` gains the field `chain_slots`, and
24
+ `graphite init` reports the same slots beside the hooks it installs; both are
25
+ additions under `docs/compatibility.md`. No field is removed or renamed,
26
+ `Requires-Dist` and the entry points are those of 1.3.0, and `graphite init`
27
+ writes what it wrote (`DOC_VERSION` 15): in a new repository, `init --all`
28
+ leaves 17 files, the three trampolines among them, and a `.git/config`, each
29
+ byte for byte as 1.3.0 leaves it.
30
+
31
+ The graph changes in this release. On this repository, measured against the
32
+ deployed 1.3.0 engine, with the same interpreter, from one tree:
33
+
34
+ - every definition and file node id survives (6,616 and 425), each naming
35
+ the same definition or file, and none is new. This tree has no pair of
36
+ paths that #82 tells apart;
37
+ - 24 placeholder nodes are gone and 4 are new, so 9,164 nodes become 9,144;
38
+ - 32 of the 120 `inherits` edges ended at a placeholder and now end at the
39
+ class the base names (#86);
40
+ - `calls` goes from 16,065 to 16,066. 21 call sites through a local alias
41
+ ended at a placeholder named for the local: 16 now end at a definition,
42
+ and 5 at a placeholder for a name the module only imports, which is where
43
+ a direct call of it already ended (#90; that gap is #107). One call site is
44
+ new (#85). `contains` (6,614) and `imports` (2,431) are unchanged, and
45
+ 25,177 of the 25,230 edge triples are unchanged;
46
+ - the Python `calls` ratio goes from 0.976 to 0.978: 12,001 of 12,276 call
47
+ sites are bound, where 11,984 of 12,275 were;
48
+ - `analysis` lists 20 cycles before and after, and one entry is new, in
49
+ `git.py`: the new `inherits` edge closes a loop through a
50
+ `super().__init__` call that both engines bind to the wrong `__init__`
51
+ (#106). The entry it displaced is still a cycle in the graph. Entry
52
+ points, god nodes, orphans, surprising connections and top files are
53
+ identical, and so is the community count (177).
54
+
55
+ Two builds of this tree with either engine are byte-identical (`graph.json`,
56
+ `GRAPH_REPORT.md`, `graph.html`).
57
+
58
+ The entries below quote figures measured when each change was made, on
59
+ earlier commits of this tree, so they differ from the ones above.
60
+
61
+ ### Added
62
+
63
+ **Where another tool's git hook goes is declared, not only written in
64
+ shell.** Each of graphite's trampolines (`post-commit`, `post-merge`,
65
+ `post-rewrite`) runs `<hook>.local` beside it first when that file exists. The
66
+ name was stated only inside the trampoline's text. When graphite was installed
67
+ first, nothing was in that slot, and a hook manager that would not chain to
68
+ graphite's trampoline left its own hook out entirely (channel rounds 322 to
69
+ 324). The name is now part of the hook contract in `docs/compatibility.md`.
70
+ It is reported in two places:
71
+
72
+ - the `hooks` check of `doctor --json`, as `chain_slots`: for each trigger,
73
+ the `file` relative to the hooks directory and whether it is `present`;
74
+ - the hook line of `graphite init`, in either install order.
75
+
76
+ An empty slot is not a finding. The trampolines are byte for byte those of
77
+ 1.3.0. This adds a field to `doctor --json`, so the next release is a minor.
78
+
79
+ ### Fixed
80
+
81
+ - Python: a name a package re-exports is followed to the file that defines it
82
+ (#87). `from pkg import Base` used to bind to `pkg/__init__.py`, so a call of
83
+ a re-exported function ended at a placeholder, and a `self.m()` in a class
84
+ whose base was re-exported had no edge and was not counted, which let
85
+ `callers` answer `decision_grade` without that caller. Only a plain
86
+ top-level `from X import name` in an `__init__.py` is followed, for as many
87
+ steps as the source states; a name the package binds some other way, a cycle,
88
+ and a module other than `__init__.py` stay as they were. The Python cache key
89
+ gains a digest of the re-exports, so an unchanged importer is re-resolved
90
+ when an `__init__.py` changes.
91
+ - Python: an `inherits` edge ends at the class its base names (#86). Its far
92
+ end was always a node made from the base's bare name, of kind `unknown`, so
93
+ the graph held no link from a class to its base and every class of one name
94
+ shared one placeholder. A base that is a bare name the file binds by a
95
+ class statement or imports by name now ends at that class's node; a
96
+ builtin, a dotted name or a call keeps its placeholder.
97
+ - Python: a call through a local alias binds to what the alias names (#90).
98
+ `t = mod.target` followed by `t(x)` used to end at a placeholder named for
99
+ the local, so `callers target` left that function out and still answered
100
+ `decision_grade`. A name bound by one assignment of a bare name or of
101
+ `module.attr` is read as that name or attribute, as the call written
102
+ directly is, also through a chain of such assignments. A name bound a
103
+ second time in its scope, a `with ... as` name, a cycle, and a longer chain
104
+ such as `a.b.c` stay as they were.
105
+ - Build: two paths that differ only by `/`, `.` or `_` keep a file node each
106
+ (#82). `a/b.py` and `a_b.py` both made `a_b_py`, so one had no node and its
107
+ definitions hung off the other's. Only the paths of the tree that actually
108
+ collide are marked, each with a hash of its exact path; every other file
109
+ node id is what it was. A tree with such a pair re-extracts once, because
110
+ the marked ids are folded into the cache key.
111
+ - Python: a bare call of a name the file binds gets its edge also when the
112
+ name is a builtin of another language (#85). The list that drops calls of
113
+ builtins holds JavaScript, Go and Rust names beside Python's, so a class
114
+ `Error` or a function `make` had no callers in the graph and its call sites
115
+ were absent from the `calls` ratio. A listed name that nothing in the file
116
+ binds still gets no edge.
117
+ - CLI: `build` and `report` of a path that is not there say `path not found`
118
+ and exit 1, as `scan` does, and create nothing (#88). The build lock made
119
+ its cache directory first, which made the path, and a complete empty
120
+ `graph-out/` was written into it with exit 0. `build --detach` is refused
121
+ the same way. Below a repository root the message is still the one that
122
+ names the root.
123
+ - CLI: `doctor` typed below a repository's root reports on the repository
124
+ (#89). It took the folder for the repository, so in a subfolder of a
125
+ repository with a fresh graph it said `Graph artifact is missing` and
126
+ offered a build that is refused there, and its incident line read a ledger
127
+ nothing writes. It now follows the root, as the read commands do (#81); a
128
+ `.git` above that graphite does not take is not followed.
129
+ - Agent hooks: the strict hook reads files piped to `Select-String` as a
130
+ search of them (#84). `Get-ChildItem -Recurse src | Select-String name` was
131
+ let through as a filter, while `Select-String name -Path src` was refused.
132
+ A listing (`Get-ChildItem`, `gci`, `ls`, `dir`) now hands its paths to a
133
+ `Select-String` after it, also across `Where-Object`, `Select-Object` and
134
+ `Sort-Object`, and the search is judged as the path form is. `-Name`
135
+ listings, other commands' output and any other stage between them stay
136
+ filters.
137
+
18
138
  ## [1.3.0] — 2026-10-16
19
139
 
20
140
  A minor release, with a security fix: from a folder below a repository's
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: graphite-code
3
- Version: 1.3.0
3
+ Version: 1.4.0
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.3.0, production/stable.** Published on PyPI as
51
+ **Status: 1.4.0, 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.3.0, production/stable.** Published on PyPI as
5
+ **Status: 1.4.0, 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.3.0"
41
+ __version__ = "1.4.0"
@@ -968,6 +968,54 @@ def _forget_what_may_be_set_again(assigned: dict[str, str], argv: list[str], pow
968
968
  assigned.pop(named.group().lower() if powershell else named.group(), None)
969
969
 
970
970
 
971
+ # PowerShell commands whose output is the files (or folders) of a path, so that a
972
+ # `Select-String` after a pipe searches what they hand it (#84).
973
+ _FILE_LISTINGS = frozenset({"get-childitem", "gci", "ls", "dir"})
974
+ # The searches that take files from the pipeline. `grep` does not: it reads the
975
+ # lines of what is piped, so `ls | grep name` filters a list of names.
976
+ _POWERSHELL_SEARCHES = frozenset({"select-string", "sls"})
977
+ # What may stand between such a listing and the search and still hand the search
978
+ # files: a stage that only keeps or orders them. Any other stage (`ForEach-Object`,
979
+ # `Out-String`, `Select-Object -ExpandProperty`) may make text, and ends the chain.
980
+ _FILE_KEEPING_STAGES = frozenset({
981
+ "where-object", "where", "?", "select-object", "select", "sort-object", "sort",
982
+ })
983
+ # Listing flags that take a word, so the word is not a path. `-Path` and
984
+ # `-LiteralPath` are the exceptions: their word IS one.
985
+ _LISTING_VALUE_FLAGS = frozenset({
986
+ "-filter", "-include", "-exclude", "-depth", "-attributes",
987
+ })
988
+ _LISTING_PATH_FLAGS = frozenset({"-path", "-literalpath"})
989
+
990
+
991
+ def _listed_paths(name: str, rest: list[str]) -> list[str] | None:
992
+ """The paths a file listing names, else None where `name` lists no files.
993
+
994
+ An empty list is a listing of the current directory. `-Name` makes the
995
+ listing text, and what is piped on is then a filter of it.
996
+ """
997
+ if name not in _FILE_LISTINGS:
998
+ return None
999
+ paths: list[str] = []
1000
+ expecting = False
1001
+ skipping = False
1002
+ for token in rest:
1003
+ lowered = token.lower()
1004
+ if expecting or skipping:
1005
+ if expecting:
1006
+ paths.append(token)
1007
+ expecting = skipping = False
1008
+ elif lowered == "-name" or lowered == "-n":
1009
+ return None
1010
+ elif lowered in _LISTING_PATH_FLAGS:
1011
+ expecting = True
1012
+ elif lowered in _LISTING_VALUE_FLAGS:
1013
+ skipping = True
1014
+ elif not token.startswith("-"):
1015
+ paths.append(token)
1016
+ return paths
1017
+
1018
+
971
1019
  def _bash_searches(
972
1020
  command: str, root: Path, msys: bool = False, cwd: Path | None = None, powershell: bool = False
973
1021
  ) -> list[_Search]:
@@ -1024,9 +1072,11 @@ def _bash_searches(
1024
1072
  base, placed = start, start_placed # `base` is where the shell is
1025
1073
  moved = False # a `cd`, or a command the hook does not model, has run in this shell
1026
1074
  pipeline_base, pipeline_placed = base, placed # where the pipeline being read started
1075
+ listed: list[str] | None = None # the paths of the file listing whose output the stage before gives
1027
1076
  for head in _bash_command_heads(lines, powershell):
1028
1077
  piped = head.before in ("|", "|&") # `||` is or-else, not a pipe
1029
1078
  if not piped:
1079
+ listed = None
1030
1080
  if head.before != "&&":
1031
1081
  base, placed = (None, False) if moved else (start, start_placed)
1032
1082
  pipeline_base, pipeline_placed = base, placed
@@ -1040,6 +1090,16 @@ def _bash_searches(
1040
1090
  if len(head.argv) == 1 and _NESTED_RE.fullmatch(head.argv[0]):
1041
1091
  continue # a subshell standing alone
1042
1092
  if piped and _command_name(head.argv[0]) != "xargs":
1093
+ stage = _command_name(head.argv[0])
1094
+ if listed is not None and stage in _POWERSHELL_SEARCHES:
1095
+ # `Get-ChildItem -Recurse src | Select-String name` searches
1096
+ # the files it is handed (#84), as `xargs grep` does.
1097
+ found = _search_in(stage, [_expanded(t, assigned, powershell) for t in head.argv[1:]], here, msys)
1098
+ if found is not None:
1099
+ found = found._replace(paths=_located(listed, here, msys))
1100
+ searches.append(found if here_placed or cwd is None else _unplaced(found, cwd))
1101
+ if stage not in _FILE_KEEPING_STAGES:
1102
+ listed = None
1043
1103
  continue # a filter: it reads what the command before it wrote
1044
1104
  assignments = _assignments(head.argv, powershell)
1045
1105
  if assignments is not None:
@@ -1063,6 +1123,7 @@ def _bash_searches(
1063
1123
  continue
1064
1124
  name = _command_name(argv[0])
1065
1125
  rest = [_expanded(token, assigned, powershell) for token in argv[1:]]
1126
+ listed = _listed_paths(name, rest) if len(argv) == len(written) else None
1066
1127
  # A `cd` behind a wrapper is not the shell's: `env cd docs` moves a
1067
1128
  # program that then exits, and `! cd docs && ...` goes on only when
1068
1129
  # the `cd` failed. It is read below, as a command that may move.
@@ -255,12 +255,17 @@ def _write_json(path: Path, data: dict[str, Any]) -> None:
255
255
  atomic_write_json(path, data, indent=2)
256
256
 
257
257
 
258
- def _scan(args: argparse.Namespace, cfg: Config) -> tuple[dict[str, Any], list[Any]]:
259
- cfg = cfg.canonical_graph()
260
- root = Path(args.path).resolve()
258
+ def _require_existing(root: Path) -> None:
259
+ """Exit 1 with `path not found` for a path that is not there."""
261
260
  if not root.exists():
262
261
  print(f"[graphite] path not found: {root}", file=sys.stderr)
263
262
  raise SystemExit(1)
263
+
264
+
265
+ def _scan(args: argparse.Namespace, cfg: Config) -> tuple[dict[str, Any], list[Any]]:
266
+ cfg = cfg.canonical_graph()
267
+ root = Path(args.path).resolve()
268
+ _require_existing(root)
264
269
  top = _root_above(root)
265
270
  if top is not None:
266
271
  raise ValueError(_below_a_root(root, top))
@@ -727,6 +732,9 @@ def cmd_build(args: argparse.Namespace) -> int:
727
732
  top = _root_above(asked)
728
733
  if top is not None:
729
734
  raise ValueError(_below_a_root(asked, top))
735
+ # A path that is not there is refused as `scan` refuses it (#88): the lock
736
+ # below would otherwise make it, and a detached child could not start in it.
737
+ _require_existing(asked)
730
738
  if getattr(args, "detach", False):
731
739
  root = Path(args.path).resolve()
732
740
  # Deliberately omits --detach: the child must build, not re-spawn.
@@ -839,6 +847,10 @@ def cmd_doctor(args: argparse.Namespace) -> int:
839
847
  root = Path(args.path).resolve()
840
848
  if not root.is_dir():
841
849
  raise ValueError("doctor path must be an existing directory")
850
+ # The report is about the repository the folder is in (#89), as the read
851
+ # commands are: from a subfolder the graph is the root's, and so are the
852
+ # incidents. A root graphite does not take (`repository_root`) is not followed.
853
+ root = repository_root(root)
842
854
  cfg = _project_scoped_config(args, root)
843
855
  report = run_doctor(
844
856
  root,
@@ -1204,6 +1216,11 @@ def cmd_init(args: argparse.Namespace) -> int:
1204
1216
  extra += f", reason={hooks['reason']}"
1205
1217
  if hooks.get("relocated"):
1206
1218
  extra += f", relocated={', '.join(hooks['relocated'])}"
1219
+ if hooks.get("chain_slots"):
1220
+ # Said in either install order: with graphite first nothing is in
1221
+ # these slots yet, and this is where another tool's hook goes.
1222
+ files = ", ".join(slot["file"] for slot in hooks["chain_slots"].values())
1223
+ extra += f"; another tool's hook for the same event belongs in {files} there, run first"
1207
1224
  print(f" - hooks: {hooks.get('action')} ({hooks.get('path')}{extra})")
1208
1225
  allowlist = result["allowlist"]
1209
1226
  if allowlist.get("changed"):
@@ -20,7 +20,7 @@ from .daemon import read_daemon_status
20
20
  from .daemon_health import HealthOptions, evaluate_daemon_health
21
21
  from .freshness import FreshnessLimitError, check_graph_freshness
22
22
  from .git import GitError, GitRunner
23
- from .hookinstall import DEFAULT_HOOKS_DIRNAME, hook_shim_present, hooks_dir
23
+ from .hookinstall import DEFAULT_HOOKS_DIRNAME, chain_slots, hook_shim_present, hooks_dir
24
24
  from .init import gitignored_managed_paths, managed_doc_paths
25
25
  from .hookshim import TRIGGERS
26
26
  from .llm import canonical_provider_name
@@ -179,7 +179,7 @@ def check_hooks(root: Path) -> DoctorCheck:
179
179
  installed = all(hook_shim_present(hdir / hook) for hook in TRIGGERS)
180
180
  configured = _hooks_path_configured(root)
181
181
  custom_hooks_dir = hdir != root / DEFAULT_HOOKS_DIRNAME
182
- details = {"onboarded": True, "custom_hooks_dir": custom_hooks_dir, "hookspath_configured": configured, "trampolines_installed": installed}
182
+ details = {"onboarded": True, "custom_hooks_dir": custom_hooks_dir, "hookspath_configured": configured, "trampolines_installed": installed, "chain_slots": chain_slots(hdir)}
183
183
 
184
184
  if configured and installed:
185
185
  return DoctorCheck("hooks", "Hooks", "ready", "Git hooks are installed and enforced.", details)