Repository navigation
docs: use one name for the proposal buffer and finish the cleanup and I/O citations - #549
Conversation
README.md and lua/claudecode/README.md called the buffer that holds Claude's proposed text both the 'proposed buffer' and the 'proposal buffer'. The READMEs, ARCHITECTURE.md and most of lua/claudecode/README.md say 'proposal buffer', so the nine 'proposed buffer' uses now say that too. Only the word changes. Code comments and strings under lua/ are unchanged.
|
@codex review |
|
@codex security review |
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Another round soon, please! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
… the write path The #547 advisor named two items in ARCHITECTURE.md: - The Resource Cleanup sketch called M.stop() with no condition. The real VimLeavePre callback runs stop only when M.state.server is set, and clears the @-mention queue otherwise (init.lua:514-525). The sketch now shows the if and the else, and its lead-in no longer says 'Automatic cleanup'. - The I/O bullet said the TCP server writes with libuv's asynchronous write without a citation. It now cites client.lua write, which calls tcp_handle:write.
|
@codex review |
|
@codex security review |
|
Codex Review: Didn't find any major issues. Nice work! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
…split layouts (coder#551) ## Why When a user empties the proposal buffer (`ggdG` or `:%delete`) and saves it with `:w`, Claude must get what Neovim's own `:w` writes. Neovim writes no bytes for an emptied buffer. With `'eol'` on, the `vertical` and `horizontal` layouts answered `" "`, so Claude wrote a one-byte file where the user had asked for an empty one. The F14 verifier found this. PR coder#542 listed it as a known, pre-existing defect that this PR fixes. ## What changed `_resolve_diff_as_saved` in `lua/claudecode/diff.lua` saves the lines of the proposal buffer, joined with line breaks. It adds a final line break when the buffer's `'eol'` is on. A buffer with no lines at all (Neovim shows "--No lines in buffer--") still gives one empty string through `nvim_buf_get_lines`. So the save could not tell it from a buffer with one empty line, and it added the line break. The save now counts the bytes of the buffer with `vim.fn.wordcount().bytes`, run inside `nvim_buf_call`. The count is 0 for an empty buffer and at least 1 for a buffer with one empty line when `'eol'` is on. The line break is added only when the count is not 0 and `'eol'` is on. That replaces the check `#content_lines > 0`, which was always true. I tried `nvim_buf_get_offset` and `line2byte` first. Both gave wrong sizes for a buffer that `ggdG` had emptied (a stale line cache), so I did not use them. Other changes: - `tests/mocks/vim.lua` gets `wordcount()` and an `empty = true` flag for a mock buffer. `nvim_buf_call` in the mock now names the buffer that the callback acts on. - `tests/unit/diff_ui_cleanup_spec.lua` gets one fixture line. The mock's `nvim_buf_call` refuses a buffer that does not exist, and that spec saved a buffer that the mock never had. - `lua/claudecode/README.md`: one bullet replaces coder#542's bullet about the saved content of the split layouts. It states both rules, `'eol'` from the proposal and the empty buffer. It also says that a buffer with one empty line is saved as `"\n"` when `'eol'` is on and as `""` when it is off. - `.agents/skills/verify-diff-lifecycle/covered.lock` is re-recorded (see Blast Radius). ## Scope - The `vertical` and `horizontal` layouts share one code path, so one change fixes both. - The `unified` layout is unchanged. It answers with the proposal, not with the buffer, so what the user does to its buffer never reaches Claude. The new spec runs it as a control. - With `'eol'` on, a buffer with one empty line is still saved as `"\n"`. With `'eol'` off, it is saved as `""`, as before. A buffer with any line is saved as before. - On `main` (with coder#542), this PR changes the answer only for a buffer that the user emptied. It changes no answer for an unedited proposal: coder#542 already makes an empty proposal give `""`, and the new spec has that case as a control. ### Minor findings that I declined Two verifiers (Opus and GPT) found these. None of them changes an answer that a user reaches with the default settings. - **A buffer set to `bomb` (Opus F3).** With `ggdG`, then `:setlocal bomb`, then `:w`, Neovim writes the 3-byte BOM. Both `main` and this PR answer without the BOM (`main` answers `"\n"` when `'eol'` is on and `""` otherwise, this PR answers `""`). The plugin never sets `'bomb'` on its scratch proposal buffer, and no answer of the plugin ever holds a BOM, so a user has to set a non-default option on a scratch buffer first. I left it. - **The `nvim_buf_call` wrapper is covered by the mock spec only (Opus F4).** If I delete the wrapper, only the mock unit spec fails. In the paths that save (`BufWriteCmd` and `accept_current_diff`) the current buffer is always the proposal buffer, so the wrapper is defensive and no live answer changes. I kept the wrapper, because `wordcount()` reads the current buffer, and I added no live test for a case that no path reaches. - **Cost of `wordcount()` (Opus F5).** It adds a second pass over the buffer. For a 33 MB proposal that is about 0.1 to 0.2 s (0.12.3: +112 ms, 0.8.0: +216 ms), about the cost of `nvim_buf_get_lines` plus the join. I accept that for one save. ## Blast Radius - User-visible: a proposal buffer that the user emptied is accepted as an empty string. Every other buffer gives the same text as before. - `wordcount()` runs in the proposal buffer through `nvim_buf_call`. It does not change the user's current buffer or window. Both functions exist in Neovim 0.8. I ran the specs on Neovim 0.12.3 only. The CI job `integration-tests (v0.8.0)` runs the new spec on 0.8. - `covered.lock`: the change touches `_resolve_diff_as_saved`, a covered block. `check.sh` printed STALE for the `diff.lua` file hash and the `resolve_saved` block hash only. It printed no changed call site, and all 21 code facts stay TRUE. The guard that lets only a `pending` diff resolve is unchanged, and so are the status change to `saved` and the one call of the resolution callback. The change only decides whether the `text` string of the `FILE_SAVED` answer ends in a line break. The model stores which kind of answer a call got, not its text, so no modelled transition changes. The model passed (`main`: 89205 distinct states, `live-DisconnectedCallsAnswered`: 5269). `check.sh --record` wrote the new lock and `check.sh --hash-only` reports that it matches. ### Merge with coder#542, coder#539 and coder#535 coder#542 (F14) landed first, on 2026-11-12 at 04:36Z, and coder#539 (F12) at 04:46Z. coder#538 and coder#550 later changed `diff.lua` too, and coder#549 renamed "proposed buffer" to "proposal buffer" in the README. This PR merged `origin/main` (9bfc39c) with merge commits. It ran `check.sh --record` after each change to `diff.lua` and kept main's README wording in the last one. Only the `diff.lua` file hash and the `resolve_saved` block hash differ from the lock on `main`. The full `check.sh` passes and `check.sh --hash-only` matches. - One README bullet replaces this branch's bullet and coder#542's bullet, because the two said almost the same sentence and this branch's sentence about one empty line was false once `'eol'` can be off. - coder#535 (F11) is still open. It touches neither `diff.lua` nor this README bullet. Its current head (`a5dfc81b`, with `main` merged) merges with this PR without a conflict. ## Verification Red before the fix, green after (same specs, only `lua/claudecode/diff.lua` swapped, on the merged head, so the red column is `main` at 9bfc39c with coder#542): | Spec | `main` (with coder#542) | with the fix | | --- | --- | --- | | `tests/unit/diff_saved_text_spec.lua` (4 cases, mock) | 3 successes, 1 failure | 4 successes, 0 failures | | `tests/integration/diff_accept_emptied_buffer_spec.lua` (40 cases, real Neovim, `:write`) | 28 successes, 12 failures | 40 successes, 0 failures | Against the older merge base (5acc100, before coder#542), the 38-case spec of round a had 24 red cases. Today's 40-case spec has 26 there: the 24 emptied cases plus the new control for an unedited empty proposal in both layouts. coder#542 fixes the 12 cases from proposals without a final line break (it sets `'eol'` off), so 12 stay red on `main`. The integration spec opens a real `openDiff` and edits the proposal window. For the `vertical` and `horizontal` layouts it runs 6 proposals (two lines and a final line break, one line and a final line break, one blank line, one line with no final line break, two lines with no final line break, and no text) with 2 ways to empty the buffer (`ggdG` and `:%delete`). Each case compares the answer with what Neovim's own `:w` writes: the spec edits a real file buffer with the same text and commands, runs `:write`, and reads the file bytes, which are empty. Five controls per layout give the right text before and after (a blank line left in the buffer, no edit, an empty proposal without an edit, one line deleted, and text typed into the emptied buffer). In the `unified` layout, 6 controls check that the answer is the proposal. Gates on the final commit, which merges `origin/main` (9bfc39c) into the branch (private XDG folders): - `mise run all`: native exit 0. luacheck: 0 warnings / 0 errors in 183 files. busted: 1710 successes / 0 failures / 0 errors / 26 pending. treefmt: 0 files changed. - `mise run format-check`: native exit 0 (360 files, 0 changed). - Diff specs (`tests/unit/diff*_spec.lua`, `opendiff_blocking_spec`, `new_file_reject_then_reopen_spec`, and the `open_diff_mcp`, `close_tab` and `close_all_diff_tabs` specs): 226 successes, 0 failures. - `scripts/run_integration_tests_individually.sh`: 457 successes, 0 failed, 0 errors. The new spec gives 40 successes. - `.agents/skills/verify-diff-lifecycle/check.sh --hash-only`: covered.lock matches the code. Two independent verifiers (Opus and GPT) checked the code at `ef8be297`. Both found the code correct. Their blockers were the README sentence and the stale base, and this head fixes both. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
…coder#535) ## Why A unified diff must hand Claude back the exact text it proposed when the user accepts it. A proposal that ends in a blank line lost one line break. A proposed file ending `"a\n\n"` was accepted as `"a\n"`, so Claude wrote a file one blank line shorter than the one the user approved. ## What changed `resolve_inline_as_saved` in `lua/claudecode/diff_inline.lua` joined the accepted lines with `"\n"`. It added the final `"\n"` only when the join did not already end in one. A proposal that ends in blank lines has a blank last line, so the join already ended in `"\n"` and the final line break was never added back. The function now adds the final `"\n"` whenever the proposal ends with one. The lines never carry a line break after the last one, so this is always one line break short. Other changes: - `lua/claudecode/README.md` states the `unified` contract on the line that describes the `FILE_SAVED` content. - `.agents/skills/verify-diff-lifecycle/covered.lock` is re-recorded (see Blast Radius). ## Scope Only the `unified` layout changes. The `vertical` and `horizontal` layouts keep their code and their results. A separate defect used to stay out of this PR. In both the `vertical` and the `horizontal` layout, a proposal without a final line break was accepted with one. F14 (coder#542) fixed it and is now in `main`. The new integration spec runs the `vertical` layout, and its 2 cases without a final line break run as active tests (they were pending before F14 landed). ## Blast Radius - User-visible: an accepted unified diff keeps every blank line at the end of the proposal. Proposals that ended in a single line break, in no line break, or in blank lines inside the file give the same text as before. - `covered.lock`: the change touches `resolve_inline_as_saved`, a covered block. It changes only the `text` string in the `FILE_SAVED` answer. It does not change the saved status, the answer count, the resolution callback order or disconnect handling. Diff commands make the same decisions. `:ClaudeCodeCloseAllDiffs` puts the answer text into the file's buffer, so for a saved unified diff it now keeps the trailing blank lines too. The model does not represent answer text: it stores which kind of answer a call got, so no modelled transition changes. `check.sh` printed STALE for exactly two entries (the file hash and the `inline_saved` block hash of `diff_inline.lua`) and no changed call site. The model passed (`main`: 89205 distinct states, `live-DisconnectedCallsAnswered`: 5269). `check.sh --record` wrote the new lock and `check.sh --hash-only` then reports that it matches. ### Merge with F14 (coder#542) F14 (coder#542) landed first, as 49df75a. The two PRs left a test that each other made stale, so this PR owed the merge rule, and its merge commit with `origin/main` (0b0de5b) applies it: 1. F11's `tests/integration/diff_accept_trailing_newlines_spec.lua`: the 7-line `local case = it` pending clause is replaced with one `it(` line, so the 2 vertical no-final-line-break cases run. 2. F14's `tests/integration/diff_accept_missing_final_newline_spec.lua`: the `blank_end` skip is removed (the `if not (layout == "unified" and proposal.blank_end) then` line and its `end`, the 3 `blank_end = true` fields and their comment), so the 9 unified blank-line cases run. On the merged head, F11's spec gives 28 successes, 0 failures and 0 pending. F14's spec gives 94 successes, 0 failures and 0 pending. Without this PR's fix (`diff_inline.lua` from `main`), F11's spec gives 20 successes and 8 failures, and F14's spec gives 85 successes and 9 failures. All of those failures are unified blank-line cases. ## Verification Red before the fix, green after (same specs, only `diff_inline.lua` swapped, on the merged head, so `main` already has F14): | Spec | origin/main | with the fix | | --- | --- | --- | | `tests/unit/diff_inline_spec.lua` (24 new cases) | 38 successes, 12 failures | 50 successes, 0 failures | | `tests/integration/diff_accept_trailing_newlines_spec.lua` (28 cases, real Neovim) | 20 successes, 8 failures (all `unified`) | 28 successes, 0 failures, 0 pending | | `tests/integration/diff_accept_missing_final_newline_spec.lua` (F14's spec, 94 cases, real Neovim) | 85 successes, 9 failures (all `unified`) | 94 successes, 0 failures, 0 pending | The unit cases run `compute_inline_diff` and `resolve_inline_as_saved` for each proposal and compare the accepted text byte for byte. The integration spec opens a real `openDiff` in both layouts, runs `:write` in the proposal window and compares the answer byte for byte. It uses proposals ending in `"\n\n"`, `"\n\n\n"`, no line break, a single `"\n"`, `"\n"` only, `"\n\n"` only and a blank line inside plus one at the end, each against a file with other text and against a file one line shorter. The audit's own probe in a real Neovim shows the same change: proposal `"a\n\n"` was accepted as `"a\n"` and is now accepted as `"a\n\n"`. Gates on the final commit, which merges `origin/main` (9bfc39c) into the branch (the merge rule was applied in the earlier merge commit 0b0de5b) (private XDG folders): - `mise run all`: native exit 0. luacheck: 0 warnings / 0 errors in 182 files. busted: 1730 successes / 0 failures / 0 errors / 26 pending. treefmt: 0 files changed. - `mise run format-check`: native exit 0 (359 files, 0 changed). - Sibling specs (`diff_inline_spec`, `diff_inline_new_tab_spec`, `diff_inline_quit_spec`, `diff_spec`, `opendiff_blocking_spec`): 83 successes, 0 failures. - `scripts/run_integration_tests_individually.sh`: 454 successes, 0 failed, 0 errors. - `.agents/skills/verify-diff-lifecycle/check.sh --hash-only`: covered.lock matches the code. Rounds 1 and 2 (heads 7e6f819 and 0b0de5b): CI green and both Codex lanes clean. Round 3 is this head, after a merge with `main` for a README conflict (coder#549). Two independent verifiers (Opus and GPT) checked the code commit before the first merge with `origin/main`. Both found no introduced defect. They reran the red and green specs, reverted the code change alone to see the new specs fail, and compared the accepted bytes through a real WebSocket call. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
…diff closes (coder#494) ## Why After a split diff closes, a window that still shows the file can stay in diff mode with no partner window. The next `openDiff` then sees a window in diff mode, opens in a new tab, and closes the user's window together with that tab. coder#491 ended diff mode in the windows that the cleanup knew about. This closes the windows that it did not. Refs coder#491, coder#483. ## Cause Neovim saves a window's options for a buffer when the buffer leaves the window ('diff', 'scrollbind', 'foldmethod' and the rest included), and sets them again when that buffer enters the window. A window that never showed the buffer can also take diff mode on, by copying the options of a window that shows the buffer (`:split <file>` copies the window that it splits). While a split diff is open, the file's buffer is in diff mode in the original-side window. So when the cleanup closes the proposal window and deletes the proposal buffer, a window can end up showing the file in diff mode with no partner. Two ways, both reproduced on current main with real Neovim 0.12.3 and v0.8.0: 1. `:b <proposal>` in the original window, then `close_tab`. The cleanup deletes the proposal buffer, the window falls back to the file, and the saved options turn diff mode on. One window, `diff=true`. 2. `:split <file>` from the original window during the diff, then `close_tab`. The copy took diff mode from its source, the cleanup closes the proposal window, and the copy keeps diff mode alone. It stays on when `close_tab` runs from a plain window of the same tab, or from another tab. ## What changed The rule: when the cleanup is done, a window of the original side's tab page that shows the file leaves diff mode only if it is the only window with diff mode on in that tab page. A diff needs a partner window, so a lone diff window holds the leftover of the diff that ended, and a diff of the user's own over two or more windows stays. - `lua/claudecode/diff.lua`: two local functions and one call. `end_diff_mode_if_alone(win)` runs `diffoff` in a window while `'diff'` is on there and no other window of its tab page has `'diff'` on. It catches the refusal that Neovim gives in the command-line window (E11). `end_diff_mode_in_windows_that_show_the_file(diff_data)` runs that check for each window of the original side's tab page that shows the file. The cleanup calls it after the buffer deletes and before `ClaudeCodeDiffClosed` fires, so nothing that a handler of that event starts is looked at. Nothing is recorded and no autocmd is added. - `lua/claudecode/README.md`: states the rule, and which cases stay as on main (below). The sentences on saved options and on the unified layout's `'signcolumn'` are rewritten from plain-Neovim probes. - `tests/integration/diff_left_window_spec.lua` (real Neovim): 33 cases. They cover the two ways above, a window that the diff never left, a copy that has a partner and two copies that the user compares (kept), the options that the user set after ending diff mode, a lone diff window of another tab (not touched), the order of the look and the event (a diff that a `ClaudeCodeDiffClosed` handler starts stays), `close_tab` from the command-line window (it answers TAB_CLOSED and the copy keeps its diff mode, because Neovim refuses `diffoff` there), and the cases below that stay as on main. - Verify skill: `journey-diff-close-save.sh` shows the proposal in the original window with `:b` and closes the diff for an existing file, and `features/open-diff.md` and `SKILL.md` describe it. - `covered.lock` is re-recorded after merging main. Against main's lock only the `diff.lua` file hash and the `cleanup_state` block hash change. `check.sh` passes with TLC (89205 states). ## What stays as on main A file that comes back to a window later gets the options that Neovim saved or copies, diff mode included, as on main: - `:e other.txt` in the proposal window, `close_tab`, then `:e <file>` there, or `:e third.txt` in the original window, `close_tab`, then `:e #`. Diff mode is on again. This is the common case. It gets its own follow-up PR that fixes it at the source: the plugin splits the window before it runs `diffthis`, in both open paths, so the saved set never holds diff mode. - `:split <file>` from a window that left the file after the diff. - A window of another tab page that took diff mode from the diff's windows during the diff (`:tabnew <file>`, `:tab sbuffer`, `:tabnew` then `:buffer <file>`). The look covers only the tab page of the original side. - A `ClaudeCodeDiffClosed` handler that brings the file back into a window. - `close_tab` from the command-line window (`q:`) when the original-side window shows the file fails with -32000 and E11 in main's own `diffoff`, as on main. ## Before and after Real headless Neovim, the plugin's own `openDiff` and `close_tab` tool handlers, a stub `claude` first on PATH: | Scenario | main | this branch | | --- | --- | --- | | 1. original window: `:b <proposal>`, `close_tab` | one window, `diff=true` | one window, `diff=false` | | 2. `:split <file>` from the original window during the diff, `close_tab` from a plain window of the same tab | copy `diff=true`, the plain window's `'foldmethod'`, `'scrollbind'` and `'wrap'` kept | copy `diff=false`, the plain window's options kept | | 3. same copy, `close_tab` from another tab that holds the user's `diffthis` over two windows | copy `diff=true` | copy `diff=false`, the other tab's diff kept | | 4. a window of the first diff that the user switched to another file, then a second `openDiff`, `:edit <file>` in that window, and the second `close_tab` | one window left in diff mode | no window in diff mode | | 5. a copy that has a partner, or two copies that the user compares | kept | kept | | 6. `:e other.txt` in the proposal window, `close_tab`, `:e <file>` | `diff=true` | `diff=true` (follow-up PR) | | a diff with no window switch | `diff=false` | `diff=false` | The same rows give the same result on 0.12.3 and v0.8.0. ## Validation Head 06bde3e (round 9: a merge-only push of main 884f7e2 that re-records `covered.lock`, with no Lua change and no Codex review; the Lua patch-id equals rounds 7 and 8). On it: `check.sh --hash-only` and `check.sh` pass, the coder#494 spec passes 33/33 on Neovim 0.12.3 and v0.8.0, `mise run all` passes (1737 successes), the integration runner passes (527 on 0.12.3 after one rerun of two load timeouts, 390 on v0.8.0), and CI is green. Head e80a5a3, a merge of main 8b77e7e into c219b15 (round 8: a merge-only push, no code change). The PR's Lua diff is the same before and after the merge: `git diff c9e57d1..c219b15 -- '*.lua'` and `git diff origin/main...HEAD -- '*.lua'` give the same patch-id (`76da21f7b53bfdadb02303aa6ef201c9f445de4e`) and the same text without the `index` lines and hunk numbers (main moved `diff.lua`, so those differ). The merge conflicted in `covered.lock` and in `lua/claudecode/README.md`. The README conflict was main's edits around the cleanup bullet (coder#549, coder#542 and others) against the one bullet that this PR rewrites: I kept every line of main and put this PR's bullet in place of the old one, so the README differs from main by that one line. I re-recorded `covered.lock` with `check.sh --record` on the merge result. Against main's lock only the `diff.lua` file hash and the `cleanup_state` block hash differ. Size against main: 7 files, 613 insertions(+), 7 deletions(-). Code and README: 2 files, 42 insertions(+), 3 deletions(-). The spec is most of the rest. - `mise run clean && mise run all` on the merge result: exit 0. luacheck 0 warnings, 0 errors in 182 files. busted 1706 successes, 0 failures, 0 errors, 26 pending. - New spec, 33 cases, on 0.12.3 and on v0.8.0: 0 fail on this branch and 3 fail on main (cases 1 and 2 above, and the one for `close_tab` from another tab). It also passes 33 of 33 on both versions on the merge of this head with the newest main (e4c56fd), which merges without a conflict, and `check.sh --hash-only` passes on that merge. - Integration runner with a stub `claude` first on PATH: 0.12.3 450 successes and v0.8.0 342, 0 failed, 0 errors (28 files). Under `TMPDIR="<dir>/odd tmp 50%"` the same: 450 and 342, 0 failed, and the cache folder stays untouched. - Journey `journey-diff-close-save.sh` in agent-tty with the scripted client (run on c219b15, before the merge): 4 of 4 runs pass (split and unified, existing and new file). Existing file: 24 checks. New file: 23. - Live probes through the plugin's own tool handlers (run on c219b15 against main c9e57d1, before the merge, which changes none of the PR's Lua), on 0.12.3 and v0.8.0, compared by scenario and check name (checks that fail on the head and pass on main: 0 in every set): | Probe set | checks | fail on head | fail on main | fail on head only | fixed on head | | --- | --- | --- | --- | --- | --- | | round-3 verifier | 432 | 97 | 101 | 0 | 4 | | gpt round-3 | 92 | 14 | 14 | 0 | 0 | | round-4 verifier | 106 | 21 | 29 | 0 | 8 | | round-5 verifier | 116 | 56 | 56 | 0 | 0 | | gpt round-5 novel | 46 | 8 | 8 | 0 | 0 | | other-tab | 18 | 12 | 12 | 0 | 0 | | Opus round-6 (copies, handlers, command-line window) | 84 | 20 | 24 | 0 | 2 | | gpt round-6 | 60 | 12 | 12 | 0 | 0 | The 8 fixed checks of the round-4 set are the `:split` copy cases (row 2 and 3), the 4 of the round-3 set are `:b <proposal>` (row 1) and the second diff (row 4), the 2 of the Opus set are the second diff. - Order matrices (handlers that switch focus, raise or open windows, before or after, for `BufEnter`, `BufWinEnter`, `WinNew`, `WinEnter`): gpt's 96 orders per version, Opus's 96 orders (48 per version) and my 40 orders (20 per version) give the same answer, in every field, on this head and on main. A focus probe with a user `BufWinEnter` and `BufEnter` handler that moves the focus to another tab gives the same answer on both. - Probes of the defects that review found in the earlier design (a `:split` copy with a lone user `:diffthis` that gets the file back with `:edit!`, `:e other` and `:e #`, and a window that got the file without a `BufWinEnter`): this head keeps the user's diff in all, as main does. - Mutation: 14 mutants of the new code (no call, no `diffoff`, no `'diff'` check, no partner check, partner in any tab, look in any tab, skip the original-side window, no catch of the refusal, no `nvim_win_call`, the partner must show another buffer, the call after the event, the call before the deletes, no buffer and window guard, `diffoff!`). 13 are caught by the spec on both versions: 12 as failed cases and the one without the guard as an "Invalid window id" error. The survivor is `diffoff!` for `diffoff`. It is equivalent: the check runs first, so no other window in the tab has `'diff'` on, and `diffoff!` acts only on windows with `'diff'` on. - `check.sh --hash-only`: covered.lock matches the code. `check.sh` (TLC, 89205 states, and 5269 for the live model): pass. ## Round limit The maintainer waived the review-round limit for one push (round 7): coder#494 (comment). A second waiver covers one merge-only push (round 8, this head): coder#494 (comment). The push replaces the earlier `BufWinEnter` waiter and its window records with the single look above and merges current main. There is no further push to this PR. A new security flaw, data loss or broken core behavior compared with main ends it as blocked. A third waiver covers one bounded merge-only push (round 9) with no Codex review, and declines the round-8 Codex P1 with a reason: coder#494 (comment). --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
…socket dir, run the static proofs in CI (coder#545) ## Why coder#506 and coder#508 left six loose ends in the `verify-claudecode-nvim` skill (their verifiers' findings N3, m1 to m5, F1 and F2, and an advisor lead): 1. The five `journey-*.sh` scripts and the dashboard command still ran `source "$VCN_STATE/run.env"` in the caller's shell. No check of the state dir covers that, so a stale or foreign `VCN_STATE` ran an unchecked file. Earlier attempts cut values out of printed text, and the coder#508 round-5 verifier showed that text is forgeable (a `VERIFY_OUT` with ` OUT=` in its name sent a journey's evidence to another folder). 2. The `doc-vars` and `no-bare-tools` self-checks had test gaps (indented and `~~~` fences, a comment that names `source run.env`, a self-test of the wrong pattern copy, `"${JQ_BIN:-jq}"`). 3. `att()` puts `NODE_DIR` in front of `PATH`, and nothing checked `NODE_DIR`. Neither static check ran in CI. 4. Deleting the `mkdir` in `att()` left both proofs green, though it decides whether `record` works on a run that an older `vcn.sh` started. 5. `SKILL.md` said the refusal proof takes about 60 s (it takes 2 to 11 minutes), and one sentence did not say that root is fine above the state dir. 6. `agent-tty` keeps its sockets under a fixed `/tmp/agent-tty` and never checks who owns it. ## What changed All changes are in `.agents/skills/verify-claudecode-nvim/` and `mise.toml`. 1. **`vcn.sh get [-z] <NAME>`** prints one value of `run.env` after the check that `load` makes of the state dir. It answers only a name that `run.env` holds (not `PATH`, not a name that is absent). `-z` ends the value with a NUL, so `IFS= read -r -d "" OUT < <(vcn.sh get -z OUT)` keeps a newline at the end of a path. The five journeys read `OUT` and `WORK` this way: nothing is sourced, no printed text is parsed, and an `OUT` that the caller exported cannot redirect them. `vcn.sh dashboard` replaces the dashboard recipe: it runs `agent-tty dashboard` through `att()`, so it gets the same `NODE_DIR`, `TMPDIR` and state check. 2. **Static proofs.** `scripts/proof-static.sh` holds `no-bare-tools` and `doc-vars` (moved out of the resolution proof, so its count drops by two), `no-source-run-env`, and (round 2) `doc-get-exact` and `doc-recipe`. It reads files only. `mise run check` runs it, so CI does. The gaps are closed: fences are three or more backticks or tildes with any indent and a closing fence of the same character (m1), a whole-line comment that names a source is not a source (m2), `units_missing` and the self-test call the same two pattern functions, and a sample block that sets a variable to a literal must still be flagged (m3), and `${JQ_BIN:-jq}`, `${SETSID_BIN=setsid}` and `${JQ_BIN-jq}` count as bare tools (m4). 3. **`NODE_DIR` guard.** `load` refuses a `run.env` whose `NODE_DIR` is empty, relative or has a colon, and `launch` refuses a `node` whose directory is one of those (no state dir is made). A `NODE_DIR` that is absolute but has lost its `node` is not refused, so `cleanup` still runs. When `load` refuses, its message now says how to stop the run by hand: stop its nvim and agent-tty processes, then `rm -rf` the state dir (round 2: `cleanup` refuses too, and the verifier asked what a user does then). 4. **`tmp-removed`** (F1): launch, remove `$VCN_STATE/tmp`, then `record` must exit 0 with a non-empty WebM and `tmp` back at mode 0700. 5. **`SKILL.md`**: the refusal proof's time is the measured one, and "no dir above it may be owned by another user" now says that root is fine. 6. **Socket root guard.** `launch` makes `/tmp/agent-tty` with mode 0700 when it is missing, and refuses one that is a symlink, is owned by another user or is group- or world-writable, before it makes anything. It also refuses a `/tmp` (or another dir above `/tmp/agent-tty`) that others can write and that has no sticky bit. ### Round 2 (after the verifiers of `397414f2`) | Finding | Change | | --- | --- | | gpt A, Opus F11: `features/README.md` read `OUT=$($V get OUT)`, which drops a newline at the end of the path | README and `SKILL.md` give `getz() { IFS= read -r -d '' "$1" < <($V get -z "$1"); }; getz OUT; getz WORK`. The full-run block of `SKILL.md` no longer exports `OUT`. New static cases `doc-get-exact` (no recipe reads a path of the run through `$(... get NAME)`) and `doc-recipe` (it runs the documented recipe on a hand-made `run.env` with a newline at the end of `OUT` and `WORK`, and a control shows that a command substitution cuts it) | | gpt B: `att-root-own-755` proved only that launch did not refuse | `att_root_case` now needs the next step to show: no `agent-tty` passes `agent-tty doctor` (the stand-in), the mode of `/tmp/agent-tty` after launch, and no refusal text. New cases `att-root-own-700` and `att-root-nosticky` (a 0700 dir under a world-writable `/tmp` with no sticky bit: launch refuses) | | Opus F3 to F6: the static scanner | A comment sets no variable and hides no source. A trailing comment that names a source is still reported (fails closed). `builtin source`, `command .`, `eval`, backticks and quotes count as sourcing. A span split over two lines is joined. `$((PORT+1))` is a use of `PORT`. `need_docs` fails when `SKILL.md` or `features/*.md` is missing. The header says what the scanner does not see | | Opus F7: nothing walked above `/tmp` | `att-root-nosticky` (above) | | Opus F8, F10, F12 | F8: kept both name checks of `get` (the regex and the `run.env` grep): without the regex, a name like `O.T` matches `OUT=` in the `grep` and then fails in `${!name}` with a bash error instead of a refusal, and `get-values` now holds `O.T`. F10: `SKILL.md` line 154. F12: kept as intended, and `SKILL.md` says so | | Opus F1, F2: this body | The time quote in the table below is the one in `SKILL.md` (it was 2 to 6 minutes, 149 s to 338 s, and a gate run then took 626 s), and each gate number names the head it was measured on | ## Threat model for item 6 `agent-tty` 0.5.0 puts sockets at `/tmp/agent-tty/<sha256(home)[:8]>/<sha256(session id)[:12]>` whatever `TMPDIR` says (`dist/storage/sessionPaths.js`). `dist/host/hostMain.js` makes the per-home dir with `mkdir` recursive and `chmod 0700`, and nothing checks who owns `/tmp/agent-tty`. The client connects to the path and accepts any answer that echoes the request id (`dist/host/rpcClient.js`). Who: another local user who owns `/tmp/agent-tty` (a squatter who made it first and left it writable) or who can rename entries in it. The owner of a directory can rename its entries even with the sticky bit. What: move the per-home dir away, make a new one with the same name, listen on a socket of the same file name (both names are readable with `ls`). Every later `agent-tty` call of the run then talks to the squatter: forged screens and forged `ok` answers, and the keys that `vcn.sh` sends. Not covered: the same uid, and root. `agent-tty` itself is not changed. I reproduced this on main in a `bwrap` user namespace that I own, with `/tmp/agent-tty` a host-root-owned 1777 dir (it shows as owned by nobody, and I can write in it): `agent-tty create` made its per-home dir and socket in it without complaint, and after the per-home dir was swapped `agent-tty snapshot` printed `FORGED-BY-SQUATTER`. No second account exists here, so the swap was done by my own uid, standing in for the owner of the root. That the owner may rename is POSIX, not something I tested. ## Evidence Before is main `a9f8d0e7` (the merge of coder#506). After is this branch. Every guard and every case below has a mutant that turns it red (the `-r` and `V=` parts of the documented recipe are the exceptions; see Not covered), and each mutant ran against the case that names it. The mutants and their uncut output are in the run's scratch dir. | Item | Before (main) | After | Case that holds it | Mutant that turns it red | | --- | --- | --- | --- | --- | | 1 `get`, journeys | Each of the five journeys sources the `run.env` of a state dir that others can write (`journey-state-probe.txt`: `run.env-sourced=YES` five times) | Each refuses (`refusing state dir ...: it is group- or world-writable`, `run.env-sourced=no`) | `get-state` (also `get` itself) | `get` placed before `load` (sources it): red. A journey that sources again: red here and in `no-source-run-env` | | 1 `get`, values | A caller parses printed text or sources | `get -z OUT` returns a `VERIFY_OUT` with a space, an equals sign, a tab, quotes and a newline (also at its end) exactly. `get PATH`, a name that `run.env` lacks and a bad name are refused | `get-values` | No name check: red. No name regex (only the `grep`): red on `O.T`. `-z` ends with a newline: red. No `-z` branch: red | | 1 stale `OUT` | A caller's exported `OUT` is replaced by the source | Same result with `get`: a journey run with `OUT` exported to a stale dir writes all its files into the `OUT` of its run, for a `VERIFY_OUT` with ` OUT=` in it, a newline in it and a newline at its end (and none in the stale dir or in the dir named inside `VERIFY_OUT`) | `journey-out` (real launches) | A journey that does not read `OUT`: red (3 of its files land in the stale and the other dir). A journey that reads it through `$(...)`: red for the newline at its end | | 1 docs recipe (round 2) | `features/README.md`: `OUT=$($V get OUT)` cuts a newline at the end of the path, so the next documented write fails | README and `SKILL.md` use `getz` (`get -z` and `read -d ''`) | `doc-get-exact`, `doc-recipe` | Old recipe back in the README: red. Line read: red. Command substitution in `SKILL.md`: red. No recipe in the README: red | | 1 dashboard | `source "$VCN_STATE/run.env"; PATH=... "$ATT_BIN" --home ... dashboard` in the caller's shell | `vcn.sh dashboard` renders the running session (`alpha`, `gamma` of `hello.txt`) in a `script(1)` terminal and leaves no process | `dashboard` | `cmd_dashboard` that does nothing: red | | 2 m1 to m4 | `doc-vars` passes an indented or `~~~` fence that expands `$SID`, a block whose comment names `source run.env`, a self-test of a copy of the pattern, and `${JQ_BIN:-jq}` | All four are caught | `doc-vars`, `no-bare-tools` in `proof-static.sh` | 21 mutants of the checks and of the docs and `vcn.sh` they read, all red (old fence rule, no comment strip, trailing comment, weak set patterns, a default in `vcn.sh`, an indented doc, a tilde doc, the dropped default pattern, no `if` or `!` in the source pattern, a second source in `vcn.sh`) | | 2 F3 to F6 (round 2) | A comment that names a source counted as one, or hid one. `builtin source`, `eval` and backticks were not seen. A span split over two lines was missed. `$((PORT+1))` was not a use. A missing doc passed | The 15 listed shapes are caught (`no-source-run-env`, `doc-vars`) | `proof-static.sh` (5 cases) | 15 mutants (`s1` to `s9c`, `d2` to `d5`), all red, and the control green | | 3 `NODE_DIR` | An empty `NODE_DIR` leaves an empty `PATH` entry: a `node` in the current dir ran as the interpreter of `agent-tty` (`nodedir-probe.txt`: `planted-node-ran=YES`) | `load` refuses an empty, relative or colon `NODE_DIR`, and `launch` refuses such a `node` before it makes a state dir. An absolute `NODE_DIR` without a node and a `run.env` without `NODE_DIR` are not refused, so `cleanup` runs. The refusal says how to stop the run by hand and keeps the state dir | `node-dir`, `node-relative`, and the `cleanup-no-agent-tty` and `jq-old-run` cases that hold the exceptions | Guard removed: red. Colon branch removed: red. Refusing a dir without a node: red. Checking an unset value: red. Early check in `launch` removed: red (and a leaked state dir). No hint: red. Hint names another dir: red | | 3 CI | The two static checks run only inside the resolution proof, which needs `agent-tty` and Chromium | `mise run check` runs `proof-static.sh`, so the `unit-tests` job does | `proof-static.sh` (5 cases) | n/a | | 4 F1 | Deleting the `mkdir` in `att()` keeps both proofs green | `record` on a run without `$VCN_STATE/tmp` makes it (0700) and writes a WebM | `tmp-removed` | `mkdir` removed: red (`record` exits 1) | | 5 F2, m2 | "It takes about 60 s"; "no dir above it may be owned by another user" | "2 to 11 minutes (149 s to 626 s measured, depending on the load of the host)"; "(root is fine)" | n/a | n/a | | 6 socket root | `agent-tty create` made its per-home dir in a foreign-owned 1777 `/tmp/agent-tty`, and after a swap `snapshot` printed `FORGED-BY-SQUATTER` (`att-root-repro-main.txt`, `att-root-swap-main.txt`) | Launch refuses a symlink, another user's dir, a group- or world-writable dir, and a non-sticky world-writable `/tmp` above it. It makes a missing one 0700 and accepts a 0700 or 0755 dir of the user, and then runs `agent-tty doctor` (round 2) | `att-root-foreign`, `-loose`, `-link`, `-made`, `-own-700`, `-own-755`, `-nosticky` (bwrap, a `/tmp` of their own) | Guard removed: 5 red (`-foreign`, `-loose`, `-link`, `-made`, `-nosticky`). `mkdir` removed: `-made` red. Refusing any mode but 0700 with another message: `-own-755` red (it must see `agent-tty doctor` run next). Refusing a 0700 dir: `-made` and `-own-700` red. Guard after `mktemp`: 4 red (`-foreign`, `-loose`, `-link`, `-nosticky`), and a state dir is left. Owner test removed: `-foreign` red. Walk above the dir removed: `-nosticky` red | ## Gate on the head of this branch The head is `98653693`: the docs commit that gives the slowest measured time of the refusal proof, on top of `2ae7ec86` (the merge of main, coder#549 and coder#532, over the docs commit `76088711`, the round-2 commit `30e86876` and the pushed round 1). `98653693` changes one line of `SKILL.md`. The runs below are on `2ae7ec86` unless a line says otherwise. The host ran at a load of 50 to 90 (other workspaces), so the times are slow ones. | Check | Result | | --- | --- | | `mise run clean && mise run all` | exit 0 in 1688 s. 1706 successes, 0 failures, 0 errors, 25 pending. `luacheck`: 0 warnings, 0 errors in 181 files. Its `check` task runs `proof-static.sh` | | `check.sh` of `verify-diff-lifecycle` | `== RESULT: PASS` | | `proof-launch-refusals.sh` | exit 0 in 626 s. 42 `PASS`. The three ACL cases print `SKIP` (no default ACL can be set here) | | `proof-agent-tty-resolution.sh` | exit 0 in 1381 s. `CASES 49`, no `SKIP`, no `FAIL` (the `bwrap` cases ran) | | `proof-static.sh`, `mise run format-check` | on `98653693` (the docs edit is in the tree for both): 5 cases `ok`, exit 0, and 359 files formatted with 0 changed, exit 0 | | The five journeys, six runs (`opendiff-accept`, `diff-close-save` for an existing and a new file, `new-tab-diff-mode`, `snacks-float-diff`, `snacks-float-reload`) | on `8df19bb1` (the first of the two merges of main in this round), `TMPDIR` a symlink of mine to a private dir: all six pass with launch 0, journey 0 and cleanup 0, and the state dir is under the resolved path. The snacks journeys ran on a snacks.nvim clone. The second merge changed only docs, the integration runner script and its spec, and no file the journeys read | | The same full gate on `8df19bb1` | `mise run all` exit 0 (1702 successes), `check.sh` PASS, refusals 42 `PASS` in 242 s, resolution `CASES 49` in 1069 s | | The same full gate on the round-2 commit `30e86876` | `mise run all` exit 0 (1702 successes), `check.sh` PASS, refusals 42 `PASS` in 149 s, resolution `CASES 49` in 722 s | | CI on `2ae7ec86` and on `98653693` | all 6 checks green on each (`conventional-title`, `unit-tests`, `covered-lock`, `integration-tests (stable)`, `integration-tests (v0.8.0)`, `Required`) | | Codex review and security review on `2ae7ec86` and on `98653693` | both clean on each ("Reviewed commit `98653693c2`"), 0 review threads | The slowest time of the refusal proof was 626 s, so `SKILL.md` now says "2 to 11 minutes (149 s to 626 s measured, depending on the load of the host)". That is the sentence in `SKILL.md` line 134. A verifier's run on `98653693` took 966 s (exit 0, 42 PASS), so that range is already exceeded. The follow-up PR fixes the sentence. ## Not covered - The 25 pending specs of `mise run all` need a real Neovim, so the Busted gate skips them. The `integration-tests` jobs of CI run them (`scripts/run_integration_tests_individually.sh`). This PR changes no Lua under `lua/`. - `proof-static.sh` does not see indented code blocks, `<pre>` blocks, a command held in a variable, or a source in a `.lua`, `.py` or `.mjs` file. - `proof-static.sh` also does not see a backslash-continued `source \`, a quoted `"source"`, `BASH_ENV` or `let`. `doc-get-exact` does not see `$(... get -z NAME)` or a line read. `doc-recipe` uses a value with no backslash, and the test supplies `V`. The follow-up PR fixes or lists these. - A failed `get` inside `getz` prints the refusal but leaves `OUT` or `WORK` empty, and a shell without `set -e` goes on. The follow-up PR makes `getz` fail loudly. - Only `launch` checks `/tmp/agent-tty`. A tmp cleaner that deletes it during a run is not seen by the later commands. - The same uid and root can still swap the socket dir. `agent-tty` itself is not changed, and nothing is filed upstream. That the owner of a directory can rename its entries is POSIX reasoning, not a live test with a second account. - A reader who sources `run.env` in a shell of their own is not stopped. `SKILL.md` says not to, and `no-source-run-env` checks the skill's own scripts and recipes. - The two snacks journeys ran on a snacks.nvim clone, as before: they need one. - `proof-static.sh` takes about a minute on a host at a load of 35. It is about 17 s of CPU (1.8 s user, 15.0 s sys), measured by a verifier on `98653693`. One mutant is equivalent, not green by accident: `mkdir -m 700` instead of `mkdir` in `ensure_att_root` changes nothing, because `vcn.sh` runs under `umask 077`. Refs coder#506 and coder#508. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
Why
The READMEs name the buffer that holds Claude's proposed text in two ways.
README.md:1144and:1148,ARCHITECTURE.mdand 8 of the 16 uses inlua/claudecode/README.mdsay "proposal buffer".lua/claudecode/README.mdand the new paragraph inREADME.md:254(from #546) also say "proposed buffer". The advisor of #546 asked for one name for one thing.The advisor of #547 named two small items in
ARCHITECTURE.md. This PR takes them too, after #546 and #547 landed.M.stop()with no condition. The realVimLeavePrecallback runsstoponly whenM.state.serveris set, and otherwise clears the @-mention queue (lua/claudecode/init.lua:514-525).write" and cites nothing for it.What changed
1. The rename. Nine words in two files. Each "proposed buffer" becomes "proposal buffer", and nothing else changes (a word-level diff shows 9 changes of "proposed" to "proposal" and no other change).
README.mdlua/claudecode/README.mdI read each changed sentence in its new form. All nine read correctly, for example "A write of the proposal buffer (
:w,:wq,:x,ZZ...)" and "Both layouts keep the proposal buffer's'bufhidden'athide". The other phrases in these two files already use "proposal" (proposal window,proposal lines,proposal buffers).proposed changesinREADME.mdis a different thing and stays.2. Resource Cleanup sketch (
ARCHITECTURE.md). The sketch now shows theif M.state.server then M.stop() else clear_mention_queue() endof the real callback, with itsClaudeCodeShutdowngroup. Every code line comes frominit.lua:514-525, except two: the label-- init.lua, M.setup (shortened)and the trailing comment onM.stop(), which the old sketch had and which describesM.stopcorrectly. The other comment line is the real comment atinit.lua:520. The lead-in "Automatic cleanup on shutdown:" had the same unconditional wording, so it now says that the autocmd thatsetupcreates runsstoponly when the server runs, and otherwise only clears the queue, with citations. This agrees with the Performance bullet ("runsstopwhen the server runs").3. I/O citation (
ARCHITECTURE.md). The write clause now cites[lua/claudecode/server/client.lua:write].writeislocal function write(client, data, callback)atclient.lua:81, and it callsclient.tcp_handle:writeat:84and:90.Scope
The two README files above and the two
ARCHITECTURE.mditems. Left unchanged on purpose:ARCHITECTURE.md. Not audited again here.lua/(9 uses of "proposed buffer" in lowercase, plusProposed buffer IDatdiff.lua:1187). They sit indiff.lua(8) andinit.lua(1). Both files are recorded incovered.lock, so a comment edit needs a new record, which is too much for a wording fix.fixtures/issue-218/README.md(2) andfixtures/issue-238/README.md(7). They describe repro scripts. The sample output infixtures/issue-238/README.md(proposed buffer bufhidden = "hide") is whatscripts/repro_issue_238.luaprints (:260and:279). A rename of the prose alone leaves two names in one README, and a rename of the output changes a script string. They stay as they are, because the fixture READMEs record what those scripts print.Blast Radius
Three Markdown files. Nothing loads them. A wrong sentence here misleads a reader and breaks no behavior.
Verification
main: exactly 9 changes of "proposed" to "proposal", no other change.grepfor "proposed buffer" inREADME.md,lua/claudecode/README.mdandARCHITECTURE.mdfinds 0 after the change.main(docs: correct the I/O and cleanup claims in ARCHITECTURE.md #547 and fix(diff): keep Neovim alive when a handler raises during a failed openDiff setup #538) was clean and added no "proposed buffer" to a README.mise run format-check: exit 0 (358 files, 0 changed).ARCHITECTURE.md: 809 entries (733 citations, 26 relative links and 50 test paths), 1 flagged. The flagged one is[plugin/claudecode.lua:pcall(require, "claudecode")], an expression thatmainalready cites. A negative control with three wrong symbols in the new citations flagged all three (bad 4, then 1 after the restore). The validator matches a plain name such aswriteonly as a whole word anywhere in the file, so I also confirmed the definitions ofwrite(client.lua:81) andclear_mention_queue(init.lua:150) withgrep.mise run all. The change is documentation only, and no test, script or CI file reads these files. CI runs the full suite on the pushed head.git merge-tree --write-treeof the head andorigin/mainis clean.Generated with
xum• Model:anthropic:claude-sonnet-5-5• Thinking:xhigh