witan-code 0.23.0__tar.gz → 0.24.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.23.0 → witan_code-0.24.0}/CHANGELOG.md +26 -1
  2. {witan_code-0.23.0 → witan_code-0.24.0}/PKG-INFO +29 -18
  3. {witan_code-0.23.0 → witan_code-0.24.0}/README.md +27 -16
  4. {witan_code-0.23.0 → witan_code-0.24.0}/docs/PACKAGE_MAP.md +1 -1
  5. {witan_code-0.23.0 → witan_code-0.24.0}/docs/SYMBOL_FORMAT.md +6 -6
  6. {witan_code-0.23.0 → witan_code-0.24.0}/docs/SYMBOL_TABLE.md +1 -1
  7. {witan_code-0.23.0 → witan_code-0.24.0}/pyproject.toml +14 -5
  8. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_cli_hooks.py +6 -3
  9. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_context.py +22 -1
  10. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_maintenance.py +69 -5
  11. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_setup.py +75 -8
  12. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/cli.py +78 -28
  13. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/context.py +43 -11
  14. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/extensions/pi/codegraph.ts +32 -9
  15. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/maintenance.py +49 -6
  16. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/skills/witan-code/SKILL.md +15 -4
  17. {witan_code-0.23.0 → witan_code-0.24.0}/.gitignore +0 -0
  18. {witan_code-0.23.0 → witan_code-0.24.0}/changelog.d/README.md +0 -0
  19. {witan_code-0.23.0 → witan_code-0.24.0}/config/claude.json +0 -0
  20. {witan_code-0.23.0 → witan_code-0.24.0}/config/copilot.json +0 -0
  21. {witan_code-0.23.0 → witan_code-0.24.0}/config/pi.json +0 -0
  22. {witan_code-0.23.0 → witan_code-0.24.0}/conftest.py +0 -0
  23. {witan_code-0.23.0 → witan_code-0.24.0}/docs/BRANCH_INDEXING.md +0 -0
  24. {witan_code-0.23.0 → witan_code-0.24.0}/docs/CLI_REFERENCE.md +0 -0
  25. {witan_code-0.23.0 → witan_code-0.24.0}/docs/EDGE_PRECISION_TIERS.md +0 -0
  26. {witan_code-0.23.0 → witan_code-0.24.0}/docs/STAGE2_STITCHING.md +0 -0
  27. {witan_code-0.23.0 → witan_code-0.24.0}/docs/USER_GUIDE.md +0 -0
  28. {witan_code-0.23.0 → witan_code-0.24.0}/install.sh +0 -0
  29. {witan_code-0.23.0 → witan_code-0.24.0}/tests/__init__.py +0 -0
  30. {witan_code-0.23.0 → witan_code-0.24.0}/tests/conftest.py +0 -0
  31. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_batched_deletes.py +0 -0
  32. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_branches.py +0 -0
  33. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_bridge.py +0 -0
  34. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_chunked_load.py +0 -0
  35. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_cli_dispatch.py +0 -0
  36. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_cli_output_format.py +0 -0
  37. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_cli_selected_target.py +0 -0
  38. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_config.py +0 -0
  39. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_config_targets.py +0 -0
  40. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_crossrepo.py +0 -0
  41. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_edges.py +0 -0
  42. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_elicit.py +0 -0
  43. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_enrichment.py +0 -0
  44. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_github_app.py +0 -0
  45. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_graph.py +0 -0
  46. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_hooks.py +0 -0
  47. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_identity.py +0 -0
  48. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_index_failure_diagnostics.py +0 -0
  49. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_index_reports_its_store.py +0 -0
  50. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_index_requires_a_repo_key.py +0 -0
  51. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_indexer.py +0 -0
  52. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_indexer_scope.py +0 -0
  53. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_ingest.py +0 -0
  54. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_languages.py +0 -0
  55. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_package_map.py +0 -0
  56. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_reaper.py +0 -0
  57. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_rebuild_honours_the_repo_key.py +0 -0
  58. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_refusal_audit.py +0 -0
  59. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_remote_proxy.py +0 -0
  60. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_remote_store.py +0 -0
  61. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_repo.py +0 -0
  62. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_server.py +0 -0
  63. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_stitch.py +0 -0
  64. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_store.py +0 -0
  65. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_symbol_table.py +0 -0
  66. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_tasks.py +0 -0
  67. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_tool_schema.py +0 -0
  68. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_views.py +0 -0
  69. {witan_code-0.23.0 → witan_code-0.24.0}/tests/test_visualize.py +0 -0
  70. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/__init__.py +0 -0
  71. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/__main__.py +0 -0
  72. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/bridge.py +0 -0
  73. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/bridge_extractors.py +0 -0
  74. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/config.py +0 -0
  75. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/edges.py +0 -0
  76. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/elicit.py +0 -0
  77. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/github_app.py +0 -0
  78. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/graph.py +0 -0
  79. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/hooks.py +0 -0
  80. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/identity.py +0 -0
  81. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/indexer.py +0 -0
  82. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/ingest.py +0 -0
  83. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/output.py +0 -0
  84. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/package_map.py +0 -0
  85. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/queries/bridge.gq +0 -0
  86. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/queries/code_mutations.gq +0 -0
  87. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/queries/code_read.gq +0 -0
  88. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/queries/delete.gq +0 -0
  89. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/queries_ts/bash.scm +0 -0
  90. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/queries_ts/hcl.scm +0 -0
  91. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/queries_ts/python.scm +0 -0
  92. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/queries_ts/sql.scm +0 -0
  93. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/queries_ts/typescript.scm +0 -0
  94. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/queries_ts/yaml.scm +0 -0
  95. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/reaper.py +0 -0
  96. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/remote/__init__.py +0 -0
  97. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/remote/oidc.py +0 -0
  98. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/remote/proxy.py +0 -0
  99. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/remote/store.py +0 -0
  100. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/repo.py +0 -0
  101. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/schema/bridge-schema.pg +0 -0
  102. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/schema/code-schema.pg +0 -0
  103. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/selected_target.py +0 -0
  104. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/server.py +0 -0
  105. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/setup.py +0 -0
  106. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/stitch.py +0 -0
  107. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/store.py +0 -0
  108. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/views.py +0 -0
  109. {witan_code-0.23.0 → witan_code-0.24.0}/witan_code/visualize.py +0 -0
