witan-code 0.23.0__tar.gz → 0.25.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.25.0}/.gitignore +1 -1
  2. {witan_code-0.23.0 → witan_code-0.25.0}/CHANGELOG.md +52 -1
  3. {witan_code-0.23.0 → witan_code-0.25.0}/PKG-INFO +33 -20
  4. {witan_code-0.23.0 → witan_code-0.25.0}/README.md +30 -17
  5. {witan_code-0.23.0 → witan_code-0.25.0}/docs/BRANCH_INDEXING.md +2 -0
  6. {witan_code-0.23.0 → witan_code-0.25.0}/docs/PACKAGE_MAP.md +1 -1
  7. {witan_code-0.23.0 → witan_code-0.25.0}/docs/SYMBOL_FORMAT.md +6 -6
  8. {witan_code-0.23.0 → witan_code-0.25.0}/docs/SYMBOL_TABLE.md +1 -1
  9. {witan_code-0.23.0 → witan_code-0.25.0}/pyproject.toml +21 -6
  10. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_cli_hooks.py +6 -3
  11. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_context.py +22 -1
  12. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_maintenance.py +100 -5
  13. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_reaper.py +78 -0
  14. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_setup.py +75 -8
  15. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_store.py +15 -7
  16. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/cli.py +115 -37
  17. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/context.py +43 -11
  18. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/extensions/pi/codegraph.ts +32 -9
  19. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/maintenance.py +49 -6
  20. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/reaper.py +59 -5
  21. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/skills/witan-code/SKILL.md +15 -4
  22. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/store.py +1 -1
  23. {witan_code-0.23.0 → witan_code-0.25.0}/changelog.d/README.md +0 -0
  24. {witan_code-0.23.0 → witan_code-0.25.0}/config/claude.json +0 -0
  25. {witan_code-0.23.0 → witan_code-0.25.0}/config/copilot.json +0 -0
  26. {witan_code-0.23.0 → witan_code-0.25.0}/config/pi.json +0 -0
  27. {witan_code-0.23.0 → witan_code-0.25.0}/conftest.py +0 -0
  28. {witan_code-0.23.0 → witan_code-0.25.0}/docs/CLI_REFERENCE.md +0 -0
  29. {witan_code-0.23.0 → witan_code-0.25.0}/docs/EDGE_PRECISION_TIERS.md +0 -0
  30. {witan_code-0.23.0 → witan_code-0.25.0}/docs/STAGE2_STITCHING.md +0 -0
  31. {witan_code-0.23.0 → witan_code-0.25.0}/docs/USER_GUIDE.md +0 -0
  32. {witan_code-0.23.0 → witan_code-0.25.0}/install.sh +0 -0
  33. {witan_code-0.23.0 → witan_code-0.25.0}/tests/__init__.py +0 -0
  34. {witan_code-0.23.0 → witan_code-0.25.0}/tests/conftest.py +0 -0
  35. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_batched_deletes.py +0 -0
  36. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_branches.py +0 -0
  37. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_bridge.py +0 -0
  38. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_chunked_load.py +0 -0
  39. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_cli_dispatch.py +0 -0
  40. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_cli_output_format.py +0 -0
  41. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_cli_selected_target.py +0 -0
  42. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_config.py +0 -0
  43. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_config_targets.py +0 -0
  44. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_crossrepo.py +0 -0
  45. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_edges.py +0 -0
  46. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_elicit.py +0 -0
  47. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_enrichment.py +0 -0
  48. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_github_app.py +0 -0
  49. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_graph.py +0 -0
  50. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_hooks.py +0 -0
  51. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_identity.py +0 -0
  52. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_index_failure_diagnostics.py +0 -0
  53. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_index_reports_its_store.py +0 -0
  54. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_index_requires_a_repo_key.py +0 -0
  55. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_indexer.py +0 -0
  56. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_indexer_scope.py +0 -0
  57. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_ingest.py +0 -0
  58. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_languages.py +0 -0
  59. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_package_map.py +0 -0
  60. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_rebuild_honours_the_repo_key.py +0 -0
  61. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_refusal_audit.py +0 -0
  62. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_remote_proxy.py +0 -0
  63. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_remote_store.py +0 -0
  64. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_repo.py +0 -0
  65. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_server.py +0 -0
  66. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_stitch.py +0 -0
  67. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_symbol_table.py +0 -0
  68. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_tasks.py +0 -0
  69. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_tool_schema.py +0 -0
  70. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_views.py +0 -0
  71. {witan_code-0.23.0 → witan_code-0.25.0}/tests/test_visualize.py +0 -0
  72. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/__init__.py +0 -0
  73. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/__main__.py +0 -0
  74. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/bridge.py +0 -0
  75. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/bridge_extractors.py +0 -0
  76. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/config.py +0 -0
  77. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/edges.py +0 -0
  78. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/elicit.py +0 -0
  79. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/github_app.py +0 -0
  80. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/graph.py +0 -0
  81. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/hooks.py +0 -0
  82. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/identity.py +0 -0
  83. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/indexer.py +0 -0
  84. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/ingest.py +0 -0
  85. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/output.py +0 -0
  86. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/package_map.py +0 -0
  87. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/queries/bridge.gq +0 -0
  88. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/queries/code_mutations.gq +0 -0
  89. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/queries/code_read.gq +0 -0
  90. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/queries/delete.gq +0 -0
  91. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/queries_ts/bash.scm +0 -0
  92. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/queries_ts/hcl.scm +0 -0
  93. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/queries_ts/python.scm +0 -0
  94. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/queries_ts/sql.scm +0 -0
  95. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/queries_ts/typescript.scm +0 -0
  96. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/queries_ts/yaml.scm +0 -0
  97. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/remote/__init__.py +0 -0
  98. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/remote/oidc.py +0 -0
  99. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/remote/proxy.py +0 -0
  100. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/remote/store.py +0 -0
  101. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/repo.py +0 -0
  102. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/schema/bridge-schema.pg +0 -0
  103. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/schema/code-schema.pg +0 -0
  104. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/selected_target.py +0 -0
  105. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/server.py +0 -0
  106. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/setup.py +0 -0
  107. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/stitch.py +0 -0
  108. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/views.py +0 -0
  109. {witan_code-0.23.0 → witan_code-0.25.0}/witan_code/visualize.py +0 -0
