# Proof audit pass 1: PR #23326 (GitCafe 23316), head b528fd6a84

Scope read: `packages/provider-cursor/src/server/skills.ts` and `skills.test.ts` (diff 8d3b061121..b528fd6a84; same as defb7e6e6b..HEAD for these files), `driver.ts:292-310`, `adapter.ts:2182`, `apps/server/src/provider/ProviderRegistry.ts:916-1010`, `apps/server/src/ws.ts:2214` (`server.refreshProviders`).
Note: 8d3b061121 is *not* an ancestor of HEAD. The branch sits on defb7e6e6b, and the `package.json` `tsc`→`tsc-rs` delta in the 8d3b..HEAD diff comes from main, not this PR.

## Accounting model derived from code

**Before (8d3b):** one counter, `remainingEntries = 10,000`. It is charged for every directory-listing entry, including `node_modules` (charged, then skipped) and ordinary files, and is charged *before* the stat. The visited directory's own `SKILL.md` stat is free. Exhaustion fires when an entry arrives while the counter is 0, so the 10,001st entry fails. Exactly 10,000 succeeds.

**After (b528):**
- `remainingStats = 100,000` is charged once per visited directory for the `SKILL.md` stat (after the realPath/visited dedupe, so revisits are free) and once per non-`node_modules` child entry, before its stat.
- `remainingEntries = 10,000` is charged only for a child that stat() reports as `Directory` (symlinks are followed), and only when `depth < 10`. The charge happens after the stat and before `visit`.
- The 100,001st stat or the 10,001st directory sets `exhausted`, which wins over `incomplete` in `failureReason`.
- The budget is shared across all 8 roots (4 project, then 4 under the *server process's* `HOME`). Missing roots cost nothing (realPath NotFound).

**Fixture arithmetic** (root `.cursor/skills` = {`a-large`/{SKILL.md, `artifacts`/N files}, `b-small`/{SKILL.md}}, empty HOME):
- Before, entry order is a-large(1), SKILL.md(2), artifacts(3), files(3..3+N), b-small(4+N), b-small/SKILL.md(5+N). The probe succeeds iff **N ≤ 9,995**. At N = 9,996 b-small is still discovered but the probe fails. At N ≥ 9,997 b-small is missing.
- After, the stat count is N + 9 and the directory count is 3. The probe succeeds iff **N ≤ 99,991**. At N = 99,992 the probe fails with b-small found. At N = 99,993 b-small's SKILL.md is not statted. At N ≥ 99,994 b-small is not visited.

## Matrix

| id | case | expected before (8d3b061121) | expected after (b528fd6a84) | existing coverage | gap |
|---|---|---|---|---|---|
| R1 | Two sibling skills, 11,000 ordinary files in `a-large/artifacts` (the bug) | Probe fails `scan-budget-exhausted` at entry 10,001. `discoverCursorSkills` returns only a-large. No workspace snapshot is published. | Probe succeeds with [a-large, b-small], about 11,009 stats and 3 directories | Test `finds sibling skills when one skill ships more files than the scan budget` (red at 23ce787387 per red-tests.txt; green 7/7 claimed at f88c). Runtime: before-runtime.json (Failure, 542 ms), after-runtime.json (Success, 2,084 ms) and transcript, **all at f88c14035e, not b528** | Runtime is one commit stale: b528 adds the SKILL.md stat charge, which adds +4 stats here and does not change the outcome. Green log has no SHA or filename. Re-run the focused test at b528, and optionally the RPC (R10). |
| R2 | Just under the old budget: 9,990 files (same layout) | Succeeds: 9,995 entries ≤ 10,000. Exact old edge is N = 9,995. | Succeeds | none | Capture: a cheap unit or fixture run at N = 9,990 (and ideally N = 9,995 / 9,996 / 9,997 on base to pin the edge) |
| R3 | Just over the old budget: 10,010 files | Fails `scan-budget-exhausted`, b-small missing (≥ 9,997) | Succeeds, both skills | Only implied by R1 (11,000) | Low. R1 subsumes it. Optional boundary capture together with R2. |
| R4 | More than 10,000 directories (for example 10,001 empty subdirs in `a-large/artifacts`) | Fails exhausted (entries) | Still fails `scan-budget-exhausted`. Directory charges are a-large(1) + subdirs, so subdir #10,000 hits 0. With b-small after it, success needs subdirs ≤ 9,998. About 20k stats, so the directory cap trips first. | none. No test asserts the directory cap after the change. | **Uncovered.** Needs a test or fixture showing the directory budget still bounds directory-heavy trees. Also note the behaviour change: directories at depth ≥ 10 and non-directories no longer spend this cap. |
| R5 | Stats cap: 100,500 files in one directory | Fails exhausted after about 10k stats (fast, roughly 0.5 s scale per base span) | Fails `scan-budget-exhausted` after exactly 100,000 stats (root SKILL.md + 99,999 files). All 100,500 names are still read and sorted first (`readDirectory` is unbounded). Stats are sequential. Extrapolating from 2,084 ms / 11k gives about **15–20 s per probe**, roughly 10× the old worst case. | Test `stops a scan of a huge file tree instead of statting every file` (mock FS, 200,000 names, asserts reason and stats == 100,000). No real-FS timing. | **Timing uncovered.** Capture a real-FS run with 100,500 files: outcome, stat count, wall time before vs after. This bounds but worsens the worst-case cost of every cwd refresh and every `$mention` turn (adapter). |
| R6a | Symlinked skill dir pointing outside root | Read SKILL.md, not walked. 1 entry charged. | Same. Costs 1 stat + 1 directory + 1 SKILL.md stat. | Test `treats a symlinked skill outside the root as a package boundary` (Success, `hidden` not listed) | Covered (unit). |
| R6b | Symlink loop: `a-large/loop -> ..` (directory cycle) | realPath already visited, so return. Costs 1 entry. | Same. stat → Directory, charges 1 stat + 1 directory, revisit returns before the SKILL.md charge. | none | Minor. Capture optional. |
| R6c | Self-loop symlink `x -> x` (ELOOP) inside a skill tree | stat ELOOP maps to `BadResource`, so `incomplete` and the probe fails `filesystem-error` (whole inventory dropped) | Same (unchanged) | none | Pre-existing harshness. Not a regression. Optional capture to document. |
| R6d | Dangling symlink | stat ENOENT maps to NotFound, silently skipped. Probe OK. | Same (still costs 1 stat) | none | Optional |
| R7 | Unreadable dir (chmod 000) inside a skill | SKILL.md stat and readDirectory return EACCES, so `incomplete` and the probe fails `filesystem-error`. discover still returns the other skills. | Same. `exhausted` would take precedence if both occur. | Indirectly: the first test's `.codex/skills` *file* gives ENOTDIR (BadResource), so probe Failure (reason not asserted) | **Uncovered** for EACCES. Must run as non-root (uid 1000 here is fine). |
| R8 | Nested skill inside a skill subdir. Depth limit 10. | Both discovered. Depth ≤ 10 visited, depth 11 ignored (still charged an entry). | Same discovery. Entries at depth ≥ 10 cost a stat but not a directory. | Tests `discovers recursive project skills…` (`nested/review`) and `skips a skill tree deeper than the limit…` (deep-skill at 10 found, `below` at 11 not, probe Success) | Covered (unit) |
| R9 | `node_modules` with many files or dirs beside skills | Not walked. Costs 1 entry. | Not walked. Costs 0. Name match only, at any depth. | none | Low risk (the delta is 1 unit). Optional test. The PR description should not claim node_modules was the fix. |
| R10 | Server path: `server.refreshProviders {instanceId:"cursor", cwd, fresh:true}` → `ProviderRegistry.refreshWorkspaceSnapshot` → `driver.snapshotForCwd` → `probeCursorSkills` | On error, `ProviderDriverError("Failed to discover Cursor skills for '<cwd>'")` wraps CursorSkillsProbeError. `recoverRefreshFailure` logs `provider registry refresh failed; preserving cached providers` and returns cached providers. No workspace snapshot for the cwd, so the composer shows no project skills. RPC still returns Success. | R1 fixture: workspace snapshot published with skills [a-large, b-small], project scope | Transcript and spans at **f88c**, real dev server, paired browser /ws. Server HOME was the real `/home/ubuntu`, so user-root contents shared the budget. | **Stale SHA.** Re-capture at b528, plus one failing case (R5 or R4) to show error surfacing on the after side |
| R11 | Real HOME user roots share the budget (`~/.agents/skills`, `~/.claude/skills`, …) | Project roots scanned first, then user roots | Same order. A heavy user tree can still exhaust the budget and fail the probe for every project. | none | Note for scope. The capture should set HOME to an isolated dir so results are deterministic. |
| R12 | `adapter.ts:2182` `discoverCursorSkills` on a `$mention` turn | Partial list silently used on exhaustion (b-small not rewritten to `/b-small`) | Full list for R1. Worst-case latency follows R5 (up to about 100k stats) on the first `$mention` turn. | none | Optional. Same function, so it is covered transitively by unit tests. |

## Correctness notes on the diff

1. **Off-by-one:** consistent. Both counters check `=== 0` before decrementing, so exactly-at-cap succeeds and cap+1 fails. The committed mock test pins stats == 100,000 (the root SKILL.md plus 99,999 children).
2. **Charge ordering:** stat is charged before the I/O (correct bound). The directory budget is charged after stat and after the depth check. That is intended, but it means directories at depth ≥ 10 are now free (they used to cost an entry), and a symlink to an outside directory spends the directory budget even though it is not walked. Both are harmless.
3. **The visited directory's SKILL.md stat is charged after the dedupe.** Good: symlink cycles do not double-charge.
4. **Bound weakened ~10× on the worst case** (R5): 10k → 100k sequential stats, plus an unbounded `readDirectory` and sort of an arbitrarily large listing. This runs on every cwd workspace refresh and the first `$mention` turn. No hang, but it is a performance regression on pathological trees that the evidence does not measure.
5. Test nit: `expect(stats).toBeGreaterThanOrEqual(100_000); expect(stats).toBeLessThanOrEqual(100_000)` is just `toBe(100_000)`.
6. The R1 test asserts `discover` and `probe` deep-equal. Good. The first test still asserts only `_tag === "Failure"` and not the reason, so ENOTDIR vs exhausted is not pinned.

## Uncovered rows that must be captured

- **R1/R10 at head b528:** focused test run with SHA and file in the log, and the RPC run re-done at b528 (current runtime proof is f88c).
- **R4:** directory cap still bounds >10,000 directories after the change (`scan-budget-exhausted`).
- **R5 timing:** real-FS 100,500-file run with outcome, stats bound and wall time, before vs after (expect roughly 10× slower failure).
- **R7:** chmod-000 dir gives `filesystem-error` (non-root).
- **R10 failure surfacing at b528:** one failing fixture (R5 or R4) through the RPC, showing the logged ProviderDriverError, the cached providers returned, and no workspace snapshot.
- R2 (and the optional R3 edge) is cheap, and nice to pin with exact boundary N = 9,995/9,996/9,997 on base. R6b/R6c/R6d/R9 are optional.

## R10: smallest real-server capture recommendation

Use the real WS RPC `server.refreshProviders` (`WS_METHODS.serverRefreshProviders`, `apps/server/src/ws.ts:2214`) with `{instanceId:"cursor", cwd:<fixture>, fresh:true}`. It is the only entry point that exercises `ProviderRegistry.refreshWorkspaceSnapshot` → `snapshotForCwd` → `probeCursorSkills` plus `recoverRefreshFailure`. No CLI subcommand calls it.

1. In a shell where `T3CODE_*` is unset, use the worktree at b528 and keep everything under `/tmp/ev-23326/run-b528/`: `home/` (T3 home), `fakehome/` (process HOME, with empty `.cursor/.agents/.codex/.claude` skill roots, so the user roots are deterministic and the real `~/.claude/skills` does not share the budget), and fixtures `fx-r1/` (R1) and `fx-r5/` (100,500 files) or `fx-r4/`.
2. Start the server only: `HOME=/tmp/ev-23326/run-b528/fakehome vp run dev:server -- --home-dir /tmp/ev-23326/run-b528/home` (or `node apps/server/src/bin.ts --home-dir …`). Capture its PID at spawn and read the port from the `[dev-runner]` line. Never touch `~/.t3`.
3. Mint a token: `HOME=… node apps/server/src/bin.ts pair --home-dir /tmp/ev-23326/run-b528/home`. Exchange it from a ~30-line Node WS client script kept outside the fixture dirs, or from the test-t3-app browser panel if allowed. Call `server.refreshProviders` for fx-r1 and then fx-r5, and record the returned provider's `workspaceSnapshots[cwd].skills`, or its absence.
4. Pull the `probeCursorSkills` spans (exit and durationMs) and the `provider registry refresh failed` log from that home's trace and log files (`node apps/server/src/bin.ts trace --home-dir …`). Then stop the server by its captured PID.

Expected: fx-r1 Success with [a-large, b-small]. fx-r5 RPC Success, but no workspace snapshot, an error log with a ProviderDriverError wrapping `CursorSkillsProbeError (scan-budget-exhausted)`, and a span durationMs that records the 100k-stat cost.

If a server launch is out of budget, the next-smallest option is a `vp test`-style script that builds the real Cursor driver instance and calls `snapshotForCwd(cwd)`. That skips the registry's error swallowing, so it is not sufficient for the error-surfacing half.