@@ -8,6 +8,32 @@ a MINOR bump may include breaking changes).
8
8
 
9
9
  <!-- scriv-insert-here -->
10
10
 
11
+ ## [0.24.0] - 2026-10-02
12
+
13
+ ### Added
14
+
15
+ - `witan-code setup --force` replaces a symlink or non-directory occupying an install destination (e.g. a stale symlink under `~/.pi/agent/extensions`). Without it, `witan-code setup` now exits 2 with the conflicting path instead of a `FileNotFoundError` traceback.
16
+
17
+ - `witan-code inject-context --client pi-builtin` renders the code-tool
18
+ discovery line for Pi's built-in MCP: a `codemode` script that finds the
19
+ `mcp__<server>__code_*` name with `searchTools()` and calls it as
20
+ `tools.<name>()`. The Pi extension passes it unless pi-mcp-adapter's `mcp`
21
+ tool is registered, in which case it still passes `--client pi`, whose
22
+ output is unchanged.
23
+
24
+ ### Changed
25
+
26
+ - The `witan-code` skill describes calling the `code_*` tools under Pi's
27
+ built-in MCP as well as under pi-mcp-adapter, and `witan-code setup --agent
28
+ pi` no longer warns that pi-mcp-adapter is missing when Pi's built-in MCP
29
+ will read the witan-code entry.
30
+ - Requires `agent-config-kit>=0.12`, which has that check; with an older one,
31
+ `witan-code setup --agent pi` still printed the pi-mcp-adapter warning.
32
+
33
+ ### Fixed
34
+
35
+ - `witan-code checkpoint`'s background `optimize` now also runs `cleanup --older-than 30d` at most once per `WITAN_CODE_CLEANUP_INTERVAL` (default weekly; `0` disables). On omnigraph 0.11 `optimize` no longer reclaims the storage of branches deleted by `branches --prune` or `reap-views --apply`, and nothing ran `cleanup` unless done by hand. `witan-code optimize` gains `--cleanup-older-than` to run both in one process.
36
+
11
37
  ## [0.23.0] - 2026-09-28
12
38
 
13
39
  ### Added
@@ -353,7 +379,6 @@ a MINOR bump may include breaking changes).
353
379
  write threw". Deliberately still non-fatal — the per-repo index succeeds and
354
380
  is worth keeping — but "non-fatal" and "unreported" are different claims.
355
381
 
356
-
357
382
  - **The test suite no longer asserts an environment.** Four tests
358
383
  (`test_branches.py`'s three branch-view assertions and
359
384
  `test_graph.py::test_a_shared_branch_view_needs_an_identity_to_own_it`)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: witan-code
3
- Version: 0.23.0
3
+ Version: 0.24.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.10
13
+ Requires-Dist: agent-config-kit<1,>=0.12
14
14
  Requires-Dist: cyclopts<6,>=4
15
15
  Requires-Dist: fastmcp<5,>=4
16
16
  Requires-Dist: joserfc<2,>=1.0
@@ -82,7 +82,7 @@ arbitration applies. See [`docs/BRANCH_INDEXING.md`](docs/BRANCH_INDEXING.md).
82
82
  The two layers compose through **soft symbol-ID references**. A Layer-1 node
83
83
  (e.g. a `lesson` or `agent_context`) can record symbol ids of the form:
84
84
 
85
- ```
85
+ ```text
86
86
  repo#relative/path.py::QualifiedName
87
87
  e.g. https://github.com/mitodl/ol-django#app/svc.py::Service.run
