Skip to content

A lane listing that is not whole no longer removes indexed worktree rows: the inventory read says PARTIAL, lanes --worktrees prints it NOT WHOLE, and lanes-index writes nothing over it; an older helper's tree with no path stays a tree (#173 item c1, #252 delta review 2) - #289

Merged
brettheap merged 9 commits into
mainfrom
fix/lanes-index-partial-listing-173
Oct 21, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Closes #173

This PR does #173's last open item, c1 (comment 6013909513, from Copilot's fourth review of #171), and adds the test that #252's delta review 2 asked for under #173's unread marker. The rest of #173 is already on main, from #219 (a7f09bd): the issue's own finding, by option 1 (the per-lane unread marker, so a first sync's unread lane and a held lane's kept rows both say NOT READ in the index), and items c2, c3 and c4. That leaves c1, so this PR closes the issue. It is lane openRepoTools-4's, under Brett Heap's 10-11 assignment of #90–#194, and it merges only on his word for this PR.

Fix round 1 answers the independent review at 03b6299 (LAND, no MUST). Its SHOULD-1 and SHOULD-2 and its N1 to N4 are done here, and the hung-indexer race it traced is fixed; its SHOULD-3 and SHOULD-4 are #290 and #291. See "Fix round 1" below.

Fix round 2 answers the delta review r2 of fix round 1, at ddf7f6e (LAND, no MUST). Its SHOULD-A, a lane's dir read again for each lane with trees, and its NIT-1 to NIT-3 are done here. Its SHOULD-B, a bash 3.2 exit in lanes --worktrees that main has too, is #292. See "Fix round 2" below.

The defect

worktree_rows lists the lanes through lanes_rows --all --closed and takes that listing as whole.

  • The listing's lanes are the names three reads give: the register's rows, the archive's rows and the object logs' file names.
  • Each lane's trees are found under the control root that its log's dir places.

lanes_rows is a listing a person reads before a launch, so it may not refuse. When one of those reads fails, it says so on stderr and lists what the other reads gave. For the object-log listing it does not even say so, because known_lanes answers 0 either way.

The inventory took that partial listing as whole. lanes-index sync --source inventory then removed the rows of every lane the listing could not name.

Each row of the table below was reproduced at 21e0829 against the test fixture's seed. In each case, a lane with a tree is named by one read alone, and that read's published object is made unreadable:

read that failed lane that read alone names what the helper said what the sync did at 21e0829
the archive (c1) repoZ-1, whose only row is the archive's a note on stderr; exit 0 without the lane removed its row
the register (the inventory's suspected sibling, which is real) repoC-1, a row with no log a note on stderr removed its row
the object-log listing (found here) repoB-1, a log with no row nothing removed its row
all three every lane exit 8, "none" 0 upserted, 8 removed: the table emptied
one published log (found here) repoB-1, whose dir only its log records only the log's own warning the control root fell from rung 2 to rung 3, and the row was removed

The design

  • The read says what it could not read.
    • For the whole listing only, worktree_rows now asks each read the listing rests on:
      • the register, the archive and the list of the object logs, before the listing is made. Where one of them reads, the listing reads that very text: the register out of its cache, the archive and the log list pinned for that one listing. So a read of them that fails after the check cannot thin the listing (fix round 1). The events list the logs once more, and that list is asked as the last item below says (worded in fix round 2);
      • with no origin, the log directory, which must be a directory this account can list and search;
      • every published log, and the events' own list of the logs, out of which every lane's dir comes. Both are taken from the warnings their read left, and the events are read once per process, so this is the read the listing used. Where those warnings could not be kept, whether a log failed is not known, and the read says so. Each lane's dir is read there once: its control root, its trees and its lifecycle snapshot are all found under that dir, and its log is not read again for them (fix round 2).
    • If any of them failed, one more line is printed beside the rows it did reach: PARTIAL, the workstation, and why.
    • An empty listing over a failed read is that line alone, and never exit 8.
    • A read of one NAMED lane goes by its name, which no listing gives, and never prints PARTIAL.
  • lanes --worktrees prints it. The line shows (every lane) in the LANE column and lane listing NOT WHOLE: followed by the reason. A footer says that a lane missing from such a listing is NOT a lane with no worktrees.
  • The indexer writes nothing over it.
    • collect_inventory refuses a read that carries a PARTIAL line.
    • sync and reconcile --source inventory, --dry-run included, exit 1 with unread: the lane listing ... was not whole - (why) - ... - nothing written, and status keeps that as the last error.
    • The table stays the last whole read, dated by its synced stamp. The first sync over a whole listing writes what moved in the meantime.
  • The --index read says so too (fix round 1, worded in fix round 2). Where this workstation recorded an error for a source, lanes --index --worktrees prints (last error: <UTC> <why>) after that source's synced <UTC>. It is the last error status shows: a sync or a reconcile of the source that refused or failed records it, a --dry-run included, and a sync or a reconcile that is not a dry run clears it when it succeeds. lanes-index export --worktrees carries the error as a fifth field of the source's meta line, and an older reader ignores that field. Another workstation's error is in its own status file, so a shared store's other sources never show one.

Where this departs from the design it started from, and why

It refuses rather than holds. The design I was given, and the announcement on #173, held every stored lane missing from the read and wrote the rest. #219's marker exists so that a held lane does not read as current, so holding by listing would have needed four more pieces:

Refusing follows rules the code already has:

  • collect_inventory already refuses a listing it could not read at all (a helper exit other than 0 or 8);
  • next-free refuses a position over this same partial read.

A read that is not whole is the same failure, in part. The register, the archive and the logs belong to the workstation's one register checkout, so a failed read of them is never just one lane's problem.

It is a new line kind, not a lane-less UNREAD. An indexer older than this PR reads any four-field UNREAD as a lane. It would hold a lane named "", give it a marker, and still remove every other lane missing from the read. The same older indexer refuses a line kind it does not know ("a line this indexer does not read ... is the helper on PATH another version?"), so it writes nothing over a newer helper's partial read.

Fix round 1

The independent review at 03b6299 found no MUST. This round is two commits on 03b6299: ca6b1b0, the tests alone on 03b6299's code, and d26658d, the fix. Then cef454e merges main a42946e. After CI's first run, two more:

  • ecfc523 moves a 14-line test setup that the two log tests shared into one helper, _a_lane_found_through_its_log. SonarCloud's gate had counted it as duplicated new code: 4.9% at cef454e, where 3% is allowed, and 0 at ddf7f6e. Neither test's assertions change.
  • ddf7f6e updates two comments in lanes-edit.sh that quote the --index read line, for N2.
finding what this round does
SHOULD-1: the archive and log-list checks were reads separate from the listing's own (probes P2 and P11) (b) the archive and the log list are pinned for the listing (PIN_ARCHIVE, PIN_LOGS), so archive_text and known_lanes hand back the text the check read instead of reading again. (a) remote_log_events notes a list of the logs it could not make (unlisted logs: ...), and worktree_rows reads that note back, so a failure of the read every dir comes from is never silent. That did not make each dir the one used: a lane with trees still had its log read again, for its trees and its snapshot, until fix round 2 (review r2's SHOULD-A). (c) docs/README-lanes.md says what is still read twice.
SHOULD-2: no test pinned the register's check before the listing (mutant mR) a git shim fails only the first show of the register, and mR is now killed
N1: with no origin, ls listed an r-- log directory (probe P12) the directory is asked with -d, -r and -x, as the trees' directory is
N2: the --index read said nothing of a refused sync (probe P9) its line names this workstation's last sync error
N3: "No exit code changes" was wrong corrected under "Docs"
N4: no test of reconcile --dry-run over a partial listing the four test_a_lane_listing_that_is_not_whole_removes_no_row cases run it: exit 1, the reason, and no removals
the hung-indexer race at tests/test_lanes_index.py:631 it waits for .argv (:633), as its sibling at :2155 does, both lines at ddf7f6e

