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.
- {witan_code-0.21.0 → witan_code-0.22.0}/CHANGELOG.md +50 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/PKG-INFO +13 -4
- {witan_code-0.21.0 → witan_code-0.22.0}/README.md +10 -1
- {witan_code-0.21.0 → witan_code-0.22.0}/pyproject.toml +14 -4
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_cli_hooks.py +32 -3
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_context.py +49 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_setup.py +91 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/cli.py +15 -2
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/context.py +59 -7
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/extensions/pi/codegraph.ts +28 -7
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/setup.py +15 -3
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/skills/witan-code/SKILL.md +29 -4
- {witan_code-0.21.0 → witan_code-0.22.0}/.gitignore +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/changelog.d/README.md +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/config/claude.json +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/config/copilot.json +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/config/pi.json +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/conftest.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/docs/BRANCH_INDEXING.md +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/docs/CLI_REFERENCE.md +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/docs/EDGE_PRECISION_TIERS.md +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/docs/PACKAGE_MAP.md +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/docs/STAGE2_STITCHING.md +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/docs/SYMBOL_FORMAT.md +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/docs/SYMBOL_TABLE.md +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/docs/USER_GUIDE.md +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/install.sh +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/__init__.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/conftest.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_batched_deletes.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_branches.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_bridge.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_chunked_load.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_cli_dispatch.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_cli_output_format.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_cli_selected_target.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_config.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_config_targets.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_crossrepo.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_edges.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_elicit.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_enrichment.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_github_app.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_graph.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_hooks.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_identity.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_index_failure_diagnostics.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_index_reports_its_store.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_index_requires_a_repo_key.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_indexer.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_indexer_scope.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_ingest.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_languages.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_maintenance.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_package_map.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_reaper.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_rebuild_honours_the_repo_key.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_refusal_audit.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_remote_proxy.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_remote_store.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_repo.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_server.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_stitch.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_store.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_symbol_table.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_tasks.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_tool_schema.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_views.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/tests/test_visualize.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/__init__.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/__main__.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/bridge.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/bridge_extractors.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/config.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/edges.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/elicit.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/github_app.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/graph.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/hooks.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/identity.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/indexer.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/ingest.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/maintenance.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/output.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/package_map.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries/bridge.gq +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries/code_mutations.gq +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries/code_read.gq +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries/delete.gq +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries_ts/bash.scm +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries_ts/hcl.scm +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries_ts/python.scm +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries_ts/sql.scm +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries_ts/typescript.scm +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/queries_ts/yaml.scm +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/reaper.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/remote/__init__.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/remote/oidc.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/remote/proxy.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/remote/store.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/repo.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/schema/bridge-schema.pg +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/schema/code-schema.pg +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/selected_target.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/server.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/stitch.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/store.py +0 -0
- {witan_code-0.21.0 → witan_code-0.22.0}/witan_code/views.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
13
|
-
*
|
|
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
|
|
21
|
-
*
|
|
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:
|
|
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
|
|
75
|
-
# degrade to no context/no compaction, never stall the
|
|
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=
|
|
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
|
|
23
|
-
background, and a
|
|
24
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|