Repository navigation
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
Conversation
…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) `plan_workflows` walked `.github/workflows/` on disk and the commit then named every rewritten file as an explicit pathspec, so an untracked draft that referenced the leg was staged into "Bump spec leg" as a new file, while the root-clean check had skipped it on the ground that an untracked file cannot reach that commit. The dry run listed the file without saying it was untracked. An ignored draft was no better, only later: the dry run exited 0 listing it, then `git add` refused it after the rewrite ("The following paths are ignored by one of your .gitignore files") and the bump was rolled back with no word about the cause. - **Tracked files only.** A row is made for a workflow file only when `git ls-files -z -- .github/workflows` holds it. The directory is still walked on disk, because that is the set `validate-pins.py` reads, and `.yml` and `.yaml` are both read. - **A file git does not track that has a reference to move is a refusal, not a skip.** Skipped, the validator reads its old `@<sha>` off the disk and the bump goes red after the rewrite (rolled back), with a finding that never says the file is not in the repository, after the plan had said `no @<sha> reference names this leg`. The refusal names every such file with its reference count and gives both exits: commit it first (it is then tracked and moves with the bump) or move it out of the directory until the bump has landed. - **Ignored counts as untracked.** The validator reads an ignored file and the commit's `git add` refuses it, so on main the bump could only fail late; it is now refused before anything is written, under the same code. - **A file with nothing to move is left alone, unnamed.** No reference to the leg, or the leg already at `--to`: not rewritten, not staged, not refused. - **Unchanged:** a bump with no such file is byte-identical to main, the tracked-workflow rewrite included (same output, same commit sha, same files). `--help` is unchanged, so `docs/cli.md` is too. The README's paragraph on what the command rewrites and refuses now says "tracked" and names the refusal (README.md stays at its 1416-line cap, so the refusal's two exits are left to its own message), and `docs/handbook.html` says the same, its footer's `README.md blob` moved with the README. - **Left out:** `validate-pins.py` still reads every file on disk, untracked included. It is a digest-pinned copy in every project, so it is not this change's to move; the refusal is what keeps the two readers agreeing. Closes #207 Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-11T18:57Z) @codex review |
|
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 5 hours by commenting |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Reviewer's GuideThe PR makes Sequence diagram for refusing an untracked workflow during bump-legsequenceDiagram
participant User
participant Bump as bump-leg.py
participant Git
participant Validator as validate-pins.py
User->>Bump: plan_workflows(root, repository, commit)
Bump->>Git: git ls-files -z -- .github/workflows
Git-->>Bump: tracked workflow paths
Bump->>Bump: Read workflow files on disk
Bump->>Bump: Find references naming leg at another commit
alt workflow is tracked
Bump->>Bump: Plan rewrite
Bump->>Validator: Validate rewritten workflows
else workflow is untracked or ignored
Bump-->>User: Refusal bump-leg-workflow-untracked
Bump-->>User: Exit 2, no files written
end
Flow diagram for tracked workflow validation during bump-legflowchart TD
A["bump-leg.py plan_workflows"] --> B["Walk .github/workflows on disk"]
B --> C["Read .yml and .yaml files"]
C --> D["Find references naming the target leg"]
D --> E{"Reference needs moving?"}
E -->|No| F["Leave file alone"]
E -->|Yes| G{"Git tracks file?"}
G -->|Yes| H["Plan rewrite and staging"]
G -->|No| I["Refuse: bump-leg-workflow-untracked"]
I --> J["Exit 2 before writes, including --dry-run"]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
…ked or ignored workflow file the bump would rewrite, and a test pins the NUL-separated read, so a tracked workflow with a non-ASCII name is never refused as untracked (#207) Fix round 1 on #222, from the independent review. - **The docs say what the code refuses.** README.md and `docs/handbook.html` said the tool refuses an untracked or ignored workflow file "that names the leg". It refuses only one it would REWRITE: one naming the leg at a commit other than `--to`. One already at `--to`, or naming no leg of the project, is left alone and unnamed (`test_an_untracked_workflow_with_nothing_to_move_is_left_alone`). Both now say "it would rewrite", which is shorter, so README.md stays at 1416 lines; the handbook footer's prose follows, and its `README.md blob` is re-taken from `git hash-object README.md`. - **`-z` has a test.** `_tracked_workflows` reads `git ls-files -z` because git quotes a name with a byte outside ASCII in the plain listing (`core.quotePath`). `test_a_tracked_workflow_with_a_non_ascii_name_is_not_taken_for_untracked` commits `.github/workflows/légs.yml` naming the leg and asserts the bump exits 0, moves it and commits it; it also asserts that git does quote the name in the plain listing, so it cannot pass for want of a quoted name. Measured: with `-z` dropped and the split on `\n`, that test fails (`REFUSED bump-leg-workflow-untracked ... légs.yml 1 reference(s)`) and the other 32 in the file pass; on the head it passes. - **Unchanged:** `scripts/bump-leg.py` is not touched. - **Left out:** a tracked file renamed by case alone outside git, on a case-insensitive filesystem, is read as untracked (the tracked set is compared by exact spelling) and is refused with an exit, "commit it first", that cannot clear it. Asking git which files are untracked (`git ls-files -o -z`, which applies `core.ignorecase`) would fix it; named under Left out in the PR. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-11T19:24Z) fix round 1 (independent review, codex is out of quota): README and handbook now say the refusal is for an untracked or ignored workflow file it would rewrite (blob re-taken, README still 1416), a new test pins the |
|
(lane openRepoShape-2, 2026-10-11T19:30:24Z) This lane reviewed PR #222 at 52f4bff read-only in its own detached worktrees; nothing authored, no claim. MERGEABLE WITH NITS. No P1, P2 or P3. Sequencing: whichever of #222 and #220 lands second keeps both footer history clauses in Reproduced offline (Atlas in local bare remotes, an untracked Findings (at 52f4bff):
Tests:
Merges (
Since this review the branch moved to 995fcb1, one commit: README and handbook now say the refusal is for an untracked or ignored workflow file the bump would rewrite, plus a test, which answers the first nit. This lane did not re-run it. Report and drivers: |
…workflows-only The handbook footer conflicted with #220 (#211), which landed first: the footer keeps both history clauses (#211, then #207) and its `README.md blob` is re-taken from the merged README (`git hash-object README.md`). README.md is 1416 lines, AGENTS.md 464. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-11T23:18Z) fix round 2: merged main with #220; footer keeps both history clauses and pins the merged README blob. #220 (#211) landed as eb6df75 and conflicted with this PR only on the |
|
…, #272 via #274, #205 via #232) into fix/adopt-record-submodule-placement Brings in 23e16ae (#251), 60588d5 (#206), 018ce0d (#207), 67a398a (#272) and d94d155 (#205) on top of 5751460. No rebase. The first four change none of this branch's files. d94d155 changes shape-doctor.py and tests/test_shape_doctor.py, as this branch does. One conflict, in shape-doctor.py's module docstring, under EXIT CODES. Both sides added a clause to exit 1's list of findings: #205 "a manifest is there and does not load", and #189 "or, for a submodule, a leg other than the one `project.yaml` records for it". Both are kept: #205's in its place in the list, and #189's after the path-policy clause it qualifies. Only the docstring's first line reaches `--help`, so docs/cli.md is unchanged. Merged without a conflict, and checked: - `check_placement`: #205 adds its `waiting_row` gate after the family-holder return and before `ctx.legs()`. The merged function is this branch's plus exactly those eight lines. So a `project.yaml` that does not load makes the placement row `n/a`, waiting on the `manifest` row, before the record is read. The record is read from `ctx.manifest`, which #205 now loads once, with the same `load_yaml`, so a manifest that loads gives the record it always gave. - #205 removes `Context._declares`, and nothing on this branch names it. This branch leaves `placement_row` unchanged, and #205's gate calls it as (NA, reason, None, detail). - 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 or update-shape.py. Each merged module's names are the base's plus what either side added. Every definition only one side changed is that side's text. The two that both changed are the docstring above and `check_placement`. - These commits leave adopt-project.py, scripts/repo_shape.py and scripts/shape_materialize.py untouched. So the record's writer and reader are the ones the round trip and the revert table ran on at a01ab02, and the record is still spelled by `double_quoted`. - AGENTS.md is untouched: 472 lines, under the cap of 472. - The branch's diff against main is still #189 alone: 12 files, +1670 -157. The one deletion more than against 5751460 is the EXIT CODES sentence above, which now rewraps two of #205's lines. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, #205, #215, #241 and #249/#250 merge with no conflict, and `_entry_findings` still hands `_leg_findings` the tree and ends with the root-symlink findings origin/main moved 60588d5 -> b30595c while the targeted runs of 8f624aa went: 018ce0d (#207 via #222), 67a398a (#272 via #274), d94d155 (#205 via #232), c2ff2b0 (#215 via #240), c3a9645 (#241 via #256) and b30595c (#249 and #250 via #284). It is merged again because c2ff2b0 changes adopt-project.py and b30595c changes scripts/shape_materialize.py's `render`. Conflicts: none. `git merge-tree --write-tree` gave tree be093a5, and the merge stages the same tree. Checked on that tree: - `_entry_findings` still calls `_leg_findings(plan, tree)` and ends with `_root_symlink_findings(plan, source, tree)`. - No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py or scripts/shape_materialize.py, none is loaded but bound nowhere (AST), and main changed the signature of no function this branch calls. - Both sides are intact by +/- line sets: each side's change against the merge base 60588d5 reappears, line for line, against the other side. - c2ff2b0 adds helpers to tests/conftest.py (`planned`, `GITLINK`, `findings` and others) and no fixture. tests/test_adopt_root_symlinks.py imports its conftest names one by one and none of these, so its own `planned` and `GITLINK` are unaffected. - b30595c's `render` spells a double-quoted YAML value with `double_quoted`; this branch's walker reads `assembly_root_paths()` and never renders. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…usals name a value to change instead of a command, so it no longer repeats AGENTS.md's old "each one names what to run instead" as if it held for `scaffold-leg-path-collides` and `scaffold-pin-collides` (#288) The page's rules for assistants quote AGENTS.md ("Read a refusal rather than retrying: each one names what to run instead"). The previous commit on this branch qualified that sentence in AGENTS.md, because two refusals name a VALUE to change and no command, so the page's copy had become the absolute sentence with an undocumented exception that #288 exists to remove. It now says the same in the page's voice: each one names what to run instead, except two that name a value to change, a `--spec-path` or `--code-path` over a path the shape writes and a `--pin` the shape cannot write, both before anything is created. The codes are left to AGENTS.md, which the section already links as the full text. The footer's history names #288 beside #207's, as #222 did for its own page edit. `README.md blob` in the footer is unchanged: this commit moves no byte of README.md, and `git hash-object README.md` still equals it. `tests/test_repo_hygiene.py`, the only test that reads the page, passes over these bytes. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… 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) `_apply_rewrite` promised in its docstring to roll every byte back and re-raise, but its handler was `except (Refusal, CommandFailed)`, and `main()` catches the same two. A `PermissionError` on a workflow file it rewrites, 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. On main at 39ac59c the issue's own steps for a read-only workflow file and for an undecodable validator line each exited 1 with a traceback, `git status` showing the pin and the gitlink modified, and the leg's HEAD on the new commit. The lane's review of the first head found the rollback short of its own promise in four more places: - The commit ran after `_apply_rewrite` returned, outside the rollback. A commit git refuses (a `commit.gpgsign` signer that cannot sign, a `commit-msg` hook) left the bump staged and the leg on the new commit, and the same command run again was refused as `bump-leg-root-dirty`. - A `bump-leg-validators-red` refusal said every byte was rolled back when a rollback step had failed. - A second Ctrl-C during the rollback skipped the steps after it, the index included. - A forward checkout that moved the leg and then failed (a `post-checkout` hook) or was interrupted left the leg moved. Re-verification found a fifth. A Ctrl-C that cut the forward checkout off before git moved HEAD left the new commit's files in the leg, and the report still said the tree was exactly as it was. The handler is now `except BaseException`, and the rollback is helpers instead of a loop and a `try`: - `_apply_rewrite` keeps the commit inside the `try` as its last statement, with `cmd_bump` building the message first and handing it in. It reads the root's HEAD just before the commit, and the leg's `git status` (`_leg_status`: `git status --porcelain -z --untracked-files=all`, as bytes) before the checkout. - `_head_guard` compares the root's HEAD with the one read before the commit after any failure. - 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, since rolling back under a commit that did land is the worse mistake. - `_roll_back` puts back, in the order it always did, every file written, the leg's checkout and the index, and returns whether all of it went back. - `_put_back` attempts each step whatever the steps before it did, and 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` restores the leg when its branch or HEAD differs from where it stood, so a forward checkout that moved the leg and then failed or was interrupted is put back, and a leg that never moved is not checked out again through a hook that refused. Either way it then compares the leg's `git status` with the one read before the checkout and raises `LegNotAsFound` when they differ. 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. - A report says one of three things: every byte is back (`ROLLED_BACK`), `THE ROLLBACK WAS NOT COMPLETE` (`NOT_ROLLED_BACK`), or the commit landed. `_with_verdict` puts the sentence in a `Refusal`, replacing `bump-leg-validators-red`'s own when the rollback did not earn it. Where it goes depends on the failure: - a `Refusal` leaves as itself, with the verdict in its text; - an `OSError` or a `UnicodeError` that nothing committed leaves as `REFUSED bump-leg-rewrite-failed` naming the exception, exit 2, the way `update-shape.py` turns an unwritable README into `update-readme-unwritable`; - a `CommandFailed` leaves as `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. - The module docstring's refusal list gains one bullet. No `--help` text changes (`render-cli-reference.py --check` is ok), and `main()` is not touched. Tests: `tests/test_bump_leg_rollback.py` is new, 24 cases in 13 functions, all passing at this head. 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. A case where the leg itself cannot be put back compares what can still be compared. The cases are: - a read-only tracked workflow file, run as a subprocess (the issue's reproduction); - a write that dies half way (`ENOSPC` after 20 bytes); - a validator's output that cannot be decoded, and one the stdout cannot echo, the second raised from a stream that fails, not from `PYTHONIOENCODING`, because #214's `escape_what_stdout_cannot_encode` reconfigures that stream at the top of `main()`; - an interrupt while writing and one while validating, and a defect in the tool; - the leg's own restore failing beside a red validator, an unwritable file and a failing `git add`, and a second interrupt inside a rollback step; - a forward checkout that moved the leg and then failed (an interrupt, a `post-checkout` hook), and one that left files in the leg (cut off by an interrupt, cut off with exit 130, and moved then interrupted); - an untracked file of the operator's own in the leg beside a red validator, and `--to` the commit already pinned, which detaches the leg where it stood; - a commit git refuses (a `gpg.program` that does not exist, a `commit-msg` hook); - a failure after the commit landed (an interrupt, a `Refusal` from reading HEAD back, an `OSError`), and a HEAD that cannot be read once the commit was asked for. Run against main's `scripts/bump-leg.py` (c7c02fa), 22 of the 24 fail for the issue's reasons. The read-only run exits 1 with the `PermissionError` traceback. The `OSError`, `UnicodeError` and rollback-step cases escape `main()`. The interrupts and the defect stop at `pin is not as it was found`, and both refused commits leave the pin changed. The two that pass are guards: an untracked file of the operator's own in the leg beside a red validator, and a leg detached at the commit it stood on, which main's own red-validator rollback already gets right. Twenty mutants of the code at 5269aac were all killed. Left out: - The README and `docs/handbook.html`, which say a refusal leaves the tree as it was found. That is now true for a refused commit as well. Where it is not (a rollback step itself fails, or a cut-off checkout left files in the leg) the refusal says THE ROLLBACK WAS NOT COMPLETE. #222 edited the same README paragraph. - Removing the files a cut-off checkout wrote into the leg. It is not provably safe: `git checkout` overwrites an IGNORED file without asking, and a new `.gitignore` written before the cut can make such a file show as untracked. Until the operator removes them, git refuses the re-run's checkout (`untracked working tree files would be overwritten`). - A file git ignores. The leg's `git status` does not list it, so a cut-off checkout that wrote only ignored paths leaves files no report names. `--ignored` would read every build output in the leg twice per bump. Ignored files do not block the re-run. - A leg `post-checkout` hook that refuses every checkout, the restoring one included. That step is reported as not put back even when git did move the leg back. This is conservative, not wrong: a failed hook can leave work undone that neither HEAD nor `git status` shows (git-lfs's sets the read-only bit on lockable files). - An interrupt outside a rollback step, such as between two steps, because no signal handler is installed from a library function. An uncaught `KeyboardInterrupt` is not turned into a refusal. - Two nits of re-verification 2: no test holds `--untracked-files=all` against `normal`, and an ignored file at a path the target commit tracks is lost on rollback, as on main. - #289, a committed workflow file or pin that is not UTF-8, which ends in a traceback before anything moves; and #285, which covers `update-shape.py`, whose `cmd_apply` also rolls back only on `Refusal` and `CommandFailed`. Reviewed adversarially by this lane at a8d31ee (CHANGES NEEDED: a refused commit, three smaller rollback gaps and a nit), re-verified at 67a1163 after fix round 1 (CHANGES NEEDED: a cut-off checkout reported as put back) and at 5269aac after fix round 2 (MERGEABLE WITH NITS). Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
…ten and committed under git's spelling, so a `legs.yml` renamed `Legs.yml` by case alone outside git on macOS or Windows is no longer refused as `bump-leg-workflow-untracked` while `git status` is clean, with an exit that could not clear it (#332) (#344) `plan_workflows` walks `.github/workflows/` on disk and made a row for a file only when the walk's spelling was in git's tracked set, compared exactly. On a disk that ignores case (APFS and NTFS by default) a tracked `legs.yml` renamed outside git to `Legs.yml` is one file under both names. Git calls the root clean, so `_ensure_root_clean` passed, and the walk's `Legs.yml` was then refused as untracked, with "commit it first" as the exit when there was nothing to commit. - **A name git does not hold is the tracked file when the filesystem says it is.** `_tracked_spelling`: a name in git's tracked set is its own spelling, as before. Any other name is the tracked file only when exactly one tracked name in the same directory is missing from the listing and `os.path.samefile` resolves it to this very file. `_same_file` is the one place the tool asks, and the seam the tests replace. A tracked name the walk lists is a file of its own, a hard link included. A name that two tracked spellings both resolve to (an index holding `legs.yml` and `LEGS.yml`) is still refused as untracked, because one rewrite cannot keep two index entries in step. - **The row carries git's spelling.** The dry run lists `.github/workflows/legs.yml`, the commit's `git add` and pathspec name it, and the rewrite reaches the one file through it. Git matches a commit's pathspec against the index by exact spelling whatever the disk does, so the disk's spelling could never have been committed. Measured with `core.ignorecase=true`: `git commit -- Legs.yml` refuses with "pathspec 'Legs.yml' did not match any file(s) known to git". The disk keeps its own spelling, and nothing is renamed. - **The filesystem is asked, not `core.ignorecase`.** The issue suggested asking git which files are untracked (`git ls-files -o`). On a disk that keeps case, with `core.ignorecase=true`, git lists a separate `Legs.yml` beside the tracked `legs.yml` as neither untracked nor stageable (`git add Legs.yml` is a no-op). Tried as a mutation, that direction took the second file for tracked, and the bump ended `bump-leg-validators-red` after the rewrite instead of a refusal before it. With `samefile` it is refused by name before anything is written, whatever the setting says. - **Tests.** Four functions in `tests/test_bump_leg.py`, seven cases. `test_a_tracked_workflow_respelled_by_case_moves_under_gits_spelling` is the issue on its own disk. A tracked `legs.yml` is renamed `Legs.yml` outside git and `git status` is clean. The dry run lists `.github/workflows/legs.yml`, and the bump exits 0 and commits exactly `spec`, the pin and `.github/workflows/legs.yml`, with the rewrite in HEAD's blob. The project's validator is green and the status is still clean. It runs only where a probe of the test's temporary directory finds a disk that ignores case (the macOS and Windows jobs), and is skipped on one that keeps it. `test_a_respelled_tracked_workflow_is_planned_under_gits_spelling` is the rule on every disk, with `_same_file` answering as a disk that ignores case would. It fails on main with the issue's refusal. `test_a_name_two_tracked_spellings_answer_to_is_refused_as_untracked` runs everywhere. `test_a_second_spelling_that_is_a_second_name_is_refused_whatever_git_says` is parametrized over a separate file and a hard link, each with `core.ignorecase` true and false, and runs only where the disk keeps case. Four mutations of the fix are each caught by at least one of these tests: the row carrying the disk's spelling, any number of matches accepted, the listing check dropped, and asking `git ls-files -o`. - **Unchanged:** main's and this branch's `bump-leg.py` were each run over a copy of one project, on an ordinary bump (a tracked `legs.yml` naming the leg). Both print the same dry-run and real output and write the same commit message and the same tree. The refusal's id, text and exits are unchanged. `--help` is unchanged, so `docs/cli.md` is too, and README's "an untracked or ignored workflow file it would rewrite" stays true. - **Left out:** an index that already holds two spellings of one workflow, on a disk that ignores case and lists one of them exactly, still has that one rewritten and committed while the other keeps its old `@<sha>` in the tree. That defect predates #222. A name APFS stores in another Unicode normalization form than the index falls under the same `samefile` rule, but is not tested here. Closes #332 Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-3 (openRepoShape-3)
Claim: #207 (comment)
What
scripts/bump-leg.pyrewrites and commits only the workflow files git tracks; an untracked or ignored workflow it would rewrite (one naming the leg at a commit other than--to) is refused by name instead of being swept into the lockstep commit.plan_workflowsmakes a row for a workflow file only whengit ls-files -z -- .github/workflowsholds it. The directory is still walked on disk, because that is the setvalidate-pins.pyreads, and.ymland.yamlare both read.REFUSED bump-leg-workflow-untracked, exit 2, naming each file with its reference count, before anything is written, under--dry-runas well. Skipped,validate-pins.pywould still read its old@<sha>off the disk and turn the bump red after the rewrite. The refusal's own text gives both exits: commit the file first (it is then tracked and moves with the bump) or move it out of.github/workflows/until the bump has landed.--to: not rewritten, not staged, not refused.--helpis unchanged, sodocs/cli.mdis too.bump-leg.pyparagraph says "tracked" and names the refusal, and stays at its 1416-line cap: the paragraph was tightened in place to net zero lines, and the refusal's two exits are left to the refusal's own message rather than spelled in the README.docs/handbook.htmlsays the same in the page's own wording, and the footer's history keeps both clauses (shape-doctor.py reads onlyproject.yamlat a root that also carries afamily.yaml, so an assembly root whosefamily.yamldeclares two unmounted members is COMPLIANT, and itsmanifest kindsrow says that kind has a registered validator, which is never run #211's, which landed first, and this one's) with itsREADME.md blobre-taken from the merged README:23b0c6a55802788ce0113c00ea476714c4f38252(git hash-object README.md). Both pages say the tool refuses an untracked or ignored workflow file "it would rewrite", which is what the code does: one naming the leg at--tois left alone.Why
Measured on this machine with
repro2.sh(kept in the lane scratch): one Atlas fixture scaffolded into local bare remotes, the spec leg advanced by one pushed commit, then bumped once byscripts/bump-leg.pyfromorigin/maineb6df75and once from this branch's head7c41d98(re-measured after the merge of main; the fixture pins author and committer dates, so the commit shas below repeat). Every.out,.err, commit sha,git show --stat, workflow file,validate-pins.pyoutput andgit statusof the two runs was compared withcmp.bump/specbranch, a trackedlegs.ymlnaming both legs)eb6df757c41d98draft-not-mine-yet.ymlnaming the leg,--dry-runworkflows 2 reference(s) in 2 file(s)listing the draft with no word that it is untrackedREFUSED bump-leg-workflow-untracked: 1 workflow file(s) that git does not track (untracked or ignored) name testorg/Atlas-spec ...,.github/workflows/draft-not-mine-yet.yml 1 reference(s); tree unchangedcommitted c1db62c4ea49: spec, contracts/spec-pin.yaml, .github/workflows/draft-not-mine-yet.yml, .github/workflows/legs.yml; the draft is a new file in "Bump spec leg"HEADstill "Reference both legs from a workflow"); the draft is still untrackeddraft.yaml(the other suffix), real runcommitted 9bf000e2f2d1: ... .github/workflows/draft.yaml ...local-draft.yml(in.git/info/exclude),--dry-runREFUSED advancing a leg. THE EXACT COMMAND WAS: git add -- spec contracts/spec-pin.yaml .github/workflows/legs.yml .github/workflows/local-draft.yml...The following paths are ignored by one of your .gitignore files; rolled back, no word about the causelegs.ymlonly)committedthe spec gitlink,contracts/spec-pin.yaml,legs.ymlHEADsha,git show --stat, workflow files,validate-pins.py,git statusother.ymlnames only the code leg,already.ymlis already at--to)Where the table says "byte-identical" the run printed
IDENTICAL main/headfordry.out,dry.err,real.out,real.err,head.txt,show.txt,workflows.txt,validate.txtandstatus-after.txt. The untracked, untracked-.yamland ignored scenarios printDIFFERSfor the files that carry the change above (in the ignored one the rootHEAD,show.txt, workflow files,validate-pins.pyoutput andgit statusstay identical, because neither run commits).Tests
Targeted set on head
7c41d98cdfa1668f8576f3c7f5cabaea5133a16f(the merge oforigin/maineb6df75, which holds #220, into the branch;tests/test_git_output_decoding.pyandtests/test_stdout_encoding.pyare added to the usual five because main's #217 and #214 changedscripts/bump-leg.py, which this branch also edits):The one skip is
tests/test_stdout_encoding.py:286("the default this is about is Windows' own"), a test of #214's, not ours. Earlier heads of this branch ran the five-file set:995fcb1296 passed in 271.16s (0:04:31),52f4bff295 passed in 513.02s (0:08:33). A scratch merge of995fcb1, main60d08a1and #220's head1ddb86c, run before #220 landed, gave306 passed in 837.24s (0:13:57)on the five-file set.tests/test_bump_leg.pygains ten test cases, 23 to 33 collected (nine functions, one parametrized overdraft.ymlanddraft.yaml): the untracked draft refused by name and never committed, the refusal rewriting nothing, every untracked file named once with its count, moving the draft out letting the bump through, committing the draft first letting it move with the bump, an untracked file with nothing to move left alone, an ignored file refused untouched, an ignored file with nothing to move left alone, and (added in the fix round) a tracked workflow namedlégs.ymlthat is not refused as untracked. That last one pins the NUL-separated read in_tracked_workflows: with-zdropped and the split on\n, it fails (REFUSED bump-leg-workflow-untracked ... légs.yml 1 reference(s), exit 2) and the other 32 tests in the file still pass; on the head it passes. It also asserts that git quotes that name in the plain listing, so it cannot pass for want of a quoted name. None of the new tests carries a skip marker, so the Windows job skips none of them; I ran them on Linux only, so the macOS and Windows jobs are their first run there.test_readme_is_short_enough_to_be_readandtest_the_handbook_follows_the_readmes_outlineare in the set and pass (README 1416 lines; footer blob equalsgit hash-object README.md). The full suite was not run here; CI runs it on three OSes.Composes with
This branch holds
origin/maineb6df75(it carries #211 / PR #220, #217 / PR #230 and #214 / PR #246, all landed). Trial merges (git merge-tree --write-tree --name-only <sibling> HEAD) of every sibling branch that touches a file this branch touches, run at the tips below against head7c41d98:fix/bump-leg-rollback-every-failure(#253, PR #264; local and pushed are the same sha)a8d31eescripts/bump-leg.pyfix/validator-child-encoding(local, not pushed)5757e29scripts/bump-leg.pyorigin/mainnowc3f4987(one commit past the mergedeb6df75:scripts/repo_shape.pyand one new test)project.yamlat a root that also carries afamily.yaml, so an assembly root whosefamily.yamldeclares two unmounted members is COMPLIANT, and itsmanifest kindsrow says that kind has a registered validator, which is never run #211 / PR shape-doctor.py: a family.yaml beside an assembly root's project.yaml is amanifest kindsFINDING, INVALID (manifest-kinds), naming each member it pins with no gitlink, off its pin or with no 40-hex pin and each gitlink under members/ no row names, so a root its family gate refuses is no longer COMPLIANT, exit 0, with that file counted as validated (#211) #220 landed first, so this branch took the footer conflict: both branches rewrote the footer's prose sentence and itsREADME.md blobline. The two README edits do not overlap (this one is at line 973, shape-doctor.py reads onlyproject.yamlat a root that also carries afamily.yaml, so an assembly root whosefamily.yamldeclares two unmounted members is COMPLIANT, and itsmanifest kindsrow says that kind has a registered validator, which is never run #211's at line 1113) and each was net zero lines, so the merged README is 1416 lines (cap 1416) and AGENTS.md is 464 (cap 464). The footer keeps both history clauses (... added (#163); a family.yaml ... added (#211); bump-leg.py ... added (#207)) and itsREADME.md blobis23b0c6a55802788ce0113c00ea476714c4f38252, the hash of the merged README.test_readme_is_short_enough_to_be_readandtest_the_handbook_follows_the_readmes_outlinepass. The merge brought in the handbook body hunks of both, with no further conflict.run()decodes every git andghmessage with the locale codec, so a push rejection or any other failure that names a non-ASCII path raisesUnicodeDecodeErrorin place ofREFUSEDand the ruleset hint under a non-UTF-8 locale, after both leg remotes exist #217 / PR shape_materialize.run() and 26 other git and gh calls read what the child wrote as UTF-8 whatever the locale, so anadopt-project.py executewhose leg push is rejected with a message naming a non-ASCII path endsREFUSED pushing the spec legand the ruleset hint underLC_ALL=C PYTHONUTF8=0, not a UnicodeDecodeError traceback (#217) #230 and adopt-project.pyplanraisesUnicodeEncodeErrorin its terminal report, after the plan file is written, for a printable name stdout cannot encode (日本.mdunder cp1252, as on a redirected Windows stdout), soplanexits 1 beforeplan written to#214 / PR adopt-project.py and five other tools escape a name their stdout cannot encode, soplanon日本.mdunder a cp1252 stdout (strict, or Windows' default surrogateescape) no longer exits 1 after writing the plan (#214) #246 are in the base now. On the merged tree,tests/test_git_output_decoding.pyandtests/test_stdout_encoding.pypass withtests/test_bump_leg.py(the 356 above). This change adds nosubprocess,text=Trueorcapture_output=Trueline and reads the index throughgit_out(..., binary=True); its refusal prints through the sameescape_what_stdout_cannot_encode()callmain()makes.Left out
scripts/validate-pins.pystill reads every workflow file on disk, untracked and ignored included; it is a digest-pinned copy in every project, so moving it is a shape update and not this change's to make, and the refusal is what keeps the two readers agreeing. No issue filed.bump-leg.pyparagraph does not spell the refusal's two exits (commit the file first, or move it out of.github/workflows/until the bump has landed) because README.md sits at its 1416-line cap; they are in the refusal's own text. Raising the cap is a human decision.core.ignorecase=true), is refused as untracked:git statusis clean, so the root counts as clean, but the tracked set is compared by exact spelling, soLegs.ymlon disk is notlegs.ymlin the index, and "commit it first" cannot clear it because there is nothing to commit. Asking git which files are untracked (git ls-files -o -z -- .github/workflows, which appliescore.ignorecase) instead of taking a set difference would fix it. Reasoned from the code, not reproduced here (no case-insensitive filesystem in the lane); main bumps such a root fine.Closes #207
🤖 Generated with Claude Code
Summary by Sourcery
Prevent bump-leg from incorporating or failing on untracked and ignored workflow files while preserving atomic, tracked-only workflow updates.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Tests: