Skip to content

bump-leg.py: a bump that fails for any reason once the leg has moved, a commit git refuses included, rolls the pin, the workflow files, the leg's checkout and the index back and says whether all of it went back, so a workflow file it cannot write, a validator line the stdout cannot encode, a signer that cannot sign or an interrupt no longer leaves the pin rewritten and the leg on the new commit (#253) - #264

Merged
brettheap merged 7 commits into
mainfrom
fix/bump-leg-rollback-every-failure
Oct 12, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-2 (openRepoShape-2)
Claim: #253 (comment)

What

scripts/bump-leg.py's _apply_rewrite now rolls every byte back on ANY failure once the leg has been asked to move, up to and including the commit, and then reports the failure. No report claims more than the rollback did, as far as git can show it. The leg is judged by its branch, its HEAD and its git status, so a report cannot vouch for what git status does not show, a file git ignores above all (see "Left out").

  • The handler is except BaseException, so an interrupt is covered as well as an OSError or a UnicodeError.
  • The commit is inside the rollback's reach. It is the last statement inside the try, and cmd_bump builds the message first and hands it in. A commit git refuses (a signing key that cannot sign, a commit-msg hook) is therefore put back like any other failure.
  • The HEAD guard. One git rev-parse HEAD is read just before the commit. After any failure, _head_guard compares the root's HEAD with it:
    • unmoved: every byte is rolled back;
    • moved, because the failure came after the commit landed (an interrupt, or the rev-parse that reads the new HEAD back): nothing is rolled back under the commit, and the report says The bump was committed as <sha>;
    • unreadable: nothing is rolled back, and the report says the commit could not be read.
  • The rollback is small helpers instead of a loop and a try.
    • _roll_back puts back, in the order it always did, every file written, the leg's checkout and the index.
    • _put_back attempts each of those steps whatever the steps before it did. It prints, never raises, a step that fails. A second Ctrl-C names its step as interrupted, and the steps after it still run; no signal handler is installed.
    • _restore_file writes a file back only when it no longer holds its original bytes, so a read-only file whose write never opened it is not reported as unrestorable.
    • _restore_leg_if_moved reads the leg's branch and HEAD and restores the leg when either differs from where it stood. A forward checkout that moved the leg and then failed (a post-checkout hook) or was interrupted is put back, and a leg that never moved is not checked out again.
    • Either way, it then compares the leg's git status --porcelain -z --untracked-files=all, read as bytes before the checkout, with the leg's status now. git writes the new commit's files before it moves the leg's index and HEAD, so a checkout cut off between the two leaves the leg on its branch at its commit with those files in it. When the status differs, the step fails. _put_back names it and sends the operator to git -C <leg> status, and the report says THE ROLLBACK WAS NOT COMPLETE.
    • Nothing in the leg is removed. An untracked file of the operator's own is allowed there, and the rollback cannot tell it from one the checkout wrote.
  • What it reports. Each report says one of three things: every byte is back (ROLLED_BACK), THE ROLLBACK WAS NOT COMPLETE (NOT_ROLLED_BACK), or the commit landed. Where that goes depends on the failure:
    • a Refusal carries it in its own text, and bump-leg-validators-red's sentence is replaced when the rollback did not earn it;
    • an OSError or a UnicodeError that nothing committed leaves as REFUSED bump-leg-rewrite-failed naming the exception (exit 2);
    • a CommandFailed leaves as main() always printed it (REFUSED advancing a leg, exit 2), after one stderr line saying what became of the tree;
    • an interrupt or a defect is re-raised as itself, after the same line.
  • Docs and main(). One bullet in the module docstring's refusal list. No --help text changes (render-cli-reference.py --check ok), and main() is not touched.

Why

_apply_rewrite's docstring promised "or roll every byte back and re-raise", but its handler was except (Refusal, CommandFailed), and main() catches the same two. So a PermissionError on a workflow file, a UnicodeError from echoing a validator's output, or an interrupt ended in a traceback, with the pin rewritten, the leg on the new commit and the index changed, and no finding about any of it.

Round 1 reproduced this on main 39ac59c with the issue's own steps:

== decode: exit 1     UnicodeDecodeError ...   git status: M  contracts/spec-pin.yaml M  spec | leg HEAD c893c2b, was 443b42c
== readonly: exit 1   PermissionError ...      git status:  M contracts/spec-pin.yaml  M spec | leg HEAD c893c2b, was 443b42c

The same script, re-run in fix round 1 with a signing case added (commit.gpgsign true and a gpg.program that does not exist), at the reviewed head and at fix round 1's head 67a1163:

a8d31ee  == signing: exit 2   REFUSED advancing a leg ... git commit ... exit 128   git status: M  contracts/spec-pin.yaml M  spec | leg HEAD 95fac77, was 8c8d650
67a1163  == decode: exit 2    REFUSED bump-leg-rewrite-failed: UnicodeDecodeError ...   git status: (clean) | leg HEAD 00e7d92, was 00e7d92
67a1163  == readonly: exit 2  REFUSED bump-leg-rewrite-failed: PermissionError ...      git status: (clean) | leg HEAD 00e7d92, was 00e7d92
67a1163  == signing: exit 2   REFUSED advancing a leg ... git commit ... exit 128   git status: (clean) | leg HEAD 00e7d92, was 00e7d92

At 67a1163 the signing run's stderr opens with The command below failed while the bump was being applied. Every byte this command wrote has been rolled back — the pin, the workflow files, the leg's checkout and the index — and the tree is exactly as it was. The same command run again once the signer is fixed exits 0 and commits. At a8d31ee that second run was refused as bump-leg-root-dirty.

The review measured the decode run on main c3f4987, with #230 and #214 landed: it no longer fails at all there. The handler still covers what the echo can still raise (an OSError such as EPIPE from sys.stdout.write).