88
88
  ```
@@ -299,11 +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.
302
+ On Pi, the MCP server entry (`~/.pi/agent/mcp.json`) is read by Pi's
303
+ built-in MCP support (Pi 0.99 and later), or by the pi-mcp-adapter Pi
304
+ package when that is installed; confirm with `pi mcp list` or `/mcp` inside
305
+ Pi. `witan-code setup --agent pi` warns when Pi's settings show neither one
306
+ would load it (the built-in turned off with `-builtin:mcp`, or a Pi older
307
+ than 0.99, and no adapter declared).
307
308
 
308
309
  If `witan` is *also* installed and witan-code is importable in that same
309
310
  environment (e.g. via the `--with` in the `uv tool install`/MCP server's
@@ -354,6 +355,7 @@ symlink alternative:
354
355
  | `WITAN_AUTHOR` / `USER` | `unknown` | attribution string |
355
356
  | `WITAN_REPO` | — | override the detected repo slug |
356
357
  | `WITAN_CODE_OPTIMIZE_INTERVAL` | `86400` (daily) | throttle window (seconds) for `checkpoint`'s opportunistic store compaction; `0` disables it |
358
+ | `WITAN_CODE_CLEANUP_INTERVAL` | `604800` (weekly) | throttle window (seconds) for the `cleanup --older-than 30d` that rides a `checkpoint` compaction; `0` disables it |
357
359
  | `WITAN_CODE_INDEX_ROLE` | `client` | `ci` designates this process the writer of a shared graph's default-branch view. Only meaningful against a shared cluster graph; local stores are unaffected. See [Branch indexing](docs/BRANCH_INDEXING.md#who-may-write-the-shared-default-branch-view) |
358
360
  | `WITAN_ACTOR` | derived from the `witan login` session | the identity that owns the branch views this process writes; an `act-…` id or a raw OIDC `sub`. For a non-interactive writer (CI, a maintenance job). See [Branch indexing](docs/BRANCH_INDEXING.md#per-writer-branch-views) |
359
361
  | `WITAN_CONFIG` | `~/.config/witan/config.toml` | config file path (see below) |
@@ -640,12 +642,18 @@ mechanisms keep this in check, mirroring that module (deliberately duplicated
640
642
  - **Opportunistic**: the `witan-code checkpoint` Stop hook spawns a
641
643
  throttled, detached `witan-code optimize` for the current repo's store and
642
644
  the shared bridge store, at most once per `WITAN_CODE_OPTIMIZE_INTERVAL`
643
- (default daily; `0` disables) each — see [Hooks](#hooks).
644
- - **Scheduled**: `witan-code optimize [--store PATH | --bridge]` /
645
- `witan-code cleanup --yes` for cron/systemd-timer driven maintenance on a
646
- busy store. `optimize` is non-destructive and safe to run repeatedly;
647
- `cleanup` GCs old Lance versions to reclaim disk and is destructive, so it
648
- requires `--yes`.
645
+ (default daily; `0` disables) each — see [Hooks](#hooks). At most once per
646
+ `WITAN_CODE_CLEANUP_INTERVAL` (default weekly) the run also cleans up
647
+ versions older than 30 days. On omnigraph 0.11 that is the only thing that
648
+ reclaims the storage of branches deleted by `branches --prune` or
649
+ `reap-views --apply`; `optimize` no longer does.
650
+ - **Scheduled**: `witan-code optimize [--store PATH | --bridge]
651
+ [--cleanup-older-than DURATION]` / `witan-code cleanup --yes` for
652
+ cron/systemd-timer driven maintenance on a busy store. `optimize` alone is
653
+ non-destructive and safe to run repeatedly. `cleanup` GCs old Lance versions
654
+ to reclaim disk and is destructive, so it requires `--yes`.
655
+ `--cleanup-older-than` makes `optimize` run that destructive cleanup
656
+ afterwards with no `--yes` prompt, which is how the Stop hook calls it.
649
657
 
650
658
  ## Rebuilding an unreadable store
651
659
 
@@ -721,10 +729,13 @@ A Pi equivalent of all four lives in one extension,
721
729
  can tell "no cross-repo consumers" from "no cross-repo data"), and the
722
730
  `ToolSearch` call that makes the `code_*` tools callable when the harness
723
731
  delivers them deferred — followed by a `code_find_definition` →
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.
732
+ `code_callers`/`code_impact` call template. The Pi extension swaps the
733
+ `ToolSearch` step for Pi's calling convention: `--client pi-builtin`, for
734
+ Pi's built-in MCP, gives a `codemode` script (`searchTools(...)` for the
735
+ exact `mcp__<server>__` name, then `tools.<name>({...})`), and
736
+ `--client pi`, which it passes when pi-mcp-adapter's `mcp` tool is
737
+ registered, gives the adapter's proxy (`mcp({ search: ... })`, then
738
+ `mcp({ tool: ..., args: {...} })`). The default stays Claude's.
728
739
  Independent of `witan`'s own `inject-context` hook (no cross-package
729
740
  coupling) — register it alone for a witan-code-only install. Prints
730
741
  nothing when the repo has neither a store nor an index in flight.
@@ -49,7 +49,7 @@ arbitration applies. See [`docs/BRANCH_INDEXING.md`](docs/BRANCH_INDEXING.md).
49
49
  The two layers compose through **soft symbol-ID references**. A Layer-1 node
50
50
  (e.g. a `lesson` or `agent_context`) can record symbol ids of the form:
51
51
 
52
- ```
52
+ ```text
53
53
  repo#relative/path.py::QualifiedName
54
54
  e.g. https://github.com/mitodl/ol-django#app/svc.py::Service.run
55
55
  ```
