Skip to content

test: start the runner's spec Neovims with tests/minimal_init.lua - #522

Merged
ThomasK33 merged 9 commits into
mainfrom
test/runner-plenary-options
Nov 12, 2026
Merged

ThomasK33 merged 9 commits into
mainfrom
test/runner-plenary-options

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

What a developer sees

The integration runner passes {minimal_init = 'tests/minimal_init.lua'} to plenary, but plenary's test_harness.test_file(path) takes no options and drops them. The Neovim that plenary starts for each spec has no -u, so:

  • it does not run tests/minimal_init.lua: no leader key, timeoutlen 1000 instead of 300, updatetime 4000 instead of 250,
  • it reads the developer's own init file, and runs the developer's ftplugin and after files, so a local run depends on ~/.config/nvim,
  • every Neovim of a run writes ShaDa, a log and swap files under ~/.local/state/nvim.

The only part of minimal_init.lua that reached the spec's Neovim was the stub claude on PATH, which it inherited from the runner's own Neovim.

What changes

  1. VIMINIT. scripts/run_integration_tests_individually.sh sets VIMINIT for the Neovim it starts. A Neovim without -u runs $VIMINIT instead of the user's init file, so the Neovim that plenary starts now runs tests/minimal_init.lua. VIMINIT reads the file from CLAUDECODE_MINIMAL_INIT, an absolute path ($PWD/tests/minimal_init.lua), so a path with a space or % needs no escaping and a nested Neovim started from another folder finds the file. A comment says that VIMINIT and the variable reach every process a spec starts. The runner's own Neovim still has -u, and the unused {minimal_init = ...} argument is gone.
  2. State and config folders. Each file gets one XDG_STATE_HOME and one XDG_CONFIG_HOME, in temp folders the runner removes afterwards. The Neovim the runner starts and the Neovim that plenary starts for the spec share them, so the runner's own Neovim takes -i NONE: two Neovims that write ShaDa into one new folder made the first fail with E886 after a timeout. A run no longer writes ~/.local/state/nvim, and the ftplugin and after files under ~/.config/nvim do not run. Files in the data folder (~/.local/share/nvim/site/...) still run, as on main. XDG_DATA_HOME stays, because plenary lives there.
  3. Cleanup on SIGTERM. An EXIT trap kills the process group of the file that runs, then removes its temp files and folders. Bash runs the trap when the runner gets SIGTERM too, just before the runner dies with 143 as it did before. On main, a runner that got SIGTERM left its temp files and left the file's Neovim running until its timeout. Killing the group first matters: a Neovim that ends later writes ShaDa and makes the removed state folder again. Two known limits: a SIGTERM that arrives before timeout has created the file's process group misses the kill (the file's Neovim then runs until its timeout, as on main), and on hosts whose timeout is uutils (this repo's CI image included) the SIGTERM test cannot tell a group kill from a kill of the timeout process alone. A follow-up PR fixes the test and the README wording.
  4. tests/minimal_init.lua loses its VimLeavePre cleanup (_G.claudecode_test_cleanup). It ran only when PLENARY_TEST_HARNESS was set, and nothing sets it (not this repo and not plenary at CI's pin). I deleted it instead of arming it: it never ran, the plugin stops its own server in its VimLeavePre handler, and arming it would add an exit-time path to every spec Neovim, where a hung handler turns into a TIMEOUT:.
  5. Tests. tests/unit/integration_runner_spec.lua gets six tests. The Neovim that plenary starts runs tests/minimal_init.lua of the working folder by an absolute path (the test runs the VIMINIT command with dofile stood in for). The VIMINIT command loads no file when CLAUDECODE_MINIMAL_INIT is missing (without its if f then guard, dofile(nil) would read standard input). Each file gets state and config folders that are removed afterwards. XDG_DATA_HOME is the one the runner got. The runner's own Neovim takes -i NONE. A runner that gets SIGTERM during a file stops that file's Neovim, leaves no temp file or folder, and exits 143.
  6. scripts/README.md lists the two per-file temp folders (shared by the file's two Neovims), their removal including on SIGTERM, and the four variables the runner sets for each file's Neovim (VIMINIT, CLAUDECODE_MINIMAL_INIT, XDG_STATE_HOME, XDG_CONFIG_HOME).
  7. A comment at PLENARY_SHA in .github/workflows/test.yml tells whoever bumps the pin to recheck plenary's own 50 s per-file timeout against the runner's FILE_TIMEOUT.
  8. The cache-check comment in .github/workflows/test.yml now says which code writes under stdpath("cache"): only the legacy diff.open_diff, which diff_legacy_swap_spec.lua points at a temp folder.
  9. tests/integration/diff_hand_over_spec.lua asserts that the :split step opened the expected file.

Why VIMINIT and not test_directory(<file>, opts)

test_directory or a direct plenary.busted call would carry the options, but they change the text test_file('<file>' in the runner's command line, which the stub nvim of the runner spec reads the file name from. VIMINIT keeps the command line as it is, and it also stops the developer's init file from running, which options alone would not. Its cost is that VIMINIT reaches nested Neovims too (the comment says so).

One behavior change

The spec Neovims of command_args_spec.lua and mcp_tools_spec.lua now run the setup({ auto_start = false, log_level = "trace" }) of tests/minimal_init.lua before their tests, because their command line matches its should_load check. That adds debug log lines. The results are identical.

The 50 s timeout

plenary applies its own per-file timeout of 50 s (timeout = 50000 in test_harness.lua), and test_file has no way to change it. The runner's FILE_TIMEOUT (40 s) plus its 5 s kill grace stays under it, as #515 set.

Evidence

The runner's real script on a tree whose only spec prints the state of its own Neovim, with a fake developer init file and a fake developer ftplugin/lua.lua in XDG_CONFIG_HOME:

Tree mapleader timeoutlen updatetime developer init ran developer ftplugin ran state folder under $HOME
main none 1000 4000 yes yes yes
this branch " " 300 250 no no no

The whole integration runner, Neovim 0.12.3 and v0.8.0, main against this branch: 316 passed (237 rows on v0.8.0, 229 passed and 8 pending), 0 failed on both. The sorted Success, Fail and Pending rows are identical for both trees on both versions, and so are the success counts of all 24 files.

The same runner with HOME set to an empty folder:

Tree Left under that HOME
main, Neovim 0.12.3 .local/state/nvim/ with lsp.log, nvim.log, shada, swap
main, Neovim v0.8.0 .local/state/nvim/ with log, shada, swap
this branch, both versions nothing

The two new runner spec tests, with each mutant of the script (a mutant is red when its row says "red"):

Mutant of the script VIMINIT test state and config test
none green green
VIMINIT renamed so it is not set red green
the variable names another file than tests/minimal_init.lua red green
the variable holds a relative path red green
the runner of main red red
XDG_STATE_HOME and XDG_CONFIG_HOME not set green red
the temp folders not removed green red

Two more mutants, for the two new assertions:

Mutant of the script Assertion Result
the if f then guard dropped no file loaded without the variable red (green on the head)
XDG_DATA_HOME also set to a temp folder XDG_DATA_HOME unchanged red (green on the head)

Mutants for the two tests of this round (green on the head):

Mutant of the script -i NONE test SIGTERM test
-i NONE removed red green
no EXIT trap (the trap on HUP) green red
the trap without the process group kill green red
the temp files not recorded for the trap green red
the process group file not recorded green red
the runner of main red red

Nested Neovim without -u from another folder (the verifier's finding): with the relative luafile tests/minimal_init.lua of the first version, both Neovim versions print E5112 ... cannot open tests/minimal_init.lua and run with defaults. With the absolute path in the variable, neither prints an error and mapleader is " ". A root folder named odd dir 50% works too.

The swap file step assertion in diff_hand_over_spec.lua, under TMPDIR set to odd tmp 50%:

Plugin Spec Result
as is with the assertion 13 passed, 0 failed
the step's fnameescape removed with the assertion 10 passed, 3 failed
the step's fnameescape removed as on main 13 passed, 0 failed (the silent pass)

Without the odd TMPDIR, the mutant with the assertion is green too: the escape only matters for a % or a space.

Hang probe from #515 (a spec that sleeps 120 s), three runs on this branch: each exits 1, prints TIMEOUT: hang_spec.lua was stopped (exit 124): nvim did not exit within 40s, and leaves no Neovim running. The TIMEOUT: line came after 40.4 s, 44.4 s and 40.5 s. The 44.4 s run was on a loaded host (the Opus verifier saw 41.8 s to 43.7 s at a load of 37 to 40, and 41.0 s to 42.2 s on main), so the limit plus the 5 s kill grace (45 s) is not far from a loaded run, and still under plenary's 50 s. The runner's state and config folders are gone from the run's TMPDIR. Only the temp folder of the killed Neovim itself is left, as before.

E886 after a timeout, the hang probe on the head of the previous round and on this branch, three runs each:

Head Runs with an E886 line
previous round (6ed9c83) 1 of 3 (the Opus verifier saw 3 of 4)
this branch 0 of 3

SIGTERM sent to the runner while the hang spec runs (the runner's TMPDIR is a fresh folder):

Tree Runner exit Left in TMPDIR right after Hang spec's Neovim
main 143 at once 2 temp files runs on until its timeout (39 s)
this branch 143 at once nothing (only Neovim's own folder) killed at once, nothing recreated at the end

The trap without the process group kill left a recreated state folder in TMPDIR after the hang Neovim ended.

Gate: mise run clean && mise run all exit 0 (seed 1794435649, 1661 passed, 0 failed, 22 pending). actionlint and shellcheck pass. The whole runner on Neovim 0.12.3 gives 316 passed, 0 failed, with rows identical to main at the branch base. Merged with current main (which adds the 3 rows of #529's spec), it gives 319 passed on 0.12.3 and 232 passed plus 8 pending on v0.8.0, with rows identical to main.


Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh

plenary's test_file takes no options, so the {minimal_init = ...} the runner
passed was dropped: the Neovim that plenary starts for a spec had no -u, did not
run tests/minimal_init.lua, and read the developer's own init file. The runner now
sets VIMINIT, which a Neovim without -u runs instead, and drops the unused option.

A comment at PLENARY_SHA tells whoever bumps the pin to recheck plenary's own
50 s per-file timeout against the runner's FILE_TIMEOUT.

_Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh_
@ThomasK33
ThomasK33 marked this pull request as ready for review November 11, 2026 18:16
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-11T22:36:48.375451Z c1fc52b Manual request
🔒 Security Review ✅ Completed 2026-11-11T22:36:28.757083Z c1fc52b Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 3fefcbfbaa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 3fefcbfbaa

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

- The runner passes tests/minimal_init.lua to the Neovim that plenary starts
  through VIMINIT by an absolute path in CLAUDECODE_MINIMAL_INIT, so a path
  with a space or % needs no escaping and a nested Neovim started from another
  folder finds it. The comment says VIMINIT reaches every process a spec starts.
- Each file's Neovim gets its own XDG_STATE_HOME and XDG_CONFIG_HOME in temp
  folders, removed afterwards. A run no longer writes ~/.local/state/nvim, and
  the developer's ftplugin and after files do not run. plenary stays under
  XDG_DATA_HOME.
- Two runner spec tests fail when VIMINIT is missing or names another file than
  tests/minimal_init.lua of the working folder, and when the state and config
  folders are missing or not removed.
- diff_hand_over_spec.lua asserts that the swap file step opens the file, which a
  dropped fnameescape in the plugin's message no longer passes with a % in TMPDIR.
- The cache check comment says that only the legacy diff.open_diff writes there.
- minimal_init.lua loses the VimLeavePre cleanup, which ran only when
  PLENARY_TEST_HARNESS was set, and nothing sets it.

_Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh_
_Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh_
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: e6af70e3c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: e6af70e3c5

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

…document the runner's folders

- The runner spec runs the VIMINIT command with no CLAUDECODE_MINIMAL_INIT and
  expects no dofile call, so dropping the `if f then` guard (dofile(nil) reads
  standard input) turns it red.
- The runner spec expects XDG_DATA_HOME in the Neovim to equal the one the
  runner got, so isolating it (plenary lives there) turns it red.
- scripts/README.md lists the two per-file temp folders and their removal and
  the four variables the runner sets for each file's Neovim.

_Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh_
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 6ed9c83c8f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 6ed9c83c8f

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

…d clean up on SIGTERM

- The runner's own Neovim takes -i NONE. It and the Neovim plenary starts share
  the per-file state folder, and after a timeout both wrote ShaDa at once: the
  first failed with E886 in most timed-out runs.
- An EXIT trap removes the running file's temp files and folders, so a runner that
  gets SIGTERM during a file leaves none behind. It still dies with 143.
- scripts/README.md says the two Neovims of a file share the folders.
- Runner spec tests pin -i NONE and the SIGTERM cleanup.

_Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh_
The EXIT trap of the runner now kills the process group of the file that runs
before it removes the file's temp folders. A Neovim that outlived the runner wrote
ShaDa when its timeout stopped it and made the removed state folder again.

_Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh_
_Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh_
_Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh_
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: c1fc52bfdd

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: c1fc52bfdd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33
ThomasK33 added this pull request to the merge queue Nov 12, 2026
Merged via the queue into main with commit ed1e087 Nov 12, 2026
7 checks passed
@ThomasK33
ThomasK33 deleted the test/runner-plenary-options branch November 12, 2026 00:26
hareki pushed a commit to hareki/claudecode.nvim that referenced this pull request Nov 13, 2026
… it contradicts (coder#533)

## Why

The goal is that every claim in the README contracts of this repository
is true for the code at `main`, and that each correction names its
evidence. A contract is the README of one folder. It states what the
code does and cites the function that does it. The branch `docs-gtz7`
corrected many claims at an old commit of `main`. `main` has moved since
(peer PRs coder#445 to coder#522 rewrote several passages and changed the code).
This PR merges `main` into that work, re-checks every line that it
changes against the current code, and fixes the lines that the peers
made stale or that the merge joined into false text.

## What changed

Docs only, in seven README files. The branch has four parts.

1. The commits of `docs-gtz7` (three content commits and a merge of
`main`): a refresh of the contracts against the code, a pass that fixed
the drift that the refresh left, and a pass that corrected claims which
runtime probes disproved.
2. Five merge commits of `main` (the second for coder#522, the third for coder#524
and coder#530, the fourth for coder#506, the fifth for coder#531 and coder#526). Where a
peer PR and `docs-gtz7` both changed a passage, `main`'s text won. A
`docs-gtz7` correction stayed only where it is still true.
3. Nine correction commits. Six verifier agents, one per file or half
file, checked every line that this diff changes against the code. They
ran headless Neovim 0.12.3 probes (one on 0.10.4) where reading did not
decide the claim. I applied their corrections in commits that group the
files: server, root and plugin, tools, lua, terminal, scripts, the
main-text fixes, and one for the exit sentences and the last seven
findings. A second verifier then found one false sentence (the exit
sentence below) and seven wording findings, which that last commit
fixes.

4. One commit for two P2 findings of the Codex code review on
`42ef1bd1`. First, `stop` and the token mismatch in `start` try to
remove the lock file and only log a warning when that fails
(`lua/claudecode/init.lua:642-647`, `:605-608`), so three sentences in
`README.md` now say "tries to". Second, the headless auto-setup check in
`plugin/README.md` used `--cmd "set rtp^=$PWD"`, which Ex splits at a
space in the checkout path. I ran it in `/…/with space/repo` on Neovim
0.12.3: it fails with `E518` and prints `0`. The `lua
vim.opt.rtp:prepend(vim.fn.getcwd())` form prints `2`, and `0` without
the auto-setup line, as the README says.

Per file, with the number of claims checked and the result:

| File | Claims checked | True | False | Incomplete | Unverified | Edits
|
|---|---|---|---|---|---|---|
| `README.md` (Repository Blueprint) | 41 | 39 | 0 | 2 | 0 | 2 |
| `plugin/README.md` | 40 | 39 | 0 | 1 | 0 | 1 |
| `lua/claudecode/README.md` | 198 | 173 | 11 | 14 | 0 | 23 |
| `lua/claudecode/server/README.md` | 88 | 75 | 3 | 9 | 1 | 13 |
| `lua/claudecode/terminal/README.md` | 106 | 100 | 2 | 4 | 0 | 5 |
| `lua/claudecode/tools/README.md` | 68 | 54 | 4 | 10 | 0 | 14 |
| `scripts/README.md` | 7 | 0 | 2 | 4 | 0 | 13 |

A false claim was true at the old base or never true. An incomplete
claim leaves out a condition that the code has. The row for
`scripts/README.md` lists the seven items of the coder#462 verifiers (one
more was misleading). The 541 claims of the first six rows split into
480 true, 20 false, 40 incomplete and 1 unverified.

### Re-sync items after the peer PRs

- coder#530 (invalid `provider`). `lua/claudecode/README.md` said that an
invalid `provider` keeps the previous value "although the warning says
Defaulting to 'native'". `main` now warns `Keeping '<provider>'`
(`lua/claudecode/terminal.lua:483-488`). The sentence says what the
warning names now. The defect is gone from the list of code defects
below.
- coder#531 (release-please runner). The `run-release-please.sh` bullet of
`scripts/README.md` keeps the verified text and adds `main`'s clause
that `commonmark` must be on the module path too. The runner requires it
at load (`scripts/release-please-runner.js:27`). The Communique,
Prettier and output code that the runner bullet cites did not change.
- coder#526 (snacks tests). It changes comments and dead window option
assignments in `lua/claudecode/terminal/snacks.lua`. No line of this PR
depends on them.
- coder#524 (visual selection and plugin edits). The two conflicts were
passages about the buffer listener and the flush of a visual selection.
`main`'s text won in both, and the root `Editor state` bullet keeps the
verified autocmd group list with `main`'s listener wording
(`lua/claudecode/selection.lua:watch_selection_buffer`,
`note_visual_edit`). The selection claims that this PR changes
(`selection_changed` positions, `selection.lua` internals) do not depend
on the code that coder#524 changed.
- coder#459 (lockfile). `README.md` no longer says that `start` leaves the
lock file on disk after a token mismatch: `start` removes it and stops
the server (`lua/claudecode/init.lua:603-619`). The System Verification
bullet about `CLAUDE_CONFIG_DIR` and `tests/lockfile_test.lua` is gone:
the test hides the caller's value and stays in a private home
(`tests/lockfile_test.lua:340-355`, `:439-442`). `lockfile.update` is
removed from `lua/claudecode/README.md`: the module has no such
function. The `start` and `stop` bullets use the text of coder#459, and
`stop` is described as an attempt to remove the lock file
(`init.lua:635-670`).
- coder#461 (openFile reload). The reload-error passage of
`lua/claudecode/tools/README.md` is the text of `main`. The claim that
the recovery read does not end Visual mode stays. A probe confirmed it
in both forms: plain Visual mode, and Visual mode that CTRL-O started.
The read runs through `nvim_buf_call`, so the mode stays on
(`lua/claudecode/tools/open_file.lua`).
- coder#462 (WebSocket shell clients removed). No README cites
`scripts/lib_claude.sh` or another deleted file (a grep over the seven
files finds none).

### Corrections per file

<details>
<summary>README.md</summary>

- The list of what rejects a pending `openDiff` now includes Claude's
`close_tab` and `closeAllDiffTabs` calls, which reject it through
`close_diff_by_tab_name` (`lua/claudecode/diff.lua:2040-2044`,
`:2105-2107`, `lua/claudecode/tools/close_tab.lua:53`,
`lua/claudecode/tools/close_all_diff_tabs.lua:30`).
- At exit, Neovim fires `BufUnload` for each loaded buffer before
`VimLeavePre`, and the proposal buffer's `BufUnload` autocmd rejects its
pending diff (`lua/claudecode/diff.lua:1279-1285`). So with one pending
diff the rejection runs before the `VimLeavePre` cleanup. With two or
more pending native diffs there is one exception: the reject of a
new-file diff in the current tab with `diff_opts.on_new_file_reject =
"keep_empty"` (the default) deletes its proposal buffer
(`diff.lua:1171-1175`, `:1523-1524`), Neovim's exit loop then stops
firing `BufUnload`, and the cleanup removes the other diffs with no
answer. Probes on Neovim 0.8.0, 0.9.5, 0.10.4, 0.11.5 and 0.12.3 show
both cases, and the `unified` layout answers every diff. The sentence
states both. A fix PR (F13) follows, see "Defects in the code".
- `start` removes the lock file on a token mismatch (see above).
- The Processes bullet adds `:checkhealth claudecode` (`<cli>
--version`, `lua/claudecode/health.lua:43-66`) and the `git rev-parse`
of `terminal.git_repo_cwd` (`lua/claudecode/cwd.lua:28-42`).
- `CLAUDE_CONFIG_DIR` set but empty uses `~/.claude/ide`
(`lua/claudecode/lockfile.lua:11-18`). Three lines say so now.
- The Side Effects bullet lists the autocmd groups of `lua/` and
`plugin/` (`ClaudeCodeShutdown`, `ClaudeCodeSelection`,
`ClaudeCodeMCPDiff`, `ClaudeCodeDiffCleanup`, `ClaudeCodeNativeAdopted`,
`ClaudeCodeOpenFileNewFiles`, and one `snacks_win_<id>` per Snacks
terminal whose split the plugin re-creates,
`lua/claudecode/terminal/snacks.lua:259`), the `nvim_buf_attach`
listeners (`lua/claudecode/selection.lua:175-191`) and the `"unified"`
highlight groups (`lua/claudecode/diff_inline.lua:33-36`).

</details>

<details>
<summary>plugin/README.md</summary>

- Commands from auto-setup exist after a startup script that waits
(`:sleep` or `vim.wait()`) lets the deferred `setup` run. They do not
exist when the `-c` commands and `VimEnter` run. Headless probes on
Neovim 0.12.3 (`plugin/claudecode.lua:13-17`).
- The other 39 changed claims held: the load guard order, the second
`source` doing nothing, the version error on Neovim older than 0.8, the
failed module load shown once, the auto-setup global read twice, and the
error from the deferred `setup`.

</details>

<details>
<summary>lua/claudecode/README.md: 23 corrections with
evidence</summary>

- Only :ClaudeCodeSend uses the local add_paths_to_claude.
:ClaudeCodeTreeAdd and :ClaudeCodeAdd call M.send_at_mention in their
own code. Evidence: lua/claudecode/init.lua:703, init.lua:830,
init.lua:904, init.lua:914, init.lua:974-975, init.lua:1021-1022,
init.lua:1126.
- expand_tilde expands only `~` and `~/...`. A path such as `~user/x`
stays as it is. Evidence: lua/claudecode/init.lua:1113-1117,
lua/claudecode/utils.lua:167-181.
- The fallback split runs create_split, whose leave_preview_window moves
from the preview window to a tiled Claude terminal window in the same
tab before it splits. Evidence: lua/claudecode/diff.lua:803-810,
diff.lua:263-276, diff.lua:279-281.
- Since coder#483 and coder#469, diff_inline.lua no longer calls
_resolve_diff_as_saved (it calls _write_proposal) and it also calls
_register_reject_when_hidden and _close_proposal_window. Evidence:
lua/claudecode/diff_inline.lua:471, diff_inline.lua:485,
diff_inline.lua:616, lua/claudecode/diff.lua:1228-1234, commits 6eef218
(coder#483) and 36f4a0e (coder#469).
- lockfile.update no longer exists: PR coder#459 (0e63cc7) removed it.
Evidence: lua/claudecode/lockfile.lua:74, lockfile.lua:84,
lockfile.lua:208, lockfile.lua:239, lockfile.lua:292 (the only M
functions), commit 0e63cc7 (coder#459).
- terminal.setup also calls get_provider, whose fallback warnings go
through logger.warn, as the bullet above says. Evidence:
lua/claudecode/terminal.lua:584, terminal.lua:124-130, terminal.lua:145,
terminal.lua:163-166, terminal.lua:178.
- A connected send that the debounce queues still fires
ClaudeCodeSendComplete. Only the disconnected branch fires none.
Evidence: lua/claudecode/init.lua:383-398,
lua/claudecode/init.lua:1395-1400, lua/claudecode/init.lua:401-416.
- The terminal call and the event happen only when _broadcast_at_mention
succeeds. Evidence: lua/claudecode/init.lua:383-400.
- No handshake polling exists since coder#454/coder#480. The 50 ms, 25 ms and
connection_wait_delay facts repeat main's queue bullet. A send with
debouncing off still queues behind a non-empty queue. Evidence:
lua/claudecode/init.lua:283-289, lua/claudecode/init.lua:110-111,
lua/claudecode/init.lua:1381-1396.
- PR coder#452 added TextChangedI and TextChangedP. Evidence:
lua/claudecode/selection.lua:387.
- Since coder#452 reconcile_held_selection reports the cursor at CursorMoved,
not after visual_demotion_delay_ms. A text change and another buffer
also end the hold. Evidence: lua/claudecode/selection.lua:237-265,
lua/claudecode/selection.lua:118-159,
lua/claudecode/selection.lua:576-584, tests/selection_test.lua:2341.
- find_main_editor_window skips only a fixed filetype list, not every
explorer or outline sidebar. Evidence: lua/claudecode/diff.lua:157-190.
- find_main_editor_window can return a window of another tab (current
tab first), as the bullet above says. Evidence:
lua/claudecode/diff.lua:125-142, lua/claudecode/diff_inline.lua:382-409.
- Probe: proposal "a\n\n" is accepted as "a\n" (split_lines drops one
trailing empty line, and the newline fix adds none back). Evidence:
lua/claudecode/diff_inline.lua:44-52,
lua/claudecode/diff_inline.lua:526-540,
lua/claudecode/diff_inline.lua:303, probe_unified_accept.out.
- An invalid diff percentage falls back to split_width_percentage, not
straight to 0.30. Evidence: lua/claudecode/diff.lua:50-69.
- PR coder#469 set the unified proposal buffer to bufhidden=hide (main's
bullet above says so), WinClosed/BufHidden reject it. Evidence:
lua/claudecode/diff_inline.lua:299,
lua/claudecode/diff_inline.lua:475-485,
lua/claudecode/diff.lua:1189-1222.
- The close_tab tool also passes a client_id. Evidence:
lua/claudecode/tools/close_tab.lua:53,
lua/claudecode/diff.lua:2105-2148.
- At qa! the BufUnload autocmd (diff.lua:1283) calls
_resolve_diff_as_rejected before the VimLeavePre cleanup runs, for one
pending diff. With two or more native diffs, a keep_empty reject of a
new-file diff deletes its buffer and stops the exit loop, so the later
diffs stay pending until the cleanup (the Augroups bullet states this).
Evidence: lua/claudecode/diff.lua:1171-1175,
lua/claudecode/diff.lua:1279-1285, lua/claudecode/diff.lua:1523-1524,
probes on 0.8.0 to 0.12.3.
- PR coder#478 added the WinClosed autocmd that deletes the handler.
Evidence: lua/claudecode/diff.lua:455-463,
tests/unit/diff_new_tab_terminal_handler_spec.lua:127.
- require('claudecode').setup validates log_level (assert) before
logger.setup runs. Evidence: lua/claudecode/init.lua:462-465,
lua/claudecode/config.lua:15, lua/claudecode/config.lua:98-106,
lua/claudecode/logger.lua:26-40.
- Neovim 0.10 has no 'messagesopt' and always stops. The parenthetical
read as if only 0.11+ stops. Evidence: probe_echospace.out (0.10.4: NO
SUCH OPTION, 0.11.5: hit-enter,history:500).
- With laststatus=1 and two windows v:echospace is 68, not 51. Evidence:
probe_echospace.out.
- PR coder#445 removed the skip. The loop runs every file. Evidence:
scripts/run_integration_tests_individually.sh:86-88.

</details>

<details>
<summary>lua/claudecode/server/README.md: 13 corrections with
evidence</summary>

- The pending check/idle pair of a tick runs judge_silence and
judge_stall, not only a stall confirmation Evidence:
lua/claudecode/server/tcp.lua:601-611, tcp.lua:424-435, tcp.lua:490.
- The state stays `closing` after on_close. On_close only removes the
client (README line on client states says it never becomes `closed`)
Evidence: lua/claudecode/server/client.lua:363-371, tcp.lua:227-228,
tcp.lua:275-283.
- start_close_timer also sets `closed` when a keepalive window passes
with no write progress Evidence:
lua/claudecode/server/client.lua:459-466, client.lua:140-144.
- Completes the state list: _disconnect_client does not touch
client.state (probe: on_disconnect saw state `connected` after EOF)
Evidence: lua/claudecode/server/tcp.lua:209-217, tcp.lua:255-294,
probe-server/events.log client_8, client_9.
- PR coder#480 added connected_at to get_client_info Evidence:
lua/claudecode/server/client.lua:522-531, client.lua:292.
- PR coder#454 moved process_mention_queue from on_connect to on_open
Evidence: lua/claudecode/server/init.lua:57-73, client.lua:294-296.
- After the merged sentences, `It` reads as process_data or the Close
frame. Process_data does check the whole message Evidence:
lua/claudecode/server/frame.lua:156-168, client.lua:184-188.
- parse_frame rejects a 1-byte Close payload with 1002 (same bullet
group says so), so only an empty payload reaches the 1000 default
Evidence: lua/claudecode/server/frame.lua:157-160, client.lua:352-361,
probe S5, S6.
- Since PR coder#455/coder#488 last_pong plays no part in the keepalive Evidence:
lua/claudecode/server/client.lua:381-383, client.lua:529,
tcp.lua:220-222, tcp.lua:474-475.
- PR coder#497/coder#500: tools/call answers -32602 for a non-string name before
handle_invoke. Contradicts the JSON-RPC errors bullet Evidence:
lua/claudecode/server/init.lua:353-368.
- Two callbacks share the name: process_data's on_error carries client
errors (other bullets say so), server.on_error only server failures
Evidence: lua/claudecode/server/tcp.lua:121,190,196,524,
tcp.lua:229-231, client.lua:202,275.
- The 101/error write callback calls on_error when err or not success
Evidence: lua/claudecode/server/client.lua:271-279, probe events
client_1..3.
- The message carries the tool name Evidence:
lua/claudecode/tools/init.lua:90-96.

</details>

<details>
<summary>lua/claudecode/terminal/README.md: 5 corrections with
evidence</summary>

- FALSE since coder#464: native close and the first get_active_bufnr of a
module copy adopt a marked live terminal. The sentence also contradicts
the same bullet's second sentence. Evidence:
lua/claudecode/terminal/native.lua:292,
lua/claudecode/terminal/native.lua:574-577, probe
probe-terminal/probe_native_reload.out.
- INCOMPLETE: on Neovim 0.12.3 termopen also returns 0 under :silent!
for a modified buffer, which is not an invalid argument. Evidence: probe
probe-terminal/probe_termopen.out (cases D, E, F, G, H, I),
lua/claudecode/terminal/native.lua:237-242.
- INCOMPLETE: through the dispatcher a missing Snacks never reaches the
ERROR notification, because get_provider falls back to native. Evidence:
lua/claudecode/terminal.lua:133-146,
lua/claudecode/terminal.lua:192-196,
lua/claudecode/terminal/snacks.lua:691-693.
- INCOMPLETE: an unset snacks_win_opts.position falls back to split_side
(left or right), so cc_show re-creates a split itself. Evidence:
lua/claudecode/terminal/snacks.lua:374-377.
- INCOMPLETE: for an adopted terminal each entry with a config replaces
term._cc.config, so it is not the config captured at open. Evidence:
lua/claudecode/terminal/snacks.lua:463-474,
lua/claudecode/terminal/snacks.lua:646-647,
lua/claudecode/terminal/snacks.lua:747.

</details>

<details>
<summary>lua/claudecode/tools/README.md: 14 corrections with
evidence</summary>

- FALSE after PR coder#500: handle_invoke uses `params.arguments or {}`, so
no Lua error and no -32000 (probe p1: openFile/saveDocument -32602,
getDiagnostics []). Also contradicted line 17. Evidence:
lua/claudecode/tools/init.lua:88, lua/claudecode/server/init.lua:357,
probes/p1_static.out.
- FALSE: the script loops over every tests/integration/*_spec.lua with
no skip. PR coder#445 (a4d3238) made it run command_args_spec again.
Evidence: scripts/run_integration_tests_individually.sh:86.
- Misleading: bufnr() finds 'file.txt', 'b/file',
'./dir/../dir/file.txt' and '' (current buffer). Probes p1 and p5.
Evidence: lua/claudecode/tools/save_document.lua:31,
lua/claudecode/tools/check_document_dirty.lua:27, probes/p1_static.out,
probes/p5_bufnr.out.
- INCOMPLETE after PR coder#457: with no stored selection it reads the editor
(the bullet above says so), so it does not always return the stored
selection. Evidence:
lua/claudecode/tools/get_current_selection.lua:37-39, :83-95.
- INCOMPLETE: the -32603 table is returned, not raised, so
build_response sends it as `{ result = ... }`. Also joins the
hard-wrapped bullet like the rest of the file. Evidence:
lua/claudecode/tools/close_tab.lua:26-33,
lua/claudecode/tools/init.lua:180-182,
lua/claudecode/server/init.lua:387.
- Format: the merge wrapped a quoted error message across two lines
inside a code span, so a search for the message misses it. Every other
bullet is one line. Evidence: lua/claudecode/tools/open_diff.lua:56.
- Coherence: the inserted sentence split 'finds it' from 'Without one',
so 'one' no longer names the loaded buffer. The moved claim is TRUE
(probe p1). Evidence: lua/claudecode/tools/open_file.lua:133-140,
probes/p1_static.out.
- INCOMPLETE: expand_tilde leaves `~user/...` unchanged. Evidence:
lua/claudecode/utils.lua:167-181,
lua/claudecode/tools/open_file.lua:547.
- INCOMPLETE: with makeFrontmost=false the answer is the JSON object,
and in the middle of a command openFile appends a suffix. Evidence:
lua/claudecode/tools/open_file.lua:735, :741, :746-760,
probes/p4_openfile_save.out.
- INCOMPLETE: source and code are nil (absent from the JSON) when the
diagnostic has none. Evidence:
lua/claudecode/tools/get_diagnostics.lua:82-83.
- INCOMPLETE: nvim_get_current_tabpage() returns a handle. Probe p1:
tabpagenr() 2, handle 3, groupIndex/viewColumn 2/3. Evidence:
lua/claudecode/tools/get_open_editors.lua:19, :65-66,
probes/p1_static.out.
- INCOMPLETE: reload_file_buffers skips a modified buffer, and a diff
kept for another client does not close. Evidence:
lua/claudecode/diff.lua:2015-2018, :2025-2027, :1104.
- INCOMPLETE: no API tells which loaded buffers were opened before their
file existed. The loop marks every loaded, named, normal buffer whose
file is not readable. Evidence:
lua/claudecode/tools/open_file.lua:170-179.
- INCOMPLETE (minor): with diff_opts.layout = "unified" diff.lua hands
the result to diff_inline, which builds it. Evidence:
lua/claudecode/diff.lua:1050-1053,
lua/claudecode/diff_inline.lua:526-537.

</details>

<details>
<summary>scripts/README.md: 7 items of the coder#462 verifiers, each checked
against the scripts</summary>

- The CI paragraph named no job for "the same job". It now names the
three jobs of `.github/workflows/test.yml` (`unit-tests`,
`integration-tests`, `covered-lock`), says that `integration-tests` runs
the runner twice (the second time with a `TMPDIR` that holds a space and
a `%`), and says that `Required` needs only `unit-tests` and
`integration-tests` (`test.yml` lines 52-80, 98-167, 215-234).
- `tla/check-lib.sh` exit precedence: 2 wins over 1, and 1 wins over 3.
A stale lock plus a broken rule exits 1. `--hash-only` exits 0 or 3, and
`--record` exits 0. A script that sources the lib and ends without
`tla_main` turns an exit 0 into exit 2. Probed with a fake `curl` and a
fake TLC: default with a broken rule and a stale lock gave rc=1, default
with a pass and a stale lock rc=3, a broken `TypeOK` rc=2, `--record` on
a stale lock rc=0 (`scripts/tla/check-lib.sh:81-84`, `:226-246`,
`:313-333`, `:406-419`).
- `tla_init` comes right after `SKILL` and `MODEL`, before
`covered_block` (`.agents/skills/verify-diff-lifecycle/check.sh:15-29`,
`scripts/tla/check-lib.sh:461`).
- `run-release-please.sh` prints nothing itself, and exits 1 with no
message when `release-please` is missing. The runner's stdout and file
outputs are listed: release-please's own log lines go to stdout (info
and debug) and stderr (warnings and errors), the `key=value` lines go to
`GITHUB_OUTPUT` or to stdout when it is unset, and `--dry-run` prints
JSON after the log lines and still runs Communique. Probed without a
token and without network: wrapper rc=1 with no output, and `Error:
GITHUB_TOKEN must be set` (`scripts/release-please-runner.js:487-492`,
`:555`, `:590`, `:614`).
- `--record` and `--hash-only` exit before the TLC jar is fetched, so
they never download it and need no `java`. A plain run and `--mutants`
do. Probe with an empty cache and a fake `curl`: `--hash-only` and
`--record` made 0 `curl` calls, a plain run and `--mutants` made 1 and
exited 2 (`scripts/tla/check-lib.sh:452-453`, `:147-151`).
- The Lua repros: under `nvim -l` an uncaught error prints `E5113` and
exits 1, `cquit N` and `os.exit(N)` exit N, and a bare file name run
from `scripts/` fails with `module 'claudecode.diff' not found`. In the
`:luafile` form of CI an uncaught error falls through to `cquit 2`
(probes on Neovim 0.12.3, and `-l` exit 1 on 0.9.5, 0.10.4 and 0.11.5).
- `repro_issue_70.sh` and `repro_issue_277.sh` side effects that the
file did not list: work dir names, the `att-home` folder, the empty
`/tmp/agent-tty/<8 hex>` socket folder and the `homes.json` entry that
`agent-tty` leaves, the inherited environment, the three background
probes of 70 and their `PORT.lock` files (a SIGKILL leaves the lock),
the unpinned diffview.nvim clone of 277, the state and cache files of
`NVIM_APPNAME=issue-277`, the `perl` timeouts, and the nested copy under
an existing `issue277-artifacts-latest`. A full run of 277 with private
dirs took 32 s. A SIGKILL run left the lock file and 3 processes until
`agent-tty destroy`.
- `manual_test_helper.lua` calls the handler itself, so no response
sender is stored for its coroutine. Any resolution after the handler
yields (accept, reject, or `qa!`) logs `No global response sender found
for coroutine`. It is expected and harmless (all four CI-form runs
exited 0).

</details>

## Scope

- No code changes. `covered.lock` does not change.
- Lines of `scripts/README.md` about `repro_issue_218.sh` stay as they
are on `main`. PR coder#458 owns them, and its merge will change them.
- I checked every line that this diff changes. Lines that the diff does
not touch were not re-verified, except where a verifier found one false
while reading its neighbors. Those fixes are in the last correction
commit: `server/mock.lua` is required by
`tests/unit/server/mock_spec.lua`, the augroup and `utils.lua` lists,
the empty `CLAUDE_CONFIG_DIR`, and the Processes bullet.
- Probes ran on Linux with Neovim 0.12.3. One probe ran on 0.10.4. The
old Neovim claims that earlier lanes proved on 0.8 were not run again.
- One claim stays unverified: the sentence in
`lua/claudecode/server/README.md` that a client whose EOF the server
read in the same read as its request is handled as the code says. Six
probe tries did not produce that case. The sentence is a softening, so I
kept it.
- The terminal README says that the wait before a `close` does not
prevent a hang at exit. A probe saw the hang with and without a `close`,
so the probe does not prove that the buffer delete causes it. I did not
open neovim/neovim#42508 (the host has no network).

### Defects in the code that the verifiers found

These need a code fix and a test, so they are not in this docs PR.

1. Accepting a `unified` diff drops one trailing empty line of the
proposal: `"a\n\n"` is accepted as `"a\n"`
(`lua/claudecode/diff_inline.lua:44-52` and `:531`). The native layout
is not affected. A probe shows it. The README says the accepted text is
the proposal lines that setup computed.
2. `lua/claudecode/server/tcp.lua:19` says the PRNG is seeded once per
process. It is seeded once per load of the module.
3. `lua/claudecode/server/init.lua:59-60` logs "Authenticated WebSocket
client connected" from `on_connect`, before the authentication check
runs.
4. `lua/claudecode/cwd.lua:36-42` runs the same `git rev-parse` twice.
5. At exit, a pending native diff can stay unanswered. Two native diffs
are pending, and the first one (the lower proposal buffer number) is a
new file in the current tab with `diff_opts.on_new_file_reject =
"keep_empty"` (the default). On `:qa!` its reject deletes its proposal
buffer (`lua/claudecode/diff.lua:1171-1175`, `:1523-1524`), Neovim's
exit loop stops firing `BufUnload` (`src/nvim/main.c`, the
`bufref_valid` check), and the `VimLeavePre` cleanup removes the second
diff with no answer. Reproduced on Neovim 0.8.0, 0.9.5, 0.10.4, 0.11.5
and 0.12.3, with and without the server. A separate PR (F13) will make
exit answer every pending diff. It will change the exit sentences of
this PR: the openDiff bullet in `README.md` (Global Invariants, the
sentence that starts "When Neovim exits") and the Augroups bullet in
`lua/claudecode/README.md` (the sentences about `BufUnload` and the
exception).

## Blast Radius

Seven README files. Nothing loads them at runtime. A wrong correction
here misleads a reader but breaks no behavior.

## Verification

- Six Opus verifiers, one per file (two for the 198 claims of
`lua/claudecode/README.md`). Each checked every claim that the diff adds
or changes against the code, and every `[file:symbol]` citation, with
file and line evidence. Their findings, edits and probe scripts are in
the evidence folder of the run.
- A second, independent verifier checked the head `bcacd14c` (134
sentences and all seven scripts items, with probes on Neovim 0.8.0 to
0.12.3). It found one false sentence (the exit sentence, now fixed with
its exception) and seven wording findings (fixed). It found no other
false claim.
- Citation check on the final head: 760 citations, links and test paths
over the seven files, 1 flagged. The flagged one is
`[plugin/claudecode.lua:pcall(require, "claudecode")]`, an expression
that `main` already cites in that README.
- `mise run all`: exit 0 on the final head `e3e48af1` (1682 successes, 0
failures, 0 errors, 24 pending, `luacheck` 0 warnings and 0 errors in
178 files). `mise run format-check`: exit 0 on the final head (355
files, 0 changed). 1676 successes, 0 failures, 0 errors, 24 pending.
`luacheck` reports 0 warnings and 0 errors in 178 files. The format step
changes 0 files.
- `git merge-tree --write-tree` of the head and `origin/main`
(`3a0bb769`) is clean.
- The merges of coder#524 and coder#530 changed one sentence of
`lua/claudecode/README.md` and merged two conflict hunks. I checked that
sentence and the merged bullets against `lua/claudecode/terminal.lua`
and `lua/claudecode/selection.lua` at `a41254a4`. I did not run a
verifier agent over the merge delta.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model:
`anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant