refs-mcp 0.2.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 (147) hide show
  1. refs_mcp-0.2.0/.gitignore +65 -0
  2. refs_mcp-0.2.0/PKG-INFO +524 -0
  3. refs_mcp-0.2.0/README.md +507 -0
  4. refs_mcp-0.2.0/pyproject.toml +194 -0
  5. refs_mcp-0.2.0/refs_mcp/__init__.py +16 -0
  6. refs_mcp-0.2.0/refs_mcp/__main__.py +23 -0
  7. refs_mcp-0.2.0/refs_mcp/_arch.py +92 -0
  8. refs_mcp-0.2.0/refs_mcp/_build.py +135 -0
  9. refs_mcp-0.2.0/refs_mcp/_exec_safety.py +88 -0
  10. refs_mcp-0.2.0/refs_mcp/_link.py +195 -0
  11. refs_mcp-0.2.0/refs_mcp/api/__init__.py +39 -0
  12. refs_mcp-0.2.0/refs_mcp/api/_slug.py +96 -0
  13. refs_mcp-0.2.0/refs_mcp/api/inventory.py +402 -0
  14. refs_mcp-0.2.0/refs_mcp/api/observability.py +109 -0
  15. refs_mcp-0.2.0/refs_mcp/api/repo.py +579 -0
  16. refs_mcp-0.2.0/refs_mcp/auto_clone.py +247 -0
  17. refs_mcp-0.2.0/refs_mcp/bench/README.md +93 -0
  18. refs_mcp-0.2.0/refs_mcp/bench/__init__.py +8 -0
  19. refs_mcp-0.2.0/refs_mcp/bench/backends/__init__.py +1 -0
  20. refs_mcp-0.2.0/refs_mcp/bench/backends/baseline.py +255 -0
  21. refs_mcp-0.2.0/refs_mcp/bench/backends/refs_mcp_backend.py +131 -0
  22. refs_mcp-0.2.0/refs_mcp/bench/corpus.json +1663 -0
  23. refs_mcp-0.2.0/refs_mcp/bench/corpus.py +127 -0
  24. refs_mcp-0.2.0/refs_mcp/bench/measure_perf.py +64 -0
  25. refs_mcp-0.2.0/refs_mcp/bench/metrics.py +185 -0
  26. refs_mcp-0.2.0/refs_mcp/bench/models.py +166 -0
  27. refs_mcp-0.2.0/refs_mcp/bench/run_offline_ir.py +385 -0
  28. refs_mcp-0.2.0/refs_mcp/bench/tools.py +56 -0
  29. refs_mcp-0.2.0/refs_mcp/bootstrap.py +290 -0
  30. refs_mcp-0.2.0/refs_mcp/cli.py +517 -0
  31. refs_mcp-0.2.0/refs_mcp/config.py +95 -0
  32. refs_mcp-0.2.0/refs_mcp/contracts/__init__.py +56 -0
  33. refs_mcp-0.2.0/refs_mcp/contracts/drift.py +713 -0
  34. refs_mcp-0.2.0/refs_mcp/contracts/models.py +349 -0
  35. refs_mcp-0.2.0/refs_mcp/contracts/passthrough.py +279 -0
  36. refs_mcp-0.2.0/refs_mcp/contracts/runtime.py +255 -0
  37. refs_mcp-0.2.0/refs_mcp/contracts/scrape_gh.py +225 -0
  38. refs_mcp-0.2.0/refs_mcp/contracts/scrape_git.py +489 -0
  39. refs_mcp-0.2.0/refs_mcp/contracts/scrape_rg.py +319 -0
  40. refs_mcp-0.2.0/refs_mcp/correlation.py +63 -0
  41. refs_mcp-0.2.0/refs_mcp/discovery.py +437 -0
  42. refs_mcp-0.2.0/refs_mcp/doctor.py +426 -0
  43. refs_mcp-0.2.0/refs_mcp/events.py +164 -0
  44. refs_mcp-0.2.0/refs_mcp/file_log.py +167 -0
  45. refs_mcp-0.2.0/refs_mcp/gh_models.py +92 -0
  46. refs_mcp-0.2.0/refs_mcp/gh_runner.py +256 -0
  47. refs_mcp-0.2.0/refs_mcp/git_ops.py +326 -0
  48. refs_mcp-0.2.0/refs_mcp/git_runner.py +485 -0
  49. refs_mcp-0.2.0/refs_mcp/git_url.py +74 -0
  50. refs_mcp-0.2.0/refs_mcp/github_schema.py +65 -0
  51. refs_mcp-0.2.0/refs_mcp/help_doc.py +676 -0
  52. refs_mcp-0.2.0/refs_mcp/host_tools.py +1086 -0
  53. refs_mcp-0.2.0/refs_mcp/index_render.py +151 -0
  54. refs_mcp-0.2.0/refs_mcp/init/__init__.py +10 -0
  55. refs_mcp-0.2.0/refs_mcp/init/_common.py +251 -0
  56. refs_mcp-0.2.0/refs_mcp/init/_vscode_shared.py +166 -0
  57. refs_mcp-0.2.0/refs_mcp/init/cmd_claude.py +111 -0
  58. refs_mcp-0.2.0/refs_mcp/init/cmd_codex.py +201 -0
  59. refs_mcp-0.2.0/refs_mcp/init/cmd_copilot.py +117 -0
  60. refs_mcp-0.2.0/refs_mcp/init/cmd_copilot_agent.py +137 -0
  61. refs_mcp-0.2.0/refs_mcp/init/cmd_gemini.py +109 -0
  62. refs_mcp-0.2.0/refs_mcp/init/cmd_opencode.py +122 -0
  63. refs_mcp-0.2.0/refs_mcp/init/cmd_vscode.py +65 -0
  64. refs_mcp-0.2.0/refs_mcp/init/cmd_vscode_insiders.py +56 -0
  65. refs_mcp-0.2.0/refs_mcp/models.py +1119 -0
  66. refs_mcp-0.2.0/refs_mcp/operations.py +677 -0
  67. refs_mcp-0.2.0/refs_mcp/path_safety.py +183 -0
  68. refs_mcp-0.2.0/refs_mcp/preseed.py +191 -0
  69. refs_mcp-0.2.0/refs_mcp/ranking.py +141 -0
  70. refs_mcp-0.2.0/refs_mcp/remote_discover.py +511 -0
  71. refs_mcp-0.2.0/refs_mcp/reorg.py +503 -0
  72. refs_mcp-0.2.0/refs_mcp/replay.py +75 -0
  73. refs_mcp-0.2.0/refs_mcp/run_metadata.py +410 -0
  74. refs_mcp-0.2.0/refs_mcp/runner.py +312 -0
  75. refs_mcp-0.2.0/refs_mcp/search.py +1586 -0
  76. refs_mcp-0.2.0/refs_mcp/selftest.py +410 -0
  77. refs_mcp-0.2.0/refs_mcp/server.py +813 -0
  78. refs_mcp-0.2.0/refs_mcp/symbol_extraction.py +674 -0
  79. refs_mcp-0.2.0/refs_mcp/symbols.py +557 -0
  80. refs_mcp-0.2.0/refs_mcp/trace_export.py +110 -0
  81. refs_mcp-0.2.0/refs_mcp/user_config.py +329 -0
  82. refs_mcp-0.2.0/tests/__init__.py +0 -0
  83. refs_mcp-0.2.0/tests/_otel_isolation.py +144 -0
  84. refs_mcp-0.2.0/tests/cli/__init__.py +0 -0
  85. refs_mcp-0.2.0/tests/cli/__snapshots__/test_doctor.ambr +24 -0
  86. refs_mcp-0.2.0/tests/cli/__snapshots__/test_help.ambr +341 -0
  87. refs_mcp-0.2.0/tests/cli/__snapshots__/test_init_print.ambr +119 -0
  88. refs_mcp-0.2.0/tests/cli/conftest.py +56 -0
  89. refs_mcp-0.2.0/tests/cli/test_doctor.py +76 -0
  90. refs_mcp-0.2.0/tests/cli/test_help.py +127 -0
  91. refs_mcp-0.2.0/tests/cli/test_init_print.py +55 -0
  92. refs_mcp-0.2.0/tests/conftest.py +193 -0
  93. refs_mcp-0.2.0/tests/contracts/__init__.py +0 -0
  94. refs_mcp-0.2.0/tests/contracts/conftest.py +28 -0
  95. refs_mcp-0.2.0/tests/contracts/test_absolute_path_discipline.py +252 -0
  96. refs_mcp-0.2.0/tests/contracts/test_gh_shape.py +86 -0
  97. refs_mcp-0.2.0/tests/contracts/test_git_shape.py +115 -0
  98. refs_mcp-0.2.0/tests/contracts/test_rg_shape.py +188 -0
  99. refs_mcp-0.2.0/tests/divergence_registry.toml +239 -0
  100. refs_mcp-0.2.0/tests/mcp/__init__.py +0 -0
  101. refs_mcp-0.2.0/tests/mcp/conftest.py +102 -0
  102. refs_mcp-0.2.0/tests/mcp/test_initialize.py +71 -0
  103. refs_mcp-0.2.0/tests/mcp/test_prompts.py +39 -0
  104. refs_mcp-0.2.0/tests/mcp/test_resources.py +58 -0
  105. refs_mcp-0.2.0/tests/mcp/test_tools.py +79 -0
  106. refs_mcp-0.2.0/tests/test_action_coverage.py +481 -0
  107. refs_mcp-0.2.0/tests/test_auto_clone.py +36 -0
  108. refs_mcp-0.2.0/tests/test_bench.py +251 -0
  109. refs_mcp-0.2.0/tests/test_bootstrap.py +238 -0
  110. refs_mcp-0.2.0/tests/test_discovery.py +164 -0
  111. refs_mcp-0.2.0/tests/test_exec_safety.py +130 -0
  112. refs_mcp-0.2.0/tests/test_extra_tools.py +657 -0
  113. refs_mcp-0.2.0/tests/test_extractor_honesty.py +232 -0
  114. refs_mcp-0.2.0/tests/test_feature_gate.py +163 -0
  115. refs_mcp-0.2.0/tests/test_file_log.py +141 -0
  116. refs_mcp-0.2.0/tests/test_git_ops_native.py +204 -0
  117. refs_mcp-0.2.0/tests/test_git_url.py +64 -0
  118. refs_mcp-0.2.0/tests/test_github_schema.py +41 -0
  119. refs_mcp-0.2.0/tests/test_help.py +164 -0
  120. refs_mcp-0.2.0/tests/test_host_tools.py +262 -0
  121. refs_mcp-0.2.0/tests/test_init_common.py +300 -0
  122. refs_mcp-0.2.0/tests/test_link.py +185 -0
  123. refs_mcp-0.2.0/tests/test_npm_packaging.py +400 -0
  124. refs_mcp-0.2.0/tests/test_otel.py +198 -0
  125. refs_mcp-0.2.0/tests/test_path_assertion_gate.py +283 -0
  126. refs_mcp-0.2.0/tests/test_path_safety.py +127 -0
  127. refs_mcp-0.2.0/tests/test_platform_contracts.py +560 -0
  128. refs_mcp-0.2.0/tests/test_privilege_gate.py +362 -0
  129. refs_mcp-0.2.0/tests/test_ranking.py +95 -0
  130. refs_mcp-0.2.0/tests/test_regex_gate.py +307 -0
  131. refs_mcp-0.2.0/tests/test_reorg_native.py +86 -0
  132. refs_mcp-0.2.0/tests/test_resource_prompt_coverage.py +481 -0
  133. refs_mcp-0.2.0/tests/test_run_metadata_redactor.py +132 -0
  134. refs_mcp-0.2.0/tests/test_runner.py +41 -0
  135. refs_mcp-0.2.0/tests/test_runners.py +93 -0
  136. refs_mcp-0.2.0/tests/test_search.py +337 -0
  137. refs_mcp-0.2.0/tests/test_search_argv_shape.py +436 -0
  138. refs_mcp-0.2.0/tests/test_search_modes_wave6b.py +443 -0
  139. refs_mcp-0.2.0/tests/test_server.py +142 -0
  140. refs_mcp-0.2.0/tests/test_streaming.py +95 -0
  141. refs_mcp-0.2.0/tests/test_subprocess_gate.py +278 -0
  142. refs_mcp-0.2.0/tests/test_symbol_extraction_helpers.py +44 -0
  143. refs_mcp-0.2.0/tests/test_symbols.py +227 -0
  144. refs_mcp-0.2.0/tests/test_symbols_escape.py +64 -0
  145. refs_mcp-0.2.0/tests/test_tool_shapes.py +336 -0
  146. refs_mcp-0.2.0/tests/test_user_config.py +441 -0
  147. refs_mcp-0.2.0/uv.lock +2012 -0