@@ -266,11 +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.
269
+ On Pi, the MCP server entry (`~/.pi/agent/mcp.json`) is read by Pi's
270
+ built-in MCP support (Pi 0.99 and later), or by the pi-mcp-adapter Pi
271
+ package when that is installed; confirm with `pi mcp list` or `/mcp` inside
272
+ Pi. `witan-code setup --agent pi` warns when Pi's settings show neither one
273
+ would load it (the built-in turned off with `-builtin:mcp`, or a Pi older
274
+ than 0.99, and no adapter declared).
274
275
 
275
276
  If `witan` is *also* installed and witan-code is importable in that same
276
277
  environment (e.g. via the `--with` in the `uv tool install`/MCP server's
@@ -321,6 +322,7 @@ symlink alternative:
321
322
  | `WITAN_AUTHOR` / `USER` | `unknown` | attribution string |
322
323
  | `WITAN_REPO` | — | override the detected repo slug |
323
324
  | `WITAN_CODE_OPTIMIZE_INTERVAL` | `86400` (daily) | throttle window (seconds) for `checkpoint`'s opportunistic store compaction; `0` disables it |
325
+ | `WITAN_CODE_CLEANUP_INTERVAL` | `604800` (weekly) | throttle window (seconds) for the `cleanup --older-than 30d` that rides a `checkpoint` compaction; `0` disables it |
324
326
  | `WITAN_CODE_INDEX_ROLE` | `client` | `ci` designates this process the writer of a shared graph's default-branch view. Only meaningful against a shared cluster graph; local stores are unaffected. See [Branch indexing](docs/BRANCH_INDEXING.md#who-may-write-the-shared-default-branch-view) |
325
327
  | `WITAN_ACTOR` | derived from the `witan login` session | the identity that owns the branch views this process writes; an `act-…` id or a raw OIDC `sub`. For a non-interactive writer (CI, a maintenance job). See [Branch indexing](docs/BRANCH_INDEXING.md#per-writer-branch-views) |
326
328
  | `WITAN_CONFIG` | `~/.config/witan/config.toml` | config file path (see below) |
@@ -607,12 +609,18 @@ mechanisms keep this in check, mirroring that module (deliberately duplicated
607
609
  - **Opportunistic**: the `witan-code checkpoint` Stop hook spawns a
608
610
  throttled, detached `witan-code optimize` for the current repo's store and
609
611
  the shared bridge store, at most once per `WITAN_CODE_OPTIMIZE_INTERVAL`
610
- (default daily; `0` disables) each — see [Hooks](#hooks).
611
- - **Scheduled**: `witan-code optimize [--store PATH | --bridge]` /
612
- `witan-code cleanup --yes` for cron/systemd-timer driven maintenance on a
613
- busy store. `optimize` is non-destructive and safe to run repeatedly;
614
- `cleanup` GCs old Lance versions to reclaim disk and is destructive, so it
615
- requires `--yes`.
612
+ (default daily; `0` disables) each — see [Hooks](#hooks). At most once per
613
+ `WITAN_CODE_CLEANUP_INTERVAL` (default weekly) the run also cleans up
614
+ versions older than 30 days. On omnigraph 0.11 that is the only thing that
615
+ reclaims the storage of branches deleted by `branches --prune` or
616
+ `reap-views --apply`; `optimize` no longer does.
617
+ - **Scheduled**: `witan-code optimize [--store PATH | --bridge]
618
+ [--cleanup-older-than DURATION]` / `witan-code cleanup --yes` for
619
+ cron/systemd-timer driven maintenance on a busy store. `optimize` alone is
620
+ non-destructive and safe to run repeatedly. `cleanup` GCs old Lance versions
621
+ to reclaim disk and is destructive, so it requires `--yes`.
622
+ `--cleanup-older-than` makes `optimize` run that destructive cleanup
623
+ afterwards with no `--yes` prompt, which is how the Stop hook calls it.
616
624
 
617
625
  ## Rebuilding an unreadable store
618
626
 
@@ -688,10 +696,13 @@ A Pi equivalent of all four lives in one extension,
688
696
  can tell "no cross-repo consumers" from "no cross-repo data"), and the
689
697
  `ToolSearch` call that makes the `code_*` tools callable when the harness
690
698
  delivers them deferred — followed by a `code_find_definition` →
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.
699
+ `code_callers`/`code_impact` call template. The Pi extension swaps the
700
+ `ToolSearch` step for Pi's calling convention: `--client pi-builtin`, for
701
+ Pi's built-in MCP, gives a `codemode` script (`searchTools(...)` for the
702
+ exact `mcp__<server>__` name, then `tools.<name>({...})`), and
703
+ `--client pi`, which it passes when pi-mcp-adapter's `mcp` tool is
704
+ registered, gives the adapter's proxy (`mcp({ search: ... })`, then
705
+ `mcp({ tool: ..., args: {...} })`). The default stays Claude's.
695
706
  Independent of `witan`'s own `inject-context` hook (no cross-package
696
707
  coupling) — register it alone for a witan-code-only install. Prints
697
708
  nothing when the repo has neither a store nor an index in flight.
@@ -80,7 +80,7 @@ only makes it authoritative.
80
80
  One `PackageMap` node per repo in the bridge store, keyed on the repo URI and
81
81
  overwritten on every full-repo index (merge-by-slug):
82
82
 
83
- ```
83
+ ```text
84
84
  node PackageMap {
85
85
  slug: String @key // canonical repo URI
86
86
  repo: String @index
@@ -13,7 +13,7 @@ the `:CALLS/heuristic` fallback.
13
13
 
14
14
  ## Format
15
15
 
16
- ```
16
+ ```text
17
17
  {scheme}:{manager}:{package}:{version}:{descriptor}
18
18
  ```
19
19
 
@@ -35,7 +35,7 @@ never encoded (they are terminal).
35
35
 
36
36
  ### `endpoint` → scheme `http`
37
37
 
38
- ```
38
+ ```text
39
39
  {METHOD} {normalized-path}
40
40
  ```
41
41
 
@@ -49,7 +49,7 @@ never encoded (they are terminal).
49
49
  representation, not the resource, and no consumer/provider pair would agree
50
50
  on them syntactically.
51
51
 
52
- ```
52
+ ```text
53
53
  provider: http:pypi:mit-learn:main:GET /api/v0/users/me
54
54
  consumer: http:.:.:.:* /api/v0/users/me
55
55
  ```
@@ -58,7 +58,7 @@ consumer: http:.:.:.:* /api/v0/users/me
58
58
 
59
59
  Descriptor is the variable name, verbatim.
60
60
 
61
- ```
61
+ ```text
62
62
  provider: env:.:mit-learn:main:MITOL_APP_BASE_URL
63
63
  consumer: env:.:.:.:MITOL_APP_BASE_URL
64
64
  ```
@@ -69,7 +69,7 @@ The symbol identifies the package itself; the descriptor is `.`. Manager and
69
69
  package name come from the import site (consumer) or the publishing repo's
70
70
  package map / `package.json` (provider).
71
71
 
72
- ```
72
+ ```text
73
73
  provider: pkg:npm:@mitodl/course-search-utils:main:.
74
74
  consumer: pkg:npm:@mitodl/course-search-utils:.:.
75
75
  ```
@@ -78,7 +78,7 @@ consumer: pkg:npm:@mitodl/course-search-utils:.:.
78
78
 
79
79
  Descriptor is `{sub_kind}/{key_norm}` (sub_kind: `repo` | `image` | `name`).
80
80
 
81
- ```
81
+ ```text
82
82
  provider: svc:.:ol-infrastructure:main:repo/https://github.com/mitodl/mit-learn
83
83
  ```
84
84
 
@@ -93,7 +93,7 @@ bridge store predates `RepoSymbol` — the write is never blocked on it.
93
93
 
94
94
  ## Inspecting
95
95
 
96
- ```
96
+ ```text
97
97
  witan code symbols [--repo URI] [--role exported|external] [--scheme http]
98
98
  witan code stitch [--repo URI] [--unresolved]
99
99
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "witan-code"
3
- version = "0.23.0"
3
+ version = "0.24.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"
@@ -18,10 +18,19 @@ dependencies = [
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`,
21
+ # cannot load MCP servers, which comes from `InstallResult.prerequisites`,
22
22
  # added in 0.10.0. witan_core's `report_install` reads it with getattr, so
23
23
  # an older resolution does not fail — it just silently drops the warning.
24
- "agent-config-kit>=0.10,<1",
24
+ # >=0.11.1: `setup --force` passes `force` through to apply(), whose
25
+ # symlink guard (ConflictingPathError instead of FileNotFoundError) is
26
+ # new in 0.11.1. Older resolutions accept the flag but still crash on a
27
+ # dangling symlink. The workspace resolves by path, so no test here
28
+ # catches a stale floor.
29
+ # >=0.12: `witan-code setup --agent pi` only stops warning that
30
+ # pi-mcp-adapter is missing when Pi's built-in MCP will load the entry
31
+ # from 0.12.0. Older resolutions import and run, and print the false
32
+ # warning.
33
+ "agent-config-kit>=0.12,<1",
25
34
  # `remote`: the ADR-0005 client stack (OIDC device grant + MCP-client
26
35
  # proxy) behind `witan-code login` and the read commands' remote mode.
27
36
  # Costs nothing extra in practice — fastmcp is already required here and
@@ -157,7 +166,7 @@ dependencies = [
157
166
  # 0.38 its text never reaches the `mcp.tool_call` log line.
158
167
  #
159
168
  # ★ >=0.40 IS A BEHAVIOUR FLOOR too: `witan-code setup --agent pi` promises
160
- # a warning when Pi lacks pi-mcp-adapter, and
169
+ # a warning when Pi cannot load MCP servers, and
161
170
  # `witan_core.cli.report_install` only prints `InstallResult.prerequisites`
162
171
  # from 0.40.0. It reads the field with getattr, so 0.39 imports and runs —
163
172
  # and silently drops the warning.
@@ -244,7 +253,7 @@ testpaths = ["tests"]
244
253
  packages = ["witan_code"]
245
254
 
246
255
  [tool.bumpversion]
247
- current_version = "0.23.0"
256
+ current_version = "0.24.0"
248
257
  allow_dirty = true
249
258
 
250
259
  [[tool.bumpversion.files]]
@@ -5,6 +5,8 @@ exception escape (the hooks are documented as "always exits 0"), and must
5
5
  not print an extra trailing newline beyond what the backend already returns.
6
6
  """
7
7
 
8
+ import pytest
9
+
8
10
  from witan_code import cli
9
11
 
10
12
 
@@ -62,7 +64,8 @@ def test_inject_context_cmd_defaults_to_the_claude_client(monkeypatch, capsys):
62
64
  assert seen == ["claude"]
63
65
 
64
66
 
65
- def test_inject_context_cmd_passes_an_explicit_pi_client_through(monkeypatch):
67
+ @pytest.mark.parametrize("client", ["pi", "pi-builtin"])
68
+ def test_inject_context_cmd_passes_an_explicit_pi_client_through(monkeypatch, client):
66
69
  import witan_code.context as context_module
67
70
 
68
71
  seen = []
@@ -70,7 +73,7 @@ def test_inject_context_cmd_passes_an_explicit_pi_client_through(monkeypatch):
70
73
  context_module, "inject_context", lambda client: seen.append(client) or ""
71
74
  )
72
75
 
73
- command, bound, _ = cli.app.parse_args(["inject-context", "--client", "pi"])
76
+ command, bound, _ = cli.app.parse_args(["inject-context", "--client", client])
74
77
  command(*bound.args, **bound.kwargs)
75
78
 
76
- assert seen == ["pi"]
79
+ assert seen == [client]
@@ -417,10 +417,31 @@ def test_inject_context_default_and_explicit_claude_render_the_same(
417
417
  assert "`/witan-code`" in default
418
418
 
419
419
 
420
+ def test_inject_context_for_pi_builtin_names_a_codemode_script(tmp_path, monkeypatch):
421
+ """Pi's built-in MCP declares no MCP tool to the model by default: a
422
+ `codemode` script finds them with `searchTools()` and calls them as
423
+ `tools.<name>()`. The `mcp__<server>__` prefix depends on the user's
424
+ config, so the block must search first and then call the returned name.
425
+ """
426
+ _indexed(tmp_path, monkeypatch)
427
+
428
+ text = context.inject_context(client="pi-builtin")
429
+
430
+ assert "ToolSearch" not in text
431
+ assert "mcp({" not in text
432
+ assert "`codemode` script" in text
433
+ assert 'return await searchTools("code_find_definition callers impact")' in text
434
+ assert 'return await tools.<name the search returned>({ name: "X" })' in text
435
+ assert "/skill:witan-code" in text
436
+ # The status lines above the discovery line are client-neutral.
437
+ assert "https://github.com/test/cg" in text
438
+ assert "3 files" in text
439
+
440
+
420
441
  def test_inject_context_for_pi_names_the_mcp_proxy_not_toolsearch(
421
442
  tmp_path, monkeypatch
422
443
  ):
423
- """Pi has no ToolSearch; its MCP tools sit behind pi-mcp-adapter's `mcp`.
444
+ """With pi-mcp-adapter (`--client pi`), MCP tools sit behind its `mcp`.
424
445
 
425
446
  The adapter prefixes each tool with its server name, and that prefix
426
447
  depends on the user's config, so the block must search first and then
@@ -70,16 +70,70 @@ def test_spawn_background_optimize_throttles(monkeypatch, tmp_path):
70
70
  monkeypatch.setattr(maintenance.subprocess, "Popen", _FakePopen)
71
71
 
72
72
  # first call spawns and stamps; second (within window) is throttled
73
- assert maintenance.spawn_background_optimize(store, now=50_000.0) is True
74
- assert maintenance.spawn_background_optimize(store, now=50_000.0 + 5) is False
73
+ assert maintenance.spawn_background_optimize(store, now=1_000_000.0) is True
74
+ assert maintenance.spawn_background_optimize(store, now=1_000_000.0 + 5) is False
75
75
  assert len(calls) == 1
76
76
  argv, kwargs = calls[0]
77
- assert argv[1:] == ["-m", "witan_code", "optimize", "--store", str(store)]
77
+ # never cleaned up before, so the first run also cleans up
78
+ assert argv[1:] == [
79
+ "-m",
80
+ "witan_code",
81
+ "optimize",
82
+ "--store",
83
+ str(store),
84
+ "--cleanup-older-than",
85
+ maintenance.CLEANUP_OLDER_THAN,
86
+ ]
78
87
  assert kwargs["start_new_session"] is True
79
88
 
80
- # after the window elapses it spawns again
81
- assert maintenance.spawn_background_optimize(store, now=50_000.0 + 4000) is True
89
+ # after the optimize window elapses it spawns again, without the cleanup,
90
+ # whose weekly window has not
91
+ assert maintenance.spawn_background_optimize(store, now=1_000_000.0 + 4000) is True
82
92
  assert len(calls) == 2
93
+ assert "--cleanup-older-than" not in calls[1][0]
94
+
95
+
96
+ def test_cleanup_rides_optimize_on_its_own_window(monkeypatch, tmp_path):
97
+ """omnigraph 0.11 reclaims deleted branches' storage only in `cleanup`, so
98
+ the throttled optimize has to run one now and then or local stores grow
99
+ without bound."""
100
+ from witan_code import maintenance
101
+
102
+ monkeypatch.setenv("TMPDIR", str(tmp_path))
103
+ monkeypatch.setenv("WITAN_CODE_OPTIMIZE_INTERVAL", "3600")
104
+ monkeypatch.setenv("WITAN_CODE_CLEANUP_INTERVAL", "86400")
105
+ store = tmp_path / "g.omni"
106
+ store.mkdir()
107
+
108
+ calls = []
109
+ monkeypatch.setattr(
110
+ maintenance.subprocess, "Popen", lambda argv, **kw: calls.append(argv)
111
+ )
112
+
113
+ for hour in range(26):
114
+ maintenance.spawn_background_optimize(store, now=100_000.0 + hour * 3600)
115
+ cleaned = ["--cleanup-older-than" in argv for argv in calls]
116
+ assert len(calls) == 26
117
+ assert cleaned.count(True) == 2
118
+ assert cleaned[0] and cleaned[24]
119
+
120
+
121
+ def test_cleanup_disabled_leaves_optimize_alone(monkeypatch, tmp_path):
122
+ from witan_code import maintenance
123
+
124
+ monkeypatch.setenv("TMPDIR", str(tmp_path))
125
+ monkeypatch.setenv("WITAN_CODE_OPTIMIZE_INTERVAL", "3600")
126
+ monkeypatch.setenv("WITAN_CODE_CLEANUP_INTERVAL", "0")
127
+ store = tmp_path / "g.omni"
128
+ store.mkdir()
129
+
130
+ calls = []
131
+ monkeypatch.setattr(
132
+ maintenance.subprocess, "Popen", lambda argv, **kw: calls.append(argv)
133
+ )
134
+
135
+ assert maintenance.spawn_background_optimize(store, now=50_000.0) is True
136
+ assert "--cleanup-older-than" not in calls[0]
83
137
 
84
138
 
85
139
  def test_spawn_marks_before_spawn_so_failure_does_not_hotloop(monkeypatch, tmp_path):
@@ -177,6 +231,16 @@ def test_cli_optimize_and_cleanup(tmp_path, capsys):
177
231
  assert "Cleaned up" in capsys.readouterr().out
178
232
 
179
233
 
234
+ @requires_omnigraph
235
+ def test_cli_optimize_then_cleanup(tmp_path, capsys):
236
+ from witan_code import cli as cli_module
237
+
238
+ store = _fresh_store(tmp_path)
239
+
240
+ cli_module.optimize(store=str(store), cleanup_older_than="30d")
241
+ assert "Optimized and cleaned up" in capsys.readouterr().out
242
+
243
+
180
244
  def test_cli_optimize_missing_store_is_noop(tmp_path, capsys):
181
245
  from witan_code import cli as cli_module
182
246
 
@@ -13,6 +13,8 @@ import json
13
13
  import re
14
14
  from pathlib import Path
15
15
 
16
+ import pytest
17
+
16
18
  from agent_config_kit import apply
17
19
  from witan_code import setup
18
20
 
@@ -151,10 +153,18 @@ def _pi_setup_output(tmp_path, monkeypatch, capsys, *, dry_run: bool) -> str:
151
153
  return " ".join(capsys.readouterr().out.split())
152
154
 
153
155
 
154
- def test_setup_pi_warns_when_pi_mcp_adapter_is_missing(tmp_path, monkeypatch, capsys):
156
+ def _pi_settings(tmp_path, cfg: dict) -> None:
157
+ settings = tmp_path / ".pi" / "agent" / "settings.json"
158
+ settings.parent.mkdir(parents=True, exist_ok=True)
159
+ settings.write_text(json.dumps(cfg))
160
+
161
+
162
+ def test_setup_pi_warns_when_pi_cannot_load_mcp(tmp_path, monkeypatch, capsys):
155
163
  """`witan-code setup --agent pi` inherits agent-config-kit's MCP
156
- prerequisite preflight: Pi ignores ~/.pi/agent/mcp.json without the
157
- pi-mcp-adapter package, so the report must say so on dry-run and apply."""
164
+ prerequisite preflight: Pi ignores ~/.pi/agent/mcp.json when its
165
+ built-in MCP is turned off and pi-mcp-adapter is not installed, so the
166
+ report must say so on dry-run and apply."""
167
+ _pi_settings(tmp_path, {"extensions": ["-builtin:mcp"]})
158
168
  for dry_run in (True, False):
159
169
  out = _pi_setup_output(tmp_path, monkeypatch, capsys, dry_run=dry_run)
160
170
  assert "WARNING:" in out
@@ -162,9 +172,10 @@ def test_setup_pi_warns_when_pi_mcp_adapter_is_missing(tmp_path, monkeypatch, ca
162
172
 
163
173
 
164
174
  def test_setup_pi_notes_when_pi_mcp_adapter_is_installed(tmp_path, monkeypatch, capsys):
165
- settings = tmp_path / ".pi" / "agent" / "settings.json"
166
- settings.parent.mkdir(parents=True)
167
- settings.write_text(json.dumps({"packages": ["npm:pi-mcp-adapter"]}))
175
+ _pi_settings(
176
+ tmp_path,
177
+ {"packages": ["npm:pi-mcp-adapter"], "extensions": ["-builtin:mcp"]},
178
+ )
168
179
 
169
180
  out = _pi_setup_output(tmp_path, monkeypatch, capsys, dry_run=True)
170
181
 
@@ -173,6 +184,53 @@ def test_setup_pi_notes_when_pi_mcp_adapter_is_installed(tmp_path, monkeypatch,
173
184
  assert "pi-mcp-adapter is declared in" in out
174
185
 
175
186
 
187
+ def test_setup_pi_notes_when_pi_builtin_mcp_is_on(tmp_path, monkeypatch, capsys):
188
+ out = _pi_setup_output(tmp_path, monkeypatch, capsys, dry_run=True)
189
+
190
+ assert "WARNING:" not in out
191
+ assert "note:" in out
192
+ assert "built-in MCP is not turned off" in out
193
+
194
+
195
+ def _dangling_pi_extension(tmp_path) -> Path:
196
+ dest = tmp_path / ".pi" / "agent" / "extensions" / "codegraph.ts"
197
+ dest.parent.mkdir(parents=True)
198
+ dest.symlink_to(tmp_path / "gone" / "codegraph.ts")
199
+ return dest
200
+
201
+
202
+ def test_setup_exits_with_force_hint_on_a_symlinked_pi_extension(
203
+ tmp_path, monkeypatch, capsys
204
+ ):
205
+ import witan_core
206
+ from witan_code import cli
207
+
208
+ monkeypatch.setattr(witan_core, "install_omnigraph", lambda dry_run, **kw: None)
209
+ monkeypatch.setattr(Path, "home", lambda: tmp_path)
210
+ dest = _dangling_pi_extension(tmp_path)
211
+
212
+ with pytest.raises(SystemExit) as exc:
213
+ cli.setup(agent="pi", author="tester")
214
+
215
+ assert exc.value.code == 2
216
+ assert "--force" in capsys.readouterr().out
217
+ assert dest.is_symlink()
218
+
219
+
220
+ def test_setup_force_replaces_a_symlinked_pi_extension(tmp_path, monkeypatch):
221
+ import witan_core
222
+ from witan_code import cli
223
+
224
+ monkeypatch.setattr(witan_core, "install_omnigraph", lambda dry_run, **kw: None)
225
+ monkeypatch.setattr(Path, "home", lambda: tmp_path)
226
+ dest = _dangling_pi_extension(tmp_path)
227
+
228
+ cli.setup(agent="pi", author="tester", force=True)
229
+
230
+ assert dest.is_file()
231
+ assert not dest.is_symlink()
232
+
233
+
176
234
  # --- Pi extension source contract ------------------------------------------
177
235
  #
178
236
  # The Pi extension is TypeScript that no test here can execute, so these read
@@ -222,7 +280,16 @@ def test_configs_pi_mirror_matches_the_package_extension():
222
280
 
223
281
 
224
282
  def test_pi_extension_asks_for_pi_rendered_context():
225
- """Without `--client pi` the block tells Pi to call a ToolSearch it lacks."""
283
+ """Without a Pi `--client` the block tells Pi to call a ToolSearch it
284
+ lacks, and with the wrong one it names the other MCP support's calling
285
+ convention: the extension must pick `pi` only when pi-mcp-adapter's
286
+ `mcp` tool is registered."""
226
287
  source = _PKG_EXT.read_text()
227
288
 
228
- assert 'spawnSync("witan-code", ["inject-context", "--client", "pi"]' in source
289
+ assert re.search(
290
+ r'spawnSync\(\s*"witan-code",\s*\["inject-context", "--client", client\]',
291
+ source,
292
+ )
293
+ assert "const client = piClient(pi);" in source
294
+ assert 'pi.getAllTools().some((tool) => tool?.name === "mcp")' in source
295
+ assert re.search(r'\?\s*"pi"\s*:\s*"pi-builtin"', source)