The issue also notes the rollback's own writes need a guard. written[target] is set before write_lf, so for the read-only file the restore would meet the same PermissionError over the original failure. _restore_file and _put_back are that guard. A test has the leg's own restore fail, and shows the files and the index are still put back.

Decision for the coordinator: a clean REFUSED (exit 2) for OSError and UnicodeError, and the traceback for everything else, after a line saying whether the tree is back.

  • The repository's other pin-moving tool does the same for an unwritable file. update-shape.py _rewrite_readme turns the write's OSError into update-readme-unwritable ("A FAILED WRITE IS A REFUSAL, NEVER A TRACEBACK"), so the one rollback arm that exists puts every byte back. family.py bump has no rollback and lets such an error trace back, which is the weaker precedent.
  • The issue's Expected also names exit 2 "the way bump-leg-validators-red does".
  • The conversion is made in _apply_rewrite, after the rollback, not in main(), because only there is the claim "every byte rolled back" known to be true or false. main() catching OSError would also dress up a failure that came before the first write.
  • An interrupt and a defect are not the operator's to fix, so they keep the interpreter's own report, after the tree is back and one line says so.
  • A failed git command keeps main()'s REFUSED advancing a leg, after the same line.

Decision: a HEAD that cannot be read after the commit was asked for counts as landed. Rolling the files and the leg back under a commit that did land would leave a tree that contradicts the commit, and that is the worse of the two mistakes.

Tests

