witan-code 0.21.0__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.21.0 → witan_code-0.22.0}/CHANGELOG.md +50 -0
  2. {witan_code-0.21.0 → witan_code-0.22.0}/PKG-INFO +13 -4
  3. {witan_code-0.21.0 → witan_code-0.22.0}/README.md +10 -1
  4. {witan_code-0.21.0 → witan_code-0.22.0}/pyproject.toml +14 -4
  5. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_cli_hooks.py +32 -3
  6. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_context.py +49 -0
  7. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_setup.py +91 -0
  8. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/cli.py +15 -2
  9. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/context.py +59 -7
  10. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/extensions/pi/codegraph.ts +28 -7
  11. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/setup.py +15 -3
  12. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/skills/witan-code/SKILL.md +29 -4
  13. {witan_code-0.21.0 → witan_code-0.22.0}/.gitignore +0 -0
  14. {witan_code-0.21.0 → witan_code-0.22.0}/changelog.d/README.md +0 -0
  15. {witan_code-0.21.0 → witan_code-0.22.0}/config/claude.json +0 -0
  16. {witan_code-0.21.0 → witan_code-0.22.0}/config/copilot.json +0 -0
  17. {witan_code-0.21.0 → witan_code-0.22.0}/config/pi.json +0 -0
  18. {witan_code-0.21.0 → witan_code-0.22.0}/conftest.py +0 -0
  19. {witan_code-0.21.0 → witan_code-0.22.0}/docs/BRANCH_INDEXING.md +0 -0
  20. {witan_code-0.21.0 → witan_code-0.22.0}/docs/CLI_REFERENCE.md +0 -0
  21. {witan_code-0.21.0 → witan_code-0.22.0}/docs/EDGE_PRECISION_TIERS.md +0 -0
  22. {witan_code-0.21.0 → witan_code-0.22.0}/docs/PACKAGE_MAP.md +0 -0
  23. {witan_code-0.21.0 → witan_code-0.22.0}/docs/STAGE2_STITCHING.md +0 -0
  24. {witan_code-0.21.0 → witan_code-0.22.0}/docs/SYMBOL_FORMAT.md +0 -0
  25. {witan_code-0.21.0 → witan_code-0.22.0}/docs/SYMBOL_TABLE.md +0 -0
  26. {witan_code-0.21.0 → witan_code-0.22.0}/docs/USER_GUIDE.md +0 -0
  27. {witan_code-0.21.0 → witan_code-0.22.0}/install.sh +0 -0
  28. {witan_code-0.21.0 → witan_code-0.22.0}/tests/__init__.py +0 -0
  29. {witan_code-0.21.0 → witan_code-0.22.0}/tests/conftest.py +0 -0
  30. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_batched_deletes.py +0 -0
  31. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_branches.py +0 -0
  32. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_bridge.py +0 -0
  33. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_chunked_load.py +0 -0
  34. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_cli_dispatch.py +0 -0
  35. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_cli_output_format.py +0 -0
  36. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_cli_selected_target.py +0 -0
  37. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_config.py +0 -0
  38. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_config_targets.py +0 -0
  39. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_crossrepo.py +0 -0
  40. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_edges.py +0 -0
  41. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_elicit.py +0 -0
  42. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_enrichment.py +0 -0
  43. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_github_app.py +0 -0
  44. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_graph.py +0 -0
  45. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_hooks.py +0 -0
  46. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_identity.py +0 -0
  47. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_index_failure_diagnostics.py +0 -0
  48. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_index_reports_its_store.py +0 -0
  49. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_index_requires_a_repo_key.py +0 -0
  50. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_indexer.py +0 -0
  51. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_indexer_scope.py +0 -0
  52. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_ingest.py +0 -0
  53. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_languages.py +0 -0
  54. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_maintenance.py +0 -0
  55. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_package_map.py +0 -0
  56. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_reaper.py +0 -0
  57. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_rebuild_honours_the_repo_key.py +0 -0
  58. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_refusal_audit.py +0 -0
  59. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_remote_proxy.py +0 -0
  60. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_remote_store.py +0 -0
  61. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_repo.py +0 -0
  62. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_server.py +0 -0
  63. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_stitch.py +0 -0
  64. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_store.py +0 -0
  65. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_symbol_table.py +0 -0
  66. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_tasks.py +0 -0
  67. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_tool_schema.py +0 -0
  68. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_views.py +0 -0
  69. {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_visualize.py +0 -0
  70. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/__init__.py +0 -0
  71. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/__main__.py +0 -0
  72. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/bridge.py +0 -0
  73. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/bridge_extractors.py +0 -0
  74. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/config.py +0 -0
  75. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/edges.py +0 -0
  76. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/elicit.py +0 -0
  77. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/github_app.py +0 -0
  78. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/graph.py +0 -0
  79. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/hooks.py +0 -0
  80. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/identity.py +0 -0
  81. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/indexer.py +0 -0
  82. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/ingest.py +0 -0
  83. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/maintenance.py +0 -0
  84. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/output.py +0 -0
  85. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/package_map.py +0 -0
  86. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries/bridge.gq +0 -0
  87. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries/code_mutations.gq +0 -0
  88. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries/code_read.gq +0 -0
  89. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries/delete.gq +0 -0
  90. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries_ts/bash.scm +0 -0
  91. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries_ts/hcl.scm +0 -0
  92. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries_ts/python.scm +0 -0
  93. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries_ts/sql.scm +0 -0
  94. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries_ts/typescript.scm +0 -0
  95. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries_ts/yaml.scm +0 -0
  96. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/reaper.py +0 -0
  97. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/remote/__init__.py +0 -0
  98. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/remote/oidc.py +0 -0
  99. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/remote/proxy.py +0 -0
  100. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/remote/store.py +0 -0
  101. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/repo.py +0 -0
  102. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/schema/bridge-schema.pg +0 -0
  103. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/schema/code-schema.pg +0 -0
  104. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/selected_target.py +0 -0
  105. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/server.py +0 -0
  106. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/stitch.py +0 -0
  107. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/store.py +0 -0
  108. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/views.py +0 -0
  109. {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/visualize.py +0 -0
@@ -8,6 +8,56 @@ 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
+
11
61
  ## [0.21.0] - 2026-09-23
12
62
 
13
63
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: witan-code
3
- Version: 0.21.0
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.39
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.21.0"
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
@@ -151,7 +155,13 @@ dependencies = [
151
155
  # code-graph writes parallel agents lose to each other. And
152
156
  # `ClusterGraphMissing` sets `log_safe_message`, which only 0.39 reads: on
153
157
  # 0.38 its text never reaches the `mcp.tool_call` log line.
154
- "witan-core[cli,remote,observability,sentry]>=0.39,<1",
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",
155
165
  "fastmcp>=4,<5",
156
166
  "cyclopts>=4,<5",
157
167
  # [targets.<name>] override models (config.py) — also transitively pulled
@@ -234,7 +244,7 @@ testpaths = ["tests"]
234
244
  packages = ["witan_code"]
235
245
 
236
246
  [tool.bumpversion]
237
- current_version = "0.21.0"
247
+ current_version = "0.22.0"
238
248
  allow_dirty = true
239
249
 
240
250
  [[tool.bumpversion.files]]
@@ -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
@@ -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"
@@ -9,16 +9,21 @@
9
9
  * tool runs.
10
10
  * - before_agent_start: report whether the code graph is indexed (file
11
11
  * count, last-updated) or still being built, plus
12
- * cross-repo coverage and the ToolSearch +
13
- * code_find_definition calls to make against it.
12
+ * cross-repo coverage and how to reach the code_*
13
+ * tools. Runs `inject-context --client pi`, so the
14
+ * block names pi-mcp-adapter's `mcp` proxy
15
+ * (search, then call) rather than Claude's
16
+ * ToolSearch, which Pi does not have.
14
17
  * - session_shutdown : opportunistically compact the current repo's store
15
18
  * and the shared cross-repo bridge store (throttled;
16
19
  * see witan_code.maintenance) — no session-id
17
20
  * dependency, unlike workflow-session-checkpoint, so
18
21
  * this one *is* mirrored under Pi.
19
22
  *
20
- * Best-effort and non-blocking: a missing CLI, non-git dir, or parse failure
21
- * never disrupts the session. Requires `witan-code` on PATH
23
+ * Best-effort: a missing CLI, non-git dir, or parse failure never disrupts
24
+ * the session. Every handler is detached and non-blocking except
25
+ * before_agent_start, which waits up to INJECT_CONTEXT_TIMEOUT_MS for the
26
+ * status block (the same budget the Claude hook gets). Requires `witan-code` on PATH
22
27
  * (`witan-code setup --agent pi`, or `uv tool install --editable
23
28
  * mcp/servers/witan-code`); otherwise it silently no-ops.
24
29
  *
@@ -31,6 +36,20 @@ import { existsSync } from "node:fs";
31
36
  import { isAbsolute, resolve } from "node:path";
32
37
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
33
38
 
39
+ /**
40
+ * How long before_agent_start waits for `witan-code inject-context`: 15s, the
41
+ * same budget as the Claude `UserPromptSubmit` hook `witan-code setup`
42
+ * installs (`witan_code.setup.INJECT_CONTEXT_TIMEOUT_SECONDS`;
43
+ * tests/test_setup.py asserts the two agree). The first prompt in a cache
44
+ * window can pay a ~10s cold store read that must finish once to populate the
45
+ * on-disk cache; the 5s this used to be killed that read every time, so the
46
+ * cache never filled and every prompt came back with no block.
47
+ *
48
+ * Only this prompt-injection read waits. session_start's index, the per-edit
49
+ * reindex, and session_shutdown's checkpoint all stay detached.
50
+ */
51
+ const INJECT_CONTEXT_TIMEOUT_MS = 15_000;
52
+
34
53
  const SRC_EXT = /\.(py|pyi|ts|tsx|js|jsx|mjs|cjs)$/;
35
54
  const EDIT_TOOLS = new Set(["edit", "write"]);
36
55
 
@@ -96,12 +115,14 @@ export default function codegraphExtension(pi: ExtensionAPI): void {
96
115
  });
97
116
 
98
117
  // Report code-graph readiness before each turn (mirrors the Claude
99
- // `witan-code inject-context` UserPromptSubmit hook).
118
+ // `witan-code inject-context` UserPromptSubmit hook). A timeout, a
119
+ // missing CLI, or a non-zero exit all land in the `r.status !== 0` branch:
120
+ // no context, never a thrown error.
100
121
  pi.on("before_agent_start", async (event: any, ctx: any) => {
101
122
  try {
102
- const r = spawnSync("witan-code", ["inject-context"], {
123
+ const r = spawnSync("witan-code", ["inject-context", "--client", "pi"], {
103
124
  encoding: "utf8",
104
- timeout: 5000,
125
+ timeout: INJECT_CONTEXT_TIMEOUT_MS,
105
126
  cwd: ctx?.cwd,
106
127
  });
107
128
  const text = (r.stdout ?? "").trim();
@@ -33,6 +33,17 @@ _WITAN_CODE_ARGS = [
33
33
  # git `main`.
34
34
  _CLI_HOOK_PLATFORMS = ("claude", "pi")
35
35
 
36
+ # How long the prompt-path `witan-code inject-context` may run before its
37
+ # output is abandoned — the Claude `UserPromptSubmit` hook below AND the Pi
38
+ # extension's `before_agent_start` (`extensions/pi/codegraph.ts`, which spells
39
+ # it `INJECT_CONTEXT_TIMEOUT_MS`; tests/test_setup.py asserts the two agree).
40
+ # The first prompt in a cache window can pay a cold store read of ~10s, and it
41
+ # has to finish once to populate the on-disk cache: a timeout inside that cold
42
+ # path kills the read, discards the block, and leaves every later prompt cold
43
+ # too. 15s gives that path headroom while still degrading a hung git/store
44
+ # read to "no context" rather than a stalled turn.
45
+ INJECT_CONTEXT_TIMEOUT_SECONDS = 15
46
+
36
47
 
37
48
  def witan_code_bundle(
38
49
  pkg_dir: Path, author: str, *, binary: str = "witan-code"
@@ -71,8 +82,9 @@ def witan_code_bundle(
71
82
  # Bare CLI commands, no wrapper script — portable everywhere `binary`
72
83
  # installs (Windows included, where bash/setsid don't exist), matching
73
84
  # witan's own `witan inject-context`/`session-checkpoint` hooks. The
74
- # prompt-path timeouts mirror witan's: a hung git or store read must
75
- # degrade to no context/no compaction, never stall the agent.
85
+ # prompt-path timeouts exist for the same reason as witan's: a hung git or
86
+ # store read must degrade to no context/no compaction, never stall the
87
+ # agent (INJECT_CONTEXT_TIMEOUT_SECONDS says why inject-context gets 15s).
76
88
  hooks: list[Hook] = [
77
89
  DeclarativeHook(
78
90
  event=HookEvent.SESSION_START,
@@ -86,7 +98,7 @@ def witan_code_bundle(
86
98
  DeclarativeHook(
87
99
  event=HookEvent.USER_PROMPT_SUBMIT,
88
100
  command=f"{binary} inject-context",
89
- timeout_seconds=15,
101
+ timeout_seconds=INJECT_CONTEXT_TIMEOUT_SECONDS,
90
102
  ),
91
103
  DeclarativeHook(
92
104
  event=HookEvent.STOP,
@@ -19,9 +19,11 @@ A per-repo, tree-sitter-derived graph of symbols (functions, methods, classes,
19
19
  modules) and their relationships (`Calls`, `References`, `Imports`,
20
20
  `Inherits`), plus a cross-repo bridge linking services through shared
21
21
  contracts (env vars, HTTP endpoints, packages, deployments). Indexing is
22
- automatic — a `SessionStart` hook seeds/refreshes the whole repo in the
23
- background, and a `PostToolUse` hook incrementally reindexes each file you
24
- edit — so you rarely need to invoke the CLI yourself.
22
+ automatic — a session-start hook seeds/refreshes the whole repo in the
23
+ background, and a post-edit hook incrementally reindexes each file you edit
24
+ (Claude Code's `SessionStart`/`PostToolUse` hooks; under Pi, the witan-code
25
+ extension's `session_start`/`tool_result` handlers) — so you rarely need to
26
+ invoke the CLI yourself.
25
27
 
26
28
  ## When to use this vs. grep / the `Explore` agent
27
29
 
@@ -57,10 +59,33 @@ results as a high-recall starting point, not a verified answer. `Defines` and
57
59
  Before relying on results, confirm the current repo has a graph: call
58
60
  `code_symbols_in_file` on a file you know exists, or `code_search_symbol` with
59
61
  a term you expect to match. An empty result on a file/symbol you know exists
60
- usually means indexing hasn't finished yet (the `SessionStart` hook runs
62
+ usually means indexing hasn't finished yet (the session-start index runs
61
63
  detached in the background on a fresh repo) — wait a few seconds and retry
62
64
  before concluding the graph is broken.
63
65
 
66
+ ## Calling the tools
67
+
68
+ The tables below give each tool's bare name and arguments. How you reach one
69
+ depends on the agent:
70
+
71
+ - **Claude Code** — the `code_*` tools often arrive deferred (listed, no
72
+ schema). Load them with
73
+ `ToolSearch(query="+code_ find_definition callers impact")`, then call them
74
+ directly: `code_find_definition(name="X")`. Their full names carry an
75
+ `mcp__<server>__` prefix that depends on your MCP config, which is why the
76
+ `+code_` query form is used rather than `select:`.
77
+ - **Pi** — Pi has no `ToolSearch`. pi-mcp-adapter puts every MCP tool behind
78
+ its `mcp` proxy tool, under a server-prefixed name (for example
79
+ `witan-code_code_find_definition`, or `witan_code_find_definition` when
80
+ witan serves the code tools). Search for the exact name, then call it with
81
+ `args`:
82
+ `mcp({ search: "code_find_definition callers impact" })`, then
83
+ `mcp({ tool: "<exact name the search returned>", args: { name: "X" } })`.
84
+ Search reads only cached tool metadata, so if it finds nothing, run
85
+ `mcp({ connect: "witan-code" })` (or `"witan"`) and search again.
86
+ `mcp({ describe: "<name>" })` shows a tool's parameters. Invoke this skill
87
+ as `/skill:witan-code`.
88
+
64
89
  ## Tool reference
65
90
 
66
91
  | Question | Tool |
File without changes
File without changes
File without changes
File without changes