@@ -26,7 +26,7 @@ build/
26
26
  *.egg-info/
27
27
  # omnigraph binary downloaded by hatch_build.py — not committed
28
28
  _bin/
29
- # omnigraph cluster state (populated by `omnigraph cluster import`)
29
+ # omnigraph cluster state (populated by `omnigraph cluster apply`)
30
30
  __cluster/
31
31
  # The witan UI bundle, written by `npm run build` in mcp/servers/witan/ui/.
32
32
  # A build output, so not committed — hatch's `artifacts` setting in that
@@ -8,6 +8,58 @@ a MINOR bump may include breaking changes).
8
8
 
9
9
  <!-- scriv-insert-here -->
10
10
 
11
+ ## [0.25.0] - 2026-10-15
12
+
13
+ ### Changed
14
+
15
+ - `witan-code cleanup` no longer defaults to `--keep 10`. From omnigraph 0.12
16
+ `--keep` counts graph commits per live branch, not versions per table, so
17
+ that default would cut a store to its last ten writes. With neither bound
18
+ it now keeps the last 30 days, as the Stop hook does. The help text
19
+ describes both bounds in graph commits. `--older-than` on its own
20
+ no longer adds an implicit `--keep 10`.
21
+ - The refusal for a shared cluster graph notes that from omnigraph 0.12
22
+ compaction is also refused while the server holds the cluster's `serve`
23
+ lock.
24
+
25
+ - Requires witan-core 0.41 or later, the first release on omnigraph 0.13.
26
+ Every deployed omnigraph server is on 0.13 and refuses a client that does
27
+ not send `Omnigraph-Http-Api: 0.13`.
28
+
29
+ ### Fixed
30
+
31
+ - `reap-views` ages a graph's views concurrently, `WITAN_CODE_VIEW_SURVEY_WORKERS` at a time (default 8; `1` is serial). Ageing a view is one `commit list --branch` that returns its whole reachable history, and serially production's 309-view bridge graph took 327s on its own, pushing the reaper CronJob past its 600s deadline; 8 workers take it to 50s. Each graph now also prints a `surveying views` line before its survey, so a job killed mid-sweep shows which graph it was on.
32
+
33
+ - A cluster graph that is not served is recognised on omnigraph 0.13, whose
34
+ 404 says `graph not found` without the quoted id. It was reported as an
35
+ unreachable cluster.
36
+
37
+ ## [0.24.0] - 2026-10-02
38
+
39
+ ### Added
40
+
41
+ - `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.
42
+
43
+ - `witan-code inject-context --client pi-builtin` renders the code-tool
44
+ discovery line for Pi's built-in MCP: a `codemode` script that finds the
45
+ `mcp__<server>__code_*` name with `searchTools()` and calls it as
46
+ `tools.<name>()`. The Pi extension passes it unless pi-mcp-adapter's `mcp`
47
+ tool is registered, in which case it still passes `--client pi`, whose
48
+ output is unchanged.
49
+
50
+ ### Changed
51
+
52
+ - The `witan-code` skill describes calling the `code_*` tools under Pi's
53
+ built-in MCP as well as under pi-mcp-adapter, and `witan-code setup --agent
54
+ pi` no longer warns that pi-mcp-adapter is missing when Pi's built-in MCP
55
+ will read the witan-code entry.
56
+ - Requires `agent-config-kit>=0.12`, which has that check; with an older one,
57
+ `witan-code setup --agent pi` still printed the pi-mcp-adapter warning.
58
+
59
+ ### Fixed
60
+
61
+ - `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.
62
+
11
63
  ## [0.23.0] - 2026-09-28
12
64
 
13
65
  ### Added
@@ -353,7 +405,6 @@ a MINOR bump may include breaking changes).
353
405
  write threw". Deliberately still non-fatal — the per-repo index succeeds and
354
406
  is worth keeping — but "non-fatal" and "unreported" are different claims.
355
407
 
356
-
357
408
  - **The test suite no longer asserts an environment.** Four tests
358
409
  (`test_branches.py`'s three branch-view assertions and
359
410
  `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.25.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
@@ -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.40
29
+ Requires-Dist: witan-core[cli,observability,remote,sentry]<1,>=0.41
30
30
  Provides-Extra: tasks
31
31
  Requires-Dist: fastmcp-tasks<5,>=4; extra == 'tasks'
32
32
  Description-Content-Type: text/markdown
@@ -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) |
@@ -582,7 +584,9 @@ graph):
582
584
  instead.
583
585
  - `cleanup [--store PATH] [--bridge] [--keep N] [--older-than DUR] --yes` —
584
586
  reclaim disk by GC'ing old Lance versions (**destructive**; requires
585
- `--yes`).
587
+ `--yes`). `--keep N` retains the newest N graph commits of every live
588
+ branch (omnigraph 0.12+; versions per table on 0.11); with neither bound it
589
+ keeps the last 30 days.
586
590
  - `checkpoint` — opportunistically compact the current repo's store and the
587
591
  bridge store if due (see [Hooks](#hooks)); registered as the `Stop` hook,
588
592
  not usually run by hand.
@@ -640,12 +644,18 @@ mechanisms keep this in check, mirroring that module (deliberately duplicated
640
644
  - **Opportunistic**: the `witan-code checkpoint` Stop hook spawns a
641
645
  throttled, detached `witan-code optimize` for the current repo's store and
642
646
  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`.
647
+ (default daily; `0` disables) each — see [Hooks](#hooks). At most once per
648
+ `WITAN_CODE_CLEANUP_INTERVAL` (default weekly) the run also cleans up
649
+ versions older than 30 days. On omnigraph 0.11 that is the only thing that
650
+ reclaims the storage of branches deleted by `branches --prune` or
651
+ `reap-views --apply`; `optimize` no longer does.
652
+ - **Scheduled**: `witan-code optimize [--store PATH | --bridge]
653
+ [--cleanup-older-than DURATION]` / `witan-code cleanup --yes` for
654
+ cron/systemd-timer driven maintenance on a busy store. `optimize` alone is
655
+ non-destructive and safe to run repeatedly. `cleanup` GCs old Lance versions
656
+ to reclaim disk and is destructive, so it requires `--yes`.
657
+ `--cleanup-older-than` makes `optimize` run that destructive cleanup
658
+ afterwards with no `--yes` prompt, which is how the Stop hook calls it.
649
659
 
650
660
  ## Rebuilding an unreadable store
651
661
 
@@ -721,10 +731,13 @@ A Pi equivalent of all four lives in one extension,
721
731
  can tell "no cross-repo consumers" from "no cross-repo data"), and the
722
732
  `ToolSearch` call that makes the `code_*` tools callable when the harness
723
733
  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.
734
+ `code_callers`/`code_impact` call template. The Pi extension swaps the
735
+ `ToolSearch` step for Pi's calling convention: `--client pi-builtin`, for
736
+ Pi's built-in MCP, gives a `codemode` script (`searchTools(...)` for the
737
+ exact `mcp__<server>__` name, then `tools.<name>({...})`), and
738
+ `--client pi`, which it passes when pi-mcp-adapter's `mcp` tool is
739
+ registered, gives the adapter's proxy (`mcp({ search: ... })`, then
740
+ `mcp({ tool: ..., args: {...} })`). The default stays Claude's.
728
741
  Independent of `witan`'s own `inject-context` hook (no cross-package
