Repository navigation
Conversation
# Conflicts: # CHANGELOG.md
`patch` stays a faithful GNU patch; `edit` is the new tool for an agent changing lines it has read (Amy, today, answering #375). It takes the `LINE:HASH` anchors `cat --hashline` prints and checks each hash before it writes, so an edit aimed at a line that changed fails instead of landing on the wrong text. The grammar came from a model panel. With no help text, four model families (DeepSeek, Gemini, GPT, Claude Sonnet) all wrote `edit FILE ANCHOR 'TEXT'`, pairs for a batch, a delete flag, and `--after`. Help text for exactly that shape, plus `A..B` ranges and `--before`, scored 24/24 with byte-identical answers across the four, so the heredoc, ed-script, and hunk candidates were not run. Anchors only, no str_replace mode: sed and patch cover substitution and diffs (Amy). edit config.toml 3:a8c7 'port = 9090' edit config.toml --delete 6:8d62..10:2325 edit config.toml --after 2:7f4b 'workers = 4' edit config.toml 12:5f5e 'enabled = true' 13:fbb6 'size = 512' Every anchor in one call refers to the file as read, so line numbers do not shift between the edits of a batch, and nothing is written unless all match. Edits touching the same line, and an insert anchored on a line the call removes, exit 2. At a gap, --after text from the line above comes before --before text from the line below. A stale anchor exits 1, writes nothing, and shows the line's current text without a usable anchor: in the panel, all four models took a fresh anchor straight from the error and overwrote someone else's change, so the message asks for a re-read (Amy). Success prints the changed lines with their new anchors, ready for the next edit, or one summary line past 40 lines naming `tail -n +START --hashline FILE | head -n COUNT`; -q prints nothing (Amy). The argv is bound verbatim and parsed in source order, so the word after an anchor is TEXT even when it starts with `-`. New lines take the file's own line ending, including inside multi-line TEXT, which fixes kaijutsu's CRLF bug, and the file keeps its final newline or lack of one. The write is an atomic replace (WriteMode::Replace via the new cas_replace) after a re-read compare against the trash gate's snapshot or the bytes edit planned on. A write landing between that re-read and the rename is not detected; LocalFs has no primitive that closes it. docs/editing-for-agents.md argued against an edit builtin; its section is rewritten with how each objection was answered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review (gpt-5.6-sol and deepseek) found five defects. Under set -o trash, edit compared the file against the trash snapshot instead of the bytes it planned on. The snapshot is read after the plan, so a write landing between the two matched the snapshot and was overwritten with a plan built from older text. edit now compares against the planned bytes and refuses when the snapshot differs from them. A trash test pins that edit snapshots like patch and sed -i. An empty last line in a file without a final newline panicked the report (it indexed a line str::lines does not return) and could vanish from the file. An empty last line only exists with a terminator after it, so the file gains one in that case; the report reads the planned lines instead of re-splitting the content. A file with mixed endings was rewritten with one terminator throughout, changing lines no anchor named. Untouched lines now keep their own ending; new lines take the file's first. Anchors were lowercased, so a configured hasher that prints uppercase could never match. They are compared exactly. The re-read and view commands edit suggests were not shell-safe: a path with a space split into two words, and a leading `-` read as a flag. They quote the path, after `--` when it starts with `-`, and tests run each suggested command as written. Also: edit joins the README builtin table, the trash and replace lists in LANGUAGE.md, the help fragments, EMBEDDING.md, limits.md, and the kernel doc comments that enumerate overwriters; json_sweep_tests has its case. A TEXT that is exactly --json or --help is taken by the kernel unless it follows `--`; limits.md says so and a test pins the `--` route. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tobert
added a commit
that referenced
this pull request
Oct 5, 2026
…#518) Stacked on #514 (atomic replace). Review after #514; GitHub retargets this to main when #514 merges. The `cas_replace` hunk in `tools/context.rs` is the same text as on #516 (`feat/edit`), so the two branches merge cleanly. Under `set -o trash`, `sed -i` and `patch` took the trash snapshot, then read the file again and transformed that second read. A write that landed between the two was transformed and replaced, while the trash held the older bytes, so the newer version had no copy anywhere. A mock trash that writes the file right after the snapshot reproduces it: both builtins exited 0. Both now start from the snapshot bytes when there is one (`read_for_replace`), and replace through `cas_replace` with what they read as the expectation. A change after the snapshot is a conflict, and the racer's bytes stay on disk. A file named twice, or a multi-group patch on one target, starts each pass from what this command last wrote there. ``` $ sed -i 's/old/new/' f.txt sed: f.txt: invalid operation: /work/f.txt: changed since kaish read it, so it was not overwritten; read it again and retry ``` OverlayFs resolved symlinks one layer at a time. A read followed a link inside the upper, and on NotFound read the same path from the lower, where the lower followed it. A write did not follow links at all. Two kaibo reviews (gpt, deepseek) traced what this did once writes started following links and reads did not: under trash, `sed -i` through a lower link to a copied-up file snapshotted and compared the lower bytes while the replace landed on the upper copy, and the version it replaced was lost. Other effects: an upper link to a lower-only file could not be read, a link to a removed file still read the removed bytes, `ls` through a directory link showed removed files, and `mkdir`/`symlink` through a lower directory link made an upper directory that hid the link. OverlayFs now has one resolver, run with the state lock held. It walks the path one component at a time, looks each component up with whiteouts first, then the upper, then the lower, and follows a link in either layer from there. Every operation goes through it: read, read_range, stat, list, set_mtime, write, replace, and mkdir follow the last component; lstat, read_link, remove, and symlink keep it; canonicalize uses it. A `..` from a link target that climbs past the root is refused with PermissionDenied, "path escapes root", as LocalFs refuses it; an absolute lower target is mapped by the lower's own canonicalize. Other fixes in the same code: - `remove` of a copied-up file dropped its base, so the delete never reached `changes()` or `kaish-vfs commit`, and the base stayed charged to the budget. It now keeps a `Some` base. - `commit_into` checked an Added path with `exists()`, which follows a dangling link; commit then wrote through the link. It now uses `lstat`. - The trash decision read any `exists()` error as "new file" and any `stat()` error as size 0. One `stat` now decides both, and an error other than NotFound fails the command. - The conflict error named an internal mechanism ("write-model gate"). It now names the fix. - The `RecordingFs`/`GhostFs` test doubles in cat, cmp, wc, and find forward `replace`. - New LocalFs test: overlay commit replaces the file (new inode, an open handle still reads the old bytes). Switching commit back to `write` fails it. `test_remove_symlink_write_is_modified_with_original_base` pinned the old result (writing through a dangling link replaced the link with a file). It now pins the link surviving, its target created, and `changes()` refusing. Rules now in force: a read-modify-write transforms exactly the bytes the trash holds and replaces only if the file still holds them. Every OverlayFs operation names the same file for a path, and it is the file a real filesystem would name. Known windows left, recorded for a later batch: `cas_write` is still a re-read then a replace, two backend calls; a write between them is not caught until a backend offers compare-and-replace as one step. `commit_into` has the same gap between its preflight and write phases. The `cas_overwrite` doc now says so. Tests are failing-first commits: 2 kernel race cases, 2 kernel overlay cases, 12 OverlayFs unit cases. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #514 (atomic replace) and #515 (
--hashline); merge those first and this diff shrinks toedititself. Answers #375:patchstays a faithful GNUpatch, andeditis the new tool for an agent changing lines it has read.The grammar came from a model panel. With no help text, four model families (DeepSeek, Gemini, GPT, Claude Sonnet) all wrote
edit FILE ANCHOR 'TEXT', pairs for a batch, a delete flag, and--after. Help text for exactly that shape, plusA..Branges and--before, scored 24/24 with byte-identical answers across the four, so the heredoc, ed-script, and hunk candidates were not run. Anchors only, no str_replace mode:sedandpatchalready cover substitution and diffs.Every anchor in one call refers to the file as read, so line numbers do not shift between the edits of a batch, and nothing is written unless every anchor matches. Edits that touch the same line, or an insert anchored on a line the call removes, exit 2.
A stale anchor exits 1, writes nothing, and shows the line's current text without a usable anchor. In the panel, all four models took a fresh anchor straight from the error and overwrote someone else's change, so the message asks for a re-read instead. Success prints the changed lines with their new anchors, ready for the next edit; past 40 lines it prints one summary naming
tail -n +START --hashline FILE | head -n COUNT, and-qprints nothing.The argv is bound verbatim and parsed in source order, so the word after an anchor is TEXT even when it starts with
-. A TEXT of exactly--jsonor--helpis taken by the kernel unless it follows--(limits.md). Untouched lines keep their own line endings; new lines take the file's first, which fixes kaijutsu's CRLF bug. The file keeps its final newline or lack of one, except that an empty last line needs a terminator to exist.The write is an atomic replace (
WriteMode::Replacevia the newcas_replace) after a re-read compared against the bytes the plan was built on. Underset -o trashthe snapshot must equal those bytes too. A write landing between the final re-read and the rename is not detected; LocalFs has no primitive that closes that window.Review (gpt-5.6-sol and deepseek via kaibo) found five defects, fixed in the last commit: the trash snapshot was used as the compare-and-set expectation (a race window), an empty last line in a file without a final newline panicked the report, mixed line endings were normalized, anchors were lowercased (breaking an uppercase custom hasher), and the suggested commands were not shell-safe for paths with spaces or a leading
-. Each has a test. deepseek also listed every doc and comment that enumerates overwriters;editis in all of them now.docs/editing-for-agents.mdargued against aneditbuiltin; that section is rewritten with how each objection was answered.Before kaijutsu adopts this, it needs to drop its own
editalias for vi, which would shadow the builtin.Gates:
cargo test --all, kernel--features subprocess,localfs, clippy--all-targets, rustdoc-D warnings, wasm32-wasip1 build. cargo-insta is not installed here; no pending snapshots.🤖 Generated with Claude Code