Repository navigation
Register sweeps and reads refuse what they could not read: retire-rows, archive-rows, migrate-state-cells, the DORMANT class, next-free and add-row (#138 lanes-edit.sh items, #157 item 4b) - #230
Merged
Conversation
…scans, so a refusal in the scan commits nothing (#157 item 4b) migrate-state-cells, retire-rows and archive-rows called handle_preexisting right after taking the lock, before the scan, so a refusal in the scan (the seam's, "named twice", "nothing to migrate", "no RETIRED row") left a LANES(pre-existing@...) commit behind. The capture now runs at the head of each write, after every refusal and skip; the lock is still taken before the scan, and the capture changes no byte, so the line numbers read are the ones rewritten. New tests/test_register_scan_hardening_138.py: a throwaway register in a local bare repository, and the 4b cases for all three sweeps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… git cannot read refuses the sweep (#138 item 17) The scan's published-log probe was `git cat-file -e`, which fails for a missing or corrupt blob exactly as for a path that is not there. A log the tree names and git could not read was therefore "no published log", lane_log_events gave an empty stream, and the row was retired. `ls-tree` of that one path now says whether it is published; a listed log that `show` cannot read, or a tree that cannot be listed, refuses with 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ted (#138 item 9) log_files_named_ci was `ls ... 2>/dev/null | awk`, whose status is the awk's, so an unreadable log directory read as "no log of any case". The sweep's scan passed on it and the run then failed inside the write loop (exit 5, "append changed line count by 0"). The helper now returns 1 where the directory exists and cannot be listed, and retire_rows' scan refuses on that with 1 before anything is written. Its other callers ignore the status, as they did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ar file (#138 item 12) The sweep appends each RETIRED line with `>>`, which follows a link. A tracked symlink at a lane's canonical log path passed the scan, and the line was written into whatever file the link named, outside the repository, while the commit recorded only the link. The scan now refuses, with 2, a log path that is a symlink or exists and is not a regular file, before it is read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pt in this checkout's row (#138 item 8) The writer's uuid came from session_for, which reads $LANES_SESSION or the PUBLISHED row, and the documented call sets only LANES_LANE. A sweeping lane whose newest transcript was stamped here and not yet pushed therefore wrote an older id into every append-only RETIRED line. The uuid is now $LANES_SESSION, else the last id of this checkout's row for the writer, else the published row's; a checkout behind origin is refused, so the local row is never the older one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le (#138 item 10) The rows are appended to lanes/archive/LANES-retired.md with `>>`, which follows a link. A tracked symlink there passed the -f and cleanliness checks, the rows went into whatever file it named outside the repository, and the commit published the link and the rows' removal from the register, so the rows left the repository altogether. The act's preflight now refuses, with 2, an archive (or archive directory) that is a symlink and an archive path that exists and is not a regular file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…138 item 2) Where origin holds lanes/archive/LANES-retired.md and this checkout does not (outside a sparse checkout, or marked skip-worktree), the file is absent and in no dirty list, so the move created a NEW archive of only its own rows and removed them from the register; a commit of that would drop every earlier reservation, and git 2.43 refuses to stage the path, so the act failed (6) with the checkout half-written. The preflight now refuses with 2, naming the cause. Taken over the issue's other option, seeding the file from origin, because a seeded skip-worktree path still cannot be staged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ar file (#138 item 7) archive_text's last rung, and retired_identity_hits' read of this checkout's archive, asked `[ -f ]` alone, so a directory, a FIFO or a broken symlink at lanes/archive/LANES-retired.md read as no archive: next-free offered the positions it should reserve, and add-row would take one. Both now read through archive_local_text, which returns 1 for such a path as for a file that will not open; next-free and add-row already refuse on that 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ro-byte or dangling register refuses (#138 item 5) lane_position_rows counted the working-tree register only under `[ -s ]`. Where origin has no register, a zero-byte register (what a failed rewrite leaves) or a broken symlink was skipped, and next-free answered from its stdin rows and the archive alone. Any path that exists or is a link is now a source, so the existing "rendered empty" check refuses it. A directory was already refused, `-s` being true for one; no register at all is still silence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y cannot be listed (#138 item 4) lanes_rows fences the logs this checkout has and origin has not with one `ls -- "$LANES_LOG_DIR" 2>/dev/null`, so a directory that exists and cannot be listed gave an empty fence, and a lane whose only object log is local was classed DORMANT: hidden by default and offered to the sweep. The listing now tells a missing directory from a failed listing, says so on stderr, and leaves the class unset on failure, as it already does when the liveness read fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d not read (#138 item 11) DORMANT is read from a lane's absence from lane_row_facts. Where a published log exists and its `git show` fails, remote_log_events gave no lines for it (the parser's status is the pipeline's), so with a readable liveness read and state cell the row was classed DORMANT: hidden, and named by `lane-end --retire-dormant`'s preview. remote_log_events now names each log it could not read on stderr, which state_events keeps in its warnings file; lanes_rows reads those paths back, leaves those lanes unclassified and listed, and says so. Without a warnings file no row is classed dormant. The --index read is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of reading it as having no such row (#138 item 18) register_text answered 0 with an empty text (and cached it) where the published register exists and `git show` of it failed, and rows_named_ci ended in an awk over it whose status is the awk's; add-row then took the answer through `|| :`. A row a peer had published and this checkout had not pulled was therefore appended a second time and, wherever the rebase did not clash, pushed: two rows for one lane on origin. register_text now returns 1 there and caches nothing, rows_named_ci returns 1 when either half of its pipeline failed, and add-row refuses with 1. Every other caller of the two still reads only the text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efused before refusal 1 reads it Already fixed on main: refusal 1 still reads both rows' session ids through `|| :`, but managed_seam_refuse, asked for the same lane just before it under the same lock, reads the published register (lane_is_managed_owned's empty check) and this checkout's row with their status and refuses UNKNOWN with 1. The case pins that, with a live id only in the published row; with 4b it also shows the refusal commits nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the archive back; a docstring reads plainly (#138 item 2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 2 hours and 53 minutes by commenting |
Reviewer's GuideThe PR hardens register sweeps, lane classification, next-free, and add-row to fail closed whenever required register, archive, log, or Git reads are unreadable or structurally unsafe, while preserving successful peer-edit capture semantics; a comprehensive isolated regression suite proves each issue item. Sequence diagram for fail-closed register sweepsequenceDiagram
participant Operator
participant Sweep
participant Git
participant Register
participant Logs
participant Archive
Operator->>Sweep: retire_rows()
Sweep->>Git: acquire_lock()
Sweep->>Git: ls-tree published log path
Git-->>Sweep: path exists or read failure
alt required read fails or path is unsafe
Sweep-->>Operator: refuse, nothing written
else reads succeed
Sweep->>Register: register_text()
Sweep->>Logs: log_files_named_ci()
Sweep->>Archive: archive_local_text()
Sweep->>Git: handle_preexisting()
Sweep-->>Operator: commit and publish sweep
end
Flow diagram for fail-closed lane classificationflowchart TD
A[lanes_rows] --> B[Read published logs with remote_log_events]
B --> C{Log or log directory readable?}
C -- No --> D[Record unreadable log warning]
D --> E[Do not classify lane as DORMANT]
C -- Yes --> F[Read local log listing and state facts]
F --> G{All required reads succeeded?}
G -- No --> H[Leave lane unclassified]
G -- Yes --> I[Classify eligible lane as DORMANT]
Flow diagram for safe next-free and add-row readsflowchart TD
A[next-free or add-row] --> B[Inspect local register or archive path]
B --> C{Path exists and has expected type?}
C -- No --> D[Refuse or report unreadable source]
C -- Yes --> E[Read published register/archive]
E --> F{Git read succeeded?}
F -- No --> G[Refuse; do not treat as empty]
F -- Yes --> H[Use rows and retired identities]
H --> I[Offer position or add one row]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… blob git cannot read is a refusal and never this checkout's copy (#138 item 18, review of #230) register_text and lane_position_rows asked `cat-file -e` whether the register is published. That fails for a missing or corrupt blob exactly as for a path origin does not have, so register_text answered with this checkout's copy: add-row's duplicate check passed (a second repoX-5 row landed, or was committed and stopped only by the rebase, exit 3), next-free offered a position, and the seam called every lane legacy, so a sweep was published. The new register_published asks `ls-tree` of the path: 0 published, 8 absent, 1 the tree unreadable. register_text and lane_position_rows use it, and a listed register that `show` cannot read is a 1, which add-row, next-free and the seam already refuse on. Tests for both modes the review named (git refusing cat-file and show, and the blob deleted from the object store), for add-row, next-free and the seam. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… class, instead of finding nothing to do (#138 items 4 and 11, review of #230) With lanes/log unlistable (item 4), or with no warnings file to tell an unreadable published log from a readable one (item 11), lanes_rows classes no row DORMANT and says so on stderr. lane-end turned only the "session records could not be read" sentence into a refusal, so it counted zero rows and exited 8, "there is nothing to do". Both notes now carry "no row is classed DORMANT here", and lane-end refuses on it with 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s: UNPRIVILEGED_ONLY Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…be read, so the sweep refuses instead of finding nothing to do (re-review of #230) lanes_rows built its rows from register_text with the status thrown away. Since c78eb92 register_text answers 1 with no text for a published register git cannot read, so those rows vanished from `lanes --closed`, and `lane-end --retire-dormant` counted none of them and exited 8, "nothing to do". The listing now checks register_text once, says the register could not be read and that no row is classed DORMANT here, and withholds the class; lane-end refuses on that sentence with 1. lane-end's refusal now names a failed read generally, not only the object logs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ive blob git cannot read reissues no retired position (re-review of #230) archive_text asked `cat-file -e`, the probe c78eb92 took out of register_text. A published archive git could not read (refused, or its blob gone) was therefore "not published", this checkout's copy answered (absent, on a checkout that had not pulled it), next-free offered the retired repoX-4 and add-row committed it locally. register_published now takes a path, and archive_text asks it of the archive: 0 shows it and a failed show is 1, 8 reads the local copy, 1 is 1. That makes the manual's "a published archive that cannot be rendered is a REFUSAL" true for this case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 18, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an-hardening-138 The one conflict is register_text, now #221's register_text_checked. It keeps #221's shape: a text and a status, a failed read printed and never cached, and register_text as the same read with the status dropped. The published-or-not question inside it is #230's register_published, the tree's answer (0 show, 8 this checkout's copy, 1 a refusal), not `cat-file -e`. The three #230 readers that relied on register_text's status now read register_text_checked, as #230's body planned: rows_named_ci (add-row's refusal), lanes_rows' one check (the withheld DORMANT class) and lane_position_rows (next-free). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This was referenced Oct 18, 2026
This was referenced Oct 19, 2026
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.



Refs #138 (items 1, 2, 4, 5, 7, 8, 9, 10, 11, 12, 17, 18 — the
lanes-edit.shitems)Refs #157 (item 4b)
The register sweeps (
retire-rows,archive-rows,migrate-state-cells) and the reads behind the DORMANT class,next-freeandadd-rownow refuse, or decline to classify, where a read failed or a path is not what it looks like. Before, each of those cases was read as "nothing there". Every fixed item was re-proved onmainfirst, against a throwaway register in a local bare repository. Each one has a proving test in the newtests/test_register_scan_hardening_138.pythat fails on the code before it and passes here. The branch carriesmainup to babd9fe as normal merge commits.Each commit carries one item. Line ranges are on this branch's head.
LANES(pre-existing@…)commit: HEAD moved by 1 forretire-rows("named twice"),archive-rows("no RETIRED row") andmigrate-state-cells("nothing to migrate")migrate_state_cells10845–10851 and 11100–11112;retire_rows11639–11641 and 11868–11870;archive_rows11943 and 12080–12083:handle_preexistingmoves from before the scan to the head of each write. The lock is still taken before the scan.test_a_refusal_in_the_scan_commits_no_peer_edit[×3],test_a_sweep_that_writes_still_captures_the_peer_edit_as_its_own_commitshow; see 18 for an unreadable blobshowfailing, the sweep exits 1 ("is UNKNOWN") before refusal 1 runs, becausemanaged_seam_refuse(from #97) reads the published register and this checkout's row with their status. With the blob unreadable, the probe read this checkout's copy instead and the sweep was published (exit 0); item 18'sregister_publishedcloses that.test_item_1_…(pins it);test_item_18_the_seam_refuses_…git addfailed with exit 6archive_rows11972–11985test_item_2_…dormantoncelanes/logcould not be listed; andlane-end --retire-dormantthen said "nothing to do" (exit 8)lanes_rows7483–7492 and 7703–7713;lane-end723–728 (review of #230)test_item_4_…,test_item_4_the_sweep_refuses_…(both skipped as root)next-freeoffered a position over a zero-byte register and over a broken symlink; a directory was already refused, because-sis true for onelane_position_rows8011–8018test_item_5_…[zero bytes, broken symlink]next-freeoffered position 4 over a local archive that is a directory, a broken symlink or a FIFOarchive_textand the newarchive_local_text3315–3329;retired_identity_hits3340test_item_7_…[×3]retire_rows11612–11621test_item_8_…log_files_named_ci2621–2632;retire_rows11801–11804test_item_9_…(skipped as root)archive_rows11963–11971test_item_10_…git showcould not read was shownNO LOG/dormantand named inlane-end --retire-dormant's previewremote_log_events3087–3097;lanes_rows7384–7402 and 7703–7713;lane-end723–728 for the no-warnings-file casetest_item_11_…retire_rows11756–11763test_item_12_…git addfailed with exit 6retire_rows11720–11737:ls-treeof the path replacescat-file -etest_item_17_…repoX-5rows landed on origin. Where git refusedcat-file -eandshow, or the blob was deleted,register_textread this checkout's copy: add-row exited 0 (two rows) or 3 (the duplicate committed here),next-freeoffered a position, and the seam published a sweep. Re-review: the listing then dropped the rows andlane-end --retire-dormantsaid "nothing to do" (exit 8).archive_texthad the samecat-file -e, so a published archive git could not read reissued retiredrepoX-4(next-freeoffered it, add-row committed it).register_published [<path>]3260–3276 asksls-tree, nevercat-file -e;register_text3238–3251 (returns 1, caches nothing);archive_text3296–3310 (asks it of the archive);lane_position_rows8018–8030;lanes_rows7403–7412 (checksregister_textonce and withholds the class);rows_named_ci4670–4682 (keeps both statuses);add-row13866–13872test_item_18_…(×8: add-row under a refusedshow; add-row in both review modes;next-free; the seam; the withheld class; the archive in both modes)Two choices between the issue's options
$LANES_SESSION, else the last id of this checkout's row, else the published row's), not requiringLANES_SESSION. Requiring it would break the documentedLANES_LANE=<lane> … retire-rowscall. A checkout behind origin is refused anyway, so the local row is never the older of the two.Merged with #221 (babd9fe,
register_text_checked)#221 landed first. Merge 2765929 resolved the one conflict,
register_text.register_text_checkedis A register that could not be read no longer reads as a lane with no session ids, so the dead-lane --force check and duplicate-holder fail closed (#67) #221's text with one change: the published-or-not probe isregister_published(0 →show, 8 → this checkout'scat, 1 → a failed read), notcat-file -e. A register that could not be read no longer reads as a lane with no session ids, so the dead-lane --force check and duplicate-holder fail closed (#67) #221's shape stays: text plus status, a failed read printed and never cached.register_textstays A register that could not be read no longer reads as a lane with no session ids, so the dead-lane --force check and duplicate-holder fail closed (#67) #221'sregister_text_checked || :. No existing caller's own return code changes.register_text_checkeddirectly:rows_named_ci(add-row's refusal),lanes_rows' one check (the withheld DORMANT class) andlane_position_rows(next-free).#221's
session_ids_checkedis untouched.How this combines with #210 (
fix/wrapper-hardening-138)git merge-tree --write-treeof the head before the #221 merge with #210's ae1b37b was clean. The combinedlane-endblock has #210's capture, which is always made and refuses when it cannot be made or read back, followed by the two refusal arms: "session records could not be read", and this PR's "no row is classed DORMANT here".Scope and neighbours
lane-end --retire-dormantuses:lanes --closed --prefix, throughstate_events.--lane <name>and--indexreads are unchanged.register_text,rows_named_ciandlog_files_named_cinow return 1 on a failed read. Every existing caller other than the ones named above takes them through$( )or|| :, so their behaviour is unchanged.commit_push, rename publication,managed-projection,lane_trees_list,history --index. Lane 5's The listing never reads the row's current-state phrase, so a migrated PAUSED row shows as NO LOG (#82 round 3, suppressed) #89 works inlanes_rows~7500–7507 (main), and mylanes_rowshunks sit outside it.delete_lines/replace_lineand therr_pubclause; add-row's archive scan treats an unparseable archive row as a non-row, so a retired position can be reissued — and the obvious refusal would fire on the archive's own header (#93 round 6, filed rather than taken) #122 covers the archive-row awk inretired_identity_hits. Item 7 replaces only theri_localread just above that awk.docs/README-lanes.mdnames the new refusals in the sweep and archive sections.cat-file -eprobes andhave_remote_ref):remote_log_events(anls-treethat fails is still silent);log_files_named_cicallers (migrate_state_cells,ensure_log,log_path_ci), which do not read its new status.Tests
main'slanes-edit.sh, 18 failed, 1 passed; against 2d45849, 19 passed. The one that passed onmainis the guard that the capture still happens.LANES_*andTMUX*unset). The round's tests run against 2d45849'slanes-edit.shandlane-end: 5 failed, 1 passed. All five new cases fail: add-row in both review modes,next-free, the seam, andlane-end's refusal. The one that passes is round 1's item 18 test, already fixed at 2d45849. This file plustests/test_repo_hygiene.pyon a284f71: 213 passed.LANES_*andTMUX*unset). The round's tests against a284f71'slanes-edit.shandlane-end: 3 failed out of 3 (the withheld class, and the archive in both modes). This file plustests/test_repo_hygiene.pyon 19ecf54: 216 passed.tests/test_repo_hygiene.pyat 2765929: 216 passed. A register that could not be read no longer reads as a lane with no session ids, so the dead-lane --force check and duplicate-holder fail closed (#67) #221's own cases are intests/test_lane_helpers.sh, which runs whole in CI'stestsjob.tests(whole shell suite included),tests-no-submoduleandguard-launch-modeall passed. Re-review round, run 39116781592 at 19ecf54: all three passed. Round 1, run 39072846177 at 2d45849:tests,tests-no-submoduleandguard-launch-modeall passed. Review round, run 39112269636 at a284f71: all three passed. The Linuxtestsjob is the suite of record.🤖 Generated with Claude Code