@@ -0,0 +1,65 @@
1
+ # Whitelist-mode .gitignore for refs/
2
+ #
3
+ # Rule: everything at the top level is ignored except entries explicitly
4
+ # un-ignored with `!` below. GitHub owner directories (emerged from reorg.sh
5
+ # discovery) are NOT listed here — reorg.sh adds them dynamically to its
6
+ # in-memory allowlist at runtime. Everything else at top-level is treated
7
+ # as "work product" and relocated out of refs/ on --apply.
8
+ #
9
+ # To keep a new top-level file in refs/, add a `!/filename` line below.
10
+
11
+ /*
12
+
13
+ # ── Our tooling ─────────────────────────────────
14
+ !/reorg.sh
15
+ !/test-reorg.sh
16
+ !/status.sh
17
+ !/update.sh
18
+
19
+ # ── Agent rule doc (Codex/Cursor/Claude convention) ─────────────
20
+ !/AGENTS.md
21
+ !/README.md
22
+
23
+ # ── Generated indexes ───────────────────────────
24
+ !/index.md
25
+ !/index.html
26
+
27
+ # ── Python MCP server (FastMCP) ─────────────────
28
+ !/refs_mcp/
29
+ !/refs.spec
30
+ !/tests/
31
+ !/.github/
32
+ !/pyproject.toml
33
+ !/uv.lock
34
+ !/justfile
35
+
36
+ # ── Local git hooks (lefthook) ──────────────────
37
+ # lefthook.yml is the shared config; -local overrides are per-developer
38
+ # and stay out of git.
39
+ !/lefthook.yml
40
+ /lefthook-local.yml
41
+
42
+ # ── npm publish tree (GitHub Packages) ──────────
43
+ # Wrapper + three platform packages staged at publish-npm time.
44
+ # CI fills the platform bin/ subdirs from PyInstaller artifacts; only
45
+ # the package.json files and bin/refs.js launcher live in the repo.
46
+ !/npm/
47
+ # Re-ignore staged binaries that the publish-npm CI job materializes.
48
+ # Keeping them out of git means the working tree never carries built
49
+ # artifacts and a stray local `npm pack` doesn't ship a stale binary.
50
+ /npm/refs-*/bin/refs
51
+ /npm/refs-*/bin/refs.exe
52
+
53
+ # Re-ignore build/test artifacts inside the allowlisted dirs above.
54
+ # Top-level ``.venv/``, ``build/``, ``dist/`` are already caught by the
55
+ # ``/*`` rule — listed only inside allowlisted paths here.
56
+ **/__pycache__/
57
+ *.pyc
58
+ refs_mcp/bench/results/
59
+
60
+ # ── Standard repo furniture ─────────────────────
61
+ !/.gitignore
62
+ !/.git/
63
+ !/.gitattributes
64
+ !/.editorconfig
65
+ !/README.md
@@ -0,0 +1,524 @@
1
+ Metadata-Version: 2.4
2
+ Name: refs-mcp
3
+ Version: 0.2.0
4
+ Summary: FastMCP server for managing the refs/ reference-repo tree
5
+ License: MIT
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: click>=8.1
8
+ Requires-Dist: fastmcp>=3.3.1
9
+ Requires-Dist: githubkit>=0.13
10
+ Requires-Dist: opentelemetry-api>=1.20
11
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20
12
+ Requires-Dist: opentelemetry-sdk>=1.20
13
+ Requires-Dist: pydantic>=2.6
14
+ Requires-Dist: tree-sitter-language-pack>=1.8
15
+ Requires-Dist: tzdata>=2024.1
16
+ Description-Content-Type: text/markdown
17
+
18
+ # refs
19
+
20
+ You have a `~/dev/refs/` folder too, don't you?
21
+
22
+ The one where every time you want to read someone's codebase — study
23
+ `drizzle-orm`'s query builder, skim `next.js` internals, grep through
24
+ `playwright` — you run
25
+
26
+ ```bash
27
+ git clone https://github.com/foo/bar.git ~/dev/refs/bar
28
+ ```
29
+
30
+ and then months later you've got a hundred loose checkouts at the top
31
+ level, half of them stale, two of them accidentally the same repo under
32
+ different names, and no idea which ones are your work vs. someone else's
33
+ code. You keep meaning to organize it. You never do. Nobody wants to
34
+ write a housekeeping tool for their own scratch directory.
35
+
36
+ This is that tool.
37
+
38
+ ## What it does
39
+
40
+ Run `./reorg.sh --apply` and your pile becomes this:
41
+
42
+ ```
43
+ refs/
44
+ ├── facebook/
45
+ │ └── react/
46
+ ├── vuejs/
47
+ │ └── vue/
48
+ ├── microsoft/
49
+ │ ├── playwright/
50
+ │ └── ...
51
+ └── ... (one directory per GitHub owner)
52
+ ```
53
+
54
+ No matter where a clone was before — flat at top, nested in a
55
+ mis-named folder, duplicated under two names — it ends up at
56
+ `<owner>/<repo>/` based on its actual `git remote`. Duplicate clones of
57
+ the same upstream get parked aside for review instead of clobbering.
58
+ Random non-repo content (your notes, binaries, scratch dirs) gets evicted
59
+ out of the tree entirely. Empty pseudo-owner folders get pruned.
60
+
61
+ Every move is journaled. `./reorg.sh undo --apply` reverses the most
62
+ recent run. Nothing magical — plain `mv`, `rmdir`, `git`.
63
+
64
+ ## The daily loop
65
+
66
+ ```bash
67
+ ./reorg.sh --apply # you cloned some more stuff; put it where it belongs
68
+ ./status.sh # quick look at what's dirty and what has updates
69
+ ./update.sh # git pull every clean repo with remote changes
70
+ ```
71
+
72
+ ## About the git repo you're looking at
73
+
74
+ This directory is itself a git repo, but it doesn't track the hundreds of
75
+ cloned reference repos — those are checkouts you can always re-clone. It
76
+ only tracks the tooling (`reorg.sh`, `status.sh`, `update.sh`) plus the
77
+ two generated indexes (`index.md`, `index.html`). A whitelist-mode
78
+ [`.gitignore`](.gitignore) enforces this. More on that below.
79
+
80
+ ## Tools
81
+
82
+ ### `reorg.sh`
83
+
84
+ Reorganizes loose clones into the `<owner>/<repo>/` layout, keeps an index
85
+ (`index.md`, `index.html`) up to date, evicts non-repo content out of the
86
+ collection, and journals every move so you can reverse it.
87
+
88
+ ```bash
89
+ ./reorg.sh # dry-run preview
90
+ ./reorg.sh --apply # perform moves, write journal
91
+ ./reorg.sh undo --apply # reverse the most recent apply
92
+ ./reorg.sh --help # full option list
93
+ ```
94
+
95
+ Destinations (overridable):
96
+
97
+ | purpose | default path |
98
+ | --- | --- |
99
+ | duplicate-URL losers | `$(dirname $ROOT)/!CONFLICTS-RESOLVE/` |
100
+ | non-repo top-level content | `$(dirname $ROOT)/!WORK-PRODUCT-FROM-REFS/` |
101
+ | apply journals | `${XDG_STATE_HOME:-~/.local/state}/reorg/<basename-of-ROOT>/` |
102
+
103
+ ### `status.sh`
104
+
105
+ Reports dirty/clean + has-updates for every repo in the tree. Walks depth 1
106
+ and 2, so both loose-at-top and `<owner>/<repo>/` layouts are covered.
107
+
108
+ ### `update.sh`
109
+
110
+ `git pull` every clean repo that has remote changes. Skips dirty repos with
111
+ a note.
112
+
113
+ ### `test-reorg.sh`
114
+
115
+ Integration test harness for `reorg.sh`. Runs `shellcheck`, then 42 tests
116
+ over synthetic git fixtures (fake `.git/config` files — no network, no
117
+ real repos needed).
118
+
119
+ ```bash
120
+ ./test-reorg.sh # full suite
121
+ ./test-reorg.sh --no-shellcheck # skip the lint step
122
+ ```
123
+
124
+ ## MCP server (FastMCP)
125
+
126
+ `refs_mcp/` ships a [FastMCP][fastmcp] server that exposes the same
127
+ operations as MCP tools, with structured outputs typed by Pydantic. Useful
128
+ when an agent wants to inspect or operate on the tree through the
129
+ [Model Context Protocol][mcp] instead of shelling out.
130
+
131
+ ### Install + run
132
+
133
+ ```bash
134
+ uv sync --frozen # install deps from uv.lock
135
+ uv run --frozen refs stdio # serve over MCP stdio
136
+ uv run --frozen refs http # serve over streamable-HTTP (daemon)
137
+ uv run --frozen refs --help # full CLI surface
138
+ ```
139
+
140
+ The CLI is a [click][click] group with two transport subcommands —
141
+ `stdio` for Claude Desktop / IDE clients that spawn the binary, and
142
+ `http` for daemons under systemd / Docker / k8s. Anchored to the
143
+ canonical pattern in
144
+ [`../refs/modelcontextprotocol/servers/src/git/`][mcp-git-ref] and
145
+ [`../refs/github/github-mcp-server/`][gh-mcp-ref].
146
+
147
+ Or build a frozen binary and invoke it directly:
148
+
149
+ ```bash
150
+ uv run --frozen python -m refs_mcp._build # produces dist/refs.exe
151
+ ./dist/refs stdio # no Python needed at run time
152
+ ```
153
+
154
+ The build wrapper installs a `logging.Handler` on PyInstaller's logger
155
+ that `sys.exit`s on every record at WARNING or above — warnings genuinely
156
+ fail the build. There is no `--werror` flag in PyInstaller or uv
157
+ (verified via JiT reads of both upstreams).
158
+
159
+ The canonical dev commands use `uv run --frozen` so dependency
160
+ resolution is locked to `uv.lock` and uv never rewrites the lockfile as
161
+ a side effect:
162
+
163
+ ```bash
164
+ uv run --frozen pytest tests/ # 177 tests
165
+ uv run --frozen ruff format refs_mcp tests # format
166
+ uv run --frozen ruff check refs_mcp tests # lint
167
+ uv run --frozen pyright refs_mcp # static type check
168
+ uv run --frozen refs selftest # live MCP smoke (in-process)
169
+ uv run --frozen refs selftest --binary dist/refs.exe # vs the frozen binary
170
+ ```
171
+
172
+ CI is a three-tier pipeline in `.github/workflows/ci.yml`, fronted by
173
+ a tiny `changes` gate. Capability is preserved across tiers — macOS,
174
+ Python 3.11/3.13, PyInstaller build, MCP stdio smoke, artifact upload,
175
+ and release upload are all defined in the workflow. What varies per
176
+ event is which tier fires AND whether the cheap default tier fires at
177
+ all on PR/branch-push (a docs-only change skips it entirely — Windows
178
+ never starts).
179
+
180
+ | Event | `changes` | `test` | `test-full` | `build` | `release` |
181
+ |---|:---:|:---:|:---:|:---:|:---:|
182
+ | pull_request to `main` (code change) | ✓ | ✓ | — | — | — |
183
+ | pull_request to `main` (docs only) | ✓ | — | — | — | — |
184
+ | push to `main` (code change) | ✓ | ✓ | — | — | — |
185
+ | push to `main` (docs only) | ✓ | — | — | — | — |
186
+ | `workflow_dispatch` with `full-ci: true` | — | ✓ | ✓ | ✓ | — |
187
+ | push tag `v*` | — | ✓ | ✓ | ✓ | ✓ |
188
+
189
+ **`changes`** is a cheap ubuntu probe (`dorny/paths-filter@v4`) that
190
+ flags whether this push touched any runtime-relevant file:
191
+ `refs_mcp/**`, `tests/**`, `pyproject.toml`,
192
+ `uv.lock`, `refs.spec`, `.github/workflows/ci.yml`, or any `*.sh`.
193
+ If only docs/comments/README changed, every downstream tier skips and
194
+ the PR's required check passes without spinning up Windows. The gate
195
+ itself is bypassed on `workflow_dispatch` + tag push (the operator
196
+ explicitly asked for the full run).
197
+
198
+ **`test`** is the default cheap signal — `ubuntu-latest` and
199
+ `windows-latest` on Python 3.12 only. Linux runs ruff + pyright +
200
+ shellcheck + bash `test-reorg.sh` + pytest; Windows runs ruff +
201
+ pytest. No macOS, no extra Python versions.
202
+
203
+ **`test-full`** is extra source-pytest coverage: `{ubuntu, windows}`
204
+ × `{3.11, 3.13}` + `macos-latest/3.12`. Source pytest only, no extra
205
+ gates (pyright + shellcheck are version-independent and already
206
+ covered by the canonical `test` cell).
207
+
208
+ **`build`** produces per-OS PyInstaller binaries pinned to py3.13
209
+ (the shipped interpreter), runs a CLI smoke + the full 6-tool live
210
+ MCP stdio smoke against three shallow-cloned target repos
211
+ (`MicrosoftDocs/mcp` + `BurntSushi/ripgrep` +
212
+ `modelcontextprotocol/python-sdk`), and uploads each binary as an
213
+ artifact (retention 7 days for non-tag runs).
214
+
215
+ **`release`** downloads the three per-OS binaries and attaches them
216
+ to a GitHub release via [`softprops/action-gh-release@v3`][gh-release].
217
+
218
+ To run the full sweep manually (e.g. before cutting a release), use
219
+ the Actions tab and dispatch the `ci` workflow with `full-ci: true`.
220
+
221
+ Set `REFS_ROOT` to point at a tree other than the current directory.
222
+ `REFS_CONFLICTS_DIR`, `REFS_WORK_PRODUCT_DIR`, and `REFS_JOURNAL_DIR`
223
+ override the matching reorg.sh defaults.
224
+
225
+ Add to a Claude Code project's `.mcp.json`:
226
+
227
+ ```json
228
+ {
229
+ "mcpServers": {
230
+ "refs": {
231
+ "command": "uv",
232
+ "args": ["run", "refs", "stdio"],
233
+ "cwd": "<absolute path to your refs/ tree>"
234
+ }
235
+ }
236
+ }
237
+ ```
238
+
239
+ Or, when the frozen binary is on PATH:
240
+
241
+ ```json
242
+ {
243
+ "mcpServers": {
244
+ "refs": {
245
+ "command": "refs",
246
+ "args": ["stdio"],
247
+ "cwd": "<absolute path to your refs/ tree>"
248
+ }
249
+ }
250
+ }
251
+ ```
252
+
253
+ ### Tools
254
+
255
+ Evidence-graded search + symbol extraction (read-only by construction):
256
+
257
+ | Tool | Read-only | Implementation |
258
+ | -------------------------- | --------- | -------------- |
259
+ | `refs_find_repo` | yes | Bounded-scope repo lookup by `owner/repo`, exact name, or fuzzy substring. |
260
+ | `refs_search_evidence` | yes | `rg --json` content search with verdict labels (MATCH / VALIDATED_EMPTY / INVALID_EMPTY / TRUNCATED / FAILED / SKIPPED_UNSAFE). |
261
+ | `refs_prove_absence` | yes | Single-pass literal probe + positive control; strict VALIDATED_EMPTY or FAILED. |
262
+ | `refs_inspect_terms` | yes | Batch verdict map over a list of terms in one repo. |
263
+ | `refs_list_symbols` | yes | Language-agnostic symbol extraction via [`tree-sitter-language-pack`][tslp] (306 languages) with `DEFINED_AND_TESTED` / `DEFINED_ONLY` / `DEFINED_PRIVATE` verdicts. |
264
+ | `refs_find_symbol` | yes | Resolve one symbol's signature + tests + head SHA across any supported language. |
265
+ | `refs_host_tools` | yes | Probe-based inventory of host CLIs (rg, git, gh, …) + Python-module gates (tree-sitter) with feature-gate map. |
266
+ | `refs_discover_remote` | yes | `gh search repos` for cross-org discovery (needs `gh` on PATH). |
267
+ | `refs_discover_remote_org` | yes | `gh repo list <owner>` for enumerating an org's repos. |
268
+ | `refs_preseed_run` | no | Auto-clone the curated upstream-reference set (FastMCP, MCP SDK + spec, Pydantic, ripgrep, OTel semconv, pgr, MicrosoftDocs/mcp). |
269
+
270
+ ### Symbol extraction (language-agnostic)
271
+
272
+ `refs_list_symbols` and `refs_find_symbol` route per-file to an
273
+ extractor stack modeled on the [Sourcegraph + GitHub code-nav
274
+ architecture][scip]:
275
+
276
+ | Tier | Extractor | Status |
277
+ | ------------- | -------------------------------------------------- | ------ |
278
+ | **primary** | tree-sitter via [`tree-sitter-language-pack`][tslp] (306 languages) | shipping |
279
+ | **enrichment** | SCIP / LSP semantic indexes — override structural records with semantic-precise resolution (overloaded / imported / external symbols). | protocol locked; future work |
280
+ | **fallback** | Universal Ctags subprocess — for languages tree-sitter doesn't ship a grammar for | protocol locked; future work |
281
+
282
+ The `Symbol` IR carries `name`, `kind` (function / async_function /
283
+ method / class / struct / enum / trait / interface / type_alias / impl
284
+ / constant / variable), `language`, `signature`, `line`, `end_line`,
285
+ `visibility`, `parent`, `decorators`, `extractor` (which extractor
286
+ produced this record — useful for verdict explainability), plus the
287
+ `DEFINED_AND_TESTED` / `DEFINED_ONLY` / `DEFINED_PRIVATE` / `NOT_FOUND`
288
+ verdict.
289
+
290
+ Across the smoke targets, tree-sitter extracts:
291
+
292
+ | Repo | Language | Symbols |
293
+ | ----------------------------------- | ------------ | ------- |
294
+ | `modelcontextprotocol/python-sdk` | Python | 631 |
295
+ | `MicrosoftDocs/mcp` | TypeScript | 100 |
296
+ | `BurntSushi/ripgrep` | Rust | 3472 |
297
+
298
+ Tree management (every tool runs in native Python or git/gh subprocess — no shell wrappers):
299
+
300
+ | Tool | Read-only | Implementation |
301
+ | -------------------------- | --------- | -------------- |
302
+ | `refs_help` | yes | Returns the typed `HelpDocument` — read this first per session. |
303
+ | `refs_status` | yes | Filesystem walk + `configparser` on `.git/config`. Offline. |
304
+ | `refs_discover` | yes | Same walk; optionally writes `$REFS_JOURNAL_DIR/refs.inventory.json`. |
305
+ | `refs_reorg_preview` | yes | Pure planner — builds `ReorgPlan` from typed inventory, no IO. |
306
+ | `refs_reorg_apply` | no | `pathlib` + `shutil.move` + `csv.writer` for the TSV journal. |
307
+ | `refs_reorg_undo` | no | `csv.reader` reads the journal; moves reversed with `shutil`. |
308
+ | `refs_clone` | no | `gh repo clone` (github.com, auth-aware) or `git clone`; dry-run default; refuses non-GitHub and overwrites. |
309
+ | `refs_update_check` | yes | `git status --porcelain=v2 --branch` + `git ls-remote` for `has_updates`. |
310
+ | `refs_update_apply` | no | `git stash push` → `git pull --ff-only` → `git stash pop`; per-repo concurrency. |
311
+ | `refs_degit_export` | no | `git archive` into an export dir; source clone untouched. |
312
+ | `refs_sparse_materialize` | no | `git sparse-checkout set`; traversal patterns refused. |
313
+ | `refs_index_generate` | no | Pure-Python renderer of `index.md` + `index.html` from typed `Inventory`. |
314
+ | `refs_journal_latest` | yes | `csv.reader` over the latest TSV journal. |
315
+ | `refs_events_tail` | yes | Read the JSONL operation events log. |
316
+
317
+ ### Feature gates (host_tools probe)
318
+
319
+ Each tool that depends on a host CLI is gated by the bootstrap
320
+ `host_tools` probe — `shutil.which` is not enough; the probe **runs**
321
+ each tool's `--version` and only marks it OK on success. Features that
322
+ need an absent tool return a typed `FAILED` verdict with the install
323
+ hint, not a silent zero. See `refs_host_tools` for the live snapshot.
324
+
325
+ Per-feature gates land in `refs_mcp.host_tools._CURATED_FEATURES`:
326
+
327
+ | Feature | Required tool(s) | Used by |
328
+ | ------------------------ | --------------------------- | ------- |
329
+ | `content_search` | `rg` | `refs_search_evidence`, `refs_prove_absence`, `refs_inspect_terms` |
330
+ | `file_enum` | `rg` | Positive-control probes, corpus building |
331
+ | `structural_symbols` | Python `tree_sitter_language_pack` | `refs_list_symbols`, `refs_find_symbol` (gated on Python import, not a CLI) |
332
+ | `auto_clone` | `gh` (preferred) or `git` | `refs_mcp.auto_clone.ensure_repo` |
333
+ | `git_status` | `git` | `refs_update_check` sweep |
334
+ | `git_pull` / `git_stash` | `git` | `refs_update_apply` sweep |
335
+ | `git_archive_local` | `git` | `refs_degit_export` |
336
+ | `git_sparse_checkout` | `git` | `refs_sparse_materialize` |
337
+ | `remote_repo_search` | `gh` | `refs_discover_remote` |
338
+ | `remote_org_list` | `gh` | `refs_discover_remote_org` |
339
+ | `remote_archive_tarball` | `gh` | Future: archive without local clone (`gh api repos/{o}/{r}/tarball/{ref}`) |
340
+ | `remote_metadata` | `gh` | Default-branch / head SHA / disk size without clone |
341
+ | `pre_clone_size_inspection` | `gh` | `refs_discover_remote` populates `DiscoverableRepo.size_kb` |
342
+
343
+ ### `~/.refs/` layout (XDG-friendly)
344
+
345
+ Bootstrap writes per-run artifacts to a user-level layout (overridable
346
+ via env vars; defaults follow the XDG Base Directory spec on Linux).
347
+ Actual on-disk layout as implemented in `refs_mcp.user_config.UserPaths`:
348
+
349
+ ```
350
+ ~/.refs/
351
+ ├── .refs.toml # user preferences (auto_clone_allowed_hosts, etc.)
352
+ ├── repos/ # default refs_root (cloned upstreams)
353
+ │ └── <owner>/<repo>/
354
+ ├── state/
355
+ │ ├── events.jsonl # structured operation events
356
+ │ ├── server-runs.jsonl # one entry per server boot (run_id, transport, env)
357
+ │ ├── logs/ # date-rotated structured file logs (trace_id-correlated)
358
+ │ └── traces/ # OTel spans as JSONL per run (file exporter)
359
+ ├── data/
360
+ │ └── replay/ # captured request/response artifacts per run
361
+ └── cache/
362
+ └── symbol-index/ # symbol-index cache keyed by (repo_path, head_sha)
363
+ ```
364
+
365
+ Override individual subtrees with `XDG_STATE_HOME` / `XDG_DATA_HOME` /
366
+ `XDG_CACHE_HOME` (each gets `/refs` appended), or the more-specific
367
+ `REFS_STATE_DIR` / `REFS_DATA_DIR` / `REFS_CACHE_DIR`. Setting
368
+ `REFS_HOME` overrides the **whole tree** — XDG vars are ignored when
369
+ REFS_HOME is set so the layout stays cohesive (no surprise splits). The
370
+ config file (`.refs.toml`) lives directly under the home root; relocate
371
+ it via `REFS_HOME`.
372
+
373
+ The shell scripts (`reorg.sh`, `status.sh`, `update.sh`) remain in the
374
+ repo for shell-CLI users. The MCP server's reorg/status/update paths do
375
+ not call them — they share the on-disk contract (whitelist `.gitignore`,
376
+ TSV journal format, `<owner>/<repo>` layout) so a journal written by
377
+ either path is reversible by the other.
378
+
379
+ ### Resources
380
+
381
+ | URI | MIME | Content |
382
+ | ------------------------------ | -------------------------- | ------- |
383
+ | `refs://inventory` | `application/json` | Structured `Inventory` of the tree (typed `ResourceResult`). |
384
+ | `refs://inventory/schema` | `application/schema+json` | JSON Schema of the `Inventory` model. |
385
+ | `refs://repo/{owner}/{repo}` | `application/json` | One typed `RepoRecord` by owner+repo. |
386
+ | `refs://config` | `application/json` | Sanitized server config (no secrets). |
387
+ | `refs://journal/latest` | `application/json` | Latest reorg journal, parsed. |
388
+ | `refs://events/latest` | `application/x-ndjson` | Tail of the structured operation events log (NDJSON). |
389
+ | `refs://index.md` | `text/markdown` | `index.md` rendered from typed `Inventory`. |
390
+ | `refs://index.html` | `text/html` | `index.html` rendered from typed `Inventory`. |
391
+ | `refs://help` | `application/json` | Typed `HelpDocument` — same content as the `refs_help` tool. |
392
+ | `refs://help/markdown` | `text/markdown` | Help rendered as markdown for humans. |
393
+
394
+ ### Prompts
395
+
396
+ | Name | Purpose |
397
+ | ------------------------------- | ------- |
398
+ | `refs-agent-onboarding` | Read-this-first onboarding for any agent connecting to the server. |
399
+ | `audit-refs-tree` | Audit-before-mutate workflow. |
400
+ | `clone-reference-repo` | Safe clone workflow (refs_clone preview → apply → verify). |
401
+ | `refresh-reference-tree` | status → check → update sequence. |
402
+ | `prepare-agent-reference-pack` | Surface local upstream docs from the inventory for a given topic. |
403
+ | `export-plain-source-snapshot` | degit-style snapshot via `refs_degit_export`. |
404
+
405
+ ### Help surface (lessons baked into the server)
406
+
407
+ `refs_help` and the two `refs://help[/markdown]` resources expose a typed
408
+ guide for anyone — operator or agent — about to use this server. The
409
+ content is generic and transferable: which tools are offline vs
410
+ network-bound, why generated indexes are never parsed as data, what the
411
+ journal contract is for undo, why the OpenTelemetry tracer is a no-op
412
+ without an exporter, why owner/repo names with `.`, `..`, control chars,
413
+ or Windows reserved names are rejected, and which upstream docs are
414
+ already cloned under the refs root. Tip severities (`info` / `warning`
415
+ / `critical`) match how aggressively a client should surface them.
416
+
417
+ ### Generated indexes are not canonical
418
+
419
+ `index.md` and `index.html` are presentation artifacts produced by
420
+ `reorg.sh`. The MCP server's source of truth is the structured inventory
421
+ built from a filesystem walk + `.git/config` reads. No tool in this
422
+ package parses `index.md` or `index.html` as data.
423
+
424
+ ### Mid-2026 MCP / Claude features in scope
425
+
426
+ What the server uses today:
427
+
428
+ - **Structured tool outputs and output schemas** — every tool returns a
429
+ Pydantic model, so FastMCP emits both `content` (legacy text JSON) and
430
+ `structuredContent` with a generated `outputSchema` (MCP 2025-11-25 §
431
+ Tool Result).
432
+ - **Tool annotations** — `read_only_hint`, `destructive_hint`,
433
+ `idempotent_hint`, `open_world_hint` are set on every tool so the
434
+ client can render safe-vs-destructive UI.
435
+ - **Resources with MIME** — JSON resources are addressable and
436
+ discoverable via `resources/list`.
437
+ - **Prompts** — `audit-refs-tree` teaches an agent the right
438
+ read-before-mutate workflow.
439
+ - **Progress + log notifications** — `refs_reorg_apply` and
440
+ `refs_update_apply` report bracketed progress to the MCP client via
441
+ `ctx.report_progress` and emit structured status via `ctx.log`. The
442
+ asyncio streaming runner in `refs_mcp.runner` is the canonical
443
+ subprocess entrypoint and remains available for any future tool that
444
+ needs line-by-line subprocess forwarding.
445
+ - **OpenTelemetry** — FastMCP emits per-call SERVER spans automatically;
446
+ this package adds a CLIENT-kind span around every subprocess call
447
+ (git, gh, ripgrep) carrying `process.command`, `process.command_args`,
448
+ `process.working_directory`, `process.exit.code`, duration, and
449
+ ERROR status on non-zero exits. Bootstrap (`refs_mcp.bootstrap`) wires
450
+ the `TracerProvider` once per process with a file `SpanExporter`
451
+ writing to `~/.refs/traces/<run_id>.jsonl` so every run has captured
452
+ spans on disk by default. OTLP export is also wired:
453
+ - `OTEL_EXPORTER_OTLP_ENDPOINT=…` adds the `OTLPSpanExporter`
454
+ (proto-http) via `BatchSpanProcessor`. The exporter reads the
455
+ standard `OTEL_EXPORTER_OTLP_*` configuration variables. The
456
+ dependency ships as a hard runtime dep — no `[otlp]` extra dance.
457
+ - `OTEL_SERVICE_NAME=…` (default `refs`) attaches a meaningful
458
+ `service.name` Resource attribute. W3C trace context, semconv
459
+ `service.*` / `host.*` / `process.*` / `os.*` attributes are
460
+ populated by `refs_mcp.run_metadata`.
461
+
462
+ What is deliberately not used:
463
+
464
+ - **Sampling** — server-requests-LLM. This server doesn't generate text;
465
+ no natural use case.
466
+ - **Elicitation** — destructive ops are gated by `destructive_hint` and
467
+ by the default-dry-run contract; a separate "ask the human" round-trip
468
+ adds friction without safety.
469
+ - **Roots** — server is launched in a specific working directory (or via
470
+ `REFS_ROOT`); root negotiation isn't useful when the path is part of
471
+ the launch contract.
472
+ - **Background tasks (`execution.taskSupport`, SEP-1686)** — would
473
+ require a runner like Docket; advertised once an MVP integrates one.
474
+ - **Resource subscriptions / `subscriptions/listen`** — useful for
475
+ watching inventory changes but out of scope for the initial drop.
476
+
477
+ ### Tests
478
+
479
+ ```bash
480
+ uv run pytest tests/
481
+ ```
482
+
483
+ The pytest suite covers path safety, GitHub URL parsing, subprocess
484
+ runner (including timeout and missing-executable paths), discovery on a
485
+ synthetic tree, native reorg / git_ops / operations behavior over the
486
+ git/gh subprocess paths (status-sweep, update-sweep, clone, archive,
487
+ sparse-checkout), verdict-graded search + symbol extraction, host_tools
488
+ probe + feature-gate derivation, auto-clone allowlist + path safety,
489
+ MCP server smoke (tools/resources/prompts list, structured inventory
490
+ return, annotation presence), OpenTelemetry span emission, and the
491
+ benchmark harness.
492
+
493
+ The pre-existing `./test-reorg.sh` Bash suite remains authoritative for
494
+ filesystem moves, conflicts, eviction, and journal/undo semantics.
495
+
496
+ [fastmcp]: https://github.com/jlowin/fastmcp
497
+ [mcp]: https://modelcontextprotocol.io/specification
498
+ [click]: https://github.com/pallets/click
499
+ [mcp-git-ref]: https://github.com/modelcontextprotocol/servers/tree/main/src/git
500
+ [gh-mcp-ref]: https://github.com/github/github-mcp-server
501
+ [tslp]: https://github.com/kreuzberg-dev/tree-sitter-language-pack
502
+ [scip]: https://github.com/sourcegraph/scip
503
+ [gh-release]: https://github.com/softprops/action-gh-release
504
+
505
+ ## `.gitignore` is a whitelist
506
+
507
+ `refs/.gitignore` uses whitelist-mode:
508
+
509
+ ```
510
+ /*
511
+ !/reorg.sh
512
+ !/test-reorg.sh
513
+ ...
514
+ ```
515
+
516
+ Everything at the top level is ignored *except* the explicitly un-ignored
517
+ entries. GitHub owner directories (over a hundred of them, in practice)
518
+ are correctly excluded — git never sees them, so no embedded-repo
519
+ warnings, no accidental staging.
520
+
521
+ To keep a new top-level file, add a `!/filename` line. `reorg.sh`'s
522
+ top-level allowlist also reads this file: anything not on the static or
523
+ dynamic (GitHub owner) allowlist will be evicted to
524
+ `!WORK-PRODUCT-FROM-REFS/` on the next `--apply`.