729
742
  coupling) — register it alone for a witan-code-only install. Prints
730
743
  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) |
@@ -549,7 +551,9 @@ graph):
549
551
  instead.
550
552
  - `cleanup [--store PATH] [--bridge] [--keep N] [--older-than DUR] --yes` —
551
553
  reclaim disk by GC'ing old Lance versions (**destructive**; requires
552
- `--yes`).
554
+ `--yes`). `--keep N` retains the newest N graph commits of every live
555
+ branch (omnigraph 0.12+; versions per table on 0.11); with neither bound it
556
+ keeps the last 30 days.
553
557
  - `checkpoint` — opportunistically compact the current repo's store and the
554
558
  bridge store if due (see [Hooks](#hooks)); registered as the `Stop` hook,
555
559
  not usually run by hand.
@@ -607,12 +611,18 @@ mechanisms keep this in check, mirroring that module (deliberately duplicated
607
611
  - **Opportunistic**: the `witan-code checkpoint` Stop hook spawns a
608
612
  throttled, detached `witan-code optimize` for the current repo's store and
609
613
  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`.
614
+ (default daily; `0` disables) each — see [Hooks](#hooks). At most once per
615
+ `WITAN_CODE_CLEANUP_INTERVAL` (default weekly) the run also cleans up
616
+ versions older than 30 days. On omnigraph 0.11 that is the only thing that
617
+ reclaims the storage of branches deleted by `branches --prune` or
618
+ `reap-views --apply`; `optimize` no longer does.
619
+ - **Scheduled**: `witan-code optimize [--store PATH | --bridge]
620
+ [--cleanup-older-than DURATION]` / `witan-code cleanup --yes` for
621
+ cron/systemd-timer driven maintenance on a busy store. `optimize` alone is
622
+ non-destructive and safe to run repeatedly. `cleanup` GCs old Lance versions
623
+ to reclaim disk and is destructive, so it requires `--yes`.
624
+ `--cleanup-older-than` makes `optimize` run that destructive cleanup
625
+ afterwards with no `--yes` prompt, which is how the Stop hook calls it.
616
626
 
617
627
  ## Rebuilding an unreadable store
618
628
 
@@ -688,10 +698,13 @@ A Pi equivalent of all four lives in one extension,
688
698
  can tell "no cross-repo consumers" from "no cross-repo data"), and the
689
699
  `ToolSearch` call that makes the `code_*` tools callable when the harness
690
700
  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.
701
+ `code_callers`/`code_impact` call template. The Pi extension swaps the
702
+ `ToolSearch` step for Pi's calling convention: `--client pi-builtin`, for
703
+ Pi's built-in MCP, gives a `codemode` script (`searchTools(...)` for the
704
+ exact `mcp__<server>__` name, then `tools.<name>({...})`), and
705
+ `--client pi`, which it passes when pi-mcp-adapter's `mcp` tool is
706
+ registered, gives the adapter's proxy (`mcp({ search: ... })`, then
707
+ `mcp({ tool: ..., args: {...} })`). The default stays Claude's.
695
708
  Independent of `witan`'s own `inject-context` hook (no cross-package
696
709
  coupling) — register it alone for a witan-code-only install. Prints
697
710
  nothing when the repo has neither a store nor an index in flight.
@@ -222,6 +222,8 @@ date, owner, or size. Two rules follow:
222
222
  ago — reaping it would race the indexer that just created it.
223
223
 
224
224
  The window is `WITAN_CODE_VIEW_MAX_IDLE_DAYS` (default 14; `0` disables).
225
+ Ageing a view is one `commit list` per view, so the sweep ages
226
+ `WITAN_CODE_VIEW_SURVEY_WORKERS` views at once (default 8; `1` is serial).
225
227
  Idleness is not abandonment: a branch parked past the window and picked back up
226
228
  loses its view, not its work, and the next index rebuilds it. Reporting is the
227
229
  default and `--apply` deletes, because the window is the one input nothing
@@ -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.25.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,11 +166,17 @@ 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.
164
- "witan-core[cli,remote,observability,sentry]>=0.40,<1",
173
+ #
174
+ # ★ >=0.41 IS A COMPATIBILITY FLOOR: 0.41.0 pins omnigraph 0.13 (storage
175
+ # format 14) and sends `Omnigraph-Http-Api: 0.13`. Every deployed server
176
+ # is on 0.13, which answers a request without that header with `400
177
+ # api_contract_mismatch`, so 0.40 imports and runs, then fails on the
178
+ # first read of a shared code graph.
179
+ "witan-core[cli,remote,observability,sentry]>=0.41,<1",
165
180
  "fastmcp>=4,<5",
166
181
  "cyclopts>=4,<6",
167
182
  # [targets.<name>] override models (config.py) — also transitively pulled
@@ -244,7 +259,7 @@ testpaths = ["tests"]
244
259
  packages = ["witan_code"]
245
260
 
246
261
  [tool.bumpversion]
247
- current_version = "0.23.0"
262
+ current_version = "0.25.0"
248
263
  allow_dirty = true
249
264
 
250
265
  [[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,47 @@ def test_cli_optimize_and_cleanup(tmp_path, capsys):
177
231
  assert "Cleaned up" in capsys.readouterr().out
178
232
 
179
233
 
234
+ def test_cli_cleanup_with_no_bound_keeps_the_hooks_window(
235
+ tmp_path, capsys, monkeypatch
236
+ ):
237
+ """From omnigraph 0.12 `--keep` counts graph commits, so a default of
238
+ `--keep 10` would cut a store to its last ten writes. No bound means the
239
+ age window the Stop hook already uses."""
240
+ from witan_code import cli as cli_module
241
+ from witan_code import maintenance
242
+
243
+ store = tmp_path / "s.omni"
244
+ store.mkdir()
245
+ calls = []
246
+
247
+ class _Client:
248
+ def cleanup(self, **kwargs):
249
+ calls.append(kwargs)
250
+
251
+ monkeypatch.setattr(cli_module, "_maintenance_client", lambda _ref: _Client())
252
+
253
+ cli_module.cleanup(store=str(store))
254
+ assert f"newer than {maintenance.CLEANUP_OLDER_THAN}" in capsys.readouterr().out
255
+ assert calls == []
256
+
257
+ cli_module.cleanup(store=str(store), yes=True)
258
+ cli_module.cleanup(store=str(store), keep=3, yes=True)
259
+ assert calls == [
260
+ {"keep": None, "older_than": maintenance.CLEANUP_OLDER_THAN},
261
+ {"keep": 3, "older_than": None},
262
+ ]
263
+
264
+
265
+ @requires_omnigraph
266
+ def test_cli_optimize_then_cleanup(tmp_path, capsys):
267
+ from witan_code import cli as cli_module
268
+
269
+ store = _fresh_store(tmp_path)
270
+
271
+ cli_module.optimize(store=str(store), cleanup_older_than="30d")
272
+ assert "Optimized and cleaned up" in capsys.readouterr().out
273
+
274
+
180
275
  def test_cli_optimize_missing_store_is_noop(tmp_path, capsys):
181
276
  from witan_code import cli as cli_module
182
277