Why a pin and not a cache for the process. The review suggested caching the archive and the log list per process, as the register is cached. A cache for the process must be dropped wherever the ref moves, and the register's is dropped in three places: a fetch, an --index read and a write's re-read. The archive has one more reader that needs it fresh. add-row rescans the archive after its rebase (retired_identity_hits) to catch a position retired in the meantime, and a stale cache there would offer that position again. The pin is set by worktree_rows from its own check and dropped as soon as the listing is made, so no other read sees it.

What is still read twice (now also in the docs):

  • The list of the logs: by the check, whose text the listing's names come from, and by the events, which say so where their list fails (added in fix round 2, review r2's NIT-3).
  • With no origin, the log directory is asked, then listed again for the names and again for the events. A log file in it that will not open leaves no warning naming it (A named worktree read answers "none" for a lane whose log or register could not be read (from #289's review) #290, item 4).
  • The events ask for themselves whether origin/<branch> is there. A rev-parse that failed in that read alone would read this checkout's logs in place of origin's.

Fix round 2

The delta review r2 of fix round 1, at ddf7f6e, found no MUST. This round is two commits on ddf7f6e: d223430, the tests alone on ddf7f6e's code, and f2723b1, the fix. Main is still a42946e, which cef454e already merged.

finding what this round does
SHOULD-A: a lane's dir was read again for each lane with trees, and a failure of that read removed the lane's rows (probe C1; also at 21e0829) lane_trees_list and lane_state_read take an optional second argument, lane_control_root's own payload, and worktree_rows hands all three reads the one payload it builds from the listing's dir
NIT-1: (its last sync failed: ...) also showed the error of a refused reconcile --dry-run the label is (last error: ...), the name status gives it, in the code, the README and this body. What a dry run records is unchanged
NIT-2: no test had a log-only lane that sorts last, so mutant mNoSentinel survived a test whose repoY-1, a lane with a log alone that sorts after repoM-2.md, must be named and indexed
NIT-3: "made ONCE" (README) and "never made" (this body) overstated both now say that the list of the logs is made twice
SHOULD-B: lanes --worktrees exits with no output under bash 3.2, on main too not here: #292

Every other caller keeps its behaviour. lane_state_of, lane_reconcile, and the lane-state and lane-trees verbs pass the lane alone. lane-start, lane-handoff, lane-end and the lifecycle verbs reach these functions only through those verbs. For each of them "${2-}" is the empty payload lane_control_root was handed before, so the root is found exactly as it was. No other file calls either function.

Read counts. A probe estate with no LANES_LANE_STATE_ROOT, whose log-only repoB-1 is found through its dir and has a snapshot there, counted the git calls of one whole worktrees --all:

read at ddf7f6e at f2723b1
ls-tree of lanes/log/ 4 2
show of repoB-1's log 3 1
show of any log (the estate has two) 4 2
show of the register 1 1
show of the archive 1 1

With LANES_LANE_STATE_ROOT set, the counts and the rows are the same at both heads. With only the second show of repoB-1's log failing:

  • at ddf7f6e, worktrees --all exits 8 with no PARTIAL;
  • at f2723b1, it exits 0 with repoB-1's tree, RUNNING at generation 1.

Through lanes-index sync, ddf7f6e shows that log three times and writes 0 upserted, 1 removed. The three are the events' read, lane_trees_list's read, which fails, and the indexer's own lane-state read of the lane's generation. f2723b1 shows it once and writes nothing. Bash 3.2.57 gives the same counts and rows.

Tests

The tests went in first, in their own commit d2c26ae, whose code is 21e0829's.

test at d2c26ae (21e0829's code) at 03b6299
test_a_lane_listing_that_is_not_whole_removes_no_row[archive] FAILS on the kept-row assertion: 1 upserted, 1 removed, repoZ-1's row gone passes
test_a_lane_listing_that_is_not_whole_removes_no_row[register] FAILS: 4 upserted, 1 removed, repoC-1's row gone passes
test_a_lane_listing_that_is_not_whole_removes_no_row[log-listing] FAILS: 1 upserted, 1 removed, repoB-1's row gone passes
test_a_lane_listing_that_is_not_whole_removes_no_row[all-three] FAILS: 0 upserted, 8 removed, the rows are {} passes
test_a_lane_whose_log_could_not_be_read_keeps_its_rows FAILS: 0 upserted, 1 removed passes
test_a_listing_whose_log_warnings_could_not_be_kept_is_not_whole FAILS: no PARTIAL line ([] == [['PARTIAL', ...]]) passes
test_an_unlistable_log_directory_with_no_origin_is_a_partial_listing FAILS: 0 upserted, 1 removed passes
test_an_older_helpers_tree_with_no_path_and_a_markers_branch_is_a_tree passes: at 21e0829 a marker is its key passes; FAILS under mC

Counts:

  • Red, tests/run.sh -k test_lanes_index at d2c26ae: 8 failed, 82 passed. Seven of the failures are the c1 cases above, each on its assertion and none on a crash. The eighth, test_a_hung_indexer_delays_no_writer_beyond_the_fork, is an existing race in that test's stub, fixed in fix round 1 (see "Fix round 1"). It passed in the green run, and also under mC and mC-whole, where the whole module ran.
  • Green, tests/run.sh -k "test_lanes_index or test_repo_hygiene" at 03b6299: 325 passed.
  • CI, workflow_dispatch run 39342747260 on 03b6299: tests, tests-no-submodule and guard-launch-mode all passed.

How a case makes a read fail. It moves the object's loose file aside in the clone, so a git show or git ls-tree that needs it fails, as on a damaged clone or a partial clone that cannot fetch it. Only the read under test fails.

What each c1 case checks. Besides keeping every row, each case checks that:

  • the whole store is byte-for-byte unchanged, and a change to a listed lane made in the meantime is not written either;
  • the refusal names the exact reason, and status and reconcile say the same;
  • lanes --worktrees prints the NOT WHOLE line and its footer;
  • a named lane's read is not partial;
  • the first sync over a whole listing writes the change, after which the --index read equals the source read again.

Fix round 1's tests

The tests went in first, in ca6b1b0, whose code is 03b6299's.

test at ca6b1b0 (03b6299's code) at d26658d
test_the_listing_names_lanes_from_the_archive_it_checked (P2: the shim fails every archive show after the first) FAILS on the kept-row assertion: 0 upserted, 1 removed, repoZ-1's row gone passes: the listing read the archive once
test_a_log_list_that_fails_after_the_check_removes_no_row[the-next-read] (the shim fails the second ls-tree of lanes/log/) FAILS: 0 upserted, 1 removed. That second list was known_lanes', so repoB-1 went unnamed passes: the second list is now the events', which says so, and the sync refuses
test_a_log_list_that_fails_after_the_check_removes_no_row[every-later-read] (the shim fails every ls-tree of lanes/log/ after the first: at 03b6299, P11's read and the one before it) FAILS: 0 upserted, 1 removed passes, the same way
test_the_register_is_asked_before_the_listing (P3: the shim fails only the first register show) passes: the check is that first read passes; FAILS under mR
test_an_unlistable_log_directory_with_no_origin_is_a_partial_listing[mode-400] (P12) FAILS: 0 upserted, 1 removed passes
test_the_index_read_says_its_last_sync_failed (N2) FAILS: the line is only synced <UTC> passes
the four test_a_lane_listing_that_is_not_whole_removes_no_row cases, now with N4's dry run, and [mode-000] pass pass

In the two log-list cases no override names a control root, so repoB-1's trees are found through its log's dir alone, as in P11.

Counts:

  • Red, tests/run.sh -k test_lanes_index at ca6b1b0: 5 failed, 91 passed. The five are the FAILS above, each on its kept-row or read-line assertion and none on a crash.
  • Green, tests/run.sh -k "test_lanes_index or test_repo_hygiene or test_register_scan_hardening_138" at d26658d: 358 passed. Again at ddf7f6e, after the refactor, -k "test_lanes_index or test_repo_hygiene": 333 passed.
  • CI: the pull_request run 39467365169 on ddf7f6e passed tests, tests-no-submodule, package and guard-launch-mode, the two test jobs on the merge of ddf7f6e into main a42946e, and SonarCloud's quality gate passed.

How a round-1 case makes a read fail. For that one call, a git shim on PATH counts the calls whose arguments carry the read's text. It fails, with exit 128, only the counted calls the case names, and every other call is the real git. _unreadable cannot do this, because it fails every read of the object. A probe at each head counted the calls a sync makes: the archive was shown 5 times at 03b6299 and once at d26658d, and the log list was listed 3 times and then twice. Under bash 3.2.57 (the musl build) the fix answered both probes as it does under 5.2.

Fix round 2's tests

The tests went in first, in d223430, whose code is ddf7f6e's.

test at d223430 (ddf7f6e's code) at f2723b1
test_a_lanes_log_that_fails_after_the_listing_removes_no_row (C1: repoB-1 found through its log's dir alone, with a snapshot there; the shim fails only the second show of its log) FAILS on the kept-row assertion: 0 upserted, 1 removed, repoB-1's row gone passes: the listing's show is the only one, and the sync writes nothing
test_the_listing_names_the_lane_whose_log_sorts_last (NIT-2: repoY-1 has a log alone, and it sorts last) passes passes; FAILS under mNoSentinel
test_the_index_read_says_its_last_sync_failed (N2, now looking for (last error: ...)) FAILS: the line says (its last sync failed: ...) passes

Besides the kept row, the C1 test checks that the store is byte-for-byte unchanged and that no lane is held. A snapshot read under the wrong root reads as generation 0, and a sync holds the lane's rows for that rather than write them. With no snapshot, both roots give the same answer, so the test gives repoB-1 one.

Counts:

  • Red, tests/run.sh -k "test_lanes_index and (fails_after_the_listing or sorts_last or last_sync_failed)" at d223430: 2 failed, 1 passed. Each fails on the assertion above, and neither on a crash.
  • Green, tests/run.sh -k "test_lanes_index or test_repo_hygiene" at f2723b1: 335 passed, which is round 1's 333 and the two new tests.
  • Bash 3.2.57 (the musl build first on PATH), -k "test_lanes_index and (fails_after_the_listing or sorts_last or last_sync_failed or log_list_that_fails or log_could_not or not_whole)" at f2723b1: 7 passed, 4 failed. The four are the not_whole c1 cases: their lanes --worktrees exits 1 with no output at Under macOS's bash 3.2, a failing command <utility> in a list or an if condition exits a set -e script: lanes --worktrees dies silently (lanes:317), and wip's .gitignore step can stop (openRepoTools:4014) #292's lanes:321, as the review found at ddf7f6e.
  • CI: the pull_request run 39507250560 on f2723b1 passed tests, tests-no-submodule, package and guard-launch-mode, the two test jobs on c594a3a, a merge of f2723b1 into main a42946e whose tree 6e6f2ae is the merge ref's, and SonarCloud's quality gate passed.

The mC test

test_an_older_helpers_tree_with_no_path_and_a_markers_branch_is_a_tree uses a wrapped LANES_EDIT as a stand-in for a helper older than #252. The wrapper adds one TREE line with an empty path and a branch that reads like the marker's. The test checks that the line:

  • is exported back as its TREE line, so the --index read is the source read;
  • is counted by status as a worktree;
  • is still one of its lane's kept rows when the lane is held.

It passes at 21e0829, where a marker is its key.

The mutant comes in two forms:

Mutants

Each mutant was applied alone to 03b6299. mC and mC-whole were run over the whole module (tests/run.sh -k test_lanes_index). The others were run over the new tests (-k "test_lanes_index and (not_whole or log_could_not or warnings_could_not or unlistable_log or older_helpers)"). Every mutant was killed.

mutant what it changes killed by
mC is_unread_marker takes 5bf29d9's rule (no path, and a branch beginning unreadable inventory () in place of the key the mC test, where the --index read is no longer the source read; and the existing test_a_first_sync_that_cannot_read_a_lane_says_not_read_in_the_index, because status has fetched only lane since #219. Whole module: 2 failed, 88 passed
mC-whole mC, plus cmd_status fetching path and branch as it did at 5bf29d9 the mC test alone, on that same assertion. Whole module: 1 failed, 89 passed
m1 worktree_rows does not ask the archive [archive] (repoZ-1's row removed); [all-three] (the reason lacks the archive)
m2 it does not ask the register [register] (repoC-1's row removed); [all-three]
m3a it does not ask the published object-log listing [log-listing] (repoB-1's row removed); [all-three]
m3b with no origin, it does not ask the log directory test_an_unlistable_log_directory_with_no_origin_is_a_partial_listing (row removed)
m4 a published log git could not read does not make the read partial test_a_lane_whose_log_could_not_be_read_keeps_its_rows (row removed)
m5 warnings that could not be kept are read as no warnings test_a_listing_whose_log_warnings_could_not_be_kept_is_not_whole (no PARTIAL line)
m6 a NAMED lane's read asks the listing's reads too the four test_a_lane_listing_that_is_not_whole_removes_no_row cases (NOT WHOLE in a named read)
m7 collect_inventory reads past a PARTIAL line the four cases, the log test and the no-origin test: 6, each a removed row
m8 the line is a lane-less UNREAD, still refused by the indexer the four cases (lanes --worktrees prints NOT READ, not NOT WHOLE) and the warnings test (the line's kind): 5
m9 an empty listing over a failed read is exit 8, "none", again [all-three] (every row removed)
m10 lanes prints no line for PARTIAL, as an older renderer would the four cases
m10b lanes prints no footer for it the four cases
m11 the log warnings are asked even where the listing found no lane [all-three] (its reason gains "not known")

Fix round 1's mutants were each applied alone to cef454e (the fix, merged with main) and run over -k "test_lanes_index and (names_lanes_from_the_archive or log_list_that_fails or register_is_asked or last_sync_failed or unlistable_log or not_whole or log_could_not or warnings_could_not)", 13 tests. Every one was killed, each by a removed row or the read's line:

mutant what it changes killed by
r1-a-note remote_log_events does not note a list of the logs it could not make both log-list cases (repoB-1's row removed)
r1-a-scan the note is made, and worktree_rows does not read it back both log-list cases
r1-b-archive-pin archive_text ignores the pin, so the listing reads the archive again the archive test (repoZ-1's row removed)
r1-b-logs-pin known_lanes ignores the pin, so the names come from a second ls-tree [the-next-read] (repoB-1's row removed)
mR (the review's) the register is asked AFTER the listing, with the reasons kept in order the register test (repoC-1's row removed)
r1-N1-ls with no origin, the log directory is asked with ls again [mode-400] (repoB-1's row removed)
r1-N2-render the --index read's line drops the fifth field the N2 test
r1-N2-export the export does not write the last error the N2 test

After ecfc523's refactor, r1-a-note, r1-a-scan and r1-b-logs-pin were run again at ddf7f6e, together with m4 rewritten for this round's code, over the two refactored tests (-k "test_lanes_index and (log_list_that_fails or log_could_not)"). Each was killed by the same tests as before, and m4 by test_a_lane_whose_log_could_not_be_read_keeps_its_rows.

Fix round 2's mutants were each applied alone to f2723b1. The two marked broad ran over the sixteen tests of rounds 1 and 2 (-k "test_lanes_index and (names_lanes_from_the_archive or log_list_that_fails or register_is_asked or last_sync_failed or unlistable_log or not_whole or log_could_not or warnings_could_not or hung_indexer or fails_after_the_listing or sorts_last)"), and the others over the one test the table names:

mutant what it changes result
m2-pass (broad) both reads work the root out again, as at ddf7f6e KILLED, 1 failed and 15 passed. Only the C1 test fails, at :2942, with 0 upserted, 1 removed and repoB-1's row gone
m2-pass-trees only lane_trees_list's call drops the payload KILLED by the C1 test at :2942: 0 upserted, 1 removed
m2-pass-state only lane_state_read's call drops it KILLED by the C1 test at :2944: the sync holds repoB-1, whose generation reads 0 against the 1 its rows were written at
m2-callee-trees lane_trees_list ignores the payload it is handed KILLED by the C1 test at :2942, as m2-pass-trees
m2-callee-state lane_state_read ignores it KILLED by the C1 test at :2944, as m2-pass-state
mNoSentinel (the review's, broad) the pinned list of the logs loses its last newline KILLED, 1 failed and 15 passed. Only the NIT-2 test fails, at :2968, with repoY-1 not indexed (5 upserted, 0 removed)
m2-N1-label the label is its last sync failed again KILLED by the N2 test at :3012

Lane 3's terms

Docs

  • docs/README-lanes.md, in the worktrees table section:
    • a paragraph, "A lane listing that is not whole writes nothing";
    • a paragraph on which reads the listing makes once and which twice: fix round 1's, which fix round 2 corrects (the list of the logs is made twice) and extends (a lane's dir is read once);
    • the read's NOT WHOLE line, and the --index read's (last error: ...).
  • Exit codes (corrected in fix round 1; the README said it right). worktrees --all still exits 0 with its rows, and the PARTIAL line is now among them. Where the failed reads left the listing EMPTY, it used to exit 8 ("none"), and it now exits 0 with the PARTIAL line alone. The indexer's refusal uses its existing exit 1.
  • The same is said in worktree_rows's comment, lanes --help and lanes-index's docstrings.

Not covered

Filed from the review: #290 (SHOULD-3 and the four items below that are marked with it) and #291 (SHOULD-4). All of them exist at 21e0829. From review r2: #292 (its SHOULD-B, on main too), under which the four not_whole cases fail on bash 3.2, at lanes:321 (:317 on main).

🤖 Generated with Claude Code

Summary by Sourcery

Protect inventory state from incomplete lane listings by reporting partial reads and refusing to write until a whole listing is available.

New Features:

  • Report partial lane listings explicitly and propagate their reasons through worktree, status, and indexed views.

Bug Fixes:

  • Prevent inventory sync and reconciliation from deleting or changing rows when the lane listing is incomplete, including failures involving the register, archive, log listing, log directory, or published logs.
  • Preserve consistent listing data across related reads so transient later read failures cannot silently omit lanes or trees.
  • Correctly classify legacy tree rows with empty paths as trees rather than unread markers.

Enhancements:

  • Display the last failed inventory synchronization alongside indexed source timestamps until a later successful write clears it.

Documentation:

  • Document partial-listing behavior, refusal semantics, indexed error reporting, and remaining read limitations.

Tests:

  • Add coverage for incomplete listings, unreadable logs and directories, pinned reads, failed sync reporting, legacy tree compatibility, and the associated race condition.

Brett Heap and others added 2 commits October 19, 2026 23:05
… and an older helper's tree with no path and a marker's branch stays a tree (#173 item c1, #252 delta review 2)

Item c1 of #173 (Copilot's fourth review of #171): `worktree_rows` takes
`lanes_rows --all --closed` as whole, and that listing only notes on stderr
a register or an archive it could not read - and says nothing at all of an
object-log listing it could not read. A lane named by the failed read alone
is then a lane with no trees, and `lanes-index sync --source inventory`
removes its rows. These tests are written before the fix and fail at
21e0829 on the assertion under test:

- `test_a_lane_listing_that_is_not_whole_removes_no_row`, four cases: the
  archive, the register (the inventory's suspected sibling, real: repoC-1's
  only row is the register's), the object-log listing, and all three - which
  left the listing empty, so the helper said "none" and the sync removed
  every row. A published object is made unreadable by moving its loose
  object aside in the clone.
- `test_a_lane_whose_log_could_not_be_read_keeps_its_rows`: a published log
  git cannot read gives the listing no `dir`, the control root falls from
  rung 2 to rung 3, and the lane's rows are removed.
- `test_a_listing_whose_log_warnings_could_not_be_kept_is_not_whole` and
  `test_an_unlistable_log_directory_with_no_origin_is_a_partial_listing`:
  whether a log failed is not known without the warnings file, and under
  `LANES_NO_GIT=1` the logs are named by listing the local directory.

And the SHOULD of #252's delta review 2:
`test_an_older_helpers_tree_with_no_path_and_a_markers_branch_is_a_tree`
feeds the reader, through a wrapped `LANES_EDIT` standing in for a helper
older than #252, a TREE line with an empty path and a branch that reads
like the unread marker's. It passes at 21e0829, where a marker is its key,
and fails under mutant mC, 5bf29d9's rule (no path and the marker's branch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ows: the inventory read says PARTIAL, lanes --worktrees prints it NOT WHOLE, and lanes-index writes nothing over it (#173 item c1)

`worktree_rows` read the lanes through `lanes_rows --all --closed`, whose
names come from the register's rows, the archive's rows and the object logs'
file names, and whose control roots come from each log's `dir`. A listing a
person reads may not refuse, so where one of those reads failed it noted it
on stderr - the log listing not even that - and listed the rest; the
inventory took that as whole and `lanes-index sync --source inventory`
removed the rows of every lane it could not name. With all three unread the
listing was empty and the read said 8, "none": every row was removed.

For the whole listing (never one named lane), `worktree_rows` now asks the
register (before the listing, so a read that succeeds is the cached text the
listing reads), the archive, the published log listing or, with no origin,
the log directory, and every published log (out of the warnings the listing's
own read of them left; where those could not be kept, whether one failed is
not known). Any failure is ONE `PARTIAL <workstation> <why>` line beside the
rows it did reach, and an empty listing over a failed read is that line
alone, never 8. `lanes --worktrees` prints it as the lane listing NOT WHOLE,
with a footer. `collect_inventory` refuses a read that carries it, so `sync`
and `reconcile --source inventory` write nothing, exit 1 and record why; the
table stays the last whole read, dated by its `synced` stamp.

Refused, not held: holding only the lanes it could not name needs a marker
for each, one for the listing (a first sync has no rows to hold) and a
one-lane `--index` read that knows of it, or the index again answers "no
worktree is inventoried" for a lane it never read. A new line kind, not a
lane-less `UNREAD`: an indexer older than this reads any four-field `UNREAD`
as a lane - it would hold a lane named "" and still remove the rest - and
refuses a line kind it does not know.

Lock lines (`acquire_lock`, `release_lock`, `lock_try`, `LOCK_HELD`)
unchanged; `lane-start` and `lane-handoff` untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 20, 2026

Copy link
Copy Markdown

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 1 day and 9 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 20, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR makes lane-listing completeness explicit, prevents partial inventory reads from deleting or modifying indexed rows, renders the condition to users, and adds broad regression coverage including legacy-helper compatibility.

Sequence diagram for refusing a partial lane inventory sync

sequenceDiagram
    participant Caller
    participant WorktreeRows as worktree_rows
    participant Register
    participant Archive
    participant Logs as ObjectLogs
    participant Inventory as lanes-index

    Caller->>WorktreeRows: worktree_rows()
    WorktreeRows->>Register: register_text_checked()
    WorktreeRows->>Archive: archive_text()
    WorktreeRows->>Logs: git ls-tree or ls log directory
    WorktreeRows->>WorktreeRows: lanes_rows --all --closed
    WorktreeRows-->>Inventory: rows plus PARTIAL workstation reason
    Inventory->>Inventory: collect_inventory()
    Inventory-->>Caller: exit 1, nothing written
Loading

Flow diagram for whole and partial lane listings

flowchart TD
    A[Read lane listing] --> B{All required reads succeeded?}
    B -->|Yes| C[Emit lane rows]
    C --> D[Sync inventory changes]
    B -->|No| E[Emit rows reached plus PARTIAL]
    E --> F[Keep last whole index unchanged]
    F --> G[Report NOT WHOLE to users]
Loading

File-Level Changes

Change Details Files
Detect and propagate incomplete lane listings instead of treating partial results as complete inventory state.
  • Re-check register, archive, log listing, and published-log readability for whole-list reads.
  • Emit a single PARTIAL record with specific failure or unknown-warning reasons, including empty-list cases.
  • Keep named-lane reads independent of whole-list partial status.
lanes-edit.sh
lanes
Prevent inventory mutations when the source listing is not whole and expose the condition consistently to users.
  • Reject PARTIAL input during inventory collection and make sync/reconcile exit without writes while preserving the last whole snapshot.
  • Surface the condition in status, worktree output, help text, and documentation with NOT WHOLE messaging and footer guidance.
  • Preserve compatibility by using a distinct PARTIAL record kind rather than a lane-less UNREAD marker.
lanes-index
lanes
docs/README-lanes.md
Add regression and compatibility coverage for partial reads and legacy helper output.
  • Test archive, register, log-listing, all-read failures, unreadable logs, lost warning capture, and unlistable no-origin log directories.
  • Verify rows, SQLite state, status, reconcile, named-lane reads, recovery, and NOT WHOLE rendering remain correct.
  • Verify an older helper's empty-path TREE record is retained as a tree rather than misclassified as an unread marker.
tests/test_lanes_index.py

Assessment against linked issues

Issue Objective Addressed Explanation
#173 Prevent a first sync from publishing usable index provenance that makes a lane with an unreadable trees directory appear to have no inventoried worktrees. ❌ The PR marks a listing PARTIAL when the reads used to discover lane names fail, and refuses to sync that listing. It does not address an individual lane whose trees directory is unreadable: that lane can still be reported as UNREAD while the overall listing is whole, and the PR does not withhold provenance in that case. Its new tests cover listing-source failures and an older helper's tree row, not the issue's first-sync unread-lane scenario.
#173 Make an index read distinguish previously stored worktree rows for a lane held because its inventory could not be read from rows freshly observed at the last sync. ❌ The PR prevents index writes when the lane listing itself is not whole, but it adds no per-lane held/unread marker or equivalent provenance for a lane whose trees inventory is unreadable. Consequently, an index read still cannot tell that lane's retained rows are held.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Brett Heap and others added 5 commits October 20, 2026 15:59
…hecked, a log list that fails after the check removes no row, the register is asked before the listing, an r-- log directory with no origin is not whole, a dry run refuses a partial listing, and the index read says its last sync failed (#173 item c1, the review of #289)

The independent review of #289 at 03b6299 found the checks and the
listing's reads to be separate reads (SHOULD-1, probes P2 and P11), no test
pinning the register's check before the listing (SHOULD-2, mutant mR), an
`ls` that lists an r-- log directory (N1), no word of a refused sync in the
`--index` read (N2) and no test of a dry run's refusal (N4). Each is a test
here, and the code is still 03b6299's:

- the archive readable for the check and not after it: repoZ-1's row is
  removed at 03b6299;
- the list of the logs failing on the read after the check, and on every
  read after it: repoB-1, found through its log's `dir` alone, loses its row;
- the register's first read failing and every later one reading: refused,
  as it already is at 03b6299 - the test that kills mR;
- the no-origin log directory at mode 400 beside mode 000;
- `reconcile --dry-run` over each partial listing exits 1;
- `lanes --index --worktrees` after a refused sync names its last error.

A `git` shim on PATH for one call fails only the counted calls a case
names, which `_unreadable` cannot do. And the hung-indexer test waits for
`.argv` as its sibling does: the stub writes `.stdin` first, so `.argv` could
be read before it was there (the race the review traced at d2c26ae).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vents say a list of the logs they could not make, an r-- log directory with no origin is not whole, and the index read names its last sync's error (#173 item c1, the review of #289)

The review of #289 at 03b6299 (SHOULD-1): `worktree_rows` asked the
archive and the list of the logs, and the listing then read both again - the
archive four more times, the list twice (`known_lanes` for the names,
`remote_log_events` for the events every `dir` comes from). A read that
failed only after the check thinned a listing taken as whole: repoZ-1's row,
or every row found through a log's `dir`, was removed (probes P2 and P11).

- (b) The check's reads are the listing's. Where the archive and the list
  of the logs read, `worktree_rows` pins their text (`PIN_ARCHIVE`,
  `PIN_LOGS`) for the listing alone and drops it after: `archive_text` and
  `known_lanes` hand it back instead of reading again. Not a cache kept for
  the process: `add-row` rescans the archive after its rebase, and must read
  it as it then stands.
- (a) `remote_log_events` says a list of the logs it could not make
  (`unlisted logs: ...`) instead of reading as no logs, and `worktree_rows`
  reads that line back out of the events' warnings, as it reads "unreadable
  log": the events are built once per process, so it speaks for the read
  every `dir` came from. The reason is the check's own, said once.
- (c) docs/README-lanes.md says each read is made once for the listing,
  and what is still read twice: with no origin, the log directory and its
  files; and the events' own question whether origin/<branch> is there.
- N1: with no origin, the log directory is asked with `-d`, `-r` and `-x`,
  as the trees' directory is: `ls` listed an r-- one whose files no glob
  could then open.
- N2: `lanes-index export --worktrees` writes this workstation's last sync
  error as a fifth field of a source's meta line, and `lanes --index
  --worktrees` prints it after `synced <UTC>` as `(its last sync failed:
  <UTC> <why>)`. An older reader ignores the field.

The tests are ca6b1b0's. No changed line names a lock function or
`LOCK_HELD`, and `lane-start` and `lane-handoff` are untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main moved from this branch's base 21e0829 by nine commits: lane-handoff's
lifecycle read (677acc0), #228's release series (c82578f to 234b670), #271
(f18b818), #284 (90eed74) and #265 (a42946e). Of this PR's files, only
lanes-edit.sh (lane_reconcile's leg sweep) and docs/README-lanes.md (the
triad section) changed there, away from its hunks, and the merge is clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…which SonarCloud's quality gate counted as 28 duplicated lines of new code (#289)

`test_a_log_list_that_fails_after_the_check_removes_no_row` took the
14-line setup of `test_a_lane_whose_log_could_not_be_read_keeps_its_rows`
whole: repoB-1 resumed in a checkout of its own, its tree under that
checkout's parent, and a sync with no override naming a control root.
SonarCloud's gate on #289 at cef454e failed on it (4.9% duplication on new
code, 3% allowed). The setup is now `_a_lane_found_through_its_log`, which
both tests call; neither test's assertions change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ame the last sync's error that the line now carries (the review of #289, N2)

The file's header and the `worktrees` dispatch both quote the stderr line
`read: index (<store>) at inventory:<ws> synced <UTC>`. Since d26658d it is
followed by `(its last sync failed: <UTC> <why>)` while this workstation's
last sync of that source wrote nothing, as docs/README-lanes.md says. Comments
only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brett Heap and others added 2 commits October 20, 2026 21:54
…s no row, the lane whose log sorts last is named, and the index read labels what it shows the last error (review r2 of #289, SHOULD-A, NIT-2 and NIT-1)

The delta review r2 of #289 at ddf7f6e found a lane's `dir` read again
for each lane with trees (SHOULD-A, probe C1). `lane_trees_list` and
`lane_state_read` work the control root out through `lane_control_root`
with no `dir`, which reads the lane's log a second and a third time, and a
`git show` that fails there is thrown away. The review also found no test
with a log-only lane that sorts last (NIT-2, mutant mNoSentinel), and an
`--index` read line saying "its last sync failed" where a refused
`reconcile --dry-run` records the error too (NIT-1). Each is a test here,
and the code is still ddf7f6e's:

- repoB-1 is found through its log's `dir` alone, with a lifecycle
  snapshot there, and synced. A `git` shim then fails only the second
  `show` of its log. At ddf7f6e that is `lane_trees_list`'s read: the root
  falls to `$PROJECTS_ROOT/.lane-state` and the sync removes the row.
- repoY-1, a lane with a log alone that sorts after repoM-2.md, is named
  by the listing and indexed. This passes at ddf7f6e, and it is the test
  that kills mNoSentinel.
- The N2 test looks for `(last error: <UTC> <why>)`, the name `status`
  gives what is recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, so a later read of the lane's log that fails removes no row, and the `--index` read calls what it shows the last error (review r2 of #289, SHOULD-A, NIT-1 and NIT-3)

Review r2 of #289 found the control root worked out three times for each
lane with trees (SHOULD-A, probe C1). `worktree_rows` hands
`lane_control_root` the listing's own `dir`, but `lane_trees_list` and
`lane_state_read` called it with the lane alone, and it read the lane's log
again for each. `lane_log_events` throws a failed `git show` away, so the
root fell to `$PROJECTS_ROOT/.lane-state` and `lane_trees_list` answered 8.
The lane read as one with no trees: `worktrees --all` said nothing, and
`lanes-index sync` removed its rows.

Both now take an optional second argument, `lane_control_root`'s own
payload. `worktree_rows` passes all three the one payload it builds from
the listing's `dir`. Every other caller passes the lane alone, and
`"${2-}"` hands `lane_control_root` the empty payload it had before. Those
callers are `lane_state_of`, `lane_reconcile` and the `lane-state` and
`lane-trees` verbs, and through them `lane-start`, `lane-handoff`,
`lane-end` and the lifecycle verbs.

With no `LANES_LANE_STATE_ROOT`, a whole `worktrees --all` now shows a
lane's log once instead of three times, and lists the logs twice instead
of four times (review r2's probe B0).

NIT-1: the `--index` read's fifth field is `status`'s `last error`, which
a refused `reconcile --dry-run` records too, so it is now labelled
`(last error: <UTC> <why>)`. What a dry run records is unchanged.

NIT-3: docs/README-lanes.md said each read is made once. It now says the
list of the logs is made twice, and that a lane's `dir` is read once. The
header and dispatch comments, `lanes-index`'s export comment and the
README's `--index` paragraph say the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit fb2cf36 into main Oct 21, 2026
6 checks passed
@brettheap
brettheap deleted the fix/lanes-index-partial-listing-173 branch October 21, 2026 11:29
brettheap pushed a commit that referenced this pull request Oct 21, 2026
…rea, and add the lines for #263, #289, #294 and #296, the changes after a42946e (#228)

`docs/release-notes/v1.1.0.md` takes the fixes of lane 3's review of d8a349d (#293 comments 6314185715 and 6314529807, the second of which replaces three of the corrected texts of the first) and the lines for the changes after a42946e.

The review's fixes:
- Line 45 (#255, #271) no longer says that `lane-reconcile` and `lane-handoff` read a dependency nested in a leg: only the sweep and `lane-end` do. It says that a leg directory this account cannot search is unreadable and never `absent`, that a nested dependency is unreadable to the sweep and `lane-end` too, and that the sweep still reads a declared leg beneath such a directory as `absent` (#272, open). It also says NOT POLLED where the tool does.
- Line 46 (#280, #283): an `awk` exits, it does not return, and the sentence on a status of 0 or 8 is gone.
- Line 61 (#214): the fence never checks whose a local commit is, only that it adds or changes a register or handoff file.
- The `<other-changes>` item counts 44 of the 53, because the openRepoShape pin's line (#233) is in the list, and the other 9, not 43 and 10. The milestone item names #247 and lists the six entries of 2026-10-20.
- #209 is not tests-only: its `-e` fixes `lanes`' stderr filter under macOS's BSD grep, so it moves from the test suite to the lane area, and its one line there carries its test changes too. It is cited once.

The lines for the changes after a42946e, each in its area and worded from its squash commit and description: #263 (d9065df) and #289 (fb2cf36) under the register and `lanes-edit.sh`, #294 (acc89ea, which closes #292) under `lane`, `lane-start`, `lane-end` and the name guard, and #296, which pins every Ubuntu runner to ubuntu-24.04, under the test suite and its checks. #296 has not landed yet: lane 3 lands it right before #293. The `<other-changes>` item says the lines were added on 2026-10-21 and are not in its counts, and the placeholder stays for #293 itself.

Not touched: lane 3's optional sentence on #271 and its NIT on the tag.

Lane: openRepoTools-2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 21, 2026
… 1.1.0 and list, by area and from the squash titles of aaa7bc8..a42946e, what else changed on main since v1.0.0 (#228) (#293)

Part of #228. It brings `docs/release-notes/v1.1.0.md` up to date on main: first on `a42946e`, as lane 3 asked on Brett Heap's word (#228 comment 6303608345, the section for lane openRepoTools-2, task 1), then with lane 3's review of `d8a349d` (comments 6314185715 and 6314529807 on this pull request), with the lines for the three changes that landed after `a42946e`, and with the line for #296, which landed right before this pull request. It is one file, changed in two commits (`d8a349d` and `bebb77d`), 59 insertions and 4 deletions against main.

Lands last before rehearsal D's freeze; lane 3 reviews and merges it (#228 6303608345)

**What changed**
- **Lane 3's review is applied,** in the second commit:
  - **The line on #255 and #271** no longer says that `lane-reconcile` and `lane-handoff` read a dependency nested in a leg, since only the sweep and `lane-end` do. It says that a leg directory this account cannot search is unreadable and never `absent`; that a dependency nested in a leg whose directory, or one above it, it cannot search is unreadable to the sweep and `lane-end` too; and that one known gap remains, #272, where the sweep reads a declared leg beneath such a directory as `absent`, so `lane-end` does not hold on it. It also says NOT POLLED where the tool prints it, and it says that the item closes two gaps in #246, where it said "both gaps" with nothing before it to refer to.
  - **The line on #280 and #283:** an `awk` exits and does not "return", and the sentence on a status of 0 or 8 is gone, because a reader of the notes cannot act on it.
  - **The line on #214:** the fence passes a local commit only when it adds or changes a register or handoff file, and never checks whose it is, so the line no longer says "the lane tooling's own".
  - **The counts in the `<other-changes>` item:** the pin's line (#233) is in the list, so the list carries 44 of the 53, and the other 9 have their lines under "What 1.1.0 adds", not 43 and 10. The item says "the openRepoShape pin" where the runner pin of #296 could be taken for it.
  - **The milestone item** names #247, so it lists the six entries the milestone held on 2026-10-20.
  - **#209 is not tests-only.** Its `-e` fixes `lanes`' stderr filter under macOS's BSD `grep`, so it moves from the test suite to the lane area, after #208's, and its one line there also carries its test changes: the `repoG#8` assertion and the count of bash files in prose. It is cited once.
- **Lines for the changes after `a42946e`:** #263 (`d9065df`), #289 (`fb2cf36`) and #294 (`acc89ea`, which closes #292) landed on 2026-10-21, from 11:28Z to 11:30Z. Each has a line in its area, worded from its squash commit and its description: #263 and #289 under "The register and `lanes-edit.sh`", and #294 under "`lane`, `lane-start`, `lane-end`, the name guard and their helpers".
  - **#296 has its line too,** last under "The test suite and its checks": CI and the release workflow run on `ubuntu-24.04`, because `ubuntu-latest` is now Ubuntu 26.04, and `macos-latest` is unchanged. It landed right before this pull request, as `5afba4a`.
- **"What 1.1.0 adds" gains two lines,** after the annotated-tags item. They are for the three entries that closed in the v1.1.0 milestone after the draft was written:
  - **#255,** fixed by #271: the lane tools name what they could not read in a triad's legs. `lane-handoff` names every tree-inventory record that is not a whole schema-1 one, and a leg directory this account cannot search is unreadable and never `absent`. Only the sweep and `lane-end` also see a dependency nested in such a leg, since `lane-reconcile` and `lane-handoff` read declared legs only, and #272 stays open as the known gap.
  - **#280,** fixed by #283: `lane-handoff` reads the lane's lifecycle whole and parses it afterwards, so an `awk` that exits early can no longer turn `lane-state`'s SIGPIPE into a lifecycle that could not be read.
  - #283 is #280's pull request, so the three numbers make two lines. Each line cites the issue and its pull request, as the hook-pair line does for #237 and #247.
- **"What else changed since v1.0.0" is drafted** from `git log aaa7bc8..a42946e`, which is 53 squash commits, one pull request each and no merge commit. It is grouped by area, one line each, and each line ends with its pull request. It lists 44 of the 53: the pin's line (#233) at its head and 43 by area, as 4 on the estate commands, 10 on the register and `lanes-edit.sh`, 11 on `lane`, `lane-start`, `lane-end` and the name guard, 6 on the handoff and its skill, 1 on the lane tools in a triad's legs (#246), 9 on the test suite and its checks, and 2 on the triad migration's records.
  - **The other 9 are accounted for elsewhere:** the package series of #228 (#262, #267, #268, #269, #270 and #284) is covered by #228's lines, and #247, #271 and #283 have the lines above.
  - **The lines added on 2026-10-21** are for the four changes after `a42946e`: #263 and #289 on the register and `lanes-edit.sh`, #294 on `lane`, `lane-start`, `lane-end` and the name guard, and #296 on the test suite and its checks. The areas now hold 47 lines, one for each of 47 pull requests, and #233's line at the head of the list makes 48.
  - **The list is redone at the tag.** It is drafted from the squash titles only.
- **"Before the tag" is updated in three items:**
  - `<pr-count>` gives the count at `a42946e`, which is 53;
  - `<other-changes>` says what was drafted, which 9 pull requests have their lines under "What 1.1.0 adds", that the lines for #263, #289, #294 and #296 were added on 2026-10-21, and what is left to do at the tag; the placeholder's own line now stands for the changes after `a42946e` that have no line above;
  - the milestone item names the six entries the milestone held on 2026-10-20: #228 still open, #237 closed by its pull request #247, #255 closed, and #280 closed by its pull request #283.

**Not added:** nothing that has landed. #296 landed right before this pull request and has its line. This pull request itself, and whatever lands after it, is what the `<other-changes>` placeholder stands for. None of #263, #289, #294, #296 and #292 is in the v1.1.0 milestone, which still holds the six entries of 2026-10-20.

**How this keeps the draft's rules**
- **One-line paragraphs:** every added line is its own list item, and none is wrapped.
- **References:** every reference above "Before the tag" is spelled `opensoft/openRepoTools#N`, with no bare `#N`, since the notes are published from another repository after the migration.
- **Placeholders:** `<other-changes>` stays as a named placeholder in code font, as the closing line of the list and as its checklist item. `<pr-count>` is unchanged apart from the drafting-time count.
- **The file's closing lines are untouched:** the self-naming last line stays last.

**Tests**
- **T023's verify command** passes: the last line names `openRepoTools-spec` and `docs/release-notes/v1.1.0.md`, `## Before the tag` is there, and so is `pipx install --force openrepotools`.
- **Coverage:** a script over `git log aaa7bc8..acc89ea`, 56 commits and no merge, finds each pull request cited above "Before the tag" except the six of #228's package series, which #228's lines cover. That is 46 by area, #233 at the head of the list, and #247, #271 and #283 under "What 1.1.0 adds". #296 is not in that range, and its line cites it.
- **Hygiene:** by grep over `tests/`, no test names this file. The two that read its content scan every tracked file, and both are in `tests/test_repo_hygiene.py`. The only other test that names `release-notes` is in `tests/test_release_workflow.py`, and it reads `release.yml`, not the notes. All of `tests/test_repo_hygiene.py` was run on the file, through `tests/run.sh` with every `LANES_*` and `TMUX*` variable unset:

  ```text
  -k 'repo_hygiene' -v
  collected 2933 items / 2696 deselected / 237 selected
  237 passed, 2696 deselected in 2.59s
  ```
- **Not run:** the whole suite. CI's `tests` job is the suite of record. This pull request's own runs on `bebb77d` went red on `ubuntu-latest`, which is Ubuntu 26.04, for the reasons #296 gives, because #296 was not yet on main. The record is lane 3's queue run over main, #296 and `bebb77d`, on `ubuntu-24.04` (below).

**Where the draft's rules left room, and lane 3's answers (comment 6314185715)**
- **Which section holds #255 and #280:** "What 1.1.0 adds", because the milestone is Brett Heap's scope for the release and #237 with #247 already sit there.
- **`<other-changes>` stays** as a closing placeholder after the drafted list, since this pull request itself lands after `a42946e`.
- **How much to list:** the tests-only and migration-record lines stay through the tag, because the check that every pull request has a line depends on them. If Brett Heap wants a shorter body, they fold into one line that still lists every pull request number.
- **Issues and pull requests:** the milestone lines cite both, and the area lines cite the pull request only.

**Lane 3's re-check of `bebb77d`** (#293 6315668503) is MERGE AFTER FIXES.
- **What holds:** every line lane 2 listed, the recount behind line 140 (44 of the 53 pull requests in `aaa7bc8..a42946e`, and 56 in `aaa7bc8..acc89ea`), the file's rules, and T023's verify commands.
- **One SHOULD and two NITs, all wording:**
  - line 45's sentence on a dependency nested in a leg, where "one above it" reads as a directory above the leg, which is #272's case. The text was lane 3's own;
  - "One known gap" in the same line;
  - line 74's last clause.
- **Why they wait:** lane 3 merges the notes as tested and carries the three fixes into the notes' final pass at the tag. A push here would cost another CI round while lane 2 is out of usage until 15:30Z, and the notes are edited again at the tag in any case.

Lane: openRepoTools-2

Landed by lane openRepoTools-3, as queue prefix `lane3-queue-u/p2-293` (tree `9b879ca`; tests.yml run 39595166877, green on `ubuntu-24.04`), right after #296. Before the landing, lane 3 merged main into the branch, so that its own checks ran over main with #296; the content is unchanged. Brett Heap's word, of 2026-10-20T22:5xZ: "merge all when green for all merges thru v1.1.0. use this for all lanes" (#228 6303614082).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 21, 2026
…scripts, each tool pinned by version and sha256, in a workflow of its own (#277)

This completes the WIP commit b1c59e6, which added lint.yml alone. It is a
branch for v1.2.0, with no PR and no label, and the job has not yet run on a
GitHub runner: every result below was taken on one machine with the pinned
tools, so the CI record is still to come.

No job ran either tool, so a warning on a workflow or a shell script showed
nowhere. Lane openRepoTools-2's review of #228's release.yml found three
mutants that actionlint's shellcheck rule flags and the suite lets through
(I4, I13 and N3), and nothing in CI would have said so.

What changed
- .github/workflows/lint.yml, one job `lint`, in a file of its own. tests.yml
  is the release gate's file: release.yml requires exactly its four jobs,
  tests/test_tests_workflow.py holds that, and rehearsal D rewrites it for the
  code leg. The job runs on pull requests, on main and release/v*, and by
  dispatch, and it is no part of the gate. It runs on ubuntu-24.04, as every
  other ubuntu job does since #296 (ubuntu-latest became Ubuntu 26.04 on
  2026-10-21).
- actionlint 1.7.12 and shellcheck 0.11.0, each a release archive checked with
  sha256sum --strict -c before it is unpacked. Each digest was computed from
  the downloaded archive and equals GitHub's asset digest, and actionlint's
  also equals upstream's checksums.txt. Neither tool comes from the runner
  image or from a package index, and both are static binaries.
- actionlint runs over every workflow with the pinned shellcheck given by
  path, with no exclusion, no severity floor and no config file, and the step
  fails if actionlint reports its shellcheck rule disabled, which it does
  silently when it cannot start the binary.
- shellcheck runs, one file at a time, on every tracked file whose first line
  is a bash or sh shebang, at the baseline below. That is 16 files on main,
  and 15 of them run.
- tests/test_lint_workflow.py (17 tests, 21 cases) pins the file and runs its
  three steps under fakes instead of only reading them: the install step over
  a genuine archive, over a tampered archive of each tool (nothing is
  unpacked, and after a bad actionlint archive the shellcheck one is never
  asked for) and over a tool that says another version; the actionlint step
  under a fake actionlint (a finding, a disabled shellcheck rule, a
  shellcheck that is not the pinned one); the shellcheck step in a throwaway
  git repository (selection by shebang, the baseline's flags, every file
  checked after one fails, an empty selection fails). The header's table of
  codes is held to the step's list, and the baselined scripts are held to be
  tracked bash files.

What the tools found on main a42946e
- release.yml, tests.yml and lint.yml: nothing, at any severity.
- The 16 shell scripts: no error. park, resume and status: nothing at all.
  The other twelve: 319 findings, 190 warnings and 129 notes.
  Warnings: SC2034 157, SC2088 14, SC2209 4, SC1007 4, SC2154 3, SC2069 2,
  SC2178 2, SC2128 2, SC2141 1, SC2318 1. Notes: SC2016 55, SC2018 18,
  SC2019 18, SC2295 11, SC2329 10, SC2086 5, SC2015 5, SC2094 4, SC2020 1,
  SC2012 1, SC2006 1. lanes-edit.sh holds 142 of the 319.
- tests/test_lane_helpers.sh, which the job does not run: 43 warnings and 218
  notes, and 16.2 GB of memory to find them.
- Two of the warnings are latent bugs and not style: SC2141 in
  lanes-edit.sh's session_start_block (IFS='\t' is a backslash and a t, so a
  live-fork row id<TAB>4242<TAB>interactive reads as id<TAB>4242<TAB>in,
  erac and ive) and SC2318 in openRepoTools's wip_fetch_template_file (bash
  expands $rel before the local that sets it takes effect, so path carries
  the caller's rel, which is right only because wip_init calls it from a loop
  over its own rel). Neither is fixed here.

Main has moved since, and the baseline holds on it. On main 50d05d4 (after
#263, #289, #294, #296 and #293) the job's three steps, run as written over
that tree, exit 0: actionlint finds nothing in release.yml, tests.yml and
lint.yml, and shellcheck finds the same 190 warnings, in the same ten codes
and the same counts, no error, and 132 notes (lanes-edit.sh gained two SC2016
and one SC2015). lanes-edit.sh peaks at 10.9 GB and takes about 90 to 130 s,
which a 16 GB runner holds.

What the baseline leaves out
- The workflows: nothing.
- park, resume, status and every script added later: nothing.
- The other twelve scripts are held to --severity=warning less ten codes, so
  the 129 notes and the 190 warnings of SC1007, SC2034, SC2069, SC2088,
  SC2128, SC2141, SC2154, SC2178, SC2209 and SC2318 are left out, and any
  error or warning of another code fails. Each code, its count and the reason
  it is left out are in lint.yml's header.
- tests/test_lane_helpers.sh is not run: it is the lane helpers' own suite,
  nothing ships it, and shellcheck needs 16.2 GB for it (measured), more than
  a 16 GB runner has. lanes-edit.sh is run: 10.5 GB and about two minutes.

The three mutants, applied to release.yml in scratch copies (lane 2's own
diffs), and run through the job's own actionlint step, on a42946e's file and
again on 50d05d4's
- I4 (3.2.*) made *)): SC2221 and SC2222, warning.
- I13 (the link fingerprint check made `if false`): SC2034, warning.
- N3 (exit 0 as the clean step's first line): SC2317, info.
Each makes the step exit 1. N3 is only info, which is why the workflows have
no severity floor.

Tests, run only through tests/run.sh -k
- -k 'test_lint_yml or test_no_committed_file_names_a_host_absolute_path or
  test_adoption_act_zero_is_cited_by_the_sha_that_landed': 23 passed. The
  last two scan every tracked file, so they read lint.yml and the new test.
- Before the workflow existed (lint.yml set aside), -k test_lint_yml: 19
  errors and 1 failure of its 21 cases. The 21st reads only tracked files and
  passed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant