witan-code 0.20.1__tar.gz → 0.22.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 (109) hide show
  1. {witan_code-0.20.1 → witan_code-0.22.0}/CHANGELOG.md +82 -0
  2. {witan_code-0.20.1 → witan_code-0.22.0}/PKG-INFO +13 -4
  3. {witan_code-0.20.1 → witan_code-0.22.0}/README.md +10 -1
  4. {witan_code-0.20.1 → witan_code-0.22.0}/pyproject.toml +23 -4
  5. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_bridge.py +16 -0
  6. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_cli_hooks.py +32 -3
  7. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_context.py +49 -0
  8. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_setup.py +91 -0
  9. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_visualize.py +35 -0
  10. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/cli.py +15 -2
  11. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/context.py +59 -7
  12. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/extensions/pi/codegraph.ts +28 -7
  13. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/queries/bridge.gq +20 -2
  14. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/server.py +28 -7
  15. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/setup.py +15 -3
  16. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/skills/witan-code/SKILL.md +29 -4
  17. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/visualize.py +17 -4
  18. {witan_code-0.20.1 → witan_code-0.22.0}/.gitignore +0 -0
  19. {witan_code-0.20.1 → witan_code-0.22.0}/changelog.d/README.md +0 -0
  20. {witan_code-0.20.1 → witan_code-0.22.0}/config/claude.json +0 -0
  21. {witan_code-0.20.1 → witan_code-0.22.0}/config/copilot.json +0 -0
  22. {witan_code-0.20.1 → witan_code-0.22.0}/config/pi.json +0 -0
  23. {witan_code-0.20.1 → witan_code-0.22.0}/conftest.py +0 -0
  24. {witan_code-0.20.1 → witan_code-0.22.0}/docs/BRANCH_INDEXING.md +0 -0
  25. {witan_code-0.20.1 → witan_code-0.22.0}/docs/CLI_REFERENCE.md +0 -0
  26. {witan_code-0.20.1 → witan_code-0.22.0}/docs/EDGE_PRECISION_TIERS.md +0 -0
  27. {witan_code-0.20.1 → witan_code-0.22.0}/docs/PACKAGE_MAP.md +0 -0
  28. {witan_code-0.20.1 → witan_code-0.22.0}/docs/STAGE2_STITCHING.md +0 -0
  29. {witan_code-0.20.1 → witan_code-0.22.0}/docs/SYMBOL_FORMAT.md +0 -0
  30. {witan_code-0.20.1 → witan_code-0.22.0}/docs/SYMBOL_TABLE.md +0 -0
  31. {witan_code-0.20.1 → witan_code-0.22.0}/docs/USER_GUIDE.md +0 -0
  32. {witan_code-0.20.1 → witan_code-0.22.0}/install.sh +0 -0
  33. {witan_code-0.20.1 → witan_code-0.22.0}/tests/__init__.py +0 -0
  34. {witan_code-0.20.1 → witan_code-0.22.0}/tests/conftest.py +0 -0
  35. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_batched_deletes.py +0 -0
  36. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_branches.py +0 -0
  37. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_chunked_load.py +0 -0
  38. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_cli_dispatch.py +0 -0
  39. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_cli_output_format.py +0 -0
  40. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_cli_selected_target.py +0 -0
  41. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_config.py +0 -0
  42. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_config_targets.py +0 -0
  43. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_crossrepo.py +0 -0
  44. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_edges.py +0 -0
  45. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_elicit.py +0 -0
  46. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_enrichment.py +0 -0
  47. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_github_app.py +0 -0
  48. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_graph.py +0 -0
  49. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_hooks.py +0 -0
  50. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_identity.py +0 -0
  51. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_index_failure_diagnostics.py +0 -0
  52. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_index_reports_its_store.py +0 -0
  53. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_index_requires_a_repo_key.py +0 -0
  54. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_indexer.py +0 -0
  55. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_indexer_scope.py +0 -0
  56. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_ingest.py +0 -0
  57. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_languages.py +0 -0
  58. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_maintenance.py +0 -0
  59. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_package_map.py +0 -0
  60. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_reaper.py +0 -0
  61. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_rebuild_honours_the_repo_key.py +0 -0
  62. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_refusal_audit.py +0 -0
  63. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_remote_proxy.py +0 -0
  64. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_remote_store.py +0 -0
  65. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_repo.py +0 -0
  66. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_server.py +0 -0
  67. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_stitch.py +0 -0
  68. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_store.py +0 -0
  69. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_symbol_table.py +0 -0
  70. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_tasks.py +0 -0
  71. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_tool_schema.py +0 -0
  72. {witan_code-0.20.1 → witan_code-0.22.0}/tests/test_views.py +0 -0
  73. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/__init__.py +0 -0
  74. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/__main__.py +0 -0
  75. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/bridge.py +0 -0
  76. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/bridge_extractors.py +0 -0
  77. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/config.py +0 -0
  78. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/edges.py +0 -0
  79. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/elicit.py +0 -0
  80. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/github_app.py +0 -0
  81. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/graph.py +0 -0
  82. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/hooks.py +0 -0
  83. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/identity.py +0 -0
  84. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/indexer.py +0 -0
  85. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/ingest.py +0 -0
  86. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/maintenance.py +0 -0
  87. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/output.py +0 -0
  88. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/package_map.py +0 -0
  89. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/queries/code_mutations.gq +0 -0
  90. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/queries/code_read.gq +0 -0
  91. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/queries/delete.gq +0 -0
  92. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/queries_ts/bash.scm +0 -0
  93. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/queries_ts/hcl.scm +0 -0
  94. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/queries_ts/python.scm +0 -0
  95. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/queries_ts/sql.scm +0 -0
  96. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/queries_ts/typescript.scm +0 -0
  97. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/queries_ts/yaml.scm +0 -0
  98. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/reaper.py +0 -0
  99. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/remote/__init__.py +0 -0
  100. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/remote/oidc.py +0 -0
  101. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/remote/proxy.py +0 -0
  102. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/remote/store.py +0 -0
  103. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/repo.py +0 -0
  104. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/schema/bridge-schema.pg +0 -0
  105. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/schema/code-schema.pg +0 -0
  106. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/selected_target.py +0 -0
  107. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/stitch.py +0 -0
  108. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/store.py +0 -0
  109. {witan_code-0.20.1 → witan_code-0.22.0}/witan_code/views.py +0 -0
@@ -8,6 +8,88 @@ a MINOR bump may include breaking changes).
8
8
 
9
9
  <!-- scriv-insert-here -->
10
10
 
11
+ ## [0.22.0] - 2026-09-25
12
+
13
+ ### Added
14
+
15
+ - **`witan-code inject-context --client pi`.** The status block told every
16
+ agent to run Claude's `ToolSearch` and then call `code_*` tools directly,
17
+ including under Pi, which has no `ToolSearch` and reaches MCP tools only
18
+ through pi-mcp-adapter's `mcp` proxy under a server-prefixed name. With
19
+ `--client pi` the block instead says to find the exact name with
20
+ `mcp({ search: "code_find_definition callers impact" })` and call it with
21
+ `mcp({ tool: "<name>", args: {...} })`, and points at `/skill:witan-code`.
22
+ The default (`--client claude`, what the Claude hook runs) is unchanged.
23
+
24
+ ### Changed
25
+
26
+ - **The Pi extension passes `--client pi`**, so a Pi install needs a
27
+ `witan-code` CLI from this release or later: an older one rejects the flag
28
+ and the extension, as with any failure, injects no block.
29
+ - **The `witan-code` skill's tool reference is client-neutral**, with short
30
+ notes on reaching the tools from Claude Code (`ToolSearch`) and Pi (the
31
+ `mcp` proxy).
32
+
33
+ - `witan-code setup --agent pi` (and `--agent all` when Pi is detected) now
34
+ warns when the pi-mcp-adapter Pi package is not declared in Pi's settings,
35
+ since Pi ignores the witan-code MCP entry in `~/.pi/agent/mcp.json` without
36
+ it. The warning comes from agent-config-kit's preflight via
37
+ `witan_core.cli.report_install`. The README and the `setup` help document
38
+ the prerequisite (`pi install npm:pi-mcp-adapter`).
39
+
40
+ - The `agent-config-kit` floor is raised from `>=0.7` to `>=0.10`, the first
41
+ release whose `InstallResult` carries the `prerequisites` that warning is
42
+ built from; on an older agent-config-kit the warning silently never
43
+ appeared.
44
+
45
+ - The `witan-core` floor is raised from `>=0.39` to `>=0.40`, the first
46
+ release whose `report_install` prints those `prerequisites`; on 0.39 the
47
+ server still runs but the warning is silently dropped.
48
+
49
+ ### Fixed
50
+
51
+ - **The Pi extension now waits as long for the code-graph status block as
52
+ Claude does.** `codegraph.ts`'s `before_agent_start` gave `witan-code
53
+ inject-context` 5s, against the 15s the Claude `UserPromptSubmit` hook gets,
54
+ so a cold store read that ran past 5s was killed before it could fill the
55
+ cache, and that prompt, and the next cold one, got no block. Both
56
+ now read one value (`INJECT_CONTEXT_TIMEOUT_SECONDS` in `witan_code.setup`,
57
+ `INJECT_CONTEXT_TIMEOUT_MS` in the extension), and a test fails if they, or
58
+ the `configs/pi/extensions` mirror, drift. Every other Pi handler stays
59
+ detached, and a timeout still degrades to no context.
60
+
61
+ ## [0.21.0] - 2026-09-23
62
+
63
+ ### Added
64
+
65
+ - **`code_repo_dependencies` marks each contract `generic`.** A stoplisted key
66
+ (DEBUG, PORT, ...) still makes its edge, as before, but its entry in the
67
+ edge's `contracts` now carries `"generic": true`, so a reader can hide the
68
+ keys that couple every repo to every other without the tool deciding for
69
+ them. Additive: every other field is unchanged.
70
+ - **`code_interface_providers`, `code_interface_consumers` and the
71
+ `cross_repo` rows of `code_cross_repo_impact` return each binding's
72
+ `confidence`.** An endpoint
73
+ consumer under 0.5 makes no `code_repo_dependencies` edge, but these tools
74
+ return every row, so without the score a reader could not tell the rows that
75
+ made an edge from the phantoms. Additive.
76
+ - **`code_interface_providers` and `code_interface_consumers` take
77
+ `in_repo`**, an exact repo URI to narrow to. It is applied in the query, so
78
+ the 500-row limit counts that repo's rows: a key bound in more than 500
79
+ places across every repo could otherwise cut off the repo being asked about.
80
+ Named `in_repo` rather than `repo` because the remote proxy fills an omitted
81
+ `repo` with the caller's detected one, which would have narrowed every
82
+ existing call. Omitted, every repo, as before.
83
+
84
+ ### Changed
85
+
86
+ - **Requires witan-core 0.39 or later.** Nothing new is imported, so an older
87
+ witan-core loads, but it runs without 0.39.0's fewer lost writes between
88
+ parallel agents (jittered conflict retries and `WriteContention`, and
89
+ in-process serialisation of `s3://` writes), and without the refusal text
90
+ that `log_safe_message` puts on the `mcp.tool_call` log line. The floor
91
+ makes an external install pick them up.
92
+
11
93
  ## [0.20.1] - 2026-09-22
12
94
 
13
95
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: witan-code
3
- Version: 0.20.1
3
+ Version: 0.22.0
4
4
  Summary: witan-code — tree-sitter code graph + cross-repo bridge (mounts under `witan code`)
5
5
  Project-URL: Homepage, https://github.com/mitodl/agent-kit/tree/main/mcp/servers/witan-code
6
6
  Project-URL: Repository, https://github.com/mitodl/agent-kit
@@ -10,7 +10,7 @@ License-Expression: BSD-3-Clause
10
10
  Keywords: agent,code-graph,coding-agent,mcp,static-analysis,tree-sitter
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.11
13
- Requires-Dist: agent-config-kit<1,>=0.7
13
+ Requires-Dist: agent-config-kit<1,>=0.10
14
14
  Requires-Dist: cyclopts<5,>=4
15
15
  Requires-Dist: fastmcp<5,>=4
16
16
  Requires-Dist: joserfc<2,>=1.0
@@ -26,7 +26,7 @@ Requires-Dist: tree-sitter-sql<0.4,>=0.3
26
26
  Requires-Dist: tree-sitter-typescript<0.24,>=0.23
27
27
  Requires-Dist: tree-sitter-yaml<0.8,>=0.7
28
28
  Requires-Dist: tree-sitter<0.26,>=0.25
29
- Requires-Dist: witan-core[cli,observability,remote,sentry]<1,>=0.38
29
+ Requires-Dist: witan-core[cli,observability,remote,sentry]<1,>=0.40
30
30
  Provides-Extra: tasks
31
31
  Requires-Dist: fastmcp-tasks<5,>=4; extra == 'tasks'
32
32
  Description-Content-Type: text/markdown
@@ -299,6 +299,12 @@ witan-code setup --dry-run # preview without writing
299
299
  witan-code setup --author "Jane Doe" # attribution (default: git config user.name)
300
300
  ```
301
301
 
302
+ On Pi, the MCP server entry (`~/.pi/agent/mcp.json`) is read only by the
303
+ third-party pi-mcp-adapter Pi package, since Pi core has no MCP support:
304
+ install it with `pi install npm:pi-mcp-adapter`, restart Pi, and confirm it
305
+ in `pi list`. `witan-code setup --agent pi` warns when it cannot find the
306
+ package declared in Pi's settings.
307
+
302
308
  If `witan` is *also* installed and witan-code is importable in that same
303
309
  environment (e.g. via the `--with` in the `uv tool install`/MCP server's
304
310
  `uvx` invocation), `witan setup` folds this same bundle in automatically —
@@ -715,7 +721,10 @@ A Pi equivalent of all four lives in one extension,
715
721
  can tell "no cross-repo consumers" from "no cross-repo data"), and the
716
722
  `ToolSearch` call that makes the `code_*` tools callable when the harness
717
723
  delivers them deferred — followed by a `code_find_definition` →
718
- `code_callers`/`code_impact` call template.
724
+ `code_callers`/`code_impact` call template. `--client pi` (what the Pi
725
+ extension passes) swaps the `ToolSearch` step for pi-mcp-adapter's `mcp`
726
+ proxy: `mcp({ search: ... })` for the exact server-prefixed name, then
727
+ `mcp({ tool: ..., args: {...} })`. The default stays Claude's.
719
728
  Independent of `witan`'s own `inject-context` hook (no cross-package
720
729
  coupling) — register it alone for a witan-code-only install. Prints
721
730
  nothing when the repo has neither a store nor an index in flight.
@@ -266,6 +266,12 @@ witan-code setup --dry-run # preview without writing
266
266
  witan-code setup --author "Jane Doe" # attribution (default: git config user.name)
267
267
  ```
268
268
 
269
+ On Pi, the MCP server entry (`~/.pi/agent/mcp.json`) is read only by the
270
+ third-party pi-mcp-adapter Pi package, since Pi core has no MCP support:
271
+ install it with `pi install npm:pi-mcp-adapter`, restart Pi, and confirm it
272
+ in `pi list`. `witan-code setup --agent pi` warns when it cannot find the
273
+ package declared in Pi's settings.
274
+
269
275
  If `witan` is *also* installed and witan-code is importable in that same
270
276
  environment (e.g. via the `--with` in the `uv tool install`/MCP server's
271
277
  `uvx` invocation), `witan setup` folds this same bundle in automatically —
@@ -682,7 +688,10 @@ A Pi equivalent of all four lives in one extension,
682
688
  can tell "no cross-repo consumers" from "no cross-repo data"), and the
683
689
  `ToolSearch` call that makes the `code_*` tools callable when the harness
684
690
  delivers them deferred — followed by a `code_find_definition` →
685
- `code_callers`/`code_impact` call template.
691
+ `code_callers`/`code_impact` call template. `--client pi` (what the Pi
692
+ extension passes) swaps the `ToolSearch` step for pi-mcp-adapter's `mcp`
693
+ proxy: `mcp({ search: ... })` for the exact server-prefixed name, then
694
+ `mcp({ tool: ..., args: {...} })`. The default stays Claude's.
686
695
  Independent of `witan`'s own `inject-context` hook (no cross-package
687
696
  coupling) — register it alone for a witan-code-only install. Prints
688
697
  nothing when the repo has neither a store nor an index in flight.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "witan-code"
3
- version = "0.20.1"
3
+ version = "0.22.0"
4
4
  description = "witan-code — tree-sitter code graph + cross-repo bridge (mounts under `witan code`)"
5
5
  readme = "README.md"
6
6
  license = "BSD-3-Clause"
@@ -17,7 +17,11 @@ dependencies = [
17
17
  # Same blind spot as the witan-core floors below — the workspace resolves
18
18
  # agent-config-kit by path — except there is NO `check-core-floor`
19
19
  # equivalent for this dependency, so this line is the only guard.
20
- "agent-config-kit>=0.7,<1",
20
+ # >=0.10: `witan-code setup`'s install report promises a warning when Pi
21
+ # lacks pi-mcp-adapter, which comes from `InstallResult.prerequisites`,
22
+ # added in 0.10.0. witan_core's `report_install` reads it with getattr, so
23
+ # an older resolution does not fail — it just silently drops the warning.
24
+ "agent-config-kit>=0.10,<1",
21
25
  # `remote`: the ADR-0005 client stack (OIDC device grant + MCP-client
22
26
  # proxy) behind `witan-code login` and the read commands' remote mode.
23
27
  # Costs nothing extra in practice — fastmcp is already required here and
@@ -142,7 +146,22 @@ dependencies = [
142
146
  # parses the keyed `code-schema.pg`, and it restores the null fields 0.11
143
147
  # drops from query rows. An older witan-core imports fine and then fails
144
148
  # on the first schema init.
145
- "witan-core[cli,remote,observability,sentry]>=0.38,<1",
149
+ #
150
+ # ★ >=0.39 IS A BEHAVIOUR FLOOR, not an import one: nothing here imports a
151
+ # 0.39 symbol, and an older witan-core loads fine. It runs without two
152
+ # things, though. 0.39.0 carries #394's conflict-retry fix (jittered
153
+ # backoff, and `WriteContention` when a write exhausts its retries) and
154
+ # #373's in-process serialisation of `s3://` writes, which reduce the
155
+ # code-graph writes parallel agents lose to each other. And
156
+ # `ClusterGraphMissing` sets `log_safe_message`, which only 0.39 reads: on
157
+ # 0.38 its text never reaches the `mcp.tool_call` log line.
158
+ #
159
+ # ★ >=0.40 IS A BEHAVIOUR FLOOR too: `witan-code setup --agent pi` promises
160
+ # a warning when Pi lacks pi-mcp-adapter, and
161
+ # `witan_core.cli.report_install` only prints `InstallResult.prerequisites`
162
+ # from 0.40.0. It reads the field with getattr, so 0.39 imports and runs —
163
+ # and silently drops the warning.
164
+ "witan-core[cli,remote,observability,sentry]>=0.40,<1",
146
165
  "fastmcp>=4,<5",
147
166
  "cyclopts>=4,<5",
148
167
  # [targets.<name>] override models (config.py) — also transitively pulled
@@ -225,7 +244,7 @@ testpaths = ["tests"]
225
244
  packages = ["witan_code"]
226
245
 
227
246
  [tool.bumpversion]
228
- current_version = "0.20.1"
247
+ current_version = "0.22.0"
229
248
  allow_dirty = true
230
249
 
231
250
  [[tool.bumpversion.files]]
@@ -410,6 +410,22 @@ def test_cross_repo_env_and_endpoint_linkage(tmp_path, monkeypatch):
410
410
  "endpoint", "/api/v1/courses/{id}/"
411
411
  )
412
412
  assert any(c["repo"] == "https://github.com/test/repo-a" for c in ep_consumers)
413
+ # The trust score comes back with each row, so a reader can tell the
414
+ # consumer rows that made a code_repo_dependencies edge (>= 0.5) from the
415
+ # ones that did not.
416
+ assert all(isinstance(c["confidence"], float) for c in ep_consumers)
417
+
418
+ # `in_repo` narrows in the query, to that exact repo.
419
+ only_b = _fn(srv.code_interface_providers)(
420
+ "env_var", "MITOL_APP_BASE_URL", in_repo="https://github.com/test/repo-b"
421
+ )
422
+ assert only_b == providers
423
+ assert (
424
+ _fn(srv.code_interface_providers)(
425
+ "env_var", "MITOL_APP_BASE_URL", in_repo="https://github.com/test/repo-a"
426
+ )
427
+ == []
428
+ )
413
429
 
414
430
 
415
431
  @requires_stack
@@ -14,7 +14,7 @@ def test_inject_context_cmd_prints_backend_output_without_extra_newline(
14
14
  import witan_code.context as context_module
15
15
 
16
16
  monkeypatch.setattr(
17
- context_module, "inject_context", lambda: "## Code Graph\n\ntext.\n"
17
+ context_module, "inject_context", lambda client: "## Code Graph\n\ntext.\n"
18
18
  )
19
19
 
20
20
  cli.inject_context_cmd()
@@ -27,7 +27,7 @@ def test_inject_context_cmd_prints_nothing_for_empty_backend_output(
27
27
  ):
28
28
  import witan_code.context as context_module
29
29
 
30
- monkeypatch.setattr(context_module, "inject_context", lambda: "")
30
+ monkeypatch.setattr(context_module, "inject_context", lambda client: "")
31
31
 
32
32
  cli.inject_context_cmd()
33
33
 
@@ -37,7 +37,7 @@ def test_inject_context_cmd_prints_nothing_for_empty_backend_output(
37
37
  def test_inject_context_cmd_never_raises_when_backend_fails(monkeypatch, capsys):
38
38
  import witan_code.context as context_module
39
39
 
40
- def _boom():
40
+ def _boom(client):
41
41
  raise RuntimeError("graph read failed")
42
42
 
43
43
  monkeypatch.setattr(context_module, "inject_context", _boom)
@@ -45,3 +45,32 @@ def test_inject_context_cmd_never_raises_when_backend_fails(monkeypatch, capsys)
45
45
  cli.inject_context_cmd() # must not raise
46
46
 
47
47
  assert capsys.readouterr().out == ""
48
+
49
+
50
+ def test_inject_context_cmd_defaults_to_the_claude_client(monkeypatch, capsys):
51
+ """The Claude hook runs the bare command, so the default must stay Claude."""
52
+ import witan_code.context as context_module
53
+
54
+ seen = []
55
+ monkeypatch.setattr(
56
+ context_module, "inject_context", lambda client: seen.append(client) or ""
57
+ )
58
+
59
+ command, bound, _ = cli.app.parse_args(["inject-context"])
60
+ command(*bound.args, **bound.kwargs)
61
+
62
+ assert seen == ["claude"]
63
+
64
+
65
+ def test_inject_context_cmd_passes_an_explicit_pi_client_through(monkeypatch):
66
+ import witan_code.context as context_module
67
+
68
+ seen = []
69
+ monkeypatch.setattr(
70
+ context_module, "inject_context", lambda client: seen.append(client) or ""
71
+ )
72
+
73
+ command, bound, _ = cli.app.parse_args(["inject-context", "--client", "pi"])
74
+ command(*bound.args, **bound.kwargs)
75
+
76
+ assert seen == ["pi"]
@@ -393,3 +393,52 @@ def test_inject_context_block_stays_small(tmp_path, monkeypatch):
393
393
  _stub_store_stats(monkeypatch)
394
394
 
395
395
  assert len(context.inject_context()) < 600
396
+
397
+
398
+ def _indexed(tmp_path, monkeypatch):
399
+ monkeypatch.setenv("WITAN_REPO", "https://github.com/test/cg")
400
+ code_dir = tmp_path / "code"
401
+ monkeypatch.setenv("WITAN_CODE_DIR", str(code_dir))
402
+ _lock(tmp_path, monkeypatch, tmp_path / "project")
403
+ (code_dir / "https_github.com_test_cg.omni").mkdir(parents=True)
404
+ _stub_store_stats(monkeypatch)
405
+
406
+
407
+ def test_inject_context_default_and_explicit_claude_render_the_same(
408
+ tmp_path, monkeypatch
409
+ ):
410
+ _indexed(tmp_path, monkeypatch)
411
+
412
+ default = context.inject_context()
413
+
414
+ assert default == context.inject_context(client="claude")
415
+ assert "ToolSearch" in default
416
+ assert "mcp({" not in default
417
+ assert "`/witan-code`" in default
418
+
419
+
420
+ def test_inject_context_for_pi_names_the_mcp_proxy_not_toolsearch(
421
+ tmp_path, monkeypatch
422
+ ):
423
+ """Pi has no ToolSearch; its MCP tools sit behind pi-mcp-adapter's `mcp`.
424
+
425
+ The adapter prefixes each tool with its server name, and that prefix
426
+ depends on the user's config, so the block must search first and then
427
+ call whatever exact name the search returned.
428
+ """
429
+ _indexed(tmp_path, monkeypatch)
430
+
431
+ text = context.inject_context(client="pi")
432
+
433
+ assert "ToolSearch" not in text
434
+ assert "select:" not in text
435
+ assert 'mcp({ search: "code_find_definition callers impact" })' in text
436
+ assert 'mcp({ tool: "<exact name the search returned>", args: {' in text
437
+ # Search sees only cached metadata; a lazy server with no cache entry
438
+ # needs a connect first, or the block dead-ends.
439
+ assert 'mcp({ connect: "witan-code" })' in text
440
+ assert "/skill:witan-code" in text # Pi's skill command form
441
+ # The status lines above the discovery line are client-neutral.
442
+ assert "https://github.com/test/cg" in text
443
+ assert "3 files" in text
444
+ assert "No other repo is indexed" in text
@@ -10,6 +10,7 @@ correct.
10
10
  """
11
11
 
12
12
  import json
13
+ import re
13
14
  from pathlib import Path
14
15
 
15
16
  from agent_config_kit import apply
@@ -138,3 +139,93 @@ def test_setup_does_not_abort_on_a_refused_omnigraph_binary(tmp_path, monkeypatc
138
139
  cli.setup(agent="claude", author="tester")
139
140
 
140
141
  assert calls == [{"dry_run": False, "strict": False}]
142
+
143
+
144
+ def _pi_setup_output(tmp_path, monkeypatch, capsys, *, dry_run: bool) -> str:
145
+ import witan_core
146
+
147
+ from witan_code import cli
148
+
149
+ monkeypatch.setattr(witan_core, "install_omnigraph", lambda dry_run, **kw: None)
150
+ monkeypatch.setattr(Path, "home", lambda: tmp_path)
151
+
152
+ cli.setup(agent="pi", author="tester", dry_run=dry_run)
153
+
154
+ return " ".join(capsys.readouterr().out.split())
155
+
156
+
157
+ def test_setup_pi_warns_when_pi_mcp_adapter_is_missing(tmp_path, monkeypatch, capsys):
158
+ """`witan-code setup --agent pi` inherits agent-config-kit's MCP
159
+ prerequisite preflight: Pi ignores ~/.pi/agent/mcp.json without the
160
+ pi-mcp-adapter package, so the report must say so on dry-run and apply."""
161
+ for dry_run in (True, False):
162
+ out = _pi_setup_output(tmp_path, monkeypatch, capsys, dry_run=dry_run)
163
+ assert "WARNING:" in out
164
+ assert "pi install npm:pi-mcp-adapter" in out
165
+
166
+
167
+ def test_setup_pi_notes_when_pi_mcp_adapter_is_installed(tmp_path, monkeypatch, capsys):
168
+ settings = tmp_path / ".pi" / "agent" / "settings.json"
169
+ settings.parent.mkdir(parents=True)
170
+ settings.write_text(json.dumps({"packages": ["npm:pi-mcp-adapter"]}))
171
+
172
+ out = _pi_setup_output(tmp_path, monkeypatch, capsys, dry_run=True)
173
+
174
+ assert "WARNING:" not in out
175
+ assert "note:" in out
176
+ assert "pi-mcp-adapter is declared in" in out
177
+
178
+
179
+ # --- Pi extension source contract ------------------------------------------
180
+ #
181
+ # The Pi extension is TypeScript that no test here can execute, so these read
182
+ # its source. They pin two things a Python-side change cannot see drifting:
183
+ # the Pi prompt-injection budget matching the Claude hook's, and the three
184
+ # copies of the file (package, configs/pi mirror) staying byte-identical.
185
+
186
+ _PKG_EXT = Path(setup.__file__).parent / "extensions" / "pi" / "codegraph.ts"
187
+ _REPO_ROOT = Path(__file__).resolve().parents[4]
188
+ _CONFIG_EXT = _REPO_ROOT / "configs" / "pi" / "extensions" / "codegraph.ts"
189
+
190
+
191
+ def _ts_const_ms(source: str, name: str) -> int:
192
+ match = re.search(rf"^const {name} = ([\d_]+);$", source, re.MULTILINE)
193
+ assert match, f"{name} is not declared as a numeric const"
194
+ return int(match.group(1).replace("_", ""))
195
+
196
+
197
+ def test_pi_inject_context_timeout_matches_the_claude_hook():
198
+ """A 5s Pi budget killed the cold read every Claude prompt waits out."""
199
+ source = _PKG_EXT.read_text()
200
+ timeout_ms = _ts_const_ms(source, "INJECT_CONTEXT_TIMEOUT_MS")
201
+
202
+ assert timeout_ms == setup.INJECT_CONTEXT_TIMEOUT_SECONDS * 1000
203
+ assert setup.INJECT_CONTEXT_TIMEOUT_SECONDS >= 15
204
+ # The one blocking call is the one that uses the constant; nothing else
205
+ # hard-codes its own number.
206
+ assert "timeout: INJECT_CONTEXT_TIMEOUT_MS" in source
207
+ assert source.count("spawnSync(") == 1
208
+ assert "timeout: 5000" not in source
209
+
210
+
211
+ def test_claude_inject_context_hook_uses_the_shared_constant(tmp_path):
212
+ bundle = setup.witan_code_bundle(tmp_path, "tester")
213
+ (hook,) = [
214
+ h
215
+ for h in bundle.hooks
216
+ if getattr(h, "command", None) == "witan-code inject-context"
217
+ ]
218
+ assert hook.timeout_seconds == setup.INJECT_CONTEXT_TIMEOUT_SECONDS
219
+
220
+
221
+ def test_configs_pi_mirror_matches_the_package_extension():
222
+ assert _CONFIG_EXT.read_bytes() == _PKG_EXT.read_bytes(), (
223
+ f"{_CONFIG_EXT} drifted from {_PKG_EXT}; copy the package file over it"
224
+ )
225
+
226
+
227
+ def test_pi_extension_asks_for_pi_rendered_context():
228
+ """Without `--client pi` the block tells Pi to call a ToolSearch it lacks."""
229
+ source = _PKG_EXT.read_text()
230
+
231
+ assert 'spawnSync("witan-code", ["inject-context", "--client", "pi"]' in source
@@ -353,6 +353,41 @@ def test_build_graph_confidence_on_emitted_edge_contracts():
353
353
  assert ep_contracts[0]["confidence"] == 0.75
354
354
 
355
355
 
356
+ def test_build_graph_flags_generic_contracts_without_dropping_them():
357
+ """A stoplisted key still makes its edge, flagged, so a reader can hide it."""
358
+ rows = [
359
+ {
360
+ "kind": "env_var",
361
+ "key_norm": "PORT",
362
+ "role": "consumer",
363
+ "repo": A,
364
+ "generic": "1",
365
+ },
366
+ {
367
+ "kind": "env_var",
368
+ "key_norm": "PORT",
369
+ "role": "provider",
370
+ "repo": B,
371
+ "generic": "1",
372
+ },
373
+ {
374
+ "kind": "env_var",
375
+ "key_norm": "MITOL_APP_BASE_URL",
376
+ "role": "consumer",
377
+ "repo": A,
378
+ },
379
+ {
380
+ "kind": "env_var",
381
+ "key_norm": "MITOL_APP_BASE_URL",
382
+ "role": "provider",
383
+ "repo": B,
384
+ },
385
+ ]
386
+ g = visualize.build_graph(rows)
387
+ flags = {c["key"]: c["generic"] for c in g.edges[(A, B)].contracts}
388
+ assert flags == {"PORT": True, "MITOL_APP_BASE_URL": False}
389
+
390
+
356
391
  def test_build_graph_rejects_invalid_min_precision():
357
392
  with pytest.raises(ValueError):
358
393
  visualize.build_graph(ROWS, min_precision="nonsense")
@@ -582,19 +582,27 @@ def stitch(repo: str | None = None, *, unresolved: bool = False) -> None:
582
582
 
583
583
 
584
584
  @app.command(name="inject-context")
585
- def inject_context_cmd() -> None:
585
+ def inject_context_cmd(*, client: Literal["claude", "pi"] = "claude") -> None:
586
586
  """Print a short code-graph status block for the UserPromptSubmit hook.
587
587
 
588
588
  Registered as the bare ``UserPromptSubmit`` hook command; always exits 0
589
589
  and prints nothing when there's no store or in-flight index for the
590
590
  current repo.
591
+
592
+ Parameters
593
+ ----------
594
+ client
595
+ Which agent the block's tool-discovery instructions are written for:
596
+ ``claude`` (``ToolSearch``, the default) or ``pi`` (pi-mcp-adapter's
597
+ ``mcp`` proxy). The Pi extension passes ``--client pi``; nothing is
598
+ inferred from the environment.
591
599
  """
592
600
  import sys
593
601
 
594
602
  from . import context as context_module
595
603
 
596
604
  try:
597
- text = context_module.inject_context()
605
+ text = context_module.inject_context(client=client)
598
606
  except Exception: # noqa: BLE001 — must never fail the hook
599
607
  return
600
608
  if text:
@@ -946,6 +954,11 @@ def setup(
946
954
  `witan setup` — running both is fine (each only touches its own entries);
947
955
  running just this one is enough for a witan-code-only install.
948
956
 
957
+ Pi has no built-in MCP support: the witan-code entry written to
958
+ ``~/.pi/agent/mcp.json`` is read only by the pi-mcp-adapter Pi package
959
+ (``pi install npm:pi-mcp-adapter``). The install report warns when that
960
+ package is not declared in Pi's settings.
961
+
949
962
  Re-run after every upgrade to refresh installed files.
950
963
 
951
964
  Parameters
@@ -21,6 +21,11 @@ cannot see in its tool list is not actionable, so the block leads with the
21
21
  ``ToolSearch`` that makes the tools callable and then gives a call template to
22
22
  fill in.
23
23
 
24
+ That unlock step is Claude Code's. Pi has no ``ToolSearch``: its MCP tools
25
+ reach the agent through pi-mcp-adapter's ``mcp`` proxy tool, so the Pi
26
+ extension asks for ``--client pi`` and gets the proxy's search-then-call
27
+ instructions instead (see :func:`_discovery_line`).
28
+
24
29
  Kept short on purpose: this is prepended to *every* prompt, so tokens spent
25
30
  here are spent for the life of every session.
26
31
  """
@@ -35,6 +40,7 @@ import stat
35
40
  import tempfile
36
41
  import time
37
42
  from pathlib import Path
43
+ from typing import Literal
38
44
 
39
45
  from . import config as cfg_module
40
46
  from . import repo as repo_module
@@ -144,6 +150,54 @@ def indexing_in_progress() -> bool:
144
150
  # under any prefix and puts the three workhorse tools first.
145
151
  _TOOLSEARCH_QUERY = '`ToolSearch(query="+code_ find_definition callers impact")`'
146
152
 
153
+ # Which agent the block is rendered for. Passed explicitly by the caller (the
154
+ # Pi extension runs `witan-code inject-context --client pi`), never inferred
155
+ # from the environment: an ambient variable leaking across a nested shell or a
156
+ # subprocess would silently hand one agent the other's calling convention.
157
+ Client = Literal["claude", "pi"]
158
+
159
+ # Pi has no ToolSearch. Under pi-mcp-adapter's default config (no
160
+ # `directTools`, which `witan-code setup --agent pi` does not set) every MCP
161
+ # tool sits behind the adapter's single `mcp` proxy tool, and its name carries
162
+ # a server prefix (`toolPrefix`, default "server": `witan-code_code_callers`
163
+ # for a server named `witan-code`, `witan_code_callers` when witan serves the
164
+ # code tools itself). The prefix is config-dependent, so the block has the
165
+ # agent search first and call whatever name the search returned. Search
166
+ # tokenizes on non-alphanumerics and ranks by term matches, so this query puts
167
+ # the three workhorse tools first under any prefix — the same job the `+code_`
168
+ # form does for ToolSearch. Syntax per pi-mcp-adapter's README "Usage" table
169
+ # (`mcp({ search })`, `mcp({ tool, args })`).
170
+ _PI_SEARCH_CALL = '`mcp({ search: "code_find_definition callers impact" })`'
171
+ _PI_TOOL_CALL = (
172
+ '`mcp({ tool: "<exact name the search returned>", args: { name: "X" } })`'
173
+ )
174
+ # Search reads only the adapter's cached tool metadata, and servers are lazy by
175
+ # default, so a server with no valid cache entry (first use after the cache
176
+ # file already existed, or unused for the cache's 7-day lifetime) returns no
177
+ # matches. Connecting refreshes the metadata; the block names both server
178
+ # names setup registers, since either may be the one serving `code_*` tools.
179
+ _PI_CONNECT_CALL = '`mcp({ connect: "witan-code" })` (or `"witan"`)'
180
+
181
+
182
+ def _discovery_line(client: Client) -> str:
183
+ """How to reach the ``code_*`` tools from this client, then what to call."""
184
+ if client == "pi":
185
+ return (
186
+ "`code_*` tools are behind the `mcp` proxy tool, not in your tool "
187
+ f"list — find their exact names with {_PI_SEARCH_CALL}, then call "
188
+ f"one with {_PI_TOOL_CALL} instead of grep (if the search finds "
189
+ f"nothing, run {_PI_CONNECT_CALL} and search again): "
190
+ "`code_find_definition` "
191
+ "→ `symbol_id` → `code_callers` / `code_impact` (blast radius "
192
+ "before editing). More: `/skill:witan-code`."
193
+ )
194
+ return (
195
+ "`code_*` tools may not be in your tool list — load them with "
196
+ f"{_TOOLSEARCH_QUERY}, then use them instead of grep: "
197
+ '`code_find_definition(name="X")` → `symbol_id` → `code_callers` / '
198
+ "`code_impact` (blast radius before editing). More: `/witan-code`."
199
+ )
200
+
147
201
 
148
202
  def _coverage_line(store: store_module.StoreRef, cfg: cfg_module.Config) -> str:
149
203
  """One line on whether cross-repo resolution can answer anything here.
@@ -245,11 +299,14 @@ def _bridge_ok(cfg: cfg_module.Config) -> bool | None:
245
299
  return ok
246
300
 
247
301
 
248
- def inject_context() -> str:
302
+ def inject_context(client: Client = "claude") -> str:
249
303
  """A short markdown status block, or "" when there's nothing worth saying.
250
304
 
251
305
  Silent when the repo has neither a store nor an index in flight (nothing
252
306
  to report), so this hook adds no noise for repos that don't use witan-code.
307
+
308
+ ``client`` picks the tool-discovery instructions: Claude's ``ToolSearch``
309
+ (the default) or Pi's ``mcp`` proxy. Everything else is client-neutral.
253
310
  """
254
311
  cfg = cfg_module.load()
255
312
  slug = store_module.detect_repo(cfg)
@@ -311,10 +368,5 @@ def inject_context() -> str:
311
368
  coverage = _coverage_line(store, cfg)
312
369
  if coverage:
313
370
  lines.append(coverage)
314
- lines.append(
315
- "`code_*` tools may not be in your tool list — load them with "
316
- f"{_TOOLSEARCH_QUERY}, then use them instead of grep: "
317
- '`code_find_definition(name="X")` → `symbol_id` → `code_callers` / '
318
- "`code_impact` (blast radius before editing). More: `/witan-code`."
319
- )
371
+ lines.append(_discovery_line(client))
320
372
  return "\n".join(lines) + "\n"