New tests/test_bump_leg_rollback.py: 24 cases in 13 functions. Each case compares what the bump could change with what it was before the bump: the root's HEAD, the pin's bytes, every workflow's bytes, the gitlink, git diff --cached --quiet, git status, the leg's HEAD and the branch the leg stood on. The leg is put ON its branch first, as bootstrap.py leaves it. There are three exceptions:

  • In the four cases where the leg's own restore is made to fail, the leg cannot be put back and git status shows it. Those cases compare the root's HEAD, the pin, every workflow, the gitlink and git diff --cached --quiet.

  • In the three cases where the forward checkout leaves a file in the leg, git status shows that file, because nothing removes it. Those cases compare the same five, plus the leg's HEAD and its branch.

  • The commit-landed cases assert that the commit and the tree agree.

  • A read-only tracked workflow file, with bump-leg.py run as a subprocess (the issue's reproduction). Exit 2, bump-leg-rewrite-failed, PermissionError, and no traceback. There is no "rolling the bump back" noise for a file never opened. (1 case; skipped where the account can write a read-only file, i.e. root.)

  • A write that dies half way (ENOSPC after the file was truncated and 20 bytes written): exit 2. (1)

  • A validator's output that cannot be read (UnicodeDecodeError from run_validator), and one the stdout cannot echo (a stdout whose write raises UnicodeEncodeError for U+FFFD): exit 2. (2)

  • An interrupt while writing the second file (with the pin already rewritten), an interrupt in the validators (with the index staged), and a RuntimeError in the validators. Each is re-raised as itself, the tree is back, and stderr says <Exception> while the bump was being applied and that every byte is back. (3)

  • The leg's own restore failing, beside a red validator, an unwritable file, or a git add that fails. The original failure is the one reported, and the failed step is named. The files and the index are still put back. Each report says THE ROLLBACK WAS NOT COMPLETE, and none says the tree is exactly as it was. (3)

  • A second interrupt inside the rollback's leg step. The step is named interrupted, the index step after it still runs, and the refusal says the rollback was not complete. (1)

  • A forward checkout that moved the leg and then failed: an interrupt after the real checkout, and a leg post-checkout hook that exits 1 for the new commit only. The leg is back on its branch. (2)

  • A forward checkout that left files in the leg. Two are cut off before the leg moved: one is interrupted, and one fails with exit 130. Each writes a file and leaves HEAD where it was. The third moves the leg, leaves a file the checkout back cannot remove, and is interrupted. In each, the line before the re-raise or the refusal says THE ROLLBACK WAS NOT COMPLETE, nothing says the tree is exactly as it was, and the leg step is named. The operator's own untracked file and the checkout's file are both still there. (3)

  • An untracked file of the operator's own in the leg, beside a red validator. The report still says every byte is back, and the file is untouched. (1)

  • --to the commit already pinned, with a workflow reference a hand-made bump left behind. The forward checkout detaches the leg at the commit it stood on, and a red validator follows. The leg is back ON main. (1)

  • A commit git refuses: a gpg.program that does not exist, and a commit-msg hook. Exit 2, the refusal names git commit, the tree is back, and stderr says every byte is back. (2)

  • A failure after the commit landed: an interrupt, a Refusal from reading HEAD back, and an OSError. Nothing is rolled back, the tree agrees with the new commit, and the report says committed as <sha>. The OSError is re-raised as itself, not made into bump-leg-rewrite-failed, whose remediation says nothing was committed. (3)

  • A HEAD that cannot be read once the commit was asked for. Every git rev-parse HEAD then fails, commit_once's and the guard's. Nothing is rolled back, the tree agrees with the commit that landed, and the refusal says whether the commit landed could not be read. (1)

The echo test uses a stream that raises, not PYTHONIOENCODING=cp1252. #214's escape_what_stdout_cannot_encode, now on main, reconfigures that exact stream to backslashreplace at the top of main(), so a test built on it would stop meaning anything. The hook tests set core.hooksPath in the repository's own config to a #!/bin/sh script, as tests/test_update_shape_add_ignored.py does. Git for Windows runs such a script through its own sh, and tests-windows passed them at 67a1163 (00:31Z).

Run against main's code, the new tests fail for the issue's reasons and the review's:

Targeted, at head 5269aac (all passed):

file passed
tests/test_bump_leg.py 23 (unchanged)
tests/test_bump_leg_rollback.py 24
tests/test_commit_trailers.py 125
tests/test_cli_reference.py 13
tests/test_repo_hygiene.py 50
  • scripts/render-cli-reference.py --check ok.
  • 20 mutants of this head were all killed. They are re-verification 1's 17, with the three on _restore_leg_if_moved re-spelled for its new shape, and three of this round's own:
    • 18: the status is never compared (3 failed);
    • 19: the status is compared only when the leg did not move (1 failed, moved-then-interrupted);
    • 20: _put_back has no clause of its own for the status step (3 failed).
  • The re-verification's three survivors now fail one test each: 9 (an unreadable HEAD rolls back), 12 (the leg compared by HEAD alone) and 16 (an OSError after a landed commit made into bump-leg-rewrite-failed).
  • The full suite was not run, because CI runs it on three OSes.

Composes with

This branch was cut from 39ac59c. Main is now b421e6b. Four changes to scripts/bump-leg.py have landed there since the cut: #246 for #214, #230 for #217, #222 for #207 (018ce0d), and #281 for #254 (b421e6b, 00:55Z).

Left out

  • The README and the handbook. README.md and docs/handbook.html say a red validator rolls the tree back and that a refusal leaves the tree as it was found. That is now true for a refused commit as well. In the two corners where it is not true, the refusal says THE ROLLBACK WAS NOT COMPLETE instead: a rollback step itself fails, or a cut-off checkout left files in the leg. bump-leg.py rewrites and commits only the workflow files git tracks, so an untracked draft workflow that names the leg is no longer added whole to the lockstep commit and an ignored one no longer dies at git add after the rewrite: both are refused by name as bump-leg-workflow-untracked, before anything is written and under --dry-run as well (#207) #222, now on main, edited the same README paragraph, so it is left alone here.
  • Nothing in the leg is removed. The files a cut-off checkout wrote stay in the leg until the operator removes them. Until then, git refuses the re-run's checkout (untracked working tree files would be overwritten). That re-run wrote nothing, so its own line, that every byte it wrote is back, is true. Removing only the paths that appeared and that the new commit's tree holds was considered and left out, because it is not provably safe. git checkout overwrites an IGNORED file without asking, and a new .gitignore written before the cut can make that file show as untracked, so such a path could be a file the operator had.
  • A file git ignores. The leg's git status does not list ignored files. A cut-off checkout that wrote only paths the leg's .gitignore matches therefore leaves files that no report names. git overwrites ignored files without asking, so they do not block the re-run. Listing ignored files (--ignored) would read every build output in the leg twice per bump, so it is not done.
  • A hook that refuses every checkout. If the leg's post-checkout hook refuses the restoring checkout too, that step is reported as not put back, even when git did move the leg back. The report then says THE ROLLBACK WAS NOT COMPLETE and sends the operator to git status. This is conservative, not wrong. Re-verification 1 suggested re-reading the branch and HEAD after such a failure, and counting the step done when they match (its nit B). That is declined: a hook that failed can leave work undone that neither the HEAD nor git status shows. git-lfs's post-checkout, for one, sets the read-only bit on lockable files, and git does not track that bit.
  • An interrupt outside a rollback step. An interrupt that lands outside _put_back is not caught, for example between two rollback steps or while the handler itself prints. By design, no signal handler is installed from a library function.
  • No refusal for an interrupt. An uncaught KeyboardInterrupt is not turned into a refusal. The operator gets one line saying whether the tree is back, then the interpreter's own traceback and exit status.
  • update-shape.py apply still handles except (Refusal, CommandFailed) in cmd_apply (at ede15ce). Whether an OSError or an interrupt there leaves a half-applied re-pin was not probed here. It is a follow-up for a lane that wants it.
  • A non-UTF-8 workflow file or pin ends in a traceback before anything moves. This is the review's follow-up item, outside bump-leg.py _apply_rewrite rolls back only on Refusal and CommandFailed, so a PermissionError on a workflow file it rewrites, or the UnicodeEncodeError that #230's errors="replace" moves the decode crash to, leaves the pin rewritten and the leg on the new commit, with a traceback and no rollback #253's rollback promise, and the coordinator files it.

Review

Lane openRepoShape-2's own adversarial review of a8d31ee found CHANGES NEEDED: #264 (comment). Fix round 1 below answers it. The lane's re-verification of fix round 1's head 67a1163 found CHANGES NEEDED again: #264 (comment). Fix round 2 below answers that. A review by another lane is welcome.

Fix round 1

This round answers #264 (comment). Every item is in commit 67a1163, and every test named failed at a8d31ee and passes at 67a1163.

  • P2: a commit git refuses is rolled back. The commit is inside the try, under the HEAD guard. Tests: test_a_commit_git_refuses_puts_every_byte_back, and test_a_failure_after_the_commit_landed_rolls_nothing_back for the guard.
  • P3: a refusal no longer claims a rollback that failed. It says THE ROLLBACK WAS NOT COMPLETE, for every Refusal, and a failed git command gets a line saying the same. Test: test_a_rollback_step_that_fails_is_named_and_the_others_still_run, which now has 3 cases and an unconditional assertion.
  • P3: a second interrupt during the rollback names its step, and the steps after it still run. Test: test_an_interrupt_inside_a_rollback_step_is_named_and_the_steps_after_it_still_run.
  • P3: the leg's restore is decided from the leg. A forward checkout that moved it and then failed or was interrupted is put back. Test: test_a_forward_checkout_that_moved_the_leg_and_then_failed_puts_it_back.
  • nit: a line before an interrupt's or a defect's traceback says whether the tree is back. Test: test_any_other_failure_puts_every_byte_back_and_is_re_raised_as_itself, which now asserts it.
  • Follow-up (a non-UTF-8 workflow or pin): not in this PR. The coordinator files it.

Fix round 2

This round answers #264 (comment).

  • P2: a forward checkout cut off before it moved the leg is no longer reported as put back. Commit d75151f.
    • The leg's git status is read before the checkout and compared in the rollback, after any restore. When it differs, the leg step is named, the operator is sent to git -C <leg> status, and the report says THE ROLLBACK WAS NOT COMPLETE. Nothing in the leg is removed.

    • Tests: test_a_forward_checkout_that_left_files_in_the_leg_is_not_called_put_back (3 cases, all failing at 67a1163 on a line that says every byte is back), and test_an_untracked_file_the_operator_keeps_in_the_leg_is_left_alone_and_not_counted.

    • The re-verification's real-SIGINT reproduction (partial.sh: a 40000-file leg commit, SIGINT to the whole process group 0.15 s after the checkout starts), re-run at 5269aac:

      terminal Ctrl-C (python's default handler): exit 130
        REFUSED rolling the bump back. the checkout of spec/ could not be put back: its `git status` no longer reads as it did before the bump, ... Read `git -C <leg> status` before anything else, ...
        KeyboardInterrupt while the bump was being applied. THE ROLLBACK WAS NOT COMPLETE: ...
        leg on main at 9b32760 (unmoved), 1305 files under big/ left; "exactly as it was": 0 times
      git alone killed: exit 2
        the same step line; The command below failed while the bump was being applied. THE ROLLBACK WAS NOT COMPLETE: ...
        leg on main at 9b32760, 40000 files left; "exactly as it was": 0 times
      

      At 67a1163 both runs said every byte was back and the tree was exactly as it was.

  • Nit A: the three surviving mutants now each fail a test. Commit 5269aac, tests only.
    • 9: test_a_head_that_cannot_be_read_after_the_commit_rolls_nothing_back.
    • 12: test_a_leg_detached_at_the_commit_it_stood_on_is_put_back_on_its_branch.
    • 16: the new oserror case of test_a_failure_after_the_commit_landed_rolls_nothing_back.
  • Nit B: declined. See "Left out" on a hook that refuses every checkout.
  • This body: the two sentences the re-verification found false are now true, and say how. "No report claims more than the rollback did" names the leg's git status and the one corner it cannot see. "A leg that never moved is not checked out again" is followed by the comparison that is still made. Main is b421e6b, which holds bump-leg.py and update-shape.py hand the project's own validators an explicit PYTHONIOENCODING, so a real bump or apply on a project named 日本 is no longer refused as bump-leg-validators-red or update-validators-red under a cp1252 stdout (#254) #281's head 754803d.

Closes #253

🤖 Generated with Claude Code

Summary by Sourcery

Make bump-leg restore all changes after any pre-commit failure and accurately report the resulting repository state.

Bug Fixes:

  • Ensure bump-leg failures after the leg starts moving—including write, validator, interrupt, checkout, and refused-commit failures—restore the pin, workflows, leg checkout, and index whenever possible.
  • Prevent reports from claiming a complete rollback when restoration fails or a partially completed checkout leaves files in the leg.
  • Preserve committed changes when a failure occurs after the commit lands, and report whether the commit landed or could not be read.

Enhancements:

  • Centralize rollback handling with per-step failure reporting, interrupt protection, file restoration guards, HEAD checks, and leg status verification.
  • Convert unrecovered OSError and UnicodeError failures into clear exit-2 refusals while preserving command failures, interrupts, and other defects.

Documentation:

  • Document the expanded rollback and failure-reporting guarantees in bump-leg.py's user-facing refusal documentation.

Tests:

  • Add comprehensive rollback coverage for partial writes, encoding failures, interrupts, rollback failures, interrupted checkouts, refused commits, post-commit failures, unreadable HEAD state, and untracked leg files.

Brett Heap and others added 3 commits October 11, 2026 22:20
…rolls the pin, the workflow files, the leg's checkout and the index back, so a workflow file it cannot write, a validator line the stdout cannot encode or an interrupt no longer leaves the pin rewritten and the leg on the new commit behind a traceback (#253)

`_apply_rewrite` rolled back on `except (Refusal, CommandFailed)` and nothing
else. A `PermissionError` on a workflow file, a `UnicodeError` from echoing a
validator's output (the crash #230's `errors="replace"` moves the decode error
to) or a `KeyboardInterrupt` passed through it: `main()` catches the same two
types, so the run ended in a traceback with the pin rewritten, the leg on the
new commit and, once staged, the index changed.

The handler is now `except BaseException`, and the rollback it runs is three
helpers instead of a loop and a `try`. `_put_back` attempts each step (every
restored file, the leg's checkout, the index) whatever the steps before it
did, and prints, never raises, a step that fails; `_restore_file` writes a
file back only when it no longer holds its original bytes, so a read-only
file the write never opened is not reported as unrestorable. A `Refusal` or a
`CommandFailed` is re-raised as it was. An `OSError` or a `UnicodeError`
leaves as `REFUSED bump-leg-rewrite-failed` naming the exception and saying
whether the tree is back, the way `update-shape.py` turns an unwritable README
into `update-readme-unwritable`; anything else, an interrupt included, is
re-raised as itself once the tree is back.

tests/test_bump_leg_rollback.py: a read-only workflow file, a stdout that
cannot encode a validator's line, a write that dies half way, an interrupt
while writing and while validating, a defect in the tool, and a rollback step
that itself fails. Each compares the root's HEAD, the pin, every workflow, the
gitlink, `git diff --cached --quiet` and the leg's HEAD and branch with what
they were before the bump. All eight fail on the code before this change.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…m, not from a codec setting, so they keep meaning the same thing once `main` escapes what a strict stdout cannot encode (#253)

The first cut of the echo test ran `bump-leg.py` with `PYTHONIOENCODING=cp1252`.
#214's `escape_what_stdout_cannot_encode` reconfigures exactly that stream to
`backslashreplace` at the top of `main`, so that test would have started to
see a bump that succeeds the day #214 landed. The echo test is now in-process
with a stdout whose `write` raises `UnicodeEncodeError` for what cp1252
cannot hold, beside a `UnicodeDecodeError` raised from the validator read, so
both ways a validator's output can fail the statement after the staging are
covered whichever of #214 and #230 is in.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…while the bump was being applied, so an `OSError` from the leg's checkout itself is no longer described as happening after the leg was moved (#253)

The refusal said "after the leg was moved", which is false for an `OSError`
raised by the checkout that moves it (a `git` that cannot be started). The
rollback it follows is unchanged; only the sentence is.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-11T22:39:49Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 11, 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 1 hour by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR hardens bump-leg.py so any failure after a bump starts restores all changed repository state, reports incomplete rollback without masking the triggering failure, and classifies operational encoding/write errors as exit-2 refusals; a new adversarial test suite covers the rollback boundary and failure modes.

Sequence diagram for bump-leg failure rollback

sequenceDiagram
    participant Apply as _apply_rewrite
    participant Rollback as _roll_back
    participant File as _restore_file
    participant Leg as leg checkout
    participant Git as git index
    participant Report as Refusal or original failure

    Apply->>Apply: write pin and workflow files
    Apply->>Leg: restore_leg(submodule, placement, was)
    Apply->>Git: run(git reset -q HEAD -- staged)
    Apply-->>Rollback: failure after first byte moved
    Rollback->>File: _restore_file(target, original)
    Rollback->>Leg: restore_leg(submodule, placement, was)
    Rollback->>Git: run(git reset -q HEAD -- staged)
    Rollback-->>Apply: rolled_back
    alt OSError or UnicodeError
        Apply->>Report: raise Refusal(bump-leg-rewrite-failed)
    else other BaseException
        Apply->>Report: re-raise failure
    end
Loading

File-Level Changes

Change Details Files
Make rewrite rollback cover every failure after the leg checkout and preserve the original failure while reporting rollback problems.
  • Catch BaseException around the rewrite process, including interrupts and unexpected defects.
  • Extract guarded file restoration, per-step rollback reporting, and ordered rollback orchestration into helpers.
  • Restore written files only when their bytes differ, then independently restore the leg checkout and index.
  • Re-raise Refusal/CommandFailed unchanged; convert OSError/UnicodeError to bump-leg-rewrite-failed after rollback, while re-raising other failures unchanged.
scripts/bump-leg.py
Add adversarial tests that verify complete tree restoration and failure classification across write, validator, interrupt, encoding, and rollback failures.
  • Test read-only and partial-write workflow failures, including no false rollback error when a file was never modified.
  • Test validator decode and stdout-encoding failures as clean exit-2 refusals.
  • Test interrupts and unexpected runtime errors are re-raised only after restoring files, checkout, and index.
  • Test rollback-step failures are named without masking the original failure, while subsequent rollback steps still run.
  • Snapshot and compare repository HEAD, pin/workflow bytes, gitlink, index state, leg HEAD, and branch placement.
tests/test_bump_leg_rollback.py

Assessment against linked issues

Issue Objective Addressed Explanation
#253 Ensure that any failure after the leg checkout begins—including PermissionError/OSError, UnicodeError while handling validator output, interrupts, and unexpected exceptions—rolls back the rewritten pin, workflow files, leg checkout, and index instead of leaving a half-applied bump. ✅
#253 Make rollback itself best-effort and non-throwing so every restoration step is attempted, rollback failures are reported, and the original failure is not obscured. ✅
#253 Report OSError and UnicodeError rewrite failures as clean refusals with exit status 2 and useful diagnostics, while preserving the original exception behavior for interrupts and unrelated defects; add regression coverage for these scenarios. ✅

Possibly linked issues


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

@chatgpt-codex-connector

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

Copy link
Copy Markdown

Codex Review Summary

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-11T22:46:32.971602Z a8d31ee Manual request
ℹ️ About Codex in GitHub

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

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

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a8d31ee1b5

ℹ️ About Codex in GitHub

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

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

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

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

Comment thread scripts/bump-leg.py
run(["git", "reset", "-q", "HEAD", "--", *staged], cwd=root)
except CommandFailed as undo:
print(undo.loudly("rolling the bump back"), file=sys.stderr)
except BaseException as failure:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Extend rollback through commit creation

When validation succeeds but commit_once() subsequently fails—for example because a commit hook rejects the commit or .git becomes unwritable—_apply_rewrite has already returned, so this handler cannot run; the rewritten pin, workflows, and gitlink remain staged and the leg stays detached. The new guarantee that no later failure leaves a half-applied bump therefore excludes a routine final write step; keep commit creation inside the same rollback transaction.

AGENTS.md reference: AGENTS.md:L300-L303

Useful? React with 👍 / 👎.

Comment thread scripts/bump-leg.py Outdated
Comment on lines +750 to +752
rolled_back = _roll_back(root, submodule, placement, was, staged,
written, moved_leg)
if isinstance(failure, (OSError, UnicodeError)):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor failed rollback status for validator refusals

When a validator goes red and an undo step then fails (for example, restore_leg raises an OSError), _roll_back returns False, but that result only affects failures that were originally OSError or UnicodeError. A validator failure is a Refusal, so it is re-raised with the existing claim that every byte was restored and the tree is exact, despite the preceding rollback error and the still-moved leg; report the incomplete rollback for these failures too so the operator knows to inspect the tree.

AGENTS.md reference: AGENTS.md:L322-L325

Useful? React with 👍 / 👎.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-11T23:47:11Z)

This lane reviewed its own PR #264 at a8d31ee, before any fix round, read-only in its own detached worktrees, and authored nothing in this pass.

CHANGES NEEDED. There is no sequencing constraint.

Findings, file:line at a8d31ee:

  • P2 (codex's first P2, reproduced). A commit git refuses after green validators is not rolled back.
    • Triggers: a failing commit.gpgsign signer, or a commit-msg hook that exits 1.
    • Result: exit 2. The pin, the workflow and the gitlink stay staged, and the leg stays detached at the new commit. The re-run is refused as bump-leg-root-dirty.
    • commit_once (scripts/bump-leg.py:835) runs outside the try at :706, yet the title and the new docstring at :42-46 promise any failure is rolled back.
  • P3 (codex's second P2, reproduced; P3 because a rollback step must fail first). With a red validator and the leg's index.lock held, the refusal still claims the leg's checkout was rolled back. git status shows M spec. rolled_back (:750) reaches only the OSError/UnicodeError branch.
  • P3. A second SIGINT during the rollback escapes _put_back (:647). The index step never runs, and nothing is printed.
  • P3. A forward checkout that moves the leg and then fails (a post-checkout hook) or is interrupted leaves the leg moved. moved_leg is set at :712, after run returns.
  • nit. An interrupt ends in a bare traceback that never says the tree was put back.
  • Follow-up. A workflow that is not UTF-8 ends in a traceback at :269 before anything moves.

Each finding is the same on main. Head fixes the issue's read-only reproduction.

Tests:

Merges: clean (rc 0) against main (c3f4987 and 60588d5), #222, 5757e29 and #266 to #270.

The full report is handoffs/xFactory/attachments/lane-openRepoShape-2/review264-a8d31ee/final-report.md in opensoft/brett-wip.

…then failed, and a second interrupt during the rollback are all put back, and no report claims a rollback that was not complete, so a signing key that cannot sign no longer leaves the bump staged for the re-run to refuse as a dirty root (#253)

Lane openRepoShape-2's review of a8d31ee found the rollback stopped short of
the PR's own promise in four places, and one report said too little:

- The commit ran after `_apply_rewrite` returned, outside the rollback. A
  commit git refuses (`commit.gpgsign` with a signer that fails, a
  `commit-msg` hook) left the pin, the workflows and the gitlink staged and
  the leg on the new commit, and the same command run again once the signer
  was fixed was refused as `bump-leg-root-dirty`. The commit is now the last
  statement inside the `try`. One `git rev-parse HEAD` is read just before
  it, and `_head_guard` compares the root's HEAD with it after any failure:
  unmoved, every byte goes back; moved (or unreadable), the bump is a commit,
  nothing is rolled back under it, and the report says it was committed.
- A `bump-leg-validators-red` refusal said every byte was rolled back even
  when a rollback step had failed. The sentence is now one constant,
  `ROLLED_BACK`, and the handler replaces it with `NOT_ROLLED_BACK` (THE
  ROLLBACK WAS NOT COMPLETE) when the rollback did not earn it, in every
  `Refusal`; a `CommandFailed` gets the same sentence on a line before
  `main()` prints it.
- `_put_back` caught `Exception` only, so a second Ctrl-C during the leg's
  checkout skipped the index step and left `MM` on the pin, the workflow and
  the gitlink. It now catches `KeyboardInterrupt` too, names the step as
  interrupted, and the steps after it still run. No signal handler: this is
  a library function.
- The leg was restored only if the forward checkout returned, but git moves
  HEAD before `post-checkout` runs, so a failing hook (git-lfs's without
  `git-lfs` on PATH) or an interrupt while the checkout was being waited on
  left the leg on the new commit. `_restore_leg_if_moved` reads the leg's
  branch and HEAD and restores only when either differs, so an unmoved leg
  is not checked out again through the hook that refused.
- An interrupt or a defect was re-raised with nothing saying whether the
  tree was back. One stderr line now says so before the traceback.

tests/test_bump_leg_rollback.py: a refused commit (a signer that does not
exist, and a `commit-msg` hook), a failure after the commit landed (an
interrupt, and a refusal), a forward checkout that moved the leg (an
interrupt, and a `post-checkout` hook that refuses the new commit only), an
interrupt inside a rollback step, a `git add` that fails beside a failing
rollback step, and the line before an interrupt's or a defect's traceback.
Twelve cases fail on a8d31ee's script, each for its finding's reason.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap brettheap changed the title bump-leg.py: a bump that fails for any reason once the leg has moved rolls the pin, the workflow files, the leg's checkout and the index back, so a workflow file it cannot write, a validator line the stdout cannot encode or an interrupt no longer leaves the pin rewritten and the leg on the new commit behind a traceback (#253) bump-leg.py: a bump that fails for any reason once the leg has moved, a commit git refuses included, rolls the pin, the workflow files, the leg's checkout and the index back and says whether all of it went back, so a workflow file it cannot write, a validator line the stdout cannot encode, a signer that cannot sign or an interrupt no longer leaves the pin rewritten and the leg on the new commit (#253) Oct 12, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-12T00:31:43Z)

This lane re-verified its own PR #264 at 67a1163 after fix round 1, read-only in its own detached worktrees.

CHANGES NEEDED

Items, re-run with the first pass's real-git reproductions:

  1. Verified. A refused commit (signer, commit-msg hook) is rolled back; one that landed, then was interrupted or left HEAD unreadable, is not, and says so.
  2. Verified. A red validator with a failed undo says THE ROLLBACK WAS NOT COMPLETE, never "exactly as it was".
  3. Verified. A second SIGINT in the leg step is named, and the index step still runs.
  4. Verified for a checkout that moved the leg, on a branch or detached.
  5. Verified. The interrupt line is printed.

New P2. A terminal Ctrl-C cutting off the forward checkout before git moves HEAD is reported as "the tree is exactly as it was", yet the leg holds 851 files the checkout wrote, and the re-run fails with untracked working tree files would be overwritten. _restore_leg_if_moved (scripts/bump-leg.py:684) checks only the branch and HEAD. Main leaves the same files but claims nothing.

Nits. Three mutants survive (unreadable HEAD, leg compared by HEAD only, OSError after a landed commit). tests-windows was still running at 00:29Z.

Tests:

  • head: 228 passed;
  • b30595c+head: 163 passed, 1 skipped;
  • with 5757e29 as well: 104 passed;
  • the 17 new cases fail on main;
  • mutants: 14 of 17 killed.

Merges: rc 0 against origin/main b30595c, 5757e29 and #281's 754803d.

The full report is handoffs/xFactory/attachments/lane-openRepoShape-2/review264-a8d31ee/reverify-1.md in opensoft/brett-wip.

brettheap and others added 2 commits October 12, 2026 00:41
…e read before the checkout, so a checkout cut off before it moved the leg is reported as THE ROLLBACK WAS NOT COMPLETE, not as a tree exactly as it was (#253)

Re-verification 1 of #264 found it with a real Ctrl-C. git writes the new
commit's files before it moves the leg's index and HEAD, and a terminal's
SIGINT reaches git and this tool together, so a large forward checkout cut
off between the two leaves the leg on its branch at its commit with the
files it wrote in it. `_restore_leg_if_moved` read only the branch and the
HEAD, found the leg where it stood, and `_roll_back` returned True: the
report said every byte was back and the tree was exactly as it was, while
the leg held 851 files of the cut-off checkout, and the operator's re-run
failed on "untracked working tree files would be overwritten". Main prints
only the traceback there, so the false claim was new in fix round 1.

`_apply_rewrite` now reads the leg's `git status --porcelain -z
--untracked-files=all` as bytes, next to `placement` and before the
checkout. `_restore_leg_if_moved` restores the leg when its branch or HEAD
moved, as before, and then compares the leg's status with that one. When it
differs the step raises `LegNotAsFound`, `_put_back` names the step with a
sentence sending the operator to `git -C <leg> status`, and the verdict is
THE ROLLBACK WAS NOT COMPLETE. The comparison runs after a restore as well,
so a checkout that moved the leg and left a file the checkout back cannot
remove is reported the same way.

Nothing in the leg is removed. An untracked file of the operator's own is
allowed there (`_ensure_root_clean` ignores untracked leg content), and
removing only the paths that appeared and that the new commit's tree holds
is not provably safe: git checkout overwrites an IGNORED file without
asking, and a new `.gitignore` written before the cut can make that file
show as untracked, so such a path could be a file the operator had.

Tests: the cut-off checkout, interrupted and failing with exit 130, and a
checkout that moved the leg, left a file and was interrupted. Each asserts
the line says THE ROLLBACK WAS NOT COMPLETE, nothing says the tree is
exactly as it was, the step is named, and the operator's own untracked file
and the checkout's file are both still there. One more puts an operator's
untracked file in the leg beside a red validator, and shows the rollback
still says every byte is back and leaves the file alone.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r the commit, an `OSError` after it, and a leg detached at the commit it stood on, so the three rules re-verification 1 could break without a test failing are now held by one each (#253)

Re-verification 1 of #264 ran 17 mutants of 67a1163 and three survived:

- 9: `_head_guard` rolls back when HEAD cannot be read after the commit
  was asked for. `test_a_head_that_cannot_be_read_after_the_commit_rolls_nothing_back`
  makes every `git rev-parse HEAD` fail once the commit has been asked for
  (`commit_once`'s read-back and the guard's), as the reviewer's real
  `post-commit` reproduction did, and asserts the commit landed, the tree
  agrees with it, and the refusal says the commit could not be read.
- 12: the leg compared by its HEAD alone.
  `test_a_leg_detached_at_the_commit_it_stood_on_is_put_back_on_its_branch`
  bumps `--to` the commit already pinned with a workflow reference left
  behind, so the forward checkout detaches the leg at the same commit; a
  red validator follows, and the leg must be back ON `main`.
- 16: an `OSError` after a landed commit made into
  `bump-leg-rewrite-failed`, whose remediation says nothing was committed.
  `test_a_failure_after_the_commit_landed_rolls_nothing_back` gains an
  `oserror` case: the error is re-raised as itself after a line saying the
  bump was committed.

The landed-commit assertions are one helper, shared by the guard's tests.
No code changes.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-12T01:12:44Z)

This lane re-verified its own PR #264 at 5269aac after fix round 2, read-only in its own detached worktrees.

MERGEABLE WITH NITS, once tests-windows passes.

  • P2, cut-off checkout: fixed. A real group SIGINT, with or without python's handler, says THE ROLLBACK WAS NOT COMPLETE and names git -C spec status; following that, the bump succeeds.
  • No false NOT COMPLETE. The operator's untracked or ignored files, a sparse leg, a nested submodule and a branchless leg all roll back as complete.
  • Status read. It cannot fail an odd leg: the one failing case, a broken nested gitdir, is refused earlier by the existing clean check, on main too.
  • Mutants 9, 12 and 16 are killed.
  • Nits. --untracked-files=normal survives (no test). An ignored file at a path the target commit tracks is lost on rollback, as on main.

Tests:

  • head: 235 passed;
  • with main b421e6b: 221 passed, 1 skipped;
  • 22 of 24 new cases fail on main;
  • mutants: 22 of 23 killed.

CI: tests, macOS and SonarCloud pass; tests-windows was pending at 01:11Z. Round 1's Windows run passed.

Merge with b421e6b: rc 0.

The full report is handoffs/xFactory/attachments/lane-openRepoShape-2/review264-a8d31ee/reverify-2.md in opensoft/brett-wip.

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit 86da808 into main Oct 12, 2026
5 checks passed
@brettheap
brettheap deleted the fix/bump-leg-rollback-every-failure branch October 12, 2026 12:09
brettheap added a commit that referenced this pull request Oct 12, 2026
…le-files

The one conflicted file is scripts/bump-leg.py, in three hunks in
`plan_workflows`, all against #222's tracked-files change (#207): the helper
block keeps main's `_tracked_workflows` and this branch's `_not_utf8`,
`_leg_pin_text` and `_workflow_texts`; the docstring keeps both paragraphs;
the loop head reads `tracked`, `untracked`, then
`for path, text in _workflow_texts(root, workflows)`, main's own
`text = path.read_text(...)` line going with the walk it belonged to. The
net diff against main is scripts/bump-leg.py and
tests/test_bump_leg_unreadable.py, nothing else.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 12, 2026
…, #253 via #264, #302 via #318, #288 via #309, #279 via #310, #306 via #311, #334 via #336, #239 via #269, #243 via #268, #321 via #335) into fix/adopt-record-submodule-placement

Brings in the twelve commits from c7c02fa to a1b3a4b on top of 71b58a1,
merged by sha. No rebase. Among the files this branch changes:
346079d (#216) and e43fa95 (#243) change adopt-project.py; 34ecbc2 (#260)
changes scripts/shape_materialize.py; 87373a6 (#306) changes
shape-doctor.py and tests/test_shape_doctor.py; 7491530 (#288) changes
AGENTS.md and tests/test_repo_hygiene.py.

Two conflicts, each resolved keeping both sides:

- adopt-project.py, the module docstring's `execute` paragraph. #243 ends
  "the root's only submodules are the two leg mounts, each at the commit
  pushed to its leg."; this branch follows "two leg mounts." with "The
  split's `project.yaml` records the leg each source submodule kept in a
  leg was answered into, under `submodules:`, for `shape-doctor.py` to
  check it against." The merge has #243's sentence, then this branch's,
  re-wrapped to the paragraph's width. The module docstring is not in
  `--help` (docs/cli.md holds neither sentence).
- tests/test_repo_hygiene.py, `test_agents_md_is_short_enough_to_be_read`.
  Each side moved the AGENTS.md cap from 464 and wrote a paragraph
  saying why: #288 to 477, for thirteen lines, and this branch to 472,
  for eight. AGENTS.md itself merged cleanly and holds both: 485 lines,
  main's 477 plus this branch's +9 -1. The merge keeps #288's paragraph
  word for word, then this branch's, renumbered "2026-10-12: 477 -> 485"
  because it now follows #288's, and asserts `<= 485`.

`git merge-tree --write-tree` of the two parents gives the same tree in
every file but these two.

Checked:

- adopt-project.py: `_commit_the_split` was changed by both sides, in
  hunks that do not touch. #243 clones the assembly with `--template=`;
  this branch builds `append` with the `submodules:` record. The merged
  function holds both, and each side's text is unchanged. Every other
  definition one side changed is that side's text.
- shape-doctor.py: #306 changes `validator_row` and `run_validator`.
  This branch's placement functions, F1's
  `gitmodules_the_record_answers` among them, are this branch's text.
- No top-level name is bound twice in adopt-project.py,
  scaffold-project.py, scripts/repo_shape.py,
  scripts/shape_materialize.py, shape-doctor.py, validate-manifest.py,
  update-shape.py, scripts/family.py or scripts/bump-leg.py. Each merged
  module's names are the base's plus what either side added.
- AGENTS.md is main's plus exactly this branch's changed lines.
- The branch's diff against main is #189 alone: 12 files, +1694 -157. That
  is +1670 at 27d039b, plus F1's fix and test (+23), plus the one line
  the docstring re-wrap adds.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 12, 2026
… of main's commits merge with no conflict, among them #243's template-less clone of the assembly and #216's literal `git grep` in adopt-project.py, and #260's one-pass `render` in shape_materialize.py

origin/main moved 71b58a1 -> 8cdca5e. It was merged by sha, as the
coordinator asked for e43fa95 or later:
- c7c02fa (#299 via #315), scripts/family.py;
- 346079d (#216 via #228), adopt-project.py: `plan`'s follow-up
  `git grep`;
- 34ecbc2 (#260 via #316), scripts/shape_materialize.py: `render`
  substitutes in one pass;
- 86da808 (#253 via #264), scripts/bump-leg.py;
- 6f4ce47 (#302 via #318), scripts/family.py;
- 7491530 (#288 via #309), AGENTS.md, README, docs and
  scaffold-project.py's --help;
- 7591455 (#279 via #310), tests/conftest.py: `git()` takes its default
  identity as `-c` config;
- 87373a6 (#306 via #311), shape-doctor.py;
- 8a14ac9 (#334 via #336), tests/test_update_shape_landing.py;
- c1b6732 (#239 via #269), update-shape.py;
- e43fa95 (#243 via #268), adopt-project.py: the assembly root is cloned
  with `--template=`, and the verification refuses a mount whose gitlink
  is not the commit pushed to its leg (`adopt-mount-commit`);
- a1b3a4b (#321 via #335), tests/test_git_output_decoding.py;
- 8cdca5e (#295 via #312), the template's scripts/validate-pins.py.

`git merge-tree` and this merge give the same tree, 07e6b49.
adopt-project.py, scripts/shape_materialize.py and
tests/test_adopt_ignored_shape_files.py auto-merge. In the last of these
main's 346079d and this branch's 86a5e30 change different cases.

Checked on the merged tree:
- AST (fin183g/names.py, base 71b58a1): no top-level name is bound twice
  in adopt-project.py, scaffold-project.py, scripts/repo_shape.py or
  scripts/shape_materialize.py, and none is loaded but bound nowhere.
  Main changed the signature of no function this branch calls.
- Every sentence 86a5e30 changed is still there: in `_through`, in
  `_link_parts`, in `_verify`'s `_resolve` (which e43fa95's
  `adopt-mount-commit` leaves as it was), and in the three test files.
- `_commit_the_split` now clones the assembly with `--template=`. The
  next commit, F3, reads what that clone holds in its own `.git`, which
  is why F3 comes after this merge.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 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